Rudraaaa76 commited on
Commit
9911bc7
·
verified ·
1 Parent(s): 53631db

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +810 -131
app.py CHANGED
@@ -1,34 +1,56 @@
1
  """
2
  Hugging Face Space Entry Point for Project Vaayu
3
- Serves both an interactive Gradio interface and REST API endpoints for Vercel.
 
 
 
 
4
  """
5
 
 
 
 
 
 
 
 
6
  import os
7
  import io
8
  import base64
9
  import json
10
- import numpy as np
11
  from pathlib import Path
 
 
12
  from PIL import Image
 
13
  import gradio as gr
 
14
  from fastapi import FastAPI, File, UploadFile, Form
15
  from fastapi.middleware.cors import CORSMiddleware
16
- from fastapi.responses import JSONResponse, StreamingResponse
17
- from fastapi.staticfiles import StaticFiles
18
 
19
- try:
20
- import spaces
21
- has_spaces = True
22
- except ImportError:
23
- has_spaces = False
24
 
25
- # Import existing inference engine
26
  from inference import VaayuSegmentationModel
27
 
28
- # Initialize FastAPI App
29
- app = FastAPI(title="Vaayu Geospatial AI API")
30
 
31
- # Enable CORS for Vercel & localhost
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  app.add_middleware(
33
  CORSMiddleware,
34
  allow_origins=["*"],
@@ -37,91 +59,346 @@ app.add_middleware(
37
  allow_headers=["*"],
38
  )
39
 
40
- # Initialize Model Instance
 
 
 
 
41
  model_instance = None
42
 
 
43
  def get_model():
 
 
 
 
 
44
  global model_instance
 
45
  if model_instance is None:
46
- model_instance = VaayuSegmentationModel(models_dir="models")
 
 
 
 
 
 
 
47
  return model_instance
48
 
 
 
 
 
 
49
  def numpy_to_base64(arr, fmt="PNG"):
 
 
 
 
 
 
 
 
50
  img = Image.fromarray(arr)
51
- buf = io.BytesIO()
52
- img.save(buf, format=fmt)
53
- return base64.b64encode(buf.getvalue()).decode("utf-8")
54
 
55
- # --- REST API Endpoints for Vercel Frontend ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
  @app.get("/api/health")
58
  def health():
59
- return {"status": "ok", "message": "Vaayu API on Hugging Face is running"}
 
 
 
 
 
 
 
 
 
60
 
61
  @app.get("/api/dataset-stats")
62
  def dataset_stats():
 
 
 
 
 
63
  stats = {
64
  "village": "Lalpur, Gujarat",
65
  "ecw_file": "ortho_lalpur(511638)_3857.ecw",
66
  "ecw_size_mb": 21.9,
67
  "resolution": "50 cm per pixel",
 
68
  "total_buildings": 550,
69
  "total_roads": 49,
 
70
  "roof_types": {
71
  "RCC / Concrete": 312,
72
  "Tiled / Terracotta": 98,
73
  "Tin / Metal Sheet": 85,
74
  "Others": 55
75
  },
 
76
  "avg_structure_area_m2": 87.3,
77
  }
78
-
 
 
 
 
79
  try:
 
80
  import shapefile
81
- bldg_path = Path("sample_data/ShapeFiles/LalPur/Gujarat_Build_Up_Area_Type.shp")
82
- road_path = Path("sample_data/ShapeFiles/LalPur/Gujarat_Road.shp")
83
-
 
 
 
 
 
 
84
  if bldg_path.exists():
85
- sf_b = shapefile.Reader(str(bldg_path))
86
- sf_r = shapefile.Reader(str(road_path))
 
 
 
 
 
 
 
 
 
 
87
  recs = sf_b.records()
88
- roof_counts = {"RCC / Concrete": 0, "Tiled / Terracotta": 0, "Tin / Metal Sheet": 0, "Others": 0}
 
 
 
 
 
 
 
89
  areas = []
 
90
  table_data = []
91
-
92
- for idx, r in enumerate(recs):
93
- d = r.as_dict()
94
- rt = d.get("roof_type", 1)
95
- area = float(d.get("area_sqm", 0.0) or d.get("area_geo", 0.0) or 0.0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  areas.append(area)
97
-
 
 
 
 
98
  if rt == 1:
99
- roof_counts["RCC / Concrete"] += 1
 
 
 
 
 
 
100
  elif rt == 2:
101
- roof_counts["Tiled / Terracotta"] += 1
 
 
 
 
 
 
102
  elif rt == 3:
103
- roof_counts["Tin / Metal Sheet"] += 1
 
 
 
 
 
 
104
  else:
105
- roof_counts["Others"] += 1
106
-
 
 
 
 
 
 
 
 
 
107
  if idx < 20:
108
- table_data.append({
109
- "objectId": d.get("objectid", idx),
110
- "roofType": "RCC / Concrete" if rt == 1 else "Tiled / Terracotta" if rt == 2 else "Tin / Metal Sheet" if rt == 3 else "Others",
111
- "areaSqm": round(area, 2),
112
- "propertyId": d.get("property_i") or d.get("uniq_id") or f"LAL-PROP-{d.get('objectid', idx)}",
113
- "village": d.get("village_na") or "Lalpur"
114
- })
115
-
116
- stats["total_buildings"] = len(recs)
117
- stats["total_roads"] = len(sf_r)
118
- stats["roof_types"] = roof_counts
119
- stats["avg_structure_area_m2"] = round(float(np.mean(areas)), 1) if areas else 0
120
- stats["sample_records"] = table_data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  except Exception as e:
122
- stats["shapefile_note"] = f"Fallback stats: {e}"
123
-
124
- return JSONResponse(content=stats)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
  @app.post("/api/predict")
127
  async def predict_api(
@@ -130,97 +407,499 @@ async def predict_api(
130
  sample_tile: str = Form(None),
131
  image: UploadFile = File(None)
132
  ):
133
- m = get_model()
134
-
135
- if sample_tile:
136
- image_path = str(Path("sample_data/tiles") / sample_tile)
137
- if not Path(image_path).exists():
138
- return JSONResponse(status_code=404, content={"error": f"Tile not found: {sample_tile}"})
139
- elif image:
140
- temp_path = Path("temp_hf_upload.png")
141
- contents = await image.read()
142
- with open(temp_path, "wb") as f:
143
- f.write(contents)
144
- image_path = str(temp_path)
145
- else:
146
- return JSONResponse(status_code=400, content={"error": "No image or sample_tile provided"})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
  try:
149
- res = m.predict(image_path, feature=feature, threshold=threshold)
150
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  response_data = {
152
- "feature": res["feature"],
153
- "detected_ratio": float(res["detected_ratio"]),
154
- "detected_pixels": int(res["detected_pixels"]),
155
- "total_pixels": int(res["total_pixels"]),
156
- "original_image": numpy_to_base64(res["original_image"]),
157
- "mask": numpy_to_base64(res["mask"]),
158
- "overlay": numpy_to_base64(res["overlay"]),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  }
160
-
161
- if res.get("rooftop_analysis"):
162
- ra = res["rooftop_analysis"]
163
- response_data["rooftop_analysis"] = {
164
- "rooftop_counts": ra["rooftop_counts"],
165
- "total_buildings": ra["total_buildings"],
166
- "total_builtup_m2": ra["total_builtup_m2"],
167
- "rcc_area_m2": ra["rcc_area_m2"],
168
- "annual_solar_kwh": ra["annual_solar_kwh"],
169
- "annual_property_tax_inr": ra["annual_property_tax_inr"],
170
- "classification_map": numpy_to_base64(ra["classification_map"]),
171
- "building_details": ra["building_details"][:50],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  }
173
-
174
- return JSONResponse(content=response_data)
 
 
 
175
  except Exception as e:
176
- return JSONResponse(status_code=500, content={"error": str(e)})
177
 
178
- # --- Gradio UI Component ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
 
180
- def _raw_gradio_predict(input_image, feature_type, confidence_threshold):
181
  if input_image is None:
182
- return None, None, "Please upload an orthophoto image."
183
-
184
- # Save temp image for inference
185
- temp_p = "temp_gradio_input.png"
186
- Image.fromarray(input_image).save(temp_p)
187
-
188
- feature_key = "buildings" if "Building" in feature_type else "roads" if "Road" in feature_type else "water_bodies"
189
-
190
- m = get_model()
191
- res = m.predict(temp_p, feature=feature_key, threshold=confidence_threshold)
192
-
193
- summary = f"Detected Pixels: {res['detected_pixels']:,} ({res['detected_ratio']*100:.1f}% coverage)\n"
194
- if res.get("rooftop_analysis"):
195
- ra = res["rooftop_analysis"]
196
- summary += f"Total Identified Parcels: {ra['total_buildings']}\n"
197
- summary += f"Total Built-up Area: {ra['total_builtup_m2']:,.1f} m²\n"
198
- summary += f"Est. Solar Potential: {ra['annual_solar_kwh']:,.0f} kWh/yr\n"
199
- summary += f"Est. Property Tax: ₹{ra['annual_property_tax_inr']:,.0f}/yr\n"
200
-
201
- return res["overlay"], res["mask"], summary
202
-
203
- if has_spaces:
204
- gradio_predict = spaces.GPU(_raw_gradio_predict)
205
- else:
206
- gradio_predict = _raw_gradio_predict
207
-
208
- # Build Gradio Interface
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  demo = gr.Interface(
 
210
  fn=gradio_predict,
 
211
  inputs=[
212
- gr.Image(label="Upload Orthophoto Tile (PNG / JPG / GeoTIFF)"),
213
- gr.Radio(["Building Footprints & Roofs", "Road Infrastructure", "Waterbodies & Tanks"], label="Geospatial Feature", value="Building Footprints & Roofs"),
214
- gr.Slider(minimum=0.1, maximum=0.9, value=0.5, step=0.05, label="Confidence Threshold"),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  ],
 
216
  outputs=[
217
- gr.Image(label="AI Demarcation Overlay"),
218
- gr.Image(label="Binary Segmentation Mask"),
219
- gr.Textbox(label="Analytics & Governance Telemetry"),
 
 
 
 
 
 
 
 
 
220
  ],
221
- title="Project Vaayu — Geospatial AI Intelligence",
222
- description="Deep neural feature extraction from 50cm drone orthophotos using UNet++ ResNet-34.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  )
224
 
225
- # Mount Gradio UI inside FastAPI App
226
- app = gr.mount_gradio_app(app, demo, path="/")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
  Hugging Face Space Entry Point for Project Vaayu
3
+
4
+ Serves:
5
+ 1. Gradio interactive interface
6
+ 2. FastAPI REST API endpoints for the Vercel frontend
7
+ 3. ZeroGPU-compatible model inference
8
  """
9
 
10
+ # ============================================================
11
+ # IMPORTANT:
12
+ # Import `spaces` before other ML/GPU-related imports.
13
+ # ============================================================
14
+
15
+ import spaces
16
+
17
  import os
18
  import io
19
  import base64
20
  import json
 
21
  from pathlib import Path
22
+
23
+ import numpy as np
24
  from PIL import Image
25
+
26
  import gradio as gr
27
+
28
  from fastapi import FastAPI, File, UploadFile, Form
29
  from fastapi.middleware.cors import CORSMiddleware
30
+ from fastapi.responses import JSONResponse
 
31
 
32
+ # ============================================================
33
+ # Existing Vaayu inference engine
34
+ # ============================================================
 
 
35
 
 
36
  from inference import VaayuSegmentationModel
37
 
 
 
38
 
39
+ # ============================================================
40
+ # FASTAPI APPLICATION
41
+ # ============================================================
42
+
43
+ app = FastAPI(
44
+ title="Vaayu Geospatial AI API",
45
+ description="AI-powered geospatial feature extraction using UNet++",
46
+ version="1.0.0",
47
+ )
48
+
49
+
50
+ # ============================================================
51
+ # CORS
52
+ # ============================================================
53
+
54
  app.add_middleware(
55
  CORSMiddleware,
56
  allow_origins=["*"],
 
59
  allow_headers=["*"],
60
  )
61
 
62
+
63
+ # ============================================================
64
+ # MODEL
65
+ # ============================================================
66
+
67
  model_instance = None
68
 
69
+
70
  def get_model():
71
+ """
72
+ Lazily initialize the Vaayu segmentation model.
73
+
74
+ The model is initialized only when inference is actually requested.
75
+ """
76
  global model_instance
77
+
78
  if model_instance is None:
79
+ print("Loading Vaayu segmentation model...")
80
+
81
+ model_instance = VaayuSegmentationModel(
82
+ models_dir="models"
83
+ )
84
+
85
+ print("Vaayu segmentation model loaded successfully.")
86
+
87
  return model_instance
88
 
89
+
90
+ # ============================================================
91
+ # UTILITY FUNCTIONS
92
+ # ============================================================
93
+
94
  def numpy_to_base64(arr, fmt="PNG"):
95
+ """
96
+ Convert a NumPy image array to a base64 encoded image.
97
+ """
98
+
99
+ # Make sure array is uint8
100
+ if arr.dtype != np.uint8:
101
+ arr = np.clip(arr, 0, 255).astype(np.uint8)
102
+
103
  img = Image.fromarray(arr)
 
 
 
104
 
105
+ buffer = io.BytesIO()
106
+
107
+ img.save(
108
+ buffer,
109
+ format=fmt
110
+ )
111
+
112
+ return base64.b64encode(
113
+ buffer.getvalue()
114
+ ).decode("utf-8")
115
+
116
+
117
+ # ============================================================
118
+ # HEALTH CHECK
119
+ # ============================================================
120
 
121
  @app.get("/api/health")
122
  def health():
123
+
124
+ return {
125
+ "status": "ok",
126
+ "message": "Vaayu API on Hugging Face is running"
127
+ }
128
+
129
+
130
+ # ============================================================
131
+ # DATASET STATS
132
+ # ============================================================
133
 
134
  @app.get("/api/dataset-stats")
135
  def dataset_stats():
136
+
137
+ # --------------------------------------------------------
138
+ # Default fallback statistics
139
+ # --------------------------------------------------------
140
+
141
  stats = {
142
  "village": "Lalpur, Gujarat",
143
  "ecw_file": "ortho_lalpur(511638)_3857.ecw",
144
  "ecw_size_mb": 21.9,
145
  "resolution": "50 cm per pixel",
146
+
147
  "total_buildings": 550,
148
  "total_roads": 49,
149
+
150
  "roof_types": {
151
  "RCC / Concrete": 312,
152
  "Tiled / Terracotta": 98,
153
  "Tin / Metal Sheet": 85,
154
  "Others": 55
155
  },
156
+
157
  "avg_structure_area_m2": 87.3,
158
  }
159
+
160
+ # --------------------------------------------------------
161
+ # Try reading actual shapefiles
162
+ # --------------------------------------------------------
163
+
164
  try:
165
+
166
  import shapefile
167
+
168
+ bldg_path = Path(
169
+ "sample_data/ShapeFiles/LalPur/Gujarat_Build_Up_Area_Type.shp"
170
+ )
171
+
172
+ road_path = Path(
173
+ "sample_data/ShapeFiles/LalPur/Gujarat_Road.shp"
174
+ )
175
+
176
  if bldg_path.exists():
177
+
178
+ sf_b = shapefile.Reader(
179
+ str(bldg_path)
180
+ )
181
+
182
+ if road_path.exists():
183
+ sf_r = shapefile.Reader(
184
+ str(road_path)
185
+ )
186
+ else:
187
+ sf_r = None
188
+
189
  recs = sf_b.records()
190
+
191
+ roof_counts = {
192
+ "RCC / Concrete": 0,
193
+ "Tiled / Terracotta": 0,
194
+ "Tin / Metal Sheet": 0,
195
+ "Others": 0
196
+ }
197
+
198
  areas = []
199
+
200
  table_data = []
201
+
202
+ # ------------------------------------------------
203
+ # Process building records
204
+ # ------------------------------------------------
205
+
206
+ for idx, record in enumerate(recs):
207
+
208
+ try:
209
+ d = record.as_dict()
210
+ except Exception:
211
+ d = {}
212
+
213
+ rt = d.get(
214
+ "roof_type",
215
+ 1
216
+ )
217
+
218
+ area = float(
219
+ d.get(
220
+ "area_sqm",
221
+ 0.0
222
+ )
223
+ or
224
+ d.get(
225
+ "area_geo",
226
+ 0.0
227
+ )
228
+ or 0.0
229
+ )
230
+
231
  areas.append(area)
232
+
233
+ # --------------------------------------------
234
+ # Roof classification
235
+ # --------------------------------------------
236
+
237
  if rt == 1:
238
+
239
+ roof_counts[
240
+ "RCC / Concrete"
241
+ ] += 1
242
+
243
+ roof_type_name = "RCC / Concrete"
244
+
245
  elif rt == 2:
246
+
247
+ roof_counts[
248
+ "Tiled / Terracotta"
249
+ ] += 1
250
+
251
+ roof_type_name = "Tiled / Terracotta"
252
+
253
  elif rt == 3:
254
+
255
+ roof_counts[
256
+ "Tin / Metal Sheet"
257
+ ] += 1
258
+
259
+ roof_type_name = "Tin / Metal Sheet"
260
+
261
  else:
262
+
263
+ roof_counts[
264
+ "Others"
265
+ ] += 1
266
+
267
+ roof_type_name = "Others"
268
+
269
+ # --------------------------------------------
270
+ # First 20 records for frontend table
271
+ # --------------------------------------------
272
+
273
  if idx < 20:
274
+
275
+ table_data.append(
276
+ {
277
+ "objectId": d.get(
278
+ "objectid",
279
+ idx
280
+ ),
281
+
282
+ "roofType": roof_type_name,
283
+
284
+ "areaSqm": round(
285
+ area,
286
+ 2
287
+ ),
288
+
289
+ "propertyId": (
290
+ d.get("property_i")
291
+ or
292
+ d.get("uniq_id")
293
+ or
294
+ f"LAL-PROP-{d.get('objectid', idx)}"
295
+ ),
296
+
297
+ "village": (
298
+ d.get("village_na")
299
+ or
300
+ "Lalpur"
301
+ )
302
+ }
303
+ )
304
+
305
+ # ------------------------------------------------
306
+ # Update statistics
307
+ # ------------------------------------------------
308
+
309
+ stats[
310
+ "total_buildings"
311
+ ] = len(recs)
312
+
313
+ if sf_r is not None:
314
+
315
+ stats[
316
+ "total_roads"
317
+ ] = len(sf_r)
318
+
319
+ stats[
320
+ "roof_types"
321
+ ] = roof_counts
322
+
323
+ stats[
324
+ "avg_structure_area_m2"
325
+ ] = (
326
+ round(
327
+ float(np.mean(areas)),
328
+ 1
329
+ )
330
+ if areas
331
+ else 0
332
+ )
333
+
334
+ stats[
335
+ "sample_records"
336
+ ] = table_data
337
+
338
  except Exception as e:
339
+
340
+ stats[
341
+ "shapefile_note"
342
+ ] = f"Fallback stats: {str(e)}"
343
+
344
+ return JSONResponse(
345
+ content=stats
346
+ )
347
+
348
+
349
+ # ============================================================
350
+ # GPU INFERENCE FUNCTION
351
+ # ============================================================
352
+ #
353
+ # IMPORTANT:
354
+ # This is the critical ZeroGPU change.
355
+ #
356
+ # Do NOT do:
357
+ #
358
+ # gradio_predict = spaces.GPU(function)
359
+ #
360
+ # Instead use:
361
+ #
362
+ # @spaces.GPU(...)
363
+ # def function(...):
364
+ #
365
+ # ============================================================
366
+
367
+
368
+ @spaces.GPU(duration=120)
369
+ def run_prediction(
370
+ image_path,
371
+ feature="buildings",
372
+ threshold=0.5
373
+ ):
374
+ """
375
+ GPU-enabled inference function.
376
+
377
+ This function is shared by:
378
+ - Gradio
379
+ - FastAPI REST API
380
+ """
381
+
382
+ print(
383
+ f"Running Vaayu inference: "
384
+ f"feature={feature}, "
385
+ f"threshold={threshold}"
386
+ )
387
+
388
+ model = get_model()
389
+
390
+ result = model.predict(
391
+ image_path,
392
+ feature=feature,
393
+ threshold=threshold
394
+ )
395
+
396
+ return result
397
+
398
+
399
+ # ============================================================
400
+ # REST API PREDICT
401
+ # ============================================================
402
 
403
  @app.post("/api/predict")
404
  async def predict_api(
 
407
  sample_tile: str = Form(None),
408
  image: UploadFile = File(None)
409
  ):
410
+
411
+ # --------------------------------------------------------
412
+ # Validate feature
413
+ # --------------------------------------------------------
414
+
415
+ allowed_features = {
416
+ "buildings",
417
+ "roads",
418
+ "water_bodies"
419
+ }
420
+
421
+ if feature not in allowed_features:
422
+
423
+ return JSONResponse(
424
+ status_code=400,
425
+ content={
426
+ "error": (
427
+ f"Invalid feature '{feature}'. "
428
+ f"Allowed values: "
429
+ f"{', '.join(allowed_features)}"
430
+ )
431
+ }
432
+ )
433
+
434
+ # --------------------------------------------------------
435
+ # Validate threshold
436
+ # --------------------------------------------------------
437
+
438
+ if threshold < 0.0 or threshold > 1.0:
439
+
440
+ return JSONResponse(
441
+ status_code=400,
442
+ content={
443
+ "error": "Threshold must be between 0 and 1."
444
+ }
445
+ )
446
+
447
+ # --------------------------------------------------------
448
+ # Determine image source
449
+ # --------------------------------------------------------
450
+
451
+ temp_path = None
452
 
453
  try:
454
+
455
+ if sample_tile:
456
+
457
+ image_path = (
458
+ Path("sample_data/tiles")
459
+ / sample_tile
460
+ )
461
+
462
+ if not image_path.exists():
463
+
464
+ return JSONResponse(
465
+ status_code=404,
466
+ content={
467
+ "error": (
468
+ f"Tile not found: "
469
+ f"{sample_tile}"
470
+ )
471
+ }
472
+ )
473
+
474
+ image_path = str(
475
+ image_path
476
+ )
477
+
478
+ elif image:
479
+
480
+ # ------------------------------------------------
481
+ # Save uploaded image
482
+ # ------------------------------------------------
483
+
484
+ extension = (
485
+ Path(image.filename).suffix
486
+ if image.filename
487
+ else ".png"
488
+ )
489
+
490
+ temp_path = Path(
491
+ f"temp_hf_upload{extension}"
492
+ )
493
+
494
+ contents = await image.read()
495
+
496
+ with open(
497
+ temp_path,
498
+ "wb"
499
+ ) as f:
500
+
501
+ f.write(contents)
502
+
503
+ image_path = str(
504
+ temp_path
505
+ )
506
+
507
+ else:
508
+
509
+ return JSONResponse(
510
+ status_code=400,
511
+ content={
512
+ "error": (
513
+ "No image or sample_tile provided"
514
+ )
515
+ }
516
+ )
517
+
518
+ # ----------------------------------------------------
519
+ # GPU inference
520
+ # ----------------------------------------------------
521
+
522
+ result = run_prediction(
523
+ image_path,
524
+ feature=feature,
525
+ threshold=threshold
526
+ )
527
+
528
+ # ----------------------------------------------------
529
+ # Build API response
530
+ # ----------------------------------------------------
531
+
532
  response_data = {
533
+
534
+ "feature": result["feature"],
535
+
536
+ "detected_ratio": float(
537
+ result["detected_ratio"]
538
+ ),
539
+
540
+ "detected_pixels": int(
541
+ result["detected_pixels"]
542
+ ),
543
+
544
+ "total_pixels": int(
545
+ result["total_pixels"]
546
+ ),
547
+
548
+ "original_image": numpy_to_base64(
549
+ result["original_image"]
550
+ ),
551
+
552
+ "mask": numpy_to_base64(
553
+ result["mask"]
554
+ ),
555
+
556
+ "overlay": numpy_to_base64(
557
+ result["overlay"]
558
+ )
559
  }
560
+
561
+ # ----------------------------------------------------
562
+ # Rooftop analysis
563
+ # ----------------------------------------------------
564
+
565
+ if result.get("rooftop_analysis"):
566
+
567
+ rooftop = result[
568
+ "rooftop_analysis"
569
+ ]
570
+
571
+ response_data[
572
+ "rooftop_analysis"
573
+ ] = {
574
+
575
+ "rooftop_counts": rooftop[
576
+ "rooftop_counts"
577
+ ],
578
+
579
+ "total_buildings": rooftop[
580
+ "total_buildings"
581
+ ],
582
+
583
+ "total_builtup_m2": rooftop[
584
+ "total_builtup_m2"
585
+ ],
586
+
587
+ "rcc_area_m2": rooftop[
588
+ "rcc_area_m2"
589
+ ],
590
+
591
+ "annual_solar_kwh": rooftop[
592
+ "annual_solar_kwh"
593
+ ],
594
+
595
+ "annual_property_tax_inr": rooftop[
596
+ "annual_property_tax_inr"
597
+ ],
598
+
599
+ "classification_map":
600
+ numpy_to_base64(
601
+ rooftop[
602
+ "classification_map"
603
+ ]
604
+ ),
605
+
606
+ "building_details":
607
+ rooftop[
608
+ "building_details"
609
+ ][:50]
610
  }
611
+
612
+ return JSONResponse(
613
+ content=response_data
614
+ )
615
+
616
  except Exception as e:
 
617
 
618
+ print(
619
+ f"Prediction error: {str(e)}"
620
+ )
621
+
622
+ return JSONResponse(
623
+ status_code=500,
624
+ content={
625
+ "error": str(e)
626
+ }
627
+ )
628
+
629
+ finally:
630
+
631
+ # ----------------------------------------------------
632
+ # Delete temporary uploaded image
633
+ # ----------------------------------------------------
634
+
635
+ if temp_path is not None:
636
+
637
+ try:
638
+
639
+ if temp_path.exists():
640
+ temp_path.unlink()
641
+
642
+ except Exception as cleanup_error:
643
+
644
+ print(
645
+ f"Temporary file cleanup failed: "
646
+ f"{cleanup_error}"
647
+ )
648
+
649
+
650
+ # ============================================================
651
+ # GRADIO PREDICTION
652
+ # ============================================================
653
+
654
+ def _raw_gradio_predict(
655
+ input_image,
656
+ feature_type,
657
+ confidence_threshold
658
+ ):
659
 
 
660
  if input_image is None:
661
+
662
+ return (
663
+ None,
664
+ None,
665
+ "Please upload an orthophoto image."
666
+ )
667
+
668
+ # --------------------------------------------------------
669
+ # Save uploaded Gradio image
670
+ # --------------------------------------------------------
671
+
672
+ temp_path = Path(
673
+ "temp_gradio_input.png"
674
+ )
675
+
676
+ try:
677
+
678
+ # Gradio can return either a NumPy array
679
+ # or PIL image depending on configuration.
680
+
681
+ if isinstance(
682
+ input_image,
683
+ Image.Image
684
+ ):
685
+
686
+ image = input_image
687
+
688
+ else:
689
+
690
+ image = Image.fromarray(
691
+ input_image
692
+ )
693
+
694
+ image.save(
695
+ temp_path
696
+ )
697
+
698
+ # ----------------------------------------------------
699
+ # Convert UI feature name to model feature key
700
+ # ----------------------------------------------------
701
+
702
+ if "Building" in feature_type:
703
+
704
+ feature_key = "buildings"
705
+
706
+ elif "Road" in feature_type:
707
+
708
+ feature_key = "roads"
709
+
710
+ else:
711
+
712
+ feature_key = "water_bodies"
713
+
714
+ # ----------------------------------------------------
715
+ # GPU inference
716
+ # ----------------------------------------------------
717
+
718
+ result = run_prediction(
719
+ str(temp_path),
720
+ feature=feature_key,
721
+ threshold=confidence_threshold
722
+ )
723
+
724
+ # ----------------------------------------------------
725
+ # Analytics summary
726
+ # ----------------------------------------------------
727
+
728
+ summary = (
729
+ f"Detected Pixels: "
730
+ f"{result['detected_pixels']:,} "
731
+ f"({result['detected_ratio'] * 100:.1f}% coverage)\n"
732
+ )
733
+
734
+ if result.get("rooftop_analysis"):
735
+
736
+ rooftop = result[
737
+ "rooftop_analysis"
738
+ ]
739
+
740
+ summary += (
741
+ f"Total Identified Parcels: "
742
+ f"{rooftop['total_buildings']}\n"
743
+ )
744
+
745
+ summary += (
746
+ f"Total Built-up Area: "
747
+ f"{rooftop['total_builtup_m2']:,.1f} m²\n"
748
+ )
749
+
750
+ summary += (
751
+ f"Est. Solar Potential: "
752
+ f"{rooftop['annual_solar_kwh']:,.0f} kWh/yr\n"
753
+ )
754
+
755
+ summary += (
756
+ f"Est. Property Tax: "
757
+ f"₹{rooftop['annual_property_tax_inr']:,.0f}/yr\n"
758
+ )
759
+
760
+ return (
761
+ result["overlay"],
762
+ result["mask"],
763
+ summary
764
+ )
765
+
766
+ except Exception as e:
767
+
768
+ print(
769
+ f"Gradio prediction error: {str(e)}"
770
+ )
771
+
772
+ return (
773
+ None,
774
+ None,
775
+ f"Inference error: {str(e)}"
776
+ )
777
+
778
+ finally:
779
+
780
+ try:
781
+
782
+ if temp_path.exists():
783
+ temp_path.unlink()
784
+
785
+ except Exception:
786
+ pass
787
+
788
+
789
+ # ============================================================
790
+ # ZERO GPU GRADIO FUNCTION
791
+ # ============================================================
792
+ #
793
+ # This is the important part that fixes:
794
+ #
795
+ # "No @spaces.GPU function detected during startup"
796
+ #
797
+ # ============================================================
798
+
799
+ @spaces.GPU(duration=120)
800
+ def gradio_predict(
801
+ input_image,
802
+ feature_type,
803
+ confidence_threshold
804
+ ):
805
+
806
+ return _raw_gradio_predict(
807
+ input_image,
808
+ feature_type,
809
+ confidence_threshold
810
+ )
811
+
812
+
813
+ # ============================================================
814
+ # GRADIO INTERFACE
815
+ # ============================================================
816
+
817
  demo = gr.Interface(
818
+
819
  fn=gradio_predict,
820
+
821
  inputs=[
822
+
823
+ gr.Image(
824
+ label="Upload Orthophoto Tile (PNG / JPG / GeoTIFF)",
825
+ type="numpy"
826
+ ),
827
+
828
+ gr.Radio(
829
+ [
830
+ "Building Footprints & Roofs",
831
+ "Road Infrastructure",
832
+ "Waterbodies & Tanks"
833
+ ],
834
+
835
+ label="Geospatial Feature",
836
+
837
+ value="Building Footprints & Roofs"
838
+ ),
839
+
840
+ gr.Slider(
841
+ minimum=0.1,
842
+ maximum=0.9,
843
+ value=0.5,
844
+ step=0.05,
845
+ label="Confidence Threshold"
846
+ )
847
  ],
848
+
849
  outputs=[
850
+
851
+ gr.Image(
852
+ label="AI Demarcation Overlay"
853
+ ),
854
+
855
+ gr.Image(
856
+ label="Binary Segmentation Mask"
857
+ ),
858
+
859
+ gr.Textbox(
860
+ label="Analytics & Governance Telemetry"
861
+ )
862
  ],
863
+
864
+ title=(
865
+ "Project Vaayu — "
866
+ "Geospatial AI Intelligence"
867
+ ),
868
+
869
+ description=(
870
+ "Deep neural feature extraction "
871
+ "from 50cm drone orthophotos "
872
+ "using UNet++ ResNet-34."
873
+ )
874
+ )
875
+
876
+
877
+ # ============================================================
878
+ # MOUNT GRADIO INTO FASTAPI
879
+ # ============================================================
880
+
881
+ app = gr.mount_gradio_app(
882
+ app,
883
+ demo,
884
+ path="/"
885
  )
886
 
887
+
888
+ # ============================================================
889
+ # LOCAL DEVELOPMENT
890
+ # ============================================================
891
+
892
+ if __name__ == "__main__":
893
+
894
+ import uvicorn
895
+
896
+ uvicorn.run(
897
+ app,
898
+ host="0.0.0.0",
899
+ port=int(
900
+ os.environ.get(
901
+ "PORT",
902
+ 7860
903
+ )
904
+ )
905
+ )