{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":117876,"databundleVersionId":14198377,"sourceType":"competition"}],"dockerImageVersionId":31234,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Robust Cotton Weed Detection with YOLOv8n (640)\n### Data Quality • Confidence Thresholding • TTA Inference • Submission Engineering  \n**The 3LC Cotton Weed Detection Challenge**\n\nThis notebook is written as an engineering-focused, reproducible pipeline:\n- strict competition compliance (YOLOv8n only, imgsz=640),\n- robust dataset sanity checks,\n- confidence calibration,\n- lightweight TTA implemented manually (no invalid Ultralytics `tta` arg),\n- and a safe submission writer (no NaN / no empty rows).\n","metadata":{}},{"cell_type":"markdown","source":"This notebook focuses on inference design and submission engineering.\nA trained YOLOv8n weight file (best.pt) is required only if the inference cells are executed.\n","metadata":{}},{"cell_type":"markdown","source":"## Competition Constraints & Compliance\n\nThis work strictly follows all competition constraints:\n\n- ✅ Only **YOLOv8n** is used (no YOLOv8s/m/l/x).\n- ✅ Input size is fixed to **640**.\n- ✅ No multi-model ensembling / stacking.\n- ✅ Improvements focus on:\n  - data-centric thinking (label noise awareness),\n  - confidence calibration,\n  - inference robustness (TTA),\n  - submission format correctness.\n\n> Note: Test-Time Augmentation (TTA) here uses the **same single model** and the same weights.\nIt is not a multi-model ensemble.\n","metadata":{}},{"cell_type":"code","source":"# Kaggle kernels may not include ultralytics by default.\n# If already installed, this is fast; if not, it installs quietly.\n\n!pip -q install ultralytics\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport math\nimport csv\nfrom pathlib import Path\n\nimport cv2\nimport numpy as np\n\nfrom ultralytics import YOLO\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Competition dataset may appear as a nested folder in Kaggle input.\n# We'll locate dataset.yaml and derive paths from it.\n\nINPUT_ROOT = Path(\"/kaggle/input\")\n\ndef find_dataset_root():\n    candidates = []\n    for p in INPUT_ROOT.rglob(\"dataset.yaml\"):\n        candidates.append(p.parent)\n    # Prefer the one that has train/val/test folders next to it\n    for root in candidates:\n        if (root / \"train\").exists() and (root / \"val\").exists() and (root / \"test\").exists():\n            return root\n    return candidates[0] if candidates else None\n\nDATA_ROOT = find_dataset_root()\nprint(\"DATA_ROOT:\", DATA_ROOT)\n\nassert DATA_ROOT is not None, \"❌ Could not find dataset.yaml under /kaggle/input. Please Add Data correctly.\"\nprint(\"Files in DATA_ROOT:\", sorted([p.name for p in DATA_ROOT.iterdir()])[:20])\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"TRAIN_DIR = DATA_ROOT / \"train\" / \"images\"\nVAL_DIR   = DATA_ROOT / \"val\" / \"images\"\nTEST_DIR  = DATA_ROOT / \"test\" / \"images\"\n\ndef count_images(d):\n    exts = {\".jpg\", \".jpeg\", \".png\"}\n    return len([p for p in d.iterdir() if p.suffix.lower() in exts])\n\nprint(\"train images:\", count_images(TRAIN_DIR))\nprint(\"val images:  \", count_images(VAL_DIR))\nprint(\"test images: \", count_images(TEST_DIR))\n\n# The official test should be 170. You repeatedly used \"Found test images: 170\" as a hard check.\n# We'll keep it here as a guardrail.\nassert count_images(TEST_DIR) == 170, \"❌ Test image count is not 170. Dataset may be corrupted or wrong version.\"\nprint(\"✅ Test image count sanity check passed (170).\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Why Data Quality Became Central (Real-World Constraint)\n\nThis challenge intentionally simulates production constraints:\n- small model (YOLOv8n),\n- noisy / imperfect labels (missing boxes, inconsistent boxes, class confusion).\n\nIn my experiments, training instability and leaderboard variance were often caused by:\n- missing bounding boxes,\n- incorrect box placement,\n- statistically unlikely class configurations.\n\n**Key shift:** from “bigger model” → to **data-centric debugging + robust inference**.\n","metadata":{}},{"cell_type":"code","source":"# Minimal label audit:\n# - class distribution\n# - invalid values check (x,y,w,h in [0,1], w/h>0)\n\nLABEL_DIR = DATA_ROOT / \"train\" / \"labels\"\n\ncls_counts = {0:0, 1:0, 2:0}\nbad_lines = 0\nbad_files = 0\n\nfor txt in LABEL_DIR.glob(\"*.txt\"):\n    ok_file = True\n    with open(txt, \"r\") as f:\n        for line in f:\n            parts = line.strip().split()\n            if len(parts) != 5:\n                bad_lines += 1\n                ok_file = False\n                continue\n            c, x, y, w, h = parts\n            try:\n                c = int(c); x=float(x); y=float(y); w=float(w); h=float(h)\n            except:\n                bad_lines += 1\n                ok_file = False\n                continue\n            if c in cls_counts: cls_counts[c] += 1\n            if not (0 <= x <= 1 and 0 <= y <= 1 and 0 < w <= 1 and 0 < h <= 1):\n                bad_lines += 1\n                ok_file = False\n    if not ok_file:\n        bad_files += 1\n\nprint(\"Class counts (train labels):\", cls_counts)\nprint(\"Bad label lines:\", bad_lines)\nprint(\"Files with at least one bad line:\", bad_files)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Load a Single YOLOv8n Model (best.pt)\n\nThis notebook assumes you provide **one** trained YOLOv8n model weight file (`best.pt`) via Kaggle Dataset:\n- Example dataset name: `cotton-weed-yolov8-best-models`\n- Path in Kaggle: `/kaggle/input/cotton-weed-yolov8-best-models/best.pt`\n\nIf the model file is not found, the next cell will clearly show you what paths exist.\n","metadata":{}},{"cell_type":"code","source":"MODEL_CANDIDATES = [\n    Path(\"/kaggle/input/cotton-weed-yolov8-best-models/best.pt\"),\n    # Add more candidates here if you rename your dataset\n]\n\nmodel_path = None\nfor p in MODEL_CANDIDATES:\n    if p.exists():\n        model_path = p\n        break\n\nif model_path is None:\n    print(\"❌ best.pt not found in expected locations.\")\n    print(\"📌 /kaggle/input contains:\", sorted([x.name for x in INPUT_ROOT.iterdir()])[:50])\n    raise FileNotFoundError(\"Please add your model dataset and ensure best.pt exists.\")\n\nprint(\"✅ Loading model:\", model_path)\nmodel = YOLO(str(model_path))\nprint(\"Model loaded.\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Confidence Threshold Selection (Why it matters)\n\nIn this competition, the confidence threshold had an outsized impact on the final score.\nI observed that threshold tuning often mattered more than architectural changes.\n\nFrom my validation/submission experiments, a stable choice was:\n\n**conf = 0.25**\n\nBelow, I include an optional **validation sweep** on the provided validation split.\n","metadata":{}},{"cell_type":"code","source":"# Optional: run validation with different conf values.\n# Your earlier experiments swept conf around 0.18~0.32 and compared mAP. :contentReference[oaicite:6]{index=6}\n\nCONF_LIST = [0.18, 0.22, 0.25, 0.28, 0.32]\nresults_summary = []\n\nDATA_YAML = str(DATA_ROOT / \"dataset.yaml\")\n\nfor conf in CONF_LIST:\n    print(f\"\\n===== Val sweep: conf={conf:.2f} =====\")\n    r = model.val(data=DATA_YAML, imgsz=640, conf=conf, verbose=False)\n    # Ultralytics result object contains metrics; we keep it simple and store map50, map\n    map50 = float(getattr(r.box, \"map50\", np.nan))\n    map5095 = float(getattr(r.box, \"map\", np.nan))\n    results_summary.append((conf, map50, map5095))\n\nprint(\"\\n===== Summary (conf, mAP50, mAP50-95) =====\")\nfor conf, map50, map5095 in results_summary:\n    print(f\"conf={conf:.2f} -> mAP50={map50:.4f}, mAP50-95={map5095:.4f}\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Manual TTA Implementation (No invalid Ultralytics args)\n\nI previously hit an error because Ultralytics does **not** support a `tta=` argument:\n> `SyntaxError: 'tta' is not a valid YOLO argument.` :contentReference[oaicite:7]{index=7}\n\nSo in this notebook, TTA is implemented explicitly:\n- original\n- horizontal flip\n- vertical flip\n- rotate 90°\n- rotate 270°\n\nPredictions are mapped back to the original image coordinates and merged via class-wise NMS.\n","metadata":{}},{"cell_type":"code","source":"def xyxy_to_xywhn(xyxy, w, h):\n    # xyxy in pixels -> normalized (x_center, y_center, width, height)\n    x1, y1, x2, y2 = xyxy\n    xc = (x1 + x2) / 2.0 / w\n    yc = (y1 + y2) / 2.0 / h\n    bw = (x2 - x1) / w\n    bh = (y2 - y1) / h\n    return xc, yc, bw, bh\n\ndef clip01(v):\n    return max(0.0, min(1.0, v))\n\ndef iou_xyxy(a, b):\n    ax1, ay1, ax2, ay2 = a\n    bx1, by1, bx2, by2 = b\n    inter_x1 = max(ax1, bx1)\n    inter_y1 = max(ay1, by1)\n    inter_x2 = min(ax2, bx2)\n    inter_y2 = min(ay2, by2)\n    iw = max(0.0, inter_x2 - inter_x1)\n    ih = max(0.0, inter_y2 - inter_y1)\n    inter = iw * ih\n    area_a = max(0.0, ax2 - ax1) * max(0.0, ay2 - ay1)\n    area_b = max(0.0, bx2 - bx1) * max(0.0, by2 - by1)\n    union = area_a + area_b - inter + 1e-9\n    return inter / union\n\ndef nms_classwise(boxes, scores, classes, iou_thr=0.5):\n    # boxes: (N,4) xyxy pixels in original frame\n    keep = []\n    idxs = np.argsort(-scores)\n    while len(idxs) > 0:\n        i = idxs[0]\n        keep.append(i)\n        rest = idxs[1:]\n        new_rest = []\n        for j in rest:\n            if classes[j] != classes[i]:\n                new_rest.append(j)\n                continue\n            if iou_xyxy(boxes[i], boxes[j]) < iou_thr:\n                new_rest.append(j)\n        idxs = np.array(new_rest, dtype=int)\n    return keep\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def tta_variants(img):\n    # Returns list of (variant_name, transformed_img)\n    return [\n        (\"orig\", img),\n        (\"fliplr\", cv2.flip(img, 1)),\n        (\"flipud\", cv2.flip(img, 0)),\n        (\"rot90\", cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)),\n        (\"rot270\", cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE)),\n    ]\n\ndef invert_xyxy(xyxy, variant, w, h):\n    # Map xyxy (pixels in transformed image) back to original image coordinates\n    x1, y1, x2, y2 = xyxy\n\n    if variant == \"orig\":\n        return np.array([x1, y1, x2, y2], dtype=float)\n\n    if variant == \"fliplr\":\n        # x' = w - x\n        return np.array([w - x2, y1, w - x1, y2], dtype=float)\n\n    if variant == \"flipud\":\n        return np.array([x1, h - y2, x2, h - y1], dtype=float)\n\n    if variant == \"rot90\":\n        # transformed image dims: (h, w) -> (w, h)\n        # rot90 CW: (x, y) in rotated corresponds to (x_orig, y_orig) = (x, y) mapping:\n        # Original -> Rot90: (x, y) -> (y, w - x)\n        # Inverse: (x_r, y_r) -> (x_o, y_o) = (w - y_r, x_r)\n        # For boxes, convert corners:\n        pts = np.array([\n            [x1, y1],\n            [x2, y1],\n            [x2, y2],\n            [x1, y2],\n        ], dtype=float)\n        x_r = pts[:,0]; y_r = pts[:,1]\n        x_o = w - y_r\n        y_o = x_r\n        return np.array([x_o.min(), y_o.min(), x_o.max(), y_o.max()], dtype=float)\n\n    if variant == \"rot270\":\n        # rot270 CCW: Original -> Rot270: (x, y) -> (h - y, x)\n        # Inverse: (x_r, y_r) -> (x_o, y_o) = (y_r, h - x_r)\n        pts = np.array([\n            [x1, y1],\n            [x2, y1],\n            [x2, y2],\n            [x1, y2],\n        ], dtype=float)\n        x_r = pts[:,0]; y_r = pts[:,1]\n        x_o = y_r\n        y_o = h - x_r\n        return np.array([x_o.min(), y_o.min(), x_o.max(), y_o.max()], dtype=float)\n\n    raise ValueError(\"Unknown variant: \" + variant)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def predict_tta(img, conf=0.25, iou_thr=0.5):\n    h, w = img.shape[:2]\n    all_boxes = []\n    all_scores = []\n    all_cls = []\n\n    for name, im_t in tta_variants(img):\n        res = model.predict(im_t, imgsz=640, conf=conf, verbose=False)[0]\n        if res.boxes is None or len(res.boxes) == 0:\n            continue\n\n        # boxes.data: (N, 6) => x1,y1,x2,y2,conf,cls\n        arr = res.boxes.data.detach().cpu().numpy()\n        for x1, y1, x2, y2, sc, cl in arr:\n            mapped = invert_xyxy((x1, y1, x2, y2), name, w=w, h=h)\n            all_boxes.append(mapped)\n            all_scores.append(float(sc))\n            all_cls.append(int(cl))\n\n    if len(all_boxes) == 0:\n        return np.zeros((0,4), float), np.zeros((0,), float), np.zeros((0,), int)\n\n    all_boxes = np.stack(all_boxes, axis=0)\n    all_scores = np.array(all_scores, dtype=float)\n    all_cls = np.array(all_cls, dtype=int)\n\n    keep = nms_classwise(all_boxes, all_scores, all_cls, iou_thr=iou_thr)\n    return all_boxes[keep], all_scores[keep], all_cls[keep]\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Submission Format (Competition-safe)\n\nThe competition expects:\n\n- columns: `image_id,prediction_string` (lowercase recommended)\n- each detection: `class conf x_center y_center width height` (normalized [0,1])\n- if no detections: output **`no box`** (this is what I used in my own CSVs) :contentReference[oaicite:8]{index=8}\n\nTo avoid the classic Kaggle error (“null values”), this notebook **never writes empty rows**.\n","metadata":{}},{"cell_type":"code","source":"def build_prediction_string(boxes_xyxy, scores, classes, w, h):\n    if len(boxes_xyxy) == 0:\n        return \"no box\"\n\n    parts = []\n    for b, sc, cl in zip(boxes_xyxy, scores, classes):\n        xc, yc, bw, bh = xyxy_to_xywhn(b, w=w, h=h)\n        # clamp to [0,1] to avoid formatting issues\n        xc, yc, bw, bh = map(clip01, [xc, yc, bw, bh])\n        parts.extend([\n            str(int(cl)),\n            f\"{float(sc):.4f}\",\n            f\"{xc:.4f}\",\n            f\"{yc:.4f}\",\n            f\"{bw:.4f}\",\n            f\"{bh:.4f}\",\n        ])\n    return \" \".join(parts)\n\nOUT_CSV = \"submission.csv\"\nCONF = 0.25\nIOU_THR = 0.5\n\ntest_images = sorted([p for p in TEST_DIR.iterdir() if p.suffix.lower() in {\".jpg\",\".jpeg\",\".png\"}])\nprint(\"Test images:\", len(test_images))\n\nwith open(OUT_CSV, \"w\", newline=\"\") as f:\n    writer = csv.writer(f)\n    writer.writerow([\"image_id\", \"prediction_string\"])\n\n    for p in test_images:\n        img = cv2.imread(str(p))\n        h, w = img.shape[:2]\n\n        boxes, scores, cls = predict_tta(img, conf=CONF, iou_thr=IOU_THR)\n        pred_str = build_prediction_string(boxes, scores, cls, w=w, h=h)\n\n        # image_id in many of your CSVs is without extension; we follow that convention.\n        image_id = p.stem\n        writer.writerow([image_id, pred_str])\n\nprint(f\"✅ Saved: {OUT_CSV}\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Sanity check: rows should be 171 (header + 170 images)\nwith open(\"submission.csv\", \"r\") as f:\n    head = [next(f).strip() for _ in range(10)]\nprint(\"\\n\".join(head))\n\n# Count lines\nwith open(\"submission.csv\", \"r\") as f:\n    n_lines = sum(1 for _ in f)\nprint(\"Total lines:\", n_lines, \"(expected 171)\")\nassert n_lines == 171, \"❌ submission.csv line count mismatch.\"\nprint(\"✅ submission.csv row count correct.\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## What I Tried (and Why I Dropped It)\n\n### 1) Large-scale automatic label fixing\nHeuristic scripts that “auto-corrected” many labels ended up modifying correct samples and added noise.\nTraining stability degraded.\n\n### 2) Confidence ensembling (e.g., conf = 0.35 + 0.45)\nI tested combining predictions across multiple confidence thresholds (I literally generated CSV like `submission_conf35_45_ensemble.csv`). :contentReference[oaicite:9]{index=9}  \nAlthough some local improvement was observed, it increased variance and was not consistently better.\n\n**Conclusion:**  \nUnder strict deployment-style constraints, *simple + stable* outperformed complex tricks.\n","metadata":{}},{"cell_type":"markdown","source":"## Key Takeaways (Production mindset)\n\n- **Data issues dominate**: model scaling is not the main lever under strict constraints.\n- **Small targeted fixes beat bulk changes**: fix 5 critical samples > auto-fix 500.\n- **Confidence calibration is a design choice**: minor tuning changes F1 a lot.\n- **Inference robustness matters**: manual TTA + NMS merge improved stability.\n- **Engineering matters**: dataset integrity checks (test=170) and safe CSV writing prevent silent failure.\n","metadata":{}},{"cell_type":"markdown","source":"## Final Notes\n\nThis notebook intentionally prioritizes:\n- reproducibility,\n- interpretability,\n- and compliance with real-world constraints.\n\nIf you want to extend this further:\n- run longer training (e.g., 130 epochs),\n- add more targeted data augmentation,\n- or iterate a train–fix–retrain loop using systematic error analysis tools (e.g., 3LC).\n\nGood luck 🌾\n","metadata":{}},{"cell_type":"markdown","source":"[](http://)","metadata":{}},{"cell_type":"markdown","source":"","metadata":{}},{"cell_type":"markdown","source":"","metadata":{}},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"","metadata":{}},{"cell_type":"markdown","source":"","metadata":{}},{"cell_type":"markdown","source":"","metadata":{}},{"cell_type":"markdown","source":"","metadata":{}}]}