GeoSpill-AI / src /inference.py
Boyakhchyan Tigran
Initial deploy - GeoSpill AI
962a956
Raw
History Blame Contribute Delete
3.94 kB
"""
src/inference.py
----------------
Model loading and inference for SAR oil spill detection.
"""
import os
import json
import numpy as np
import torch
try:
import segmentation_models_pytorch as smp
except ImportError:
smp = None
DB_CLIP_MIN = -50.0
DB_CLIP_MAX = 0.0
def build_unet(in_channels=2, classes=1):
if smp is None:
raise ImportError("segmentation_models_pytorch is required.")
return smp.Unet(encoder_name="resnet34", encoder_weights=None,
in_channels=in_channels, classes=classes)
def load_checkpoint(checkpoint_path, stats_path, device=None):
if not os.path.exists(checkpoint_path):
return None, None, f"Checkpoint not found: {checkpoint_path}"
if not os.path.exists(stats_path):
return None, None, f"Stats file not found: {stats_path}"
try:
if device is None:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = build_unet()
ckpt = torch.load(checkpoint_path, map_location=device, weights_only=False)
state = ckpt.get("model_state", ckpt.get("model_state_dict", ckpt))
model.load_state_dict(state)
model.to(device).eval()
with open(stats_path) as f:
stats = json.load(f)
return model, stats, None
except Exception as e:
return None, None, f"{type(e).__name__}: {e}"
def tile_positions(length, patch, stride):
if length <= patch:
return [0]
positions = list(range(0, length - patch + 1, stride))
if positions[-1] + patch < length:
positions.append(length - patch)
return positions
def predict_sliding(img, model, mean, std, patch_size=256, stride=128,
threshold=0.5, progress_cb=None):
device = next(model.parameters()).device
img_norm = np.clip(img[:2], DB_CLIP_MIN, DB_CLIP_MAX).astype(np.float32)
img_norm = (img_norm - mean[:, None, None]) / (std[:, None, None] + 1e-6)
_, H, W = img_norm.shape
pred_sum = np.zeros((H, W), dtype=np.float32)
pred_count = np.zeros((H, W), dtype=np.float32)
ys = tile_positions(H, patch_size, stride)
xs = tile_positions(W, patch_size, stride)
total = len(ys) * len(xs)
done = 0
with torch.no_grad():
for y in ys:
for x in xs:
patch = img_norm[:, y:y+patch_size, x:x+patch_size]
t = torch.from_numpy(patch).unsqueeze(0).to(device)
logits = model(t)
probs = torch.sigmoid(logits).squeeze().cpu().numpy()
pred_sum[y:y+patch_size, x:x+patch_size] += probs
pred_count[y:y+patch_size, x:x+patch_size] += 1.0
done += 1
if progress_cb:
progress_cb(done, total)
avg_prob = pred_sum / np.maximum(pred_count, 1e-6)
mask = (avg_prob > threshold).astype(np.float32)
return mask, avg_prob
def predict_demo(img, threshold_pct=8):
vv = img[0] if img.ndim == 3 else img
vv_clipped = np.clip(vv, DB_CLIP_MIN, DB_CLIP_MAX)
vv_norm = (vv_clipped - vv_clipped.min()) / (vv_clipped.max() - vv_clipped.min() + 1e-6)
thresh_val = np.percentile(vv_norm, threshold_pct)
# Invert so darker = higher probability (matching model convention)
prob = 1.0 - vv_norm
mask = (vv_norm < thresh_val).astype(np.float32)
return mask, prob.astype(np.float32)
def filter_small_regions(mask, min_pixels=500):
"""
Remove connected components smaller than min_pixels.
Real oil spills form large coherent regions — small isolated patches
are almost always calm water, wind shadows, or sensor noise.
"""
from scipy.ndimage import label
labeled, n_features = label(mask > 0.5)
filtered = np.zeros_like(mask)
for i in range(1, n_features + 1):
component = (labeled == i)
if component.sum() >= min_pixels:
filtered[component] = 1.0
return filtered