Spaces:
Sleeping
Sleeping
Download app.py from Rudraaaa76/geospatial-api: direct link, hf CLI and curl.
- Browser
- Download file 8.61 kB
-
https://huggingface.co/spaces/Rudraaaa76/geospatial-api/resolve/main/app.py
- Command line
-
hf download hf://spaces/Rudraaaa76/geospatial-api/app.py
-
curl -L -o app.py https://huggingface.co/spaces/Rudraaaa76/geospatial-api/resolve/main/app.py
8.61 kB
| """ | |
| Aerix — Hugging Face Spaces Gradio App with ZeroGPU | |
| Wraps the inference.py pipeline as a Gradio interface with automatic A100 GPU | |
| allocation via @spaces.GPU decorator. | |
| Key fix vs the previous version: CPU-bound work (loading/downsampling the | |
| image, building tiles, postprocessing/rooftop classification) now happens | |
| OUTSIDE the @spaces.GPU-decorated functions. Only the actual tensor forward | |
| pass runs inside them. This is what was causing crashes/hangs on large TIFF | |
| uploads — a big GeoTIFF's decode+resize could take longer than the ZeroGPU | |
| duration window, or blow past available RAM before the GPU was even attached. | |
| """ | |
| import io | |
| import gc | |
| import base64 | |
| import numpy as np | |
| from pathlib import Path | |
| from PIL import Image | |
| import cv2 | |
| import torch | |
| try: | |
| import spaces | |
| except ImportError: | |
| class _SpacesFallback: | |
| def GPU(duration=0): | |
| def decorator(func): | |
| return func | |
| return decorator | |
| spaces = _SpacesFallback() | |
| import gradio as gr | |
| from inference import AerixSegmentationModel, get_image_info, validate_upload | |
| # --------------------------------------------------------------------------- | |
| # Model initialisation (module-level, CPU — ZeroGPU attaches the GPU later, | |
| # only for the duration of a @spaces.GPU-decorated call) | |
| # --------------------------------------------------------------------------- | |
| print("Initialising AerixSegmentationModel …") | |
| model = AerixSegmentationModel(models_dir="models") | |
| print(f"Model ready on device: {model.device}") | |
| # Single tile vs sliding-window routing threshold (post-downsample px, longest side) | |
| TILE_ROUTE_THRESHOLD = 512 | |
| MAX_DISPLAY_DIM = 2048 | |
| def numpy_to_base64(arr: np.ndarray, fmt: str = "PNG") -> str: | |
| img = Image.fromarray(arr) | |
| buf = io.BytesIO() | |
| img.save(buf, format=fmt) | |
| return base64.b64encode(buf.getvalue()).decode("utf-8") | |
| # --------------------------------------------------------------------------- | |
| # Available sample tiles | |
| # --------------------------------------------------------------------------- | |
| TILES_DIR = Path("sample_data/tiles") | |
| TILE_CHOICES = ["none"] | |
| if TILES_DIR.exists(): | |
| TILE_CHOICES += [ | |
| f.name for f in sorted(TILES_DIR.glob("*.png")) | |
| if "mask" not in f.name and "gt_" not in f.name | |
| ] | |
| # --------------------------------------------------------------------------- | |
| # GPU stages — kept as thin as possible. These are the ONLY functions that | |
| # touch CUDA. Everything else in this file is plain CPU code. | |
| # --------------------------------------------------------------------------- | |
| def _gpu_run_single(feature: str, input_array: np.ndarray) -> np.ndarray: | |
| return model.run_model(feature, input_array) | |
| def _gpu_run_tiles(feature: str, chips): | |
| return model.run_model_tiles(feature, chips) | |
| # --------------------------------------------------------------------------- | |
| # Main handler — plain function, NOT @spaces.GPU. It does CPU work directly | |
| # and only reaches into the GPU stages above for the actual model forward. | |
| # --------------------------------------------------------------------------- | |
| def predict(image, feature, threshold, sample_tile): | |
| """ | |
| Run UNet++ segmentation on a drone orthophoto. | |
| """ | |
| if sample_tile and sample_tile != "none": | |
| image_path = str(TILES_DIR / sample_tile) | |
| if not Path(image_path).exists(): | |
| return {"error": f"Sample tile not found: {sample_tile}"} | |
| elif image is not None: | |
| image_path = image | |
| else: | |
| return {"error": "No image provided. Upload an image or select a sample tile."} | |
| # --- Fast, cheap rejection before touching any pixel data ------------- | |
| try: | |
| info = get_image_info(image_path) | |
| validate_upload(image_path) | |
| except ValueError as e: | |
| return {"error": str(e)} | |
| except Exception as e: | |
| return {"error": f"Could not read file: {e}"} | |
| try: | |
| # ---- CPU stage: load + downsample (rasterio windowed read for TIFF) | |
| original_image = _load_for_routing(image_path) | |
| h, w = original_image.shape[:2] | |
| if max(h, w) <= TILE_ROUTE_THRESHOLD: | |
| # ---- small image: single-tile path | |
| resized = cv2.resize(original_image, (512, 512)) | |
| input_array = resized.astype(np.float32) / 255.0 | |
| raw = _gpu_run_single(feature, input_array) # GPU | |
| raw_full = cv2.resize(raw, (w, h)) | |
| result = model.postprocess(original_image, raw_full, feature, threshold) # CPU | |
| else: | |
| # ---- large orthomosaic: tiled / sliding-window path | |
| tiles = model.prepare_tiles(original_image) # CPU | |
| preds = _gpu_run_tiles(feature, tiles["chips"]) # GPU | |
| mask = model.stitch_tiles(preds, tiles["coords"], tiles["weight_map"], | |
| tiles["image_shape"], threshold) # CPU | |
| overlay = model._create_overlay(original_image, mask) | |
| detected_pixels = int(np.sum(mask > 0)) | |
| total_pixels = int(mask.size) | |
| rooftop_analysis = ( | |
| model.classify_rooftops(original_image, mask) | |
| if feature == "buildings" and detected_pixels > 0 else None | |
| ) | |
| result = { | |
| "original_image": original_image, | |
| "mask": mask, | |
| "overlay": overlay, | |
| "feature": feature, | |
| "detected_ratio": detected_pixels / total_pixels if total_pixels else 0, | |
| "detected_pixels": detected_pixels, | |
| "total_pixels": total_pixels, | |
| "rooftop_analysis": rooftop_analysis, | |
| } | |
| response = { | |
| "feature": result["feature"], | |
| "detected_ratio": float(result["detected_ratio"]), | |
| "detected_pixels": int(result["detected_pixels"]), | |
| "total_pixels": int(result["total_pixels"]), | |
| "original_image": numpy_to_base64(result["original_image"]), | |
| "mask": numpy_to_base64(result["mask"]), | |
| "overlay": numpy_to_base64(result["overlay"]), | |
| } | |
| if result.get("rooftop_analysis"): | |
| ra = result["rooftop_analysis"] | |
| response["rooftop_analysis"] = { | |
| "rooftop_counts": ra["rooftop_counts"], | |
| "total_buildings": ra["total_buildings"], | |
| "total_builtup_m2": ra["total_builtup_m2"], | |
| "rcc_area_m2": ra["rcc_area_m2"], | |
| "annual_solar_kwh": ra["annual_solar_kwh"], | |
| "annual_property_tax_inr": ra["annual_property_tax_inr"], | |
| "classification_map": numpy_to_base64(ra["classification_map"]), | |
| "building_details": ra["building_details"][:50], | |
| } | |
| return response | |
| except Exception as e: | |
| return {"error": str(e)} | |
| finally: | |
| gc.collect() | |
| if torch.cuda.is_available(): | |
| torch.cuda.empty_cache() | |
| def _load_for_routing(image_path: str) -> np.ndarray: | |
| """CPU-only load, used to decide single-tile vs sliding-window routing.""" | |
| from inference import load_robust_image | |
| return load_robust_image(image_path, max_dim=MAX_DISPLAY_DIM) | |
| # --------------------------------------------------------------------------- | |
| # Gradio Interface | |
| # --------------------------------------------------------------------------- | |
| demo = gr.Interface( | |
| fn=predict, | |
| inputs=[ | |
| gr.Image(type="filepath", label="Drone Orthophoto"), | |
| gr.Dropdown(choices=["buildings", "roads", "water_bodies"], value="buildings", label="Feature Class"), | |
| gr.Slider(minimum=0.1, maximum=0.9, step=0.05, value=0.5, label="Confidence Threshold"), | |
| gr.Dropdown(choices=TILE_CHOICES, value="none", label="Sample Tile"), | |
| ], | |
| outputs=gr.JSON(label="Inference Results"), | |
| title="🛰️ Aerix — SVAMITVA AI Segmentation", | |
| description=( | |
| "UNet++ deep learning inference on 50 cm GSD drone orthophotos for " | |
| "building footprint demarcation, road network vectorisation, and " | |
| "waterbody extraction. Part of SIH Problem Statement 1705 — " | |
| "Ministry of Panchayati Raj, Government of India.\n\n" | |
| "Uploads over 1 GB are rejected up front — downsample or crop first." | |
| ), | |
| api_name="predict", | |
| flagging_mode="never", | |
| ) | |
| if __name__ == "__main__": | |
| # max_file_size caps the raw upload so an oversized file fails fast at the | |
| # Gradio layer instead of hanging the request indefinitely. | |
| demo.queue().launch(max_file_size="1gb") |