{"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":19989,"databundleVersionId":1160143,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":8216181,"sourceType":"datasetVersion","datasetId":4869911},{"sourceId":175206582,"sourceType":"kernelVersion"}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"from pathlib import Path\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nimport matplotlib.pyplot as plt\n\nDATA_ROOT = Path(\"/kaggle/input/global-wheat-detection\")\nprint(\"Root exists:\", DATA_ROOT.exists())\nprint(\"Contents:\", [p.name for p in DATA_ROOT.iterdir()])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T08:55:48.788071Z","iopub.execute_input":"2025-11-27T08:55:48.788364Z","iopub.status.idle":"2025-11-27T08:55:48.793798Z","shell.execute_reply.started":"2025-11-27T08:55:48.788335Z","shell.execute_reply":"2025-11-27T08:55:48.793139Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_csv = DATA_ROOT / \"train.csv\"\ndf = pd.read_csv(train_csv)\ndf.head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T08:38:50.091664Z","iopub.execute_input":"2025-11-27T08:38:50.091939Z","iopub.status.idle":"2025-11-27T08:38:50.401046Z","shell.execute_reply.started":"2025-11-27T08:38:50.091919Z","shell.execute_reply":"2025-11-27T08:38:50.400275Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# choose one image id\nimage_id = df[\"image_id\"].iloc[0]\nprint(\"Using image_id:\", image_id)\n\n# all rows (boxes) for this image\ndf_img = df[df[\"image_id\"] == image_id].reset_index(drop=True)\nprint(\"Number of boxes:\", len(df_img))\n\n# image path\nimg_path = DATA_ROOT / \"train\" / f\"{image_id}.jpg\"\nprint(\"Image path:\", img_path, \"exists:\", img_path.exists())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T08:38:55.092769Z","iopub.execute_input":"2025-11-27T08:38:55.093038Z","iopub.status.idle":"2025-11-27T08:38:55.119698Z","shell.execute_reply.started":"2025-11-27T08:38:55.093019Z","shell.execute_reply":"2025-11-27T08:38:55.119006Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# load image\nimg = Image.open(img_path).convert(\"RGB\")\nimg_np = np.array(img).astype(np.float32) / 255.0\nH, W, _ = img_np.shape\nprint(\"Image size:\", W, \"x\", H)\n\n\n\n\n\n\n# --- Color filter: keep only green & yellow-ish pixels ---\n\nR = img_np[..., 0]\nG = img_np[..., 1]\nB = img_np[..., 2]\n\n# very simple rules (you can tweak thresholds):\n# green: G is strongest channel and not too dark\nmask_green = (G > R) & (G > B) & (G > 0.25)\n\n# yellow: R and G are both high, B is low\nmask_yellow = (R > 0.4) & (G > 0.4) & (B < 0.3)\n\nmask_plant = mask_green | mask_yellow\n\n# make a copy and zero-out everything that is not plant-colored\nimg_filtered = img_np.copy()\nimg_filtered[~mask_plant] = 0.0\n\n# from here on, use img_filtered instead of img_np\nimg_np = img_filtered\n\n\n\n\n\n\n\n# parse bboxes: each bbox string -> x, y, w, h\nboxes = []\nfor s in df_img[\"bbox\"]:\n    # s looks like \"[x, y, w, h]\"\n    x, y, w, h = eval(s)  # safe here: controlled csv format\n    boxes.append((x, y, w, h))\n\nboxes = np.array(boxes, dtype=np.float32) if boxes else np.zeros((0, 4), dtype=np.float32)\nprint(\"Parsed boxes shape:\", boxes.shape)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T08:39:00.487923Z","iopub.execute_input":"2025-11-27T08:39:00.488606Z","iopub.status.idle":"2025-11-27T08:39:00.593625Z","shell.execute_reply.started":"2025-11-27T08:39:00.488582Z","shell.execute_reply":"2025-11-27T08:39:00.592954Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# choose grid resolution (tune if you like)\nGRID_H, GRID_W = 10, 10\n\nhead_counts = np.zeros((GRID_H, GRID_W), dtype=np.float32)\n\nfor 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)  # column\n    i = min(int(cy / H * GRID_H), GRID_H - 1)  # row\n    head_counts[i, j] += 1.0\n\n# normalize density (1 = max density in this image)\nif head_counts.max() > 0:\n    density_norm = head_counts / head_counts.max()\nelse:\n    density_norm = head_counts\n\ndensity_norm.shape\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T08:39:06.825486Z","iopub.execute_input":"2025-11-27T08:39:06.826011Z","iopub.status.idle":"2025-11-27T08:39:06.833095Z","shell.execute_reply.started":"2025-11-27T08:39:06.825989Z","shell.execute_reply":"2025-11-27T08:39:06.832388Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"##Yellow as Bad\n\n\"\"\"GI = np.zeros((GRID_H, GRID_W), dtype=np.float32)\ncell_h = H // GRID_H\ncell_w = W // GRID_W\n\nfor i in range(GRID_H):\n    for j in range(GRID_W):\n        y0 = i * cell_h\n        y1 = (i + 1) * cell_h if i < GRID_H - 1 else H\n        x0 = j * cell_w\n        x1 = (j + 1) * cell_w if j < GRID_W - 1 else W\n\n        tile = img_np[y0:y1, x0:x1, :]\n        R = tile[..., 0].mean()\n        G = tile[..., 1].mean()\n        B = tile[..., 2].mean()\n        GI[i, j] = G / (R + G + B + 1e-6)\n\nGI_mean = GI.mean()\nGI_diff = np.clip(GI_mean - GI, 0, None)    # >0 where less green (more yellow)\nGI_norm = GI_diff / (GI_diff.max() + 1e-6)\n\n# suspicion: high when density is low & greenness is low\nsuspicion = 0.7 * (1.0 - density_norm) + 0.3 * GI_norm\"\"\"\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T14:30:18.212141Z","iopub.execute_input":"2025-11-25T14:30:18.212346Z","iopub.status.idle":"2025-11-25T14:30:18.302725Z","shell.execute_reply.started":"2025-11-25T14:30:18.212329Z","shell.execute_reply":"2025-11-25T14:30:18.302088Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"## Yellow as Bad only if Lesser heads..\n\nGI = np.zeros((GRID_H, GRID_W), dtype=np.float32)\ncell_h = H // GRID_H\ncell_w = W // GRID_W\n\nfor i in range(GRID_H):\n    for j in range(GRID_W):\n        y0 = i * cell_h\n        y1 = (i + 1) * cell_h if i < GRID_H - 1 else H\n        x0 = j * cell_w\n        x1 = (j + 1) * cell_w if j < GRID_W - 1 else W\n\n        tile = img_np[y0:y1, x0:x1, :]\n        R = tile[..., 0].mean()\n        G = tile[..., 1].mean()\n        B = tile[..., 2].mean()\n        GI[i, j] = G / (R + G + B + 1e-6)\n\nGI_mean = GI.mean()\nGI_diff = np.clip(GI_mean - GI, 0, None)    # >0 where more yellow\nGI_norm = GI_diff / (GI_diff.max() + 1e-6)\n\n# --- NEW PART: yellow only bad when head density is low ---\n# density_norm is already in [0,1]; high density = good crop\ncolor_term = GI_norm.copy()\ncolor_term[density_norm >= 0.5] = 0.0   # yellow ignored where there are many heads\n\n# suspicion: base = few heads, plus extra if yellow in low-head tiles\nbase_susp = 1.0 - density_norm\nsuspicion = 0.7 * base_susp + 0.3 * color_term\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T08:55:58.122298Z","iopub.execute_input":"2025-11-27T08:55:58.122601Z","iopub.status.idle":"2025-11-27T08:55:58.137742Z","shell.execute_reply.started":"2025-11-27T08:55:58.122579Z","shell.execute_reply":"2025-11-27T08:55:58.136990Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport matplotlib.pyplot as plt\nfrom PIL import Image\n\n# --- reload true original image ---\nimg = Image.open(img_path).convert(\"RGB\")\nimg_np_orig = np.array(img).astype(np.float32) / 255.0\nH, W, _ = img_np_orig.shape\n\n# --- grid + head density from GT boxes ---\nGRID_H, GRID_W = 10, 10\ncell_h = H // GRID_H\ncell_w = W // GRID_W\n\nhead_counts = np.zeros((GRID_H, GRID_W), dtype=np.float32)\nfor 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\nif head_counts.max() > 0:\n    density_norm = head_counts / head_counts.max()\nelse:\n    density_norm = head_counts.copy()\n\nbase_susp = 1.0 - density_norm  # few heads ⇒ more suspicious\n\n# ---------- heatmap WITHOUT filter ----------\nGI_unf = np.zeros((GRID_H, GRID_W), dtype=np.float32)\nfor i in range(GRID_H):\n    for j in range(GRID_W):\n        y0 = i * cell_h\n        y1 = (i + 1) * cell_h if i < GRID_H - 1 else H\n        x0 = j * cell_w\n        x1 = (j + 1) * cell_w if j < GRID_W - 1 else W\n\n        tile = img_np_orig[y0:y1, x0:x1, :]\n        Rm = tile[..., 0].mean()\n        Gm = tile[..., 1].mean()\n        Bm = tile[..., 2].mean()\n        GI_unf[i, j] = Gm / (Rm + Gm + Bm + 1e-6)\n\nGI_mean_unf = GI_unf.mean()\nGI_diff_unf = np.clip(GI_mean_unf - GI_unf, 0, None)\nGI_norm_unf = GI_diff_unf / (GI_diff_unf.max() + 1e-6)\n\ncolor_term_unf = GI_norm_unf.copy()\ncolor_term_unf[density_norm >= 0.5] = 0.0   # yellow only bad if few heads\n\nsuspicion_unf = 0.7 * base_susp + 0.3 * color_term_unf\n\n# ---------- build filtered image (green + yellow only) ----------\nR = img_np_filt[..., 0]\nG = img_np_filt[..., 1]\nB = img_np_filt[..., 2]\n\n# green: G clearly strongest\nmask_green = (G > R) & (G > B) & (G > 0.25)\n\n# yellow: R and G both fairly high, similar to each other, and B lower\n'''mask_yellow = (R > 0.3) & (G > 0.3) & (B < 0.6) & (np.abs(R - G) < 0.25)'''\nmask_yellow = (R > 0.25) & (G > 0.2) & (B < 0.8)\n\nmask_plant = mask_green | mask_yellow\n\n\n# grab MORE yellow: lower thresholds and remove the strict R≈G condition\n\n\nmask_plant = mask_green | mask_yellow\n\n\n\n\n\n\n# ---------- heatmap WITH filter ----------\nGI_filt = np.zeros((GRID_H, GRID_W), dtype=np.float32)\nfor i in range(GRID_H):\n    for j in range(GRID_W):\n        y0 = i * cell_h\n        y1 = (i + 1) * cell_h if i < GRID_H - 1 else H\n        x0 = j * cell_w\n        x1 = (j + 1) * cell_w if j < GRID_W - 1 else W\n\n        tile = img_np_filt[y0:y1, x0:x1, :]\n        Rm = tile[..., 0].mean()\n        Gm = tile[..., 1].mean()\n        Bm = tile[..., 2].mean()\n        GI_filt[i, j] = Gm / (Rm + Gm + Bm + 1e-6)\n\nGI_mean_filt = GI_filt.mean()\nGI_diff_filt = np.clip(GI_mean_filt - GI_filt, 0, None)\nGI_norm_filt = GI_diff_filt / (GI_diff_filt.max() + 1e-6)\n\ncolor_term_filt = GI_norm_filt.copy()\ncolor_term_filt[density_norm >= 0.5] = 0.0\n\nsuspicion_filt = 0.7 * base_susp + 0.3 * color_term_filt\n\n# ---------- plot 4 panels ----------\nplt.figure(figsize=(20, 8))\n\n# 1) original image\nplt.subplot(2, 2, 1)\nplt.imshow(img_np_orig)\nplt.title(\"Original image\")\nplt.axis(\"off\")\n\n# 2) heatmap WITHOUT filter\nplt.subplot(2, 2, 2)\nplt.imshow(img_np_orig)\nplt.imshow(\n    suspicion_unf,\n    cmap=\"jet\",\n    alpha=0.45,\n    extent=(0, W, H, 0),\n    interpolation=\"bilinear\",\n)\nplt.title(\"Heatmap WITHOUT filter\")\nplt.axis(\"off\")\n\n# 3) filtered image (green + yellow only)\nplt.subplot(2, 2, 3)\nplt.imshow(img_np_filt)\nplt.title(\"Filtered image (green + yellow kept)\")\nplt.axis(\"off\")\n\n# 4) heatmap WITH filter\nplt.subplot(2, 2, 4)\nplt.imshow(img_np_filt)\nplt.imshow(\n    suspicion_filt,\n    cmap=\"jet\",\n    alpha=0.45,\n    extent=(0, W, H, 0),\n    interpolation=\"bilinear\",\n)\nplt.title(\"Heatmap WITH filter\")\nplt.axis(\"off\")\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T08:56:02.880024Z","iopub.execute_input":"2025-11-27T08:56:02.880633Z","iopub.status.idle":"2025-11-27T08:56:02.940827Z","shell.execute_reply.started":"2025-11-27T08:56:02.880610Z","shell.execute_reply":"2025-11-27T08:56:02.939931Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#HEADCOUNT USING REGRESSION - LIGHT & FAST ====> FOR DRONE","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-26T10:40:40.551531Z","iopub.execute_input":"2025-11-26T10:40:40.551775Z","iopub.status.idle":"2025-11-26T10:40:40.556746Z","shell.execute_reply.started":"2025-11-26T10:40:40.551752Z","shell.execute_reply":"2025-11-26T10:40:40.556116Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# HC1: Build a dataset that returns (image_tensor, 10x10 head-count map) for each wheat image\n\nfrom pathlib import Path\nimport pandas as pd\nimport numpy as np\nfrom PIL import Image\n\nimport torch\nfrom torch.utils.data import Dataset, DataLoader, random_split\nfrom torchvision import transforms\n\n# Global Wheat dataset root + annotations\nDATA_ROOT = Path(\"/kaggle/input/global-wheat-detection\")\ndf_wheat = pd.read_csv(DATA_ROOT / \"train.csv\")\n\n# Use a subset for faster training (you can increase max_images later)\nall_ids = df_wheat[\"image_id\"].unique()\nmax_images = 1200\nimage_ids = all_ids[:max_images]\n\nGRID_H, GRID_W = 10, 10  # we keep the same 10x10 grid\n\nclass WheatHeadCountDataset(Dataset):\n    \"\"\"\n    For each image_id:\n    - load the RGB image\n    - build a 10x10 head-count map using the bbox centres\n    - return (image_tensor, head_map_tensor)\n    \"\"\"\n    def __init__(self, data_root, df, image_ids, grid_h=10, grid_w=10, transform=None):\n        self.data_root = data_root\n        self.df = df\n        self.image_ids = list(image_ids)\n        self.grid_h = grid_h\n        self.grid_w = grid_w\n        self.transform = transform\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.data_root / \"train\" / f\"{image_id}.jpg\"\n        img = Image.open(img_path).convert(\"RGB\")\n        W, H = img.size\n\n        # build 10x10 head-count map (same logic as before using bbox centres)\n        head_map = np.zeros((self.grid_h, self.grid_w), dtype=np.float32)\n        df_img = self.df[self.df[\"image_id\"] == image_id]\n\n        for s in df_img[\"bbox\"]:\n            x, y, w, h = eval(s)  # bbox = [x_min, y_min, w, h] in pixels\n            cx = x + w / 2.0\n            cy = y + h / 2.0\n            j = min(int(cx / W * self.grid_w), self.grid_w - 1)\n            i = min(int(cy / H * self.grid_h), self.grid_h - 1)\n            head_map[i, j] += 1.0  # increment count for that tile\n\n        if self.transform is not None:\n            img_tensor = self.transform(img)\n        else:\n            img_tensor = transforms.ToTensor()(img)\n\n        head_map_tensor = torch.from_numpy(head_map)  # shape [10,10], dtype float32\n\n        return img_tensor, head_map_tensor\n\n# Simple image transform: resize to 320x320, random flip, to tensor\ntransform_hc = transforms.Compose([\n    transforms.Resize((320, 320)),\n    transforms.RandomHorizontalFlip(),\n    transforms.ToTensor(),\n])\n\nfull_hc_ds = WheatHeadCountDataset(\n    DATA_ROOT, df_wheat, image_ids,\n    grid_h=GRID_H, grid_w=GRID_W,\n    transform=transform_hc,\n)\n\n# 80/20 train/val split\ntrain_size = int(0.8 * len(full_hc_ds))\nval_size   = len(full_hc_ds) - train_size\nhc_train_ds, hc_val_ds = random_split(full_hc_ds, [train_size, val_size])\n\nhc_train_loader = DataLoader(hc_train_ds, batch_size=8, shuffle=True,  num_workers=2)\nhc_val_loader   = DataLoader(hc_val_ds,   batch_size=8, shuffle=False, num_workers=2)\n\nprint(\"Head-count dataset size:\", len(full_hc_ds))\nprint(\"Train:\", len(hc_train_ds), \" Val:\", len(hc_val_ds))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T08:39:45.184943Z","iopub.execute_input":"2025-11-27T08:39:45.185801Z","iopub.status.idle":"2025-11-27T08:39:51.372983Z","shell.execute_reply.started":"2025-11-27T08:39:45.185767Z","shell.execute_reply":"2025-11-27T08:39:51.372297Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# HC2: Define a small fully-convolutional network that predicts a 10x10 head-count map from the image\n\nimport torch.nn as nn\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Device for head-count model:\", device)\n\nclass HeadCountNet(nn.Module):\n    \"\"\"\n    Input:   [B, 3, 320, 320] RGB image batch\n    Output:  [B, 1, 10, 10] head-count map (counts per tile)\n\n    We use only conv + pooling so output is a map, not a single number.\n    ReLU at the end keeps predicted counts >= 0.\n    \"\"\"\n    def __init__(self):\n        super().__init__()\n        self.features = nn.Sequential(\n            nn.Conv2d(3, 32, 3, padding=1), nn.ReLU(),\n            nn.MaxPool2d(2),    # 160x160\n\n            nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(),\n            nn.MaxPool2d(2),    # 80x80\n\n            nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(),\n            nn.MaxPool2d(2),    # 40x40\n\n            nn.Conv2d(128, 128, 3, padding=1), nn.ReLU(),\n            nn.MaxPool2d(2),    # 20x20\n\n            nn.Conv2d(128, 128, 3, padding=1), nn.ReLU(),\n            nn.MaxPool2d(2),    # 10x10\n        )\n        # 1x1 conv to convert 128 channels -> 1 head-count channel\n        self.head_map = nn.Conv2d(128, 1, kernel_size=1)\n\n    def forward(self, x):\n        x = self.features(x)           # [B, 128, 10, 10]\n        x = self.head_map(x)           # [B, 1, 10, 10]\n        x = torch.relu(x)              # ReLU: f(z)=max(0,z); keep counts non-negative\n        return x\n\nhc_model = HeadCountNet().to(device)\n\n# MSE (Mean Squared Error) = average of (predicted - true)^2 over all cells\nhc_criterion = nn.MSELoss()\nhc_optimizer = torch.optim.Adam(hc_model.parameters(), lr=1e-3)\n\nprint(hc_model)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T08:39:56.023954Z","iopub.execute_input":"2025-11-27T08:39:56.024695Z","iopub.status.idle":"2025-11-27T08:39:56.227528Z","shell.execute_reply.started":"2025-11-27T08:39:56.024671Z","shell.execute_reply":"2025-11-27T08:39:56.226805Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# HC3 (low-head focused): train with weighted MSE that emphasises low-head tiles\n\ndef weighted_mse(preds, targets, alpha=4.0, low_thresh=1.0):\n    \"\"\"\n    preds, targets: [B, 10, 10]\n    alpha: how much extra weight for low-head tiles\n    low_thresh: tiles with GT < low_thresh are considered 'low head'\n    \"\"\"\n    # base weight = 1\n    weights = torch.ones_like(targets)\n\n    # tiles with few heads in GT\n    low_mask = (targets < low_thresh).float()   # 1 for low-head tiles\n\n    # increase their weight\n    weights = weights + alpha * low_mask        # low tiles -> 1+alpha\n\n    sq_err = (preds - targets) ** 2\n    loss = (sq_err * weights).mean()\n    return loss\n\ndef evaluate_headcount(model, loader, alpha=4.0, low_thresh=1.0):\n    model.eval()\n    total_loss = 0.0\n    total_batches = 0\n    with torch.no_grad():\n        for imgs, maps_gt in loader:\n            imgs = imgs.to(device)\n            maps_gt = maps_gt.to(device)\n            preds = model(imgs).squeeze(1)       # [B,10,10]\n            loss = weighted_mse(preds, maps_gt, alpha=alpha, low_thresh=low_thresh)\n            total_loss += loss.item()\n            total_batches += 1\n    return total_loss / max(total_batches, 1)\n\nnum_epochs = 15\nalpha = 4.0        # importance boost for low-head tiles\nlow_thresh = 1.0   # GT < 1 head -> \"low\"\n\nfor epoch in range(1, num_epochs + 1):\n\n    # simple LR decay\n    if epoch == 6:\n        for g in hc_optimizer.param_groups:\n            g[\"lr\"] *= 0.2\n\n    hc_model.train()\n    running_loss = 0.0\n    batches = 0\n\n    for imgs, maps_gt in hc_train_loader:\n        imgs = imgs.to(device)\n        maps_gt = maps_gt.to(device)\n\n        hc_optimizer.zero_grad()\n        preds = hc_model(imgs).squeeze(1)\n        loss = weighted_mse(preds, maps_gt, alpha=alpha, low_thresh=low_thresh)\n        loss.backward()\n        hc_optimizer.step()\n\n        running_loss += loss.item()\n        batches += 1\n\n    train_loss = running_loss / max(batches, 1)\n    val_loss   = evaluate_headcount(hc_model, hc_val_loader, alpha=alpha, low_thresh=low_thresh)\n    print(f\"Epoch {epoch}: train_loss={train_loss:.4f}, val_loss={val_loss:.4f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T08:40:02.003755Z","iopub.execute_input":"2025-11-27T08:40:02.004397Z","iopub.status.idle":"2025-11-27T08:44:35.043631Z","shell.execute_reply.started":"2025-11-27T08:40:02.004370Z","shell.execute_reply":"2025-11-27T08:44:35.042706Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# HC4: Visual check – show image, true 10x10 head map, and predicted 10x10 head map for one sample\n\nimport matplotlib.pyplot as plt\n\nhc_model.eval()\n\n# pick one sample from validation set\nimg_tensor, head_map_gt = hc_val_ds[1]\nimg_np = img_tensor.permute(1, 2, 0).cpu().numpy()  # [H,W,C] for plotting\n\nwith torch.no_grad():\n    pred_map = hc_model(img_tensor.unsqueeze(0).to(device))\n    pred_map = pred_map.squeeze(0).squeeze(0).cpu().numpy()  # [10,10]\n\ngt_display   = head_map_gt.numpy()\npred_display = pred_map\n\nplt.figure(figsize=(12, 4))\n\nplt.subplot(1, 3, 1)\nplt.imshow(img_np)\nplt.axis(\"off\")\nplt.title(\"Input image (resized)\")\n\nplt.subplot(1, 3, 2)\nplt.imshow(gt_display, cmap=\"viridis\")\nplt.colorbar()\nplt.title(\"Ground-truth head-count map\\n(10x10)\")\n\nplt.subplot(1, 3, 3)\nplt.imshow(pred_display, cmap=\"viridis\")\nplt.colorbar()\nplt.title(\"Predicted head-count map\\n(10x10)\")\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T08:46:22.827594Z","iopub.execute_input":"2025-11-27T08:46:22.828155Z","iopub.status.idle":"2025-11-27T08:46:23.358558Z","shell.execute_reply.started":"2025-11-27T08:46:22.828125Z","shell.execute_reply":"2025-11-27T08:46:23.357746Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# HC5: measure average inference time of the head-count model (per image)\n\nimport time\nimport torch\n\nhc_model.eval()\n\nn_batches = 20          # how many batches to test over (can increase)\ntotal_imgs = 0\nstart = time.time()\n\nwith torch.no_grad():\n    for b, (imgs, maps_gt) in enumerate(hc_val_loader):\n        imgs = imgs.to(device)\n\n        _ = hc_model(imgs)   # forward pass only\n\n        total_imgs += imgs.size(0)\n        if (b + 1) >= n_batches:\n            break\n\nelapsed = time.time() - start\n\nprint(\"Images processed:\", total_imgs)\nprint(f\"Total time: {elapsed:.4f} s\")\nprint(f\"Avg time per image: {elapsed / total_imgs:.4f} s\")\nprint(f\"Approx FPS (images per second): {total_imgs / elapsed:.1f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T08:50:25.255378Z","iopub.execute_input":"2025-11-27T08:50:25.255921Z","iopub.status.idle":"2025-11-27T08:50:27.790341Z","shell.execute_reply.started":"2025-11-27T08:50:25.255897Z","shell.execute_reply":"2025-11-27T08:50:27.789612Z"}},"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":"#YOLO for DRONE","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":"!pip install -q ultralytics\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T08:51:20.385155Z","iopub.execute_input":"2025-11-27T08:51:20.385884Z","iopub.status.idle":"2025-11-27T08:52:36.114296Z","shell.execute_reply.started":"2025-11-27T08:51:20.385856Z","shell.execute_reply":"2025-11-27T08:52:36.113585Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ONE-CELL YOLO PIPELINE ON GLOBAL-WHEAT + 10x10 HEATMAP COMPARISON\n\nimport os, shutil, yaml, random\nimport pandas as pd\nimport numpy as np\nfrom tqdm import tqdm\nfrom ultralytics import YOLO\nfrom IPython.display import display, Image as IPyImage\nfrom PIL import Image\nimport matplotlib.pyplot as plt\nimport torch\nimport torch.nn.modules.container\nfrom ultralytics.nn.modules import Conv\n\n# ---------- 1. CONFIG ----------\nGWD_PATH = '/kaggle/input/global-wheat-detection'\nWORK_DIR = '/kaggle/working/yolo_gwd_data'\nMODEL_NAME = 'yolov8n_gwd_wheat'\nEPOCHS = 10          # you can set back to 50 if you want\nIMG_SIZE = 640\nBATCH_SIZE = 16      # reduce if OOM\nVAL_SPLIT_RATIO = 0.1\nORIGINAL_IMG_SIZE = 1024\nGRID_H = GRID_W = 10\nrandom.seed(42)\n\nprint(\"Setup OK.\")\n\n# ---------- 2. SAFE LOAD PATCH (light) ----------\ntry:\n    torch.serialization.add_safe_globals([torch.nn.modules.container.Sequential])\n    torch.serialization.add_safe_globals([Conv])\nexcept Exception:\n    pass\n\n# ---------- 3. DATA PREP: CSV -> YOLO FORMAT ----------\nprint(\"\\n--- Preparing YOLO dataset from Global Wheat ---\")\ndf = pd.read_csv(os.path.join(GWD_PATH, 'train.csv'))\nimage_ids = df['image_id'].unique()\nrandom.shuffle(image_ids)\n\nval_samples = int(len(image_ids) * VAL_SPLIT_RATIO)\nval_ids = image_ids[:val_samples]\ntrain_ids = image_ids[val_samples:]\n\n# Clean & create dirs\nif os.path.exists(WORK_DIR):\n    shutil.rmtree(WORK_DIR)\nos.makedirs(os.path.join(WORK_DIR, 'images/train'), exist_ok=True)\nos.makedirs(os.path.join(WORK_DIR, 'images/val'), exist_ok=True)\nos.makedirs(os.path.join(WORK_DIR, 'labels/train'), exist_ok=True)\nos.makedirs(os.path.join(WORK_DIR, 'labels/val'), exist_ok=True)\n\ndef coco_to_yolo(bbox_str):\n    x_min, y_min, w, h = eval(bbox_str)\n    x_center = (x_min + w / 2) / ORIGINAL_IMG_SIZE\n    y_center = (y_min + h / 2) / ORIGINAL_IMG_SIZE\n    width = w / ORIGINAL_IMG_SIZE\n    height = h / ORIGINAL_IMG_SIZE\n    return f\"0 {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}\"\n\nIMAGE_SOURCE_DIR = os.path.join(GWD_PATH, 'train')\n\nfor image_id in tqdm(image_ids, desc=\"Processing images\"):\n    is_val = image_id in val_ids\n    img_dst_dir = os.path.join(WORK_DIR, 'images/val') if is_val else os.path.join(WORK_DIR, 'images/train')\n    lbl_dst_dir = os.path.join(WORK_DIR, 'labels/val') if is_val else os.path.join(WORK_DIR, 'labels/train')\n\n    src_img_path = os.path.join(IMAGE_SOURCE_DIR, f'{image_id}.jpg')\n    dst_img_path = os.path.join(img_dst_dir, f'{image_id}.jpg')\n    shutil.copyfile(src_img_path, dst_img_path)\n\n    annotations = df[df['image_id'] == image_id]['bbox'].dropna()\n    yolo_lines = [coco_to_yolo(bbox) for bbox in annotations]\n    with open(os.path.join(lbl_dst_dir, f'{image_id}.txt'), 'w') as f:\n        f.write('\\n'.join(yolo_lines))\n\nprint(\"✅ Annotation conversion & split done.\")\n\n# ---------- 4. data.yaml ----------\nDATA_YAML_PATH = os.path.join(WORK_DIR, 'wheat_data.yaml')\ndata_yaml = dict(\n    path=WORK_DIR,\n    train='images/train',\n    val='images/val',\n    names={0: 'wheat_head'},\n    cache='disk'\n)\nwith open(DATA_YAML_PATH, 'w') as out:\n    yaml.dump(data_yaml, out, default_flow_style=False)\nprint(f\"data.yaml written at: {DATA_YAML_PATH}\")\n\n# ---------- 5. TRAIN YOLOv8n VIA CLI ----------\nprint(\"\\n--- Training YOLOv8n via CLI ---\")\n!yolo train model=yolov8n.pt data={DATA_YAML_PATH} epochs={EPOCHS} imgsz={IMG_SIZE} batch={BATCH_SIZE} name={MODEL_NAME} project=\"/kaggle/working/runs/detect\"\n\nprint(\"\\n--- Training command finished (check logs above) ---\")\n\n# ---------- 6. LOAD BEST WEIGHTS & RUN INFERENCE ----------\nBEST_WEIGHTS = f'/kaggle/working/runs/detect/{MODEL_NAME}/weights/best.pt'\nif not os.path.exists(BEST_WEIGHTS):\n    print(f\"❌ best.pt not found at {BEST_WEIGHTS}. Training may have failed.\")\nelse:\n    print(f\"\\n✅ Found best weights at: {BEST_WEIGHTS}\")\n    best_model = YOLO(BEST_WEIGHTS)\n\n    val_img_dir = os.path.join(WORK_DIR, 'images/val')\n    val_images = os.listdir(val_img_dir)\n    if not val_images:\n        print(\"No val images found for inference.\")\n    else:\n        sample_img_name = val_images[0]\n        sample_img_path = os.path.join(val_img_dir, sample_img_name)\n        image_id = sample_img_name.replace('.jpg', '')\n        print(f\"\\nRunning inference on sample: {sample_img_name}\")\n\n        # YOLO prediction\n        pred = best_model.predict(source=sample_img_path, conf=0.25, verbose=False, save=True, name='gwd_inference_test')[0]\n        boxes = pred.boxes\n        wheat_head_count = len(boxes) if boxes is not None else 0\n        print(f\"Predicted wheat heads: {wheat_head_count}\")\n\n        # Display YOLO result image\n        result_path = f'/kaggle/working/runs/detect/gwd_inference_test/{sample_img_name}'\n        if os.path.exists(result_path):\n            display(IPyImage(filename=result_path))\n\n        # ---------- 7. BUILD 10x10 GT vs YOLO HEATMAPS ----------\n        # reload df (already loaded, but safe)\n        # df already in memory\n\n        # Load image for size + display\n        with Image.open(sample_img_path) as im:\n            W, H = im.size\n            img_np = np.array(im).astype(np.float32) / 255.0\n\n        # GT tile map\n        gt_map = np.zeros((GRID_H, GRID_W), dtype=np.float32)\n        rows = df[df['image_id'] == image_id]\n        for s in rows['bbox']:\n            x, y, w, h = eval(s)\n            cx = x + w/2\n            cy = y + h/2\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\n        # YOLO tile map\n        yolo_map = np.zeros((GRID_H, GRID_W), dtype=np.float32)\n        if boxes is not None:\n            xyxy = boxes.xyxy.cpu().numpy()\n            for (x1, y1, x2, y2) in xyxy:\n                cx = (x1 + x2) / 2.0\n                cy = (y1 + y2) / 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                yolo_map[i, j] += 1.0\n\n        gt_norm   = gt_map   / (gt_map.max()   + 1e-6)\n        yolo_norm = yolo_map / (yolo_map.max() + 1e-6)\n\n        plt.figure(figsize=(15, 4))\n        plt.subplot(1, 3, 1)\n        plt.imshow(img_np)\n        plt.axis(\"off\")\n        plt.title(f\"Image {image_id}\")\n\n        plt.subplot(1, 3, 2)\n        plt.imshow(gt_norm, cmap=\"viridis\")\n        plt.colorbar()\n        plt.title(\"GT head density (10×10)\")\n\n        plt.subplot(1, 3, 3)\n        plt.imshow(yolo_norm, 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-27T08:52:51.901999Z","iopub.execute_input":"2025-11-27T08:52:51.902689Z","iopub.status.idle":"2025-11-27T08:53:59.528838Z","shell.execute_reply.started":"2025-11-27T08:52:51.902657Z","shell.execute_reply":"2025-11-27T08:53:59.528044Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Measure YOLO inference FPS and example drone speed\n\nimport os, time\nfrom ultralytics import YOLO\n\n# use the same paths as in your YOLO cell\nBEST_WEIGHTS = f'/kaggle/working/runs/detect/yolov8n_gwd_wheat/weights/best.pt'\nWORK_DIR = '/kaggle/working/yolo_gwd_data'\n\nmodel = YOLO(BEST_WEIGHTS)\n\nval_dir = os.path.join(WORK_DIR, \"images/val\")\nval_images = sorted([f for f in os.listdir(val_dir) if f.endswith(\".jpg\")])\n\n# number of images to time\nN = min(100, len(val_images))\npaths = [os.path.join(val_dir, f) for f in val_images[:N]]\n\nprint(f\"Timing on {N} validation images...\")\n\n# warm-up once (loads model on GPU, etc.)\n_ = model.predict(source=paths[0], conf=0.25, verbose=False, save=False)\n\nt0 = time.time()\nfor p in paths:\n    _ = model.predict(source=p, conf=0.25, verbose=False, save=False)\nt1 = time.time()\n\ntotal_time = t1 - t0\navg_time = total_time / N\nfps = N / total_time\n\nprint(f\"Total time       : {total_time:.4f} s\")\nprint(f\"Avg time / image : {avg_time:.4f} s\")\nprint(f\"Approx FPS       : {fps:.1f} images/s\")\n\n# simple drone-speed estimate (change numbers as you like)\nGROUND_SWATH_M = 15.0   # ground length covered per frame along flight path\nOVERLAP = 0.3           # want 30% overlap between frames\n\nmax_speed = GROUND_SWATH_M * fps * (1 - OVERLAP)\nprint(f\"Example max drone speed ≈ {max_speed:.1f} m/s \"\n      f\"for {GROUND_SWATH_M} m/frame and {OVERLAP*100:.0f}% overlap.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T09:07:56.852566Z","iopub.execute_input":"2025-11-27T09:07:56.853328Z","iopub.status.idle":"2025-11-27T09:07:58.426579Z","shell.execute_reply.started":"2025-11-27T09:07:56.853304Z","shell.execute_reply":"2025-11-27T09:07:58.425849Z"}},"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":"markdown","source":"# STEP 2: ","metadata":{}},{"cell_type":"code","source":"import numpy as np\n\n# ground-truth stress from density (0..1)\nlow_thr = 0.4                      # you can tune this\ngt_stress = (density_norm < low_thr).astype(int)   # 1 = stressed, 0 = ok\n\ndef compute_metrics(gt, pred):\n    gt = gt.flatten()\n    pred = pred.flatten()\n    tp = np.sum((gt == 1) & (pred == 1))\n    tn = np.sum((gt == 0) & (pred == 0))\n    fp = np.sum((gt == 0) & (pred == 1))\n    fn = np.sum((gt == 1) & (pred == 0))\n    \n    acc = (tp + tn) / (tp + tn + fp + fn + 1e-6)\n    prec = tp / (tp + fp + 1e-6)\n    rec  = tp / (tp + fn + 1e-6)\n    f1   = 2 * prec * rec / (prec + rec + 1e-6)\n    \n    return {\n        \"tp\": tp, \"tn\": tn, \"fp\": fp, \"fn\": fn,\n        \"accuracy\": acc,\n        \"precision\": prec,\n        \"recall\": rec,\n        \"f1\": f1,\n    }\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T09:47:38.602814Z","iopub.execute_input":"2025-11-27T09:47:38.603086Z","iopub.status.idle":"2025-11-27T09:47:38.609085Z","shell.execute_reply.started":"2025-11-27T09:47:38.603066Z","shell.execute_reply":"2025-11-27T09:47:38.608339Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# choose suspicion threshold (tune if you like)\nthr = 0.6\n\npred_unf  = (suspicion_unf  >= thr).astype(int)\npred_filt = (suspicion_filt >= thr).astype(int)\n\nmetrics_unf  = compute_metrics(gt_stress, pred_unf)\nmetrics_filt = compute_metrics(gt_stress, pred_filt)\n\nprint(\"WITHOUT filter:\", metrics_unf)\nprint(\"WITH filter:   \", metrics_filt)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T09:49:08.291600Z","iopub.execute_input":"2025-11-27T09:49:08.291913Z","iopub.status.idle":"2025-11-27T09:49:08.306387Z","shell.execute_reply.started":"2025-11-27T09:49:08.291891Z","shell.execute_reply":"2025-11-27T09:49:08.305499Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Plant Coverage of TILE   - Use this later as:\n\nWhen we go to leaf-level disease / N-def, only consider tiles where:\n\nplant_frac is high, and\n\ncombined suspicion is high.\n\nThat way, we reduce:\n\nnumber of tiles we process,\n\nnumber of candidate leaf images,\n\ntotal compute for the later CNNs.","metadata":{}},{"cell_type":"code","source":"# plant mask already computed when we built img_np_filt:\n# mask_plant = (green or yellow)  -> shape H x W\n\nplant_frac = np.zeros((GRID_H, GRID_W), dtype=np.float32)\nfor i in range(GRID_H):\n    for j in range(GRID_W):\n        y0 = i * cell_h\n        y1 = (i + 1) * cell_h if i < GRID_H - 1 else H\n        x0 = j * cell_w\n        x1 = (j + 1) * cell_w if j < GRID_W - 1 else W\n\n        tile_mask = mask_plant[y0:y1, x0:x1]\n        plant_frac[i, j] = tile_mask.mean()   # fraction of pixels that are plant\n\n# example: tiles with <5% plant -> ignore in later steps\nignore_tiles = plant_frac < 0.05\nprint(\"Tiles to ignore (no crop there):\", ignore_tiles.sum(), \"out of\", GRID_H * GRID_W)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T09:49:12.792209Z","iopub.execute_input":"2025-11-27T09:49:12.792868Z","iopub.status.idle":"2025-11-27T09:49:12.801884Z","shell.execute_reply.started":"2025-11-27T09:49:12.792843Z","shell.execute_reply":"2025-11-27T09:49:12.801179Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport numpy as np\nfrom PIL import Image\n\n# reload original image to be safe\nimg = Image.open(img_path).convert(\"RGB\")\nimg_np_orig = np.array(img).astype(np.float32) / 255.0\nH, W, _ = img_np_orig.shape\n\n# make float masks for plotting\ngt_mask   = gt_stress.astype(float)\nunf_mask  = pred_unf.astype(float)\nfilt_mask = pred_filt.astype(float)\n\nplt.figure(figsize=(18, 5))\n\n# 1) Ground-truth stress (from head counts)\nplt.subplot(1, 3, 1)\nplt.imshow(img_np_orig)\nplt.imshow(\n    gt_mask,\n    cmap=\"Reds\",\n    alpha=0.4,\n    extent=(0, W, H, 0),\n    interpolation=\"nearest\",\n)\nplt.title(\"GT stressed tiles (from head count)\")\nplt.axis(\"off\")\n\n# 2) Predicted stress WITHOUT filter\nplt.subplot(1, 3, 2)\nplt.imshow(img_np_orig)\nplt.imshow(\n    unf_mask,\n    cmap=\"Reds\",\n    alpha=0.4,\n    extent=(0, W, H, 0),\n    interpolation=\"nearest\",\n)\nplt.title(\"Predicted stressed tiles\\nWITHOUT color filter\")\nplt.axis(\"off\")\n\n# 3) Predicted stress WITH filter\nplt.subplot(1, 3, 3)\nplt.imshow(img_np_orig)\nplt.imshow(\n    filt_mask,\n    cmap=\"Reds\",\n    alpha=0.4,\n    extent=(0, W, H, 0),\n    interpolation=\"nearest\",\n)\nplt.title(\"Predicted stressed tiles\\nWITH green+yellow filter\")\nplt.axis(\"off\")\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T09:49:17.707634Z","iopub.execute_input":"2025-11-27T09:49:17.708336Z","iopub.status.idle":"2025-11-27T09:49:17.741657Z","shell.execute_reply.started":"2025-11-27T09:49:17.708310Z","shell.execute_reply":"2025-11-27T09:49:17.740695Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#🔹 STEP 3 – helper: metrics + per-image processing","metadata":{}},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nfrom pathlib import Path\nfrom PIL import Image\n\nDATA_ROOT = Path(\"/kaggle/input/global-wheat-detection\")\ndf = pd.read_csv(DATA_ROOT / \"train.csv\")\n\n# metric helper\ndef compute_metrics(gt, pred):\n    gt = gt.flatten()\n    pred = pred.flatten()\n    tp = np.sum((gt == 1) & (pred == 1))\n    tn = np.sum((gt == 0) & (pred == 0))\n    fp = np.sum((gt == 0) & (pred == 1))\n    fn = np.sum((gt == 1) & (pred == 0))\n    \n    acc = (tp + tn) / (tp + tn + fp + fn + 1e-6)\n    prec = tp / (tp + fp + 1e-6)\n    rec  = tp / (tp + fn + 1e-6)\n    f1   = 2 * prec * rec / (prec + rec + 1e-6)\n    \n    return acc, prec, rec, f1, tp, tn, fp, fn\n\ndef process_one_image(image_id, grid_h=10, grid_w=10,\n                      stress_thr=0.4, susp_thr=0.6):\n    # subset annotations for this image\n    df_img = df[df[\"image_id\"] == image_id]\n    img_path = DATA_ROOT / \"train\" / f\"{image_id}.jpg\"\n    \n    img = Image.open(img_path).convert(\"RGB\")\n    img_np = np.array(img).astype(np.float32) / 255.0\n    H, W, _ = img_np.shape\n    \n    # parse bbox list\n    boxes = []\n    for s in df_img[\"bbox\"]:\n        x, y, w, h = eval(s)\n        boxes.append((x, y, w, h))\n    boxes = np.array(boxes, dtype=np.float32) if boxes else np.zeros((0, 4), dtype=np.float32)\n    \n    # grid\n    GRID_H, GRID_W = grid_h, grid_w\n    cell_h = H // GRID_H\n    cell_w = W // GRID_W\n    \n    # head counts\n    head_counts = np.zeros((GRID_H, GRID_W), dtype=np.float32)\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    if head_counts.max() > 0:\n        density_norm = head_counts / head_counts.max()\n    else:\n        density_norm = head_counts.copy()\n    \n    # ground-truth stress from density\n    gt_stress = (density_norm < stress_thr).astype(int)   # 1 = stressed\n    \n    # plant mask (green + yellow-ish)\n    R = img_np[..., 0]\n    G = img_np[..., 1]\n    B = img_np[..., 2]\n    mask_green  = (G > R) & (G > B) & (G > 0.25)\n    mask_yellow = (R > 0.25) & (G > 0.2) & (B < 0.8)\n    mask_plant  = mask_green | mask_yellow\n    \n    # plant fraction per tile\n    plant_frac = np.zeros((GRID_H, GRID_W), dtype=np.float32)\n    for i in range(GRID_H):\n        for j in range(GRID_W):\n            y0 = i * cell_h\n            y1 = (i + 1) * cell_h if i < GRID_H - 1 else H\n            x0 = j * cell_w\n            x1 = (j + 1) * cell_w if j < GRID_W - 1 else W\n            tile_mask = mask_plant[y0:y1, x0:x1]\n            plant_frac[i, j] = tile_mask.mean()\n    \n    ignore_tiles = plant_frac < 0.05   # almost no crop\n    \n    base_susp = 1.0 - density_norm\n    \n    # ---------- suspicion WITHOUT filter ----------\n    GI_unf = np.zeros((GRID_H, GRID_W), dtype=np.float32)\n    for i in range(GRID_H):\n        for j in range(GRID_W):\n            y0 = i * cell_h\n            y1 = (i + 1) * cell_h if i < GRID_H - 1 else H\n            x0 = j * cell_w\n            x1 = (j + 1) * cell_w if j < GRID_W - 1 else W\n            tile = img_np[y0:y1, x0:x1, :]\n            Rm = tile[..., 0].mean()\n            Gm = tile[..., 1].mean()\n            Bm = tile[..., 2].mean()\n            GI_unf[i, j] = Gm / (Rm + Gm + Bm + 1e-6)\n    \n    GI_mean_unf = GI_unf.mean()\n    GI_diff_unf = np.clip(GI_mean_unf - GI_unf, 0, None)\n    GI_norm_unf = GI_diff_unf / (GI_diff_unf.max() + 1e-6)\n    \n    color_term_unf = GI_norm_unf.copy()\n    color_term_unf[density_norm >= 0.5] = 0.0\n    \n    suspicion_unf = 0.7 * base_susp + 0.3 * color_term_unf\n    \n    # ---------- suspicion WITH filter ----------\n    img_np_filt = img_np.copy()\n    img_np_filt[~mask_plant] = 0.0\n    \n    GI_filt = np.zeros((GRID_H, GRID_W), dtype=np.float32)\n    for i in range(GRID_H):\n        for j in range(GRID_W):\n            y0 = i * cell_h\n            y1 = (i + 1) * cell_h if i < GRID_H - 1 else H\n            x0 = j * cell_w\n            x1 = (j + 1) * cell_w if j < GRID_W - 1 else W\n            tile = img_np_filt[y0:y1, x0:x1, :]\n            Rm = tile[..., 0].mean()\n            Gm = tile[..., 1].mean()\n            Bm = tile[..., 2].mean()\n            GI_filt[i, j] = Gm / (Rm + Gm + Bm + 1e-6)\n    \n    GI_mean_filt = GI_filt.mean()\n    GI_diff_filt = np.clip(GI_mean_filt - GI_filt, 0, None)\n    GI_norm_filt = GI_diff_filt / (GI_diff_filt.max() + 1e-6)\n    \n    color_term_filt = GI_norm_filt.copy()\n    color_term_filt[density_norm >= 0.5] = 0.0\n    \n    suspicion_filt = 0.7 * base_susp + 0.3 * color_term_filt\n    \n    # predictions from suspicion maps\n    pred_unf  = (suspicion_unf  >= susp_thr).astype(int)\n    pred_filt = (suspicion_filt >= susp_thr).astype(int)\n    \n    # metrics\n    acc_u, prec_u, rec_u, f1_u, tp_u, tn_u, fp_u, fn_u = compute_metrics(gt_stress, pred_unf)\n    acc_f, prec_f, rec_f, f1_f, tp_f, tn_f, fp_f, fn_f = compute_metrics(gt_stress, pred_filt)\n    \n    # resource usage: how many tiles flagged (and not ignored)\n    flagged_unf  = pred_unf.sum()\n    flagged_filt = pred_filt.sum()\n    flagged_unf_valid  = ((pred_unf == 1)  & (~ignore_tiles)).sum()\n    flagged_filt_valid = ((pred_filt == 1) & (~ignore_tiles)).sum()\n    \n    return {\n        \"image_id\": image_id,\n        \"acc_unf\": acc_u,\n        \"f1_unf\": f1_u,\n        \"acc_filt\": acc_f,\n        \"f1_filt\": f1_f,\n        \"flagged_unf\": int(flagged_unf),\n        \"flagged_filt\": int(flagged_filt),\n        \"flagged_unf_valid\": int(flagged_unf_valid),\n        \"flagged_filt_valid\": int(flagged_filt_valid),\n        \"ignored_tiles\": int(ignore_tiles.sum()),\n        \"total_tiles\": GRID_H * GRID_W,\n    }\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T09:49:23.576887Z","iopub.execute_input":"2025-11-27T09:49:23.577596Z","iopub.status.idle":"2025-11-27T09:49:23.745964Z","shell.execute_reply.started":"2025-11-27T09:49:23.577570Z","shell.execute_reply":"2025-11-27T09:49:23.745197Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# run on many images and summarise\n\n# take first N images for a quick test (you can increase N later)\nimage_ids = df[\"image_id\"].unique()\nN = 30   # try 30 or 50 depending on time\nimage_ids = image_ids[:N]\n\nresults = []\nfor k, iid in enumerate(image_ids, 1):\n    print(f\"{k}/{N}: {iid}\")\n    res = process_one_image(iid)\n    results.append(res)\n\nres_df = pd.DataFrame(results)\nres_df.head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T09:49:30.818062Z","iopub.execute_input":"2025-11-27T09:49:30.818663Z","iopub.status.idle":"2025-11-27T09:49:32.915116Z","shell.execute_reply.started":"2025-11-27T09:49:30.818634Z","shell.execute_reply":"2025-11-27T09:49:32.914369Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"### VISUAL SUMMARY\n\nimport numpy as np\nimport matplotlib.pyplot as plt\n\n# x-axis: image index\nx = np.arange(len(res_df))\n\nplt.figure(figsize=(16, 4))\n\n# 1) Accuracy per image\nplt.subplot(1, 3, 1)\nplt.plot(x, res_df[\"acc_unf\"], marker=\"o\", linestyle=\"-\", label=\"no filter\")\nplt.plot(x, res_df[\"acc_filt\"], marker=\"x\", linestyle=\"--\", label=\"with filter\")\nplt.title(\"Accuracy per image\")\nplt.xlabel(\"Image index\")\nplt.ylabel(\"Accuracy\")\nplt.ylim(0, 1)\nplt.legend()\n\n# 2) F1-score per image\nplt.subplot(1, 3, 2)\nplt.plot(x, res_df[\"f1_unf\"], marker=\"o\", linestyle=\"-\", label=\"no filter\")\nplt.plot(x, res_df[\"f1_filt\"], marker=\"x\", linestyle=\"--\", label=\"with filter\")\nplt.title(\"F1-score per image\")\nplt.xlabel(\"Image index\")\nplt.ylabel(\"F1-score\")\nplt.ylim(0, 1)\nplt.legend()\n\n# 3) Average number of stressed tiles (with crop)\nmean_unf_valid  = res_df[\"flagged_unf_valid\"].mean()\nmean_filt_valid = res_df[\"flagged_filt_valid\"].mean()\n\nplt.subplot(1, 3, 3)\nplt.bar([\"no filter\", \"with filter\"], [mean_unf_valid, mean_filt_valid])\nplt.title(\"Avg. stressed tiles with crop\\n(per image)\")\nplt.ylabel(\"Tiles per image\")\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T09:49:37.071920Z","iopub.execute_input":"2025-11-27T09:49:37.072601Z","iopub.status.idle":"2025-11-27T09:49:37.543076Z","shell.execute_reply.started":"2025-11-27T09:49:37.072577Z","shell.execute_reply":"2025-11-27T09:49:37.542483Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# New training cell: longer training + LR decay + best-model saving\nimport torch\n\ndef evaluate(loader):\n    model.eval()\n    correct, total = 0, 0\n    with torch.no_grad():\n        for imgs, labels in loader:\n            imgs, labels = imgs.to(device), labels.to(device)\n            outputs = model(imgs)\n            preds = outputs.argmax(dim=1)\n            correct += (preds == labels).sum().item()\n            total   += labels.size(0)\n    return correct / total if total > 0 else 0.0\n\nnum_epochs = 12          # was 3\nbest_acc   = 0.0\n\nfor epoch in range(1, num_epochs + 1):\n    # simple LR decay after a few epochs\n    if epoch == 6:\n        for g in optimizer.param_groups:\n            g[\"lr\"] *= 0.2   # 1e-3 -> 2e-4\n\n    model.train()\n    running_loss = 0.0\n\n    for imgs, labels in train_loader:\n        imgs, labels = imgs.to(device), labels.to(device)\n\n        optimizer.zero_grad()\n        outputs = model(imgs)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n\n        running_loss += loss.item() * labels.size(0)\n\n    train_loss = running_loss / len(train_loader.dataset)\n    val_acc = evaluate(val_loader)\n\n    # track best model\n    if val_acc > best_acc:\n        best_acc = val_acc\n        torch.save(\n            {\"model_state\": model.state_dict(),\n             \"class_to_idx\": full_ds.class_to_idx},\n            \"leaf_cnn_lcdcd_best.pth\",\n        )\n\n    print(f\"Epoch {epoch:2d}: train_loss={train_loss:.4f}, val_acc={val_acc:.3f}, best={best_acc:.3f}\")\n\nprint(\"Training done. Best val_acc =\", best_acc)\nprint(\"Best model saved as leaf_cnn_lcdcd_best.pth\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T09:49:44.786527Z","iopub.execute_input":"2025-11-27T09:49:44.787271Z","execution_failed":"2025-11-27T09:50:05.118Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# STEP 3: choose which tiles will go to Step-2 (leaf / disease stage)","metadata":{}},{"cell_type":"code","source":"from pathlib import Path\n\n# your dataset root\nleaf_base = Path(\"/kaggle/input/lcdcd-wheat-disease-inceptionresnetv2\")\nprint(\"leaf_base:\", leaf_base)\nprint(\"Immediate subfolders:\")\nfor p in leaf_base.iterdir():\n    print(\"  \", p)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T14:30:18.502407Z","iopub.status.idle":"2025-11-25T14:30:18.502669Z","shell.execute_reply.started":"2025-11-25T14:30:18.502536Z","shell.execute_reply":"2025-11-25T14:30:18.502546Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nfrom torch.utils.data import Dataset, DataLoader, random_split\nfrom torchvision import transforms\nfrom PIL import Image\n\n# We will search for all folders under leaf_base that actually contain images\nLEAF_ROOT = leaf_base\nprint(\"Using LEAF_ROOT =\", LEAF_ROOT)\n\nclass LeafDataset(Dataset):\n    def __init__(self, root, transform=None):\n        self.transform = transform\n        self.samples = []\n        self.class_to_idx = {}\n\n        # find all dirs that contain at least one image file\n        img_exts = {\".jpg\", \".jpeg\", \".png\", \".bmp\"}\n        candidate_dirs = []\n        for d in root.rglob(\"*\"):\n            if d.is_dir():\n                has_img = any(\n                    (f.is_file() and f.suffix.lower() in img_exts)\n                    for f in d.iterdir()\n                )\n                if has_img:\n                    candidate_dirs.append(d)\n\n        candidate_dirs = sorted(candidate_dirs)\n        classes = [d.name for d in candidate_dirs]\n        self.class_to_idx = {cls: i for i, cls in enumerate(classes)}\n        print(\"Found classes:\", self.class_to_idx)\n\n        for d in candidate_dirs:\n            label = self.class_to_idx[d.name]\n            for img_path in d.glob(\"*\"):\n                if img_path.suffix.lower() in img_exts:\n                    self.samples.append((img_path, label))\n\n        print(\"Total images:\", len(self.samples))\n\n    def __len__(self):\n        return len(self.samples)\n\n    def __getitem__(self, idx):\n        path, label = self.samples[idx]\n        img = Image.open(path).convert(\"RGB\")\n        if self.transform:\n            img = self.transform(img)\n        return img, label\n\n# transforms\ntransform = transforms.Compose([\n    transforms.Resize((128, 128)),\n    transforms.RandomHorizontalFlip(),\n    transforms.ToTensor(),\n])\n\nfull_ds = LeafDataset(LEAF_ROOT, transform=transform)\n\n# 80/20 train/val split\ntrain_size = int(0.8 * len(full_ds))\nval_size   = len(full_ds) - train_size\ntrain_ds, val_ds = random_split(full_ds, [train_size, val_size])\n\ntrain_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=2)\nval_loader   = DataLoader(val_ds,   batch_size=32, shuffle=False, num_workers=2)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T14:30:18.503604Z","iopub.status.idle":"2025-11-25T14:30:18.503791Z","shell.execute_reply.started":"2025-11-25T14:30:18.503702Z","shell.execute_reply":"2025-11-25T14:30:18.503710Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#SIMPLE CNN\n\n\nimport torch.nn as nn\n\nnum_classes = len(full_ds.class_to_idx)\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Device:\", device, \" | num_classes:\", num_classes)\n\nclass SimpleCNN(nn.Module):\n    def __init__(self, num_classes):\n        super().__init__()\n        self.features = nn.Sequential(\n            nn.Conv2d(3, 32, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),   # 64x64\n            nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),  # 32x32\n            nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), # 16x16\n            nn.AdaptiveAvgPool2d(1),                                      # 1x1\n        )\n        self.fc = nn.Linear(128, num_classes)\n\n    def forward(self, x):\n        x = self.features(x)\n        x = x.view(x.size(0), -1)\n        return self.fc(x)\n\nmodel = SimpleCNN(num_classes).to(device)\ncriterion = nn.CrossEntropyLoss()\noptimizer = torch.optim.Adam(model.parameters(), lr=1e-3)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T14:30:18.504870Z","iopub.status.idle":"2025-11-25T14:30:18.505077Z","shell.execute_reply.started":"2025-11-25T14:30:18.504972Z","shell.execute_reply":"2025-11-25T14:30:18.504981Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from pathlib import Path\n\nroot = Path(\"/kaggle/input\")\nfor p in root.iterdir():\n    print(\" -\", p)\n\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T14:31:06.968611Z","iopub.execute_input":"2025-11-25T14:31:06.969120Z","iopub.status.idle":"2025-11-25T14:31:06.973859Z","shell.execute_reply.started":"2025-11-25T14:31:06.969098Z","shell.execute_reply":"2025-11-25T14:31:06.973184Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell L1: locate LCDCD 2020 dataset folder and inspect contents\nfrom pathlib import Path\n\nroot = Path(\"/kaggle/input\")\n\nprint(\"All folders in /kaggle/input:\")\nfor p in root.iterdir():\n    print(\" -\", p)\n\nleaf_base = None\nfor p in root.iterdir():\n    name = p.name.lower()\n    if \"lcdcd-2020-dataset\" in name:\n        leaf_base = p\n        break\n\nprint(\"\\nSelected leaf_base:\", leaf_base)\n\nif leaf_base is not None:\n    print(\"\\nSubfolders inside leaf_base:\")\n    for p in leaf_base.iterdir():\n        print(\"  \", p)\nelse:\n    raise RuntimeError(\"LCDCD dataset not found. Attach 'lcdcd-2020-dataset' in Add data.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T15:52:37.324847Z","iopub.execute_input":"2025-11-25T15:52:37.325168Z","iopub.status.idle":"2025-11-25T15:52:37.334144Z","shell.execute_reply.started":"2025-11-25T15:52:37.325146Z","shell.execute_reply":"2025-11-25T15:52:37.333410Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell L2: build LeafDataset from leaf_base and create train/val loaders\nimport torch\nfrom torch.utils.data import Dataset, DataLoader, random_split\nfrom torchvision import transforms\nfrom PIL import Image\n\nLEAF_ROOT = leaf_base\nprint(\"Using LEAF_ROOT =\", LEAF_ROOT)\n\nclass LeafDataset(Dataset):\n    def __init__(self, root, transform=None):\n        self.transform = transform\n        self.samples = []\n        self.class_to_idx = {}\n\n        img_exts = {\".jpg\", \".jpeg\", \".png\", \".bmp\"}\n\n        # find all dirs that contain at least one image\n        candidate_dirs = []\n        for d in root.rglob(\"*\"):\n            if d.is_dir():\n                has_img = any(\n                    (f.is_file() and f.suffix.lower() in img_exts)\n                    for f in d.iterdir()\n                )\n                if has_img:\n                    candidate_dirs.append(d)\n\n        candidate_dirs = sorted(candidate_dirs)\n        if not candidate_dirs:\n            raise RuntimeError(f\"No image folders found under {root}\")\n\n        classes = [d.name for d in candidate_dirs]\n        self.class_to_idx = {cls: i for i, cls in enumerate(classes)}\n        print(\"Found classes:\", self.class_to_idx)\n\n        for d in candidate_dirs:\n            label = self.class_to_idx[d.name]\n            for img_path in d.glob(\"*\"):\n                if img_path.suffix.lower() in img_exts:\n                    self.samples.append((img_path, label))\n\n        if not self.samples:\n            raise RuntimeError(\"No image files collected. Check dataset structure.\")\n\n        print(\"Total images:\", len(self.samples))\n\n    def __len__(self):\n        return len(self.samples)\n\n    def __getitem__(self, idx):\n        path, label = self.samples[idx]\n        img = Image.open(path).convert(\"RGB\")\n        if self.transform:\n            img = self.transform(img)\n        return img, label\n\n# basic transforms\ntransform = transforms.Compose([\n    transforms.Resize((128, 128)),\n    transforms.RandomHorizontalFlip(),\n    transforms.ToTensor(),\n])\n\nfull_ds = LeafDataset(LEAF_ROOT, transform=transform)\nprint(\"Total images in dataset:\", len(full_ds))\n\n# 80/20 train-val split\ntrain_size = int(0.8 * len(full_ds))\nval_size   = len(full_ds) - train_size\ntrain_ds, val_ds = random_split(full_ds, [train_size, val_size])\n\ntrain_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=2)\nval_loader   = DataLoader(val_ds,   batch_size=32, shuffle=False, num_workers=2)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T15:52:42.582942Z","iopub.execute_input":"2025-11-25T15:52:42.583224Z","iopub.status.idle":"2025-11-25T15:52:54.221391Z","shell.execute_reply.started":"2025-11-25T15:52:42.583205Z","shell.execute_reply":"2025-11-25T15:52:54.220567Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell L2: build LeafDataset from leaf_base and create train/val loaders\nimport torch\nfrom torch.utils.data import Dataset, DataLoader, random_split\nfrom torchvision import transforms\nfrom PIL import Image\n\nLEAF_ROOT = leaf_base\nprint(\"Using LEAF_ROOT =\", LEAF_ROOT)\n\nclass LeafDataset(Dataset):\n    def __init__(self, root, transform=None):\n        self.transform = transform\n        self.samples = []\n        self.class_to_idx = {}\n\n        img_exts = {\".jpg\", \".jpeg\", \".png\", \".bmp\"}\n\n        # find all dirs that contain at least one image\n        candidate_dirs = []\n        for d in root.rglob(\"*\"):\n            if d.is_dir():\n                has_img = any(\n                    (f.is_file() and f.suffix.lower() in img_exts)\n                    for f in d.iterdir()\n                )\n                if has_img:\n                    candidate_dirs.append(d)\n\n        candidate_dirs = sorted(candidate_dirs)\n        if not candidate_dirs:\n            raise RuntimeError(f\"No image folders found under {root}\")\n\n        classes = [d.name for d in candidate_dirs]\n        self.class_to_idx = {cls: i for i, cls in enumerate(classes)}\n        print(\"Found classes:\", self.class_to_idx)\n\n        for d in candidate_dirs:\n            label = self.class_to_idx[d.name]\n            for img_path in d.glob(\"*\"):\n                if img_path.suffix.lower() in img_exts:\n                    self.samples.append((img_path, label))\n\n        if not self.samples:\n            raise RuntimeError(\"No image files collected. Check dataset structure.\")\n\n        print(\"Total images:\", len(self.samples))\n\n    def __len__(self):\n        return len(self.samples)\n\n    def __getitem__(self, idx):\n        path, label = self.samples[idx]\n        img = Image.open(path).convert(\"RGB\")\n        if self.transform:\n            img = self.transform(img)\n        return img, label\n\n# basic transforms\ntransform = transforms.Compose([\n    transforms.Resize((128, 128)),\n    transforms.RandomHorizontalFlip(),\n    transforms.ToTensor(),\n])\n\nfull_ds = LeafDataset(LEAF_ROOT, transform=transform)\nprint(\"Total images in dataset:\", len(full_ds))\n\n# 80/20 train-val split\ntrain_size = int(0.8 * len(full_ds))\nval_size   = len(full_ds) - train_size\ntrain_ds, val_ds = random_split(full_ds, [train_size, val_size])\n\ntrain_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=2)\nval_loader   = DataLoader(val_ds,   batch_size=32, shuffle=False, num_workers=2)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T15:52:59.815397Z","iopub.execute_input":"2025-11-25T15:52:59.816032Z","iopub.status.idle":"2025-11-25T15:53:02.186268Z","shell.execute_reply.started":"2025-11-25T15:52:59.816005Z","shell.execute_reply":"2025-11-25T15:53:02.185509Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell L3: define a small CNN model, loss, optimizer\nimport torch.nn as nn\n\nnum_classes = len(full_ds.class_to_idx)\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Device:\", device, \"| num_classes:\", num_classes)\n\nclass SimpleCNN(nn.Module):\n    def __init__(self, num_classes):\n        super().__init__()\n        self.features = nn.Sequential(\n            nn.Conv2d(3, 32, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),   # 64x64\n            nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),  # 32x32\n            nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), # 16x16\n            nn.AdaptiveAvgPool2d(1),                                      # 1x1\n        )\n        self.fc = nn.Linear(128, num_classes)\n\n    def forward(self, x):\n        x = self.features(x)\n        x = x.view(x.size(0), -1)\n        return self.fc(x)\n\nmodel = SimpleCNN(num_classes).to(device)\ncriterion = nn.CrossEntropyLoss()\noptimizer = torch.optim.Adam(model.parameters(), lr=1e-3)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T15:53:07.640535Z","iopub.execute_input":"2025-11-25T15:53:07.641078Z","iopub.status.idle":"2025-11-25T15:53:07.852779Z","shell.execute_reply.started":"2025-11-25T15:53:07.641052Z","shell.execute_reply":"2025-11-25T15:53:07.852040Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell L4: train for a few epochs and print validation accuracy\n'''def evaluate(loader):\n    model.eval()\n    correct, total = 0, 0\n    with torch.no_grad():\n        for imgs, labels in loader:\n            imgs, labels = imgs.to(device), labels.to(device)\n            outputs = model(imgs)\n            preds = outputs.argmax(dim=1)\n            correct += (preds == labels).sum().item()\n            total   += labels.size(0)\n    return correct / total if total > 0 else 0.0\n\nnum_epochs = 10  # you can increase later\n\nfor epoch in range(1, num_epochs + 1):\n    model.train()\n    running_loss = 0.0\n    for imgs, labels in train_loader:\n        imgs, labels = imgs.to(device), labels.to(device)\n\n        optimizer.zero_grad()\n        outputs = model(imgs)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n\n        running_loss += loss.item() * labels.size(0)\n\n    train_loss = running_loss / len(train_loader.dataset)\n    val_acc = evaluate(val_loader)\n    print(f\"Epoch {epoch}: train_loss={train_loss:.4f}, val_acc={val_acc:.3f}\")\n'''","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T14:38:56.384905Z","iopub.execute_input":"2025-11-25T14:38:56.385268Z","iopub.status.idle":"2025-11-25T14:41:41.620172Z","shell.execute_reply.started":"2025-11-25T14:38:56.385232Z","shell.execute_reply":"2025-11-25T14:41:41.619235Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# New training cell: longer training + LR decay + best-model saving\nimport torch\n\ndef evaluate(loader):\n    model.eval()\n    correct, total = 0, 0\n    with torch.no_grad():\n        for imgs, labels in loader:\n            imgs, labels = imgs.to(device), labels.to(device)\n            outputs = model(imgs)\n            preds = outputs.argmax(dim=1)\n            correct += (preds == labels).sum().item()\n            total   += labels.size(0)\n    return correct / total if total > 0 else 0.0\n\nnum_epochs = 12          # was 3\nbest_acc   = 0.0\n\nfor epoch in range(1, num_epochs + 1):\n    # simple LR decay after a few epochs\n    if epoch == 6:\n        for g in optimizer.param_groups:\n            g[\"lr\"] *= 0.2   # 1e-3 -> 2e-4\n\n    model.train()\n    running_loss = 0.0\n\n    for imgs, labels in train_loader:\n        imgs, labels = imgs.to(device), labels.to(device)\n\n        optimizer.zero_grad()\n        outputs = model(imgs)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n\n        running_loss += loss.item() * labels.size(0)\n\n    train_loss = running_loss / len(train_loader.dataset)\n    val_acc = evaluate(val_loader)\n\n    # track best model\n    if val_acc > best_acc:\n        best_acc = val_acc\n        torch.save(\n            {\"model_state\": model.state_dict(),\n             \"class_to_idx\": full_ds.class_to_idx},\n            \"leaf_cnn_lcdcd_best.pth\",\n        )\n\n    print(f\"Epoch {epoch:2d}: train_loss={train_loss:.4f}, val_acc={val_acc:.3f}, best={best_acc:.3f}\")\n\nprint(\"Training done. Best val_acc =\", best_acc)\nprint(\"Best model saved as leaf_cnn_lcdcd_best.pth\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T15:53:19.870271Z","iopub.execute_input":"2025-11-25T15:53:19.870948Z","iopub.status.idle":"2025-11-25T16:04:13.793796Z","shell.execute_reply.started":"2025-11-25T15:53:19.870922Z","shell.execute_reply":"2025-11-25T16:04:13.793035Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# QUICK SANITY CHECK","metadata":{}},{"cell_type":"code","source":"# Quick sanity-check on a few validation images\nimport torch, random\nimport matplotlib.pyplot as plt\n\n# 1) Load best checkpoint (if not already loaded)\nckpt = torch.load(\"leaf_cnn_lcdcd_best.pth\", map_location=device)\nmodel.load_state_dict(ckpt[\"model_state\"])\nclass_to_idx = ckpt.get(\"class_to_idx\", full_ds.class_to_idx)\nidx_to_class = {v: k for k, v in class_to_idx.items()}\nmodel.eval()\n\n# 2) Sample some images from validation set\nn_samples = 8  # change if you want more/less\nindices = random.sample(range(len(val_ds)), n_samples)\n\nfig, axes = plt.subplots(2, n_samples // 2, figsize=(3*(n_samples//2), 6))\naxes = axes.flatten()\n\nfor ax, i in zip(axes, indices):\n    img_tensor, label = val_ds[i]           # tensor CxHxW in [0,1]\n    img = img_tensor.permute(1, 2, 0).cpu().numpy()  # HxWxC for imshow\n\n    with torch.no_grad():\n        out = model(img_tensor.unsqueeze(0).to(device))\n        probs = torch.softmax(out, dim=1)[0]\n        pred_idx = int(torch.argmax(probs))\n        conf = float(probs[pred_idx])\n\n    true_class = idx_to_class[int(label)]\n    pred_class = idx_to_class[pred_idx]\n\n    ax.imshow(img)\n    ax.axis(\"off\")\n    ax.set_title(f\"T:{true_class}\\nP:{pred_class}\\n{conf:.2f}\")\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T16:12:43.198734Z","iopub.execute_input":"2025-11-25T16:12:43.199577Z","iopub.status.idle":"2025-11-25T16:12:44.062383Z","shell.execute_reply.started":"2025-11-25T16:12:43.199543Z","shell.execute_reply":"2025-11-25T16:12:44.061449Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Confusion matrix and per-class accuracy for the leaf CNN\nimport torch\nimport numpy as np\n\n# load best model (if not already loaded)\nckpt = torch.load(\"leaf_cnn_lcdcd_best.pth\", map_location=device)\nmodel.load_state_dict(ckpt[\"model_state\"])\nclass_to_idx = ckpt.get(\"class_to_idx\", full_ds.class_to_idx)\nidx_to_class = {v: k for k, v in class_to_idx.items()}\n\nnum_classes = len(class_to_idx)\nmodel.eval()\n\n# build confusion matrix: rows = true class, cols = predicted class\nconf_mat = np.zeros((num_classes, num_classes), dtype=int)\n\nwith torch.no_grad():\n    for imgs, labels in val_loader:\n        imgs, labels = imgs.to(device), labels.to(device)\n        outputs = model(imgs)\n        preds = outputs.argmax(dim=1)\n\n        for t, p in zip(labels.cpu().numpy(), preds.cpu().numpy()):\n            conf_mat[t, p] += 1\n\nprint(\"Classes (index -> name):\")\nfor idx, name in sorted(idx_to_class.items()):\n    print(f\"  {idx}: {name}\")\n\nprint(\"\\nConfusion matrix (rows=true, cols=pred):\")\nprint(conf_mat)\n\n# per-class accuracy\nprint(\"\\nPer-class accuracy:\")\nfor c in range(num_classes):\n    true_total = conf_mat[c].sum()\n    correct    = conf_mat[c, c]\n    acc_c = correct / true_total if true_total > 0 else 0.0\n    print(f\"{idx_to_class[c]}: {acc_c:.3f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T16:31:59.195953Z","iopub.execute_input":"2025-11-25T16:31:59.196570Z","iopub.status.idle":"2025-11-25T16:32:10.420720Z","shell.execute_reply.started":"2025-11-25T16:31:59.196541Z","shell.execute_reply":"2025-11-25T16:32:10.420021Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# map disease class → recommendation","metadata":{}},{"cell_type":"markdown","source":"# ","metadata":{}},{"cell_type":"code","source":"# Cell R1: define simple \"action\" for each disease class and demo on some images\nimport random\nimport torch\nimport matplotlib.pyplot as plt\n\n# load best model again (safe)\nckpt = torch.load(\"leaf_cnn_lcdcd_best.pth\", map_location=device)\nmodel.load_state_dict(ckpt[\"model_state\"])\nclass_to_idx = ckpt.get(\"class_to_idx\", full_ds.class_to_idx)\nidx_to_class = {v: k for k, v in class_to_idx.items()}\nmodel.eval()\n\n# simple text recommendations for each class\naction_map = {\n    \"Healthy Wheat\": \"No action – area OK, just monitor.\",\n    \"Leaf Rust\": \"Apply leaf fungicide for rust in this tile.\",\n    \"Crown and Root Rot\": \"Check soil moisture/drainage; root fungicide treatment.\",\n    \"Wheat Loose Smut\": \"Treat seed for next season; consider crop rotation.\",\n}\n\n# pick a few random validation samples\nn_samples = 6\nindices = random.sample(range(len(val_ds)), n_samples)\n\nfig, axes = plt.subplots(2, n_samples // 2, figsize=(3*(n_samples//2), 6))\naxes = axes.flatten()\n\nfor ax, i in zip(axes, indices):\n    img_tensor, label = val_ds[i]\n    img = img_tensor.permute(1, 2, 0).cpu().numpy()\n\n    with torch.no_grad():\n        out = model(img_tensor.unsqueeze(0).to(device))\n        probs = torch.softmax(out, dim=1)[0]\n        pred_idx = int(torch.argmax(probs))\n        conf = float(probs[pred_idx])\n\n    true_class = idx_to_class[int(label)]\n    pred_class = idx_to_class[pred_idx]\n    action = action_map.get(pred_class, \"No action defined.\")\n\n    ax.imshow(img)\n    ax.axis(\"off\")\n    ax.set_title(\n        f\"T:{true_class}\\nP:{pred_class}\\nConf:{conf:.2f}\\n{action}\",\n        fontsize=8,\n    )\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T16:36:29.970071Z","iopub.execute_input":"2025-11-25T16:36:29.970692Z","iopub.status.idle":"2025-11-25T16:36:30.648459Z","shell.execute_reply.started":"2025-11-25T16:36:29.970662Z","shell.execute_reply":"2025-11-25T16:36:30.647669Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#YOLO FOR BOXES AROUND GRAINS","metadata":{}},{"cell_type":"code","source":"# YOLO-1 (robust): install/import ultralytics and show exact status\nfrom pathlib import Path\nimport pandas as pd\nimport numpy as np\nimport shutil, os\nfrom PIL import Image\n\nprint(\"Trying to import ultralytics...\")\n\ntry:\n    from ultralytics import YOLO\n    print(\"Imported ultralytics successfully (no install needed).\")\nexcept ImportError as e:\n    print(\"ImportError, trying to pip install ultralytics...\")\n    try:\n        import sys, subprocess\n        subprocess.check_call([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"ultralytics\"])\n        from ultralytics import YOLO\n        print(\"Installed and imported ultralytics successfully.\")\n    except Exception as e2:\n        print(\"Failed to install/import ultralytics.\")\n        print(\"Exact error:\")\n        print(e2)\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T17:22:14.126031Z","iopub.execute_input":"2025-11-25T17:22:14.126600Z","iopub.status.idle":"2025-11-25T17:22:15.985136Z","shell.execute_reply.started":"2025-11-25T17:22:14.126575Z","shell.execute_reply":"2025-11-25T17:22:15.984428Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# YOLO-2: convert part of global-wheat-detection into YOLO format\nDATA_ROOT = Path(\"/kaggle/input/global-wheat-detection\")\ndf = pd.read_csv(DATA_ROOT / \"train.csv\")\n\n# take a small subset (you can increase N later)\nimage_ids = df[\"image_id\"].unique()\nN = 200\nimage_ids = image_ids[:N]\n\n# simple train/val split\nsplit = int(0.8 * len(image_ids))\ntrain_ids = set(image_ids[:split])\nval_ids   = set(image_ids[split:])\n\nyolo_root = Path(\"/kaggle/working/yolo_wheat\")\nimg_train_dir = yolo_root / \"images\" / \"train\"\nimg_val_dir   = yolo_root / \"images\" / \"val\"\nlbl_train_dir = yolo_root / \"labels\" / \"train\"\nlbl_val_dir   = yolo_root / \"labels\" / \"val\"\n\nfor d in [img_train_dir, img_val_dir, lbl_train_dir, lbl_val_dir]:\n    d.mkdir(parents=True, exist_ok=True)\n\ndef write_yolo_labels(image_id, target_dir_img, target_dir_lbl):\n    # copy image\n    src_img = DATA_ROOT / \"train\" / f\"{image_id}.jpg\"\n    dst_img = target_dir_img / f\"{image_id}.jpg\"\n    if not dst_img.exists():\n        shutil.copy(str(src_img), str(dst_img))\n\n    # get all bboxes for this image\n    df_img = df[df[\"image_id\"] == image_id]\n\n    # open image to get width/height\n    with Image.open(src_img) as im:\n        W, H = im.size\n\n    # YOLO label file\n    lbl_path = target_dir_lbl / f\"{image_id}.txt\"\n    with open(lbl_path, \"w\") as f:\n        for s in df_img[\"bbox\"]:\n            x, y, w, h = eval(s)  # [x_min, y_min, w, h] in pixels\n            xc = x + w / 2.0\n            yc = y + h / 2.0\n\n            # normalize to 0..1\n            xc_n = xc / W\n            yc_n = yc / H\n            w_n  = w  / W\n            h_n  = h  / H\n\n            cls_id = 0  # single class: wheat head\n            f.write(f\"{cls_id} {xc_n:.6f} {yc_n:.6f} {w_n:.6f} {h_n:.6f}\\n\")\n\n# create YOLO train/val files\nfor iid in train_ids:\n    write_yolo_labels(iid, img_train_dir, lbl_train_dir)\n\nfor iid in val_ids:\n    write_yolo_labels(iid, img_val_dir, lbl_val_dir)\n\nprint(\"YOLO dataset built at:\", yolo_root)\nprint(\"Train images:\", len(list(img_train_dir.glob('*.jpg'))))\nprint(\"Val images:\",   len(list(img_val_dir.glob('*.jpg'))))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T17:22:20.046578Z","iopub.execute_input":"2025-11-25T17:22:20.047096Z","iopub.status.idle":"2025-11-25T17:22:22.485033Z","shell.execute_reply.started":"2025-11-25T17:22:20.047060Z","shell.execute_reply":"2025-11-25T17:22:22.484398Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# YOLO-3: create data.yaml for YOLO training\nimport yaml\n\ndata_yaml = {\n    \"path\": str(yolo_root),         # base path\n    \"train\": \"images/train\",\n    \"val\":   \"images/val\",\n    \"names\": {0: \"wheat_head\"},     # one class\n}\n\nwith open(yolo_root / \"data.yaml\", \"w\") as f:\n    yaml.dump(data_yaml, f, default_flow_style=False)\n\nprint((yolo_root / \"data.yaml\").read_text())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T17:22:26.475136Z","iopub.execute_input":"2025-11-25T17:22:26.475421Z","iopub.status.idle":"2025-11-25T17:22:26.481621Z","shell.execute_reply.started":"2025-11-25T17:22:26.475399Z","shell.execute_reply":"2025-11-25T17:22:26.480961Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# YOLO-4: train a small YOLO model on wheat heads\n# \"yolov8n.yaml\" = tiny YOLOv8 model architecture (no pretrained weights needed)\nmodel_yolo = YOLO(\"yolov8n.yaml\")\n\nresults = model_yolo.train(\n    data=str(yolo_root / \"data.yaml\"),\n    epochs=5,          # you can increase later\n    imgsz=640,\n    batch=8,\n    project=\"wheat_yolo_runs\",\n    name=\"yolov8n_wheat\",\n)\n\nprint(\"YOLO training done.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T17:22:32.182889Z","iopub.execute_input":"2025-11-25T17:22:32.183476Z","iopub.status.idle":"2025-11-25T17:22:32.671511Z","shell.execute_reply.started":"2025-11-25T17:22:32.183447Z","shell.execute_reply":"2025-11-25T17:22:32.670540Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}