{"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":"gpu","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":14656566,"sourceType":"datasetVersion","datasetId":9363053}],"dockerImageVersionId":31260,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport sys\nimport subprocess\n\n# --- Setup ---\ntry:\n    BASE_DIR = os.path.dirname(os.path.abspath(__file__))\nexcept NameError:\n    BASE_DIR = \"/kaggle/input/final-submission/kaggle_submission\"\n\nWHEELS_DIR = os.path.join(BASE_DIR, \"wheels\")\nsys.path.append(BASE_DIR)\nsys.path.append(os.path.join(BASE_DIR, \"dinov3\"))\n\ndef install_package(package_name):\n    try:\n        subprocess.check_call([sys.executable, \"-m\", \"pip\", \"install\", \n            package_name,\n            \"--no-index\", \n            \"--find-links\", WHEELS_DIR\n        ])\n    except Exception as e:\n        print(f\"Warning: {e}\")\n\nprint(\"Installing dependencies...\")\ninstall_package(\"pillow-heif\")\ninstall_package(\"peft\")\n\n# --- Imports ---\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nfrom PIL import Image\nfrom tqdm import tqdm\nfrom scipy.stats import skew\nfrom skimage.measure import shannon_entropy\nfrom transformers import SegformerForSemanticSegmentation, SegformerImageProcessor\nfrom peft import LoraConfig, get_peft_model\nimport depth_pro\nfrom depth_pro.depth_pro import DEFAULT_MONODEPTH_CONFIG_DICT\n\n# --- Config ---\nINPUT_DIR = \"/kaggle/input/csiro-biomass/test\" \nOUTPUT_CSV = \"submission.csv\"\nMODEL_DIR = os.path.join(BASE_DIR, \"models\")\nSEG_MODEL_PATH = os.path.join(MODEL_DIR, \"segformer\")\nDEPTH_CHECKPOINT = os.path.join(MODEL_DIR, \"depth_pro.pt\")\nDINO_BACKBONE = os.path.join(MODEL_DIR, \"dinov3_backbone.pth\")\nPASTURE_MODEL = os.path.join(MODEL_DIR, \"pasture_model.pth\")\nDINO_REPO = os.path.join(BASE_DIR, \"dinov3\")\nDEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n\n# --- Models ---\nclass FusionModule(nn.Module):\n    def __init__(self, img_dim, aux_dim, hidden_dim=512):\n        super().__init__()\n        self.img_proj = nn.Linear(img_dim, hidden_dim)\n        self.aux_proj = nn.Linear(aux_dim, hidden_dim)\n        self.fusion = nn.Sequential(nn.Linear(hidden_dim * 2, hidden_dim), nn.LayerNorm(hidden_dim), nn.GELU(), nn.Dropout(0.1))\n    def forward(self, i, a):\n        return self.fusion(torch.cat([self.img_proj(i), self.aux_proj(a)], dim=1))\n\nclass PastureModel(nn.Module):\n    def __init__(self, backbone, aux_dim=18):\n        super().__init__()\n        self.backbone = backbone\n        img_dim = 768 * 2\n        fusion_dim = 512\n        self.fusion_module = FusionModule(img_dim, aux_dim, fusion_dim)\n        self.head_green = nn.Sequential(nn.Linear(fusion_dim, 256), nn.GELU(), nn.Dropout(0.2), nn.Linear(256, 1))\n        self.head_dead = nn.Sequential(nn.Linear(fusion_dim, 128), nn.GELU(), nn.Linear(128, 1))\n        self.head_clover_cls = nn.Linear(fusion_dim, 1)\n        self.head_clover_reg = nn.Linear(fusion_dim, 1)\n        self.head_physics = nn.Linear(fusion_dim, 2)\n        self.head_species = nn.Linear(fusion_dim, 5)\n    def forward(self, l, r, a):\n        f = self.fusion_module(torch.cat([self.backbone(l), self.backbone(r)], dim=1), a)\n        return {\"green\": self.head_green(f), \"dead\": self.head_dead(f), \"clover_logit\": self.head_clover_cls(f), \"clover_amt\": self.head_clover_reg(f), \"physics\": self.head_physics(f), \"species_logits\": self.head_species(f)}\n\ndef load_segformer_safe(p, dev):\n    try:\n        return SegformerForSemanticSegmentation.from_pretrained(p).to(dev), SegformerImageProcessor.from_pretrained(p)\n    except:\n        import shutil, json\n        t = \"/tmp/segformer_fix\"; [os.makedirs(t, exist_ok=True) if not os.path.exists(t) else None]\n        for f in os.listdir(p): [shutil.copy2(os.path.join(p, f), os.path.join(t, f)) if f.endswith((\".bin\", \".json\", \".safetensors\")) else None]\n        with open(os.path.join(t, \"preprocessor_config.json\"), \"w\") as f: json.dump({\"do_normalize\": True, \"do_reduce_labels\": False, \"do_rescale\": True, \"do_resize\": True, \"image_mean\": [0.485, 0.456, 0.406], \"image_processor_type\": \"SegformerImageProcessor\", \"image_std\": [0.229, 0.224, 0.225], \"resample\": 2, \"rescale_factor\": 0.00392156862745098, \"size\": {\"height\": 512, \"width\": 512}}, f)\n        return SegformerForSemanticSegmentation.from_pretrained(t).to(dev), SegformerImageProcessor.from_pretrained(t)\n\ndef load_models():\n    sm, sp = load_segformer_safe(SEG_MODEL_PATH, DEVICE); sm.eval()\n    dc = DEFAULT_MONODEPTH_CONFIG_DICT; dc.checkpoint_uri = DEPTH_CHECKPOINT\n    dm, dt = depth_pro.create_model_and_transforms(config=dc, device=DEVICE, precision=torch.half); dm.eval()\n    bb = torch.hub.load(DINO_REPO, \"dinov3_vitb16\", source='local', weights=DINO_BACKBONE)\n    bb = get_peft_model(bb, LoraConfig(r=32, lora_alpha=64, target_modules=[\"qkv\", \"proj\", \"w1\", \"w2\", \"w3\"], lora_dropout=0.05, bias=\"none\")).to(DEVICE)\n    m = PastureModel(bb).to(DEVICE); m.load_state_dict(torch.load(PASTURE_MODEL, map_location=DEVICE)); m.eval()\n    from torchvision import transforms\n    tr = transforms.Compose([transforms.Resize((518, 518)), transforms.ToTensor(), transforms.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225))])\n    return sm, sp, dm, dt, m, tr\n\ndef process_image(p, mods):\n    sm, sp, dm, dt, m, tr = mods\n    try:\n        img = Image.open(p).convert(\"RGB\"); ow, oh = img.size\n        # Seg\n        rs = img.resize((1024, 512), Image.Resampling.BILINEAR)\n        masks = []\n        with torch.amp.autocast('cuda') if torch.cuda.is_available() else torch.autocast('cpu'):\n            for part in [rs.crop((0, 0, 512, 512)), rs.crop((512, 0, 1024, 512))]:\n                logits = sm(**sp(images=part, return_tensors=\"pt\").to(DEVICE)).logits\n                masks.append(torch.nn.functional.interpolate(logits, size=(512, 512), mode=\"bilinear\", align_corners=False).argmax(dim=1).squeeze().cpu().numpy())\n        fm = np.concatenate(masks, axis=1); tp = fm.size\n        cr = np.sum(fm == 1) / tp; gr = np.sum(fm == 2) / tp; wr = np.sum(fm == 3) / tp; dr = np.sum(fm == 4) / tp; sr = np.sum(fm == 0) / tp\n        # Depth\n        with torch.no_grad():\n            with torch.amp.autocast('cuda') if torch.cuda.is_available() else torch.autocast('cpu'):\n                dep = dm.infer(dt(img).unsqueeze(0).to(DEVICE), f_px=torch.tensor([1570.0 * (ow / 2000.0)], device=DEVICE))[\"depth\"].float().cpu().numpy()\n        mr = np.array(Image.fromarray(fm.astype(np.uint8)).resize((dep.shape[1], dep.shape[0]), Image.Resampling.NEAREST))\n        sd = dep[mr == 0]; ch = np.median(sd) if len(sd) > 0 else np.median(dep)\n        nh = np.clip(ch - dep, 0, None).astype(np.float32); vm = np.isin(mr, [1, 2, 3, 4])\n        if np.any(vm):\n            vv = nh[vm]; mn = float(np.mean(vv)); st = float(np.std(vv)); p95 = float(np.percentile(vv, 95)); sk = float(skew(vv))\n            ia = np.array(img.resize((dep.shape[1], dep.shape[0]), Image.Resampling.BILINEAR)); vg = ia[:, :, 1][vm]\n            te = float(shannon_entropy(vg)); sh = np.sum(vg < 40) / len(vg)\n        else: mn, st, p95, sk, te, sh = 0.0, 0.0, 0.0, 0.0, 0.0, 0.0\n        clh = float(np.mean(nh[mr == 1])) if np.any(mr == 1) else 0.0\n        grh = float(np.mean(nh[mr == 2])) if np.any(mr == 2) else 0.0\n        deh = float(np.mean(nh[mr == 4])) if np.any(mr == 4) else 0.0\n        aux = torch.tensor([cr, gr, wr, dr, sr, clh, grh, deh, cr*clh, gr*grh, dr*deh, mn, st, p95, sk, te, sh, mn*(cr+gr+wr)], dtype=torch.float32).unsqueeze(0).to(DEVICE)\n        # 2x TTA\n        vars = [img, img.transpose(Image.FLIP_LEFT_RIGHT)]\n        ag, ad, acl, aca = 0, 0, 0, 0\n        for v in vars:\n            w, h = v.size; lc = tr(v.crop((0, 0, h, h))).unsqueeze(0).to(DEVICE); rc = tr(v.crop((w-h, 0, w, h))).unsqueeze(0).to(DEVICE)\n            with torch.no_grad():\n                with torch.amp.autocast('cuda') if torch.cuda.is_available() else torch.autocast('cpu'):\n                    ps = m(lc, rc, aux)\n            ag += ps['green']; ad += ps['dead']; acl += ps['clover_logit']; aca += ps['clover_amt']\n        rg = max(0, torch.expm1(ag/2.0).cpu().numpy()[0][0]); rd = max(0, torch.expm1(ad/2.0).cpu().numpy()[0][0])\n        cp = torch.sigmoid(acl/2.0).cpu().numpy()[0][0]; ca = max(0, torch.expm1(aca/2.0).cpu().numpy()[0][0])\n        rc = 0.0 if cp < 0.5 else ca\n        # 2. Additive Bias Correction (Heavy Optimization)\n        corr_g = max(0, rg + 3.99)\n        corr_d = max(0, rd - 5.08)\n        corr_c = max(0, rc - 3.89)\n        \n        # 2.1 Conditional Interactions (Heavy Optimization)\n        # Green -> Dead\n        if corr_g > 10.38:\n            corr_d += 7.27\n            \n        # Clover -> Dead\n        if corr_c > 35.52:\n            corr_d += 0.70\n            \n        # Total -> Dead\n        temp_total_check = corr_g + corr_d + corr_c\n        if temp_total_check > 26.08:\n            corr_d -= 4.52\n            \n        # Re-clamp\n        corr_g = max(0, corr_g)\n        corr_d = max(0, corr_d)\n        corr_c = max(0, corr_c)\n        \n        # 3. Saturation Correction (Heavy Optimization)\n        # Rule: If Total > 51.91, Scale by 1.032x\n        temp_total = corr_g + corr_d + corr_c\n        \n        if temp_total > 51.91:\n            scale = 1.032\n            fg = corr_g * scale\n            fd = corr_d * scale\n            fc = corr_c * scale\n        else:\n            fg = corr_g\n            fd = corr_d\n            fc = corr_c\n        return {\"Dry_Total_g\": fg+fd+fc, \"Dry_Green_g\": fg, \"Dry_Dead_g\": fd, \"Dry_Clover_g\": fc, \"GDM_g\": fg+fc}\n    except Exception as e:\n        print(f\"Err: {e}\"); return None\n\ndef main():\n    if not os.path.exists(INPUT_DIR): return\n    ms = load_models(); img_files = [f for f in os.listdir(INPUT_DIR) if f.lower().endswith(('.jpg', '.jpeg', '.png'))]\n    results = []\n    for fn in tqdm(img_files):\n        p = process_image(os.path.join(INPUT_DIR, fn), ms)\n        if p:\n            bid = os.path.splitext(fn)[0]\n            for tn, val in p.items(): results.append({\"sample_id\": f\"{bid}__{tn}\", \"target\": val})\n    pd.DataFrame(results).to_csv(OUTPUT_CSV, index=False)\n\nif __name__ == \"__main__\":\n    main()","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-01-28T22:21:07.111593Z","iopub.execute_input":"2026-01-28T22:21:07.112148Z","iopub.status.idle":"2026-01-28T22:21:33.831780Z","shell.execute_reply.started":"2026-01-28T22:21:07.112120Z","shell.execute_reply":"2026-01-28T22:21:33.831152Z"}},"outputs":[],"execution_count":null}]}