{"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":117876,"databundleVersionId":14198377,"sourceType":"competition"}],"dockerImageVersionId":31193,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# Single-cell Kaggle notebook for the 3LC Cotton Weed Detection Challenge (UPDATED)\n# - Entire workflow in one runnable cell.\n# - Comments explain research ideas, pipeline steps, and rationale (the word \"you\" is not used in comments).\n# - No external 3LC dependency.\n# - Pipeline: environment, dataset discovery, copy writable label files, label sanitation,\n#   baseline YOLOv8n training, pseudo-label mining (self-training), retrain, inference, submission CSV.\n# - Fixed ultralytics argument compatibility (replaced deprecated 'hsv' argument with hsv_h/hsv_s/hsv_v).\n#\n# Notes:\n# - This cell can be long-running and GPU-intensive. Reduce epochs if kernel runtime is constrained.\n# - The script copies label .txt files to /kaggle/working to avoid read-only filesystem errors.\n\nimport os\nimport sys\nimport math\nimport glob\nimport shutil\nimport random\nimport subprocess\nfrom pathlib import Path\nfrom collections import defaultdict\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image, ImageOps\n\n# --- 1) Ensure ultralytics YOLOv8 availability ---\ntry:\n    import ultralytics\nexcept Exception:\n    print(\"Installing ultralytics...\")\n    subprocess.check_call([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"ultralytics==8.*\"])\n    import ultralytics\n\nfrom ultralytics import YOLO\n\n# --- 2) Dataset root discovery (explicit path for Kaggle competition) ---\ndataset_root = Path(\"/kaggle/input/the-3lc-cotton-weed-detection-challenge/cotton_weed_competition_dataset\")\nif not dataset_root.exists():\n    # fallback: scan /kaggle/input\n    for p in Path(\"/kaggle/input\").iterdir():\n        if \"cotton\" in p.name.lower() and \"weed\" in p.name.lower():\n            dataset_root = p\n            break\nif not dataset_root.exists():\n    raise FileNotFoundError(\"Dataset root not found under /kaggle/input. Place dataset there or adjust dataset_root path.\")\n\n# --- 3) Image and label directories (images read from input; labels will be copied to writable area) ---\ntrain_images_dir = dataset_root / \"train\" / \"images\"\norig_train_labels_dir = dataset_root / \"train\" / \"labels\"\nval_images_dir = dataset_root / \"val\" / \"images\"\norig_val_labels_dir = dataset_root / \"val\" / \"labels\"\ntest_images_dir = dataset_root / \"test\" / \"images\"\n\n# Validate presence and warn if something missing\nfor p in (train_images_dir, orig_train_labels_dir, val_images_dir, orig_val_labels_dir, test_images_dir):\n    if not p.exists():\n        print(f\"Warning: expected path not found: {p}\")\n\n# --- 4) Create writable copies of label files under /kaggle/working (Kaggle /kaggle/input is read-only) ---\nwrk = Path(\"/kaggle/working/cotton_dataset\")\nwrk_train_labels = wrk / \"train\" / \"labels\"\nwrk_val_labels = wrk / \"val\" / \"labels\"\nos.makedirs(wrk_train_labels, exist_ok=True)\nos.makedirs(wrk_val_labels, exist_ok=True)\n\ndef copy_label_files(src_dir: Path, dst_dir: Path):\n    txts = sorted(glob.glob(str(src_dir / \"*.txt\"))) if src_dir.exists() else []\n    for t in txts:\n        dst = dst_dir / Path(t).name\n        if not dst.exists():\n            shutil.copy2(t, str(dst))\n\ncopy_label_files(orig_train_labels_dir, wrk_train_labels)\ncopy_label_files(orig_val_labels_dir, wrk_val_labels)\n\n# Update label dir variables to writable copies\ntrain_labels_dir = wrk_train_labels\nval_labels_dir = wrk_val_labels\n\nprint(\"Dataset directories:\")\nprint(\" train_images_dir ->\", train_images_dir)\nprint(\" train_labels_dir ->\", train_labels_dir)\nprint(\" val_images_dir   ->\", val_images_dir)\nprint(\" val_labels_dir   ->\", val_labels_dir)\nprint(\" test_images_dir  ->\", test_images_dir)\n\n# --- 5) Utility: read/write YOLO-format label files ---\ndef read_yolo_label(path):\n    boxes = []\n    if not os.path.exists(path):\n        return boxes\n    with open(path, \"r\") as f:\n        for line in f:\n            line = line.strip()\n            if not line:\n                continue\n            parts = line.split()\n            if len(parts) < 5:\n                continue\n            cls = int(parts[0])\n            vals = list(map(float, parts[1:5]))\n            boxes.append((cls, *vals))\n    return boxes\n\ndef write_yolo_label(path, boxes):\n    os.makedirs(os.path.dirname(path), exist_ok=True)\n    with open(path, \"w\") as f:\n        for b in boxes:\n            f.write(\"{} {:.6f} {:.6f} {:.6f} {:.6f}\\n\".format(int(b[0]), b[1], b[2], b[3], b[4]))\n\n# --- 6) Geometry helpers (normalized coords -> absolute and back) ---\ndef yolo_to_xyxy(box, img_w, img_h):\n    x_c, y_c, w, h = box\n    x1 = (x_c - w/2) * img_w\n    y1 = (y_c - h/2) * img_h\n    x2 = (x_c + w/2) * img_w\n    y2 = (y_c + h/2) * img_h\n    return [x1, y1, x2, y2]\n\ndef xyxy_to_yolo(x1, y1, x2, y2, img_w, img_h):\n    w = max(0, x2 - x1)\n    h = max(0, y2 - y1)\n    x_c = x1 + w/2\n    y_c = y1 + h/2\n    if img_w == 0 or img_h == 0:\n        return [0.5, 0.5, 0.0, 0.0]\n    return [x_c/img_w, y_c/img_h, w/img_w, h/img_h]\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    inter_w = max(0, inter_x2 - inter_x1)\n    inter_h = max(0, inter_y2 - inter_y1)\n    inter_area = inter_w * inter_h\n    area_a = max(0, ax2-ax1) * max(0, ay2-ay1)\n    area_b = max(0, bx2-bx1) * max(0, by2-by1)\n    union = area_a + area_b - inter_area\n    if union == 0:\n        return 0.0\n    return inter_area / union\n\n# --- 7) Label sanitation: clip coords, remove tiny boxes, de-duplicate by IoU ---\nMIN_REL_AREA = 0.0008  # 0.08% of image area threshold for filtering tiny boxes\n\ndef sanitize_labels(images_dir, labels_dir, min_rel_area=MIN_REL_AREA, dry_run=False):\n    print(\"Sanitizing labels in:\", labels_dir)\n    modified = 0\n    total_boxes = 0\n    label_paths = sorted(glob.glob(str(labels_dir / \"*.txt\")))\n    for lab in label_paths:\n        img_name = Path(lab).stem\n        img_path = None\n        for ext in (\"jpg\",\"jpeg\",\"png\",\"JPG\",\"PNG\"):\n            cand = images_dir / (img_name + \".\" + ext)\n            if cand.exists():\n                img_path = cand\n                break\n        if img_path is None:\n            # If corresponding image not present, skip sanitation for this label file\n            continue\n        img = Image.open(img_path)\n        w,h = img.size\n        boxes = read_yolo_label(lab)\n        total_boxes += len(boxes)\n        new_boxes = []\n        for b in boxes:\n            cls, x_c, y_c, bw, bh = b\n            x_c = min(0.999999, max(0.0, x_c))\n            y_c = min(0.999999, max(0.0, y_c))\n            bw = min(0.999999, max(0.0, bw))\n            bh = min(0.999999, max(0.0, bh))\n            area_rel = bw * bh\n            if area_rel < min_rel_area:\n                continue\n            if bw <= 0 or bh <= 0:\n                continue\n            new_boxes.append((cls, x_c, y_c, bw, bh))\n        # de-duplicate by IoU among boxes of same class (keep larger area)\n        kept = []\n        for cand in sorted(new_boxes, key=lambda x: x[3]*x[4], reverse=True):\n            cls, x_c, y_c, bw, bh = cand\n            cand_xy = yolo_to_xyxy((x_c,y_c,bw,bh), w, h)\n            overlap = False\n            for k in kept:\n                if k[0] != cls:\n                    continue\n                k_xy = yolo_to_xyxy((k[1],k[2],k[3],k[4]), w, h)\n                if iou_xyxy(cand_xy, k_xy) > 0.85:\n                    overlap = True\n                    break\n            if not overlap:\n                kept.append(cand)\n        if dry_run:\n            if len(kept) != len(boxes):\n                modified += 1\n        else:\n            write_yolo_label(lab, kept)\n            if len(kept) != len(boxes):\n                modified += 1\n    print(f\"Sanitation complete. Modified files: {modified}. Total input boxes: {total_boxes}\")\n    return modified\n\n# Run sanitation on the writable copies of labels\nsanitize_labels(train_images_dir, train_labels_dir, dry_run=False)\nsanitize_labels(val_images_dir, val_labels_dir, dry_run=False)\n\n# --- 8) Prepare YOLO data.yaml for ultralytics (points at image folders; labels are auto-discovered by filename) ---\ndata_yaml_path = Path(\"cotton_yolo_data.yaml\")\ndata_yaml = {\n    \"names\": {0: \"carpetweed\", 1: \"morningglory\", 2: \"palmeramaranth\"},\n    \"nc\": 3,\n    \"train\": str(train_images_dir.resolve()),\n    \"val\": str(val_images_dir.resolve()),\n    \"test\": str(test_images_dir.resolve())\n}\nimport yaml\nwith open(data_yaml_path, \"w\") as f:\n    yaml.safe_dump(data_yaml, f)\nprint(\"Wrote data yaml to\", data_yaml_path)\n\n# --- 9) Training hyperparameters (adaptive) ---\nimport torch\n# choose device explicitly: 0 for first GPU, 'cpu' otherwise\ndevice = 0 if torch.cuda.is_available() else \"cpu\"\ngpu = torch.cuda.is_available()\nbs = 16 if gpu else 4\nimgsz = 640  # fixed by competition\nepochs_baseline = 40\n\n# Important: ultralytics no longer accepts a single 'hsv' float; use hsv_h, hsv_s, hsv_v instead.\n# Values chosen: hsv_h=0.015, hsv_s=0.7, hsv_v=0.4 (typical defaults)\ntrain_args = dict(\n    data=str(data_yaml_path),\n    epochs=epochs_baseline,\n    imgsz=imgsz,\n    batch=bs,\n    device=device,\n    name=\"yolov8n_baseline\",\n    exist_ok=True,\n    workers=4,\n    mosaic=1,\n    mixup=0.3,\n    lr0=0.01,\n    weight_decay=5e-4,\n    degrees=10.0,\n    translate=0.1,\n    scale=0.1,\n    shear=2.0,\n    perspective=0.0,\n    flipud=0.0,\n    fliplr=0.5,\n    hsv_h=0.015,\n    hsv_s=0.7,\n    hsv_v=0.4\n)\n\n# --- 10) Baseline training using YOLOv8n pretrained weights ---\nprint(\"Initializing YOLOv8n model (pretrained COCO).\")\nmodel = YOLO(\"yolov8n.pt\")  # will download weights if needed\n\nprint(\"Starting baseline training (this step may be time-consuming).\")\n# Wrap training in try/except to provide clearer error messages if configuration issues arise\ntry:\n    results = model.train(**train_args)\nexcept Exception as e:\n    print(\"Error during baseline training. Exception follows:\")\n    raise\n\n# After training, the model object refers to the trained model; runs saved under runs/detect/<name>\nruns_dir = Path(\"runs\") / \"detect\" / train_args[\"name\"]\nbest_pt = None\nif runs_dir.exists():\n    ckpts = sorted(runs_dir.glob(\"weights/*.pt\"), key=os.path.getmtime)\n    if ckpts:\n        best_pt = str(ckpts[-1])\nprint(\"Baseline best checkpoint:\", best_pt or \"Not found; model object will be used.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T03:05:53.650220Z","iopub.execute_input":"2025-11-27T03:05:53.650980Z","iopub.status.idle":"2025-11-27T03:38:38.084311Z","shell.execute_reply.started":"2025-11-27T03:05:53.650953Z","shell.execute_reply":"2025-11-27T03:38:38.083045Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- 11) Pseudo-label mining: add high-confidence predictions on training images that do not match GT (IoU < threshold) ---\nCONF_THR = 0.6\nIOU_MATCH = 0.5\n\ntrained_model = model  # reference to trained model\n\ntrain_img_paths = sorted(glob.glob(str(train_images_dir / \"*.*\")))\ntrain_label_paths = {Path(p).stem: p for p in glob.glob(str(train_labels_dir / \"*.txt\"))}\n\n# helpers to safely extract scalar and xyxy array\ndef tensor_to_float(t):\n    try:\n        return float(t.item())\n    except Exception:\n        return float(np.array(t).squeeze())\n\ndef xyxy_tensor_to_list(box_xyxy):\n    # box_xyxy may be a tensor of shape (1,4) or (4,)\n    arr = np.array(box_xyxy.cpu())\n    arr = np.asarray(arr).reshape(-1)\n    if arr.size < 4:\n        # fallback to zeros (should not happen)\n        return [0.0, 0.0, 0.0, 0.0]\n    return [float(arr[0]), float(arr[1]), float(arr[2]), float(arr[3])]\n\ndef mine_pseudo_labels(trained_model, img_paths, labels_dir, conf_thr=CONF_THR, iou_match=IOU_MATCH, min_rel_area=MIN_REL_AREA):\n    added = 0\n    explored = 0\n    for img_p in img_paths:\n        stem = Path(img_p).stem\n        explored += 1\n\n        # run inference on single image; use low-level predict API\n        res = trained_model.predict(source=img_p, imgsz=imgsz, conf=conf_thr, iou=0.45, device=device, verbose=False)\n        if not res:\n            continue\n        r = res[0]\n        # r.boxes may be an object with fields; if no boxes, skip\n        if not hasattr(r, \"boxes\") or len(r.boxes) == 0:\n            continue\n\n        # image size\n        img = Image.open(img_p)\n        w, h = img.size\n\n        preds = []\n        # iterate predicted boxes\n        for box in r.boxes:\n            # box.conf and box.cls are tensors; extract scalars safely\n            conf = tensor_to_float(box.conf)\n            # box.cls might be an array-like; coerce to int\n            cls = int(tensor_to_float(box.cls))\n            # box.xyxy might be tensor shape (1,4) or (4,)\n            xyxy_list = xyxy_tensor_to_list(box.xyxy)\n            # convert to yolo normalized\n            x1, y1, x2, y2 = xyxy_list\n            yolo_box = xyxy_to_yolo(x1, y1, x2, y2, w, h)\n            rel_area = float(yolo_box[2]) * float(yolo_box[3])\n            # filter by confidence and size\n            if conf < conf_thr or rel_area < min_rel_area:\n                continue\n            preds.append((cls, conf, yolo_box))\n\n        if not preds:\n            continue\n\n        # load ground-truth boxes for this image (writable labels folder)\n        gt_path = os.path.join(labels_dir, stem + \".txt\")\n        gt_boxes = read_yolo_label(gt_path)\n        gt_xy = []\n        for g in gt_boxes:\n            _, x_c, y_c, bw, bh = g\n            gt_xy.append(yolo_to_xyxy((x_c, y_c, bw, bh), w, h))\n\n        to_add = []\n        for cls, conf, ybox in preds:\n            pred_xy = yolo_to_xyxy((ybox[0], ybox[1], ybox[2], ybox[3]), w, h)\n            overlaps = [iou_xyxy(pred_xy, gxy) for gxy in gt_xy] if gt_xy else []\n            max_iou = max(overlaps) if overlaps else 0.0\n            # only add if it does not match existing GT (likely missed annotation)\n            if max_iou < iou_match:\n                to_add.append((cls, ybox))\n\n        if to_add:\n            # append to writable label file\n            label_file = os.path.join(labels_dir, stem + \".txt\")\n            existing = read_yolo_label(label_file)\n            for cls, yb in to_add:\n                existing.append((cls, float(yb[0]), float(yb[1]), float(yb[2]), float(yb[3])))\n                added += 1\n            write_yolo_label(label_file, existing)\n\n    print(f\"Pseudo-label mining complete. Images scanned: {explored}. Boxes added: {added}\")\n    return added\n\nprint(\"Starting pseudo-label mining (high-confidence training set predictions appended to label files).\")\nadded = mine_pseudo_labels(trained_model, train_img_paths, str(train_labels_dir))\n\n\n# --- 12) Retrain model after pseudo-label augmentation (robust implementation) ---\nretrain_epochs = 30\nretrain_name = \"yolov8n_selftrain\"\nretrain_args = dict(\n    data=str(data_yaml_path),\n    epochs=retrain_epochs,\n    imgsz=imgsz,\n    batch=bs,\n    device=device,\n    name=retrain_name,\n    exist_ok=True,\n    workers=4,\n    lr0=0.005,\n    weight_decay=5e-4,\n    mosaic=1,\n    mixup=0.15,\n    degrees=8.0,\n    translate=0.08,\n    scale=0.08,\n    shear=1.5,\n    perspective=0.0,\n    flipud=0.0,\n    fliplr=0.5,\n    hsv_h=0.015,\n    hsv_s=0.7,\n    hsv_v=0.4\n)\n\n# locate best checkpoint from baseline run\nbaseline_run_dir = Path(\"runs\") / \"detect\" / train_args[\"name\"]\nbest_ckpt = None\nif baseline_run_dir.exists():\n    # prefer 'weights/best.pt', fallback to 'weights/last.pt'\n    cand_best = baseline_run_dir / \"weights\" / \"best.pt\"\n    cand_last = baseline_run_dir / \"weights\" / \"last.pt\"\n    if cand_best.exists():\n        best_ckpt = str(cand_best)\n    elif cand_last.exists():\n        best_ckpt = str(cand_last)\n\n# fallback to the original pretrained if no baseline checkpoint found\nif best_ckpt is None:\n    print(\"Warning: baseline checkpoint not found; retraining will start from pretrained yolov8n weights.\")\n    best_ckpt = \"yolov8n.pt\"\n\nprint(\"Retrain: using checkpoint:\", best_ckpt)\n\n# create a fresh YOLO model object loaded from the checkpoint (clears prior overrides)\nmodel_for_retrain = YOLO(best_ckpt)\n\nprint(\"Starting retraining after pseudo-label augmentation.\")\ntry:\n    retrain_results = model_for_retrain.train(**retrain_args)\n    # update reference so subsequent inference uses the retrained model\n    trained_model = model_for_retrain\nexcept Exception as e:\n    print(\"Error during retraining. Exception follows:\")\n    raise\n\n\n\n# --- 13) Inference on test set and build submission CSV (robust xyxy handling for inference too) ---\ntest_image_paths = sorted(glob.glob(str(test_images_dir / \"*.*\")))\nsub_rows = []\nFINAL_CONF_THR = 0.35\n\ndef infer_with_tta(model, img_path, imgsz=640, conf=0.001, iou=0.45, tta=True):\n    detections = []\n\n    def parse_result(res_obj, img_w, img_h):\n        parsed = []\n        if not res_obj or not hasattr(res_obj, \"boxes\") or len(res_obj.boxes) == 0:\n            return parsed\n        for box in res_obj.boxes:\n            conf = tensor_to_float(box.conf)\n            cls = int(tensor_to_float(box.cls))\n            xyxy_list = xyxy_tensor_to_list(box.xyxy)\n            x1,y1,x2,y2 = xyxy_list\n            ybox = xyxy_to_yolo(x1,y1,x2,y2, img_w, img_h)\n            parsed.append((cls, conf, ybox))\n        return parsed\n\n    # base prediction\n    base_res = model.predict(source=img_path, imgsz=imgsz, conf=conf, iou=iou, device=device, verbose=False)\n    if base_res:\n        # determine image size once\n        img = Image.open(img_path)\n        w,h = img.size\n        detections += parse_result(base_res[0], w, h)\n\n    # TTA: horizontal flip\n    if tta:\n        img = Image.open(img_path)\n        w,h = img.size\n        img_flipped = ImageOps.mirror(img)\n        tmp_path = \"/tmp/tta_tmp.jpg\"\n        img_flipped.save(tmp_path)\n        res_f = model.predict(source=tmp_path, imgsz=imgsz, conf=conf, iou=iou, device=device, verbose=False)\n        if res_f:\n            # parsed boxes are on flipped image; flip x coords back\n            parsed = []\n            for box in res_f[0].boxes if hasattr(res_f[0], \"boxes\") else []:\n                c = tensor_to_float(box.conf)\n                cl = int(tensor_to_float(box.cls))\n                xyxy_list = xyxy_tensor_to_list(box.xyxy)\n                x1f,y1f,x2f,y2f = xyxy_list\n                # mirror horizontally back to original coords\n                mx1 = w - x2f\n                mx2 = w - x1f\n                ybox = xyxy_to_yolo(mx1, y1f, mx2, y2f, w, h)\n                parsed.append((cl, c, ybox))\n            detections += parsed\n\n    # Simple per-class NMS (suppress overlapping boxes per class)\n    final = []\n    detections_sorted = sorted(detections, key=lambda x: x[1], reverse=True)\n    img_w, img_h = Image.open(img_path).size\n    for cls, conf, ybox in detections_sorted:\n        keep = True\n        xy = yolo_to_xyxy((ybox[0], ybox[1], ybox[2], ybox[3]), img_w, img_h)\n        for k in final:\n            if k[0] != cls:\n                continue\n            kxy = yolo_to_xyxy((k[2][0], k[2][1], k[2][2], k[2][3]), img_w, img_h)\n            if iou_xyxy(xy, kxy) > 0.45:\n                keep = False\n                break\n        if keep:\n            final.append((cls, conf, ybox))\n    return final\n\nprint(\"Running inference on test images (this will take time).\")\nfor timg in test_image_paths:\n    stem = Path(timg).stem\n    dets = infer_with_tta(trained_model, timg, imgsz=imgsz, conf=0.001, iou=0.45, tta=True)\n    # format prediction string\n    parts = []\n    for cls, conf, ybox in dets:\n        if conf < FINAL_CONF_THR:\n            continue\n        parts += [str(int(cls)), f\"{float(conf):.3f}\", f\"{ybox[0]:.6f}\", f\"{ybox[1]:.6f}\", f\"{ybox[2]:.6f}\", f\"{ybox[3]:.6f}\"]\n    pred_str = \" \".join(parts) if parts else \"no box\"\n    sub_rows.append({\"image_id\": stem, \"prediction_string\": pred_str})\n\n# Build and save submission\nsubmission_df = pd.DataFrame(sub_rows)\nall_test_stems = [Path(p).stem for p in test_image_paths]\nexisting_stems = set(submission_df['image_id'].tolist())\nfor s in all_test_stems:\n    if s not in existing_stems:\n        submission_df = submission_df.append({\"image_id\": s, \"prediction_string\": \"no box\"}, ignore_index=True)\n\nsubmission_df = submission_df[[\"image_id\",\"prediction_string\"]]\nsubmission_csv = \"submission.csv\"\nsubmission_df.to_csv(submission_csv, index=False)\nprint(\"Submission saved to\", submission_csv)\nprint(submission_df.head(10))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T03:51:11.664154Z","iopub.execute_input":"2025-11-27T03:51:11.664951Z","iopub.status.idle":"2025-11-27T04:17:50.077495Z","shell.execute_reply.started":"2025-11-27T03:51:11.664924Z","shell.execute_reply":"2025-11-27T04:17:50.076625Z"}},"outputs":[],"execution_count":null}]}