Spaces:
Sleeping
Sleeping
| import io | |
| import torch | |
| import numpy as np | |
| from PIL import Image | |
| from fastapi import FastAPI, File, UploadFile | |
| from fastapi.responses import Response | |
| from torchvision import transforms | |
| import matplotlib | |
| matplotlib.use('Agg') # Use non-interactive backend for server | |
| import matplotlib.pyplot as plt | |
| from matplotlib.patches import Patch | |
| # Import architecture, colors, and utils from your model.py | |
| from model import ( | |
| LSNN, | |
| IMAGENET_MEAN, | |
| IMAGENET_STD, | |
| DISPLAY_COLORS, | |
| LEGEND_ENTRIES, | |
| predict_tta | |
| ) | |
| app = FastAPI(title="Semantic Change Detection API") | |
| device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') | |
| model = None | |
| preprocess = transforms.Compose([ | |
| transforms.Resize((256, 256)), | |
| transforms.ToTensor(), | |
| transforms.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD), | |
| ]) | |
| def class_map_to_rgb(cls_map): | |
| rgb = np.zeros((*cls_map.shape, 3), dtype=np.uint8) | |
| for cls, col in DISPLAY_COLORS.items(): | |
| rgb[cls_map == cls] = col | |
| return rgb | |
| def make_legend_patches(): | |
| return [Patch(facecolor=np.array(c) / 255, label=l) for c, l in LEGEND_ENTRIES] | |
| async def health_check(): | |
| return { | |
| "status": "ok", | |
| "model_loaded": model is not None, | |
| "device": str(device), | |
| } | |
| async def load_model(): | |
| global model | |
| print("Loading LSNN model...") | |
| model = LSNN(hidden=96).to(device) | |
| ckpt = torch.load("best_lsnn.pth", map_location=device) | |
| if "model" in ckpt: | |
| model.load_state_dict(ckpt["model"]) | |
| else: | |
| model.load_state_dict(ckpt) | |
| model.eval() | |
| print("Model loaded!") | |
| async def predict_change(img1: UploadFile = File(...), img2: UploadFile = File(...)): | |
| # 1. Read and prep images | |
| raw_img1 = Image.open(io.BytesIO(await img1.read())).convert("RGB") | |
| raw_img2 = Image.open(io.BytesIO(await img2.read())).convert("RGB") | |
| # Resize raw images for plotting so they match the 256x256 prediction map | |
| disp_img1 = raw_img1.resize((256, 256)) | |
| disp_img2 = raw_img2.resize((256, 256)) | |
| # 2. Preprocess for the model | |
| t1 = preprocess(raw_img1).unsqueeze(0).to(device) | |
| t2 = preprocess(raw_img2).unsqueeze(0).to(device) | |
| # 3. Inference | |
| with torch.no_grad(): | |
| sem_logits = predict_tta(model, t1, t2) | |
| pred_mask = torch.argmax(sem_logits, dim=1).squeeze(0).cpu().numpy() | |
| # 4. Process predictions | |
| change_pct = 100.0 * (pred_mask != 0).sum() / pred_mask.size | |
| pred_rgb = class_map_to_rgb(pred_mask) | |
| # 5. Generate Matplotlib Figure | |
| fig = plt.figure(figsize=(15, 6)) | |
| gs = fig.add_gridspec(2, 3, height_ratios=[5, 1], hspace=0.1, wspace=0.1) | |
| # Old Image | |
| ax1 = fig.add_subplot(gs[0, 0]) | |
| ax1.imshow(disp_img1) | |
| ax1.set_title("Old Image", fontweight='bold') | |
| ax1.axis('off') | |
| # New Image | |
| ax2 = fig.add_subplot(gs[0, 1]) | |
| ax2.imshow(disp_img2) | |
| ax2.set_title("New Image", fontweight='bold') | |
| ax2.axis('off') | |
| # Prediction | |
| ax3 = fig.add_subplot(gs[0, 2]) | |
| ax3.imshow(pred_rgb) | |
| ax3.set_title("Prediction", fontweight='bold') | |
| ax3.axis('off') | |
| # Legend | |
| ax_lg = fig.add_subplot(gs[1, :]) | |
| ax_lg.axis('off') | |
| ax_lg.legend(handles=make_legend_patches(), loc='center', ncol=7, fontsize=10, frameon=False) | |
| # Percentage Text | |
| fig.text(0.5, 0.05, f"Change Detected: {change_pct:.2f}%", | |
| ha='center', fontsize=14, fontweight='bold', color='red') | |
| # 6. Save plot to buffer and return | |
| buf = io.BytesIO() | |
| plt.savefig(buf, format="png", bbox_inches='tight', dpi=150) | |
| plt.close(fig) | |
| buf.seek(0) | |
| return Response(content=buf.getvalue(), media_type="image/png") |