{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"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":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"},{"sourceId":293031629,"sourceType":"kernelVersion"},{"sourceId":268942,"sourceType":"modelInstanceVersion","modelInstanceId":230141,"modelId":251887},{"sourceId":711299,"sourceType":"modelInstanceVersion","modelInstanceId":540408,"modelId":553604},{"sourceId":719660,"sourceType":"modelInstanceVersion","modelInstanceId":547330,"modelId":560139}],"dockerImageVersionId":31236,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":441.06159,"end_time":"2026-01-05T05:15:36.197688","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2026-01-05T05:08:15.136098","version":"2.6.0"},"widgets":{"application/vnd.jupyter.widget-state+json":{"state":{"5cea4344fed44fffaac71b6fe02ac50b":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"5fd6570613f943ecb07631ffcecddacb":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"6b204ec19a394a818e17994aae3263e6":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"864a5d2d04374e688a854f72ad0d04d9":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"8f988be66966496085d6e6b8909bf0da":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_864a5d2d04374e688a854f72ad0d04d9","placeholder":"​","style":"IPY_MODEL_5cea4344fed44fffaac71b6fe02ac50b","tabbable":null,"tooltip":null,"value":" 1/1 [00:01&lt;00:00,  1.98s/it]"}},"9a1a60c1824e4af4a02be7dd08d54449":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"af818733f9ec4e22a3500a5692c607b2":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HBoxModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HBoxModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HBoxView","box_style":"","children":["IPY_MODEL_e8a7fcf1682642f9a95f077413387b58","IPY_MODEL_d548382ab4904dd9b1c2742bec3463f0","IPY_MODEL_8f988be66966496085d6e6b8909bf0da"],"layout":"IPY_MODEL_5fd6570613f943ecb07631ffcecddacb","tabbable":null,"tooltip":null}},"b695855014a24a7184902662cc419d1e":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"d372dd276f534b6ba203146ce2f78865":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"ProgressStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"ProgressStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","bar_color":null,"description_width":""}},"d548382ab4904dd9b1c2742bec3463f0":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"FloatProgressModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"FloatProgressModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"ProgressView","bar_style":"success","description":"","description_allow_html":false,"layout":"IPY_MODEL_9a1a60c1824e4af4a02be7dd08d54449","max":1,"min":0,"orientation":"horizontal","style":"IPY_MODEL_d372dd276f534b6ba203146ce2f78865","tabbable":null,"tooltip":null,"value":1}},"e8a7fcf1682642f9a95f077413387b58":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_b695855014a24a7184902662cc419d1e","placeholder":"​","style":"IPY_MODEL_6b204ec19a394a818e17994aae3263e6","tabbable":null,"tooltip":null,"value":"100%"}}},"version_major":2,"version_minor":0}}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"\"\"\"\nCSIRO Image2Biomass - Clean Inference Pipeline\nSigLIP + DinoV2 Ensemble for biomass prediction\n\"\"\"\n\nimport os\nimport gc\nimport math\nimport random\nimport warnings\nfrom pathlib import Path\nfrom copy import deepcopy\nfrom dataclasses import dataclass\n\nimport cv2\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom PIL import Image\nfrom torch.optim.lr_scheduler import LambdaLR\nfrom torch.utils.data import Dataset, DataLoader\nfrom tqdm.auto import tqdm\n\nimport timm\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nfrom timm.utils import ModelEmaV2\n\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.decomposition import PCA\nfrom sklearn.cross_decomposition import PLSRegression\nfrom sklearn.mixture import GaussianMixture\nfrom sklearn.ensemble import GradientBoostingRegressor, HistGradientBoostingRegressor\nfrom lightgbm import LGBMRegressor\nfrom catboost import CatBoostRegressor\nfrom transformers import AutoModel, AutoImageProcessor, AutoTokenizer\n\nwarnings.filterwarnings('ignore')\nos.environ[\"TOKENIZERS_PARALLELISM\"] = \"false\"\n\ntorch.backends.cuda.matmul.allow_tf32 = True\ntorch.backends.cudnn.allow_tf32 = True\ntorch.backends.cudnn.benchmark = True\ntorch.set_float32_matmul_precision(\"high\")\n\n# =============================================================================\n# CONFIGURATION\n# =============================================================================\n@dataclass\nclass SigLIPConfig:\n    \"\"\"Config for SigLIP + ML pipeline\"\"\"\n    DATA_PATH: Path = Path(\"/kaggle/input/csiro-biomass/\")\n    SPLIT_PATH: Path = Path(\"/kaggle/input/csiro-datasplit/csiro_data_split.csv\")\n    SIGLIP_PATH: str = \"/kaggle/input/google-siglip-so400m-patch14-384/transformers/default/1\"\n    SEED: int = 42\n    DEVICE: str = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n    PATCH_SIZE: int = 520\n    OVERLAP: int = 16\n    TARGET_NAMES = ['Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'Dry_Total_g', 'GDM_g']\n    TARGET_MAX = {\n        \"Dry_Clover_g\": 71.7865, \"Dry_Dead_g\": 83.8407, \"Dry_Green_g\": 157.9836,\n        \"Dry_Total_g\": 185.70, \"GDM_g\": 157.9836,\n    }\n\n\nclass DinoConfig:\n    \"\"\"Config for DinoV2 ViT pipeline\"\"\"\n    CREATE_SUBMISSION = True\n    BASE_PATH = '/kaggle/input/csiro-biomass'\n    SEED = 82947501\n    FOLDS_TO_TRAIN = [0, 1, 2, 3, 4]\n    TEST_IMAGE_DIR = '/kaggle/input/csiro-biomass/test'\n    TEST_CSV = '/kaggle/input/csiro-biomass/test.csv'\n    SUBMISSION_DIR = './'\n    MODEL_DIR = '/kaggle/input/baseline-dinov3/pytorch/default/1/baseline-dinov3'\n    N_FOLDS = 5\n    MODEL_NAME = 'vit_huge_plus_patch16_dinov3.lvd1689m'\n    BACKBONE_PATH = '/kaggle/input/vit-huge-plus-patch16-dinov3-lvd1689m/vit_huge_plus_patch16_dinov3.lvd1689m_backbone.pth'\n    DINO_GRAD_CHECKPOINTING = False\n    IMG_SIZE = 512\n    TTA_STEPS = 1\n    BATCH_SIZE = 1\n    NUM_WORKERS = 4\n    ALL_TARGET_COLS = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n    R2_WEIGHTS = np.array([0.1, 0.1, 0.1, 0.2, 0.5])\n    LOSS_WEIGHTS = np.array([0.1, 0.1, 0.1, 0.0, 0.0])\n    DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\n\ncfg_siglip = SigLIPConfig()\nCFG = DinoConfig()\n\nALL_TARGETS = ['Dry_Green_g', 'Dry_Clover_g', 'Dry_Dead_g', 'GDM_g', 'Dry_Total_g']\n\n\ndef seeding(SEED):\n    np.random.seed(SEED)\n    random.seed(SEED)\n    os.environ['PYTHONHASHSEED'] = str(SEED)\n    torch.manual_seed(SEED)\n    if torch.cuda.is_available():\n        torch.cuda.manual_seed(SEED)\n        torch.backends.cudnn.deterministic = True\n        torch.backends.cudnn.benchmark = False\n\n\nseeding(cfg_siglip.SEED)\n\n# =============================================================================\n# DATA UTILS\n# =============================================================================\ndef pivot_table(df: pd.DataFrame) -> pd.DataFrame:\n    if 'target' in df.columns.tolist():\n        df_pt = pd.pivot_table(\n            df, values='target',\n            index=['image_path', 'Sampling_Date', 'State', 'Species', 'Pre_GSHH_NDVI', 'Height_Ave_cm'],\n            columns='target_name', aggfunc='mean'\n        ).reset_index()\n    else:\n        df['target'] = 0\n        df_pt = pd.pivot_table(df, values='target', index='image_path', columns='target_name', aggfunc='mean').reset_index()\n    return df_pt\n\n\ndef melt_table(df: pd.DataFrame) -> pd.DataFrame:\n    melted = df.melt(id_vars='image_path', value_vars=cfg_siglip.TARGET_NAMES, var_name='target_name', value_name='target')\n    melted['sample_id'] = melted['image_path'].str.replace(r'^.*/', '', regex=True).str.replace('.jpg', '', regex=False) + '__' + melted['target_name']\n    return melted[['sample_id', 'target']]\n\n\ndef post_process_biomass(df_preds):\n    ordered_cols = [\"Dry_Green_g\", \"Dry_Clover_g\", \"Dry_Dead_g\", \"GDM_g\", \"Dry_Total_g\"]\n    for c in ordered_cols:\n        if c not in df_preds.columns:\n            df_preds[c] = 0.0\n    df_out = df_preds.copy()\n    df_out['Dry_Clover_g'] = 0.0\n    df_out['GDM_g'] = df_out['Dry_Green_g'] + df_out['Dry_Clover_g']\n    df_out['Dry_Total_g'] = df_out['GDM_g'] + df_out['Dry_Dead_g']\n    df_out['GDM_g'] = df_out['GDM_g'].clip(lower=0.0)\n    df_out['Dry_Total_g'] = df_out['Dry_Total_g'].clip(lower=0.0)\n    return df_out\n\n\n# =============================================================================\n# SIGLIP PIPELINE\n# =============================================================================\ndef split_image(image, patch_size=520, overlap=16):\n    h, w, c = image.shape\n    stride = patch_size - overlap\n    patches = []\n    for y in range(0, h, stride):\n        for x in range(0, w, stride):\n            y2 = min(y + patch_size, h)\n            x2 = min(x + patch_size, w)\n            y1 = max(0, y2 - patch_size)\n            x1 = max(0, x2 - patch_size)\n            patch = image[y1:y2, x1:x2, :]\n            patches.append(patch)\n    return patches\n\n\ndef compute_embeddings(model_path, df, cfg):\n    print(f\"Computing Embeddings for {len(df)} images...\")\n    model = AutoModel.from_pretrained(model_path, local_files_only=True).eval().to(cfg.DEVICE)\n    processor = AutoImageProcessor.from_pretrained(model_path)\n    EMBEDDINGS = []\n    for _, row in tqdm(df.iterrows(), total=len(df)):\n        try:\n            img = cv2.imread(row['image_path'])\n            if img is None:\n                raise ValueError(\"Image not found\")\n            img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n            patches = split_image(img, patch_size=cfg.PATCH_SIZE, overlap=cfg.OVERLAP)\n            images = [Image.fromarray(p) for p in patches]\n            inputs = processor(images=images, return_tensors=\"pt\").to(cfg.DEVICE)\n            with torch.no_grad():\n                features = model.get_image_features(**inputs)\n            avg_embed = features.mean(dim=0).cpu().numpy()\n            EMBEDDINGS.append(avg_embed)\n        except Exception as e:\n            print(f\"Error processing {row['image_path']}: {e}\")\n            EMBEDDINGS.append(np.zeros(1152))\n    torch.cuda.empty_cache()\n    return np.stack(EMBEDDINGS)\n\n\ndef generate_semantic_features(image_embeddings_np, model_path, cfg):\n    print(\"Generating Semantic Features...\")\n    model = AutoModel.from_pretrained(model_path).to(cfg.DEVICE)\n    tokenizer = AutoTokenizer.from_pretrained(model_path)\n    concept_groups = {\n        \"bare\": [\"bare soil\", \"dirt ground\", \"sparse vegetation\", \"exposed earth\"],\n        \"sparse\": [\"low density pasture\", \"thin grass\", \"short clipped grass\"],\n        \"medium\": [\"average pasture cover\", \"medium height grass\", \"grazed pasture\"],\n        \"dense\": [\"dense tall pasture\", \"thick grassy volume\", \"high biomass\", \"overgrown vegetation\"],\n        \"green\": [\"lush green vibrant pasture\", \"photosynthesizing leaves\", \"fresh growth\"],\n        \"dead\": [\"dry brown dead grass\", \"yellow straw\", \"senesced material\", \"standing hay\"],\n        \"clover\": [\"white clover\", \"trifolium repens\", \"broadleaf legume\", \"clover flowers\"],\n        \"grass\": [\"ryegrass\", \"blade-like leaves\", \"fescue\", \"grassy sward\"]\n    }\n    concept_vectors = {}\n    with torch.no_grad():\n        for name, prompts in concept_groups.items():\n            inputs = tokenizer(prompts, padding=\"max_length\", return_tensors=\"pt\").to(cfg.DEVICE)\n            emb = model.get_text_features(**inputs)\n            emb = emb / emb.norm(p=2, dim=-1, keepdim=True)\n            concept_vectors[name] = emb.mean(dim=0, keepdim=True)\n    img_tensor = torch.tensor(image_embeddings_np, dtype=torch.float32).to(cfg.DEVICE)\n    img_tensor = img_tensor / img_tensor.norm(p=2, dim=-1, keepdim=True)\n    scores = {}\n    for name, vec in concept_vectors.items():\n        scores[name] = torch.matmul(img_tensor, vec.T).cpu().numpy().flatten()\n    df_scores = pd.DataFrame(scores)\n    df_scores['ratio_greenness'] = df_scores['green'] / (df_scores['green'] + df_scores['dead'] + 1e-6)\n    df_scores['ratio_clover'] = df_scores['clover'] / (df_scores['clover'] + df_scores['grass'] + 1e-6)\n    df_scores['ratio_cover'] = (df_scores['dense'] + df_scores['medium']) / (df_scores['bare'] + df_scores['sparse'] + 1e-6)\n    torch.cuda.empty_cache()\n    return df_scores.values\n\n\nclass SupervisedEmbeddingEngine:\n    def __init__(self, n_pca=0.80, n_pls=8, n_gmm=6, random_state=42):\n        self.scaler = StandardScaler()\n        self.pca = PCA(n_components=n_pca, random_state=random_state)\n        self.pls = PLSRegression(n_components=n_pls, scale=False)\n        self.gmm = GaussianMixture(n_components=n_gmm, covariance_type='diag', random_state=random_state)\n        self.pls_fitted_ = False\n\n    def fit(self, X, y=None, X_semantic=None):\n        X_scaled = self.scaler.fit_transform(X)\n        self.pca.fit(X_scaled)\n        self.gmm.fit(X_scaled)\n        if y is not None:\n            self.pls.fit(X_scaled, y)\n            self.pls_fitted_ = True\n        return self\n\n    def transform(self, X, X_semantic=None):\n        X_scaled = self.scaler.transform(X)\n        features = [self.pca.transform(X_scaled)]\n        if self.pls_fitted_:\n            features.append(self.pls.transform(X_scaled))\n        features.append(self.gmm.predict_proba(X_scaled))\n        if X_semantic is not None:\n            sem_norm = (X_semantic - np.mean(X_semantic, axis=0)) / (np.std(X_semantic, axis=0) + 1e-6)\n            features.append(sem_norm)\n        return np.hstack(features)\n\n\ndef cross_validate_predict(model_cls, model_params, train_data, test_data, sem_tr, sem_te, feature_engine, emb_cols, cfg):\n    target_max_arr = np.array([cfg.TARGET_MAX[t] for t in cfg.TARGET_NAMES], dtype=float)\n    y_pred_test_accum = np.zeros([len(test_data), len(cfg.TARGET_NAMES)], dtype=float)\n    n_splits = int(train_data['fold'].nunique())\n    X_train_full = train_data[emb_cols].values.astype(np.float32)\n    X_test_raw = test_data[emb_cols].values.astype(np.float32)\n    y_train_full = train_data[cfg.TARGET_NAMES].values.astype(np.float32)\n\n    for fold in range(n_splits):\n        print(f\"Processing Fold {fold}...\")\n        train_mask = train_data['fold'] != fold\n        X_tr = X_train_full[train_mask]\n        y_tr = y_train_full[train_mask] / target_max_arr\n        sem_tr_fold = sem_tr[train_mask]\n        engine = deepcopy(feature_engine)\n        engine.fit(X_tr, y=y_tr, X_semantic=sem_tr_fold)\n        x_tr_eng = engine.transform(X_tr, X_semantic=sem_tr_fold)\n        x_te_eng = engine.transform(X_test_raw, X_semantic=sem_te)\n        fold_test_pred = np.zeros([len(test_data), len(cfg.TARGET_NAMES)])\n        for k in range(len(cfg.TARGET_NAMES)):\n            target_name = cfg.TARGET_NAMES[k]\n            if target_name == 'Dry_Clover_g':\n                fold_test_pred[:, k] = 0.0\n            else:\n                model = model_cls(**model_params)\n                model.fit(x_tr_eng, y_tr[:, k])\n                pred_raw = model.predict(x_te_eng)\n                fold_test_pred[:, k] = pred_raw * target_max_arr[k]\n        y_pred_test_accum += fold_test_pred\n    return y_pred_test_accum / n_splits\n\n\n# =============================================================================\n# DINO PIPELINE - Model\n# =============================================================================\ndef clean_image(img):\n    h, w = img.shape[:2]\n    img = img[0:int(h * 0.90), :]\n    hsv = cv2.cvtColor(img, cv2.COLOR_RGB2HSV)\n    lower = np.array([5, 150, 150])\n    upper = np.array([25, 255, 255])\n    mask = cv2.inRange(hsv, lower, upper)\n    mask = cv2.dilate(mask, np.ones((3, 3), np.uint8), iterations=2)\n    if np.sum(mask) > 0:\n        img = cv2.inpaint(img, mask, 3, cv2.INPAINT_TELEA)\n    return img\n\n\nclass LocalMambaBlock(nn.Module):\n    def __init__(self, dim, kernel_size=5, dropout=0.0):\n        super().__init__()\n        self.norm = nn.LayerNorm(dim)\n        self.dwconv = nn.Conv1d(dim, dim, kernel_size=kernel_size, padding=kernel_size // 2, groups=dim)\n        self.gate = nn.Linear(dim, dim)\n        self.proj = nn.Linear(dim, dim)\n        self.drop = nn.Dropout(dropout)\n\n    def forward(self, x):\n        shortcut = x\n        x = self.norm(x)\n        g = torch.sigmoid(self.gate(x))\n        x = x * g\n        x = x.transpose(1, 2)\n        x = self.dwconv(x)\n        x = x.transpose(1, 2)\n        x = self.proj(x)\n        x = self.drop(x)\n        return shortcut + x\n\n\nclass BiomassModel(nn.Module):\n    def __init__(self, model_name, pretrained=True, backbone_path=None):\n        super().__init__()\n        self.model_name = model_name\n        self.backbone_path = backbone_path\n        self.backbone = timm.create_model(self.model_name, pretrained=False, num_classes=0, global_pool='')\n        if hasattr(self.backbone, 'set_grad_checkpointing') and CFG.DINO_GRAD_CHECKPOINTING:\n            self.backbone.set_grad_checkpointing(True)\n        nf = self.backbone.num_features\n        self.fusion = nn.Sequential(\n            LocalMambaBlock(nf, kernel_size=5, dropout=0.1),\n            LocalMambaBlock(nf, kernel_size=5, dropout=0.1)\n        )\n        self.pool = nn.AdaptiveAvgPool1d(1)\n        self.head_green_raw = nn.Sequential(nn.Linear(nf, nf // 2), nn.GELU(), nn.Dropout(0.2), nn.Linear(nf // 2, 1), nn.Softplus())\n        self.head_clover_raw = nn.Sequential(nn.Linear(nf, nf // 2), nn.GELU(), nn.Dropout(0.2), nn.Linear(nf // 2, 1), nn.Softplus())\n        self.head_dead_raw = nn.Sequential(nn.Linear(nf, nf // 2), nn.GELU(), nn.Dropout(0.2), nn.Linear(nf // 2, 1), nn.Softplus())\n        if pretrained:\n            self.load_pretrained()\n\n    def load_pretrained(self):\n        try:\n            if self.backbone_path and os.path.exists(self.backbone_path):\n                sd = torch.load(self.backbone_path, map_location='cpu')\n                if 'model' in sd:\n                    sd = sd['model']\n                elif 'state_dict' in sd:\n                    sd = sd['state_dict']\n            else:\n                sd = timm.create_model(self.model_name, pretrained=True, num_classes=0, global_pool='').state_dict()\n            self.backbone.load_state_dict(sd, strict=False)\n            print('Pretrained weights loaded.')\n        except Exception as e:\n            print(f'Warning: pretrained load failed: {e}')\n\n    def forward(self, left, right):\n        x_l = self.backbone(left)\n        x_r = self.backbone(right)\n        x_cat = torch.cat([x_l, x_r], dim=1)\n        x_fused = self.fusion(x_cat)\n        x_pool = self.pool(x_fused.transpose(1, 2)).flatten(1)\n        green = self.head_green_raw(x_pool)\n        clover = self.head_clover_raw(x_pool)\n        dead = self.head_dead_raw(x_pool)\n        gdm = green + clover\n        total = gdm + dead\n        return total, gdm, green, clover, dead\n\n\n# =============================================================================\n# DINO PIPELINE - Dataset & Inference\n# =============================================================================\nclass BiomassTestDataset(Dataset):\n    def __init__(self, img_dir):\n        self.img_dir = img_dir\n        self.paths = sorted([os.path.join(img_dir, f) for f in os.listdir(img_dir) if f.lower().endswith(('.jpg', '.jpeg', '.png'))])\n        self.filenames = [os.path.basename(p) for p in self.paths]\n\n    def __len__(self):\n        return len(self.paths)\n\n    def __getitem__(self, idx):\n        path = self.paths[idx]\n        img = cv2.imread(path)\n        if img is None:\n            img = np.zeros((1000, 2000, 3), dtype=np.uint8)\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        img = clean_image(img)\n        h, w = img.shape[:2]\n        mid = w // 2\n        left = img[:, :mid].copy()\n        right = img[:, mid:].copy()\n        return left, right, self.filenames[idx]\n\n\ndef get_tta_transforms(num_transforms):\n    normalize = A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n    to_tensor = ToTensorV2()\n    all_tta = [\n        A.Compose([A.Resize(CFG.IMG_SIZE, CFG.IMG_SIZE), normalize, to_tensor]),\n        A.Compose([A.Resize(CFG.IMG_SIZE, CFG.IMG_SIZE), A.HorizontalFlip(p=1.0), normalize, to_tensor]),\n        A.Compose([A.Resize(CFG.IMG_SIZE, CFG.IMG_SIZE), A.VerticalFlip(p=1.0), normalize, to_tensor]),\n        A.Compose([A.Resize(CFG.IMG_SIZE, CFG.IMG_SIZE), A.HorizontalFlip(p=1.0), A.VerticalFlip(p=1.0), normalize, to_tensor]),\n    ]\n    return all_tta[:num_transforms]\n\n\n@torch.no_grad()\ndef predict_with_tta(model, left_np, right_np, tta_transforms):\n    all_tta_preds = []\n    for tfm in tta_transforms:\n        left_tensor = tfm(image=left_np)['image'].unsqueeze(0).to(CFG.DEVICE)\n        right_tensor = tfm(image=right_np)['image'].unsqueeze(0).to(CFG.DEVICE)\n        total, gdm, green, clover, dead = model(left_tensor, right_tensor)\n        all_tta_preds.append([total.cpu().item(), gdm.cpu().item(), green.cpu().item()])\n    return np.mean(all_tta_preds, axis=0)\n\n\ndef run_dino_inference():\n    print(\"\\n\" + \"=\" * 70)\n    print(\"DINO INFERENCE\")\n    print(\"=\" * 70)\n    dataset = BiomassTestDataset(CFG.TEST_IMAGE_DIR)\n    loader = DataLoader(dataset, batch_size=1, shuffle=False, num_workers=CFG.NUM_WORKERS, pin_memory=True)\n    tta_transforms = get_tta_transforms(CFG.TTA_STEPS)\n    accumulated_preds = np.zeros((len(dataset), 3), dtype=np.float32)\n    folds_to_use = CFG.FOLDS_TO_TRAIN\n    filenames = dataset.filenames.copy()\n    successful_folds = 0\n\n    for fold in folds_to_use:\n        print(f\"\\nProcessing Fold {fold}...\")\n        model = BiomassModel(CFG.MODEL_NAME, pretrained=False, backbone_path=CFG.BACKBONE_PATH)\n        weight_path = os.path.join(CFG.MODEL_DIR, f'best_model_fold{fold}.pth')\n        if not os.path.exists(weight_path):\n            print(f\"Warning: Model file {weight_path} not found! Skipping fold {fold}.\")\n            del model\n            torch.cuda.empty_cache()\n            gc.collect()\n            continue\n        state = torch.load(weight_path, map_location='cpu')\n        if isinstance(state, dict) and ('model_state_dict' in state or 'state_dict' in state):\n            key = 'model_state_dict' if 'model_state_dict' in state else 'state_dict'\n            sd = state[key]\n        else:\n            sd = state\n        model.load_state_dict(sd)\n        model.to(CFG.DEVICE)\n        model.eval()\n        for i, (left, right, filename) in enumerate(tqdm(loader, desc=f\"Fold {fold}\")):\n            left_np = left[0].numpy()\n            right_np = right[0].numpy()\n            pred = predict_with_tta(model, left_np, right_np, tta_transforms)\n            accumulated_preds[i] += pred\n        successful_folds += 1\n        del model\n        torch.cuda.empty_cache()\n        gc.collect()\n\n    if successful_folds == 0:\n        raise FileNotFoundError(f\"No model weights found for requested folds: {folds_to_use}\")\n    final_predictions = accumulated_preds / successful_folds\n    print(f\"\\nInference complete. Used {successful_folds} fold(s).\")\n    return final_predictions, filenames\n\n\ndef postprocess_dino_predictions(preds_direct):\n    pred_total = preds_direct[:, 0]\n    pred_gdm = preds_direct[:, 1]\n    pred_green = preds_direct[:, 2]\n    pred_clover = np.maximum(0, pred_gdm - pred_green)\n    pred_dead = np.maximum(0, pred_total - pred_gdm)\n    return np.stack([pred_green, pred_dead, pred_clover, pred_gdm, pred_total], axis=1)\n\n\n# =============================================================================\n# ENSEMBLE\n# =============================================================================\ndef enforce_mass_balance(df_wide, fixed_clover=None):\n    ordered_cols = ['Dry_Green_g', 'Dry_Clover_g', 'Dry_Dead_g', 'GDM_g', 'Dry_Total_g']\n    Y = df_wide[ordered_cols].values.T\n    if fixed_clover:\n        clover_fixed = Y[1, :].copy()\n        Y[3, :] = Y[0, :] + clover_fixed\n        Y[4, :] = Y[3, :] + Y[2, :]\n        Y_reconciled = Y\n    else:\n        C = np.array([[1, 1, 0, -1, 0], [0, 0, 1, 1, -1]])\n        C_T = C.T\n        try:\n            inv_CCt = np.linalg.inv(C @ C_T)\n            P = np.eye(5) - C_T @ inv_CCt @ C\n        except np.linalg.LinAlgError:\n            return df_wide\n        Y_reconciled = P @ Y\n    Y_reconciled = Y_reconciled.T\n    Y_reconciled = np.maximum(0, Y_reconciled)\n    df_out = df_wide.copy()\n    df_out[ordered_cols] = Y_reconciled\n    return df_out\n\n\ndef robust_ensemble(file_paths, weights):\n    print(f\"--- Starting Ensemble ---\")\n    print(f\"Weights: {weights}\")\n    dfs = []\n    for name, path in file_paths.items():\n        if not os.path.exists(path):\n            raise FileNotFoundError(f\"Missing file: {path}\")\n        df = pd.read_csv(path).sort_values('sample_id').reset_index(drop=True)\n        dfs.append(df)\n        print(f\"Loaded {name}: {len(df)} rows\")\n\n    base_ids = dfs[0]['sample_id']\n    if not all(df['sample_id'].equals(base_ids) for df in dfs[1:]):\n        raise ValueError(\"Sample IDs do not match between submission files!\")\n\n    dfs_split = []\n    for df in dfs:\n        df_split = df.copy()\n        df_split[['image_id', 'target_name']] = df_split['sample_id'].str.rsplit('__', n=1, expand=True)\n        dfs_split.append(df_split)\n\n    dino_idx = list(file_paths.keys()).index('dino')\n    siglip_idx = list(file_paths.keys()).index('siglip')\n    dino_df = dfs_split[dino_idx]\n    siglip_df = dfs_split[siglip_idx]\n\n    ensemble_results = []\n    for target in ALL_TARGETS:\n        dino_target = dino_df[dino_df['target_name'] == target].copy()\n        siglip_target = siglip_df[siglip_df['target_name'] == target].copy()\n        if target == 'Dry_Clover_g':\n            print(f\"Using DINO-only for {target}\")\n            ensemble_results.append(dino_target[['sample_id', 'target']])\n        else:\n            merged = pd.merge(\n                dino_target[['sample_id', 'target']].rename(columns={'target': 'dino_pred'}),\n                siglip_target[['sample_id', 'target']].rename(columns={'target': 'siglip_pred'}),\n                on='sample_id'\n            )\n            w_vec = np.array([weights['dino'], weights['siglip']])\n            w_vec = w_vec / w_vec.sum()\n            merged['target'] = merged['dino_pred'] * w_vec[0] + merged['siglip_pred'] * w_vec[1]\n            ensemble_results.append(merged[['sample_id', 'target']])\n\n    ensemble_df = pd.concat(ensemble_results, ignore_index=True)\n    ensemble_df[['image_id', 'target_name']] = ensemble_df['sample_id'].str.rsplit('__', n=1, expand=True)\n    wide_df = ensemble_df.pivot(index='image_id', columns='target_name', values='target').reset_index()\n    wide_balanced = enforce_mass_balance(wide_df, fixed_clover=True)\n    long_balanced = wide_balanced.melt(id_vars='image_id', value_vars=ALL_TARGETS, var_name='target_name', value_name='target')\n    long_balanced['sample_id'] = long_balanced['image_id'] + '__' + long_balanced['target_name']\n    return long_balanced[['sample_id', 'target']].sort_values('sample_id').reset_index(drop=True)\n\n\n# =============================================================================\n# MAIN - Full Pipeline\n# =============================================================================\ndef run_siglip_pipeline():\n    \"\"\"Run SigLIP + ML models pipeline\"\"\"\n    print(\"\\n\" + \"=\" * 70)\n    print(\"SIGLIP PIPELINE\")\n    print(\"=\" * 70)\n    \n    # Load Train Data\n    print(\"Loading Data...\")\n    train_df = pd.read_csv(cfg_siglip.SPLIT_PATH)\n    cols_to_keep = [c for c in train_df.columns if not c.startswith('emb')]\n    train_df = train_df[cols_to_keep]\n    if not str(train_df['image_path'].iloc[0]).startswith('/'):\n        train_df['image_path'] = train_df['image_path'].apply(lambda p: str(cfg_siglip.DATA_PATH / 'train' / os.path.basename(p)))\n    \n    # Load Test Data\n    test_df_raw = pd.read_csv(cfg_siglip.DATA_PATH / 'test.csv')\n    test_df = pivot_table(test_df_raw)\n    test_df['image_path'] = test_df['image_path'].apply(lambda p: str(cfg_siglip.DATA_PATH / p))\n    \n    # Compute Embeddings\n    train_embeddings = compute_embeddings(cfg_siglip.SIGLIP_PATH, train_df, cfg_siglip)\n    test_embeddings = compute_embeddings(cfg_siglip.SIGLIP_PATH, test_df, cfg_siglip)\n    \n    # Feature DataFrames\n    emb_cols = [f\"emb{i}\" for i in range(train_embeddings.shape[1])]\n    train_feat_df = pd.concat([train_df, pd.DataFrame(train_embeddings, columns=emb_cols)], axis=1)\n    test_feat_df = pd.concat([test_df, pd.DataFrame(test_embeddings, columns=emb_cols)], axis=1)\n    \n    # Semantic Features\n    all_emb = np.vstack([train_embeddings, test_embeddings])\n    all_semantic = generate_semantic_features(all_emb, cfg_siglip.SIGLIP_PATH, cfg_siglip)\n    sem_train = all_semantic[:len(train_df)]\n    sem_test = all_semantic[len(train_df):]\n    \n    # Model Parameters\n    params_cat = {'iterations': 1900, 'learning_rate': 0.045, 'depth': 4, 'l2_leaf_reg': 0.56,\n                  'random_strength': 0.045, 'bagging_temperature': 0.98, 'verbose': 0, 'random_state': 42, 'allow_writing_files': False}\n    params_xgb = {'n_estimators': 1354, 'learning_rate': 0.010, 'max_depth': 3, 'subsample': 0.60, 'random_state': 42}\n    params_lgbm = {'n_estimators': 807, 'learning_rate': 0.014, 'num_leaves': 48, 'min_child_samples': 19,\n                   'subsample': 0.745, 'colsample_bytree': 0.745, 'reg_alpha': 0.21, 'reg_lambda': 3.78, 'verbose': -1, 'random_state': 42}\n    params_hist = {'max_iter': 300, 'learning_rate': 0.05, 'max_depth': None, 'l2_regularization': 0.44, 'random_state': 42}\n    \n    feat_engine = SupervisedEmbeddingEngine(n_pca=0.80, n_pls=8, n_gmm=6)\n    \n    print(\"Training & Inferring Models...\")\n    pred_hist = cross_validate_predict(HistGradientBoostingRegressor, params_hist, train_feat_df, test_feat_df, sem_train, sem_test, feat_engine, emb_cols, cfg_siglip)\n    pred_gb = cross_validate_predict(GradientBoostingRegressor, params_xgb, train_feat_df, test_feat_df, sem_train, sem_test, feat_engine, emb_cols, cfg_siglip)\n    pred_cat = cross_validate_predict(CatBoostRegressor, params_cat, train_feat_df, test_feat_df, sem_train, sem_test, feat_engine, emb_cols, cfg_siglip)\n    pred_lgbm = cross_validate_predict(LGBMRegressor, params_lgbm, train_feat_df, test_feat_df, sem_train, sem_test, feat_engine, emb_cols, cfg_siglip)\n    \n    # Ensemble\n    final_pred = (pred_hist + pred_gb + pred_cat + pred_lgbm) / 4.0\n    test_feat_df[cfg_siglip.TARGET_NAMES] = final_pred\n    test_processed = post_process_biomass(test_feat_df)\n    sub_df = melt_table(test_processed)\n    sub_df.to_csv('submission_siglip.csv', index=False)\n    print(\"✓ SigLIP submission saved: submission_siglip.csv\")\n    return sub_df\n\n\ndef run_dino_pipeline():\n    \"\"\"Run DinoV2 ViT pipeline\"\"\"\n    predictions_direct, test_filenames = run_dino_inference()\n    predictions_all = postprocess_dino_predictions(predictions_direct)\n    \n    # Create submission\n    test_df = pd.read_csv(CFG.TEST_CSV)\n    test_path_example = test_df['image_path'].iloc[0]\n    if '/' in test_path_example:\n        prefix = test_path_example.rsplit('/', 1)[0] + '/'\n        corrected_filenames = [prefix + fn for fn in test_filenames]\n    else:\n        corrected_filenames = test_filenames\n    \n    preds_wide = pd.DataFrame(predictions_all, columns=CFG.ALL_TARGET_COLS)\n    preds_wide.insert(0, 'image_path', corrected_filenames)\n    preds_long = preds_wide.melt(id_vars=['image_path'], value_vars=CFG.ALL_TARGET_COLS, var_name='target_name', value_name='target')\n    \n    submission = pd.merge(test_df[['sample_id', 'image_path', 'target_name']], preds_long, on=['image_path', 'target_name'], how='left')\n    submission = submission[['sample_id', 'target']].sort_values('sample_id').reset_index(drop=True)\n    submission.to_csv('submission_dino.csv', index=False)\n    print(\"✓ DINO submission saved: submission_dino.csv\")\n    return submission\n\n\nif __name__ == \"__main__\":\n    # Step 1: Run SigLIP pipeline\n    run_siglip_pipeline()\n    \n    # Step 2: Run DINO pipeline\n    run_dino_pipeline()\n    \n    # Step 3: Ensemble\n    W_SIGLIP = 0.1\n    W_DINO = 0.9\n    FILES = {'siglip': 'submission_siglip.csv', 'dino': 'submission_dino.csv'}\n    ensemble_weights = {'siglip': W_SIGLIP, 'dino': W_DINO}\n    \n    submission = robust_ensemble(FILES, ensemble_weights)\n    submission.to_csv('submission.csv', index=False)\n    print(f\"\\n✓ Final submission saved: submission.csv\")\n    print(submission.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-24T06:40:33.002644Z","iopub.execute_input":"2026-01-24T06:40:33.003242Z","iopub.status.idle":"2026-01-24T06:54:05.465713Z","shell.execute_reply.started":"2026-01-24T06:40:33.003210Z","shell.execute_reply":"2026-01-24T06:54:05.464653Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}