{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":129750,"databundleVersionId":15563344,"sourceType":"competition"}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install ultralytics\n!pip install ensemble-boxes","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-02-03T20:37:24.026601Z","iopub.execute_input":"2026-02-03T20:37:24.027265Z","iopub.status.idle":"2026-02-03T20:38:34.055394Z","shell.execute_reply.started":"2026-02-03T20:37:24.027241Z","shell.execute_reply":"2026-02-03T20:38:34.054645Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport shutil\nfrom pathlib import Path\nimport pandas as pd\nfrom sklearn.model_selection import train_test_split\n\nROOT = Path(\"/kaggle/input/dal-shemagh-detection-challenge\")  # dataset root\nIMAGES_DIR = ROOT / \"images\" / \"train\"\nLABELS_DIR = ROOT / \"labels\" / \"train\"\nCSV_PATH   = ROOT / \"train_labels.csv\"\n\n# Output dataset folder (will be created). If exists, it will be overwritten.\nOUT_ROOT = Path(\"yolo_split_80_20\")\n\n# YOLO class names (order must match class ids)\nNAMES = [\"head\", \"shemagh\"]  # id0, id1\n\nRANDOM_STATE = 42\nTEST_SIZE = 0.2\n\n# =========================\n# Helpers\n# =========================\ndef count_bins(n: int) -> int:\n    if n == 0: return 0\n    if n == 1: return 1\n    if n == 2: return 2\n    return 3  # 3+\n\ndef safe_read_label_counts(label_path: Path, head_id=0, shemagh_id=1):\n    n_head = 0\n    n_shemagh = 0\n    if label_path.exists():\n        txt = label_path.read_text(encoding=\"utf-8\").strip().splitlines()\n        for line in txt:\n            line = line.strip()\n            if not line:\n                continue\n            cls = int(line.split()[0])\n            if cls == head_id:\n                n_head += 1\n            elif cls == shemagh_id:\n                n_shemagh += 1\n    return n_head, n_shemagh\n\ndef ensure_clean_dir(p: Path):\n    if p.exists():\n        shutil.rmtree(p)\n    p.mkdir(parents=True, exist_ok=True)\n\n# =========================\n# Load CSV + build stratification key\n# =========================\ndf = pd.read_csv(CSV_PATH)\nif \"filename\" not in df.columns or \"right_place\" not in df.columns:\n    raise ValueError(\"CSV must contain columns: filename, right_place\")\n\ndf[\"filename\"] = df[\"filename\"].astype(str)\ndf[\"right_place\"] = df[\"right_place\"].astype(bool).astype(int)\n\nn_head_list = []\nn_shemagh_list = []\nmissing_images = []\n\nfor fn in df[\"filename\"]:\n    img_path = IMAGES_DIR / fn\n    if not img_path.exists():\n        missing_images.append(fn)\n\n    stem = Path(fn).stem\n    lab_path = LABELS_DIR / f\"{stem}.txt\"\n    n_head, n_shemagh = safe_read_label_counts(lab_path, head_id=0, shemagh_id=1)\n\n    n_head_list.append(n_head)\n    n_shemagh_list.append(n_shemagh)\n\nif missing_images:\n    raise FileNotFoundError(f\"Missing {len(missing_images)} images in {IMAGES_DIR}. Example: {missing_images[:5]}\")\n\ndf[\"n_head\"] = n_head_list\ndf[\"n_shemagh\"] = n_shemagh_list\ndf[\"head_bin\"] = df[\"n_head\"].map(count_bins)\ndf[\"shemagh_bin\"] = df[\"n_shemagh\"].map(count_bins)\n\n# Fine stratum: right_place + amount bins for head/shemagh\ndf[\"stratum_fine\"] = (\n    df[\"right_place\"].astype(str) + \"|\" +\n    df[\"head_bin\"].astype(str) + \"|\" +\n    df[\"shemagh_bin\"].astype(str)\n)\n\n# Coarsen rare strata so stratify doesn't break\nvc = df[\"stratum_fine\"].value_counts()\nrare = set(vc[vc < 2].index)\n\ndf[\"has_head\"] = (df[\"n_head\"] > 0).astype(int)\ndf[\"has_shemagh\"] = (df[\"n_shemagh\"] > 0).astype(int)\ndf[\"stratum_mid\"] = (\n    df[\"right_place\"].astype(str) + \"|\" +\n    df[\"has_head\"].astype(str) + \"|\" +\n    df[\"has_shemagh\"].astype(str)\n)\n\ndf[\"stratum\"] = df[\"stratum_fine\"]\ndf.loc[df[\"stratum\"].isin(rare), \"stratum\"] = df.loc[df[\"stratum\"].isin(rare), \"stratum_mid\"]\n\nvc2 = df[\"stratum\"].value_counts()\nrare2 = set(vc2[vc2 < 2].index)\ndf.loc[df[\"stratum\"].isin(rare2), \"stratum\"] = df.loc[df[\"stratum\"].isin(rare2), \"right_place\"].astype(str)\n\n# =========================\n# Split\n# =========================\ntrain_df, val_df = train_test_split(\n    df,\n    test_size=TEST_SIZE,\n    random_state=RANDOM_STATE,\n    stratify=df[\"stratum\"]\n)\n\nprint(\"Train:\", len(train_df), \"Val:\", len(val_df))\nprint(\"right_place mean - Train:\", train_df[\"right_place\"].mean(), \"Val:\", val_df[\"right_place\"].mean())\n\n# =========================\n# Create output folder structure\n# =========================\nensure_clean_dir(OUT_ROOT)\n\n(out_train_img := OUT_ROOT / \"images\" / \"train\").mkdir(parents=True, exist_ok=True)\n(out_val_img   := OUT_ROOT / \"images\" / \"val\").mkdir(parents=True, exist_ok=True)\n(out_train_lab := OUT_ROOT / \"labels\" / \"train\").mkdir(parents=True, exist_ok=True)\n(out_val_lab   := OUT_ROOT / \"labels\" / \"val\").mkdir(parents=True, exist_ok=True)\n\n# =========================\n# Copy files\n# =========================\ndef copy_split(split_df: pd.DataFrame, img_out: Path, lab_out: Path):\n    copied_img = 0\n    copied_lab = 0\n    missing_lab = 0\n\n    for fn in split_df[\"filename\"]:\n        src_img = IMAGES_DIR / fn\n        dst_img = img_out / fn\n        dst_img.parent.mkdir(parents=True, exist_ok=True)\n        shutil.copy2(src_img, dst_img)\n        copied_img += 1\n\n        stem = Path(fn).stem\n        src_lab = LABELS_DIR / f\"{stem}.txt\"\n        dst_lab = lab_out / f\"{stem}.txt\"\n        if src_lab.exists():\n            shutil.copy2(src_lab, dst_lab)\n            copied_lab += 1\n        else:\n            # YOLO can handle missing labels as \"no objects\", but it's better to have empty txt.\n            dst_lab.write_text(\"\", encoding=\"utf-8\")\n            missing_lab += 1\n\n    return copied_img, copied_lab, missing_lab\n\ntr_i, tr_l, tr_m = copy_split(train_df, out_train_img, out_train_lab)\nva_i, va_l, va_m = copy_split(val_df, out_val_img, out_val_lab)\n\nprint(f\"Train copied images={tr_i}, labels={tr_l}, missing_labels_created_empty={tr_m}\")\nprint(f\"Val   copied images={va_i}, labels={va_l}, missing_labels_created_empty={va_m}\")\n\n# =========================\n# Write dataset.yaml for YOLO\n# =========================\nyaml_path = OUT_ROOT / \"dataset.yaml\"\nyaml_text = (\n    f\"path: {OUT_ROOT.resolve()}\\n\"\n    f\"train: images/train\\n\"\n    f\"val: images/val\\n\"\n    f\"names:\\n\" + \"\\n\".join([f\"  {i}: {name}\" for i, name in enumerate(NAMES)]) + \"\\n\"\n)\nyaml_path.write_text(yaml_text, encoding=\"utf-8\")\nprint(\"Wrote:\", yaml_path)\n\n# =========================\n# Save split CSVs (keep classification label per image)\n# =========================\ntrain_df[[\"filename\", \"right_place\"]].to_csv(OUT_ROOT / \"train_labels_split.csv\", index=False)\nval_df[[\"filename\", \"right_place\"]].to_csv(OUT_ROOT / \"val_labels_split.csv\", index=False)\nprint(\"Wrote split CSVs:\", OUT_ROOT / \"train_labels_split.csv\", \"and\", OUT_ROOT / \"val_labels_split.csv\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-03T20:38:34.057119Z","iopub.execute_input":"2026-02-03T20:38:34.057445Z","iopub.status.idle":"2026-02-03T20:38:50.479691Z","shell.execute_reply.started":"2026-02-03T20:38:34.057411Z","shell.execute_reply":"2026-02-03T20:38:50.47894Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import random\nfrom pathlib import Path\nimport matplotlib.pyplot as plt\nfrom PIL import Image\n\nSPLIT_ROOT = Path(\"yolo_split_80_20\")  # must match OUT_ROOT above\nSPLIT = \"val\"  # \"train\" or \"val\"\nK = 9            # number of images to show\n\n# If your images are not all jpg, this still works because we read from CSV filenames.\nlabels_csv = SPLIT_ROOT / (\"train_labels_split.csv\" if SPLIT == \"train\" else \"val_labels_split.csv\")\ndf_split = pd.read_csv(labels_csv)\ndf_split[\"filename\"] = df_split[\"filename\"].astype(str)\ndf_split[\"right_place\"] = df_split[\"right_place\"].astype(int)\n\nimg_dir = SPLIT_ROOT / \"images\" / SPLIT\nlab_dir = SPLIT_ROOT / \"labels\" / SPLIT\n\n# Class names for drawing\nnames = [\"head\", \"shemagh\"]\n\ndef yolo_to_xyxy(x, y, w, h, W, H):\n    # YOLO normalized center format -> pixel corners\n    x_c = x * W\n    y_c = y * H\n    bw = w * W\n    bh = h * H\n    x1 = x_c - bw / 2\n    y1 = y_c - bh / 2\n    x2 = x_c + bw / 2\n    y2 = y_c + bh / 2\n    return x1, y1, x2, y2\n\nsamples = df_split.sample(n=min(K, len(df_split)), random_state=23).to_dict(\"records\")\n\nplt.figure(figsize=(14, 9))\nfor idx, row in enumerate(samples, start=1):\n    fn = row[\"filename\"]\n    rp = row[\"right_place\"]\n\n    img_path = img_dir / fn\n    lab_path = lab_dir / (Path(fn).stem + \".txt\")\n\n    img = Image.open(img_path).convert(\"RGB\")\n    W, H = img.size\n\n    ax = plt.subplot((len(samples) + 2) // 3, 3, idx)\n    ax.imshow(img)\n    ax.set_title(f\"{fn} | right_place={rp}\")\n    ax.axis(\"off\")\n\n    if lab_path.exists():\n        lines = lab_path.read_text(encoding=\"utf-8\").strip().splitlines()\n        for line in lines:\n            line = line.strip()\n            if not line:\n                continue\n            parts = line.split()\n            cls = int(parts[0])\n            x, y, w, h = map(float, parts[1:5])\n            x1, y1, x2, y2 = yolo_to_xyxy(x, y, w, h, W, H)\n\n            rect = plt.Rectangle((x1, y1), x2 - x1, y2 - y1, fill=False, linewidth=2)\n            ax.add_patch(rect)\n            label = names[cls] if 0 <= cls < len(names) else str(cls)\n            ax.text(x1, y1, label, fontsize=10, bbox=dict(alpha=0.6))\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-03T20:38:50.480485Z","iopub.execute_input":"2026-02-03T20:38:50.480741Z","iopub.status.idle":"2026-02-03T20:38:51.935567Z","shell.execute_reply.started":"2026-02-03T20:38:50.480716Z","shell.execute_reply":"2026-02-03T20:38:51.93258Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import yaml\nfrom pathlib import Path\n\n\n# Build YAML dict\ndata_yaml = {\n    \"train\": [\"/kaggle/working/yolo_split_80_20/images/val\" ,\"/kaggle/working/yolo_split_80_20/images/train\"],\n    \"val\":   \"/kaggle/working/yolo_split_80_20/images/val\" ,\n    \"test\": \"/kaggle/input/dal-shemagh-detection-challenge/images/test\",\n    \"nc\":   2,\n    \"names\": ['Face', 'Shemagh']\n}\n\n# Save to data.yaml\nwith open(\"data.yaml\", \"w\") as f:\n    yaml.safe_dump(data_yaml, f, default_flow_style=False)\n\nprint(\"Created data.yaml with the following content:\")\nprint(yaml.safe_dump(data_yaml, default_flow_style=False))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-03T20:38:51.937311Z","iopub.execute_input":"2026-02-03T20:38:51.937566Z","iopub.status.idle":"2026-02-03T20:38:51.96704Z","shell.execute_reply.started":"2026-02-03T20:38:51.937547Z","shell.execute_reply":"2026-02-03T20:38:51.966205Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom ultralytics import YOLO\nfrom pathlib import Path\nimport csv\nimport os\nimport random\nimport torch\n# Set random seeds for reproducibility\nnp.random.seed(42)\nrandom.seed(42)\ntorch.manual_seed(42)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-03T20:38:51.967867Z","iopub.execute_input":"2026-02-03T20:38:51.968087Z","iopub.status.idle":"2026-02-03T20:38:58.414366Z","shell.execute_reply.started":"2026-02-03T20:38:51.96807Z","shell.execute_reply":"2026-02-03T20:38:58.413692Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model = YOLO(\"yolo11x.pt\")\ndata_yaml = '/kaggle/working/data.yaml'\n\nmodel.train(\n    data=data_yaml,\n    epochs=50,                \n    batch=4,                   \n    imgsz=672,\n    patience=500,               \n    optimizer='SGD',        \n    lr0=0.001,\n    lrf = 0.0005,\n    weight_decay=0.0005,       \n    cos_lr=True,               \n    save_period=10,             \n    workers=4,\n    # Augmentations\n    close_mosaic=15,\n    hsv_h=0.5,\n    hsv_s=0.9,\n    hsv_v=0.7,\n    bgr=0.2,\n    flipud=0,\n    fliplr=0.5,\n    translate=0.01,\n    scale=0.5,\n    # shear=0.05,\n    mixup = 0.15,\n    cutmix = 0.01,\n    warmup_epochs= 3,\n    warmup_momentum= 1,\n    \n    exist_ok = True,\n    project = 'runs1/train',\n    plots = True, \n    augment = True,\n    conf = 0.01,\n    iou = 0.4,\n    dfl = 3,\n    box = 15,\n    # multi_scale = True,\n    freeze = 8\n    # agnostic_nms=True,\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-03T22:50:15.145866Z","iopub.execute_input":"2026-02-03T22:50:15.146196Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# model.val(iou = 0.45,conf = 0.005,augment= True, plots = False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-03T21:02:47.755559Z","iopub.execute_input":"2026-02-03T21:02:47.756326Z","iopub.status.idle":"2026-02-03T21:03:45.271991Z","shell.execute_reply.started":"2026-02-03T21:02:47.756295Z","shell.execute_reply":"2026-02-03T21:03:45.270596Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ## 8. Plot Validation Losses & Confusion Matrix\n\n# # %% Cell: plot_val_and_confusion_matrix\n# import pandas as pd\n# import matplotlib.pyplot as plt\n# from pathlib import Path\n# from PIL import Image\n\n# # 1. Point to your training experiment directory\n# exp_dir = Path(\"/kaggle/working/runs1/train/train\")  # adjust if your run folder is different\n# results_csv = exp_dir / \"results.csv\"\n\n# # 2. Load the epoch-by-epoch metrics\n# results = pd.read_csv(results_csv)\n\n# # 3. Plot validation losses over epochs\n# plt.figure(figsize=(10, 5))\n# plt.plot(results['epoch'], results['val/box_loss'], label='val box loss')\n# plt.plot(results['epoch'], results['val/cls_loss'], label='val cls loss')\n# plt.plot(results['epoch'], results['val/dfl_loss'], label='val dfl loss')\n# plt.xlabel('Epoch')\n# plt.ylabel('Loss')\n# # plt.title('YOLO Validation Loasses')\n# plt.legend()\n# plt.grid(True)\n# plt.show()\n\n# # 4. Locate and display the confusion matrix image\n# #    (typically saved when you run `model.val()` in Ultralytics)\n# cm_paths = ['/kaggle/working/runs1/train/train/results.png',\n#             '/kaggle/working/runs1/train/train/confusion_matrix.png']\n# print(cm_paths)\n# for p in cm_paths :\n#     cm_img = Image.open(p)\n#     plt.figure(figsize=(25, 10))\n#     plt.imshow(cm_img)\n#     plt.axis('off')\n#     # plt.title('Validation Confusion Matrix')\n#     plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-03T21:02:20.952233Z","iopub.execute_input":"2026-02-03T21:02:20.952997Z","iopub.status.idle":"2026-02-03T21:02:20.957315Z","shell.execute_reply.started":"2026-02-03T21:02:20.95297Z","shell.execute_reply":"2026-02-03T21:02:20.956469Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# model = YOLO(\"yolo12x.pt\")\n# # data_yaml = \"/kaggle/input/multi-instance-object-detection-challenge/Starter_Dataset/yolo_params.yaml\"\n# data_yaml = '/kaggle/working/data.yaml'\n\n# model.train(\n#     data=data_yaml,\n#     epochs=20,           \n#     batch=16,                   \n#     imgsz=512,\n#     patience=500,               \n#     optimizer='SGD', \n#     lr0=0.001,\n#     lrf = 0.001,  \n#     weight_decay=0.0001,       \n#     cos_lr=True,               \n#     save_period=10,             \n#     workers=8,\n#     # Augmentations\n#     close_mosaic=5,\n#     hsv_h=0.015,\n#     hsv_s=0.2,\n#     hsv_v=0.25,\n#     flipud=0.0,\n#     fliplr=0.5,\n#     translate=0.01,\n#     scale=0.75,\n#     shear=2,\n#     perspective = 0.001,\n#     mixup = 0.15,\n#     cutmix = 0.25,\n#     warmup_epochs= 5,\n#     warmup_momentum= 1,\n#     exist_ok = True,\n#     project = 'runs2/train',\n#     plots = True, \n#     augment = True,\n#     conf = 0.001,\n#     iou = 0.3,\n#     freeze = 6,\n#     # multi_scale = True\n    \n# )\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-03T21:02:20.958233Z","iopub.execute_input":"2026-02-03T21:02:20.958739Z","iopub.status.idle":"2026-02-03T21:02:20.972041Z","shell.execute_reply.started":"2026-02-03T21:02:20.958721Z","shell.execute_reply":"2026-02-03T21:02:20.97139Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model = YOLO(\"yolov8x.pt\")\n# data_yaml = \"/kaggle/input/multi-instance-object-detection-challenge/Starter_Dataset/yolo_params.yaml\"\ndata_yaml = '/kaggle/working/data.yaml'\n\nmodel.train(\n    data=data_yaml,\n    epochs=45,                \n    batch=32,                   \n    imgsz=640,\n    patience=500,               \n    optimizer='SGD',         \n    lr0=0.001,\n    lrf = 0.0007,\n    weight_decay=0.00025,       \n    cos_lr=True,               \n    save_period=10,             \n    workers=8,\n    # Augmentations\n    close_mosaic=10,\n    hsv_h=0.6,\n    hsv_s=0.9,\n    hsv_v=0.7,\n    bgr=0.1,\n    flipud=0,\n    fliplr=0.5,\n    translate=0.01,\n    scale=0.5,\n    # shear=5,\n    mixup = 0.15,\n    cutmix = 0.01,\n    warmup_epochs= 3,\n    warmup_momentum= 1,\n    \n    exist_ok = True,\n    project = 'runs3/train',\n    plots = False, \n    augment = True,\n    conf = 0.01,\n    iou = 0.4,\n    dfl = 3,\n    box = 15,\n    freeze = 6,\n    # multi_scale = True,\n    \n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-03T21:05:03.150844Z","iopub.execute_input":"2026-02-03T21:05:03.151461Z","iopub.status.idle":"2026-02-03T21:26:22.302977Z","shell.execute_reply.started":"2026-02-03T21:05:03.151433Z","shell.execute_reply":"2026-02-03T21:26:22.302119Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# model.val(iou=0.4,conf =0.05,augment = True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-03T21:31:05.630801Z","iopub.execute_input":"2026-02-03T21:31:05.631655Z","iopub.status.idle":"2026-02-03T21:31:21.820287Z","shell.execute_reply.started":"2026-02-03T21:31:05.631624Z","shell.execute_reply":"2026-02-03T21:31:21.819384Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nfrom pathlib import Path\nfrom PIL import Image\nimport pandas as pd\nimport csv\nfrom ensemble_boxes import weighted_boxes_fusion\n\ndef filter_invalid_boxes(boxes, scores, labels, conf_thr):\n    # keep only non-degenerate boxes; ignore confidence here by passing conf_thr < 0\n    filtered_boxes, filtered_scores, filtered_labels = [], [], []\n    for b, s, l in zip(boxes, scores, labels):\n        if abs(b[2] - b[0]) <= 1e-6 or abs(b[3] - b[1]) <= 1e-6:\n            continue\n        if s < conf_thr:  # will be disabled by using conf_thr=-1.0\n            continue\n        filtered_boxes.append(b)\n        filtered_scores.append(s)\n        filtered_labels.append(l)\n    return filtered_boxes, filtered_scores, filtered_labels\n\ndef run_inference(models,\n                  image_sizes,\n                  test_images_path,\n                  conf=0.15,          # user-threshold to apply AFTER WBF\n                  iou_thr=0.45,\n                  max_det=50,\n                  pre_conf=1e-3):     # very low pre-threshold so we don't drop boxes before WBF\n    \"\"\"\n    Runs YOLO inference at multiple scales, stores raw preds (no confidence filtering),\n    saves per-model/size CSVs, and returns a nested dict for WBF.\n    \"\"\"\n    image_paths = [p for p in Path(test_images_path).glob(\"*\")\n                   if p.suffix.lower() in {\".jpg\", \".jpeg\", \".png\"}]\n    predictions = {}\n\n    for model_idx, model in enumerate(models):\n        model.eval()\n        predictions[model_idx] = {}\n\n        for size in image_sizes:\n            predictions[model_idx][size] = {}\n            rows = []\n\n            for img_path in image_paths:\n                image_id = img_path.stem\n                img = Image.open(img_path).convert(\"RGB\")\n                w, h = img.size\n\n                # run prediction with a very low conf to keep candidates\n                results = model.predict(\n                    source=str(img_path),\n                    conf=pre_conf,\n                    iou=iou_thr,\n                    max_det=max_det,\n                    augment=True,\n                    imgsz=size,\n                    verbose=False\n                )\n                res = results[0]\n\n                raw_boxes  = res.boxes.xyxy.cpu().numpy().tolist()\n                raw_scores = res.boxes.conf.cpu().numpy().tolist()\n                raw_labels = res.boxes.cls.cpu().numpy().tolist()\n\n                # normalize and keep geometry-valid boxes only (no conf filtering here)\n                rel_boxes = [[x1/w, y1/h, x2/w, y2/h] for x1, y1, x2, y2 in raw_boxes]\n                boxes, scores, labels = filter_invalid_boxes(rel_boxes, raw_scores, raw_labels, conf_thr=-1.0)\n\n                predictions[model_idx][size][image_id] = {\n                    \"boxes\":  boxes,\n                    \"scores\": scores,\n                    \"labels\": labels\n                }\n\n                # per-model CSV (raw, unfiltered by confidence)\n                if boxes:\n                    pred_str = \" \".join(\n                        f\"{int(lbl)} {score:.6f} \"\n                        f\"{(b[0]+b[2])/2:.6f} {(b[1]+b[3])/2:.6f} \"\n                        f\"{(b[2]-b[0]):.6f} {(b[3]-b[1]):.6f}\"\n                        for b, score, lbl in zip(boxes, scores, labels)\n                    )\n                else:\n                    pred_str = \"no boxes\"\n\n                rows.append({\"image_id\": image_id, \"prediction_string\": pred_str})\n\n            df = pd.DataFrame(rows)\n            csv_path = f\"submission_{model_idx}_{size}.csv\"\n            df.to_csv(csv_path, index=False, quoting=csv.QUOTE_MINIMAL)\n            print(f\"[saved] {csv_path}\")\n            print(df.head(5))\n\n    return predictions\n\ndef apply_wbf_and_save_final_submission(predictions,\n                                        image_ids,\n                                        output_path=\"submission.csv\",\n                                        iou_thr=0.4,\n                                        skip_box_thr=0.0,    # do NOT filter before fusion\n                                        conf_post=0.15,      # filter AFTER WBF using this threshold\n                                        conf_type=\"avg\"):    # or \"max\" if you prefer\n    \"\"\"\n    Applies WBF across all models & sizes, then filters fused boxes by conf_post,\n    and saves a single merged submission CSV.\n    \"\"\"\n    final_rows = []\n\n    for image_id in image_ids:\n        all_boxes, all_scores, all_labels = [], [], []\n\n        for model_preds in predictions.values():\n            for size_preds in model_preds.values():\n                pred = size_preds.get(image_id)\n                if not pred or not pred[\"boxes\"]:\n                    continue\n                all_boxes.append(pred[\"boxes\"])\n                all_scores.append(pred[\"scores\"])\n                all_labels.append(pred[\"labels\"])\n\n        if not all_boxes:\n            pred_str = \"no boxes\"\n        else:\n            fb, fs, fl = weighted_boxes_fusion(\n                all_boxes,\n                all_scores,\n                all_labels,\n                iou_thr=iou_thr,\n                skip_box_thr=skip_box_thr,\n                conf_type=conf_type\n            )\n\n            keep = [i for i, s in enumerate(fs) if s >= conf_post]\n            if not keep:\n                pred_str = \"no boxes\"\n            else:\n                pred_str = \" \".join(\n                    f\"{int(fl[i])} {fs[i]:.6f} \"\n                    f\"{(fb[i][0]+fb[i][2])/2:.6f} {(fb[i][1]+fb[i][3])/2:.6f} \"\n                    f\"{(fb[i][2]-fb[i][0]):.6f} {(fb[i][3]-fb[i][1]):.6f}\"\n                    for i in keep\n                )\n\n        final_rows.append({\"image_id\": image_id, \"prediction_string\": pred_str})\n\n    pd.DataFrame(final_rows).to_csv(output_path, index=False, quoting=csv.QUOTE_MINIMAL)\n    print(f\"[notice] ✅ WBF submission saved to {output_path}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-03T22:10:00.674424Z","iopub.execute_input":"2026-02-03T22:10:00.674752Z","iopub.status.idle":"2026-02-03T22:10:00.694305Z","shell.execute_reply.started":"2026-02-03T22:10:00.674729Z","shell.execute_reply":"2026-02-03T22:10:00.693614Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nfrom pathlib import Path\nimport pandas as pd\nimport csv\nfrom ultralytics import YOLO\nfrom ensemble_boxes import weighted_boxes_fusion\nfrom PIL import Image\n\nmodel_paths = [\n    # '/kaggle/working/runs2/train/train/weights/last.pt',\n    '/kaggle/working/runs/detect/runs1/train/train/weights/last.pt',\n    '/kaggle/working/runs/detect/runs3/train/train/weights/last.pt',\n    # '/kaggle/working/runs/detect/runs1/train/train/weights/best.pt',\n    # '/kaggle/working/runs/detect/runs3/train/train/weights/best.pt',\n]\n\ntest_images_path = \"/kaggle/input/dal-shemagh-detection-challenge/images/test\"\noutput_dir = \"/kaggle/working/predictions/labels\"\n\nconf = 0.01\niou_thr = 0.4\nskip_box_thr = conf\n# image_sizes = [640,800,864,1024,1216,512,1344,2048]\nimage_sizes = [640,800,864,1024]\n# image_sizes = [1024]\n\n\nmodels = [YOLO(path) for path in model_paths]\npredictions = run_inference(models, image_sizes, test_images_path,conf=conf,iou_thr=iou_thr)\n\nimage_ids = list(next(iter(next(iter(predictions.values())).values())).keys())\n\napply_wbf_and_save_final_submission(predictions, image_ids,iou_thr=0.45,skip_box_thr=0.001,conf_post=0.05)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-03T22:10:02.214208Z","iopub.execute_input":"2026-02-03T22:10:02.2145Z","iopub.status.idle":"2026-02-03T22:43:11.871076Z","shell.execute_reply.started":"2026-02-03T22:10:02.21448Z","shell.execute_reply":"2026-02-03T22:43:11.870139Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport matplotlib.pyplot as plt\nfrom pathlib import Path\nfrom PIL import Image\nfrom matplotlib.patches import Rectangle\n\ndef plot_submission_predictions(\n    submission_csv: str,\n    test_images_path: str,\n    K: int = 5,\n    min_conf: float = 0.0\n):\n    \"\"\"\n    Plot the first K images from submission.csv with their predicted boxes,\n    but only show boxes with confidence >= min_conf.\n    \n    submission_csv:   path to your final submission.csv\n    test_images_path: folder containing the test images\n    K:                number of images to visualize\n    min_conf:         minimum confidence threshold for drawing a box\n    \"\"\"\n    df = pd.read_csv(submission_csv)\n    img_folder = Path(test_images_path)\n    \n    for _, row in df.head(K).iterrows():\n        image_id = row[\"image_id\"]\n        pred_str = row[\"prediction_string\"]\n        \n        # locate the image file\n        matches = list(img_folder.glob(f\"{image_id}.*\"))\n        if not matches:\n            print(f\"⚠️  Could not find file for {image_id}\")\n            continue\n        img = Image.open(matches[0])\n        \n        fig, ax = plt.subplots(figsize=(8, 6))\n        ax.imshow(img)\n        ax.axis(\"off\")\n        \n        if pred_str.lower() != \"no boxes\":\n            toks = pred_str.split()\n            for i in range(0, len(toks), 6):\n                lbl   = int(toks[i])\n                score = float(toks[i+1])\n                if score < min_conf:\n                    continue\n                x_c   = float(toks[i+2])\n                y_c   = float(toks[i+3])\n                w     = float(toks[i+4])\n                h     = float(toks[i+5])\n                \n                # convert normalized center w,h to absolute top-left corner + size\n                x1    = (x_c - w/2) * img.width\n                y1    = (y_c - h/2) * img.height\n                abs_w = w * img.width\n                abs_h = h * img.height\n                \n                rect = Rectangle((x1, y1), abs_w, abs_h,\n                                 fill=False, edgecolor=\"red\", lw=2)\n                ax.add_patch(rect)\n                ax.text(\n                    x1, y1 - 3,\n                    f\"{lbl}:{score:.2f}\",\n                    color=\"yellow\", fontsize=10,\n                    backgroundcolor=\"black\", alpha=0.7\n                )\n        \n        plt.show()\nplot_submission_predictions(\n    submission_csv=\"/kaggle/working/submission.csv\",\n    test_images_path= \"/kaggle/input/dal-shemagh-detection-challenge/images/test\",\n    K=28,\n    min_conf=0.005   # only show boxes with confidence ≥ min_conf\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-03T22:44:56.133332Z","iopub.execute_input":"2026-02-03T22:44:56.133851Z","iopub.status.idle":"2026-02-03T22:45:01.651272Z","shell.execute_reply.started":"2026-02-03T22:44:56.133829Z","shell.execute_reply":"2026-02-03T22:45:01.650496Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nfrom pathlib import Path\n\n# =========================\n# PATHS (edit if needed)\n# =========================\nSPLIT_ROOT = Path(\"yolo_split_80_20\")\nTRAIN_CSV = SPLIT_ROOT / \"train_labels_split.csv\"\nVAL_CSV   = SPLIT_ROOT / \"val_labels_split.csv\"\n\nGT_TRAIN_DIR = SPLIT_ROOT / \"labels/train\"\nGT_VAL_DIR   = SPLIT_ROOT / \"labels/val\"\n\n# =========================\n# SETTINGS\n# =========================\nFACE = 0\nSHEMAGH = 1\n\nGRID = np.linspace(0.0, 1.0, 101)  # contain_th candidates\n\n# =========================\n# Utils\n# =========================\ndef to_bool_series(s: pd.Series):\n    if s.dtype == object:\n        return s.astype(str).str.lower().isin([\"1\",\"true\",\"yes\",\"y\",\"t\"])\n    if s.dtype.kind in \"fc\":\n        return s.astype(float) >= 0.5\n    return s.astype(int) == 1\n\ndef xywhc_to_xyxy(xc, yc, w, h):\n    return (xc - w/2.0, yc - h/2.0, xc + w/2.0, yc + h/2.0)\n\ndef contain_ratio(face_xyxy, shem_xyxy):\n    fx1, fy1, fx2, fy2 = face_xyxy\n    sx1, sy1, sx2, sy2 = shem_xyxy\n\n    fa = max(0.0, fx2-fx1) * max(0.0, fy2-fy1)\n    if fa <= 0:\n        return 0.0\n\n    ix1 = max(fx1, sx1); iy1 = max(fy1, sy1)\n    ix2 = min(fx2, sx2); iy2 = min(fy2, sy2)\n    inter = max(0.0, ix2-ix1) * max(0.0, iy2-iy1)\n\n    return inter / (fa + 1e-12)\n\ndef f1(y_true, y_pred):\n    y_true = np.asarray(y_true).astype(int)\n    y_pred = np.asarray(y_pred).astype(int)\n    tp = int(((y_true==1) & (y_pred==1)).sum())\n    fp = int(((y_true==0) & (y_pred==1)).sum())\n    fn = int(((y_true==1) & (y_pred==0)).sum())\n    if tp == 0:\n        return 0.0\n    p = tp / (tp + fp + 1e-12)\n    r = tp / (tp + fn + 1e-12)\n    return 2*p*r / (p + r + 1e-12)\n\ndef load_gt_boxes_for_filenames(label_dir: Path, filenames):\n    # returns dict: stem -> list of (cls, xyxy)\n    out = {}\n    for fn in filenames:\n        stem = Path(fn).stem\n        p = label_dir / f\"{stem}.txt\"\n        boxes = []\n        if p.exists():\n            txt = p.read_text().strip()\n            if txt:\n                for line in txt.splitlines():\n                    parts = line.split()\n                    if len(parts) < 5:\n                        continue\n                    cls = int(float(parts[0]))\n                    xc, yc, w, h = map(float, parts[1:5])\n                    boxes.append((cls, xywhc_to_xyxy(xc, yc, w, h)))\n        out[stem] = boxes\n    return out\n\ndef pred_right_place_from_gt(gt_boxes, contain_th):\n    faces = [b for c,b in gt_boxes if c == FACE]\n    shems = [b for c,b in gt_boxes if c == SHEMAGH]\n    if not faces or not shems:\n        return 0\n    for f in faces:\n        for s in shems:\n            if contain_ratio(f, s) >= contain_th:\n                return 1\n    return 0\n\ndef eval_split(split_csv: Path, gt_dir: Path, contain_th: float):\n    df = pd.read_csv(split_csv)\n    df[\"filename\"] = df[\"filename\"].astype(str)\n    y_true = to_bool_series(df[\"right_place\"]).astype(int).tolist()\n    filenames = df[\"filename\"].tolist()\n\n    gt = load_gt_boxes_for_filenames(gt_dir, filenames)\n\n    y_pred = [pred_right_place_from_gt(gt[Path(fn).stem], contain_th) for fn in filenames]\n    f = f1(y_true, y_pred)\n\n    # If you insist on \"mAP@0.5:0.95\" with GT boxes as predictions:\n    # it's essentially perfect whenever there are GT instances of a class in the split.\n    # We'll compute a simple constant proxy:\n    n_face = sum(1 for boxes in gt.values() for (c,_) in boxes if c==FACE)\n    n_shem = sum(1 for boxes in gt.values() for (c,_) in boxes if c==SHEMAGH)\n    map95 = 0.5 * (1.0 if n_face > 0 else 0.0) + 0.5 * (1.0 if n_shem > 0 else 0.0)\n\n    score = 0.5 * map95 + 0.5 * f\n    return map95, f, score, int(sum(y_pred)), len(y_pred)\n\n# =========================\n# Tune on TRAIN\n# =========================\nrows = []\nbest = (-1.0, None, None, None)\n\nfor th in GRID:\n    m, f, sc, pos, n = eval_split(TRAIN_CSV, GT_TRAIN_DIR, float(th))\n    rows.append((float(th), m, f, sc, pos, n))\n    if sc > best[0]:\n        best = (sc, float(th), m, f)\n\nres = pd.DataFrame(rows, columns=[\"contain_th\",\"map95\",\"f1_rightplace\",\"score\",\"pred_pos\",\"n\"])\nprint(\"\\nTOP 10 TRAIN:\")\nprint(res.sort_values(\"score\", ascending=False).head(10))\n\nBEST_SCORE, BEST_CONTAIN_TH, BEST_MAP, BEST_F1 = best\nprint(\"\\nBEST on TRAIN:\", {\"contain_th\":BEST_CONTAIN_TH, \"map95\":BEST_MAP, \"f1\":BEST_F1, \"score\":BEST_SCORE})\n\n# =========================\n# Eval tuned threshold on VAL\n# =========================\nm_val, f_val, sc_val, pos_val, n_val = eval_split(VAL_CSV, GT_VAL_DIR, BEST_CONTAIN_TH)\nprint(\"\\n=== VAL @ tuned threshold ===\")\nprint({\"contain_th\":BEST_CONTAIN_TH, \"map95\":m_val, \"f1\":f_val, \"score\":sc_val, \"pred_pos\":pos_val, \"n\":n_val})\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-03T22:45:14.496034Z","iopub.execute_input":"2026-02-03T22:45:14.496626Z","iopub.status.idle":"2026-02-03T22:45:16.9525Z","shell.execute_reply.started":"2026-02-03T22:45:14.496602Z","shell.execute_reply":"2026-02-03T22:45:16.951679Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nfrom pathlib import Path\n\nFACE = 0\nSHEMAGH = 1\n\nCONF_TH = 0.2\nCONTAIN_TH = float(BEST_CONTAIN_TH)   \n\ndef xywhc_to_xyxy(xc, yc, w, h):\n    return (xc - w/2.0, yc - h/2.0, xc + w/2.0, yc + h/2.0)\n\ndef contain_ratio(face_xyxy, shem_xyxy):\n    fx1, fy1, fx2, fy2 = face_xyxy\n    sx1, sy1, sx2, sy2 = shem_xyxy\n    fa = max(0.0, fx2-fx1) * max(0.0, fy2-fy1)\n    if fa <= 0:\n        return 0.0\n    ix1 = max(fx1, sx1); iy1 = max(fy1, sy1)\n    ix2 = min(fx2, sx2); iy2 = min(fy2, sy2)\n    inter = max(0.0, ix2-ix1) * max(0.0, iy2-iy1)\n    return inter / (fa + 1e-12)\n\ndef parse_predstring_xywhc(ps):\n    if pd.isna(ps):\n        return []\n    s = str(ps).strip()\n    if s == \"\" or s.lower() == \"no boxes\" or s == \"-\":\n        return []\n    t = s.split()\n    if len(t) % 6 != 0:\n        return []\n    dets = []\n    for i in range(0, len(t), 6):\n        cls = int(float(t[i]))\n        conf = float(t[i+1])\n        xc = float(t[i+2]); yc = float(t[i+3])\n        w  = float(t[i+4]); h  = float(t[i+5])\n        dets.append((cls, conf, xywhc_to_xyxy(xc, yc, w, h)))\n    return dets\n\ndef compute_right_place(dets):\n    faces = [box for (cls, conf, box) in dets if cls==FACE and conf >= CONF_TH]\n    shems = [box for (cls, conf, box) in dets if cls==SHEMAGH and conf >= CONF_TH]\n    if not faces or not shems:\n        return 0\n    for f in faces:\n        for s in shems:\n            if contain_ratio(f, s) >= CONTAIN_TH:\n                return 1\n    return 0\n\ndf = pd.read_csv(\"submission.csv\")\n\n# expects at least: image_id, prediction_string\nif \"image_id\" not in df.columns or \"prediction_string\" not in df.columns:\n    raise ValueError(f\"submission.csv must have columns ['image_id','prediction_string']. Found: {list(df.columns)}\")\n\ndf[\"image_id\"] = df[\"image_id\"].astype(str)\ndf[\"filename\"] = df[\"image_id\"] + \".jpg\"\n\nnew_ps = []\nrp = []\n\nfor ps in df[\"prediction_string\"]:\n    s = \"\" if pd.isna(ps) else str(ps).strip()\n    dets = parse_predstring_xywhc(s)\n\n    if not dets:\n        new_ps.append(\"-\")\n        rp.append(0)\n    else:\n        new_ps.append(s if s.lower() != \"no boxes\" else \"-\")\n        rp.append(compute_right_place(dets))\n\nout = pd.DataFrame({\n    \"filename\": df[\"filename\"].astype(str),\n    \"prediction_string\": new_ps,\n    \"right_place\": rp\n})\n\nout = out.sort_values(\"filename\").reset_index(drop=True)\nout.to_csv(\"submission.csv\", index=False)\n\nprint(\"Overwrote submission.csv\")\nprint(\"CONTAIN_TH =\", CONTAIN_TH, \"CONF_TH =\", CONF_TH)\nprint(\"positives:\", int((out[\"right_place\"]==1).sum()), \"/\", len(out))\nout.head(5)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-03T22:45:38.22065Z","iopub.execute_input":"2026-02-03T22:45:38.220983Z","iopub.status.idle":"2026-02-03T22:45:38.254107Z","shell.execute_reply.started":"2026-02-03T22:45:38.22096Z","shell.execute_reply":"2026-02-03T22:45:38.253385Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"out['right_place'].value_counts()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-03T22:45:40.479965Z","iopub.execute_input":"2026-02-03T22:45:40.480675Z","iopub.status.idle":"2026-02-03T22:45:40.486436Z","shell.execute_reply.started":"2026-02-03T22:45:40.480643Z","shell.execute_reply":"2026-02-03T22:45:40.485724Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import math\nfrom pathlib import Path\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom PIL import Image\n\nSPLIT_ROOT = Path(\"yolo_split_80_20\")\nTRAIN_CSV  = SPLIT_ROOT / \"train_labels_split.csv\"\n\nTRAIN_IMG_DIR = SPLIT_ROOT / \"images/train\"\nTRAIN_GT_LABEL_DIR = SPLIT_ROOT / \"labels/train\"\n\nTEST_IMG_DIR = Path(\"/kaggle/input/dal-shemagh-detection-challenge/images/test\")\nTEST_GT_LABEL_DIR = None   # set to a labels folder if you have GT labels for test; else leave None\n\nSUBMISSION_CSV = Path(\"submission.csv\")\n\nNAMES = [\"Face\", \"Shemagh\"]\n\nCOLS = 4\nFIG_W = 18\nMAX_SHOW_PER_SET = 150\n\ndef to_bool_series(s: pd.Series):\n    if s.dtype == object:\n        return s.astype(str).str.lower().isin([\"1\", \"true\", \"yes\", \"y\", \"t\"])\n    if s.dtype.kind in \"fc\":\n        return s.astype(float) >= 0.5\n    return s.astype(int) == 1\n\ndef yolo_to_xyxy(x, y, w, h, W, H):\n    x_c = x * W\n    y_c = y * H\n    bw = w * W\n    bh = h * H\n    return (x_c - bw/2, y_c - bh/2, x_c + bw/2, y_c + bh/2)\n\ndef draw_yolo_label_boxes(ax, lab_path: Path, W: int, H: int, names, title_prefix=\"gt\"):\n    if lab_path is None or (not Path(lab_path).exists()):\n        return 0\n    txt = Path(lab_path).read_text(encoding=\"utf-8\").strip()\n    if not txt:\n        return 0\n\n    cnt = 0\n    for line in txt.splitlines():\n        p = line.strip().split()\n        if len(p) < 5:\n            continue\n        cls = int(float(p[0]))\n        x, y, w, h = map(float, p[1:5])\n        x1, y1, x2, y2 = yolo_to_xyxy(x, y, w, h, W, H)\n        rect = plt.Rectangle((x1, y1), x2 - x1, y2 - y1, fill=False, linewidth=2)\n        ax.add_patch(rect)\n        label = names[cls] if 0 <= cls < len(names) else str(cls)\n        ax.text(x1, y1, f\"{title_prefix}:{label}\", fontsize=9, bbox=dict(alpha=0.6))\n        cnt += 1\n    return cnt\n\ndef parse_prediction_string_xywhc(ps):\n    # expects: cls conf xc yc w h (normalized)\n    if pd.isna(ps):\n        return []\n    s = str(ps).strip()\n    if s == \"\" or s.lower() == \"no boxes\" or s == \"-\":\n        return []\n    t = s.split()\n    if len(t) % 6 != 0:\n        return []\n    dets = []\n    for i in range(0, len(t), 6):\n        cls = int(float(t[i]))\n        conf = float(t[i+1])\n        xc  = float(t[i+2]); yc = float(t[i+3])\n        w   = float(t[i+4]); h  = float(t[i+5])\n        dets.append((cls, conf, xc, yc, w, h))\n    return dets\n\ndef draw_submission_boxes(ax, pred_str, W, H, names, conf_th=0.0, title_prefix=\"pred\"):\n    dets = parse_prediction_string_xywhc(pred_str)\n    cnt = 0\n    for cls, conf, xc, yc, w, h in dets:\n        if conf < conf_th:\n            continue\n        x1, y1, x2, y2 = yolo_to_xyxy(xc, yc, w, h, W, H)\n        rect = plt.Rectangle((x1, y1), x2 - x1, y2 - y1, fill=False, linewidth=2)\n        ax.add_patch(rect)\n        label = names[cls] if 0 <= cls < len(names) else str(cls)\n        ax.text(x1, y1, f\"{title_prefix}:{label} {conf:.2f}\", fontsize=9, bbox=dict(alpha=0.6))\n        cnt += 1\n    return cnt\n\ndef resolve_img_path(img_dir: Path, filename: str):\n    p = img_dir / filename\n    if p.exists():\n        return p\n    stem = Path(filename).stem\n    for e in [\".jpg\", \".jpeg\", \".png\", \".bmp\", \".webp\"]:\n        p2 = img_dir / f\"{stem}{e}\"\n        if p2.exists():\n            return p2\n    hits = list(img_dir.glob(f\"{stem}.*\"))\n    return hits[0] if hits else None\n\ndef show_grid(df, img_dir: Path, gt_label_dir: Path, title: str,\n              filename_col=\"filename\",\n              pred_str_col=None,\n              conf_th_pred=0.0,\n              limit=None):\n\n    df = df.copy()\n    df[filename_col] = df[filename_col].astype(str)\n    df = df.sort_values(filename_col).reset_index(drop=True)\n    if limit is not None:\n        df = df.iloc[:limit].copy()\n\n    n = len(df)\n    if n == 0:\n        print(title, \": 0 items\")\n        return\n\n    rows = math.ceil(n / COLS)\n    plt.figure(figsize=(FIG_W, rows * (FIG_W / COLS) * 0.8))\n\n    for i in range(n):\n        fn = df.loc[i, filename_col]\n        img_path = resolve_img_path(img_dir, fn)\n\n        ax = plt.subplot(rows, COLS, i + 1)\n        if img_path is None or not Path(img_path).exists():\n            ax.set_title(f\"Missing: {fn}\")\n            ax.axis(\"off\")\n            continue\n\n        img = Image.open(img_path).convert(\"RGB\")\n        W, H = img.size\n        ax.imshow(img)\n        ax.axis(\"off\")\n\n        stem = Path(fn).stem\n        lab_path = None if gt_label_dir is None else (Path(gt_label_dir) / f\"{stem}.txt\")\n        gt_cnt = draw_yolo_label_boxes(ax, lab_path, W, H, NAMES, title_prefix=\"gt\")\n\n        pred_cnt = 0\n        if pred_str_col is not None:\n            pred_cnt = draw_submission_boxes(\n                ax,\n                df.loc[i, pred_str_col],\n                W, H,\n                NAMES,\n                conf_th=conf_th_pred,\n                title_prefix=\"pred\"\n            )\n\n        ax.set_title(f\"{Path(img_path).name} | gt={gt_cnt} pred={pred_cnt}\", fontsize=9)\n\n    plt.suptitle(title, fontsize=14)\n    plt.tight_layout()\n    plt.show()\n\ntrain_df = pd.read_csv(TRAIN_CSV)\ntrain_df[\"right_place\"] = to_bool_series(train_df[\"right_place\"])\ntrain_pos = train_df[train_df[\"right_place\"]].copy()\n\nsub = pd.read_csv(SUBMISSION_CSV)\ncols_low = {c.lower(): c for c in sub.columns}\nfname_col = cols_low.get(\"filename\", \"filename\")\nrp_col = cols_low.get(\"right_place\", \"right_place\")\nps_col = cols_low.get(\"prediction_string\", \"prediction_string\")\n\nsub[rp_col] = to_bool_series(sub[rp_col])\ntest_pos = sub[sub[rp_col]].copy()\n\nshow_grid(\n    train_pos,\n    img_dir=TRAIN_IMG_DIR,\n    gt_label_dir=TRAIN_GT_LABEL_DIR,\n    title=\"TRAIN positives: image + GT boxes + submission boxes (if provided)\",\n    filename_col=\"filename\",\n    pred_str_col=None,            # train CSV doesn't have prediction_string\n    conf_th_pred=0.0,\n    limit=MAX_SHOW_PER_SET\n)\n\nshow_grid(\n    test_pos,\n    img_dir=TEST_IMG_DIR,\n    gt_label_dir=TEST_GT_LABEL_DIR, \n    title=\"TEST predicted positives: image + GT boxes (if any) + submission boxes\",\n    filename_col=fname_col,\n    pred_str_col=ps_col,\n    conf_th_pred=0.0,               # change if you want to filter drawn preds\n    limit=MAX_SHOW_PER_SET\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-03T22:46:23.42111Z","iopub.execute_input":"2026-02-03T22:46:23.421963Z","iopub.status.idle":"2026-02-03T22:46:47.486063Z","shell.execute_reply.started":"2026-02-03T22:46:23.421937Z","shell.execute_reply":"2026-02-03T22:46:47.484707Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}