{"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,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":13765784,"sourceType":"datasetVersion","datasetId":8760675,"isSourceIdPinned":true},{"sourceId":13767723,"sourceType":"datasetVersion","datasetId":8762132}],"dockerImageVersionId":31193,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import torch\nimport numpy as np\nimport pandas as pd\nimport os\nfrom PIL import Image\nfrom torchvision import transforms\nimport timm\nimport torch.nn as nn\nimport joblib\n\nfrom skimage.color import rgb2hsv, rgb2gray\nfrom skimage.feature import graycomatrix, graycoprops\n\ndevice = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(\"Running on:\", device)\n\n# ============================================================\n# 1) Load PCA + scalers\n# ============================================================\npca = joblib.load(\"/kaggle/input/d/nuwaithet/biomass-pca-models/pca_model.pkl\")\nscaler_eng = joblib.load(\"/kaggle/input/d/nuwaithet/biomass-pca-models/scaler_eng.pkl\")\nscaler_emb = joblib.load(\"/kaggle/input/d/nuwaithet/biomass-pca-models/scaler_emb.pkl\")\n\n# ============================================================\n# LOAD EfficientNet-B0 *WITHOUT INTERNET* using your saved weights\n# ============================================================\neffnet = timm.create_model(\"tf_efficientnet_b0\", pretrained=False, num_classes=0)\neffnet.load_state_dict(torch.load(\n    \"/kaggle/input/d/nuwaithet/biomass-pca-models/effb0_embed_weights.pth\",\n    map_location=device\n))\neffnet = effnet.to(device)\neffnet.eval()\n\n# =====================================================================================\n# 2) Feature extraction — EXACT MATCH with training functions\n# =====================================================================================\n\ndef compute_exg_features(img):\n    img = img.astype(np.float32) / 255.0\n    R, G, B = img[:,:,0], img[:,:,1], img[:,:,2]\n\n    ExG = 2*G - R - B\n    ExG_norm = (ExG - ExG.min()) / (ExG.max() - ExG.min() + 1e-6)\n\n    mask = ExG_norm > 0.35\n    veg_pct = mask.mean()\n    soil_pct = 1.0 - veg_pct\n    return veg_pct, soil_pct\n\n\ndef compute_color_features(img):\n    img_f = img.astype(np.float32) / 255.0\n    hsv = rgb2hsv(img_f)\n\n    hue_mean = hsv[:,:,0].mean()\n    sat_mean = hsv[:,:,1].mean()\n    val_mean = hsv[:,:,2].mean()\n\n    R, G, B = img_f[:,:,0], img_f[:,:,1], img_f[:,:,2]\n    gr_ratio = G.mean() / (R.mean() + 1e-6)\n\n    return hue_mean, sat_mean, val_mean, gr_ratio\n\n\ndef compute_texture_features(img):\n    gray = (rgb2gray(img) * 255).astype(np.uint8)\n\n    glcm = graycomatrix(gray, distances=[5], angles=[0], symmetric=True, normed=True)\n\n    contrast      = graycoprops(glcm, 'contrast')[0,0]\n    energy        = graycoprops(glcm, 'energy')[0,0]\n    homogeneity   = graycoprops(glcm, 'homogeneity')[0,0]\n    dissimilarity = graycoprops(glcm, 'dissimilarity')[0,0]\n\n    p = glcm[:,:,0,0]\n    entropy = -np.sum(p * np.log2(p + 1e-12))\n\n    return contrast, energy, homogeneity, dissimilarity, entropy\n\n\ndef extract_features_from_image(img_pil):\n    img = np.array(img_pil.convert(\"RGB\"))\n    veg_pct, soil_pct = compute_exg_features(img)\n    hue_mean, sat_mean, val_mean, gr_ratio = compute_color_features(img)\n    contrast, energy, homogeneity, dissimilarity, entropy = compute_texture_features(img)\n\n    return {\n        \"veg_pct\": veg_pct,\n        \"soil_pct\": soil_pct,\n        \"hue_mean\": hue_mean,\n        \"sat_mean\": sat_mean,\n        \"val_mean\": val_mean,\n        \"gr_ratio\": gr_ratio,\n        \"glcm_contrast\": contrast,\n        \"glcm_energy\": energy,\n        \"glcm_homogeneity\": homogeneity,\n        \"glcm_dissimilarity\": dissimilarity,\n        \"glcm_entropy\": entropy\n    }\n\n\ndef get_pca_embedding(img_pil):\n    arr = np.array(img_pil.resize((224,224))).astype(np.float32) / 255.0\n    tensor = torch.tensor(arr).permute(2,0,1).unsqueeze(0).to(device)\n\n    with torch.no_grad():\n        raw_emb = effnet(tensor).cpu().numpy()   # (1,1024)\n\n    return pca.transform(raw_emb)[0]             # (64,)\n\n# =====================================================================================\n# 3) Teacher + Student models (exact match from training)\n# =====================================================================================\n\nclass TeacherModel(nn.Module):\n    def __init__(self, meta_dim, out_dim=5):\n        super().__init__()\n        self.backbone = timm.create_model(\"convnext_base\", pretrained=False, num_classes=0)\n        img_dim = self.backbone.num_features\n\n        self.meta_mlp = nn.Sequential(\n            nn.Linear(meta_dim, 256), nn.ReLU(),\n            nn.Linear(256, 256), nn.ReLU()\n        )\n        self.head = nn.Sequential(\n            nn.Linear(img_dim + 256, 512), nn.ReLU(),\n            nn.Linear(512, out_dim)\n        )\n        self.softplus = nn.Softplus()\n\n    def forward(self, img, meta):\n        fi = self.backbone(img)\n        fm = self.meta_mlp(meta)\n        z = torch.cat([fi, fm], dim=1)\n        return self.softplus(self.head(z))\n\n\nclass StudentConvNeXtBase(nn.Module):\n    def __init__(self, backbone=\"convnext_base\", out_dim=5):\n        super().__init__()\n        self.backbone = timm.create_model(backbone, pretrained=False, num_classes=0)\n        img_dim = self.backbone.num_features\n        self.head = nn.Sequential(\n            nn.Linear(img_dim, 512), nn.ReLU(),\n            nn.Linear(512, out_dim)\n        )\n\n    def forward(self, img):\n        f = self.backbone(img)\n        return self.head(f)\n\n# Load weights\neng_meta = [\n    \"veg_pct\",\"soil_pct\",\"hue_mean\",\"sat_mean\",\"val_mean\",\"gr_ratio\",\n    \"glcm_contrast\",\"glcm_energy\",\"glcm_homogeneity\",\n    \"glcm_dissimilarity\",\"glcm_entropy\"\n]\nemb_meta = [f\"emb_{i}\" for i in range(64)]\nmeta_cols = eng_meta + emb_meta\nMETA_DIM = len(meta_cols)\n\nteacher = TeacherModel(META_DIM).to(device)\nteacher.load_state_dict(torch.load(\"/kaggle/input/d/nuwaithet/biomass-pca-models/best_teacher_v2.pth\", map_location=device))\nteacher.eval()\n\nstudent = StudentConvNeXtBase().to(device)\nstudent.load_state_dict(torch.load(\"/kaggle/input/d/nuwaithet/biomass-pca-models/best_student_distilled_v2.pth\", map_location=device))\nstudent.eval()\n\n\n# =====================================================================================\n# 5) Build test DF\n# =====================================================================================\ntest_dir = \"/kaggle/input/csiro-biomass/test/\"\nrows = []\n\nfor fname in sorted(os.listdir(test_dir)):\n    img = Image.open(os.path.join(test_dir, fname)).convert(\"RGB\")\n\n    feats = extract_features_from_image(img)\n\n    emb = get_pca_embedding(img)\n    for i in range(64):\n        feats[f\"emb_{i}\"] = emb[i]\n\n    feats[\"image_path\"] = fname\n    rows.append(feats)\n\ndf_test = pd.DataFrame(rows)\ndf_test = df_test[meta_cols + [\"image_path\"]]\n\n# =====================================================================================\n# 6) Convert to numeric and scale\n# =====================================================================================\nfor c in meta_cols:\n    df_test[c] = pd.to_numeric(df_test[c], errors='coerce')\n\ndf_test[eng_meta] = scaler_eng.transform(df_test[eng_meta])\ndf_test[emb_meta] = scaler_emb.transform(df_test[emb_meta])\n\ndf_test[meta_cols] = df_test[meta_cols].astype(np.float32)\n\n# =====================================================================================\n# 7) Inference\n# =====================================================================================\ntest_tfms = transforms.Compose([\n    transforms.Resize((384,384)),\n    transforms.ToTensor(),\n])\n\npreds_teacher = []\npreds_student = []\n\nfor idx, row in df_test.iterrows():\n    img = Image.open(os.path.join(test_dir, row[\"image_path\"])).convert(\"RGB\")\n    img_t = test_tfms(img).unsqueeze(0).to(device)\n\n    meta_np = row[meta_cols].to_numpy(dtype=np.float32)\n    meta_vec = torch.from_numpy(meta_np).unsqueeze(0).to(device)\n\n    with torch.no_grad():\n        p_t = teacher(img_t, meta_vec).cpu().numpy()[0]\n        p_s = student(img_t).cpu().numpy()[0]\n\n    preds_teacher.append(p_t)\n    preds_student.append(p_s)\n\npreds_teacher = np.array(preds_teacher)\npreds_student = np.array(preds_student)\n\n# =====================================================================================\n# 8) Ensemble\n# =====================================================================================\npreds_log = 0.7 * preds_student + 0.3 * preds_teacher\npreds = np.expm1(preds_log)\n\n# =====================================================================================\n# 9) Submission\n# =====================================================================================\ntarget_names = [\"Dry_Green_g\",\"Dry_Dead_g\",\"Dry_Clover_g\",\"GDM_g\",\"Dry_Total_g\"]\n\nrows = []\nfor img_path, values in zip(df_test[\"image_path\"], preds):\n    img_id = img_path.replace(\".jpg\", \"\")\n    for t_name, val in zip(target_names, values):\n        rows.append({\"sample_id\": f\"{img_id}__{t_name}\", \"target\": float(val)})\n\nsubmission = pd.DataFrame(rows)\nsubmission.to_csv(\"submission.csv\", index=False)\nprint(\"✔ submission.csv saved!\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-17T15:47:10.424192Z","iopub.execute_input":"2025-11-17T15:47:10.424792Z","iopub.status.idle":"2025-11-17T15:47:34.117289Z","shell.execute_reply.started":"2025-11-17T15:47:10.424759Z","shell.execute_reply":"2025-11-17T15:47:34.116266Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-17T15:47:40.528967Z","iopub.execute_input":"2025-11-17T15:47:40.529300Z","iopub.status.idle":"2025-11-17T15:47:40.548000Z","shell.execute_reply.started":"2025-11-17T15:47:40.529278Z","shell.execute_reply":"2025-11-17T15:47:40.547323Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# !ls /kaggle/input/d/nuwaithet/biomass-pca-models\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-17T15:38:19.923929Z","iopub.execute_input":"2025-11-17T15:38:19.924700Z","iopub.status.idle":"2025-11-17T15:38:20.053066Z","shell.execute_reply.started":"2025-11-17T15:38:19.924670Z","shell.execute_reply":"2025-11-17T15:38:20.052193Z"}},"outputs":[],"execution_count":null}]}