Add visualization picker to zone generation and unify tile colors

- Web map: multi-select picker in the generation bar (aspect, slope,
  positive openness, anisotropic openness, wavelet) passed to the API;
  the layer panel is restricted to the same shortlist (PANEL_VIZ) and
  a refresh button rebuilds the index when new layers appear on disk;
  jobs started outside the UI are now adopted into the visible queue
- Uniform colors across tiles: openness/anisotropic/sailore now store
  local z-scores, and all renderers use fixed ranges (0-3 sigma, SVF
  0-1 physical, slope 0-30 deg) instead of per-tile percentile stretches
- Ray-tracing falls back to CPU when VRAM is exhausted so openness and
  SVF no longer fail silently on shared GPUs
- build_index merges visualizations available at only one resolution
  into the displayed tile so in-progress layers stay visible
- 11 new tests (142 passing)
This commit is contained in:
Antoine Jacquin
2026-08-31 19:02:14 +02:00
parent 8ca65155db
commit ed3e90ea89
7 changed files with 412 additions and 37 deletions

View File

@ -291,7 +291,7 @@ def _prepare_dem_for_raycast(dem_file, shared, resolution):
return dem, dem_np, rows, cols, res, nan_mask, transform, crs
def _ray_trace_horizons(dem, rows, cols, res, n_dirs, max_dist, radii_m=None):
def _ray_trace_horizons_core(dem, rows, cols, res, n_dirs, max_dist, radii_m=None):
"""Core ray-tracing: compute max zenith/nadir angles per direction and radius.
For each pixel, in each direction, traces rays outward up to max_dist steps,
@ -410,6 +410,25 @@ def _ray_trace_horizons(dem, rows, cols, res, n_dirs, max_dist, radii_m=None):
return pos_angles, neg_angles
def _ray_trace_horizons(dem, rows, cols, res, n_dirs, max_dist, radii_m=None):
"""Ray-tracing avec repli CPU automatique si la VRAM est insuffisante.
Les dalles 0,2 m (5000×5000 px) multi-rayons peuvent dépasser la VRAM
disponible (GPU partagé avec d'autres services) : plutôt que d'abandonner
la visualisation, on désactive le GPU pour ce worker et on relance le
calcul sur CPU.
"""
try:
return _ray_trace_horizons_core(dem, rows, cols, res, n_dirs, max_dist, radii_m)
except Exception as e:
if _gpu_mod.is_gpu_active() and "out of memory" in str(e).lower():
logger.warning(" ⚠ VRAM insuffisante (ray-tracing) — repli CPU pour ce worker")
_gpu_mod.disable_gpu()
gpu_cleanup()
return _ray_trace_horizons_core(dem, rows, cols, res, n_dirs, max_dist, radii_m)
raise
# ============================================================
# Core terrain visualizations
# ============================================================
@ -629,11 +648,12 @@ def generate_openness(dem_file, basename, vis_dir, resolution, positive=True, sh
openness_result = np.degrees(openness).astype(np.float32)
openness_result[nan_mask] = np.nan
# Std normalization for cross-tile comparability
# Z-score (écarts locaux en sigmas) : unités comparables entre tuiles,
# plage de rendu fixe → mosaïque de couleur homogène
valid = openness_result[~nan_mask]
if len(valid) > 0:
std_val = max(np.nanstd(valid), 0.01)
openness_result = openness_result / std_val
openness_result = (openness_result - np.nanmean(valid)) / std_val
_save_tif(output, openness_result, transform, crs)
logger.info(f" ✓ {name} terminé ({time.time()-t0:.1f}s){' [GPU]' if _gpu_mod.HAS_GPU else ''}")
@ -791,6 +811,13 @@ def generate_sailore(dem_file, basename, vis_dir, resolution, shared=None):
sailore = (w_fine * lrm_fine + w_medium * lrm_medium + w_coarse * lrm_coarse) / w_total
sailore[nan_mask] = np.nan
# Z-score (σ locales) : unités comparables entre tuiles, plage de
# rendu fixe ±3σ → mosaïque de couleur homogène
valid = sailore[~nan_mask]
if len(valid) > 0:
std_val = max(np.nanstd(valid), 0.01)
sailore = (sailore - np.nanmean(valid)) / std_val
_save_tif(output, sailore.astype(np.float32), transform, crs)
logger.info(f" ✓ SAILORE terminé ({time.time()-t0:.1f}s){' [GPU]' if _gpu_mod.HAS_GPU else ''}")
return output
@ -1248,11 +1275,12 @@ def generate_aniso_open(dem_file, basename, vis_dir, resolution, shared=None):
aniso_result = np.degrees(pos_combined - neg_combined).astype(np.float32)
aniso_result[nan_mask] = np.nan
# Std normalization for cross-tile comparability
# Z-score (écarts locaux en sigmas) : unités comparables entre tuiles,
# plage de rendu fixe → mosaïque de couleur homogène
valid = aniso_result[~nan_mask]
if len(valid) > 0:
std_val = max(np.nanstd(valid), 0.01)
aniso_result = aniso_result / std_val
aniso_result = (aniso_result - np.nanmean(valid)) / std_val
_save_tif(output, aniso_result, transform, crs)
logger.info(f" ✓ Openness anisotropique terminé ({time.time()-t0:.1f}s){' [GPU]' if _gpu_mod.HAS_GPU else ''}")