| 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
|
|
|
|
|
| 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)
|
|
|
|
|
| if pre_img.shape != post_img.shape:
|
| post_image = post_image.resize(pre_image.size)
|
| post_img = preprocess_image(post_image)
|
|
|
|
|
| stacked = np.concatenate([pre_img, post_img], axis=2)
|
|
|
|
|
| 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 ],
|
| 1: [46, 204, 113],
|
| 2: [241, 196, 15 ],
|
| 3: [230, 126, 34 ],
|
| 4: [231, 76, 60 ],
|
| }
|
| 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
|
|
|
|
|
| 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
|
|
|
|
|
| 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)
|
|
|
|
|
| tmp = tempfile.NamedTemporaryFile(suffix='.geojson', delete=False)
|
| gdf.to_file(tmp.name, driver='GeoJSON')
|
| return tmp.name |