{"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"}],"dockerImageVersionId":31240,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport glob\nimport cv2\nimport numpy as np\nimport matplotlib.pyplot as plt\n\n# 1. SEARCH for any JPG image in the entire input directory\n# This recursive search (**) will find the images wherever they are hiding\nprint(\"🔍 Searching for images in /kaggle/input/...\")\npossible_files = glob.glob(\"/kaggle/input/**/*.jpg\", recursive=True)\n\nif len(possible_files) > 0:\n    # Pick the first image found\n    test_path = possible_files[0]\n    print(f\"✅ Found valid image at: {test_path}\")\n\n    # 2. LOAD and ANALYZE\n    img = cv2.imread(test_path)\n    if img is None:\n        print(\"❌ Error: Could not read the image file.\")\n    else:\n        # Convert to HSV\n        hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)\n\n        # --- Green Mask (Vegetation) ---\n        mask_g = cv2.inRange(hsv, np.array([30, 30, 30]), np.array([90, 255, 255]))\n        \n        # --- Brown Mask (Dead/Dry) ---\n        mask_b = cv2.inRange(hsv, np.array([10, 20, 20]), np.array([30, 255, 200]))\n\n        # Calculate Percentages\n        total_px = img.shape[0] * img.shape[1]\n        green_pct = (np.count_nonzero(mask_g) / total_px) * 100\n        brown_pct = (np.count_nonzero(mask_b) / total_px) * 100\n\n        # 3. VISUALIZE RESULTS\n        fig, ax = plt.subplots(1, 3, figsize=(15, 5))\n        \n        # Original\n        ax[0].imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))\n        ax[0].set_title(f\"Image: {os.path.basename(test_path)}\")\n        ax[0].axis(\"off\")\n        \n        # Green\n        ax[1].imshow(mask_g, cmap='gray')\n        ax[1].set_title(f\"Green Cov: {green_pct:.2f}%\")\n        ax[1].axis(\"off\")\n        \n        # Brown\n        ax[2].imshow(mask_b, cmap='gray')\n        ax[2].set_title(f\"Brown Cov: {brown_pct:.2f}%\")\n        ax[2].axis(\"off\")\n        \n        plt.tight_layout()\n        plt.show()\n\nelse:\n    print(\"❌ CRITICAL: No .jpg files found anywhere in /kaggle/input/\")\n    print(\"Please check: Did you add the dataset to the notebook on the right sidebar?\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-11T08:32:56.035934Z","iopub.execute_input":"2026-01-11T08:32:56.036695Z","iopub.status.idle":"2026-01-11T08:32:57.409938Z","shell.execute_reply.started":"2026-01-11T08:32:56.036668Z","shell.execute_reply":"2026-01-11T08:32:57.409039Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport cv2\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\nimport matplotlib.pyplot as plt\nfrom scipy.stats import pearsonr\nfrom tqdm import tqdm\n\n# 1. SETUP & LOAD DATA\nDATA_ROOT = \"/kaggle/input/csiro-biomass\"\ndf = pd.read_csv(f\"{DATA_ROOT}/train.csv\")\n\n# 2. DEFINE THE MASK FUNCTION\ndef calculate_ratios(rel_path):\n    full_path = f\"{DATA_ROOT}/{rel_path}\"\n    try:\n        img = cv2.imread(full_path)\n        if img is None: return None, None\n        \n        hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)\n        total = img.shape[0] * img.shape[1]\n        \n        # Green Mask\n        mask_g = cv2.inRange(hsv, np.array([30, 30, 30]), np.array([90, 255, 255]))\n        g_pct = np.count_nonzero(mask_g) / total\n        \n        # Brown Mask\n        mask_b = cv2.inRange(hsv, np.array([10, 20, 20]), np.array([30, 255, 200]))\n        b_pct = np.count_nonzero(mask_b) / total\n        \n        return g_pct, b_pct\n    except:\n        return None, None\n\n# 3. SAMPLE DATA (Fast Check)\n# We take 200 random rows for 'Dry_Green_g' and 200 for 'Dry_Dead_g'\nprint(\"Sampling data for quick validation...\")\ngreen_df = df[df['target_name'] == 'Dry_Green_g'].sample(n=200, random_state=42).copy()\ndead_df = df[df['target_name'] == 'Dry_Dead_g'].sample(n=200, random_state=42).copy()\n\n# 4. CALCULATE FEATURES\nprint(\"Processing Green samples...\")\ngreen_vals = []\nfor path in tqdm(green_df['image_path']):\n    g, _ = calculate_ratios(path)\n    green_vals.append(g)\ngreen_df['Green_Pct'] = green_vals\n\nprint(\"Processing Dead/Brown samples...\")\nbrown_vals = []\nfor path in tqdm(dead_df['image_path']):\n    _, b = calculate_ratios(path)\n    brown_vals.append(b)\ndead_df['Brown_Pct'] = brown_vals\n\n# Remove failed reads (if any)\ngreen_df = green_df.dropna(subset=['Green_Pct'])\ndead_df = dead_df.dropna(subset=['Brown_Pct'])\n\n# 5. VISUALIZE THE CORRELATION\nfig, ax = plt.subplots(1, 2, figsize=(16, 6))\n\n# Green Plot\ncorr_g, _ = pearsonr(green_df['Green_Pct'], green_df['target'])\nsns.regplot(x=green_df['Green_Pct'], y=green_df['target'], ax=ax[0], color='green', scatter_kws={'alpha':0.4})\nax[0].set_title(f\"Green % vs. Dry Green Biomass\\nCorrelation: {corr_g:.4f}\", fontsize=14)\nax[0].set_xlabel(\"Calculated Green Coverage %\")\nax[0].set_ylabel(\"Actual Dry Green Biomass (g)\")\n\n# Brown Plot\ncorr_b, _ = pearsonr(dead_df['Brown_Pct'], dead_df['target'])\nsns.regplot(x=dead_df['Brown_Pct'], y=dead_df['target'], ax=ax[1], color='brown', scatter_kws={'alpha':0.4})\nax[1].set_title(f\"Brown % vs. Dry Dead Biomass\\nCorrelation: {corr_b:.4f}\", fontsize=14)\nax[1].set_xlabel(\"Calculated Brown Coverage %\")\nax[1].set_ylabel(\"Actual Dry Dead Biomass (g)\")\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-11T08:36:03.628961Z","iopub.execute_input":"2026-01-11T08:36:03.629220Z","iopub.status.idle":"2026-01-11T08:36:32.023657Z","shell.execute_reply.started":"2026-01-11T08:36:03.629199Z","shell.execute_reply":"2026-01-11T08:36:32.022929Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport cv2\nimport numpy as np\nimport pandas as pd\nfrom skimage.feature import graycomatrix, graycoprops\nfrom tqdm import tqdm\n\n# 1. SETUP\nDATA_ROOT = \"/kaggle/input/csiro-biomass\"\n# Load the CSV you just made (or the original train.csv)\nif os.path.exists(\"train_with_meta.csv\"):\n    df = pd.read_csv(\"train_with_meta.csv\")\n    print(\"Loaded 'train_with_meta.csv'...\")\nelse:\n    df = pd.read_csv(f\"{DATA_ROOT}/train.csv\")\n    print(\"Loaded original 'train.csv'...\")\n\n# 2. DEFINE ADVANCED EXTRACTOR\ndef get_advanced_features(rel_path):\n    full_path = f\"{DATA_ROOT}/{rel_path}\"\n    try:\n        # Load Image\n        img = cv2.imread(full_path)\n        if img is None: return [0]*5\n        \n        # --- A. Texture Features (GLCM) ---\n        # Convert to Grayscale\n        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)\n        \n        # Calculate GLCM (distances=1, angles=0 degrees)\n        # We quantify how pixel i relates to pixel i+1\n        glcm = graycomatrix(gray, distances=[1], angles=[0], levels=256, 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        \n        # --- B. Scientific Color Indices ---\n        # Normalize to 0-1 for math stability\n        img_norm = img.astype('float32') / 255.0\n        B, G, R = cv2.split(img_norm)\n        \n        # ExG (Excess Green) - Great for total biomass volume\n        # Formula: 2*G - R - B\n        exg_map = 2*G - R - B\n        avg_exg = np.mean(exg_map)\n        \n        # ExR (Excess Red) - Great for soil/dead matter distinguishing\n        # Formula: 1.4*R - G\n        exr_map = 1.4*R - G\n        avg_exr = np.mean(exr_map)\n        \n        return [contrast, energy, homogeneity, avg_exg, avg_exr]\n\n    except:\n        return [0]*5\n\n# 3. EXECUTE LOOP\nprint(\"Extracting Texture & Spectral Indices...\")\nfeatures = []\n\nfor path in tqdm(df['image_path']):\n    feats = get_advanced_features(path)\n    features.append(feats)\n\n# 4. APPEND TO DATAFRAME\nfeatures_arr = np.array(features)\ndf['Texture_Contrast'] = features_arr[:, 0]\ndf['Texture_Energy'] = features_arr[:, 1]\ndf['Texture_Homogeneity'] = features_arr[:, 2]\ndf['Index_ExG'] = features_arr[:, 3]\ndf['Index_ExR'] = features_arr[:, 4]\n\n# Save for the final training\ndf.to_csv(\"train_full_engineered.csv\", index=False)\nprint(\"\\n✅ Done! Saved 'train_full_engineered.csv' with 5 new columns.\")\nprint(df[['Texture_Contrast', 'Index_ExG', 'target']].head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-11T08:38:42.617978Z","iopub.execute_input":"2026-01-11T08:38:42.618624Z","iopub.status.idle":"2026-01-11T08:41:55.903485Z","shell.execute_reply.started":"2026-01-11T08:38:42.618597Z","shell.execute_reply":"2026-01-11T08:41:55.902820Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STATE CLASSIFIER — MAX ACCURACY (FROZEN DINOv2 LARGE)\n# ============================================================\n\nimport os\nimport cv2\nimport random\nimport warnings\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.preprocessing import LabelEncoder\nfrom sklearn.metrics import accuracy_score\nfrom tqdm import tqdm\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\nwarnings.filterwarnings(\"ignore\")\n\n# ---------------- CONFIG ----------------\nclass CFG:\n    data_root = \"/kaggle/input/csiro-biomass\"\n    train_csv = f\"{data_root}/train.csv\"\n    out_dir = \"/kaggle/working/state_models\"\n    device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n\n    img_size = 224\n    batch_size = 32     # DINO Large is heavy; reduce to 16 if OOM\n    epochs = 12         # Classification converges faster than regression\n    lr_head = 3e-4      # Standard classification LR\n    folds = 5\n    seed = 42\n    \n    # Preventing Overfitting\n    dropout = 0.5\n    label_smoothing = 0.1 \n\ndef seed_everything(seed):\n    random.seed(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.backends.cudnn.deterministic = True\n\nos.makedirs(CFG.out_dir, exist_ok=True)\nseed_everything(CFG.seed)\n\n# ---------------- PREPARE DATA ----------------\nprint(\"Loading and encoding data...\")\ndf = pd.read_csv(CFG.train_csv)\n# We only need one row per image for classification\ndf = df.drop_duplicates(subset=['image_path']).reset_index(drop=True)\n\n# Encode Targets (State -> 0, 1, 2...)\nle = LabelEncoder()\ndf['label'] = le.fit_transform(df['State'])\nnum_classes = len(le.classes_)\n\n# Save the classes so we can decode later during inference!\nnp.save(f\"{CFG.out_dir}/classes.npy\", le.classes_)\nprint(f\"Classes Saved: {le.classes_}\")\n\n# ---------------- DATASET ----------------\nclass StateDataset(Dataset):\n    def __init__(self, df, train=True):\n        self.df = df.reset_index(drop=True)\n        self.train = train\n        \n        # Stronger augmentation for classification\n        self.tfms = A.Compose([\n            A.Resize(CFG.img_size, CFG.img_size),\n            A.HorizontalFlip(p=0.5),\n            A.VerticalFlip(p=0.5),\n            A.RandomRotate90(p=0.5),\n            # Subtle color shifts to handle lighting variance\n            A.ColorJitter(brightness=0.1, contrast=0.1, saturation=0.1, hue=0.05, p=0.3) if train else A.NoOp(),\n            A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n            ToTensorV2()\n        ])\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        path = os.path.join(CFG.data_root, row.image_path)\n        \n        try:\n            img = cv2.imread(path)\n            if img is None: raise ValueError\n            img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        except:\n            img = np.zeros((CFG.img_size, CFG.img_size, 3), dtype=np.uint8)\n\n        img = self.tfms(image=img)[\"image\"]\n        label = torch.tensor(row.label, dtype=torch.long)\n        return img, label\n\n# ---------------- MODEL ----------------\nclass StateClassifier(nn.Module):\n    def __init__(self, n_classes):\n        super().__init__()\n        # Use DINOv2 Large for maximum texture recognition\n        print(\"Loading DINOv2 Large backbone...\")\n        self.backbone = torch.hub.load(\"facebookresearch/dinov2\", \"dinov2_vitl14\")\n        \n        # Freeze Backbone\n        for param in self.backbone.parameters():\n            param.requires_grad = False\n            \n        dim = self.backbone.embed_dim  # 1024 for Large\n        \n        # Robust Head\n        self.head = nn.Sequential(\n            nn.Linear(dim, 512),\n            nn.BatchNorm1d(512),\n            nn.ReLU(),\n            nn.Dropout(CFG.dropout),\n            nn.Linear(512, n_classes)\n        )\n        \n    def forward(self, x):\n        with torch.no_grad():\n            x = self.backbone(x)\n        return self.head(x)\n\n# ---------------- TRAIN LOOP ----------------\n# Stratified K-Fold is CRITICAL for classification\nskf = StratifiedKFold(n_splits=CFG.folds, shuffle=True, random_state=CFG.seed)\n\nfor fold, (tr_idx, va_idx) in enumerate(skf.split(df, df['label'])):\n    print(f\"\\n================ FOLD {fold} =================\")\n\n    tr_ds = StateDataset(df.iloc[tr_idx], train=True)\n    va_ds = StateDataset(df.iloc[va_idx], train=False)\n\n    tr_dl = DataLoader(tr_ds, CFG.batch_size, shuffle=True, num_workers=2, pin_memory=True)\n    va_dl = DataLoader(va_ds, CFG.batch_size, shuffle=False, num_workers=2, pin_memory=True)\n\n    model = StateClassifier(num_classes).to(CFG.device)\n    optimizer = torch.optim.AdamW(model.head.parameters(), lr=CFG.lr_head, weight_decay=1e-4)\n    scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='max', factor=0.5, patience=2)\n    \n    # Label Smoothing Loss\n    loss_fn = nn.CrossEntropyLoss(label_smoothing=CFG.label_smoothing)\n\n    best_acc = 0.0\n\n    for epoch in range(CFG.epochs):\n        model.train()\n        tr_loss = 0\n        correct = 0\n        total = 0\n\n        for imgs, labels in tqdm(tr_dl, leave=False, desc=f\"Ep {epoch+1}\"):\n            imgs, labels = imgs.to(CFG.device), labels.to(CFG.device)\n            \n            optimizer.zero_grad()\n            outputs = model(imgs)\n            loss = loss_fn(outputs, labels)\n            loss.backward()\n            optimizer.step()\n            \n            tr_loss += loss.item()\n            _, preds = torch.max(outputs, 1)\n            correct += (preds == labels).sum().item()\n            total += labels.size(0)\n\n        tr_acc = correct / total\n        tr_loss /= len(tr_dl)\n\n        # --- VALIDATION ---\n        model.eval()\n        va_correct = 0\n        va_total = 0\n        \n        with torch.no_grad():\n            for imgs, labels in va_dl:\n                imgs, labels = imgs.to(CFG.device), labels.to(CFG.device)\n                outputs = model(imgs)\n                _, preds = torch.max(outputs, 1)\n                va_correct += (preds == labels).sum().item()\n                va_total += labels.size(0)\n\n        va_acc = va_correct / va_total\n        scheduler.step(va_acc) # Step based on accuracy\n        \n        current_lr = optimizer.param_groups[0]['lr']\n        print(f\"Ep {epoch+1:02d} | Train Loss: {tr_loss:.4f} | Train Acc: {tr_acc:.2%} | Val Acc: {va_acc:.2%} | LR: {current_lr:.1e}\")\n\n        if va_acc > best_acc:\n            best_acc = va_acc\n            torch.save(model.state_dict(), f\"{CFG.out_dir}/state_model_fold{fold}.pth\")\n            print(f\"  >>> New Best Accuracy! Saved.\")\n\nprint(\"\\n✅ STATE CLASSIFIER TRAINING FINISHED\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-11T08:54:57.802620Z","iopub.execute_input":"2026-01-11T08:54:57.803191Z","iopub.status.idle":"2026-01-11T09:19:40.906606Z","shell.execute_reply.started":"2026-01-11T08:54:57.803166Z","shell.execute_reply":"2026-01-11T09:19:40.905670Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}