yakvrz commited on
Commit
78d796a
·
1 Parent(s): af8f4ba

Auto-scale thresholds with resolution

Browse files
Files changed (3) hide show
  1. ARCHITECTURE.md +3 -2
  2. app/safety.py +13 -5
  3. app/ui.py +1 -0
ARCHITECTURE.md CHANGED
@@ -14,6 +14,7 @@ This document describes the flow in the current Gradio app (`app/ui.py`), from i
14
  - Coverage strictness: default 0.95 (fraction of the footprint that must be safe).
15
  - Depth smoothing: `depth_smoothing_base` scaled by resolution to reduce speckle before scoring.
16
  - Roof mask: depth-based, with large components (>20% of the map) discarded to avoid masking whole fields.
 
17
 
18
  ## Per-Image Processing Pipeline
19
  1. **Load and crop** the selected image (RGB, 5% border removed).
@@ -28,9 +29,9 @@ This document describes the flow in the current Gradio app (`app/ui.py`), from i
28
  - Roof via depth MAD threshold + grad gate; components smaller than `area_thresh` and not exceeding `max_area_frac` (20%) are kept; also dilated to the footprint for blocking.
29
  6. **Flat region search (`pick_flat_patch`)**:
30
  - Normalize depth to [0,1], compute `std_map` via box mean/mean_sq, and `grad_norm` via `np.gradient` normalized at the 95th percentile.
31
- - Landing mask starts from `grad_norm < grad_thresh`, excludes water/road if present, and keeps the lowest-variance patch as a fallback box.
32
  7. **Safe mask construction**:
33
- - Base safe mask: `(std_map < std_thresh) & (grad_norm < grad_thresh) & landing_mask & texture_mask`.
34
  - Apply segmentation blocks (expanded masks) to remove water/road/roof regions.
35
  - Clearance: dilate hazards by `clearance_factor * patch_px` (default 0, so off unless raised).
36
  - Coverage: box filter with `patch_px` window; keep pixels meeting `coverage_strictness` (default 0.95).
 
14
  - Coverage strictness: default 0.95 (fraction of the footprint that must be safe).
15
  - Depth smoothing: `depth_smoothing_base` scaled by resolution to reduce speckle before scoring.
16
  - Roof mask: depth-based, with large components (>20% of the map) discarded to avoid masking whole fields.
17
+ - Resolution-aware thresholds: flatness (`std_thresh`) and gradient (`grad_thresh`) are auto-scaled with the actual depth resolution; the UI sliders act as base values.
18
 
19
  ## Per-Image Processing Pipeline
20
  1. **Load and crop** the selected image (RGB, 5% border removed).
 
29
  - Roof via depth MAD threshold + grad gate; components smaller than `area_thresh` and not exceeding `max_area_frac` (20%) are kept; also dilated to the footprint for blocking.
30
  6. **Flat region search (`pick_flat_patch`)**:
31
  - Normalize depth to [0,1], compute `std_map` via box mean/mean_sq, and `grad_norm` via `np.gradient` normalized at the 95th percentile.
32
+ - Landing mask starts from `grad_norm < grad_thresh_eff` (resolution-adjusted), excludes water/road if present, and keeps the lowest-variance patch as a fallback box.
33
  7. **Safe mask construction**:
34
+ - Base safe mask: `(std_map < std_thresh_eff) & (grad_norm < grad_thresh_eff) & landing_mask & texture_mask`.
35
  - Apply segmentation blocks (expanded masks) to remove water/road/roof regions.
36
  - Clearance: dilate hazards by `clearance_factor * patch_px` (default 0, so off unless raised).
37
  - Coverage: box filter with `patch_px` window; keep pixels meeting `coverage_strictness` (default 0.95).
app/safety.py CHANGED
@@ -60,6 +60,8 @@ class AnalysisSummary:
60
  roof_mask_enabled: bool
61
  used_valid_center: bool
62
  warnings: list[str]
 
 
63
 
64
 
65
  @dataclass
@@ -213,11 +215,15 @@ class SafetyAnalyzer:
213
  road_mask_resized = np.array(road_mask_resized) > 0
214
  road_mask_block = expand_mask_for_footprint(road_mask_resized)
215
 
 
 
 
 
216
  box, std_map, grad_norm, grad_mask, landing_mask = pick_flat_patch(
217
  depth,
218
  patch=patch_px,
219
- std_thresh=request.std_thresh,
220
- grad_thresh=request.grad_thresh,
221
  water_mask=water_mask_block if water_mask_block is not None else water_mask_resized,
222
  )
223
  if request.use_roof_mask:
@@ -251,7 +257,7 @@ class SafetyAnalyzer:
251
  interior_mask = np.ones_like(landing_mask, dtype=bool)
252
  landing_mask = landing_mask & interior_mask
253
  texture_mask = texture_norm <= max(0.0, min(1.0, request.texture_threshold))
254
- safe_mask = (std_map < request.std_thresh) & (grad_norm < request.grad_thresh) & landing_mask & texture_mask
255
 
256
  try:
257
  clearance_px = max(1, int(round(request.clearance_factor * patch_px)))
@@ -289,8 +295,8 @@ class SafetyAnalyzer:
289
  keep |= labels == i
290
  safe_mask = keep
291
 
292
- risk_std = np.clip((std_map - request.std_thresh) / (request.std_thresh + 1e-6), 0.0, 1.0)
293
- risk_grad = np.clip((grad_norm - request.grad_thresh) / (request.grad_thresh + 1e-6), 0.0, 1.0)
294
  risk_map = np.maximum(risk_std, risk_grad) * (~safe_mask)
295
 
296
  safe_fit = safe_mask.astype(np.float32)
@@ -443,6 +449,8 @@ class SafetyAnalyzer:
443
  roof_mask_enabled=request.use_roof_mask,
444
  used_valid_center=used_valid_center,
445
  warnings=warnings,
 
 
446
  )
447
  return AnalysisResult(images=layers, summary=summary)
448
 
 
60
  roof_mask_enabled: bool
61
  used_valid_center: bool
62
  warnings: list[str]
63
+ std_thresh_applied: float
64
+ grad_thresh_applied: float
65
 
66
 
67
  @dataclass
 
215
  road_mask_resized = np.array(road_mask_resized) > 0
216
  road_mask_block = expand_mask_for_footprint(road_mask_resized)
217
 
218
+ # Autoscale sensitivity with resolution: stricter when resolution is low
219
+ std_thresh_eff = max(1e-6, float(request.std_thresh)) * (res_scale ** -0.5)
220
+ grad_thresh_eff = max(1e-6, float(request.grad_thresh)) * (res_scale ** -0.3)
221
+
222
  box, std_map, grad_norm, grad_mask, landing_mask = pick_flat_patch(
223
  depth,
224
  patch=patch_px,
225
+ std_thresh=std_thresh_eff,
226
+ grad_thresh=grad_thresh_eff,
227
  water_mask=water_mask_block if water_mask_block is not None else water_mask_resized,
228
  )
229
  if request.use_roof_mask:
 
257
  interior_mask = np.ones_like(landing_mask, dtype=bool)
258
  landing_mask = landing_mask & interior_mask
259
  texture_mask = texture_norm <= max(0.0, min(1.0, request.texture_threshold))
260
+ safe_mask = (std_map < std_thresh_eff) & (grad_norm < grad_thresh_eff) & landing_mask & texture_mask
261
 
262
  try:
263
  clearance_px = max(1, int(round(request.clearance_factor * patch_px)))
 
295
  keep |= labels == i
296
  safe_mask = keep
297
 
298
+ risk_std = np.clip((std_map - std_thresh_eff) / (std_thresh_eff + 1e-6), 0.0, 1.0)
299
+ risk_grad = np.clip((grad_norm - grad_thresh_eff) / (grad_thresh_eff + 1e-6), 0.0, 1.0)
300
  risk_map = np.maximum(risk_std, risk_grad) * (~safe_mask)
301
 
302
  safe_fit = safe_mask.astype(np.float32)
 
449
  roof_mask_enabled=request.use_roof_mask,
450
  used_valid_center=used_valid_center,
451
  warnings=warnings,
452
+ std_thresh_applied=std_thresh_eff,
453
+ grad_thresh_applied=grad_thresh_eff,
454
  )
455
  return AnalysisResult(images=layers, summary=summary)
456
 
app/ui.py CHANGED
@@ -90,6 +90,7 @@ def _format_metrics(summary: AnalysisSummary | None) -> str:
90
  f"**Hazard coverage:** {summary.hazard_pct:.1f}%",
91
  f"**Landing center (px):** {summary.landing_center_image[0]}, {summary.landing_center_image[1]}",
92
  f"**Footprint size:** {summary.footprint_m:.1f} m ≈ {summary.footprint_image_px}px",
 
93
  ]
94
  if not summary.used_valid_center:
95
  lines.append("Warning: No fully safe footprint; showing lowest-variance patch.")
 
90
  f"**Hazard coverage:** {summary.hazard_pct:.1f}%",
91
  f"**Landing center (px):** {summary.landing_center_image[0]}, {summary.landing_center_image[1]}",
92
  f"**Footprint size:** {summary.footprint_m:.1f} m ≈ {summary.footprint_image_px}px",
93
+ f"**Effective thresholds:** std ≤ {summary.std_thresh_applied:.4f}, grad ≤ {summary.grad_thresh_applied:.3f}",
94
  ]
95
  if not summary.used_valid_center:
96
  lines.append("Warning: No fully safe footprint; showing lowest-variance patch.")