{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":126119,"databundleVersionId":14953781}],"dockerImageVersionId":31287,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"**Import Desried Things;**","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19"}},{"cell_type":"code","source":"import os","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-27T14:46:13.364360Z","iopub.execute_input":"2026-02-27T14:46:13.364637Z","iopub.status.idle":"2026-02-27T14:46:13.375074Z","shell.execute_reply.started":"2026-02-27T14:46:13.364604Z","shell.execute_reply":"2026-02-27T14:46:13.374240Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install -qU timm tifffile","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-27T14:46:15.363685Z","iopub.execute_input":"2026-02-27T14:46:15.364408Z","iopub.status.idle":"2026-02-27T14:46:24.098503Z","shell.execute_reply.started":"2026-02-27T14:46:15.364362Z","shell.execute_reply":"2026-02-27T14:46:24.097740Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Import Necessaries Librries;\nimport numpy as np\nimport pandas as pd\nimport random\nimport re\nfrom dataclasses import dataclass\nimport cv2\nimport tifffile as tiff\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.model_selection import StratifiedKFold\nimport timm","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-27T14:47:43.710078Z","iopub.execute_input":"2026-02-27T14:47:43.710786Z","iopub.status.idle":"2026-02-27T14:48:00.587735Z","shell.execute_reply.started":"2026-02-27T14:47:43.710753Z","shell.execute_reply":"2026-02-27T14:48:00.587160Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Seed\ndef seed_everything(seed):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.deterministic = True\n\n\nprint(f\"Device: {torch.device('cuda' if torch.cuda.is_available() else 'cpu')}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-27T14:51:31.760490Z","iopub.execute_input":"2026-02-27T14:51:31.761599Z","iopub.status.idle":"2026-02-27T14:51:32.030395Z","shell.execute_reply.started":"2026-02-27T14:51:31.761566Z","shell.execute_reply":"2026-02-27T14:51:32.029754Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Config\n@dataclass\nclass CFG:\n    ROOT: str = \"/kaggle/input/beyond-visible-spectrum-ai-for-agriculture-2026/Kaggle_Prepared\"\n    OUT_DIR: str = \"/kaggle/working/\"\n\n    IMG_SIZE: int = 128\n    BATCH_SIZE: int = 32\n    HS_CHANNELS: int = 30\n\n    EPOCHS: int = 10\n    LR: float = 3e-4\n    WD: float = 1e-3\n    FOLDS: int = 5\n    SEED: int = 42\n\n    RGB_BACKBONE: str = \"tf_efficientnetv2_s.in21k\"\n    MIXUP_ALPHA: float = 1.0\n\n    USE_PSEUDO_LABELING: bool = True\n    PSEUDO_THRESH: float = 0.90\n\n    LABELS = [\"Health\", \"Rust\", \"Other\"]\n    LBL2ID = {k: i for i, k in enumerate(LABELS)}\n    ID2LBL = {i: k for k, i in LBL2ID.items()}\n\n\nseed_everything(CFG.SEED)\nos.makedirs(CFG.OUT_DIR, exist_ok=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-27T14:52:29.106522Z","iopub.execute_input":"2026-02-27T14:52:29.107344Z","iopub.status.idle":"2026-02-27T14:52:29.122828Z","shell.execute_reply.started":"2026-02-27T14:52:29.107311Z","shell.execute_reply":"2026-02-27T14:52:29.122150Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Utilities\ndef robust_minmax(img):\n    mn, mx = img.min(), img.max()\n    if mx - mn < 1e-8:\n        return np.zeros_like(img, dtype=np.float32)\n    return (img - mn) / (mx - mn)\n\n\ndef read_rgb(path, size):\n    img = cv2.imread(path)\n    if img is None:\n        return np.zeros((size, size, 3), dtype=np.float32)\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    img = cv2.resize(img, (size, size))\n    return img.astype(np.float32) / 255.0\n\n\ndef read_spectral(ms_path, hs_path, size, target_hs_ch):\n    if os.path.exists(ms_path):\n        ms = tiff.imread(ms_path).astype(np.float32)\n        ms = cv2.resize(ms, (size, size), interpolation=cv2.INTER_CUBIC)\n\n        eps = 1e-8\n        nir, red, re, green = ms[..., 4], ms[..., 2], ms[..., 3], ms[..., 1]\n        ndvi = (nir - red) / (nir + red + eps)\n        gndvi = (nir - green) / (nir + green + eps)\n        ndre = (nir - re) / (nir + re + eps)\n        mcari = ((re - red) - 0.2 * (re - green)) * (re / (red + eps))\n\n        ms = robust_minmax(ms)\n        indices = np.stack([ndvi, gndvi, ndre, mcari], axis=-1)\n        indices = (indices - indices.min()) / (indices.max() - indices.min() + eps)\n        ms_block = np.concatenate([ms, indices], axis=-1)\n    else:\n        ms_block = np.zeros((size, size, 9), dtype=np.float32)\n\n    if os.path.exists(hs_path):\n        hs = tiff.imread(hs_path).astype(np.float32)\n        hs = hs[..., 10:-14]\n\n        if hs.shape[2] > target_hs_ch:\n            idx = np.linspace(0, hs.shape[2] - 1, target_hs_ch).astype(int)\n            hs = hs[..., idx]\n        elif hs.shape[2] < target_hs_ch:\n            pad = np.zeros((hs.shape[0], hs.shape[1], target_hs_ch - hs.shape[2]), dtype=np.float32)\n            hs = np.concatenate([hs, pad], axis=-1)\n\n        hs = cv2.resize(hs, (size, size), interpolation=cv2.INTER_CUBIC)\n        hs = robust_minmax(hs)\n    else:\n        hs = np.zeros((size, size, target_hs_ch), dtype=np.float32)\n\n    return np.concatenate([ms_block, hs], axis=-1)\n\n\ndef get_file_dataframe(root, split):\n    data = []\n    rgb_dir = os.path.join(root, split, \"RGB\")\n    ms_dir = os.path.join(root, split, \"MS\")\n    hs_dir = os.path.join(root, split, \"HS\")\n\n    for f in os.listdir(rgb_dir):\n        if not f.endswith(\".png\"):\n            continue\n        bid = f.replace(\".png\", \"\")\n        label = \"Unknown\"\n        if split == \"train\":\n            match = re.match(r\"^(Health|Rust|Other)_\", bid)\n            if match:\n                label = match.group(1)\n\n        data.append(\n            {\n                \"base_id\": bid,\n                \"label\": label,\n                \"rgb_path\": os.path.join(rgb_dir, f),\n                \"ms_path\": os.path.join(ms_dir, bid + \".tif\"),\n                \"hs_path\": os.path.join(hs_dir, bid + \".tif\"),\n            }\n        )\n    return pd.DataFrame(data)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-27T14:53:37.517354Z","iopub.execute_input":"2026-02-27T14:53:37.517638Z","iopub.status.idle":"2026-02-27T14:53:37.531531Z","shell.execute_reply.started":"2026-02-27T14:53:37.517615Z","shell.execute_reply":"2026-02-27T14:53:37.530931Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Dataset Class\nclass AgriDataset(Dataset):\n    def __init__(self, df, train=True):\n        self.df = df.reset_index(drop=True)\n        self.train = train\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n\n        rgb = read_rgb(row[\"rgb_path\"], CFG.IMG_SIZE)\n        spec = read_spectral(row[\"ms_path\"], row[\"hs_path\"], CFG.IMG_SIZE, CFG.HS_CHANNELS)\n\n        if self.train:\n            if random.random() > 0.5:\n                rgb = np.fliplr(rgb)\n                spec = np.fliplr(spec)\n            if random.random() > 0.5:\n                rgb = np.flipud(rgb)\n                spec = np.flipud(spec)\n            if random.random() > 0.5:\n                k = random.randint(1, 3)\n                rgb = np.rot90(rgb, k)\n                spec = np.rot90(spec, k)\n\n        rgb = np.ascontiguousarray(rgb.transpose(2, 0, 1))\n        spec = np.ascontiguousarray(spec.transpose(2, 0, 1))\n\n        rgb_t = torch.from_numpy(rgb).float()\n        spec_t = torch.from_numpy(spec).float()\n\n        label = CFG.LBL2ID.get(row[\"label\"], -1)\n        return rgb_t, spec_t, torch.tensor(label, dtype=torch.long)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-27T14:54:11.935767Z","iopub.execute_input":"2026-02-27T14:54:11.936392Z","iopub.status.idle":"2026-02-27T14:54:11.943336Z","shell.execute_reply.started":"2026-02-27T14:54:11.936362Z","shell.execute_reply":"2026-02-27T14:54:11.942753Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Deep Learning Model\nclass DualStreamModel(nn.Module):\n    def __init__(self, spec_ch=39, n_classes=3):\n        super().__init__()\n\n        self.rgb_net = timm.create_model(CFG.RGB_BACKBONE, pretrained=True, num_classes=0)\n\n        self.spec_net = nn.Sequential(\n            nn.Conv2d(spec_ch, 64, 1),\n            nn.BatchNorm2d(64),\n            nn.SiLU(),\n            nn.Conv2d(64, 64, 3, padding=1, groups=64),\n            nn.Conv2d(64, 128, 1),\n            nn.BatchNorm2d(128),\n            nn.SiLU(),\n            nn.MaxPool2d(2),\n            nn.Conv2d(128, 128, 3, padding=1, groups=128),\n            nn.Conv2d(128, 256, 1),\n            nn.BatchNorm2d(256),\n            nn.SiLU(),\n            nn.AdaptiveAvgPool2d(1),\n            nn.Flatten(),\n        )\n\n        combined_dim = self.rgb_net.num_features + 256\n        self.head = nn.Sequential(\n            nn.Linear(combined_dim, 512),\n            nn.BatchNorm1d(512),\n            nn.Dropout(0.4),\n            nn.SiLU(),\n            nn.Linear(512, n_classes),\n        )\n\n    def forward(self, rgb, spec):\n        f1 = self.rgb_net(rgb)\n        f2 = self.spec_net(spec)\n        return self.head(torch.cat([f1, f2], dim=1))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-27T14:55:13.342748Z","iopub.execute_input":"2026-02-27T14:55:13.343128Z","iopub.status.idle":"2026-02-27T14:55:13.350384Z","shell.execute_reply.started":"2026-02-27T14:55:13.343098Z","shell.execute_reply":"2026-02-27T14:55:13.349757Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Model Training\ndef mixup(rgb, spec, y, alpha):\n    lam = np.random.beta(alpha, alpha) if alpha > 0 else 1\n    idx = torch.randperm(rgb.size(0)).to(rgb.device)\n    m_rgb = lam * rgb + (1 - lam) * rgb[idx]\n    m_spec = lam * spec + (1 - lam) * spec[idx]\n    return m_rgb, m_spec, y, y[idx], lam\n\n\ndef train_epoch(model, loader, opt, scaler, device):\n    model.train()\n    loss_sum = 0\n    for rgb, spec, y in loader:\n        rgb, spec, y = rgb.to(device), spec.to(device), y.to(device)\n        rgb, spec, y_a, y_b, lam = mixup(rgb, spec, y, CFG.MIXUP_ALPHA)\n\n        opt.zero_grad(set_to_none=True)\n        with torch.amp.autocast(\"cuda\"):\n            out = model(rgb, spec)\n            loss = lam * F.cross_entropy(out, y_a) + (1 - lam) * F.cross_entropy(out, y_b)\n\n        scaler.scale(loss).backward()\n        scaler.step(opt)\n        scaler.update()\n        loss_sum += loss.item()\n    return loss_sum / len(loader)\n\n\n@torch.no_grad()\ndef infer_loop(model, loader, device):\n    model.eval()\n    probs = []\n    for rgb, spec, _ in loader:\n        rgb, spec = rgb.to(device), spec.to(device)\n        p1 = model(rgb, spec).softmax(1)\n        p2 = model(torch.flip(rgb, [3]), torch.flip(spec, [3])).softmax(1)\n        probs.append((p1 + p2) / 2)\n    return torch.cat(probs).cpu().numpy()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-27T14:55:50.091722Z","iopub.execute_input":"2026-02-27T14:55:50.092304Z","iopub.status.idle":"2026-02-27T14:55:50.101813Z","shell.execute_reply.started":"2026-02-27T14:55:50.092273Z","shell.execute_reply":"2026-02-27T14:55:50.101260Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df = get_file_dataframe(CFG.ROOT, \"train\")\ntest_df = get_file_dataframe(CFG.ROOT, \"val\")\nSPEC_CH = 9 + CFG.HS_CHANNELS\n\nskf = StratifiedKFold(n_splits=CFG.FOLDS, shuffle=True, random_state=CFG.SEED)\ndevice = torch.device(\"cuda\")\n\n# --- CYCLE 1 ---\nprint(\">>> Cycle 1: Training\")\nfor fold, (t_idx, v_idx) in enumerate(skf.split(train_df, train_df[\"label\"])):\n    ds_tr = AgriDataset(train_df.iloc[t_idx], train=True)\n    dl_tr = DataLoader(ds_tr, batch_size=CFG.BATCH_SIZE, shuffle=True, num_workers=2, drop_last=True)\n\n    model = DualStreamModel(SPEC_CH).to(device)\n    opt = torch.optim.AdamW(model.parameters(), lr=CFG.LR, weight_decay=CFG.WD)\n    scaler = torch.amp.GradScaler(\"cuda\")\n\n    for ep in range(CFG.EPOCHS):\n        train_epoch(model, dl_tr, opt, scaler, device)\n\n    torch.save(model.state_dict(), f\"{CFG.OUT_DIR}/model_c1_f{fold}.pt\")\n    print(f\"Fold {fold} done.\")\n\n# --- PSEUDO LABELING ---\nif CFG.USE_PSEUDO_LABELING:\n    print(\"\\n>>> Generating Pseudo Labels\")\n    ds_test = AgriDataset(test_df, train=False)\n    dl_test = DataLoader(ds_test, batch_size=CFG.BATCH_SIZE * 2, num_workers=2)\n\n    avg_preds = np.zeros((len(test_df), 3))\n    for fold in range(CFG.FOLDS):\n        m = DualStreamModel(SPEC_CH).to(device)\n        m.load_state_dict(torch.load(f\"{CFG.OUT_DIR}/model_c1_f{fold}.pt\"))\n        m.eval()\n        avg_preds += infer_loop(m, dl_test, device) / CFG.FOLDS\n\n    conf = avg_preds.max(1)\n    pseudo_mask = conf > CFG.PSEUDO_THRESH\n    pseudo_df = test_df[pseudo_mask].copy()\n    pseudo_df[\"label\"] = [CFG.ID2LBL[p] for p in avg_preds[pseudo_mask].argmax(1)]\n    print(f\"Added {len(pseudo_df)} pseudo-samples.\")\n\n    # --- CYCLE 2 ---\n    print(\"\\n>>> Cycle 2: Retraining\")\n    for fold, (t_idx, v_idx) in enumerate(skf.split(train_df, train_df[\"label\"])):\n        tr_curr = pd.concat([train_df.iloc[t_idx], pseudo_df]).reset_index(drop=True)\n        ds_tr = AgriDataset(tr_curr, train=True)\n        dl_tr = DataLoader(\n            ds_tr,\n            batch_size=CFG.BATCH_SIZE,\n            shuffle=True,\n            num_workers=2,\n            drop_last=True,\n        )\n\n        model = DualStreamModel(SPEC_CH).to(device)\n        model.load_state_dict(torch.load(f\"{CFG.OUT_DIR}/model_c1_f{fold}.pt\"))\n        opt = torch.optim.AdamW(model.parameters(), lr=CFG.LR * 0.5, weight_decay=CFG.WD)\n        scaler = torch.amp.GradScaler(\"cuda\")\n\n        for ep in range(10):\n            train_epoch(model, dl_tr, opt, scaler, device)\n\n        torch.save(model.state_dict(), f\"{CFG.OUT_DIR}/model_c2_f{fold}.pt\")\n        print(f\"Fold {fold} refined.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-27T14:56:09.736146Z","iopub.execute_input":"2026-02-27T14:56:09.736430Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Inference for Sumission Results;\nprint(\"\\n>>> Creating Submission\")\nds_test = AgriDataset(test_df, train=False)\ndl_test = DataLoader(ds_test, batch_size=CFG.BATCH_SIZE * 2, num_workers=2)\n\navg_preds = np.zeros((len(test_df), 3))\nprefix = \"model_c2_\" if CFG.USE_PSEUDO_LABELING else \"model_c1_\"\n\nfor fold in range(CFG.FOLDS):\n    m = DualStreamModel(SPEC_CH).to(device)\n    m.load_state_dict(torch.load(f\"{CFG.OUT_DIR}/{prefix}f{fold}.pt\"))\n    m.eval()\n    avg_preds += infer_loop(m, dl_test, device) / CFG.FOLDS\n\nfinal_cats = [CFG.ID2LBL[p] for p in avg_preds.argmax(1)]\nfinal_ids = [bid + \".tif\" for bid in test_df[\"base_id\"]]\n\nsub = pd.DataFrame({\"Id\": final_ids, \"Category\": final_cats})\nsub.to_csv(\"submission.csv\", index=False)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sub.head(10)","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}