{"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":"nvidiaTeslaT4","dataSources":[{"sourceId":19989,"databundleVersionId":1160143,"sourceType":"competition"},{"sourceId":8216181,"sourceType":"datasetVersion","datasetId":4869911},{"sourceId":13899843,"sourceType":"datasetVersion","datasetId":8855663}],"dockerImageVersionId":31193,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 0: Install YOLO (Ultralytics)\n# Function: Install the ultralytics package providing the YOLO class.\n# Input: None\n# Output: ultralytics package available in the environment.\n# Hyperparameters: None\n# Assumptions: Internet access + pip allowed in this environment.\n\n!pip install -q ultralytics\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T17:02:24.558918Z","iopub.execute_input":"2025-11-27T17:02:24.559464Z","iopub.status.idle":"2025-11-27T17:02:27.849686Z","shell.execute_reply.started":"2025-11-27T17:02:24.559436Z","shell.execute_reply":"2025-11-27T17:02:27.848887Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 1: Setup, imports, and global configuration\n# ------------------------------------------------------------\n# 1. Name:\n#    Setup & Global Config\n#\n# 2. Function:\n#    - Import all required libraries.\n#    - Define global paths and hyperparameters shared across pipeline stages.\n#\n# 3. Inputs / Outputs (example):\n#    - Inputs: None at runtime (you manually set DATA_ROOT_* paths).\n#    - Outputs: \n#        * Global constants like GRID_H, GRID_W, thresholds, etc.\n#        * Example: HEADCOUNT_TILE_THR = 5 means tiles with < 5 heads trigger zoom.\n#\n# 4. Hyperparameters:\n#    - GRID_H, GRID_W: 10, 10      (grid size for head density map)\n#    - HEADCOUNT_TILE_THR: 5       (minimum acceptable heads per tile)\n#    - STRESS_DENSITY_THR: 0.4     (optional: density threshold for “stress”)\n#    - ZOOM_MARGIN_METERS: 2.0     (safety margin around suspicious tile)\n#    - TARGET_OVERLAP: 0.6         (image overlap fraction between frames)\n#\n# 5. Assumptions / Thresholds:\n#    - Assume Global Wheat Detection dataset structure under DATA_ROOT_WHEAT.\n#    - Assume disease dataset (LCDCD / 2020 wheat disease) is under DATA_ROOT_DISEASE.\n#    - Assume YOLOv8 + PyTorch are available in environment.\n# ============================================================\n\nimport os\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\nimport torch\nfrom torch import nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms, models\n\n# YOLO (Ultralytics) - used via Python API\ntry:\n    from ultralytics import YOLO\nexcept ImportError:\n    YOLO = None  # You can install with: !pip install ultralytics\n\n# ---- Paths (edit as needed) ----\nDATA_ROOT_WHEAT = Path(\"/kaggle/input/global-wheat-detection\")  # Example\nDATA_ROOT_DISEASE = Path(\"/kaggle/input/wheat-disease-2020\")    # Example\nOUTPUT_ROOT = Path(\"./outputs\")\nOUTPUT_ROOT.mkdir(parents=True, exist_ok=True)\n\n# ---- Grid & thresholds ----\nGRID_H, GRID_W = 10, 10\n\nHEADCOUNT_TILE_THR = 5      # tiles with < 5 heads are considered sparse → candidate for zoom\nSTRESS_DENSITY_THR = 0.4    # optional: density_norm < 0.4 → stressed\nTARGET_OVERLAP = 0.6        # 60% forward overlap between consecutive frames\nZOOM_MARGIN_METERS = 2.0    # expand around suspicious tile region in ground coordinates\n\n# ---- Generic device ----\nDEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(\"Using device:\", DEVICE)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T17:02:36.055863Z","iopub.execute_input":"2025-11-27T17:02:36.056513Z","iopub.status.idle":"2025-11-27T17:02:39.092498Z","shell.execute_reply.started":"2025-11-27T17:02:36.056472Z","shell.execute_reply":"2025-11-27T17:02:39.091801Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 2: Global Wheat dataset loader & 10×10 tiling utilities\n# ------------------------------------------------------------\n# 1. Name:\n#    WheatDataset & Tile Utilities\n#\n# 2. Function:\n#    - Load images + bounding boxes from Global Wheat Detection.\n#    - Provide utilities to:\n#        * Convert bounding boxes to per-tile headcounts on a 10×10 grid.\n#\n# 3. Inputs / Outputs (example):\n#    - Input example:\n#        image_id = \"abc123\", image size = (H=1024, W=1024),\n#        boxes = [[x, y, w, h], ...]\n#    - Output example:\n#        head_counts.shape = (10, 10)\n#        head_counts[2, 3] = 7  (tile row 2, col 3 has 7 heads)\n#\n# 4. Hyperparameters:\n#    - GRID_H, GRID_W from global config.\n#\n# 5. Assumptions / Thresholds:\n#    - CSV file \"train.csv\" exists with 'image_id' and 'bbox' fields.\n#    - Bounding boxes are in COCO-style [x, y, width, height] in pixel units.\n# ============================================================\n\nclass WheatDataset(Dataset):\n    def __init__(self, csv_path: Path, img_dir: Path, transform=None):\n        self.df = pd.read_csv(csv_path)\n        self.img_dir = img_dir\n        self.transform = transform\n\n        # Group boxes by image_id\n        self.image_ids = self.df[\"image_id\"].unique()\n        self.grouped = self.df.groupby(\"image_id\")\n\n    def __len__(self):\n        return len(self.image_ids)\n\n    def __getitem__(self, idx):\n        image_id = self.image_ids[idx]\n        img_path = self.img_dir / f\"{image_id}.jpg\"\n        img = Image.open(img_path).convert(\"RGB\")\n\n        boxes_str = self.grouped.get_group(image_id)[\"bbox\"].tolist()\n        boxes = []\n        for s in boxes_str:\n            # bbox is in format \"[x, y, w, h]\"\n            x, y, w, h = eval(s)\n            boxes.append([x, y, w, h])\n        boxes = np.array(boxes, dtype=np.float32)\n\n        if self.transform:\n            img = self.transform(img)\n\n        return image_id, img, boxes\n\n\ndef boxes_to_tile_headcounts(boxes: np.ndarray, H: int, W: int, grid_h: int, grid_w: int):\n    \"\"\"\n    Convert box centers into a (grid_h, grid_w) head_counts array.\n    \"\"\"\n    head_counts = np.zeros((grid_h, grid_w), dtype=np.float32)\n    if boxes.size == 0:\n        return head_counts\n\n    for (x, y, w, h) in boxes:\n        cx = x + w / 2.0\n        cy = y + h / 2.0\n        j = min(int(cx / W * grid_w), grid_w - 1)\n        i = min(int(cy / H * grid_h), grid_h - 1)\n        head_counts[i, j] += 1.0\n\n    return head_counts\n\n\n# Example usage (commented):\n# csv_path = DATA_ROOT_WHEAT / \"train.csv\"\n# img_dir = DATA_ROOT_WHEAT / \"train\"\n# ds = WheatDataset(csv_path, img_dir)\n# image_id, img, boxes = ds[0]\n# H, W = np.array(img).shape[:2]\n# head_counts = boxes_to_tile_headcounts(boxes, H, W, GRID_H, GRID_W)\n# print(image_id, head_counts.sum())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T17:02:42.840454Z","iopub.execute_input":"2025-11-27T17:02:42.840854Z","iopub.status.idle":"2025-11-27T17:02:42.84969Z","shell.execute_reply.started":"2025-11-27T17:02:42.840831Z","shell.execute_reply":"2025-11-27T17:02:42.849105Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 3: Train YOLO head detector on Global Wheat\n# ------------------------------------------------------------\n# 1. Name:\n#    Train YOLO Head Detector\n#\n# 2. Function:\n#    - Configure and train a YOLO model (e.g. YOLOv8n) for wheat head detection.\n#    - Uses Global Wheat dataset formatted for YOLO (data.yaml).\n#\n# 3. Inputs / Outputs (example):\n#    - Input example:\n#        data_yaml = \"./wheat_yolo_data.yaml\"\n#    - Output example:\n#        best model weights saved at:\n#        ./runs/detect/wheat_yolo/weights/best.pt\n#\n# 4. Hyperparameters:\n#    - model_name: \"yolov8n.pt\"\n#    - epochs: 50\n#    - img_size: 1024\n#    - batch: 8\n#\n# 5. Assumptions / Thresholds:\n#    - Assumes you have already created a YOLO data.yaml file pointing to images & labels.\n#    - Assumes ultralytics YOLO is installed and GPU is available for training.\n# ============================================================\n\nYOLO_DATA_YAML = \"./wheat_yolo_data.yaml\"  # EDIT to your actual path\nYOLO_MODEL_NAME = \"yolov8n.pt\"             # lightweight model\nYOLO_EPOCHS = 50\nYOLO_IMG_SIZE = 1024\nYOLO_BATCH = 8\n\nif YOLO is not None:\n    yolo_model = YOLO(YOLO_MODEL_NAME)\n    # You can uncomment this cell to train.\n    # yolo_model.train(\n    #     data=YOLO_DATA_YAML,\n    #     epochs=YOLO_EPOCHS,\n    #     imgsz=YOLO_IMG_SIZE,\n    #     batch=YOLO_BATCH,\n    #     project=\"runs/detect\",\n    #     name=\"wheat_yolo\",\n    # )\nelse:\n    print(\"YOLO not imported. Install ultralytics or check environment.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T16:37:38.906295Z","iopub.execute_input":"2025-11-27T16:37:38.906555Z","iopub.status.idle":"2025-11-27T16:37:38.943557Z","shell.execute_reply.started":"2025-11-27T16:37:38.906535Z","shell.execute_reply":"2025-11-27T16:37:38.942836Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":" # ============================================================\n# Cell 4: YOLO Inference to Tile Headcounts\n# ------------------------------------------------------------\n# 1. Name:\n#    YOLO Inference → Tile Headcounts\n# 2. Function:\n#    - Load trained YOLO weights.\n#    - Run detection on a given image.\n#    - Convert boxes to 10×10 head_counts + density_norm.\n# 3. I/O (example):\n#    - Input: image_path = \".../3b778c35e.jpg\"\n#    - Output: head_counts.shape=(10,10), density_norm.shape=(10,10)\n# 4. Hyperparameters:\n#    - CONF_THR = 0.25, IOU_THR = 0.45\n# 5. Assumptions / thresholds:\n#    - Trained weights at runs/detect/yolov8n_gwd_wheat/weights/best.pt\n# ============================================================\n\nfrom pathlib import Path\nimport numpy as np\nfrom PIL import Image\n\nYOLO_WEIGHTS = \"runs/detect/yolov8n_gwd_wheat/weights/best.pt\"  # <- from cell-3 result\nCONF_THR = 0.25\nIOU_THR = 0.45\n\nif YOLO is not None and Path(YOLO_WEIGHTS).exists():\n    head_yolo = YOLO(YOLO_WEIGHTS)\n    print(\"Loaded YOLO weights from:\", YOLO_WEIGHTS)\nelse:\n    head_yolo = None\n    print(\"Trained YOLO weights not found at\", YOLO_WEIGHTS, \"– using placeholder inference.\")\n\n\ndef run_yolo_headcount(image_path: Path, grid_h: int = GRID_H, grid_w: int = GRID_W):\n    \"\"\"\n    Run YOLO on image_path and return (head_counts, density_norm).\n    \"\"\"\n    img = Image.open(image_path).convert(\"RGB\")\n    img_np = np.array(img)\n    H, W = img_np.shape[:2]\n\n    if head_yolo is None:\n        head_counts = np.zeros((grid_h, grid_w), dtype=np.float32)\n        density_norm = head_counts.copy()\n        return head_counts, density_norm\n\n    results = head_yolo.predict(\n        source=str(image_path),\n        conf=CONF_THR,\n        iou=IOU_THR,\n        verbose=False\n    )\n\n    boxes = []\n    for r in results:\n        if r.boxes is None:\n            continue\n        for b in r.boxes.xywh.cpu().numpy():\n            x_c, y_c, w, h = b\n            x = x_c - w / 2.0\n            y = y_c - h / 2.0\n            boxes.append([x, y, w, h])\n\n    boxes = np.array(boxes, dtype=np.float32) if len(boxes) > 0 else np.zeros((0, 4), dtype=np.float32)\n    head_counts = boxes_to_tile_headcounts(boxes, H, W, grid_h, grid_w)\n\n    if head_counts.max() > 0:\n        density_norm = head_counts / head_counts.max()\n    else:\n        density_norm = head_counts.copy()\n\n    return head_counts, density_norm\n\n# Example (commented):\n# sample = Path(\"/kaggle/working/yolo_gwd_data/images/val/3b778c35e.jpg\")\n# hc, dn = run_yolo_headcount(sample)\n# print(hc.sum(), dn.mean())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T17:26:33.412668Z","iopub.execute_input":"2025-11-27T17:26:33.413048Z","iopub.status.idle":"2025-11-27T17:26:33.459651Z","shell.execute_reply.started":"2025-11-27T17:26:33.413026Z","shell.execute_reply":"2025-11-27T17:26:33.459048Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 5: Drone Speed & FPS Calculator\n# ------------------------------------------------------------\n# 1. Name:\n#    Drone Speed & FPS Calculator\n#\n# 2. Function:\n#    - Relate ground swath width, required overlap, FPS and drone speed.\n#\n# 3. Inputs / Outputs (typical example):\n#    - Input:\n#        W_ground_m = 30.0   # metres covered by one image on ground\n#        fps = 5.0           # frames per second\n#    - Output:\n#        max_speed_for_fps(...) → v_max ≈ 60 m/s  (for 60% overlap)\n#\n# 4. Hyperparameters:\n#    - TARGET_OVERLAP (global) = 0.6   # 60% forward overlap\n#\n# 5. Assumptions / thresholds:\n#    - Straight-line flight, constant speed, rectangular FOV.\n#    - Ignores turn-arounds, wind, and camera motion blur limits.\n# ============================================================\n\ndef max_speed_for_fps(W_ground_m: float, fps: float, overlap: float = TARGET_OVERLAP):\n    \"\"\"\n    Maximum drone speed for given ground swath and FPS\n    so that forward overlap ≥ overlap.\n    \"\"\"\n    step_allowed = (1.0 - overlap) * W_ground_m   # ground distance between frames\n    return step_allowed * fps                     # v_max = step * fps\n\n\ndef min_fps_for_speed(W_ground_m: float, speed_m_s: float, overlap: float = TARGET_OVERLAP):\n    \"\"\"\n    Minimum FPS required for given speed and overlap.\n    \"\"\"\n    step_allowed = (1.0 - overlap) * W_ground_m\n    return speed_m_s / step_allowed               # fps_min = v / step\n\n\n# --- Tiny demo so the cell prints something ---\nW_ground_example = 30.0   # metres\nfps_example = 5.0         # Hz\nspeed_example = 12.0      # m/s\n\nprint(f\"Max speed at {fps_example} FPS over {W_ground_example} m swath:\",\n      f\"{max_speed_for_fps(W_ground_example, fps_example):.2f} m/s\")\n\nprint(f\"Min FPS at {speed_example} m/s over {W_ground_example} m swath:\",\n      f\"{min_fps_for_speed(W_ground_example, speed_example):.2f} FPS\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T17:28:30.951232Z","iopub.execute_input":"2025-11-27T17:28:30.951927Z","iopub.status.idle":"2025-11-27T17:28:30.958041Z","shell.execute_reply.started":"2025-11-27T17:28:30.951902Z","shell.execute_reply":"2025-11-27T17:28:30.957214Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 6: Zoom Decision from Tile Headcounts\n# ------------------------------------------------------------\n# 1. Name:\n#    Zoom Decision from Tile Headcounts\n#\n# 2. Function:\n#    - Decide which grid tiles need a zoomed / closer image\n#      based on low wheat-head counts.\n#\n# 3. Inputs / Outputs (typical example):\n#    - Input:\n#        head_counts.shape = (10, 10)\n#        HEADCOUNT_TILE_THR = 5\n#    - Output:\n#        zoom_tiles = [(i, j), ...]  where head_counts[i, j] < 5\n#\n# 4. Hyperparameters:\n#    - HEADCOUNT_TILE_THR (global) = 5\n#\n# 5. Assumptions / thresholds:\n#    - Each (i, j) tile corresponds to a ground region where the\n#      drone can either zoom, lower altitude, or mark for a second pass.\n# ============================================================\n\ndef get_zoom_tiles(head_counts: np.ndarray, head_thr: float = HEADCOUNT_TILE_THR):\n    \"\"\"\n    Return list of (i, j) tile indices where head_counts < head_thr.\n    \"\"\"\n    zoom_mask = head_counts < head_thr\n    tiles = list(zip(*np.where(zoom_mask)))\n    return tiles\n\n\n# --- Tiny demo so the cell prints something ---\n# Fake headcount map for illustration\ndemo_head_counts = np.random.randint(0, 10, size=(GRID_H, GRID_W)).astype(float)\ndemo_zoom_tiles = get_zoom_tiles(demo_head_counts)\n\nprint(\"Demo head_counts (10x10):\")\nprint(demo_head_counts)\nprint(\"\\nTiles flagged for zoom (i, j) with head_count <\",\n      HEADCOUNT_TILE_THR, \":\\n\", demo_zoom_tiles)\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T17:31:44.420561Z","iopub.execute_input":"2025-11-27T17:31:44.421075Z","iopub.status.idle":"2025-11-27T17:31:44.427265Z","shell.execute_reply.started":"2025-11-27T17:31:44.421049Z","shell.execute_reply":"2025-11-27T17:31:44.426408Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 7: LCDCD 2020 Disease Dataset Loader (robust auto-split)\n# ------------------------------------------------------------\n# 1. Name:\n#    LCDCD DiseaseDataset Loader\n#\n# 2. Function:\n#    - Start at /kaggle/input/lcdcd-2020-dataset.\n#    - If there's a single nested folder (e.g. \"Large Wheat Disease Classification Dataset\"),\n#      treat THAT as the actual root.\n#    - If root has train/val → load directly.\n#    - Else → treat immediate subfolders as classes and build 80/20 split\n#      in /kaggle/working/lcdcd_split.\n# ============================================================\n\nfrom pathlib import Path\nimport os, shutil, random\nfrom torchvision.datasets import ImageFolder\nfrom torchvision import transforms\nfrom torch.utils.data import DataLoader\n\nBASE_ROOT        = Path(\"/kaggle/input/lcdcd-2020-dataset\")\nWORK_SPLIT_ROOT  = Path(\"/kaggle/working/lcdcd_split\")\n\nIMG_SIZE_DISEASE = 224\nBATCH_DISEASE    = 32\nTRAIN_FRACTION   = 0.8\n\ndisease_transform_train = transforms.Compose([\n    transforms.Resize((IMG_SIZE_DISEASE, IMG_SIZE_DISEASE)),\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomVerticalFlip(),\n    transforms.ToTensor(),\n])\n\ndisease_transform_val = transforms.Compose([\n    transforms.Resize((IMG_SIZE_DISEASE, IMG_SIZE_DISEASE)),\n    transforms.ToTensor(),\n])\n\ndisease_train_ds = disease_val_ds = None\ndisease_train_loader = disease_val_loader = None\n\nif not BASE_ROOT.exists():\n    print(\"❌ BASE_ROOT does NOT exist:\", BASE_ROOT)\n\nelse:\n    # --- Step 1: find the actual root (handle one-level nesting) ---\n    root = BASE_ROOT\n    lvl1_dirs = [d for d in root.iterdir() if d.is_dir()]\n    if not (root / \"train\").exists() and not (root / \"val\").exists():\n        if len(lvl1_dirs) == 1:\n            # e.g. root / \"Large Wheat Disease Classification Dataset\"\n            root = lvl1_dirs[0]\n            print(\"Using nested root:\", root)\n    DATA_ROOT_DISEASE = root  # for later cells if needed\n\n    train_dir = root / \"train\"\n    val_dir   = root / \"val\"\n\n    # ---------- Case 1: already split ----------\n    if train_dir.exists() and val_dir.exists():\n        disease_train_ds = ImageFolder(train_dir, transform=disease_transform_train)\n        disease_val_ds   = ImageFolder(val_dir,   transform=disease_transform_val)\n\n        disease_train_loader = DataLoader(disease_train_ds, batch_size=BATCH_DISEASE,\n                                          shuffle=True, num_workers=2)\n        disease_val_loader   = DataLoader(disease_val_ds,   batch_size=BATCH_DISEASE,\n                                          shuffle=False, num_workers=2)\n\n        print(\"✅ LCDCD dataset loaded with existing train/val split.\")\n        print(\"  Root        :\", root)\n        print(\"  Train samples:\", len(disease_train_ds))\n        print(\"  Val samples  :\", len(disease_val_ds))\n        print(\"  Classes      :\", disease_train_ds.classes)\n\n    # ---------- Case 2: auto-split classes under root ----------\n    else:\n        print(\"No train/val folders under:\", root)\n        class_dirs = [d for d in root.iterdir() if d.is_dir()]\n        if not class_dirs:\n            print(\"❌ No class subfolders found under:\", root)\n        else:\n            print(\"Auto-splitting these class folders:\")\n            for d in class_dirs:\n                print(\"  -\", d.name)\n\n            # Clean working split dir\n            if WORK_SPLIT_ROOT.exists():\n                shutil.rmtree(WORK_SPLIT_ROOT)\n            (WORK_SPLIT_ROOT / \"train\").mkdir(parents=True, exist_ok=True)\n            (WORK_SPLIT_ROOT / \"val\").mkdir(parents=True, exist_ok=True)\n\n            random.seed(42)\n\n            for cdir in class_dirs:\n                cname = cdir.name\n                img_files = [f for f in cdir.iterdir() if f.is_file()]\n                if not img_files:\n                    # skip weird empty folders\n                    print(\"    (warning: no images in class folder\", cname, \")\")\n                    continue\n\n                random.shuffle(img_files)\n                k = int(TRAIN_FRACTION * len(img_files))\n                train_files = img_files[:k]\n                val_files   = img_files[k:]\n\n                (WORK_SPLIT_ROOT / \"train\" / cname).mkdir(parents=True, exist_ok=True)\n                (WORK_SPLIT_ROOT / \"val\"   / cname).mkdir(parents=True, exist_ok=True)\n\n                for src in train_files:\n                    shutil.copy2(src, WORK_SPLIT_ROOT / \"train\" / cname / src.name)\n                for src in val_files:\n                    shutil.copy2(src, WORK_SPLIT_ROOT / \"val\" / cname / src.name)\n\n            split_train_dir = WORK_SPLIT_ROOT / \"train\"\n            split_val_dir   = WORK_SPLIT_ROOT / \"val\"\n\n            disease_train_ds = ImageFolder(split_train_dir, transform=disease_transform_train)\n            disease_val_ds   = ImageFolder(split_val_dir,   transform=disease_transform_val)\n\n            disease_train_loader = DataLoader(disease_train_ds, batch_size=BATCH_DISEASE,\n                                              shuffle=True, num_workers=2)\n            disease_val_loader   = DataLoader(disease_val_ds,   batch_size=BATCH_DISEASE,\n                                              shuffle=False, num_workers=2)\n\n            print(\"\\n✅ Built 80/20 split at:\", WORK_SPLIT_ROOT)\n            print(\"  Train samples:\", len(disease_train_ds))\n            print(\"  Val samples  :\", len(disease_val_ds))\n            print(\"  Classes      :\", disease_train_ds.classes)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T18:15:21.203125Z","iopub.execute_input":"2025-11-27T18:15:21.203493Z","iopub.status.idle":"2025-11-27T18:15:25.196515Z","shell.execute_reply.started":"2025-11-27T18:15:21.20347Z","shell.execute_reply":"2025-11-27T18:15:25.195858Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Train samples:\", len(disease_train_ds))\nprint(\"Val samples  :\", len(disease_val_ds))\nprint(\"Classes      :\", disease_train_ds.classes)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T18:15:34.483133Z","iopub.execute_input":"2025-11-27T18:15:34.483826Z","iopub.status.idle":"2025-11-27T18:15:34.487728Z","shell.execute_reply.started":"2025-11-27T18:15:34.4838Z","shell.execute_reply":"2025-11-27T18:15:34.487118Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 8: Disease Classifier Training (ResNet18 on LCDCD)\n# ============================================================\n\n# 1. Name:\n#    LCDCD Disease Classifier (ResNet18)\n#\n# 2. Function:\n#    - Build a ResNet18-based CNN for disease classification.\n#    - Train it on the LCDCD 2020 auto-split train/val sets.\n#    - Save the best-performing model weights.\n#\n# 3. Inputs / Outputs (typical example):\n#    - Inputs:\n#        • disease_train_loader, disease_val_loader  (from Cell 7)\n#        • DEVICE  (\"cuda\" or \"cpu\")\n#    - Outputs:\n#        • disease_model: trained ResNet18 model\n#        • Saved weights file:\n#              OUTPUT_ROOT / \"disease_resnet18_best.pth\"\n#\n# 4. Hyperparameters:\n#    - LR_DISEASE     = 1e-4      # learning rate for Adam\n#    - EPOCHS_DISEASE = 10        # training epochs (increase for better accuracy)\n#    - BATCH_DISEASE  = 32        # batch size (defined in Cell 7)\n#\n# 5. Assumptions / Thresholds:\n#    - disease_train_ds and disease_val_ds are not None (Cell 7 ran successfully).\n#    - disease_train_ds.classes contains labels:\n#          [\"Healthy Wheat\", \"Wheat Loose Smut\", \"Leaf Rust\", \"Crown and Root Rot\"]\n#      or similar class names.\n#    - OUTPUT_ROOT is a valid Path where models can be saved.\n# ============================================================\n\n\n\nimport torch\nfrom torch import nn\nfrom torchvision import models\n\nLR_DISEASE     = 1e-4\nEPOCHS_DISEASE = 10   # bump if you want\n# BATCH_DISEASE already defined in Cell 7\n\ndef build_disease_model(num_classes: int):\n    model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)\n    in_features = model.fc.in_features\n    model.fc = nn.Linear(in_features, num_classes)\n    return model.to(DEVICE)\n\ndef train_disease_model(model, train_loader, val_loader, epochs: int = EPOCHS_DISEASE):\n    criterion = nn.CrossEntropyLoss()\n    optimizer = torch.optim.Adam(model.parameters(), lr=LR_DISEASE)\n\n    best_val_acc, best_state = 0.0, None\n\n    for ep in range(epochs):\n        # ---- Train ----\n        model.train()\n        total, correct, running_loss = 0, 0, 0.0\n        for imgs, labels in train_loader:\n            imgs, labels = imgs.to(DEVICE), labels.to(DEVICE)\n            optimizer.zero_grad()\n            logits = model(imgs)\n            loss = criterion(logits, labels)\n            loss.backward()\n            optimizer.step()\n\n            running_loss += loss.item() * labels.size(0)\n            preds = logits.argmax(1)\n            total += labels.size(0)\n            correct += (preds == labels).sum().item()\n        train_acc  = correct / total if total else 0.0\n        train_loss = running_loss / total if total else 0.0\n\n        # ---- Val ----\n        model.eval()\n        total_v, correct_v = 0, 0\n        with torch.no_grad():\n            for imgs, labels in val_loader:\n                imgs, labels = imgs.to(DEVICE), labels.to(DEVICE)\n                logits = model(imgs)\n                preds = logits.argmax(1)\n                total_v += labels.size(0)\n                correct_v += (preds == labels).sum().item()\n        val_acc = correct_v / total_v if total_v else 0.0\n\n        print(f\"Epoch {ep+1}/{epochs}: \"\n              f\"train_loss={train_loss:.4f}, train_acc={train_acc:.3f}, val_acc={val_acc:.3f}\")\n\n        if val_acc > best_val_acc:\n            best_val_acc = val_acc\n            best_state   = model.state_dict()\n\n    if best_state is not None:\n        model.load_state_dict(best_state)\n    print(f\"Best val_acc: {best_val_acc:.3f}\")\n    return model\n\n# ---- Run training if loaders exist ----\ndisease_model = None\nif disease_train_loader is None or disease_val_loader is None:\n    print(\"⚠️ Disease loaders not available – run Cell 7 successfully first.\")\nelse:\n    num_classes = len(disease_train_ds.classes)\n    print(\"Building disease model with\", num_classes, \"classes:\",\n          disease_train_ds.classes)\n    disease_model = build_disease_model(num_classes)\n    disease_model = train_disease_model(disease_model,\n                                        disease_train_loader,\n                                        disease_val_loader,\n                                        epochs=EPOCHS_DISEASE)\n    OUTPUT_ROOT.mkdir(parents=True, exist_ok=True)\n    save_path = OUTPUT_ROOT / \"disease_resnet18_best.pth\"\n    torch.save(disease_model.state_dict(), save_path)\n    print(\"✅ Saved best disease model to:\", save_path)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T18:27:16.951221Z","iopub.execute_input":"2025-11-27T18:27:16.952264Z","iopub.status.idle":"2025-11-27T18:27:23.485202Z","shell.execute_reply.started":"2025-11-27T18:27:16.952226Z","shell.execute_reply":"2025-11-27T18:27:23.483642Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 9: Recommendation Engine (Headcount + Disease)\n# ------------------------------------------------------------\n# 1. Name:\n#    Wheat Field Recommendation Engine\n#\n# 2. Function:\n#    - Combine:\n#        (a) tile-wise wheat-head density from YOLO\n#        (b) disease class from the LCDCD ResNet18 model\n#      into a simple action recommendation.\n#\n# 3. Inputs / Outputs (typical example):\n#    - Inputs:\n#        head_counts: 2D np.ndarray, e.g. (10, 10)\n#        density_norm: same shape, normalized [0,1]\n#        disease_label: string, e.g. \"Leaf Rust\" / \"Healthy Wheat\" / None\n#        tile_idx: optional (i, j)\n#    - Output:\n#        dict with fields: tile, total_heads, mean_density_norm,\n#        disease, action, priority, reason.\n#\n# 4. Hyperparameters:\n#    - LOW_DENSITY_THR       = 0.4\n#    - HIGH_SEVERITY_CLASSES = {\"Leaf Rust\",\n#                               \"Wheat Loose Smut\",\n#                               \"Crown and Root Rot\"}\n#\n# 5. Assumptions / Thresholds:\n#    - disease_label matches disease_train_ds.classes or is None.\n#    - head_counts / density_norm computed on same grid (e.g. 10×10).\n# ============================================================\n\nLOW_DENSITY_THR       = 0.4\nHIGH_SEVERITY_CLASSES = {\n    \"Leaf Rust\",\n    \"Wheat Loose Smut\",\n    \"Crown and Root Rot\",\n}\n\ndef make_recommendation(\n    head_counts: np.ndarray,\n    density_norm: np.ndarray,\n    disease_label: str | None,\n    tile_idx: tuple[int, int] | None = None,\n):\n    \"\"\"Rule-based recommendation combining density + disease label.\"\"\"\n    mean_density = float(density_norm.mean())\n    total_heads  = float(head_counts.sum())\n\n    info = {\n        \"tile\": tile_idx,\n        \"total_heads\": total_heads,\n        \"mean_density_norm\": mean_density,\n        \"disease\": disease_label,\n        \"action\": \"monitor\",\n        \"priority\": \"low\",\n        \"reason\": \"\",\n    }\n\n    # 1) Severe diseases → high priority treatment\n    if disease_label in HIGH_SEVERITY_CLASSES:\n        info[\"action\"]   = \"apply_fungicide\"\n        info[\"priority\"] = \"high\"\n        info[\"reason\"]   = f\"Detected {disease_label} with visible symptoms; treat urgently.\"\n        return info\n\n    # 2) Low density, no specific disease → agronomic stress\n    if mean_density < LOW_DENSITY_THR and disease_label is None:\n        info[\"action\"]   = \"check_soil_and_irrigation\"\n        info[\"priority\"] = \"medium\"\n        info[\"reason\"]   = \"Low head density without clear disease; possible nutrient/water stress.\"\n        return info\n\n    # 3) Healthy / adequate density → no immediate action\n    if mean_density >= LOW_DENSITY_THR and (disease_label is None or \"Healthy\" in disease_label):\n        info[\"action\"]   = \"no_immediate_action\"\n        info[\"priority\"] = \"low\"\n        info[\"reason\"]   = \"Adequate head density and no serious disease detected.\"\n        return info\n\n    # 4) Fallback: weird mixed case\n    info[\"action\"]   = \"manual_inspection\"\n    info[\"priority\"] = \"medium\"\n    info[\"reason\"]   = \"Mixed signals; recommend manual scouting in this region.\"\n    return info\n\n\n# -------- Tiny demo so the cell actually prints something --------\n_demo_hc = np.random.randint(0, 10, size=(GRID_H, GRID_W)).astype(float)\n_demo_dn = _demo_hc / (_demo_hc.max() + 1e-6)\n\n_demo_rec = make_recommendation(\n    head_counts=_demo_hc,\n    density_norm=_demo_dn,\n    disease_label=\"Leaf Rust\",\n    tile_idx=(4, 5),\n)\n\nprint(\"Demo recommendation (typical data):\")\nfor k, v in _demo_rec.items():\n    print(f\"  {k}: {v}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T18:40:02.638566Z","iopub.execute_input":"2025-11-27T18:40:02.639091Z","iopub.status.idle":"2025-11-27T18:40:02.648673Z","shell.execute_reply.started":"2025-11-27T18:40:02.639064Z","shell.execute_reply":"2025-11-27T18:40:02.647867Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 10: Mini End-to-End Demo for One Image\n# ------------------------------------------------------------\n# 1. Name:\n#    Single-Image / Single-Tile Demo\n#\n# 2. Function:\n#    - Take one wheat val image.\n#    - Run YOLO to get head_counts + density_norm.\n#    - Take one LCDCD disease val sample.\n#    - Run ResNet18 to get a disease_label.\n#    - Feed both into make_recommendation() and print the result.\n#\n# 3. Inputs / Outputs (typical example):\n#    - Inputs:\n#        head_yolo      (from Cell 4)\n#        run_yolo_headcount()\n#        disease_model  (from Cell 8)\n#        disease_val_ds (from Cell 7)\n#        WORK_DIR, GRID_H, GRID_W\n#    - Output:\n#        Printed summary:\n#          • chosen wheat image + total heads\n#          • chosen disease class\n#          • recommendation dict\n#\n# 4. Hyperparameters:\n#    - Uses global GRID_H, GRID_W for tile size.\n#\n# 5. Assumptions / Thresholds:\n#    - YOLO training (Cell 3) and weights load (Cell 4) finished successfully.\n#    - Disease model training (Cell 8) finished successfully.\n# ============================================================\n\nif head_yolo is None:\n    print(\"⚠️ YOLO model not loaded – run training + Cell 4 first.\")\nelif disease_model is None:\n    print(\"⚠️ Disease model not trained – run Cell 8 first.\")\nelse:\n    # ---- 1) Pick one wheat validation image ----\n    wheat_val_img_dir = Path(WORK_DIR) / \"images\" / \"val\"\n    wheat_val_imgs = sorted(wheat_val_img_dir.glob(\"*.jpg\"))\n    if not wheat_val_imgs:\n        print(\"❌ No wheat val images found in\", wheat_val_img_dir)\n    else:\n        wheat_img = wheat_val_imgs[0]\n        hc, dn = run_yolo_headcount(wheat_img)\n        print(\"Using wheat image:\", wheat_img.name)\n        print(\"Total heads (YOLO):\", hc.sum())\n\n        # ---- 2) Pick one disease validation image ----\n        if disease_val_ds is None or len(disease_val_ds) == 0:\n            print(\"❌ disease_val_ds is empty – check Cell 7.\")\n        else:\n            img_d, label_d = disease_val_ds[0]\n            disease_model.eval()\n            with torch.no_grad():\n                logits = disease_model(img_d.unsqueeze(0).to(DEVICE))\n                pred_idx = logits.argmax(1).item()\n            disease_label = disease_val_ds.classes[pred_idx]\n            print(\"Disease prediction (sample from val set):\", disease_label)\n\n            # ---- 3) Get recommendation ----\n            rec = make_recommendation(hc, dn, disease_label=disease_label, tile_idx=None)\n\n            print(\"\\nRecommendation dict:\")\n            for k, v in rec.items():\n                print(f\"  {k}: {v}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T18:33:30.041942Z","iopub.execute_input":"2025-11-27T18:33:30.04252Z","iopub.status.idle":"2025-11-27T18:33:30.16062Z","shell.execute_reply.started":"2025-11-27T18:33:30.042496Z","shell.execute_reply":"2025-11-27T18:33:30.160023Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 10: End-to-End Demo + GT Comparison & Accuracy\n# ------------------------------------------------------------\n# 1. Name:\n#    End-to-End Check (Yield + Disease Accuracy)\n#\n# 2. Function:\n#    - Yield part:\n#        * Pick one wheat val image.\n#        * Build GT 10×10 headcount map from Global Wheat train.csv.\n#        * Run YOLO to get predicted 10×10 headcount map.\n#        * Compare: total heads + tile-wise MAE / RMSE.\n#    - Disease part:\n#        * Run disease_model on full LCDCD val set.\n#        * Report classification accuracy.\n#        * Show one example with true vs predicted label.\n#\n# 3. Inputs / Outputs (typical example):\n#    - Inputs:\n#        • df (Global Wheat train.csv dataframe)\n#        • WORK_DIR / images/val (YOLO val images)\n#        • head_yolo, run_yolo_headcount()\n#        • disease_model, disease_val_loader, disease_val_ds\n#    - Outputs (printed):\n#        • Yield GT vs YOLO:\n#            - GT total heads, predicted total heads\n#            - abs error, relative error\n#            - tile-wise MAE, RMSE\n#        • Disease:\n#            - val accuracy over LCDCD val set\n#            - one example: true label + predicted label\n#\n# 4. Hyperparameters:\n#    - Uses GRID_H, GRID_W (tile grid, e.g. 10×10)\n#\n# 5. Assumptions / Thresholds:\n#    - Global Wheat df has columns: image_id, bbox, width, height.\n#    - Validation images in WORK_DIR / \"images/val\" with names \"<image_id>.jpg\".\n#    - LCDCD pipeline (Cells 7–8) already set up.\n# ============================================================\n\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom pathlib import Path\nfrom PIL import Image\nimport torchvision.transforms.functional as F\nimport torch\n\n# ---------- Helper: build GT 10×10 map from Global Wheat ----------\ndef build_gt_tile_map(image_id: str, grid_h: int = GRID_H, grid_w: int = GRID_W):\n    \"\"\"\n    From df (Global Wheat), build GT headcount map (grid_h × grid_w) for a given image_id.\n    \"\"\"\n    rows = df[df[\"image_id\"] == image_id]\n    if rows.empty:\n        raise ValueError(f\"No GT rows found for image_id={image_id}\")\n\n    # image size from csv if present, else we infer from actual image\n    if \"width\" in rows.columns and \"height\" in rows.columns:\n        W = float(rows[\"width\"].iloc[0])\n        H = float(rows[\"height\"].iloc[0])\n    else:\n        raise ValueError(\"df does not contain width/height columns; cannot tile GT.\")\n\n    gt_map = np.zeros((grid_h, grid_w), dtype=np.float32)\n    for s in rows[\"bbox\"]:\n        x, y, w, h = eval(s)\n        cx = x + w / 2.0\n        cy = y + h / 2.0\n        j = min(int(cx / W * grid_w), grid_w - 1)\n        i = min(int(cy / H * grid_h), grid_h - 1)\n        gt_map[i, j] += 1.0\n    return gt_map\n\n# ---------- Part A: Yield – YOLO vs GT on one wheat image ----------\nprint(\"=== Part A: Yield accuracy on one wheat image ===\")\n\nif head_yolo is None:\n    print(\"⚠️ YOLO model not loaded – run training + Cell 4 first.\")\nelse:\n    wheat_val_img_dir = Path(WORK_DIR) / \"images\" / \"val\"\n    wheat_val_imgs = sorted(wheat_val_img_dir.glob(\"*.jpg\"))\n\n    if not wheat_val_imgs:\n        print(\"❌ No wheat val images found in\", wheat_val_img_dir)\n    else:\n        # pick first val image whose id appears in df\n        chosen_img = None\n        chosen_id  = None\n        for p in wheat_val_imgs:\n            img_id = p.stem\n            if img_id in df[\"image_id\"].values:\n                chosen_img = p\n                chosen_id  = img_id\n                break\n\n        if chosen_img is None:\n            print(\"❌ No val image found that matches df.image_id – check paths.\")\n        else:\n            print(\"Using wheat image:\", chosen_img.name, \" (image_id =\", chosen_id, \")\")\n\n            # GT map\n            gt_map = build_gt_tile_map(chosen_id, GRID_H, GRID_W)\n            gt_total = gt_map.sum()\n\n            # YOLO map\n            pred_counts, pred_norm = run_yolo_headcount(chosen_img, GRID_H, GRID_W)\n            pred_total = pred_counts.sum()\n\n            # Metrics\n            diff = pred_counts - gt_map\n            mae  = np.mean(np.abs(diff))\n            rmse = np.sqrt(np.mean(diff**2))\n\n            if gt_total > 0:\n                rel_err = abs(pred_total - gt_total) / gt_total\n            else:\n                rel_err = np.nan\n\n            print(f\"GT total heads       : {gt_total:.0f}\")\n            print(f\"Predicted total heads: {pred_total:.0f}\")\n            print(f\"Absolute error       : {abs(pred_total - gt_total):.0f}\")\n            print(f\"Relative error       : {rel_err*100:.2f}%\")\n            print(f\"Tile-wise MAE        : {mae:.3f}\")\n            print(f\"Tile-wise RMSE       : {rmse:.3f}\")\n\n            # Optional: visualize GT vs YOLO maps\n            img = Image.open(chosen_img).convert(\"RGB\")\n            plt.figure(figsize=(15, 4))\n\n            plt.subplot(1, 3, 1)\n            plt.imshow(img)\n            plt.axis(\"off\")\n            plt.title(chosen_img.name)\n\n            plt.subplot(1, 3, 2)\n            plt.imshow(gt_map / (gt_map.max() + 1e-6), cmap=\"viridis\")\n            plt.colorbar()\n            plt.title(\"GT head density (10×10)\")\n\n            plt.subplot(1, 3, 3)\n            plt.imshow(pred_norm, cmap=\"viridis\")\n            plt.colorbar()\n            plt.title(\"YOLO head density (10×10)\")\n\n            plt.tight_layout()\n            plt.show()\n\n# ---------- Part B: Disease – final val accuracy on LCDCD ----------\nprint(\"\\n=== Part B: Disease classification accuracy (LCDCD val set) ===\")\n\nif disease_model is None:\n    print(\"⚠️ Disease model not trained – run Cell 8 first.\")\nelif disease_val_loader is None or disease_val_ds is None or len(disease_val_ds) == 0:\n    print(\"⚠️ disease_val_loader / disease_val_ds not available – check Cell 7.\")\nelse:\n    disease_model.eval()\n    total_v, correct_v = 0, 0\n    all_preds, all_labels = [], []\n\n    with torch.no_grad():\n        for imgs, labels in disease_val_loader:\n            imgs, labels = imgs.to(DEVICE), labels.to(DEVICE)\n            logits = disease_model(imgs)\n            preds = logits.argmax(1)\n            total_v += labels.size(0)\n            correct_v += (preds == labels).sum().item()\n            all_preds.extend(preds.cpu().tolist())\n            all_labels.extend(labels.cpu().tolist())\n\n    val_acc = correct_v / total_v if total_v else 0.0\n    print(f\"Validation accuracy on LCDCD val set: {val_acc*100:.2f}%\")\n    print(\"Val samples:\", total_v)\n\n    # Show one example with true vs predicted\n    idx = 0\n    img_d, label_true = disease_val_ds[idx]\n    with torch.no_grad():\n        logits = disease_model(img_d.unsqueeze(0).to(DEVICE))\n        pred_idx = logits.argmax(1).item()\n\n    true_label = disease_val_ds.classes[label_true]\n    pred_label = disease_val_ds.classes[pred_idx]\n    print(f\"\\nExample val sample idx={idx}:\")\n    print(\"  True label :\", true_label)\n    print(\"  Pred label:\", pred_label)\n\n    plt.figure(figsize=(3, 3))\n    plt.imshow(F.to_pil_image(img_d))\n    plt.axis(\"off\")\n    plt.title(f\"Pred: {pred_label}\\nTrue: {true_label}\")\n    plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T18:55:54.216531Z","iopub.execute_input":"2025-11-27T18:55:54.217367Z","iopub.status.idle":"2025-11-27T18:56:06.277817Z","shell.execute_reply.started":"2025-11-27T18:55:54.217339Z","shell.execute_reply":"2025-11-27T18:56:06.27668Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 11: Custom Yield Image – YOLO Headcount + 10×10 Heatmap\n# ------------------------------------------------------------\n# 1. Name:\n#    Custom Yield Inference (Your Own Field Image)\n#\n# 2. Function:\n#    - Take your own RGB field image (top-down or oblique).\n#    - Run YOLO head_yolo on it.\n#    - Compute 10×10 tile-wise head_counts and density_norm.\n#    - Show the image + heatmap and print total headcount.\n#\n# 3. Inputs / Outputs (example):\n#    - Input:\n#        CUSTOM_YIELD_IMAGE = \"/kaggle/working/my_inputs/my_yield_image.jpg\"\n#    - Output:\n#        • Printed total_heads (approx yield proxy)\n#        • 10×10 density_heatmap for that image.\n#\n# 4. Hyperparameters:\n#    - Uses global GRID_H, GRID_W (typically 10×10).\n#    - Uses CONF_THR, IOU_THR from YOLO inference.\n#\n# 5. Assumptions / Thresholds:\n#    - Image is reasonably similar to training data (wheat heads visible).\n#    - head_yolo and run_yolo_headcount() exist (Cells 3–4 done).\n# ============================================================\n\nfrom pathlib import Path\nimport matplotlib.pyplot as plt\n\n# TODO: CHANGE THIS PATH TO YOUR IMAGE\nCUSTOM_YIELD_IMAGE = Path(\"/kaggle/input/wheats/Wheats.jpg\")\n\nif not CUSTOM_YIELD_IMAGE.exists():\n    print(\"❌ Custom yield image not found at:\", CUSTOM_YIELD_IMAGE)\n    print(\"→ Upload your image to this path or change CUSTOM_YIELD_IMAGE.\")\nelif head_yolo is None:\n    print(\"⚠️ YOLO model not loaded – train + run Cell 4 first.\")\nelse:\n    # Run YOLO headcount on your image\n    hc, dn = run_yolo_headcount(CUSTOM_YIELD_IMAGE)\n    print(\"Custom yield image:\", CUSTOM_YIELD_IMAGE)\n    print(\"Total heads (YOLO):\", hc.sum())\n\n    # Show image + heatmap\n    img = Image.open(CUSTOM_YIELD_IMAGE).convert(\"RGB\")\n    plt.figure(figsize=(10, 4))\n\n    plt.subplot(1, 2, 1)\n    plt.imshow(img)\n    plt.axis(\"off\")\n    plt.title(CUSTOM_YIELD_IMAGE.name)\n\n    plt.subplot(1, 2, 2)\n    plt.imshow(dn, cmap=\"viridis\")\n    plt.colorbar()\n    plt.title(\"YOLO head density (10×10)\")\n\n    plt.tight_layout()\n    plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T18:46:40.789779Z","iopub.execute_input":"2025-11-27T18:46:40.790564Z","iopub.status.idle":"2025-11-27T18:46:41.270371Z","shell.execute_reply.started":"2025-11-27T18:46:40.790531Z","shell.execute_reply":"2025-11-27T18:46:41.269576Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 12: Custom Disease Image – LCDCD ResNet18 Prediction\n# ------------------------------------------------------------\n# 1. Name:\n#    Custom Disease Inference (Your Own Close-up Image)\n#\n# 2. Function:\n#    - Take your own close-up wheat image (leaf / spike).\n#    - Apply same validation transform as LCDCD (resize + tensor).\n#    - Run the trained ResNet18 disease_model on it.\n#    - Print predicted disease class and show the image.\n#\n# 3. Inputs / Outputs (example):\n#    - Input:\n#        CUSTOM_DISEASE_IMAGE = \"/kaggle/working/my_inputs/my_disease_image.jpg\"\n#    - Output:\n#        • Printed predicted class, e.g. \"Leaf Rust\"\n#        • Image display with predicted label.\n#\n# 4. Hyperparameters:\n#    - Uses IMG_SIZE_DISEASE = 224 and disease_transform_val from Cell 7.\n#\n# 5. Assumptions / Thresholds:\n#    - disease_model has been trained and loaded (Cell 8 done).\n#    - disease_val_ds.classes matches training classes so we can reuse names.\n# ============================================================\n\nimport matplotlib.pyplot as plt\nimport torchvision.transforms.functional as F\n\n# TODO: CHANGE THIS PATH TO YOUR IMAGE\nCUSTOM_DISEASE_IMAGE = Path(\"/kaggle/input/wheats/Rust.jpg\")\n\nif not CUSTOM_DISEASE_IMAGE.exists():\n    print(\"❌ Custom disease image not found at:\", CUSTOM_DISEASE_IMAGE)\n    print(\"→ Upload your image to this path or change CUSTOM_DISEASE_IMAGE.\")\nelif disease_model is None:\n    print(\"⚠️ Disease model not trained – run Cell 8 first.\")\nelif disease_val_ds is None or len(disease_val_ds) == 0:\n    print(\"⚠️ disease_val_ds is empty – run Cell 7 to load LCDCD dataset.\")\nelse:\n    # Load your image and apply the same val transform\n    img_pil = Image.open(CUSTOM_DISEASE_IMAGE).convert(\"RGB\")\n    img_t   = disease_transform_val(img_pil)  # from Cell 7\n\n    disease_model.eval()\n    with torch.no_grad():\n        logits = disease_model(img_t.unsqueeze(0).to(DEVICE))\n        pred_idx = logits.argmax(1).item()\n\n    pred_label = disease_val_ds.classes[pred_idx]\n\n    print(\"Custom disease image:\", CUSTOM_DISEASE_IMAGE)\n    print(\"Predicted class:\", pred_label)\n\n    plt.figure(figsize=(3, 3))\n    plt.imshow(img_pil)\n    plt.axis(\"off\")\n    plt.title(f\"Predicted: {pred_label}\")\n    plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T18:50:19.799391Z","iopub.execute_input":"2025-11-27T18:50:19.799704Z","iopub.status.idle":"2025-11-27T18:50:19.99811Z","shell.execute_reply.started":"2025-11-27T18:50:19.799681Z","shell.execute_reply":"2025-11-27T18:50:19.997405Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 14: Save All Models & Configs (One-Time Export)\n# ------------------------------------------------------------\n# 1. Name:\n#    Save Artifacts (YOLO + Disease + Configs)\n#\n# 2. Function:\n#    - Collect everything needed to avoid re-training:\n#        * YOLO wheat best weights\n#        * YOLO data.yaml\n#        * Disease ResNet18 weights\n#        * Disease class names\n#    - Put them into a single folder:\n#        /kaggle/working/wheat_edge_ai_artifacts\n#\n# 3. Inputs:\n#    - YOLO weights at: runs/detect/yolov8n_gwd_wheat/weights/best.pt\n#    - YOLO data.yaml at: WORK_DIR / \"wheat_data.yaml\"\n#    - Disease weights at: OUTPUT_ROOT / \"disease_resnet18_best.pth\"\n#    - Disease classes from: disease_train_ds or disease_val_ds\n#\n# 4. Outputs:\n#    - /kaggle/working/wheat_edge_ai_artifacts/\n#        yolo_wheat_best.pt\n#        wheat_data.yaml\n#        disease_resnet18_best.pth\n#        disease_classes.txt\n# ============================================================\n\nfrom pathlib import Path\nimport shutil\n\nARTIFACTS_ROOT = Path(\"/kaggle/working/wheat_edge_ai_artifacts\")\nARTIFACTS_ROOT.mkdir(parents=True, exist_ok=True)\n\n# ---- YOLO artifacts ----\nyolo_best_path = Path(\"runs/detect/yolov8n_gwd_wheat/weights/best.pt\")\nyolo_yaml_path = Path(WORK_DIR) / \"wheat_data.yaml\"\n\nif yolo_best_path.exists():\n    shutil.copy2(yolo_best_path, ARTIFACTS_ROOT / \"yolo_wheat_best.pt\")\n    print(\"✅ Copied YOLO weights:\", yolo_best_path)\nelse:\n    print(\"⚠️ YOLO weights not found at:\", yolo_best_path)\n\nif yolo_yaml_path.exists():\n    shutil.copy2(yolo_yaml_path, ARTIFACTS_ROOT / \"wheat_data.yaml\")\n    print(\"✅ Copied YOLO data.yaml:\", yolo_yaml_path)\nelse:\n    print(\"⚠️ YOLO data.yaml not found at:\", yolo_yaml_path)\n\n# ---- Disease artifacts ----\nOUTPUT_ROOT = Path(\"./outputs\")\ndisease_weights_path = OUTPUT_ROOT / \"disease_resnet18_best.pth\"\nif disease_weights_path.exists():\n    shutil.copy2(disease_weights_path, ARTIFACTS_ROOT / \"disease_resnet18_best.pth\")\n    print(\"✅ Copied disease weights:\", disease_weights_path)\nelse:\n    print(\"⚠️ Disease weights not found at:\", disease_weights_path)\n\n# Save disease class names\ndisease_classes = None\nif disease_train_ds is not None:\n    disease_classes = disease_train_ds.classes\nelif disease_val_ds is not None:\n    disease_classes = disease_val_ds.classes\n\nif disease_classes is not None:\n    with open(ARTIFACTS_ROOT / \"disease_classes.txt\", \"w\") as f:\n        for c in disease_classes:\n            f.write(c + \"\\n\")\n    print(\"✅ Saved disease classes:\", disease_classes)\nelse:\n    print(\"⚠️ No disease dataset found; cannot save class names.\")\n\nprint(\"\\nArtifacts folder:\", ARTIFACTS_ROOT)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T18:59:23.517339Z","iopub.execute_input":"2025-11-27T18:59:23.51784Z","iopub.status.idle":"2025-11-27T18:59:23.565115Z","shell.execute_reply.started":"2025-11-27T18:59:23.5178Z","shell.execute_reply":"2025-11-27T18:59:23.564542Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from pathlib import Path\n\nroot = Path(\"/kaggle/working/wheat_edge_ai_artifacts\")\nprint(\"Exists:\", root.exists())\nprint(\"\\nFiles:\")\nfor p in root.iterdir():\n    print(\" \", p.name, \" | \", p.stat().st_size, \"bytes\")\n\nprint(\"\\nDisease classes file:\")\nprint((root / \"disease_classes.txt\").read_text())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T19:02:31.272654Z","iopub.execute_input":"2025-11-27T19:02:31.272946Z","iopub.status.idle":"2025-11-27T19:02:31.278769Z","shell.execute_reply.started":"2025-11-27T19:02:31.272929Z","shell.execute_reply":"2025-11-27T19:02:31.278225Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from pathlib import Path\n\nroot = Path(\"/kaggle/working/wheat_edge_ai_artifacts\")\nprint(\"Exists:\", root.exists())\nfor p in root.iterdir():\n    print(\" \", p.name, \"|\", p.stat().st_size, \"bytes\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T19:04:02.135416Z","iopub.execute_input":"2025-11-27T19:04:02.135945Z","iopub.status.idle":"2025-11-27T19:04:02.140586Z","shell.execute_reply.started":"2025-11-27T19:04:02.135921Z","shell.execute_reply":"2025-11-27T19:04:02.13979Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}