Caler les faisceaux de vol, décimer l'openness et cibler les couches affichées

Trois chantiers liés à la qualité et au coût des rendus :

- Calage vertical des faisceaux : les passes d'une tuile peuvent être biaisées
  de quelques cm (±2,5 cm mesurés sur 1000_6882), créant des marches aux
  recouvrements. Les offsets par PointSourceId sont mesurés sur les points
  sol de la tuile (réf. médiane itérée) et retranchés ≥ 0,5 cm avant
  rastérisation, avec sidecar de cache et application au plancher bare-earth.
- Openness décimée ×2 : lancé de rayons sur grille par blocs (max/min) puis
  rééchantillonnage bilinéaire — 532 s → 40 s par tuile à 0,2 m sur CPU,
  signal archéologique préservé. Réglable --openness-downsample.
- Génération webapp concentrée sur les couches affichées : les défauts
  /api/generate, /api/preview et le sélecteur génèrent le panneau complet
  (slope, aspect, pos_open) au lieu d'aspect seul.
This commit is contained in:
Antoine Jacquin
2026-09-19 00:53:22 +02:00
parent 796e68c870
commit 41206f65de
11 changed files with 455 additions and 25 deletions

View File

@ -628,12 +628,24 @@ def generate_svf(dem_file, basename, vis_dir, resolution, shared=None):
return None
# Sous-échantillonnage du calcul d'openness : le lancé de rayons est l'étape
# la plus coûteuse du pipeline (rayons de 100 m = 500 pas à 0,2 m/px). L'openness
# étant un champ angulaire lisse (moyenne d'horizons jusqu'à 100 m), on la
# calcule sur une grille décimée par blocs — max local pour l'openness
# positive, min pour la négative, afin de préserver les reliefs qui bornent
# l'angle d'horizon (talus, murs) — puis on la rééchantillonne à la
# résolution demandée. Coût ÷ facteur³ (cellules ÷ facteur², rayons ÷ facteur)
# ; facteur 2 ≈ ×8 plus rapide, rendu quasi identique. 1 = pleine résolution.
OPENNESS_DOWNSAMPLE = 2
def generate_openness(dem_file, basename, vis_dir, resolution, positive=True, shared=None):
"""Positive/Negative Openness - multi-radius ray-tracing with std normalization.
Traces rays in 8 directions at 3 radii (25, 50, 100m).
Results are combined with equal weight across radii, then normalized
by standard deviation for cross-tile comparability.
Traces rays in 8 directions at 3 radii (25, 50, 100m) on a block-decimated
grid (cf. OPENNESS_DOWNSAMPLE), then bilinearly resamples the result back
to the requested resolution. Results are combined with equal weight across
radii, then normalized by standard deviation for cross-tile comparability.
"""
name = "positive_openness" if positive else "negative_openness"
gpu_tag = " [GPU]" if _gpu_mod.HAS_GPU else ""
@ -646,8 +658,21 @@ def generate_openness(dem_file, basename, vis_dir, resolution, positive=True, sh
_prepare_dem_for_raycast(dem_file, shared, resolution)
radii_m = [25, 50, 100]
max_dist = int(max(radii_m) / res) # rayon réel en pixels, non tronqué
n_dirs = 8
full_rows, full_cols = rows, cols
# Grille décimée par blocs (cf. OPENNESS_DOWNSAMPLE)
factor = max(1, int(OPENNESS_DOWNSAMPLE))
if factor > 1 and rows >= 2 * factor and cols >= 2 * factor:
r2 = (rows // factor) * factor
c2 = (cols // factor) * factor
blocks = dem[:r2, :c2].reshape(r2 // factor, factor, c2 // factor, factor)
dem = blocks.max(axis=(1, 3)) if positive else blocks.min(axis=(1, 3))
rows, cols = dem.shape
res = res * factor
logger.info(f" Grille décimée ×{factor} ({rows}×{cols}) — rééchantillonnage final")
max_dist = int(max(radii_m) / res) # rayon réel en pixels, non tronqué
pos_angles, neg_angles = _ray_trace_horizons(dem, rows, cols, res, n_dirs, max_dist, radii_m)
@ -660,6 +685,18 @@ def generate_openness(dem_file, basename, vis_dir, resolution, positive=True, sh
# Mean across directions and radii (equal weight) — on CPU now
openness = np.mean(angles, axis=(0, 1))
openness_result = np.degrees(openness).astype(np.float32)
# Retour à la grille pleine résolution (bilinéaire ; bord ajusté au plus
# proche voisin si les dimensions n'étaient pas divisibles par le facteur)
if (rows, cols) != (full_rows, full_cols):
from scipy.ndimage import zoom
zoomed = zoom(openness_result, factor, order=1)
if zoomed.shape != (full_rows, full_cols):
zoomed = np.pad(zoomed,
((0, full_rows - zoomed.shape[0]),
(0, full_cols - zoomed.shape[1])),
mode='edge')
openness_result = zoomed.astype(np.float32)
openness_result[nan_mask] = np.nan
# Z-score (écarts locaux en sigmas) : unités comparables entre tuiles,