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

@ -80,6 +80,52 @@ class TestOpenness:
assert result is not None
assert result.exists()
def test_downsampled_matches_full_resolution(self, tmp_path, tmp_output_dir):
"""Factor 2: same output shape, highly correlated with factor 1.
MNT dédié à 1 m/px (structures de plusieurs dizaines de pixels, régime
de production 0,2 m/px) : la fixture synthétique partagée à 5 m/px a un
mur large de 2 px, hors régime pour valider une décimation ×2.
"""
import rasterio
from rasterio.transform import from_bounds
from lidar_pipeline import visualizations
from lidar_pipeline.visualizations import generate_openness
size = 240
x = np.linspace(0, size, size)
X, Y = np.meshgrid(x, x)
dem = 100.0 + 0.01 * X + 0.005 * Y
dem += 5.0 * np.exp(-((X - 120)**2 + (Y - 120)**2) / (2 * 40**2))
dist_wall = np.abs((X - 40) * 0.707 + (Y - 60) * 0.707) / np.sqrt(2)
dem += 1.5 * np.exp(-dist_wall**2 / (2 * 8**2))
dem -= 2.0 * np.exp(-np.abs(X - 200)**2 / (2 * 12**2))
# Sans bruit blanc : à 1 m/px un bruit σ=5 cm domine l'angle
# d'horizon à courte distance (max le long du rayon) et masque la
# géométrie que ce test cherche à valider (décimation + zoom).
dem_file = tmp_path / "dem_1m.tif"
with rasterio.open(dem_file, 'w', driver='GTiff', height=size, width=size,
count=1, dtype='float32', crs='EPSG:2154',
transform=from_bounds(660000, 6700000, 660240, 6700240, size, size)) as dst:
dst.write(dem.astype('float32'), 1)
saved = visualizations.OPENNESS_DOWNSAMPLE
try:
visualizations.OPENNESS_DOWNSAMPLE = 1
r1 = generate_openness(dem_file, "full", tmp_output_dir, 1.0, positive=True)
visualizations.OPENNESS_DOWNSAMPLE = 2
r2 = generate_openness(dem_file, "dec", tmp_output_dir, 1.0, positive=True)
finally:
visualizations.OPENNESS_DOWNSAMPLE = saved
with rasterio.open(r1) as s1, rasterio.open(r2) as s2:
a, b = s1.read(1), s2.read(1)
assert a.shape == b.shape
m = ~np.isnan(a) & ~np.isnan(b)
assert m.sum() > 0
corr = np.corrcoef(a[m], b[m])[0, 1]
assert corr > 0.97, f"corrélation openness décimée/native trop faible : {corr:.3f}"
class TestMSLRM:
def test_generates_tif(self, synthetic_dem, tmp_output_dir):