File size: 4,749 Bytes
5c52fb9
66bcd8c
5c52fb9
 
 
66bcd8c
5c52fb9
 
9096a3a
5c52fb9
 
 
4f26dc9
5c52fb9
 
 
9096a3a
5c52fb9
 
 
 
 
66bcd8c
5c52fb9
e6f2efc
5c52fb9
 
 
 
 
 
eb2a5d2
5c52fb9
eb2a5d2
 
5c52fb9
 
 
 
80940c5
5c52fb9
eb2a5d2
5c52fb9
80940c5
5c52fb9
5ad10c2
eb2a5d2
5c52fb9
 
 
 
 
 
 
 
 
80940c5
5c52fb9
 
 
80940c5
eb2a5d2
5c52fb9
eb2a5d2
 
 
5c52fb9
 
eb2a5d2
5c52fb9
 
 
 
 
 
 
 
 
 
 
 
 
80940c5
66bcd8c
5c52fb9
66bcd8c
5c52fb9
 
 
 
 
 
 
 
eb2a5d2
80940c5
5c52fb9
 
eb2a5d2
5c52fb9
66bcd8c
5c52fb9
 
 
 
 
 
 
 
eb2a5d2
 
80940c5
5c52fb9
 
73fbf43
 
5c52fb9
80940c5
5c52fb9
66bcd8c
 
5c52fb9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66bcd8c
5c52fb9
80940c5
66bcd8c
5c52fb9
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
"""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()