geoai-disastermapper / utils /inference.py
meli9's picture
Climate Analysis and Inference
1614abd verified
Raw
History Blame Contribute Delete
4.29 kB
import torch
import numpy as np
from PIL import Image
import segmentation_models_pytorch as smp
import geopandas as gpd
from shapely.geometry import shape
import rasterio
from rasterio.features import shapes
from rasterio.transform import from_bounds
import tempfile
import os
# Damage labels and colors
LABELS = ['background', 'no-damage', 'minor', 'major', 'destroyed']
COLORS = ['#000000', '#2ecc71', '#f1c40f', '#e67e22', '#e74c3c']
def load_model(model_path: str, device: str = 'cpu'):
"""Load trained U-Net model from path."""
model = smp.Unet(
encoder_name='resnet50',
encoder_weights=None,
in_channels=6,
classes=5,
activation=None
).to(device)
model.load_state_dict(torch.load(model_path, map_location=device))
model.eval()
return model
def preprocess_image(image: Image.Image) -> np.ndarray:
"""Convert PIL image to normalized numpy array."""
img = np.array(image.convert('RGB')).astype(np.float32) / 255.0
return img
def run_inference(model, pre_image: Image.Image, post_image: Image.Image, device: str = 'cpu') -> np.ndarray:
"""
Run damage detection inference on before/after image pair.
Returns damage prediction map as numpy array.
"""
pre_img = preprocess_image(pre_image)
post_img = preprocess_image(post_image)
# Resize to same size if needed
if pre_img.shape != post_img.shape:
post_image = post_image.resize(pre_image.size)
post_img = preprocess_image(post_image)
# Stack 6 channels
stacked = np.concatenate([pre_img, post_img], axis=2)
# Convert to tensor
tensor = torch.tensor(stacked)\
.permute(2, 0, 1)\
.unsqueeze(0)\
.float()\
.to(device)
with torch.no_grad():
output = model(tensor)
pred = torch.argmax(output, dim=1)\
.squeeze(0)\
.cpu()\
.numpy()
return pred
def get_damage_stats(pred: np.ndarray) -> dict:
"""Calculate damage statistics from prediction map."""
unique, counts = np.unique(pred, return_counts=True)
total = pred.size
stats = {}
for u, c in zip(unique, counts):
if u < len(LABELS):
stats[LABELS[u]] = {
'pixels' : int(c),
'percent' : round(c / total * 100, 2)
}
return stats
def prediction_to_colored_image(pred: np.ndarray) -> np.ndarray:
"""Convert prediction map to RGB colored image."""
color_map = {
0: [0, 0, 0 ], # background - black
1: [46, 204, 113], # no damage - green
2: [241, 196, 15 ], # minor - yellow
3: [230, 126, 34 ], # major - orange
4: [231, 76, 60 ], # destroyed - red
}
colored = np.zeros((*pred.shape, 3), dtype=np.uint8)
for class_id, color in color_map.items():
colored[pred == class_id] = color
return colored
def export_geojson(pred: np.ndarray, bounds: tuple = None) -> str:
"""
Export damage predictions as GeoJSON file.
bounds = (left, bottom, right, top) in WGS84
Returns path to saved GeoJSON file.
"""
h, w = pred.shape
# Use provided bounds or default
if bounds:
transform = from_bounds(*bounds, w, h)
crs = 'EPSG:4326'
else:
from rasterio.transform import Affine
transform = Affine(1, 0, 0, 0, -1, h)
crs = None
# Only export damaged areas
damage_only = np.where(pred >= 2, pred, 0).astype(np.uint8)
results = []
for geom, val in shapes(damage_only, transform=transform):
if val >= 2:
results.append({
'geometry' : shape(geom),
'damage_class' : int(val),
'damage_label' : LABELS[int(val)]
})
if not results:
return None
gdf = gpd.GeoDataFrame(results)
if crs:
gdf.set_crs(crs, inplace=True)
# Save to temp file
tmp = tempfile.NamedTemporaryFile(suffix='.geojson', delete=False)
gdf.to_file(tmp.name, driver='GeoJSON')
return tmp.name