atmos / src /models /land_mask.py
Edge001's picture
Upload folder using huggingface_hub
c2a61b6 verified
Raw
History Blame Contribute Delete
4.25 kB
"""
Land-Sea Mask for ERA5 India domain.
IMPROVEMENT 4: Land-sea mask
The AI model (Swin2SR) has no knowledge of land vs ocean.
Applying super-resolution to ocean pixels creates hallucinated
fine-scale SST structure that doesn't exist physically.
This module builds a land mask at both ERA5 resolution (129x121)
and output resolution (516x484) using two methods:
Method 1 - Temperature variance proxy:
Ocean pixels have very low temporal variance (SST is smooth and slow).
Land pixels have high variance (diurnal cycle, seasons, weather).
Threshold the annual std dev to separate land from sea.
Method 2 - Known ocean bounds (backup):
Hard-code the approximate lat/lon rectangles of:
- Arabian Sea (west coast)
- Bay of Bengal (east coast)
- Indian Ocean (south)
Use as fallback or cross-validation.
The mask is applied after AI inference:
final_output[ocean] = ERA5_upsampled[ocean] (original SST preserved)
final_output[land] = AI_prediction[land] (AI enhancement applied)
This is scientifically correct: ERA5 SST is already high quality
at 0.25deg. Only land surface temperatures benefit from SR enhancement.
"""
import numpy as np
from scipy.ndimage import zoom as spz, binary_dilation
def build_land_mask(data: np.ndarray,
lat_min: float, lat_max: float,
lon_min: float, lon_max: float,
output_scale: int = 4) -> tuple:
"""
Build land mask from temporal variance of ERA5 data.
Returns:
mask_lr : (H, W) bool True=land at ERA5 resolution
mask_hr : (4H, 4W) bool True=land at output resolution
"""
T, H, W = data.shape
# Temporal std dev at each grid point across all 8784 timesteps
std_map = data.std(axis=0) # (H, W)
# Ocean = low variance (SST changes slowly)
# Land = high variance (weather, diurnal, seasonal)
# Use Otsu-like threshold: midpoint between min and median
thresh = (std_map.min() + np.percentile(std_map, 40)) / 2
land_mask_lr = std_map > thresh # True = land
# Morphological cleanup — remove isolated ocean pixels on coast
land_mask_lr = binary_dilation(land_mask_lr, iterations=1)
# Hard-mask: known open ocean bounding boxes
lats = np.linspace(lat_max, lat_min, H) # row 0 = north
lons = np.linspace(lon_min, lon_max, W)
ocean_boxes = [
# (lat_north, lat_south, lon_west, lon_east)
# Conservative boxes — only clear open ocean, not coastal zones
# Coastal land (Gujarat, Kerala, AP coast) handled by variance method
(38.0, 6.0, 68.0, 71.0), # Arabian Sea open water (far west)
(12.0, 6.0, 71.0, 79.0), # Indian Ocean (south tip, below tip of India)
(22.0, 6.0, 88.0, 98.0), # Bay of Bengal (far east open water)
]
for ln, ls, lw, le in ocean_boxes:
for r in range(H):
for c in range(W):
if ls <= lats[r] <= ln and lw <= lons[c] <= le:
land_mask_lr[r, c] = False # force ocean
# Upsample to output resolution
land_mask_hr_f = spz(land_mask_lr.astype(float), output_scale, order=0)
land_mask_hr = land_mask_hr_f > 0.5
n_land = land_mask_lr.sum()
n_total = H * W
print(f" [Mask] Land pixels: {n_land}/{n_total} "
f"({n_land/n_total*100:.1f}%) at ERA5 resolution")
print(f" [Mask] Output mask: {land_mask_hr.shape}")
return land_mask_lr, land_mask_hr
def apply_land_mask(pred_K: np.ndarray,
era5_K: np.ndarray,
mask_hr: np.ndarray) -> np.ndarray:
"""
Blend AI prediction with ERA5 using land-sea mask.
pred_K : (4H, 4W) AI prediction in Kelvin
era5_K : (H, W) ERA5 input in Kelvin
mask_hr : (4H, 4W) True=land, False=ocean
Returns: (4H, 4W) masked output
"""
# Upsample ERA5 to output resolution for ocean pixels
H, W = era5_K.shape
era5_up = spz(era5_K, pred_K.shape[0] / H, order=3)
era5_up = era5_up[:pred_K.shape[0], :pred_K.shape[1]]
# Blend: land=AI, ocean=ERA5
out = era5_up.copy()
out[mask_hr] = pred_K[mask_hr]
return out