yakvrz commited on
Commit
ba15bb2
·
1 Parent(s): 05c6078

Use modular app entrypoint

Browse files
Files changed (1) hide show
  1. gradio_app.py +31 -1319
gradio_app.py CHANGED
@@ -1,1333 +1,45 @@
1
  #!/usr/bin/env python3
2
- """
3
- Gradio demo: depth overlays on VISLOC imagery using Depth Anything 3.
4
 
5
- Run:
6
- python gradio_app.py
7
-
8
- Then open the printed local URL. Requires: gradio, pillow, torch, transformers (for water mask).
9
- """
10
-
11
- import cv2
12
- import functools
13
- import math
14
  import os
15
- from pathlib import Path
16
-
17
- import gradio as gr
18
- import numpy as np
19
- import torch
20
- from PIL import Image, ImageDraw, ImageFilter
21
- import matplotlib.cm as cm
22
-
23
- # Prefer installed package; fall back to local src for dev runs.
24
- try:
25
- from depth_anything_3.api import DepthAnything3 # type: ignore
26
- from depth_anything_3.utils.visualize import visualize_depth # type: ignore
27
- except ModuleNotFoundError:
28
- import sys
29
-
30
- ROOT = Path(__file__).resolve().parent
31
- sys.path.append(str(ROOT / "src"))
32
- from depth_anything_3.api import DepthAnything3 # noqa: E402
33
- from depth_anything_3.utils.visualize import visualize_depth # noqa: E402
34
-
35
- VISLOC_DIR = Path("data/Image/VISLOC")
36
- HAGDAVS_DIR = Path("data/Image/HAGDAVS")
37
- VIDEO_DIR = Path("data/Video")
38
- IMAGE_EXTS = (".jpg", ".jpeg", ".png", ".JPG", ".JPEG", ".PNG")
39
- VIDEO_EXTS = {".mp4", ".avi", ".mov", ".mkv", ".flv", ".wmv", ".webm", ".m4v"}
40
- DEFAULT_ALTITUDE_M = 450.0
41
- ASSUMED_FOV_DEG = 90.0
42
- WATER_MODEL_ID = "facebook/mask2former-swin-large-ade-semantic"
43
- ROAD_MODEL_ID = "facebook/mask2former-swin-large-ade-semantic"
44
 
45
- def crop_nonblack(img: Image.Image, frac: float = 0.05) -> Image.Image:
46
- """Naively crop a fixed fraction off each border (to drop black padding)."""
47
- w, h = img.size
48
- dx = int(round(w * frac))
49
- dy = int(round(h * frac))
50
- return img.crop((dx, dy, w - dx, h - dy))
51
 
52
 
53
- class SemanticSegmenter:
54
- """Run Mask2Former once per image and extract multiple semantic masks."""
55
-
56
- def __init__(self, model_id: str):
 
 
 
 
 
 
 
57
  try:
58
- from transformers import AutoImageProcessor, Mask2FormerForUniversalSegmentation
59
- except ImportError as e:
60
- raise ImportError(
61
- "transformers is required for masking; install with `pip install transformers`"
62
- ) from e
63
- preferred_device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
 
 
 
 
64
  try:
65
- processor = AutoImageProcessor.from_pretrained(model_id, use_fast=True)
66
  except TypeError:
67
- processor = AutoImageProcessor.from_pretrained(model_id)
68
- model = Mask2FormerForUniversalSegmentation.from_pretrained(model_id)
69
- try:
70
- model = model.to(preferred_device)
71
- except RuntimeError:
72
- preferred_device = torch.device("cpu")
73
- model = model.to(preferred_device)
74
- model.eval()
75
- self.processor = processor
76
- self.model = model
77
- self.device = preferred_device
78
- labels = model.config.id2label
79
- self.water_ids = {
80
- i for i, name in labels.items() if any(k in name.lower() for k in ["water", "sea", "lake", "river", "ocean", "pond"])
81
- }
82
- road_include = ["highway", "road", "street", "runway"]
83
- road_block = {"field", "park", "grass", "lawn", "garden", "court", "yard", "green"}
84
- self.road_ids = {
85
- i for i, name in labels.items() if any(k in name.lower() for k in road_include) and not any(b in name.lower() for b in road_block)
86
- }
87
-
88
- def segment(self, img: Image.Image, max_side: int) -> dict[str, np.ndarray]:
89
- img_proc = img
90
- if max(img.size) > max_side:
91
- scale = max_side / max(img.size)
92
- new_size = (int(round(img.size[0] * scale)), int(round(img.size[1] * scale)))
93
- img_proc = img.resize(new_size, resample=Image.BILINEAR)
94
  try:
95
- inputs = self.processor(images=img_proc, return_tensors="pt", use_fast=True).to(self.device)
96
  except TypeError:
97
- inputs = self.processor(images=img_proc, return_tensors="pt").to(self.device)
98
- with torch.inference_mode():
99
- outputs = self.model(**inputs)
100
- seg = self.processor.post_process_semantic_segmentation(outputs, target_sizes=[img_proc.size[::-1]])[0]
101
- if torch.is_tensor(seg):
102
- seg = seg.cpu()
103
- seg_np = np.array(seg)
104
- masks = {}
105
- for name, ids in (("water", self.water_ids), ("road", self.road_ids)):
106
- mask_small = np.isin(seg_np, list(ids)).astype(np.uint8) * 255
107
- mask_img = Image.fromarray(mask_small).resize(img.size, resample=Image.NEAREST)
108
- masks[name] = np.array(mask_img) > 0
109
- return masks
110
-
111
-
112
- @functools.lru_cache(maxsize=2)
113
- def get_segmenter(model_id: str) -> SemanticSegmenter:
114
- return SemanticSegmenter(model_id)
115
-
116
-
117
- def compute_roof_mask_depth(depth: np.ndarray, aggressiveness: float = 1.3, morph_kernel: int = 5) -> np.ndarray:
118
- """Depth-based roof/structure mask: flag pixels significantly closer than the median (raised surfaces)."""
119
- d = depth.astype(np.float32)
120
- med = np.median(d)
121
- mad = np.median(np.abs(d - med)) + 1e-6
122
- threshold = med - aggressiveness * mad
123
- mask = d < threshold
124
- mask = mask.astype(np.uint8)
125
- k = max(1, int(morph_kernel))
126
- if k % 2 == 0:
127
- k += 1
128
- kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (k, k))
129
- try:
130
- mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
131
- mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
132
- except Exception:
133
- pass
134
- return mask > 0
135
-
136
-
137
- def fit_plane_ransac(points: np.ndarray, values: np.ndarray, iterations: int = 200, threshold: float = 0.01):
138
- best_coef = None
139
- best_inliers = -1
140
- n_samples = points.shape[0]
141
- if n_samples < 3:
142
- return None
143
- for _ in range(iterations):
144
- idx = np.random.choice(n_samples, 3, replace=False)
145
- A = np.concatenate([points[idx], np.ones((3, 1))], axis=1)
146
- try:
147
- coef = np.linalg.lstsq(A, values[idx], rcond=None)[0]
148
- except np.linalg.LinAlgError:
149
- continue
150
- residuals = np.abs(points[:, 0] * coef[0] + points[:, 1] * coef[1] + coef[2] - values.flatten())
151
- inliers = np.sum(residuals < threshold)
152
- if inliers > best_inliers:
153
- best_inliers = inliers
154
- best_coef = coef
155
- return best_coef
156
-
157
-
158
- def remove_global_plane(depth: np.ndarray) -> np.ndarray:
159
- """Remove global plane using RANSAC to ignore tall structures."""
160
- if depth.ndim != 2:
161
- return depth
162
- h, w = depth.shape
163
- yy, xx = np.mgrid[0:h, 0:w].astype(np.float32)
164
- points = np.stack((xx.flatten(), yy.flatten()), axis=1)
165
- values = depth.astype(np.float32).reshape(-1, 1)
166
- coef = fit_plane_ransac(points, values, iterations=300, threshold=0.01 * depth.ptp())
167
- if coef is None:
168
- return depth
169
- plane = (points @ coef[:2] + coef[2]).reshape(h, w)
170
- return depth - plane
171
-
172
-
173
- def pick_flat_patch(
174
- depth: np.ndarray,
175
- patch: int = 96,
176
- std_thresh: float = 0.03,
177
- grad_thresh: float = 0.35,
178
- water_mask: np.ndarray | None = None,
179
- ):
180
- """Find a low-variance depth window as a proxy for flat landing area."""
181
- depth = depth.astype(np.float32)
182
- if depth.ndim != 2:
183
- raise ValueError("Depth map must be 2D (H, W)")
184
-
185
- patch = max(3, min(patch, min(depth.shape)))
186
- if patch % 2 == 0:
187
- patch += 1 # keeps pooling output same size
188
- depth_norm = (depth - depth.min()) / (depth.ptp() + 1e-6)
189
-
190
- # Efficient box std via torch avg pooling
191
- import torch.nn.functional as F
192
-
193
- def box_mean(arr, k):
194
- pad = k // 2
195
- t = torch.from_numpy(arr).unsqueeze(0).unsqueeze(0)
196
- # Reflective padding avoids dark/bright rims in the std map
197
- t = F.pad(t, (pad, pad, pad, pad), mode="reflect")
198
- mean = F.avg_pool2d(t, kernel_size=k, stride=1, padding=0, count_include_pad=False)
199
- return mean.squeeze(0).squeeze(0).numpy()
200
-
201
- mean = box_mean(depth_norm, patch)
202
- mean_sq = box_mean(depth_norm * depth_norm, patch)
203
- var = np.maximum(mean_sq - mean * mean, 0.0)
204
- std_map = np.sqrt(var)
205
-
206
- # Gradient mask to down-weight slopes/edges
207
- dy, dx = np.gradient(depth_norm)
208
- grad = np.sqrt(dx * dx + dy * dy)
209
- grad_ref = np.percentile(grad, 95) + 1e-6
210
- grad_norm = np.clip(grad / grad_ref, 0.0, 1.0)
211
- grad_mask = grad_norm < grad_thresh
212
-
213
- landing_mask = grad_mask
214
- if water_mask is not None and water_mask.shape == grad_mask.shape:
215
- landing_mask = landing_mask & (~water_mask)
216
-
217
- masked_std = np.where(landing_mask, std_map, np.inf)
218
- if not np.isfinite(masked_std).any():
219
- masked_std = std_map # fallback: just take the flattest spot
220
- y, x = np.unravel_index(np.argmin(masked_std), masked_std.shape)
221
- half = patch // 2
222
- y0, y1 = max(y - half, 0), min(y + half, depth.shape[0] - 1)
223
- x0, x1 = max(x - half, 0), min(x + half, depth.shape[1] - 1)
224
- return (x0, y0, x1, y1), std_map, grad_norm, grad_mask, landing_mask
225
-
226
-
227
- def make_safety_heatmap(
228
- rgb: Image.Image,
229
- safe_mask: np.ndarray,
230
- risk_map: np.ndarray,
231
- risk_threshold: float = 0.35,
232
- ):
233
- """Produce overlay: green for safe, transparent background, red only on high-risk areas."""
234
- safe = np.clip(safe_mask.astype(np.float32), 0.0, 1.0)
235
- risk = np.clip(risk_map.astype(np.float32), 0.0, 1.0)
236
- risk = risk * (safe <= 0.0)
237
-
238
- h, w = safe.shape
239
- overlay = np.zeros((h, w, 4), dtype=np.uint8)
240
-
241
- # Green safe mask with full alpha
242
- safe_pixels = safe > 0.0
243
- overlay[safe_pixels, 1] = 255
244
- overlay[safe_pixels, 3] = 255
245
-
246
- # Red highlights only for high risk (above threshold)
247
- risk_pixels = risk > risk_threshold
248
- overlay[risk_pixels, 0] = 255
249
- overlay[risk_pixels, 1] = 0
250
- overlay[risk_pixels, 2] = 0
251
- overlay[risk_pixels, 3] = (np.clip(risk[risk_pixels], 0.0, 1.0) * 255).astype(np.uint8)
252
-
253
- heat_img = Image.fromarray(overlay, mode="RGBA").resize(rgb.size, resample=Image.NEAREST)
254
- score_gray = Image.fromarray((safe * 255).astype(np.uint8)).resize(rgb.size, resample=Image.NEAREST)
255
- return heat_img, score_gray
256
-
257
-
258
- @functools.lru_cache(maxsize=1)
259
- def get_model(model_id: str = "depth-anything/DA3METRIC-LARGE"):
260
- """Load model once and cache."""
261
- device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
262
- model = DepthAnything3.from_pretrained(model_id).to(device)
263
- model.eval()
264
- return model, device
265
-
266
-
267
- @functools.lru_cache(maxsize=1)
268
- def list_visloc_images() -> list[Path]:
269
- """Return sorted VISLOC image paths from data/Image/VISLOC."""
270
- if not VISLOC_DIR.exists():
271
- return []
272
- files = [p for p in VISLOC_DIR.iterdir() if p.suffix in IMAGE_EXTS]
273
- return sorted(files)
274
-
275
-
276
- @functools.lru_cache(maxsize=1)
277
- def list_hagdavs_images() -> list[Path]:
278
- """Return sorted HAGDAVS image paths from data/Image/HAGDAVS."""
279
- if not HAGDAVS_DIR.exists():
280
- return []
281
- files = [p for p in HAGDAVS_DIR.iterdir() if p.suffix in IMAGE_EXTS]
282
- return sorted(files)
283
-
284
-
285
- @functools.lru_cache(maxsize=1)
286
- def list_videos() -> list[Path]:
287
- if not VIDEO_DIR.exists():
288
- return []
289
- files = [p for p in VIDEO_DIR.iterdir() if p.suffix.lower() in VIDEO_EXTS]
290
- return sorted(files)
291
-
292
-
293
- @functools.lru_cache(maxsize=1)
294
- def list_all_data_inputs() -> list[str]:
295
- """Collect VISLOC image files for selection."""
296
- return [str(p) for p in list_visloc_images()]
297
-
298
-
299
- # Simple cache for segmentation outputs keyed by (model_id, path, max_side)
300
- SEGMENTATION_CACHE: dict[tuple[str, str, int], dict[str, np.ndarray]] = {}
301
-
302
-
303
- def run_on_image(
304
- image: Image.Image,
305
- footprint_m: float,
306
- std_thresh: float,
307
- grad_thresh: float,
308
- use_water_mask: bool,
309
- use_road_mask: bool,
310
- use_roof_mask: bool,
311
- altitude_m: float,
312
- fov_deg: float,
313
- flatness_detail: float,
314
- clearance_factor: float,
315
- process_res_cap: int,
316
- roof_aggressiveness: float,
317
- roof_morph_frac: float,
318
- depth_smoothing_base: float,
319
- coverage_strictness: float,
320
- model_id: str,
321
- source_path: str | None = None,
322
- ) -> dict:
323
- rgb_np = np.array(image)
324
-
325
- model, device = get_model(model_id)
326
- # Fixed upper-bound resolution (cap) while avoiding upscaling small images.
327
- process_res = min(max(image.size), int(process_res_cap))
328
- with torch.inference_mode():
329
- pred = model.inference(
330
- image=[rgb_np],
331
- process_res=process_res,
332
- process_res_method="upper_bound_resize",
333
- export_dir=None,
334
- )
335
- depth_raw = np.array(pred.depth[0])
336
- depth = remove_global_plane(depth_raw)
337
- # Smooth depth for resolution-invariant flatness/gradient (higher res -> slightly more smoothing)
338
- res_scale = max(0.5, min(2.5, process_res / 1024))
339
- sigma = max(0.0, depth_smoothing_base) * res_scale
340
- k = max(3, int(round(sigma * 3)) * 2 + 1)
341
- try:
342
- depth = cv2.GaussianBlur(depth, (k, k), sigmaX=sigma, sigmaY=sigma)
343
- except Exception:
344
- pass
345
- # Convert landing footprint (meters) to pixels at current processed resolution
346
- fov = max(10.0, min(170.0, float(fov_deg)))
347
- altitude = max(1.0, float(altitude_m))
348
- fx = (depth.shape[1] / 2.0) / math.tan(math.radians(fov) / 2.0)
349
- patch_px = footprint_m * fx / altitude
350
- patch_px = max(3, min(int(round(patch_px)), min(depth.shape) - 1))
351
- if patch_px % 2 == 0:
352
- patch_px += 1 # keep pooling symmetric
353
-
354
- # For visualization, compute a flatness map with a smaller, sharper window (decoupled from footprint)
355
- depth_norm = (depth - depth.min()) / (depth.ptp() + 1e-6)
356
- vis_patch = max(
357
- 5,
358
- min(
359
- int(max(1.0, flatness_detail) * patch_px),
360
- min(depth.shape) // 10,
361
- min(depth.shape) - 1,
362
- ),
363
- )
364
- if vis_patch % 2 == 0:
365
- vis_patch += 1
366
- import torch.nn.functional as F
367
-
368
- def box_mean_np(arr: np.ndarray, k: int):
369
- pad = k // 2
370
- t = torch.from_numpy(arr).unsqueeze(0).unsqueeze(0)
371
- t = F.pad(t, (pad, pad, pad, pad), mode="reflect")
372
- mean = F.avg_pool2d(t, kernel_size=k, stride=1, padding=0, count_include_pad=False)
373
- return mean.squeeze(0).squeeze(0).numpy()
374
-
375
- std_map_vis = np.sqrt(
376
- np.maximum(box_mean_np(depth_norm * depth_norm, vis_patch) - box_mean_np(depth_norm, vis_patch) ** 2, 0.0)
377
- )
378
-
379
- # Optional water mask (resized to depth resolution)
380
- SEG_MAX = 640
381
- water_mask_resized = None
382
- road_mask_resized = None
383
- if use_water_mask or use_road_mask:
384
- cache_key = (WATER_MODEL_ID, source_path or "", SEG_MAX)
385
- seg_masks = SEGMENTATION_CACHE.get(cache_key)
386
- if seg_masks is None:
387
- segmenter = get_segmenter(WATER_MODEL_ID)
388
- try:
389
- seg_masks = segmenter.segment(image, SEG_MAX)
390
- except RuntimeError as e:
391
- print(f"[WARN] Segmentation failed; skipping water/road masks: {e}")
392
- seg_masks = {}
393
- if source_path is not None and seg_masks:
394
- SEGMENTATION_CACHE[cache_key] = seg_masks
395
- if use_water_mask and seg_masks.get("water") is not None:
396
- water_mask_resized = Image.fromarray(seg_masks["water"].astype(np.uint8) * 255).resize(
397
- (depth.shape[1], depth.shape[0]), resample=Image.NEAREST
398
- )
399
- water_mask_resized = np.array(water_mask_resized) > 0
400
- if use_road_mask and seg_masks.get("road") is not None:
401
- road_mask_resized = Image.fromarray(seg_masks["road"].astype(np.uint8) * 255).resize(
402
- (depth.shape[1], depth.shape[0]), resample=Image.NEAREST
403
- )
404
- road_mask_resized = np.array(road_mask_resized) > 0
405
- roof_mask_resized = None
406
- if use_roof_mask:
407
- # Depth-based elevation mask: closer-than-median surfaces are treated as roofs/structures.
408
- aggressiveness = max(0.5, min(3.0, roof_aggressiveness))
409
- morph_k = max(3, int(round(patch_px * roof_morph_frac)))
410
- roof_mask_resized = compute_roof_mask_depth(depth, aggressiveness=aggressiveness, morph_kernel=morph_k)
411
-
412
- box, std_map, grad_norm, grad_mask, landing_mask = pick_flat_patch(
413
- depth,
414
- patch=patch_px,
415
- std_thresh=std_thresh,
416
- grad_thresh=grad_thresh,
417
- water_mask=water_mask_resized,
418
- )
419
- if road_mask_resized is not None:
420
- landing_mask = landing_mask & (~road_mask_resized)
421
- if roof_mask_resized is not None:
422
- landing_mask = landing_mask & (~roof_mask_resized)
423
- safe_mask = (std_map < std_thresh) & (grad_norm < grad_thresh) & landing_mask
424
- # Clearance: dilate hazards to enforce buffer around unsafe regions
425
- try:
426
- clearance_px = max(1, int(round(clearance_factor * patch_px)))
427
- if clearance_px % 2 == 0:
428
- clearance_px += 1
429
- kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (clearance_px, clearance_px))
430
- hazard = (~safe_mask).astype(np.uint8)
431
- buffered = cv2.dilate(hazard, kernel, iterations=1).astype(bool)
432
- safe_mask = safe_mask & (~buffered)
433
- except Exception:
434
- pass
435
- # Strict footprint coverage: a center is safe only if the full footprint is safe
436
- try:
437
- coverage = cv2.boxFilter(
438
- safe_mask.astype(np.float32),
439
- ddepth=-1,
440
- ksize=(patch_px, patch_px),
441
- normalize=True,
442
- anchor=(patch_px // 2, patch_px // 2),
443
- )
444
- safe_mask = coverage >= max(0.0, min(1.0, coverage_strictness))
445
- except Exception:
446
- pass
447
-
448
- # Drop tiny components: require at least one footprint area
449
- area_thresh = max(1, int(patch_px * patch_px))
450
- num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(safe_mask.astype(np.uint8), connectivity=8)
451
- if num_labels > 1:
452
- keep = np.zeros_like(labels, dtype=bool)
453
- for i in range(1, num_labels):
454
- if stats[i, cv2.CC_STAT_AREA] >= area_thresh:
455
- keep |= labels == i
456
- safe_mask = keep
457
-
458
- # Build risk map from std/grad overruns (used for red highlights)
459
- risk_std = np.clip((std_map - std_thresh) / (std_thresh + 1e-6), 0.0, 1.0)
460
- risk_grad = np.clip((grad_norm - grad_thresh) / (grad_thresh + 1e-6), 0.0, 1.0)
461
- risk_map = np.maximum(risk_std, risk_grad) * (~safe_mask)
462
-
463
- # Recommended landing spot overlay (scaled to input image size)
464
- # Prefer centers where the full footprint is safe; fall back to best flat spot
465
- safe_fit = safe_mask.astype(np.float32)
466
- try:
467
- coverage = cv2.boxFilter(
468
- safe_fit.astype(np.float32),
469
- ddepth=-1,
470
- ksize=(patch_px, patch_px),
471
- normalize=True,
472
- anchor=(patch_px // 2, patch_px // 2),
473
- )
474
- valid_centers = coverage >= 1.0
475
- except Exception:
476
- valid_centers = safe_fit > 0.5
477
-
478
- if valid_centers.any():
479
- cc_mask = valid_centers.astype(np.uint8)
480
- num_c, labels_c, stats_c, _ = cv2.connectedComponentsWithStats(cc_mask, connectivity=8)
481
- target_mask = valid_centers
482
- if num_c > 1:
483
- # Pick largest safe component by area (skip background)
484
- areas = stats_c[1:, cv2.CC_STAT_AREA]
485
- largest_idx = 1 + int(np.argmax(areas))
486
- target_mask = labels_c == largest_idx
487
- cand = np.where(target_mask)
488
- std_cand = std_map[cand]
489
- idx = np.argmin(std_cand)
490
- cy, cx = cand[0][idx], cand[1][idx]
491
- else:
492
- y0, x0, y1, x1 = box[1], box[0], box[3], box[2]
493
- cy, cx = (y0 + y1) // 2, (x0 + x1) // 2
494
-
495
- half = patch_px // 2
496
- x0 = max(int(cx - half), 0)
497
- x1 = min(int(cx + half), depth.shape[1] - 1)
498
- y0 = max(int(cy - half), 0)
499
- y1 = min(int(cy + half), depth.shape[0] - 1)
500
-
501
- scale_x = image.width / depth.shape[1]
502
- scale_y = image.height / depth.shape[0]
503
- # Draw a box whose side length matches the footprint in input-image pixels
504
- side_img = max(3, int(round(patch_px * scale_x)))
505
- cx_img = int(round(cx * scale_x))
506
- cy_img = int(round(cy * scale_y))
507
- half_img = side_img // 2
508
- bx0 = max(cx_img - half_img, 0)
509
- bx1 = min(cx_img + half_img, image.width - 1)
510
- by0 = max(cy_img - half_img, 0)
511
- by1 = min(cy_img + half_img, image.height - 1)
512
- spot_overlay = Image.new("RGBA", image.size, (0, 0, 0, 0))
513
- draw = ImageDraw.Draw(spot_overlay)
514
- draw.rectangle((bx0, by0, bx1, by1), outline=(0, 255, 0, 255), width=4)
515
- cx, cy = (bx0 + bx1) // 2, (by0 + by1) // 2
516
- draw.ellipse((cx - 5, cy - 5, cx + 5, cy + 5), fill=(0, 255, 0, 255))
517
-
518
- depth_vis = Image.fromarray(visualize_depth(depth_raw, cmap="Spectral")).resize(
519
- image.size, resample=Image.BILINEAR
520
- )
521
- flatness_img = Image.fromarray((std_map_vis / (std_map_vis.max() + 1e-6) * 255).astype(np.uint8)).resize(
522
- image.size, resample=Image.NEAREST
523
- )
524
- grad_img = Image.fromarray((grad_norm * 255).astype(np.uint8)).resize(
525
- image.size, resample=Image.BILINEAR
526
- )
527
- grad_mask_img = Image.fromarray(((grad_norm < grad_thresh).astype(np.uint8) * 255)).resize(
528
- image.size, resample=Image.NEAREST
529
- )
530
- water_mask_view = None
531
- if use_water_mask and water_mask_resized is not None:
532
- water_mask_view = Image.fromarray((water_mask_resized.astype(np.uint8) * 255)).resize(
533
- image.size, resample=Image.NEAREST
534
- )
535
- road_mask_view = None
536
- if use_road_mask and road_mask_resized is not None:
537
- road_mask_view = Image.fromarray((road_mask_resized.astype(np.uint8) * 255)).resize(
538
- image.size, resample=Image.NEAREST
539
- )
540
- roof_mask_view = None
541
- if use_roof_mask and roof_mask_resized is not None:
542
- roof_mask_view = Image.fromarray((roof_mask_resized.astype(np.uint8) * 255))
543
- roof_mask_view = roof_mask_view.resize(image.size, resample=Image.NEAREST)
544
-
545
- heat_overlay, heat_gray = make_safety_heatmap(image, safe_mask, risk_map)
546
-
547
- images = {
548
- "RGB": image,
549
- "Depth": depth_vis,
550
- "Flatness map (std)": flatness_img,
551
- "Depth gradient": grad_img,
552
- "Gradient mask": grad_mask_img,
553
- "Water mask": water_mask_view if water_mask_view is not None else Image.new("L", image.size, 0),
554
- "Road mask": road_mask_view if road_mask_view is not None else Image.new("L", image.size, 0),
555
- "Roof mask": roof_mask_view if roof_mask_view is not None else Image.new("L", image.size, 0),
556
- "Safety heatmap overlay": heat_overlay,
557
- "Safety score": heat_gray,
558
- "Landing spot overlay": spot_overlay,
559
- }
560
- return images
561
-
562
-
563
- def process_image(
564
- input_path: str,
565
- footprint_m: float,
566
- std_thresh: float,
567
- grad_thresh: float,
568
- use_water_mask: bool,
569
- use_road_mask: bool,
570
- use_roof_mask: bool,
571
- altitude_m: float,
572
- fov_deg: float,
573
- flatness_detail: float,
574
- clearance_factor: float,
575
- process_res_cap: int,
576
- roof_aggressiveness: float,
577
- roof_morph_frac: float,
578
- depth_smoothing_base: float,
579
- coverage_strictness: float,
580
- model_id: str,
581
- source_path: str | None = None,
582
- ) -> dict:
583
- path = Path(input_path)
584
- if not path.exists():
585
- raise gr.Error(f"Input path not found: {path}")
586
- if path.suffix.lower() not in IMAGE_EXTS:
587
- raise gr.Error(f"Unsupported image type for path: {path}")
588
- image = crop_nonblack(Image.open(path).convert("RGB"))
589
- return run_on_image(
590
- image=image,
591
- footprint_m=footprint_m,
592
- std_thresh=std_thresh,
593
- grad_thresh=grad_thresh,
594
- use_water_mask=use_water_mask,
595
- use_road_mask=use_road_mask,
596
- use_roof_mask=use_roof_mask,
597
- altitude_m=altitude_m,
598
- fov_deg=fov_deg,
599
- flatness_detail=flatness_detail,
600
- clearance_factor=clearance_factor,
601
- process_res_cap=process_res_cap,
602
- roof_aggressiveness=roof_aggressiveness,
603
- roof_morph_frac=roof_morph_frac,
604
- depth_smoothing_base=depth_smoothing_base,
605
- coverage_strictness=coverage_strictness,
606
- model_id=model_id,
607
- source_path=str(path),
608
- )
609
-
610
-
611
- def compose_view(
612
- images_dict: dict,
613
- base_view: str,
614
- heat_on: bool,
615
- heat_alpha: float,
616
- grad_on: bool,
617
- grad_alpha: float,
618
- flat_on: bool,
619
- flat_alpha: float,
620
- water_on: bool,
621
- water_alpha: float,
622
- water_enabled: bool,
623
- spot_on: bool,
624
- road_on: bool,
625
- road_alpha: float,
626
- road_enabled: bool,
627
- roof_on: bool,
628
- roof_alpha: float,
629
- roof_enabled: bool,
630
- ) -> Image.Image:
631
- """Return a composited view with per-layer alpha controls."""
632
- if not images_dict:
633
- raise gr.Error("Run inference first, then select a view.")
634
- if base_view not in images_dict:
635
- raise gr.Error(f"Unknown view: {base_view}")
636
-
637
- base = images_dict.get(base_view)
638
- if base is None:
639
- raise gr.Error(f"No image for view: {base_view}")
640
- out = base.convert("RGBA")
641
-
642
- if heat_on and "Safety heatmap overlay" in images_dict:
643
- heat = images_dict["Safety heatmap overlay"]
644
- if heat is not None:
645
- heat_rgba = heat.convert("RGBA")
646
- alpha_factor = min(max(heat_alpha, 0.0), 1.0)
647
- alpha_channel = np.array(heat_rgba.getchannel("A"), dtype=np.uint8)
648
- alpha_channel = (alpha_channel.astype(np.float32) * alpha_factor).astype(np.uint8)
649
- heat_rgba.putalpha(Image.fromarray(alpha_channel, mode="L"))
650
- out = Image.alpha_composite(out, heat_rgba)
651
-
652
- if grad_on and "Depth gradient" in images_dict:
653
- grad_img = images_dict["Depth gradient"]
654
- if grad_img is not None:
655
- grad_rgba = grad_img.convert("RGBA")
656
- grad_rgba.putalpha(int(min(max(grad_alpha, 0.0), 1.0) * 255))
657
- out = Image.alpha_composite(out, grad_rgba)
658
-
659
- if flat_on and "Flatness map (std)" in images_dict:
660
- flat_img = images_dict["Flatness map (std)"]
661
- if flat_img is not None:
662
- flat_rgba = flat_img.convert("RGBA")
663
- flat_rgba.putalpha(int(min(max(flat_alpha, 0.0), 1.0) * 255))
664
- out = Image.alpha_composite(out, flat_rgba)
665
-
666
- if water_on and water_enabled and "Water mask" in images_dict:
667
- wm = images_dict["Water mask"]
668
- if wm is not None:
669
- m = wm.convert("L")
670
- overlay = Image.new("RGBA", wm.size, (255, 0, 0, 0))
671
- alpha = int(min(max(water_alpha, 0.0), 1.0) * 255)
672
- overlay.putalpha(Image.eval(m, lambda px: int(px * (alpha / 255.0))))
673
- out = Image.alpha_composite(out, overlay)
674
-
675
- if road_on and road_enabled and "Road mask" in images_dict:
676
- rm = images_dict["Road mask"]
677
- if rm is not None:
678
- m = rm.convert("L")
679
- overlay = Image.new("RGBA", rm.size, (255, 165, 0, 0)) # orange
680
- alpha = int(min(max(road_alpha, 0.0), 1.0) * 255)
681
- overlay.putalpha(Image.eval(m, lambda px: int(px * (alpha / 255.0))))
682
- out = Image.alpha_composite(out, overlay)
683
-
684
- if roof_on and roof_enabled and "Roof mask" in images_dict:
685
- rf = images_dict["Roof mask"]
686
- if rf is not None:
687
- m = rf.convert("L")
688
- overlay = Image.new("RGBA", rf.size, (255, 0, 255, 0)) # magenta tint for roofs
689
- alpha = int(min(max(roof_alpha, 0.0), 1.0) * 255)
690
- overlay.putalpha(Image.eval(m, lambda px: int(px * (alpha / 255.0))))
691
- out = Image.alpha_composite(out, overlay)
692
-
693
- if spot_on and "Landing spot overlay" in images_dict:
694
- spot = images_dict["Landing spot overlay"]
695
- if spot is not None:
696
- out = Image.alpha_composite(out, spot.convert("RGBA"))
697
-
698
- return out.convert("RGB")
699
-
700
-
701
- def build_ui():
702
- with gr.Blocks(title="Landing Site Safety Analyzer (VISLOC)") as demo:
703
- gr.Markdown(
704
- "## Landing Site Safety Analyzer\n"
705
- "Run DepthAnything3 on VISLOC images under `data/Image/VISLOC` to evaluate landing zones: depth, safety heatmap, gradients, flatness, and water masks. Toggle layers, footprint, and opacity to assess safety."
706
- )
707
- with gr.Row():
708
- with gr.Column(scale=1, min_width=320):
709
- gr.Markdown("### Input")
710
- all_choices = list_all_data_inputs()
711
- input_path = gr.Dropdown(
712
- label="Input file",
713
- choices=all_choices,
714
- value=all_choices[0] if all_choices else "",
715
- info="Pick any VISLOC image under data/Image/VISLOC/.",
716
- )
717
- footprint_m = gr.Slider(
718
- label="Landing footprint (meters)",
719
- value=10,
720
- minimum=1,
721
- maximum=150,
722
- step=1,
723
- info="Side length (meters) of the clear area required for landing (assumes ~450m altitude, 90° FOV).",
724
- )
725
- std_thresh = gr.Slider(
726
- label="Flatness threshold",
727
- value=0.01,
728
- minimum=0.001,
729
- maximum=0.08,
730
- step=0.001,
731
- info="Lower values favor flatter regions when computing the heatmap.",
732
- )
733
- grad_thresh = gr.Slider(
734
- label="Gradient threshold",
735
- value=0.1,
736
- minimum=0.02,
737
- maximum=1.0,
738
- step=0.01,
739
- info="Lower values suppress sloped/edgy areas in the heatmap.",
740
- )
741
- flatness_detail = gr.Slider(
742
- label="Flatness detail (relative)",
743
- value=1.0,
744
- minimum=0.5,
745
- maximum=2.5,
746
- step=0.1,
747
- info="Scales the window for the flatness visualization; lower = finer detail.",
748
- )
749
- clearance_factor = gr.Slider(
750
- label="Clearance factor",
751
- value=0.5,
752
- minimum=0.0,
753
- maximum=2.0,
754
- step=0.05,
755
- info="How much to dilate unsafe regions relative to the footprint to enforce buffer distance.",
756
- )
757
- process_res_cap = gr.Slider(
758
- label="Processing resolution cap",
759
- value=1024,
760
- minimum=512,
761
- maximum=2048,
762
- step=32,
763
- info="Upper bound on the longest side fed to the depth model; avoids oversized, noisy inference.",
764
- )
765
- depth_smoothing_base = gr.Slider(
766
- label="Depth smoothing base",
767
- value=0.8,
768
- minimum=0.0,
769
- maximum=2.0,
770
- step=0.05,
771
- info="Base Gaussian sigma multiplier for depth smoothing (scaled by resolution).",
772
- )
773
- coverage_strictness = gr.Slider(
774
- label="Coverage strictness",
775
- value=0.999,
776
- minimum=0.8,
777
- maximum=1.0,
778
- step=0.001,
779
- info="Minimum fraction of a footprint that must be safe to count a center as safe.",
780
- )
781
- with gr.Accordion("Camera settings", open=False):
782
- altitude_m = gr.Slider(
783
- label="Camera altitude (m)",
784
- value=450,
785
- minimum=10,
786
- maximum=1500,
787
- step=5,
788
- info="Altitude used to convert footprint meters to pixels.",
789
- )
790
- fov_deg = gr.Slider(
791
- label="Camera FOV (deg)",
792
- value=90,
793
- minimum=30,
794
- maximum=150,
795
- step=1,
796
- info="Horizontal field of view used for footprint sizing.",
797
- )
798
- model_id = gr.Dropdown(
799
- label="Model",
800
- value="depth-anything/DA3MONO-LARGE",
801
- choices=[
802
- "depth-anything/DA3MONO-LARGE",
803
- "depth-anything/DA3METRIC-LARGE",
804
- "depth-anything/DA3-BASE",
805
- "depth-anything/DA3NESTED-GIANT-LARGE",
806
- ],
807
- info="Which pretrained DepthAnything3 checkpoint to use.",
808
- )
809
- with gr.Accordion("Masking", open=True):
810
- with gr.Row():
811
- use_water_mask = gr.Checkbox(
812
- label="Exclude water (segmentation)", value=True, info="Apply water segmentation to down-weight water regions."
813
- )
814
- use_road_mask = gr.Checkbox(
815
- label="Exclude roads (segmentation)", value=True, info="Apply road segmentation to avoid roads/highways."
816
- )
817
- use_roof_mask = gr.Checkbox(
818
- label="Exclude rooftops (depth)", value=True, info="Use depth (closer-than-median) to avoid rooftops/raised structures."
819
- )
820
- roof_aggressiveness = gr.Slider(
821
- label="Rooftop aggressiveness (MAD multiplier)",
822
- value=1.3,
823
- minimum=0.5,
824
- maximum=3.0,
825
- step=0.05,
826
- info="Higher = more aggressive exclusion of raised areas in the depth-based rooftop mask.",
827
- )
828
- roof_morph_frac = gr.Slider(
829
- label="Rooftop morph kernel (fraction of footprint px)",
830
- value=0.15,
831
- minimum=0.05,
832
- maximum=0.5,
833
- step=0.01,
834
- info="Controls smoothing/merging of rooftop mask relative to footprint size.",
835
- )
836
- with gr.Row():
837
- run_btn = gr.Button("Run", variant="primary", scale=1)
838
- stop_btn = gr.Button("Stop", variant="stop", scale=1)
839
- images_state = gr.State({})
840
- with gr.Column(scale=3):
841
- gr.Markdown("### Preview")
842
- main_view = gr.Image(
843
- label="Preview",
844
- height=800,
845
- elem_id="main-preview",
846
- show_fullscreen_button=False,
847
- )
848
- gr.HTML(
849
- """
850
- <style>
851
- #main-preview img,
852
- #main-preview canvas { cursor: zoom-in; }
853
- #main-preview-zoom-overlay {
854
- position: fixed;
855
- inset: 0;
856
- z-index: 1000;
857
- display: none;
858
- align-items: center;
859
- justify-content: center;
860
- background: rgba(0, 0, 0, 0.85);
861
- }
862
- #main-preview-zoom-overlay img {
863
- max-width: 95vw;
864
- max-height: 95vh;
865
- box-shadow: 0 0 24px rgba(0, 0, 0, 0.6);
866
- }
867
- </style>
868
- <div id="main-preview-zoom-overlay"></div>
869
- <script>
870
- (() => {
871
- const containerId = "main-preview";
872
- const overlayId = "main-preview-zoom-overlay";
873
-
874
- const ensureOverlay = () => {
875
- let overlay = document.getElementById(overlayId);
876
- if (!overlay) {
877
- overlay = document.createElement("div");
878
- overlay.id = overlayId;
879
- document.body.appendChild(overlay);
880
- }
881
- overlay.onclick = () => {
882
- overlay.style.display = "none";
883
- overlay.innerHTML = "";
884
- };
885
- return overlay;
886
- };
887
-
888
- const getMedia = (container) => {
889
- if (!container) return null;
890
- const img = container.querySelector("img");
891
- if (img) return { type: "img", el: img, getSrc: () => img.currentSrc || img.src };
892
- const canvas = container.querySelector("canvas");
893
- if (canvas) return { type: "canvas", el: canvas, getSrc: () => canvas.toDataURL("image/png") };
894
- return null;
895
- };
896
-
897
- const bind = () => {
898
- const container = document.getElementById(containerId);
899
- if (!container || container.dataset.zoomBound) return;
900
- container.dataset.zoomBound = "1";
901
- container.addEventListener("click", (ev) => {
902
- const media = getMedia(container);
903
- if (!media) return;
904
- const src = media.getSrc();
905
- if (!src) return;
906
- const overlay = ensureOverlay();
907
- overlay.innerHTML = "";
908
- const zoomed = document.createElement("img");
909
- zoomed.src = src;
910
- overlay.appendChild(zoomed);
911
- overlay.style.display = "flex";
912
- ev.stopPropagation();
913
- });
914
- };
915
-
916
- // Poll because Gradio swaps the image element on updates.
917
- const interval = setInterval(() => {
918
- const media = getMedia(document.getElementById(containerId));
919
- if (media && media.el && !media.el.dataset.cursorSet) {
920
- media.el.dataset.cursorSet = "1";
921
- media.el.style.cursor = "zoom-in";
922
- }
923
- bind();
924
- }, 500);
925
- window.addEventListener("beforeunload", () => clearInterval(interval));
926
- })();
927
- </script>
928
- """,
929
- elem_id="main-preview-zoom-helper",
930
- )
931
- with gr.Column(scale=1, min_width=260):
932
- gr.Markdown("### Overlays")
933
- base_view = gr.Dropdown(
934
- label="Base view",
935
- value="RGB",
936
- choices=[
937
- "RGB",
938
- "Depth",
939
- "Flatness map (std)",
940
- "Depth gradient",
941
- "Gradient mask",
942
- "Water mask",
943
- "Safety score",
944
- "Safety heatmap overlay",
945
- ],
946
- )
947
- heat_on = gr.Checkbox(label="Heatmap", value=True, info="Show the safety heatmap overlay.")
948
- heat_alpha = gr.Slider(
949
- label="Heatmap alpha", value=0.15, minimum=0.0, maximum=1.0, step=0.05, info="Heatmap opacity."
950
- )
951
- grad_on = gr.Checkbox(label="Depth gradient", value=False, info="Overlay the depth gradient magnitude.")
952
- grad_alpha = gr.Slider(
953
- label="Gradient alpha", value=0.35, minimum=0.0, maximum=1.0, step=0.05, info="Gradient overlay opacity."
954
- )
955
- flat_on = gr.Checkbox(label="Flatness map", value=False, info="Overlay per-pixel flatness (std).")
956
- flat_alpha = gr.Slider(
957
- label="Flatness alpha", value=0.25, minimum=0.0, maximum=1.0, step=0.05, info="Flatness overlay opacity."
958
- )
959
- spot_on = gr.Checkbox(label="Show landing spot", value=True, info="Overlay the recommended landing box.")
960
- with gr.Accordion("Mask overlays", open=True):
961
- water_on = gr.Checkbox(label="Water mask overlay", value=False, info="Overlay detected water regions.")
962
- water_alpha = gr.Slider(
963
- label="Water mask alpha",
964
- value=0.5,
965
- minimum=0.0,
966
- maximum=1.0,
967
- step=0.05,
968
- info="Water overlay opacity.",
969
- )
970
- road_on = gr.Checkbox(label="Road mask overlay", value=False, info="Overlay detected road regions.")
971
- road_alpha = gr.Slider(
972
- label="Road mask alpha",
973
- value=0.5,
974
- minimum=0.0,
975
- maximum=1.0,
976
- step=0.05,
977
- info="Road overlay opacity.",
978
- )
979
- roof_on = gr.Checkbox(label="Roof mask overlay", value=False, info="Overlay detected roof regions.")
980
- roof_alpha = gr.Slider(
981
- label="Roof mask alpha",
982
- value=0.5,
983
- minimum=0.0,
984
- maximum=1.0,
985
- step=0.05,
986
- info="Roof overlay opacity.",
987
- )
988
-
989
- def process_any(
990
- input_path,
991
- footprint_m,
992
- std_thresh,
993
- grad_thresh,
994
- use_water_mask,
995
- use_road_mask,
996
- use_roof_mask,
997
- altitude_m,
998
- fov_deg,
999
- flatness_detail,
1000
- clearance_factor,
1001
- process_res_cap,
1002
- roof_aggressiveness,
1003
- roof_morph_frac,
1004
- depth_smoothing_base,
1005
- coverage_strictness,
1006
- model_id,
1007
- base_view,
1008
- heat_on,
1009
- heat_alpha,
1010
- grad_on,
1011
- grad_alpha,
1012
- flat_on,
1013
- flat_alpha,
1014
- water_on,
1015
- water_alpha,
1016
- spot_on,
1017
- road_on,
1018
- road_alpha,
1019
- roof_on,
1020
- roof_alpha,
1021
- ):
1022
- if not input_path:
1023
- raise gr.Error("Select an input image first.")
1024
- path = Path(input_path)
1025
- if not path.exists():
1026
- raise gr.Error(f"Input not found: {path}")
1027
- if path.suffix.lower() in IMAGE_EXTS:
1028
- imgs = process_image(
1029
- input_path=str(path),
1030
- footprint_m=footprint_m,
1031
- std_thresh=std_thresh,
1032
- grad_thresh=grad_thresh,
1033
- use_water_mask=use_water_mask,
1034
- use_road_mask=use_road_mask,
1035
- use_roof_mask=use_roof_mask,
1036
- altitude_m=altitude_m,
1037
- fov_deg=fov_deg,
1038
- flatness_detail=flatness_detail,
1039
- clearance_factor=clearance_factor,
1040
- process_res_cap=process_res_cap,
1041
- roof_aggressiveness=roof_aggressiveness,
1042
- roof_morph_frac=roof_morph_frac,
1043
- depth_smoothing_base=depth_smoothing_base,
1044
- coverage_strictness=coverage_strictness,
1045
- model_id=model_id,
1046
- source_path=str(path),
1047
- )
1048
- composed = compose_view(
1049
- imgs,
1050
- base_view,
1051
- heat_on,
1052
- heat_alpha,
1053
- grad_on,
1054
- grad_alpha,
1055
- flat_on,
1056
- flat_alpha,
1057
- water_on,
1058
- water_alpha,
1059
- water_enabled=use_water_mask,
1060
- road_on=road_on,
1061
- road_alpha=road_alpha,
1062
- road_enabled=use_road_mask,
1063
- roof_on=roof_on,
1064
- roof_alpha=roof_alpha,
1065
- roof_enabled=use_roof_mask,
1066
- spot_on=spot_on,
1067
- )
1068
- yield imgs, composed
1069
- else:
1070
- raise gr.Error(f"Unsupported input type for path: {path} (images only)")
1071
-
1072
- run_event = run_btn.click(
1073
- fn=process_any,
1074
- inputs=[
1075
- input_path,
1076
- footprint_m,
1077
- std_thresh,
1078
- grad_thresh,
1079
- use_water_mask,
1080
- use_road_mask,
1081
- use_roof_mask,
1082
- altitude_m,
1083
- fov_deg,
1084
- flatness_detail,
1085
- clearance_factor,
1086
- process_res_cap,
1087
- roof_aggressiveness,
1088
- roof_morph_frac,
1089
- depth_smoothing_base,
1090
- coverage_strictness,
1091
- model_id,
1092
- base_view,
1093
- heat_on,
1094
- heat_alpha,
1095
- grad_on,
1096
- grad_alpha,
1097
- flat_on,
1098
- flat_alpha,
1099
- water_on,
1100
- water_alpha,
1101
- spot_on,
1102
- road_on,
1103
- road_alpha,
1104
- roof_on,
1105
- roof_alpha,
1106
- ],
1107
- outputs=[images_state, main_view],
1108
- )
1109
- stop_btn.click(fn=None, inputs=None, outputs=None, cancels=[run_event])
1110
- def update_preview_ui(
1111
- images_state_val,
1112
- input_path_val,
1113
- footprint_m_val,
1114
- std_thresh_val,
1115
- grad_thresh_val,
1116
- use_water_mask_val,
1117
- use_road_mask_val,
1118
- use_roof_mask_val,
1119
- altitude_m_val,
1120
- fov_deg_val,
1121
- flatness_detail_val,
1122
- clearance_factor_val,
1123
- process_res_cap_val,
1124
- roof_aggressiveness_val,
1125
- roof_morph_frac_val,
1126
- depth_smoothing_base_val,
1127
- coverage_strictness_val,
1128
- model_id_val,
1129
- base_view_val,
1130
- heat_on_val,
1131
- heat_alpha_val,
1132
- grad_on_val,
1133
- grad_alpha_val,
1134
- flat_on_val,
1135
- flat_alpha_val,
1136
- water_on_val,
1137
- water_alpha_val,
1138
- spot_on_val,
1139
- road_on_val,
1140
- road_alpha_val,
1141
- roof_on_val,
1142
- roof_alpha_val,
1143
- ):
1144
- path = Path(str(input_path_val))
1145
- imgs_val = images_state_val
1146
- # If current input is an image, re-run processing to reflect new settings
1147
- if path.exists() and path.suffix.lower() in IMAGE_EXTS:
1148
- try:
1149
- imgs_val = process_image(
1150
- input_path=str(path),
1151
- footprint_m=footprint_m_val,
1152
- std_thresh=std_thresh_val,
1153
- grad_thresh=grad_thresh_val,
1154
- use_water_mask=use_water_mask_val,
1155
- use_road_mask=use_road_mask_val,
1156
- use_roof_mask=use_roof_mask_val,
1157
- altitude_m=altitude_m_val,
1158
- fov_deg=fov_deg_val,
1159
- flatness_detail=flatness_detail_val,
1160
- clearance_factor=clearance_factor_val,
1161
- process_res_cap=process_res_cap_val,
1162
- roof_aggressiveness=roof_aggressiveness_val,
1163
- roof_morph_frac=roof_morph_frac_val,
1164
- depth_smoothing_base=depth_smoothing_base_val,
1165
- coverage_strictness=coverage_strictness_val,
1166
- model_id=model_id_val,
1167
- )
1168
- except Exception:
1169
- imgs_val = images_state_val
1170
- if not imgs_val:
1171
- return images_state_val, gr.update()
1172
- composed = compose_view(
1173
- imgs_val,
1174
- base_view_val,
1175
- heat_on_val,
1176
- heat_alpha_val,
1177
- grad_on_val,
1178
- grad_alpha_val,
1179
- flat_on_val,
1180
- flat_alpha_val,
1181
- water_on_val,
1182
- water_alpha_val,
1183
- use_water_mask_val,
1184
- spot_on_val,
1185
- road_on_val,
1186
- road_alpha_val,
1187
- use_road_mask_val,
1188
- roof_on_val,
1189
- roof_alpha_val,
1190
- use_roof_mask_val,
1191
- )
1192
- return imgs_val, composed
1193
-
1194
- overlay_inputs = [
1195
- images_state,
1196
- base_view,
1197
- heat_on,
1198
- heat_alpha,
1199
- grad_on,
1200
- grad_alpha,
1201
- flat_on,
1202
- flat_alpha,
1203
- water_on,
1204
- water_alpha,
1205
- spot_on,
1206
- use_water_mask,
1207
- road_on,
1208
- road_alpha,
1209
- use_road_mask,
1210
- roof_on,
1211
- roof_alpha,
1212
- use_roof_mask,
1213
- ]
1214
-
1215
- def update_overlays_only(
1216
- images_state_val,
1217
- base_view_val,
1218
- heat_on_val,
1219
- heat_alpha_val,
1220
- grad_on_val,
1221
- grad_alpha_val,
1222
- flat_on_val,
1223
- flat_alpha_val,
1224
- water_on_val,
1225
- water_alpha_val,
1226
- spot_on_val,
1227
- use_water_mask_val,
1228
- road_on_val,
1229
- road_alpha_val,
1230
- use_road_mask_val,
1231
- roof_on_val,
1232
- roof_alpha_val,
1233
- use_roof_mask_val,
1234
- ):
1235
- if not images_state_val:
1236
- return images_state_val, gr.update()
1237
- return images_state_val, compose_view(
1238
- images_state_val,
1239
- base_view_val,
1240
- heat_on_val,
1241
- heat_alpha_val,
1242
- grad_on_val,
1243
- grad_alpha_val,
1244
- flat_on_val,
1245
- flat_alpha_val,
1246
- water_on_val,
1247
- water_alpha_val,
1248
- use_water_mask_val,
1249
- spot_on_val,
1250
- road_on_val,
1251
- road_alpha_val,
1252
- use_road_mask_val,
1253
- roof_on_val,
1254
- roof_alpha_val,
1255
- use_roof_mask_val,
1256
- )
1257
-
1258
- base_view.change(fn=update_overlays_only, inputs=overlay_inputs, outputs=[images_state, main_view])
1259
- for control in (
1260
- heat_on,
1261
- heat_alpha,
1262
- grad_on,
1263
- grad_alpha,
1264
- flat_on,
1265
- flat_alpha,
1266
- water_on,
1267
- water_alpha,
1268
- spot_on,
1269
- use_water_mask,
1270
- road_on,
1271
- road_alpha,
1272
- use_road_mask,
1273
- roof_on,
1274
- roof_alpha,
1275
- use_roof_mask,
1276
- ):
1277
- control.change(fn=update_overlays_only, inputs=overlay_inputs, outputs=[images_state, main_view])
1278
-
1279
- model_inputs = [
1280
- images_state,
1281
- input_path,
1282
- footprint_m,
1283
- std_thresh,
1284
- grad_thresh,
1285
- use_water_mask,
1286
- use_road_mask,
1287
- use_roof_mask,
1288
- altitude_m,
1289
- fov_deg,
1290
- flatness_detail,
1291
- clearance_factor,
1292
- process_res_cap,
1293
- roof_aggressiveness,
1294
- roof_morph_frac,
1295
- depth_smoothing_base,
1296
- coverage_strictness,
1297
- model_id,
1298
- base_view,
1299
- heat_on,
1300
- heat_alpha,
1301
- grad_on,
1302
- grad_alpha,
1303
- flat_on,
1304
- flat_alpha,
1305
- water_on,
1306
- water_alpha,
1307
- spot_on,
1308
- road_on,
1309
- road_alpha,
1310
- roof_on,
1311
- roof_alpha,
1312
- ]
1313
- for control in (
1314
- input_path,
1315
- footprint_m,
1316
- std_thresh,
1317
- grad_thresh,
1318
- use_water_mask,
1319
- use_road_mask,
1320
- use_roof_mask,
1321
- altitude_m,
1322
- fov_deg,
1323
- flatness_detail,
1324
- clearance_factor,
1325
- model_id,
1326
- ):
1327
- control.change(fn=update_preview_ui, inputs=model_inputs, outputs=[images_state, main_view])
1328
- return demo
1329
 
1330
 
1331
  if __name__ == "__main__":
1332
- demo = build_ui()
1333
- demo.queue().launch()
 
1
  #!/usr/bin/env python3
2
+ """Launch the modular Landing Site Safety Analyzer Gradio demo."""
 
3
 
 
 
 
 
 
 
 
 
 
4
  import os
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
+ from app.ui import build_ui
 
 
 
 
 
7
 
8
 
9
+ def main() -> None:
10
+ demo = build_ui()
11
+ use_queue = os.getenv("DA_USE_QUEUE")
12
+ use_queue_flag = False if use_queue is None else use_queue.lower() not in {"0", "false", "no"}
13
+ share = os.getenv("DA_SHARE")
14
+ share_flag = False if share is None else share.lower() not in {"0", "false", "no"}
15
+ server_port_str = os.getenv("GRADIO_SERVER_PORT")
16
+ server_port = int(server_port_str) if server_port_str else None
17
+ server_port_range = None
18
+ range_env = os.getenv("GRADIO_SERVER_PORT_RANGE")
19
+ if range_env:
20
  try:
21
+ start_str, end_str = range_env.split(",", 1)
22
+ server_port_range = (int(start_str), int(end_str))
23
+ except ValueError:
24
+ server_port_range = None
25
+ launch_kwargs = {"share": share_flag}
26
+ if server_port is not None:
27
+ launch_kwargs["server_port"] = server_port
28
+ if server_port_range is not None:
29
+ launch_kwargs["server_port_range"] = server_port_range
30
+ if use_queue_flag:
31
  try:
32
+ demo.queue().launch(**launch_kwargs)
33
  except TypeError:
34
+ launch_kwargs.pop("server_port_range", None)
35
+ demo.queue().launch(**launch_kwargs)
36
+ else:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  try:
38
+ demo.launch(**launch_kwargs)
39
  except TypeError:
40
+ launch_kwargs.pop("server_port_range", None)
41
+ demo.launch(**launch_kwargs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
 
44
  if __name__ == "__main__":
45
+ main()