{"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":114094,"databundleVersionId":13610510,"sourceType":"competition"},{"sourceId":13426959,"sourceType":"datasetVersion","datasetId":8522224},{"sourceId":13433331,"sourceType":"datasetVersion","datasetId":8526304},{"sourceId":260929698,"sourceType":"kernelVersion"},{"sourceId":260932708,"sourceType":"kernelVersion"},{"sourceId":261579440,"sourceType":"kernelVersion"},{"sourceId":263977845,"sourceType":"kernelVersion"},{"sourceId":268766952,"sourceType":"kernelVersion"}],"dockerImageVersionId":31090,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install ultralytics ensemble-boxes > /dev/null","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-09-10T20:21:47.648815Z","iopub.execute_input":"2025-09-10T20:21:47.649075Z","iopub.status.idle":"2025-09-10T20:23:10.90868Z","shell.execute_reply.started":"2025-09-10T20:21:47.649054Z","shell.execute_reply":"2025-09-10T20:23:10.90768Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import csv\nimport torch\nimport random\nimport numpy as np\nimport pandas as pd\n\nfrom PIL import Image\nfrom pathlib import Path\nfrom ultralytics import YOLO\nfrom ensemble_boxes import weighted_boxes_fusion\n\nnp.random.seed(0)\nrandom.seed(0)\ntorch.manual_seed(0)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-10T20:23:10.910102Z","iopub.execute_input":"2025-09-10T20:23:10.910456Z","iopub.status.idle":"2025-09-10T20:23:16.371915Z","shell.execute_reply.started":"2025-09-10T20:23:10.910401Z","shell.execute_reply":"2025-09-10T20:23:16.371214Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def 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    \n    return filtered_boxes, filtered_scores, filtered_labels\n\ndef run_inference(models,\n                  image_sizes,\n                  test_images_path,\n                  conf=0.15,\n                  iou_thr=0.55,\n                  max_det=65,\n                  pre_conf=1e-3):\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\"✅ {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.35,\n                                        skip_box_thr=0.001,\n                                        conf_post=0.0015,\n                                        conf_type=\"avg\"): # \"max\"\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                weights=[2.5, 2.5, 2, 3], \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\"✅ Success!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-10T20:23:16.372738Z","iopub.execute_input":"2025-09-10T20:23:16.37308Z","iopub.status.idle":"2025-09-10T20:23:16.389097Z","shell.execute_reply.started":"2025-09-10T20:23:16.373061Z","shell.execute_reply":"2025-09-10T20:23:16.38829Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model_paths = [\n    '/kaggle/input/train-yolo11/runs/train/train/weights/best.pt',\n    '/kaggle/input/train-yolov11/runs3/train/train/weights/best.pt',\n    '/kaggle/input/train-yolov8/runs3/train/train/weights/best.pt',\n    \"/kaggle/input/train-yolov12/runs3/train/train/weights/best.pt\"\n]\n\ntest_images_path = \"/kaggle/input/grocery-items-multi-class-object-detection/testImages/testImages/images\"\n\nconf = 1e-6\niou_thr = 0.3\nimage_sizes = [960, 1024, 1440, 2272]\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())\napply_wbf_and_save_final_submission(predictions, image_ids, iou_thr=0.4, skip_box_thr=0.00001, conf_post=0.001)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-10T20:23:16.390628Z","iopub.execute_input":"2025-09-10T20:23:16.390892Z","iopub.status.idle":"2025-09-10T20:27:09.937104Z","shell.execute_reply.started":"2025-09-10T20:23:16.390874Z","shell.execute_reply":"2025-09-10T20:27:09.936209Z"}},"outputs":[],"execution_count":null}]}