{"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},{"sourceType":"datasetVersion","sourceId":14898381,"datasetId":9532558,"databundleVersionId":15762905},{"sourceType":"kernelVersion","sourceId":236321004},{"sourceType":"kernelVersion","sourceId":236798451}],"dockerImageVersionId":31287,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-02-20T12:58:31.249237Z","iopub.execute_input":"2026-02-20T12:58:31.249568Z","iopub.status.idle":"2026-02-20T12:58:36.804937Z","shell.execute_reply.started":"2026-02-20T12:58:31.249541Z","shell.execute_reply":"2026-02-20T12:58:36.804127Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport cv2\nimport timm\nimport torch\nimport random\nimport tifffile as tiff\nimport numpy as np\nimport pandas as pd\nimport torch.nn as nn\n\nfrom pathlib import Path\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.metrics import f1_score\nfrom torch.utils.data import Dataset, DataLoader","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-20T12:58:36.806472Z","iopub.execute_input":"2026-02-20T12:58:36.806695Z","iopub.status.idle":"2026-02-20T12:58:36.811078Z","shell.execute_reply.started":"2026-02-20T12:58:36.806674Z","shell.execute_reply":"2026-02-20T12:58:36.810275Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"BASE = Path(\"/kaggle/input/competitions/beyond-visible-spectrum-ai-for-agriculture-2026/Kaggle_Prepared/train\")\n\nrecords = []\nfor rgb in sorted((BASE/\"RGB\").glob(\"*.png\")):\n    name = rgb.stem\n    label = name.split(\"_\")[0]\n\n    records.append({\n        \"id\": name,\n        \"label\": label,\n        \"RGB_path\": str(rgb),\n        \"HS_path\": str(BASE/\"HS\"/f\"{name}.tif\")\n    })\n\ndf = pd.DataFrame(records)\nprint(df.label.value_counts())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-20T12:58:36.811966Z","iopub.execute_input":"2026-02-20T12:58:36.812227Z","iopub.status.idle":"2026-02-20T12:58:36.839366Z","shell.execute_reply.started":"2026-02-20T12:58:36.812192Z","shell.execute_reply":"2026-02-20T12:58:36.838841Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)\ndf[\"fold\"] = -1\n\nfor fold, (_, val_idx) in enumerate(skf.split(df, df[\"label\"])):\n    df.loc[val_idx, \"fold\"] = fold","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-20T12:58:36.840654Z","iopub.execute_input":"2026-02-20T12:58:36.840904Z","iopub.status.idle":"2026-02-20T12:58:36.849678Z","shell.execute_reply.started":"2026-02-20T12:58:36.840885Z","shell.execute_reply":"2026-02-20T12:58:36.848908Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"TARGET_BANDS = 125\n\ndef load_hs_aligned(path):\n    hs = tiff.imread(path).astype(\"float32\")\n    B = hs.shape[2]\n\n    if B == TARGET_BANDS:\n        return hs\n\n    old_idx = np.linspace(0,1,B)\n    new_idx = np.linspace(0,1,TARGET_BANDS)\n\n    aligned = np.zeros((32,32,TARGET_BANDS),dtype=np.float32)\n    for i in range(32):\n        for j in range(32):\n            aligned[i,j]=np.interp(new_idx,old_idx,hs[i,j])\n    return aligned","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-20T12:58:36.850643Z","iopub.execute_input":"2026-02-20T12:58:36.850938Z","iopub.status.idle":"2026-02-20T12:58:36.859701Z","shell.execute_reply.started":"2026-02-20T12:58:36.850913Z","shell.execute_reply":"2026-02-20T12:58:36.858960Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def load_rgb(path):\n    img=cv2.imread(path)\n    img=cv2.cvtColor(img,cv2.COLOR_BGR2RGB)\n    img=cv2.resize(img,(128,128))\n    return img.astype(\"float32\")/255.0","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-20T12:58:36.860512Z","iopub.execute_input":"2026-02-20T12:58:36.860840Z","iopub.status.idle":"2026-02-20T12:58:36.871280Z","shell.execute_reply.started":"2026-02-20T12:58:36.860812Z","shell.execute_reply":"2026-02-20T12:58:36.870673Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def augment_hs(hs):\n    if random.random()<0.5: hs=np.flip(hs,1)\n    if random.random()<0.5: hs=np.flip(hs,0)\n    if random.random()<0.5: hs=np.rot90(hs,random.randint(1,3),(0,1))\n    return hs.copy()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-20T12:58:36.871991Z","iopub.execute_input":"2026-02-20T12:58:36.872217Z","iopub.status.idle":"2026-02-20T12:58:36.883374Z","shell.execute_reply.started":"2026-02-20T12:58:36.872197Z","shell.execute_reply":"2026-02-20T12:58:36.882708Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"label_map={\"Health\":0,\"Other\":1,\"Rust\":2}\n\nclass MultiModalDataset(Dataset):\n    def __init__(self,df,fold,train=True):\n        self.train=train\n        self.df=df[df.fold!=fold] if train else df[df.fold==fold]\n\n    def __len__(self): return len(self.df)\n\n    def __getitem__(self,idx):\n        row=self.df.iloc[idx]\n\n        # HS\n        hs=load_hs_aligned(row.HS_path)\n        if self.train:\n            x=np.random.randint(0,8); y=np.random.randint(0,8)\n            hs=augment_hs(hs[x:x+24,y:y+24,:])\n        else:\n            hs=hs[4:28,4:28,:]\n\n        mean=hs.mean((0,1),keepdims=True)\n        std=hs.std((0,1),keepdims=True)+1e-6\n        hs=(hs-mean)/std\n        hs=torch.tensor(hs).permute(2,0,1).float()\n\n        # RGB\n        rgb=torch.tensor(load_rgb(row.RGB_path)).permute(2,0,1).float()\n\n        label=torch.tensor(label_map[row.label])\n        return rgb,hs,label","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-20T12:58:36.884867Z","iopub.execute_input":"2026-02-20T12:58:36.885111Z","iopub.status.idle":"2026-02-20T12:58:36.895050Z","shell.execute_reply.started":"2026-02-20T12:58:36.885089Z","shell.execute_reply":"2026-02-20T12:58:36.894459Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class SpectralAttention(nn.Module):\n    def __init__(self,bands):\n        super().__init__()\n        self.fc=nn.Sequential(\n            nn.Linear(bands,bands//4),\n            nn.ReLU(),\n            nn.Linear(bands//4,bands),\n            nn.Sigmoid()\n        )\n    def forward(self,x):\n        b,c,d,h,w=x.shape\n        s=x.mean((3,4)).view(b,d)\n        wts=self.fc(s).view(b,1,d,1,1)\n        return x*wts","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-20T12:58:36.895857Z","iopub.execute_input":"2026-02-20T12:58:36.896102Z","iopub.status.idle":"2026-02-20T12:58:36.910004Z","shell.execute_reply.started":"2026-02-20T12:58:36.896069Z","shell.execute_reply":"2026-02-20T12:58:36.909319Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class HS3DCNN(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.attn=SpectralAttention(125)\n        self.features=nn.Sequential(\n            nn.Conv3d(1,8,(7,3,3),padding=(3,1,1)),nn.BatchNorm3d(8),nn.ReLU(),\n            nn.Conv3d(8,16,(5,3,3),padding=(2,1,1)),nn.BatchNorm3d(16),nn.ReLU(),\n            nn.Conv3d(16,32,(3,3,3),padding=1),nn.BatchNorm3d(32),nn.ReLU(),\n            nn.AdaptiveAvgPool3d(1)\n        )\n    def forward(self,x):\n        x=self.attn(x.unsqueeze(1))\n        return self.features(x).flatten(1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-20T12:58:36.911792Z","iopub.execute_input":"2026-02-20T12:58:36.911998Z","iopub.status.idle":"2026-02-20T12:58:36.922866Z","shell.execute_reply.started":"2026-02-20T12:58:36.911978Z","shell.execute_reply":"2026-02-20T12:58:36.922380Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class RGBEncoder(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.backbone=timm.create_model(\"efficientnet_b0\",pretrained=True,num_classes=0)\n    def forward(self,x): return self.backbone(x)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-20T12:58:36.923546Z","iopub.execute_input":"2026-02-20T12:58:36.923754Z","iopub.status.idle":"2026-02-20T12:58:36.933906Z","shell.execute_reply.started":"2026-02-20T12:58:36.923721Z","shell.execute_reply":"2026-02-20T12:58:36.933257Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class FusionNet(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.rgb=RGBEncoder()\n        self.hs=HS3DCNN()\n        self.classifier=nn.Sequential(\n            nn.Linear(1280+32,256),\n            nn.ReLU(),\n            nn.Dropout(0.4),\n            nn.Linear(256,3)\n        )\n    def forward(self,rgb,hs):\n        return self.classifier(torch.cat([self.rgb(rgb),self.hs(hs)],1))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-20T12:58:36.934748Z","iopub.execute_input":"2026-02-20T12:58:36.934993Z","iopub.status.idle":"2026-02-20T12:58:36.948730Z","shell.execute_reply.started":"2026-02-20T12:58:36.934960Z","shell.execute_reply":"2026-02-20T12:58:36.948212Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device=\"cuda\" if torch.cuda.is_available() else \"cpu\"\n\ntrain_ds=MultiModalDataset(df,fold=0,train=True)\nval_ds  =MultiModalDataset(df,fold=0,train=False)\n\ntrain_loader=DataLoader(train_ds,batch_size=8,shuffle=True)\nval_loader  =DataLoader(val_ds,batch_size=8)\n\nmodel=FusionNet().to(device)\nopt=torch.optim.AdamW(model.parameters(),lr=3e-4,weight_decay=1e-4)\ncrit=nn.CrossEntropyLoss(label_smoothing=0.1)\n\ndef freeze_rgb(m,f=True):\n    for p in m.rgb.parameters(): p.requires_grad=not f\n\ndef eval_f1():\n    model.eval();p=[];t=[]\n    with torch.no_grad():\n        for rgb,hs,y in val_loader:\n            o=model(rgb.to(device),hs.to(device))\n            p+=o.argmax(1).cpu().tolist();t+=y.tolist()\n    return f1_score(t,p,average=\"macro\")\n\nbest=0\nfor ep in range(18):\n    freeze_rgb(model,ep<5)\n    model.train()\n    for rgb,hs,y in train_loader:\n        opt.zero_grad()\n        loss=crit(model(rgb.to(device),hs.to(device)),y.to(device))\n        loss.backward();opt.step()\n    f1=eval_f1();best=max(best,f1)\n    print(ep+1,f1,best)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-20T12:58:36.949494Z","iopub.execute_input":"2026-02-20T12:58:36.949721Z","iopub.status.idle":"2026-02-20T13:00:41.538583Z","shell.execute_reply.started":"2026-02-20T12:58:36.949687Z","shell.execute_reply":"2026-02-20T13:00:41.537788Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ===============================\n# FINAL SUBMISSION (TTA ENABLED)\n# ===============================\n\nimport os\nimport re\nimport torch\nimport pandas as pd\nfrom torch.utils.data import Dataset, DataLoader\n\nVAL_PATH = \"/kaggle/input/competitions/beyond-visible-spectrum-ai-for-agriculture-2026/Kaggle_Prepared/val\"\ndevice = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n\n\n# -------- collect official evaluation images --------\nval_ids = sorted([\n    f for f in os.listdir(f\"{VAL_PATH}/HS\")\n    if re.fullmatch(r\"val_[0-9a-f]{8}\\.tif\", f)\n])\nprint(\"Evaluation images:\", len(val_ids))   # should be 200\n\n\n# -------- Dataset --------\nclass TestDataset(Dataset):\n    def __init__(self, root, ids):\n        self.root = root\n        self.ids = ids\n\n    def __len__(self):\n        return len(self.ids)\n\n    def __getitem__(self, i):\n        name = self.ids[i]\n\n        # RGB\n        rgb = load_rgb(f\"{self.root}/RGB/{name.replace('.tif','.png')}\")\n        rgb = torch.tensor(rgb).permute(2,0,1).float()\n\n        # HS\n        hs = load_hs_aligned(f\"{self.root}/HS/{name}\")\n        hs = hs[4:28,4:28,:]\n        hs = (hs-hs.mean((0,1),keepdims=True))/(hs.std((0,1),keepdims=True)+1e-6)\n        hs = torch.tensor(hs).permute(2,0,1).float()\n\n        return name, rgb, hs\n\n\nloader = DataLoader(TestDataset(VAL_PATH,val_ids), batch_size=8, shuffle=False)\n\n\n# -------- TTA prediction --------\ndef predict_tta(model, rgb, hs):\n    outputs = []\n\n    outputs.append(model(rgb,hs))\n    outputs.append(model(torch.flip(rgb,[3]), torch.flip(hs,[3])))\n    outputs.append(model(torch.flip(rgb,[2]), torch.flip(hs,[2])))\n    outputs.append(model(torch.rot90(rgb,1,[2,3]), torch.rot90(hs,1,[2,3])))\n    outputs.append(model(torch.rot90(rgb,2,[2,3]), torch.rot90(hs,2,[2,3])))\n    outputs.append(model(torch.rot90(rgb,3,[2,3]), torch.rot90(hs,3,[2,3])))\n\n    return torch.stack(outputs).mean(0)\n\n\n# -------- Inference --------\nmodel.eval()\ninv={0:\"Health\",1:\"Other\",2:\"Rust\"}\n\nrows=[]\nwith torch.no_grad():\n    for names, rgb, hs in loader:\n        rgb = rgb.to(device)\n        hs  = hs.to(device)\n\n        logits = predict_tta(model,rgb,hs)\n        preds = logits.argmax(1).cpu().tolist()\n\n        for n,p in zip(names,preds):\n            rows.append((n,inv[p]))\n\n\n# -------- Save CSV --------\nsub=pd.DataFrame(rows,columns=[\"Id\",\"Category\"])\nsub.to_csv(\"/kaggle/working/submission.csv\",index=False)\n\nprint(\"Saved submission.csv with\",len(sub),\"rows\")\nsub.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-20T13:11:22.064216Z","iopub.execute_input":"2026-02-20T13:11:22.064752Z","iopub.status.idle":"2026-02-20T13:11:28.456115Z","shell.execute_reply.started":"2026-02-20T13:11:22.064722Z","shell.execute_reply":"2026-02-20T13:11:28.455357Z"}},"outputs":[],"execution_count":null}]}