Deagin's picture
Rewrite: C-RADIOv4-H + RANSAC fusion pipeline
5c52fb9
"""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", "")
@GPU_DECORATOR
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()