Spaces:
Sleeping
Sleeping
| from fastapi import FastAPI, UploadFile, File | |
| from fastapi.responses import Response | |
| import torch | |
| import torch.nn.functional as F | |
| import numpy as np | |
| import cv2 | |
| from PIL import Image | |
| import io | |
| import torchvision.transforms.functional as TF | |
| from transformers import SegformerForSemanticSegmentation | |
| app = FastAPI(title="Unified Cartographer API", version="1.0") | |
| # ========================================== | |
| # 1. LOAD MODEL ON SERVER STARTUP | |
| # ========================================== | |
| DEVICE = "cuda" if torch.cuda.is_available() else "cpu" | |
| print(f"Server booting on: {DEVICE}") | |
| class UnifiedCartographer(torch.nn.Module): | |
| def __init__(self, num_classes=5): | |
| super().__init__() | |
| self.model = SegformerForSemanticSegmentation.from_pretrained( | |
| "nvidia/segformer-b4-finetuned-cityscapes-1024-1024", | |
| num_labels=num_classes, ignore_mismatched_sizes=True | |
| ) | |
| def forward(self, x): | |
| outputs = self.model(pixel_values=x) | |
| return F.interpolate(outputs.logits, size=x.shape[-2:], mode="bilinear", align_corners=False) | |
| model = UnifiedCartographer(num_classes=5).to(DEVICE) | |
| # ⚠️ Ensure this filename matches your uploaded weights perfectly | |
| model.load_state_dict(torch.load("unified_cartographer_andhra_v4.pth", map_location=DEVICE)) | |
| model.eval() | |
| # ========================================== | |
| # 2. THE RENDERING ENGINE (Now with Legend!) | |
| # ========================================== | |
| def draw_legend(canvas): | |
| """Draws a semi-transparent legend in the top-left corner""" | |
| legend_items = [ | |
| ("Bldg (High Conf)", (255, 0, 0)), # Red | |
| ("Bldg (Med Conf)", (255, 165, 0)), # Orange | |
| ("Bldg (Low Conf)", (255, 0, 255)), # Magenta | |
| ("Roads", (255, 255, 0)), # Yellow | |
| ("Water", (0, 100, 255)), # Blue | |
| ("Open Area", (50, 205, 50)) # Green | |
| ] | |
| # 1. Create a semi-transparent black background box for readability | |
| overlay = canvas.copy() | |
| cv2.rectangle(overlay, (10, 10), (210, 160), (0, 0, 0), -1) | |
| cv2.addWeighted(overlay, 0.7, canvas, 0.3, 0, canvas) | |
| # 2. Add the title of the legend | |
| cv2.putText(canvas, "AI TACTICAL MAP", (20, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2) | |
| # 3. Draw the color boxes and labels | |
| y_offset = 55 | |
| for label, color in legend_items: | |
| # Draw the little color square | |
| cv2.rectangle(canvas, (20, y_offset - 10), (35, y_offset + 5), color, -1) | |
| # Draw the text next to it | |
| cv2.putText(canvas, label, (45, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 255, 255), 1) | |
| y_offset += 20 | |
| return canvas | |
| def draw_hollow_confidence_map(base_img, mask_pred, bldg_probs): | |
| # Darken the original satellite image for the background | |
| canvas = np.ascontiguousarray((base_img * 0.6).astype(np.uint8)) | |
| is_bldg = (mask_pred == 1) | |
| # Calculate confidence tiers | |
| mask_high = np.uint8(is_bldg & (bldg_probs >= 0.75)) | |
| mask_med = np.uint8(is_bldg & (bldg_probs >= 0.25) & (bldg_probs < 0.75)) | |
| mask_low = np.uint8(is_bldg & (bldg_probs < 0.25)) | |
| # Draw Hollow Buildings First | |
| cv2.drawContours(canvas, cv2.findContours(mask_high, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0], -1, (255, 0, 0), 3) | |
| cv2.drawContours(canvas, cv2.findContours(mask_med, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0], -1, (255, 165, 0), 3) | |
| cv2.drawContours(canvas, cv2.findContours(mask_low, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0], -1, (255, 0, 255), 3) | |
| # Paint Solid Terrain Underneath | |
| road_mask = np.uint8(mask_pred == 2) | |
| canvas[road_mask == 1] = [255, 255, 0] # Yellow Roads | |
| canvas[mask_pred == 3] = [0, 100, 255] # Blue Water | |
| canvas[mask_pred == 4] = [50, 205, 50] # Green Open Area | |
| # ✨ Add the Legend to the final canvas before returning | |
| canvas = draw_legend(canvas) | |
| return canvas | |
| # ========================================== | |
| # 3. THE API ENDPOINTS | |
| # ========================================== | |
| def health_check(): | |
| return {"status": "online", "model": "SegFormer-B4", "device": DEVICE} | |
| async def predict_segmentation(file: UploadFile = File(...)): | |
| # 1. Read the uploaded image into memory | |
| image_bytes = await file.read() | |
| raw_pil = Image.open(io.BytesIO(image_bytes)).convert("RGB").resize((512, 512), Image.BILINEAR) | |
| img_np = np.array(raw_pil) | |
| # 2. Prepare for the AI | |
| tensor_img = TF.to_tensor(raw_pil) | |
| tensor_img = TF.normalize(tensor_img, mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)) | |
| tensor_img = tensor_img.unsqueeze(0).to(DEVICE) | |
| # 3. Run Inference & Extract Probabilities | |
| with torch.no_grad(): | |
| logits = model(tensor_img) | |
| mask_pred = torch.argmax(logits, dim=1).squeeze().cpu().numpy() | |
| probs = F.softmax(logits, dim=1).squeeze().cpu().numpy() | |
| bldg_probs = probs[1, :, :] # Extract Class 1 (Buildings) probability map | |
| # 4. Generate the proper tactical map (now with the legend baked in) | |
| output_canvas = draw_hollow_confidence_map(img_np, mask_pred, bldg_probs) | |
| # 5. Compress back to a PNG and send to the user | |
| is_success, buffer = cv2.imencode(".png", cv2.cvtColor(output_canvas, cv2.COLOR_RGB2BGR)) | |
| io_buf = io.BytesIO(buffer) | |
| return Response(content=io_buf.getvalue(), media_type="image/png") |