{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"},{"sourceId":14451883,"sourceType":"datasetVersion","datasetId":9230803},{"sourceId":14488369,"sourceType":"datasetVersion","datasetId":9253942},{"sourceId":717604,"sourceType":"modelInstanceVersion","modelInstanceId":545589,"modelId":558599}],"dockerImageVersionId":31192,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"**Environment setup for pre-trained models.**","metadata":{}},{"cell_type":"code","source":"!pip install --no-index --no-deps --force-reinstall --find-links=/kaggle/input/okeluon/hf_wheels_flat protobuf","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-19T08:34:10.866765Z","iopub.execute_input":"2026-01-19T08:34:10.867402Z","iopub.status.idle":"2026-01-19T08:34:14.752696Z","shell.execute_reply.started":"2026-01-19T08:34:10.867377Z","shell.execute_reply":"2026-01-19T08:34:14.751514Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"rm -rf /usr/local/lib/python3.12/dist-packages/~ensorflow*","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-19T08:34:16.707372Z","iopub.execute_input":"2026-01-19T08:34:16.707863Z","iopub.status.idle":"2026-01-19T08:34:16.839533Z","shell.execute_reply.started":"2026-01-19T08:34:16.707825Z","shell.execute_reply":"2026-01-19T08:34:16.838441Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!python -m pip install --no-index --find-links /kaggle/input/okeluon/hf_wheels_flat tokenizers==0.22.0 transformers==4.56.2","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-19T08:34:19.438456Z","iopub.execute_input":"2026-01-19T08:34:19.439157Z","iopub.status.idle":"2026-01-19T08:34:31.503582Z","shell.execute_reply.started":"2026-01-19T08:34:19.439117Z","shell.execute_reply":"2026-01-19T08:34:31.502627Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip -qqq uninstall -y tensorflow tf-keras keras","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-19T08:34:31.505108Z","iopub.execute_input":"2026-01-19T08:34:31.505429Z","iopub.status.idle":"2026-01-19T08:34:57.896886Z","shell.execute_reply.started":"2026-01-19T08:34:31.505399Z","shell.execute_reply":"2026-01-19T08:34:57.896008Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# SINGLE-CHECKPOINT OFFLINE INFERENCE – DINOv3+CLIP TILED-MIL – TTA optional\n# =========================================================\nimport os\nimport random\nimport gc\nfrom PIL import Image\nimport numpy as np\nimport pandas as pd\nfrom tqdm import tqdm\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch.cuda.amp import autocast\n\nfrom transformers import AutoImageProcessor, CLIPVisionModel, DINOv3ViTModel\nimport inspect\n\n# =========================================================\n# CONFIG\n# =========================================================\nclass CFG:\n    # local model directories (pretrained backbones)\n    dinov3_model = \"/kaggle/input/pretrained-dinov3-and-clip/dinov3_model\"\n    clip_model  = \"/kaggle/input/pretrained-dinov3-and-clip/clip_model\"\n\n    # single checkpoint -- auto-detect if None\n    ckpt_path = None\n\n    # data\n    test_csv = \"/kaggle/input/csiro-biomass/test.csv\"\n    img_dir  = \"/kaggle/input/csiro-biomass/\"\n\n    # tiling & resource control\n    tile_size = 512\n    stride = 384\n    max_tiles = 8        # hard cap per image (set 6~12 by memory)\n    batch_size = 1       # images per loop iteration (keep 1 to be safe)\n    num_workers = os.cpu_count()\n    device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n    use_amp = False\n\n    # TTA options\n    tta_hflip = True\n    tta_vflip = True\n\n    # output\n    out_csv = \"/kaggle/working/submission.csv\"\n\n    target_cols = [\n        \"Dry_Green_g\",\n        \"Dry_Dead_g\",\n        \"Dry_Clover_g\",\n        \"GDM_g\",\n        \"Dry_Total_g\"\n    ]\n\n# =========================================================\n# Auto-detect checkpoint helper\n# =========================================================\ndef find_checkpoint():\n    if CFG.ckpt_path and os.path.exists(CFG.ckpt_path):\n        return CFG.ckpt_path\n\n    search_dirs = [\n        \"/kaggle/input\",\n        \"/kaggle/working\",\n        \"/kaggle/input/pretrained-dinov3-and-clip\",\n        \"/kaggle/input/models\",\n    ]\n    candidates = []\n    for d in search_dirs:\n        if not os.path.exists(d):\n            continue\n        for root, _, files in os.walk(d):\n            for f in files:\n                if f.endswith(\".pth\") or f.endswith(\".pt\"):\n                    # prefer names that start with best_dinov3 or similar\n                    candidates.append(os.path.join(root, f))\n    # prefer ones with best_dinov3 substring\n    for c in candidates:\n        if \"best_dinov3\" in os.path.basename(c).lower():\n            return c\n    if candidates:\n        return candidates[0]\n    return None\n\n# =========================================================\n# Helpers: tiling + pooling + safe forward\n# =========================================================\ndef compute_tile_grid(W, H, tile, stride):\n    def positions(L):\n        if L <= tile:\n            return [0]\n        xs = list(range(0, L - tile + 1, stride))\n        if xs[-1] != L - tile:\n            xs.append(L - tile)\n        return xs\n    xs = positions(W); ys = positions(H)\n    return xs, ys\n\ndef tile_image(img: Image.Image, tile=512, stride=384):\n    W, H = img.size\n    xs, ys = compute_tile_grid(W, H, tile, stride)\n    tiles = []\n    for y in ys:\n        for x in xs:\n            tiles.append(img.crop((x, y, x + tile, y + tile)))\n    if len(tiles) == 0:\n        return [Image.new(\"RGB\", (tile, tile))]\n    return tiles\n\ndef mean_pool_patches(last_hidden_state):\n    if last_hidden_state.size(1) > 1:\n        return last_hidden_state[:, 1:, :].mean(dim=1)\n    return last_hidden_state.mean(dim=1)\n\ndef safe_forward_with_interp(model, pixel_values):\n    try:\n        sig = inspect.signature(model.forward)\n        if \"interpolate_pos_encoding\" in sig.parameters:\n            return model(pixel_values=pixel_values, interpolate_pos_encoding=True)\n        else:\n            return model(pixel_values=pixel_values)\n    except TypeError:\n        return model(pixel_values=pixel_values)\n\n# =========================================================\n# Model components (same as training)\n# =========================================================\nclass TileAttentionPool(nn.Module):\n    def __init__(self, d):\n        super().__init__()\n        self.attn = nn.Sequential(nn.Linear(d, 256), nn.Tanh(), nn.Linear(256, 1))\n    def forward(self, x):\n        w = torch.softmax(self.attn(x), dim=1)\n        return (x * w).sum(dim=1)\n\nclass GatedFusion(nn.Module):\n    def __init__(self, dino_dim, clip_dim, out_dim):\n        super().__init__()\n        self.dino_proj = nn.Linear(dino_dim, out_dim)\n        self.clip_proj = nn.Linear(clip_dim, out_dim)\n        self.gate = nn.Sequential(nn.Linear(dino_dim + clip_dim, out_dim), nn.Sigmoid())\n    def forward(self, dino_feat, clip_feat):\n        gate = self.gate(torch.cat([dino_feat, clip_feat], dim=-1))\n        dino_m = self.dino_proj(dino_feat)\n        clip_m = self.clip_proj(clip_feat)\n        return gate * dino_m + (1.0 - gate) * clip_m\n\nclass DINOv3CLIP_TiledMIL_Regressor(nn.Module):\n    def __init__(self, dinov3_path, clip_path, emb_dim=768, dropout=0.2, local_files=True):\n        super().__init__()\n        self.dino = DINOv3ViTModel.from_pretrained(dinov3_path, local_files_only=local_files)\n        self.clip = CLIPVisionModel.from_pretrained(clip_path, local_files_only=local_files)\n        for p in self.dino.parameters(): p.requires_grad = False\n        for p in self.clip.parameters(): p.requires_grad = False\n\n        self.dino_dim = self.dino.config.hidden_size\n        self.clip_dim = self.clip.config.hidden_size\n\n        self.fusion = GatedFusion(self.dino_dim, self.clip_dim, emb_dim)\n        self.meta_proj = nn.Sequential(nn.Linear(2, 128), nn.ReLU(), nn.Linear(128, emb_dim))\n        self.norm = nn.LayerNorm(emb_dim)\n        self.pool = TileAttentionPool(emb_dim)\n        self.head = nn.Sequential(\n            nn.Linear(emb_dim, 512), nn.ReLU(), nn.Dropout(dropout),\n            nn.Linear(512, 256), nn.ReLU(), nn.Dropout(dropout),\n            nn.Linear(256, 5)\n        )\n\n    def forward(self, dino_tiles, clip_tiles, meta):\n        B, T, C, H, W = dino_tiles.shape\n        dino_in = dino_tiles.view(B * T, C, H, W)\n        clip_in = clip_tiles.view(B * T, C, H, W)\n\n        with torch.no_grad():\n            dino_out = safe_forward_with_interp(self.dino, dino_in)\n            clip_out = safe_forward_with_interp(self.clip, clip_in)\n            dino_feat = mean_pool_patches(dino_out.last_hidden_state)\n            clip_feat = mean_pool_patches(clip_out.last_hidden_state)\n\n        fused_tile = self.fusion(dino_feat, clip_feat)\n        fused_tile = fused_tile.view(B, T, -1)\n\n        meta_tok = self.meta_proj(meta).unsqueeze(1)\n        tokens = torch.cat([fused_tile, meta_tok], dim=1)\n        pooled = self.pool(tokens)\n        pooled = self.norm(pooled)\n        pred = self.head(pooled)\n        return pred\n\n# =========================================================\n# Dataset (returns image_path relative + meta)\n# =========================================================\nclass TestDataset(Dataset):\n    def __init__(self, df):\n        self.df = df.reset_index(drop=True)\n    def __len__(self):\n        return len(self.df)\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img_rel = row[\"image_path\"]\n        h = float(row[\"Height_Ave_cm\"]) if \"Height_Ave_cm\" in row else 0.0\n        ndvi = float(row[\"Pre_GSHH_NDVI\"]) if \"Pre_GSHH_NDVI\" in row else 0.0\n        return img_rel, np.array([h, ndvi], dtype=np.float32)\n\n# =========================================================\n# Load single model\n# =========================================================\ndef load_model_single(path):\n    if not os.path.exists(path):\n        raise FileNotFoundError(f\"Checkpoint not found: {path}\")\n    device = CFG.device\n    ckpt = torch.load(path, map_location=\"cpu\")\n    emb_dim = ckpt.get(\"config\", {}).get(\"emb_dim\", 768)\n    dropout = ckpt.get(\"config\", {}).get(\"dropout\", 0.2)\n    model = DINOv3CLIP_TiledMIL_Regressor(CFG.dinov3_model, CFG.clip_model, emb_dim=emb_dim, dropout=dropout, local_files=True)\n    model.load_state_dict(ckpt[\"model_state\"])\n    model.to(device)\n    model.eval()\n    return model\n\n# =========================================================\n# single-image predict with simple TTA and tile capping\n# =========================================================\ndef tta_predict_one_image(model, dino_proc, clip_proc, img: Image.Image, meta_vec,\n                          tile_size, stride, max_tiles, tta_h=True, tta_v=True):\n    device = CFG.device\n\n    def predict_for(img_obj):\n        tiles = tile_image(img_obj, tile=tile_size, stride=stride)\n        if len(tiles) > max_tiles:\n            random.seed(42)\n            tiles = random.sample(tiles, max_tiles)\n        # processors -> (T, C, H, W). add batch dim -> (1, T, C, H, W)\n        dino_px = dino_proc(images=tiles, return_tensors=\"pt\")[\"pixel_values\"].unsqueeze(0).to(device)\n        clip_px = clip_proc(images=tiles, return_tensors=\"pt\")[\"pixel_values\"].unsqueeze(0).to(device)\n        meta = torch.tensor(meta_vec, dtype=torch.float32).unsqueeze(0).to(device)\n        with torch.no_grad():\n            pred = model(dino_px, clip_px, meta)    # (1,5)\n        pred = pred.cpu().numpy()[0]\n        del dino_px, clip_px, meta\n        if device.type == \"cuda\":\n            torch.cuda.empty_cache()\n        return pred\n\n    preds = []\n    try:\n        preds.append(predict_for(img))\n    except RuntimeError as e:\n        # try CPU fallback\n        print(\"RuntimeError on GPU, retrying this image on CPU:\", e)\n        model_cpu = model.to(\"cpu\")\n        dino_proc_cpu = dino_proc\n        clip_proc_cpu = clip_proc\n        preds.append(predict_for(img))\n        model.to(CFG.device)\n\n    if tta_h:\n        preds.append(predict_for(img.transpose(Image.FLIP_LEFT_RIGHT)))\n    if tta_v:\n        preds.append(predict_for(img.transpose(Image.FLIP_TOP_BOTTOM)))\n\n    arr = np.stack(preds, axis=0)\n    return arr.mean(axis=0)\n\n# =========================================================\n# INFERENCE\n# =========================================================\n@torch.no_grad()\ndef infer():\n    # -------------------------------------------------\n    # 1) FIND CHECKPOINT\n    # -------------------------------------------------\n    ckpt = find_checkpoint()\n    if ckpt is None:\n        raise FileNotFoundError(\n            \"No checkpoint found automatically. Set CFG.ckpt_path to the .pth file path and re-run.\"\n        )\n    print(\"Using checkpoint:\", ckpt)\n\n    # -------------------------------------------------\n    # 2) LOAD PROCESSORS (LOCAL ONLY)\n    # -------------------------------------------------\n    dino_proc = AutoImageProcessor.from_pretrained(\n        CFG.dinov3_model, local_files_only=True\n    )\n    clip_proc = AutoImageProcessor.from_pretrained(\n        CFG.clip_model, local_files_only=True\n    )\n    for p in (dino_proc, clip_proc):\n        if hasattr(p, \"do_resize\"):\n            p.do_resize = False\n        if hasattr(p, \"do_center_crop\"):\n            p.do_center_crop = False\n\n    # -------------------------------------------------\n    # 3) LOAD MODEL\n    # -------------------------------------------------\n    model = load_model_single(ckpt)\n\n    # -------------------------------------------------\n    # 4) READ TEST CSV (LONG FORMAT)\n    # -------------------------------------------------\n    df_test = pd.read_csv(CFG.test_csv)\n\n    if \"image_path\" not in df_test.columns:\n        if \"image\" in df_test.columns:\n            df_test[\"image_path\"] = df_test[\"image\"]\n        else:\n            raise ValueError(\n                \"test.csv must contain 'image_path' (or 'image') column\"\n            )\n\n    # meta fallback\n    if \"Height_Ave_cm\" not in df_test.columns:\n        df_test[\"Height_Ave_cm\"] = 0.0\n    if \"Pre_GSHH_NDVI\" not in df_test.columns:\n        df_test[\"Pre_GSHH_NDVI\"] = 0.0\n\n    # -------------------------------------------------\n    # 5) CACHE PREDICTIONS PER IMAGE (KEY FIX)\n    # -------------------------------------------------\n    pred_cache = {}  # image_path -> np.array shape (5,)\n\n    # -------------------------------------------------\n    # 6) LOOP OVER TEST ROWS (NOT OVER MELTED DATA)\n    # -------------------------------------------------\n    rows = []\n\n    for _, row in tqdm(df_test.iterrows(), total=len(df_test), desc=\"Infer\"):\n        img_rel = row[\"image_path\"]\n\n        # predict ONCE per image\n        if img_rel not in pred_cache:\n            img_path = os.path.join(CFG.img_dir, img_rel)\n            try:\n                img = Image.open(img_path).convert(\"RGB\")\n            except Exception:\n                img = Image.new(\"RGB\", (CFG.tile_size, CFG.tile_size))\n\n            meta_vec = np.array(\n                [row[\"Height_Ave_cm\"], row[\"Pre_GSHH_NDVI\"]],\n                dtype=np.float32\n            )\n\n            pvec = tta_predict_one_image(\n                model,\n                dino_proc,\n                clip_proc,\n                img,\n                meta_vec,\n                tile_size=CFG.tile_size,\n                stride=CFG.stride,\n                max_tiles=CFG.max_tiles,\n                tta_h=CFG.tta_hflip,\n                tta_v=CFG.tta_vflip\n            )\n\n            pred_cache[img_rel] = pvec\n\n            gc.collect()\n            if torch.cuda.is_available():\n                torch.cuda.empty_cache()\n\n        # -------------------------------------------------\n        # 7) PICK CORRECT TARGET (NO DUPLICATION)\n        # -------------------------------------------------\n        tname = row[\"target_name\"]\n        idx = CFG.target_cols.index(tname)\n        value = float(pred_cache[img_rel][idx])\n\n        rows.append({\n            \"sample_id\": row[\"sample_id\"],\n            \"target\": value\n        })\n\n    # -------------------------------------------------\n    # 8) SAVE SUBMISSION\n    # -------------------------------------------------\n    submission = pd.DataFrame(rows)\n    submission.to_csv(CFG.out_csv, index=False)\n\n    print(\"✅ submission.csv saved (no duplicates)\")\n    return submission\n\n\n# =========================================================\n# RUN\n# =========================================================\nif __name__ == \"__main__\":\n    # if you already know exact path, set it here before running:\n    # CFG.ckpt_path = \"/kaggle/input/MODELS/best_dinov3_clip_tiled_mil (1).pth\"\n    submit_df = infer()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-19T08:41:11.528110Z","iopub.execute_input":"2026-01-19T08:41:11.528478Z","iopub.status.idle":"2026-01-19T08:41:22.372509Z","shell.execute_reply.started":"2026-01-19T08:41:11.528452Z","shell.execute_reply":"2026-01-19T08:41:22.371635Z"}},"outputs":[],"execution_count":null}]}