In [ ]:
# CELL 1: SETUP
import os
import time
import json
import numpy as np
import cv2
import rasterio
from rasterio.windows import from_bounds
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import TensorDataset, DataLoader, random_split
from scipy.ndimage import distance_transform_edt as distance
from scipy.spatial.distance import directed_hausdorff
from huggingface_hub import hf_hub_download
import ee
import geemap
from google.colab import drive

# 1. Environment
drive.mount('/content/drive', force_remount=True)
os.system('pip install -q rasterio geopandas timm segmentation-models-pytorch huggingface_hub geedim')

try:
    ee.Initialize(project='[REDACTED_FOR_SECURITY]')
except:
    ee.Authenticate()
    ee.Initialize(project='[REDACTED_FOR_SECURITY]')

# 2. Config (Optimized for Fine-Tuning)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f" Training on: {device}")

SAVE_DIR = '/content/drive/MyDrive/SatMAE_Advanced_FT/'
if not os.path.exists(SAVE_DIR): os.makedirs(SAVE_DIR)

BATCH_SIZE = 8
EPOCHS = 50
LR_MAX = 1e-4   # Peak LR
LR_MIN = 1e-6   # Min LR
PATCH_SIZE = 224
ASSET_ID = 'projects/[REDACTED_FOR_SECURITY]/assets/Punjab_Mask_2024_NEW'

TIME_WINDOWS = [
    ('2024-11-01', '2024-11-30'),
    ('2025-02-15', '2025-03-15'),
    ('2025-04-01', '2025-04-15')
]

# 3. Data Loading
def get_satmae_data():
    print("1. Ingesting Asset...")
    mask_img = ee.Image(ASSET_ID)
    roi_geom = mask_img.geometry()
    mask_file = 'local_mask.tif'
    if not os.path.exists(mask_file):
        geemap.download_ee_image(mask_img, mask_file, region=roi_geom, scale=10, crs='EPSG:4326', overwrite=True)

    with rasterio.open(mask_file) as src:
        b = src.bounds
        cx, cy = (b.left + b.right)/2, (b.bottom + b.top)/2
        offset = 0.06
        window = from_bounds(cx-offset, cy-offset, cx+offset, cy+offset, src.transform)
        mask = src.read(1, window=window)
        mask = np.where(mask > 0, 1.0, 0.0).astype(np.float32)
        target_h, target_w = mask.shape
        small_roi = ee.Geometry.Rectangle([cx-offset, cy-offset, cx+offset, cy+offset], proj=str(src.crs), geodesic=False)

    stack = []
    print("2. Stacking Time Steps...")
    for i, (start, end) in enumerate(TIME_WINDOWS):
        fname = f'time_{i}.tif'
        if not os.path.exists(fname):
            s2 = ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED').filterBounds(small_roi).filterDate(start, end).median().select(['B2','B3','B4','B8','B11','B12'])
            s1 = ee.ImageCollection('COPERNICUS/S1_GRD').filterBounds(small_roi).filterDate(start, end).mean().select(['VV','VH'])
            fused = ee.Image.cat([s2, s1]).clip(small_roi)
            geemap.download_ee_image(fused, fname, region=small_roi, scale=10, crs='EPSG:4326', overwrite=True)

        with rasterio.open(fname) as src:
            arr = src.read()
            arr = np.transpose(arr, (1, 2, 0))
            if arr.shape[:2] != (target_h, target_w):
                arr = cv2.resize(arr, (target_w, target_h), interpolation=cv2.INTER_LINEAR)
            s2_n = np.clip(arr[:,:,:6] / 5000.0, 0, 1)
            s1_n = np.clip((arr[:,:,6:] - (-25.0)) / (0.0 - (-25.0)), 0, 1)
            stack.append(np.concatenate([s2_n, s1_n], axis=2))

    full_cube = np.stack(stack, axis=2)
    x_out, y_out = [], []
    stride = PATCH_SIZE

    print("3. Creating Patches...")
    for y in range(0, target_h, stride):
        for x in range(0, target_w, stride):
            img_p = full_cube[y:y+stride, x:x+stride]
            mask_p = mask[y:y+stride, x:x+stride]
            if img_p.shape[0] != PATCH_SIZE or img_p.shape[1] != PATCH_SIZE: continue
            if np.min(img_p) < 0: continue
            x_out.append(img_p)
            y_out.append(mask_p)

    X = np.array(x_out, dtype=np.float32).transpose(0, 4, 3, 1, 2)
    y = np.array(y_out, dtype=np.float32)[:, None, :, :]
    print(f" Data Ready. Shape: {X.shape}")
    return torch.tensor(X), torch.tensor(y)

X_data, y_data = get_satmae_data()
Mounted at /content/drive
 Training on: cuda
1. Ingesting Asset...
/usr/local/lib/python3.12/dist-packages/geemap/common.py:12471: FutureWarning: 'BaseImage' is deprecated and will be removed in a future release.  Please use the 'ee.Image.gd' accessor instead.
  img = gd.download.BaseImage(image)
...tmae-2026/assets/Punjab_Mask_2024_NEW:   0%|          |0/585 tiles [00:00<?]
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:googleapiclient.http:Sleeping 1.59 seconds before retry 1 of 5 for request: POST https://earthengine.googleapis.com/v1/projects/satmae-2026/thumbnails?fields=name&alt=json, after 429
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
/usr/local/lib/python3.12/dist-packages/geedim/image.py:254: RuntimeWarning: Couldn't find STAC entry for: 'projects/satmae-2026/assets/Punjab_Mask_2024_NEW'.
  return STACClient().get(self.id)
2. Stacking Time Steps...
  0%|          |0/48 tiles [00:00<?]
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
/usr/local/lib/python3.12/dist-packages/geedim/image.py:254: RuntimeWarning: Couldn't find STAC entry for: 'None'.
  return STACClient().get(self.id)
  0%|          |0/48 tiles [00:00<?]
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
  0%|          |0/48 tiles [00:00<?]
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
3. Creating Patches...
 Data Ready. Shape: (25, 8, 3, 224, 224)
In [ ]:
# CELL 2:  COMPOUND LOSS


class DiceLoss(nn.Module):
    def __init__(self, smooth=1e-6):
        super().__init__()
        self.smooth = smooth
    def forward(self, inputs, targets):
        inputs = torch.sigmoid(inputs).view(-1)
        targets = targets.view(-1)
        inter = (inputs * targets).sum()
        dice = (2. * inter + self.smooth) / (inputs.sum() + targets.sum() + self.smooth)
        return 1 - dice

class FocalTverskyLoss(nn.Module):
    def __init__(self, alpha=0.7, beta=0.3, gamma=2.0, smooth=1e-6):
        super().__init__()
        self.alpha = alpha
        self.beta = beta
        self.gamma = gamma
        self.smooth = smooth

    def forward(self, inputs, targets):
        inputs = torch.sigmoid(inputs).view(-1)
        targets = targets.view(-1)

        # Tversky Index
        TP = (inputs * targets).sum()
        FP = ((1-targets) * inputs).sum()
        FN = (targets * (1-inputs)).sum()

        Tversky = (TP + self.smooth) / (TP + self.alpha*FP + self.beta*FN + self.smooth)
        return (1 - Tversky)**self.gamma

class HausdorffDTLoss(nn.Module):
    def __init__(self, alpha=2.0):
        super().__init__()
        self.alpha = alpha

    def forward(self, pred, gt):
        # 1. Move to CPU for Scipy
        with torch.no_grad():
            gt_np = gt.cpu().numpy()
            dist_map = np.zeros_like(gt_np)

            for i in range(len(gt_np)):

                mask = (gt_np[i, 0] > 0.5).astype(np.uint8)

                if mask.sum() == 0: continue

                # d_in: dist to nearest zero (background)
                # d_out: dist to nearest one (foreground)
                d_in = distance(mask)
                d_out = distance(1 - mask)
                dist_map[i, 0] = (d_out - d_in)


            dist_map = torch.tensor(dist_map, device=pred.device, dtype=torch.float32)

        probs = torch.sigmoid(pred)
        # Loss: Weighted MSE based on distance map
        loss = torch.mean((probs - gt) ** 2 * (1 + self.alpha * torch.abs(dist_map)))
        return loss

class CompoundLoss(nn.Module):
    def __init__(self):
        super().__init__()
        self.dice = DiceLoss()
        self.boundary = HausdorffDTLoss(alpha=2.0)
        self.focal = FocalTverskyLoss()

    def forward(self, preds, targets):
        # Weights: 50% Dice, 30% Boundary, 20% Focal
        return 0.5*self.dice(preds, targets) + \
               0.3*self.boundary(preds, targets) + \
               0.2*self.focal(preds, targets)
In [ ]:
# CELL 3: SATMAE MODEL ( PARTIAL FINE-TUNE)

class SatMAEPatchEmbed(nn.Module):
    def __init__(self, in_chans=8, embed_dim=768, patch_size=16):
        super().__init__()
        self.proj = nn.Conv2d(in_chans, embed_dim, kernel_size=patch_size, stride=patch_size)
    def forward(self, x):
        B, C, T, H, W = x.shape
        x = x.permute(0, 2, 1, 3, 4).reshape(B * T, C, H, W)
        x = self.proj(x).flatten(2).transpose(1, 2)
        x = x.reshape(B, T, -1, x.shape[-1])
        return x

class SatMAEBackbone(nn.Module):
    def __init__(self, num_frames=3, in_chans=8, embed_dim=768, depth=12, num_heads=12):
        super().__init__()
        self.patch_embed = SatMAEPatchEmbed(in_chans=in_chans, embed_dim=embed_dim)
        num_patches = (224 // 16) ** 2
        self.pos_embed = nn.Parameter(torch.zeros(1, 1, num_patches + 1, embed_dim))
        self.time_embed = nn.Parameter(torch.zeros(1, num_frames, 1, embed_dim))
        self.cls_token = nn.Parameter(torch.zeros(1, 1, 1, embed_dim))

        encoder_layer = nn.TransformerEncoderLayer(d_model=embed_dim, nhead=num_heads, dim_feedforward=embed_dim*4, activation="gelu", batch_first=True, norm_first=True)
        self.blocks = nn.TransformerEncoder(encoder_layer, num_layers=depth)
        self.norm = nn.LayerNorm(embed_dim)

    def forward(self, x):
        x = self.patch_embed(x)
        B, T, N, D = x.shape
        x = x + self.time_embed
        x = x.reshape(B, T*N, D)
        spatial_pos = self.pos_embed[:, :, 1:, :].expand(B, T, -1, -1).reshape(B, T*N, D)
        x = x + spatial_pos
        cls_token = self.cls_token.expand(B, -1, -1, -1).reshape(B, 1, D) + self.pos_embed[:, :, 0, :].expand(B, 1, D)
        x = torch.cat((cls_token, x), dim=1)
        x = self.blocks(x)
        x = self.norm(x)
        return x

class SatMAE_FineTune(nn.Module):
    def __init__(self, num_frames=3, embed_dim=768):
        super().__init__()
        print(" Initializing SatMAE (Partial Fine-Tune Mode)...")
        self.backbone = SatMAEBackbone(num_frames=num_frames, embed_dim=embed_dim)

        # 1. Load Pre-trained Weights
        try:
            print(" Loading Weights...")
            p = hf_hub_download("google/vit-base-patch16-224", "pytorch_model.bin")
            sd = torch.load(p, map_location='cpu')
            w = sd['vit.embeddings.patch_embeddings.projection.weight']
            new_w = torch.zeros(768, 8, 16, 16)
            new_w[:, :3] = w
            new_w[:, 3:] = w.mean(1, keepdim=True).repeat(1, 5, 1, 1)
            self.backbone.patch_embed.proj.weight.data = new_w
            self.backbone.patch_embed.proj.bias.data = sd['vit.embeddings.patch_embeddings.projection.bias']
            print(" Weights Loaded.")
        except:
            print(" Weights missing, using random init.")

        # 2. FREEZING STRATEGY

        # A. Freeze EVERYTHING first
        for p in self.backbone.blocks.parameters(): p.requires_grad = False

        # B. Unfreeze Inputs (Adapt to Sentinel-2/1)
        self.backbone.patch_embed.proj.weight.requires_grad = True
        self.backbone.time_embed.requires_grad = True

        # C. Unfreeze Last 2 Layers (Fixed Syntax)
        # We access .layers (ModuleList) -> slice it -> loop through layers -> loop through params
        for layer in self.backbone.blocks.layers[-2:]:
            for p in layer.parameters():
                p.requires_grad = True

        # 3. Standard Decoder
        self.temp_agg = nn.Conv2d(embed_dim * num_frames, embed_dim, kernel_size=1)
        self.decoder = nn.Sequential(
            nn.Upsample(scale_factor=2), nn.Conv2d(768, 256, 3, 1, 1), nn.BatchNorm2d(256), nn.GELU(),
            nn.Upsample(scale_factor=2), nn.Conv2d(256, 128, 3, 1, 1), nn.BatchNorm2d(128), nn.GELU(),
            nn.Upsample(scale_factor=2), nn.Conv2d(128, 64, 3, 1, 1), nn.BatchNorm2d(64), nn.GELU(),
            nn.Upsample(scale_factor=2), nn.Conv2d(64, 32, 3, 1, 1), nn.BatchNorm2d(32), nn.GELU(),
            nn.Conv2d(32, 1, 1)
        )

    def forward(self, x):
        features = self.backbone(x)[:, 1:, :]
        B, L, D = features.shape
        features = features.view(B, 3, 14, 14, D).permute(0, 4, 1, 2, 3).flatten(1, 2)
        features = self.temp_agg(features)
        return self.decoder(features)
In [ ]:
# CELL 4: TRAINING & BOUNDARY EVALUATION


def calculate_boundary_iou(gt_mask, pred_mask, dilation=5):
    """ Calculates IoU only in the 5px border region """
    gt_mask = gt_mask.astype(np.uint8)
    pred_mask = pred_mask.astype(np.uint8)
    kernel = np.ones((dilation, dilation), np.uint8)
    gt_b = cv2.dilate(gt_mask, kernel) - cv2.erode(gt_mask, kernel)
    pred_b = cv2.dilate(pred_mask, kernel) - cv2.erode(pred_mask, kernel)

    inter = np.logical_and(gt_b, pred_b).sum()
    union = np.logical_or(gt_b, pred_b).sum()
    if union == 0: return 1.0
    return inter / union

# Setup
model = SatMAE_FineTune().to(device)
criterion = CompoundLoss()
optimizer = optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr=LR_MAX)
scheduler = optim.lr_scheduler.CosineAnnealingWarmRestarts(optimizer, T_0=10, T_mult=2, eta_min=LR_MIN)

ds = TensorDataset(X_data, y_data)
tr_sz = int(0.85 * len(ds))
t_ds, v_ds = random_split(ds, [tr_sz, len(ds)-tr_sz])
train_loader = DataLoader(t_ds, BATCH_SIZE, shuffle=True)
val_loader = DataLoader(v_ds, BATCH_SIZE, shuffle=False)

print(f" Starting Advanced Fine-Tuning ({EPOCHS} Epochs)...")
history = []

for ep in range(EPOCHS):
    model.train()
    train_loss = 0

    for x, y in train_loader:
        x, y = x.to(device), y.to(device)
        optimizer.zero_grad()
        preds = model(x)
        loss = criterion(preds, y)
        loss.backward()
        optimizer.step()
        train_loss += loss.item()

    # Update Scheduler
    scheduler.step()
    current_lr = scheduler.get_last_lr()[0]

    # Validation & Metrics
    model.eval()
    val_loss = 0
    b_ious = []

    with torch.no_grad():
        for x, y in val_loader:
            x, y = x.to(device), y.to(device)
            preds = model(x)
            val_loss += criterion(preds, y).item()

            # Quick Boundary Check (Sample 1 per batch to save time)
            p_bin = (torch.sigmoid(preds) > 0.5).cpu().numpy().astype(np.uint8)
            y_bin = y.cpu().numpy().astype(np.uint8)
            b_ious.append(calculate_boundary_iou(y_bin[0,0], p_bin[0,0]))

    avg_t = train_loss / len(train_loader)
    avg_v = val_loss / len(val_loader)
    avg_b_iou = np.mean(b_ious)
    history.append(avg_v)

    if (ep+1) % 5 == 0:
        print(f"Ep {ep+1} | LR: {current_lr:.2e} | Train: {avg_t:.4f} | Val: {avg_v:.4f} | Boundary IoU: {avg_b_iou:.4f}")

torch.save(model.state_dict(), SAVE_DIR + "SatMAE_Advanced_FT_Final.pth")
🏗️ Initializing SatMAE (Partial Fine-Tune Mode)...
📥 Loading Weights...
✅ Weights Loaded.
 Starting Advanced Fine-Tuning (50 Epochs)...
Ep 5 | LR: 5.05e-05 | Train: 0.4842 | Val: 0.9113 | Boundary IoU: 0.1137
Ep 10 | LR: 1.00e-04 | Train: 0.4332 | Val: 0.5578 | Boundary IoU: 0.2069
Ep 15 | LR: 8.55e-05 | Train: 0.3724 | Val: 0.3377 | Boundary IoU: 0.2240
Ep 20 | LR: 5.05e-05 | Train: 0.3509 | Val: 0.3124 | Boundary IoU: 0.2332
Ep 25 | LR: 1.55e-05 | Train: 0.3424 | Val: 0.3216 | Boundary IoU: 0.2242
Ep 30 | LR: 1.00e-04 | Train: 0.3392 | Val: 0.3190 | Boundary IoU: 0.2375
Ep 35 | LR: 9.62e-05 | Train: 0.3278 | Val: 0.3009 | Boundary IoU: 0.2366
Ep 40 | LR: 8.55e-05 | Train: 0.3131 | Val: 0.2898 | Boundary IoU: 0.2462
Ep 45 | LR: 6.94e-05 | Train: 0.2992 | Val: 0.2984 | Boundary IoU: 0.2772
Ep 50 | LR: 5.05e-05 | Train: 0.2910 | Val: 0.3563 | Boundary IoU: 0.2887
In [ ]:
# ==========================================
# CELL 5: VISUALIZATION & METRICS REPORTING
# ==========================================
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
import numpy as np
from datetime import datetime
from scipy.spatial.distance import directed_hausdorff
from sklearn.metrics import accuracy_score, f1_score, jaccard_score, precision_score, recall_score

# --- HELPER FUNCTIONS ---
def calculate_boundary_iou(gt_mask, pred_mask, dilation=5):
    """Calculates IoU only in the 5px border region."""
    import cv2
    gt_mask = gt_mask.astype(np.uint8)
    pred_mask = pred_mask.astype(np.uint8)
    kernel = np.ones((dilation, dilation), np.uint8)

    gt_b = cv2.dilate(gt_mask, kernel) - cv2.erode(gt_mask, kernel)
    pred_b = cv2.dilate(pred_mask, kernel) - cv2.erode(pred_mask, kernel)

    inter = np.logical_and(gt_b, pred_b).sum()
    union = np.logical_or(gt_b, pred_b).sum()
    if union == 0: return 1.0
    return inter / union

def symmetric_hausdorff(mask_pred, mask_gt):
    """
    Computes Symmetric Hausdorff Distance.
    FIX: directed_hausdorff returns (distance, idx1, idx2), so we take [0].
    """
    # Get coordinates of True pixels
    coords_pred = np.argwhere(mask_pred)
    coords_gt = np.argwhere(mask_gt)

    # If either is empty, return a penalty (diagonal of 224x224 image)
    if len(coords_pred) == 0 or len(coords_gt) == 0:
        return 316.0 # approx sqrt(224^2 + 224^2)

    # Calculate directed distances
    d_pg = directed_hausdorff(coords_pred, coords_gt)[0] # Fix: Access first element
    d_gp = directed_hausdorff(coords_gt, coords_pred)[0] # Fix: Access first element

    return max(d_pg, d_gp)

def generate_final_report(model, loader, history, save_dir, model_name="SatMAE_Full_FT"):
    print(f" Generating Final Report for {model_name}...")
    model.eval()

    # 1. VISUALIZATION (Save to Drive)
    # --------------------------------
    try:
        x_batch, y_batch = next(iter(loader))
        x_batch, y_batch = x_batch.to(device), y_batch.to(device)

        with torch.no_grad():
            logits = model(x_batch)
            preds = (torch.sigmoid(logits) > 0.5).float()

        # Plot 4 Samples
        fig, axes = plt.subplots(4, 3, figsize=(12, 16))
        cols = ["Input (RGB - Peak Season)", "Ground Truth", "Prediction"]
        for ax, col in zip(axes[0], cols): ax.set_title(col, fontsize=14, fontweight='bold')

        for i in range(4):
            if i >= len(x_batch): break

            # Extract RGB (B4, B3, B2) -> Indices [2, 1, 0]
            rgb = x_batch[i, [2, 1, 0], 1, :, :].permute(1, 2, 0).cpu().numpy()
            rgb = np.clip(rgb * 3.5, 0, 1) # Brighten

            gt_img = y_batch[i, 0].cpu().numpy()
            pred_img = preds[i, 0].cpu().numpy()

            # Sample IoU
            inter = np.logical_and(gt_img, pred_img).sum()
            union = np.logical_or(gt_img, pred_img).sum()
            iou = inter / (union + 1e-6)

            axes[i, 0].imshow(rgb)
            axes[i, 1].imshow(gt_img, cmap='gray')
            axes[i, 2].imshow(pred_img, cmap='gray')
            axes[i, 2].text(5, 20, f"IoU: {iou:.2f}", color='lime', fontweight='bold', bbox=dict(facecolor='black', alpha=0.7))

            for ax in axes[i]: ax.axis('off')

        plt.tight_layout()
        viz_path = f"{save_dir}{model_name}_Visuals.png"
        plt.savefig(viz_path)
        plt.show()
        print(f"Visualizations saved to: {viz_path}")
    except Exception as e:
        print(f" Visualization skipped due to error: {e}")

    # 2. FULL DATASET METRICS
    # -----------------------
    print(" Calculating Metrics on Full Validation Set...")
    all_preds, all_targets = [], []
    boundary_ious = []
    hausdorff_dists = []

    with torch.no_grad():
        for x, y in loader:
            x = x.to(device)
            logits = model(x)
            p_batch = (torch.sigmoid(logits) > 0.5).float().cpu().numpy()
            y_batch_np = y.numpy()

            # Per-Image Metrics
            for j in range(len(y_batch_np)):
                p = p_batch[j, 0].astype(np.uint8)
                t = y_batch_np[j, 0].astype(np.uint8)

                boundary_ious.append(calculate_boundary_iou(t, p))

                if np.sum(t) > 0 and np.sum(p) > 0:
                    hausdorff_dists.append(symmetric_hausdorff(p, t))

            all_preds.extend(p_batch.flatten())
            all_targets.extend(y_batch_np.flatten())

    y_p = np.array(all_preds).astype(int)
    y_t = np.array(all_targets).astype(int)

    metrics = {
        "Model": model_name,
        "Date": datetime.now().strftime("%Y-%m-%d %H:%M"),
        "Standard_IoU": round(jaccard_score(y_t, y_p, average='binary'), 4),
        "Boundary_IoU": round(np.mean(boundary_ious), 4),
        "Hausdorff_Dist_px": round(np.mean(hausdorff_dists), 2),
        "F1_Score": round(f1_score(y_t, y_p, average='binary'), 4),
        "Precision": round(precision_score(y_t, y_p, average='binary'), 4),
        "Recall": round(recall_score(y_t, y_p, average='binary'), 4),
        "Accuracy": round(accuracy_score(y_t, y_p), 4)
    }

    # 3. PLOT LOSS CURVE
    # ------------------
    if len(history) > 0:
        plt.figure(figsize=(10, 5))
        plt.plot(history, label='Validation Loss', color='orange', linewidth=2)
        plt.title(f'{model_name} Training Loss Curve')
        plt.xlabel('Epochs')
        plt.ylabel('Loss')
        plt.legend()
        plt.grid(True, alpha=0.3)
        loss_path = f"{save_dir}{model_name}_LossCurve.png"
        plt.savefig(loss_path)
        plt.show()

    # 4. SAVE TO DRIVE
    # ----------------
    json_path = f"{save_dir}{model_name}_Metrics.json"
    with open(json_path, 'w') as f:
        json.dump(metrics, f, indent=4)

    print("\nFINAL REPORT CARD:")
    print(json.dumps(metrics, indent=4))
    print(f" All files saved to {save_dir}")

# EXECUTE
generate_final_report(model, val_loader, history, SAVE_DIR, "SatMAE_Full_FineTune")
 Generating Final Report for SatMAE_Full_FineTune...
No description has been provided for this image
Visualizations saved to: /content/drive/MyDrive/SatMAE_Advanced_FT/SatMAE_Full_FineTune_Visuals.png
 Calculating Metrics on Full Validation Set...
No description has been provided for this image
FINAL REPORT CARD:
{
    "Model": "SatMAE_Full_FineTune",
    "Date": "2026-01-13 04:27",
    "Standard_IoU": 0.8213,
    "Boundary_IoU": 0.3582,
    "Hausdorff_Dist_px": 21.24,
    "F1_Score": 0.9019,
    "Precision": 0.9425,
    "Recall": 0.8646,
    "Accuracy": 0.8679
}
 All files saved to /content/drive/MyDrive/SatMAE_Advanced_FT/
In [ ]:
# ==========================================
# CELL 5: VISUALIZATION & METRICS REPORTING
# ==========================================
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
import numpy as np
import json
import time
from datetime import datetime
from scipy.spatial.distance import directed_hausdorff
from sklearn.metrics import accuracy_score, f1_score, jaccard_score, precision_score, recall_score

# --- HELPER FUNCTIONS ---
def calculate_boundary_iou(gt_mask, pred_mask, dilation=5):
    """Calculates IoU only in the 5px border region."""
    import cv2
    gt_mask = gt_mask.astype(np.uint8)
    pred_mask = pred_mask.astype(np.uint8)
    kernel = np.ones((dilation, dilation), np.uint8)

    gt_b = cv2.dilate(gt_mask, kernel) - cv2.erode(gt_mask, kernel)
    pred_b = cv2.dilate(pred_mask, kernel) - cv2.erode(pred_mask, kernel)

    inter = np.logical_and(gt_b, pred_b).sum()
    union = np.logical_or(gt_b, pred_b).sum()
    if union == 0: return 1.0
    return inter / union

def symmetric_hausdorff(mask_pred, mask_gt):
    """
    Computes Symmetric Hausdorff Distance.
    Returns max(d(Pred, GT), d(GT, Pred)).
    """
    coords_pred = np.argwhere(mask_pred)
    coords_gt = np.argwhere(mask_gt)

    # Penalty if empty
    if len(coords_pred) == 0 or len(coords_gt) == 0:
        return 316.0 # approx diagonal of 224x224

    d_pg = directed_hausdorff(coords_pred, coords_gt)[0]
    d_gp = directed_hausdorff(coords_gt, coords_pred)[0]

    return max(d_pg, d_gp)

def generate_final_report(model, loader, history, save_dir, model_name="SatMAE_Full_FT"):
    print(f" Generating Final Report for {model_name}...")
    model.eval()

    # -------------------------------------------------------
    # 1. VISUALIZATION (Sample Predictions)
    # -------------------------------------------------------
    try:
        x_batch, y_batch = next(iter(loader))
        x_batch, y_batch = x_batch.to(device), y_batch.to(device)

        with torch.no_grad():
            logits = model(x_batch)
            preds = (torch.sigmoid(logits) > 0.5).float()

        # Plot 4 Samples
        fig, axes = plt.subplots(4, 3, figsize=(12, 16))
        cols = ["Input (RGB - Peak Season)", "Ground Truth", "Prediction"]
        for ax, col in zip(axes[0], cols): ax.set_title(col, fontsize=14, fontweight='bold')

        for i in range(4):
            if i >= len(x_batch): break

            # Extract RGB (B4, B3, B2) -> Indices [2, 1, 0] from Time Step 1
            rgb = x_batch[i, [2, 1, 0], 1, :, :].permute(1, 2, 0).cpu().numpy()
            rgb = np.clip(rgb * 3.5, 0, 1) # Brighten

            gt_img = y_batch[i, 0].cpu().numpy()
            pred_img = preds[i, 0].cpu().numpy()

            # Sample IoU for Display
            inter = np.logical_and(gt_img, pred_img).sum()
            union = np.logical_or(gt_img, pred_img).sum()
            iou = inter / (union + 1e-6)

            axes[i, 0].imshow(rgb)
            axes[i, 1].imshow(gt_img, cmap='gray')
            axes[i, 2].imshow(pred_img, cmap='gray')
            axes[i, 2].text(5, 20, f"IoU: {iou:.2f}", color='lime', fontweight='bold', bbox=dict(facecolor='black', alpha=0.7))

            for ax in axes[i]: ax.axis('off')

        plt.tight_layout()
        viz_path = f"{save_dir}{model_name}_Visuals.png"
        plt.savefig(viz_path)
        plt.show()
        print(f" Visualizations saved to: {viz_path}")
    except Exception as e:
        print(f" Visualization skipped error: {e}")

    # -------------------------------------------------------
    # 2. FULL DATASET METRICS (With FPS)
    # -------------------------------------------------------
    print(" Calculating Metrics & FPS on Validation Set...")
    all_preds, all_targets = [], []
    boundary_ious = []
    hausdorff_dists = []

    # Start Timer
    start_time = time.time()

    with torch.no_grad():
        for x, y in loader:
            x = x.to(device)
            logits = model(x)
            p_batch = (torch.sigmoid(logits) > 0.5).float().cpu().numpy()
            y_batch_np = y.numpy()

            # Per-Image Metrics
            for j in range(len(y_batch_np)):
                p = p_batch[j, 0].astype(np.uint8)
                t = y_batch_np[j, 0].astype(np.uint8)

                boundary_ious.append(calculate_boundary_iou(t, p))

                if np.sum(t) > 0 and np.sum(p) > 0:
                    hausdorff_dists.append(symmetric_hausdorff(p, t))

            all_preds.extend(p_batch.flatten())
            all_targets.extend(y_batch_np.flatten())

    # Stop Timer
    end_time = time.time()
    total_time = end_time - start_time
    total_images = len(loader.dataset)
    fps = total_images / (total_time + 1e-6)

    y_p = np.array(all_preds).astype(int)
    y_t = np.array(all_targets).astype(int)

    metrics = {
        "Model": model_name,
        "Date": datetime.now().strftime("%Y-%m-%d %H:%M"),
        "Standard_IoU": round(jaccard_score(y_t, y_p, average='binary'), 4),
        "Boundary_IoU": round(np.mean(boundary_ious), 4),
        "Hausdorff_Dist_px": round(np.mean(hausdorff_dists), 2),
        "F1_Score": round(f1_score(y_t, y_p, average='binary'), 4),
        "Precision": round(precision_score(y_t, y_p, average='binary'), 4),
        "Recall": round(recall_score(y_t, y_p, average='binary'), 4),
        "Accuracy": round(accuracy_score(y_t, y_p), 4),
        "Inference_Time_Sec": round(total_time, 2),
        "FPS": round(fps, 2)
    }

    # -------------------------------------------------------
    # 3. DUAL LOSS GRAPH (Train vs Validation)
    # -------------------------------------------------------
    if isinstance(history, dict) and 'train_loss' in history:
        train_loss = history['train_loss']
        val_loss = history['val_loss']
        epochs = range(1, len(train_loss) + 1)

        plt.figure(figsize=(10, 6))
        plt.plot(epochs, train_loss, 'b-', label='Training Loss', linewidth=2)
        plt.plot(epochs, val_loss, 'r--', label='Validation Loss', linewidth=2)

        plt.title(f'{model_name} Learning Curve', fontsize=16)
        plt.xlabel('Epochs', fontsize=12)
        plt.ylabel('Compound Loss', fontsize=12)
        plt.legend(fontsize=12)
        plt.grid(True, alpha=0.3)

        loss_path = f"{save_dir}{model_name}_DualLossCurve.png"
        plt.savefig(loss_path)
        plt.show()
        print(f" Loss Graph saved to: {loss_path}")
    else:
        print(" History format not recognized for dual plotting.")

    # -------------------------------------------------------
    # 4. SAVE RESULTS
    # -------------------------------------------------------
    json_path = f"{save_dir}{model_name}_Metrics.json"
    with open(json_path, 'w') as f:
        json.dump(metrics, f, indent=4)

    print("\n FINAL REPORT CARD:")
    print(json.dumps(metrics, indent=4))
    print(f"All files saved to {save_dir}")

# EXECUTE
generate_final_report(model, val_loader, history, SAVE_DIR, "SatMAE_Full_FineTune")
 Generating Final Report for SatMAE_Full_FineTune...
No description has been provided for this image
 Visualizations saved to: /content/drive/MyDrive/SatMAE_Advanced_FT/SatMAE_Full_FineTune_Visuals.png
 Calculating Metrics & FPS on Validation Set...
 History format not recognized for dual plotting.

 FINAL REPORT CARD:
{
    "Model": "SatMAE_Full_FineTune",
    "Date": "2026-01-13 04:34",
    "Standard_IoU": 0.8213,
    "Boundary_IoU": 0.3582,
    "Hausdorff_Dist_px": 21.24,
    "F1_Score": 0.9019,
    "Precision": 0.9425,
    "Recall": 0.8646,
    "Accuracy": 0.8679,
    "Inference_Time_Sec": 0.49,
    "FPS": 8.16
}
All files saved to /content/drive/MyDrive/SatMAE_Advanced_FT/
In [ ]:
#Summary of the Work
#here i froze the first 10 block but unfroze the last 2 transformer blocks , it is just for experimenting but the result i got was poor than our satmae-finetuned model     FINAL STANDARD METRICS:
{"results of our Satmae finetunned with Compound loss "
    "Model": "SatMAE_Code1_Fair",
    "Pixel_Accuracy": 0.8757,-
    "IoU_Score": 0.8464,
    "F1_Score": 0.9168,
    "Precision": 0.8918,
    "Recall": 0.9433

    "Standard_IoU": 0.8464,
    "Boundary_IoU": 0.3481,
    "Hausdorff_Dist": 18.72,
    "FPS": 6.46
}
{

  # --- FINAL PERFORMANCE REPORT of our Satmae finetunned withouth Compounnd loss (hausdroff loss)
 Pixel Accuracy:   0.8732  (Overall correct pixels)
 IoU Score:        0.8358  (Intersection over Union - Target > 0.70)
 F1 / Dice Score:  0.9106   (Harmonic Mean of Precision & Recall)
 Precision:        0.9456  (Low False Positives)
 Recall:           0.8781  (Low False Negatives)

 Confusion Matrix Counts:
   True Wheat (TP):  129,585 pixels
   False Wheat (FP): 7,461 pixels (Over-segmentation)
   Missed Wheat (FN):17,991 pixels (Under-segmentation)
   Background (TN):  45,667 pixels

}
{"Results of the experimented 10 layer frozen and last 2 layer unfrozen"
    "Model": "SatMAE_partial_FineTune",
    "Date": "2026-01-13 04:34",
    "Standard_IoU": 0.8213,
    "Boundary_IoU": 0.3582,
    "Hausdorff_Dist_px": 21.24,
    "F1_Score": 0.9019,
    "Precision": 0.9425,
    "Recall": 0.8646,
    "Accuracy": 0.8679,
    "Inference_Time_Sec": 0.49,
    "FPS": 8.16
}

#After that i start working on Satmae finetunned with compound loss with 500 epochs but the results were poor than the 50 epochs model thenn i change the time 3 to 6 months and increase channel to 8 to 10 and implementing new methods and techniques to overcome the overfitting and over training issues
# Right now that colab notebook is running , when it will be completed i will share to you