{"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":"code","source":"# =====================================================\n# ICPR 2026 — CORRECT SUBMISSION VERSION\n# =====================================================\n\n!pip install timm tifffile -q\n\nimport os\nimport warnings\nwarnings.filterwarnings(\"ignore\")\n\nimport numpy as np\nimport cv2\nimport tifffile as tiff\nimport pandas as pd\nfrom tqdm import tqdm\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\n\nimport timm\nfrom sklearn.metrics import accuracy_score\n\n# =====================================================\n# CONFIG\n# =====================================================\nclass CFG:\n    IMG_SIZE = 224\n    BATCH_SIZE = 16\n    EPOCHS = 8\n    LR = 3e-4\n    NUM_CLASSES = 3\n    DEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n    DATA_PATH = \"/kaggle/input/beyond-visible-spectrum-ai-for-agriculture-2026/Kaggle_Prepared\"\n    HS_CHANNELS = 128\n    MS_CHANNELS = 5\n\nCLASS_MAP = {0: \"Rust\", 1: \"Health\", 2: \"Other\"}\n\n# =====================================================\n# CHANNEL FIX\n# =====================================================\ndef fix_channels(img, target_c):\n    c = img.shape[2]\n    if c > target_c:\n        img = img[:, :, :target_c]\n    elif c < target_c:\n        pad = target_c - c\n        img = np.pad(img, ((0,0),(0,0),(0,pad)), mode='edge')\n    return img\n\n# =====================================================\n# TRAIN DATASET\n# =====================================================\nclass TrainDataset(Dataset):\n    def __init__(self, root_dir):\n        self.root = root_dir\n        self.rgb_paths = sorted(os.listdir(os.path.join(root_dir,\"RGB\")))\n        self.ms_paths  = sorted(os.listdir(os.path.join(root_dir,\"MS\")))\n        self.hs_paths  = sorted(os.listdir(os.path.join(root_dir,\"HS\")))\n        self.labels = [self.get_label(x) for x in self.rgb_paths]\n\n    def get_label(self, name):\n        name = name.lower()\n        if name.startswith(\"rust\"): return 0\n        elif name.startswith(\"health\"): return 1\n        else: return 2\n\n    def __len__(self): return len(self.rgb_paths)\n\n    def read_img(self, path, modality=None):\n        ext = path.lower().split('.')[-1]\n        img = tiff.imread(path) if ext in [\"tif\",\"tiff\"] else cv2.imread(path, cv2.IMREAD_UNCHANGED)\n\n        if len(img.shape) == 2:\n            img = np.expand_dims(img, -1)\n\n        if modality==\"ms\": img = fix_channels(img, CFG.MS_CHANNELS)\n        if modality==\"hs\": img = fix_channels(img, CFG.HS_CHANNELS)\n\n        img = cv2.resize(img,(CFG.IMG_SIZE,CFG.IMG_SIZE))\n        img = img.astype(np.float32)\n        img = (img-img.min())/(img.max()-img.min()+1e-6)\n        img = np.transpose(img,(2,0,1))\n        return torch.tensor(img,dtype=torch.float32)\n\n    def __getitem__(self,idx):\n        rgb=self.read_img(os.path.join(self.root,\"RGB\",self.rgb_paths[idx]))\n        ms =self.read_img(os.path.join(self.root,\"MS\", self.ms_paths[idx]),modality=\"ms\")\n        hs =self.read_img(os.path.join(self.root,\"HS\", self.hs_paths[idx]),modality=\"hs\")\n        label=torch.tensor(self.labels[idx]).long()\n        return rgb,ms,hs,label\n\n# =====================================================\n# VAL / TEST DATASET\n# =====================================================\nclass TestDataset(Dataset):\n    def __init__(self, root_dir):\n        self.root=root_dir\n        self.rgb_paths=sorted(os.listdir(os.path.join(root_dir,\"RGB\")))\n        self.ms_paths =sorted(os.listdir(os.path.join(root_dir,\"MS\")))\n        self.hs_paths =sorted(os.listdir(os.path.join(root_dir,\"HS\")))\n\n    def __len__(self): return len(self.rgb_paths)\n\n    def read_img(self,path,modality=None):\n        ext=path.lower().split('.')[-1]\n        img=tiff.imread(path) if ext in [\"tif\",\"tiff\"] else cv2.imread(path,cv2.IMREAD_UNCHANGED)\n\n        if len(img.shape)==2:\n            img=np.expand_dims(img,-1)\n\n        if modality==\"ms\": img=fix_channels(img,CFG.MS_CHANNELS)\n        if modality==\"hs\": img=fix_channels(img,CFG.HS_CHANNELS)\n\n        img=cv2.resize(img,(CFG.IMG_SIZE,CFG.IMG_SIZE))\n        img=img.astype(np.float32)\n        img=(img-img.min())/(img.max()-img.min()+1e-6)\n        img=np.transpose(img,(2,0,1))\n        return torch.tensor(img,dtype=torch.float32)\n\n    def __getitem__(self,idx):\n        rgb=self.read_img(os.path.join(self.root,\"RGB\",self.rgb_paths[idx]))\n        ms =self.read_img(os.path.join(self.root,\"MS\", self.ms_paths[idx]),modality=\"ms\")\n        hs =self.read_img(os.path.join(self.root,\"HS\", self.hs_paths[idx]),modality=\"hs\")\n        return rgb,ms,hs,self.rgb_paths[idx]\n\n# =====================================================\n# MODEL (lightweight & safe)\n# =====================================================\nclass MultiModalNet(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.ms_proj=nn.Conv2d(CFG.MS_CHANNELS,3,1)\n        self.hs_proj=nn.Conv2d(CFG.HS_CHANNELS,3,1)\n\n        self.backbone=timm.create_model(\"efficientnet_b0\",pretrained=True,num_classes=0)\n        feat_dim=self.backbone.num_features\n\n        self.classifier=nn.Linear(feat_dim*3,CFG.NUM_CLASSES)\n\n    def forward(self,rgb,ms,hs):\n        ms=self.ms_proj(ms)\n        hs=self.hs_proj(hs)\n\n        f1=self.backbone(rgb)\n        f2=self.backbone(ms)\n        f3=self.backbone(hs)\n\n        fused=torch.cat([f1,f2,f3],dim=1)\n        return self.classifier(fused)\n\n# =====================================================\n# LOADERS\n# =====================================================\ntrain_ds=TrainDataset(os.path.join(CFG.DATA_PATH,\"train\"))\nval_ds  =TestDataset(os.path.join(CFG.DATA_PATH,\"val\"))\n\ntrain_loader=DataLoader(train_ds,batch_size=CFG.BATCH_SIZE,shuffle=True,num_workers=0)\nval_loader  =DataLoader(val_ds,batch_size=CFG.BATCH_SIZE,shuffle=False,num_workers=0)\n\n# =====================================================\n# TRAIN\n# =====================================================\nmodel=MultiModalNet().to(CFG.DEVICE)\ncriterion=nn.CrossEntropyLoss()\noptimizer=optim.AdamW(model.parameters(),lr=CFG.LR)\n\nfor epoch in range(CFG.EPOCHS):\n    model.train()\n    for rgb,ms,hs,y in tqdm(train_loader):\n        rgb,ms,hs,y=rgb.to(CFG.DEVICE),ms.to(CFG.DEVICE),hs.to(CFG.DEVICE),y.to(CFG.DEVICE)\n        optimizer.zero_grad()\n        loss=criterion(model(rgb,ms,hs),y)\n        loss.backward()\n        optimizer.step()\n\n# =====================================================\n# INFERENCE ON VAL\n# =====================================================\nmodel.eval()\npreds=[]\nids=[]\n\nwith torch.no_grad():\n    for rgb,ms,hs,names in tqdm(val_loader):\n        rgb,ms,hs=rgb.to(CFG.DEVICE),ms.to(CFG.DEVICE),hs.to(CFG.DEVICE)\n        out=model(rgb,ms,hs)\n        p=out.argmax(1).cpu().numpy()\n        preds.extend(p)\n        ids.extend(names)\n\n# =====================================================\n# CORRECT SUBMISSION FORMAT\n# =====================================================\n# =====================================================\n# BULLETPROOF SUBMISSION CREATION\n# =====================================================\n\n# force correct Id format (convert png -> tif name)\ndef to_tif_name(x):\n    base = os.path.splitext(x)[0]\n    return base + \".tif\"\n\nsubmission = pd.DataFrame({\n    \"Id\": [to_tif_name(x) for x in ids],\n    \"Category\": [CLASS_MAP[int(x)] for x in preds]\n})\n\n# sort by Id (VERY IMPORTANT for some ICPR comps)\nsubmission = submission.sort_values(\"Id\").reset_index(drop=True)\n\nsubmission.to_csv(\"submission.csv\", index=False)\n\nprint(\"submission.csv created:\", submission.shape)\nprint(submission.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-22T06:44:17.035089Z","iopub.execute_input":"2026-02-22T06:44:17.035693Z","iopub.status.idle":"2026-02-22T06:50:05.970641Z","shell.execute_reply.started":"2026-02-22T06:44:17.035656Z","shell.execute_reply":"2026-02-22T06:50:05.969909Z"}},"outputs":[],"execution_count":null}]}