Spaces:
Sleeping
Sleeping
| """RoofSight — Roof Plane Segmentation | |
| Gradio web app using NVIDIA C-RADIOv4-H (631M params) for zero-shot | |
| roof type segmentation combined with RANSAC 3D plane fitting on | |
| Google Solar API DSM data. | |
| Deployed on HuggingFace Spaces with ZeroGPU. | |
| """ | |
| import json | |
| import os | |
| import tempfile | |
| import gradio as gr | |
| import numpy as np | |
| import torch | |
| # Pre-load model weights to CPU at startup (moved to GPU per-request by ZeroGPU) | |
| from radio_backbone import load_model | |
| print("Initializing RoofSight...") | |
| device = "cuda" if torch.cuda.is_available() else "cpu" | |
| load_model(device="cpu") | |
| # Try importing spaces for ZeroGPU decorator | |
| try: | |
| import spaces | |
| GPU_DECORATOR = spaces.GPU | |
| except ImportError: | |
| # Local dev without ZeroGPU | |
| def GPU_DECORATOR(fn): | |
| return fn | |
| GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY", "") | |
| def process(address, api_key, radius, ransac_threshold, max_planes, min_area): | |
| """Run the full pipeline and return results for Gradio.""" | |
| from pipeline import run as run_pipeline | |
| api_key = api_key.strip() if api_key.strip() else GOOGLE_API_KEY | |
| if not api_key: | |
| return None, None, "", "No API key provided. Enter your Google Solar API key or set GOOGLE_API_KEY." | |
| compute_device = "cuda" if torch.cuda.is_available() else "cpu" | |
| try: | |
| result = run_pipeline( | |
| address=address, | |
| api_key=api_key, | |
| radius_meters=int(radius), | |
| ransac_threshold=ransac_threshold, | |
| max_planes=int(max_planes), | |
| min_area_sqft=min_area, | |
| device=compute_device, | |
| ) | |
| geojson_str = json.dumps(result.geojson, indent=2) | |
| status = "\n".join(result.status) | |
| return result.original_image, result.overlay, geojson_str, status | |
| except Exception as e: | |
| return None, None, "", f"Error: {str(e)}" | |
| def save_geojson(geojson_str): | |
| """Save GeoJSON to a downloadable temp file.""" | |
| if not geojson_str or geojson_str.strip() == "": | |
| return None | |
| tmp = tempfile.NamedTemporaryFile(suffix=".geojson", delete=False, mode="w") | |
| tmp.write(geojson_str) | |
| tmp.close() | |
| return tmp.name | |
| # --- Gradio UI --- | |
| with gr.Blocks(title="RoofSight", theme=gr.themes.Soft()) as demo: | |
| gr.Markdown("# RoofSight") | |
| gr.Markdown( | |
| "Roof plane segmentation from satellite imagery. \n" | |
| "**C-RADIOv4-H** (DINOv3 + SAM3 + SigLIP2) + **RANSAC** 3D plane fitting." | |
| ) | |
| with gr.Row(): | |
| # --- Left column: inputs --- | |
| with gr.Column(scale=1): | |
| address = gr.Textbox( | |
| label="Property Address", | |
| placeholder="123 Main St, City, ST 12345", | |
| ) | |
| api_key = gr.Textbox( | |
| label="Google API Key", | |
| type="password", | |
| placeholder="Leave blank to use server key", | |
| ) | |
| with gr.Accordion("Settings", open=False): | |
| radius = gr.Slider( | |
| 25, 100, value=50, step=5, | |
| label="Search Radius (m)", | |
| ) | |
| ransac_thresh = gr.Slider( | |
| 0.05, 0.50, value=0.15, step=0.01, | |
| label="RANSAC Threshold (m)", | |
| info="Distance tolerance for plane inliers. Lower = tighter fit.", | |
| ) | |
| max_planes = gr.Slider( | |
| 2, 15, value=8, step=1, | |
| label="Max Planes", | |
| ) | |
| min_area = gr.Slider( | |
| 10, 500, value=50, step=10, | |
| label="Min Area (sqft)", | |
| info="Polygons smaller than this are excluded.", | |
| ) | |
| run_btn = gr.Button("Segment Roof", variant="primary", size="lg") | |
| # --- Right column: outputs --- | |
| with gr.Column(scale=2): | |
| with gr.Row(): | |
| orig_img = gr.Image(label="Satellite Image", show_download_button=False) | |
| overlay_img = gr.Image(label="Roof Planes", show_download_button=True) | |
| status_md = gr.Markdown(label="Status") | |
| with gr.Accordion("GeoJSON Output", open=True): | |
| geojson_out = gr.Code(language="json", lines=15, label="GeoJSON") | |
| with gr.Row(): | |
| dl_btn = gr.Button("Download GeoJSON", size="sm") | |
| dl_file = gr.File(label="Download", visible=True) | |
| # --- Wiring --- | |
| run_btn.click( | |
| fn=process, | |
| inputs=[address, api_key, radius, ransac_thresh, max_planes, min_area], | |
| outputs=[orig_img, overlay_img, geojson_out, status_md], | |
| ) | |
| dl_btn.click(fn=save_geojson, inputs=[geojson_out], outputs=[dl_file]) | |
| if __name__ == "__main__": | |
| demo.launch() | |