Recaler les lignes de balayage des passes et accélérer le rendu
Troisième passe du calage vertical : chaque ligne de balayage (décalage et inclinaison due au roulis) est recalée contre le consensus des autres faisceaux, à toutes les échelles, avec un profil d'étalonnage par faisceau et par degré d'angle qui retire les écarts non linéaires en travers de la fauchée. Les lignes sans recouvrement sont corrigées contre leur propre faisceau. Efface les lignes en creux et la marche au bord de fauchée mesurées sur LHD_FXX_0999_6882 (validé sur des blocs jamais vus). Calcul vectorisé, CuPy si GPU ; la gigue par fenêtres de temps devient inutile quand scan_angle existe. Rendu plus rapide : encodage AVIF speed 9 (0,6 s au lieu de 4 s par dalle), classification IGN par extraction directe laspy au lieu de PDAL (4,9 s au lieu de 13,5 s), comblement des trous et gradients sur GPU, cache numba persistant dans l'image. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@ -29,15 +29,20 @@
|
||||
- **`generate_*` signature is strict**: `(dem_file, basename, vis_dir, resolution, shared=None)` returning `Path` on success, `None` on failure. IGN overlays (`ortho`, `topo`) omit `shared`.
|
||||
- **Return `None` on failure, never raise**: `dtm.py`, `visualizations.py`, and `ign.py` all return `None` to let the pipeline continue. Raising aborts the entire file.
|
||||
- **Logger is always `logging.getLogger("lidar")`**, never `__name__`. All modules route through this single logger so worker processes can configure it.
|
||||
- **Ajustement conjoint des lignes de balayage (3ᵉ passe du calage, `STRIP_ALIGN_VERSION` 3)** : chaque ligne de balayage (~150 lignes/s, découpée par `_scan_line_ids` : retour de la dent de scie de `scan_angle`) peut être décalée ET inclinée (roulis) de 1 à 15 cm — lignes en creux isolées, passe entière basculée visible en bande (mesuré sur LHD_FXX_0999_6882). `_joint_line_corrections` (`dtm.py`) recale chaque ligne (a + b·u) contre le consensus des AUTRES faisceaux de sa maille 1 m (plan local par la pente), moindres carrés tronqués à 5 MAD par `bincount`, pas amorti 0,5, arrêt sous 1 mm, recentrage (pas de dérive d'ensemble), estimation sur 1 point sur 3 ; CuPy si GPU actif, repli numpy. S'y ajoute un **profil d'étalonnage par faisceau et par degré d'angle** (`STRIP_ANGLE_BIN`, commun à toutes les lignes du faisceau, moyenne retirée, pente conservée car l'inclinaison par ligne est indéterminable quand la fauchée ne traverse la dalle qu'en partie ; classes pauvres = valeur voisine) : il retire les écarts non linéaires en travers de la fauchée (±1,5 cm mesurés au bord), sources de marches parallèles au vol au bord de fauchée. Lignes sans recouvrement : `_scan_line_corrections_beam` (contre la surface de leur propre faisceau, composante ligne à ligne seule, σ 3 lignes). Validé sur blocs jamais vus (damier 10 m). La gigue par fenêtres de temps (2ᵉ passe) n'est plus calculée quand `scan_angle` existe (redondante). Coût CPU ~26 s par dalle pour tout le calage (73 s avant). Sidecar : `lines`, `line_window`, `line_cell`, `line_model`.
|
||||
- **Openness à échelle fixe** : `generate_openness` normalise par des références figées `OPENNESS_POS_REF` / `OPENNESS_NEG_REF` (degrés, médianes mesurées sur 15 dalles réparties sur le territoire) et plus par z-score de dalle — même ouverture = même couleur, mosaïque jointive.
|
||||
- **Filename special-cases** in `_expected_output_path()`: `pos_open` → `positive_openness`, `neg_open` → `negative_openness`, `hillshade` → `hillshade_multi`.
|
||||
- **Génération imposée** : la carte ne propose plus de réglage de classification ni de raccord — `_build_command` (`mapserve.py`) passe toujours `--ground-classification ign --ign-classes sol --edge-buffer 100` ; les champs `ground_class`/`ign_classes`/`reclassify`/`edge_buffer` éventuellement envoyés sont ignorés. Défauts CLI alignés (`ign`, `100`).
|
||||
- **Classification IGN par extraction directe** : `_extract_ign_ground` (`dtm.py`) filtre les classes avec laspy (retours ≥ 1) et écrit le LAS sol sans passer par PDAL (4,9 s au lieu de 13,5 s par dalle) ; la lecture de la détection automatique est réutilisée (`_LAST_READ`). PDAL reste le repli.
|
||||
- **Encodage AVIF rapide** : `AVIF_SPEED = 9` (`rendering.py`, `tiles.py`, `_SUBTILE_AVIF_SPEED` dans `index.py`) — dalle 5000 × 5000 px encodée en 0,6 s au lieu de 4 s (+3 % de taille, −0,3 dB) ; l'encodage était l'étape la plus longue du rendu d'une couche.
|
||||
- **Préparation sur GPU** : `_fill_nans` (transformée de distance `cupyx`) et les gradients de `SharedDEM` passent sur GPU quand il est actif (repli CPU). `NUMBA_CACHE_DIR=/tmp/numba-cache` (Dockerfile) évite la recompilation des noyaux numba à chaque worker.
|
||||
- **Default output is AVIF**, not WebP. Use `--format webp` for WebP. Quality default is 60 (visually lossless on smooth color ramps, ~÷3 vs q98).
|
||||
- **Calage vertical des faisceaux de vol** : chaque tuile mélange plusieurs passes (1-2 `PointSourceId` par passe) parfois biaisées verticalement de quelques cm (±2,5 cm mesurés sur 1000_6882). `create_dtm_fast` mesure l'offset robuste de chaque faisceau (points sol, maille 1 m, surface médiane itérée 3×) et retranche les offsets ≥ 0,5 cm (`STRIP_ALIGN_THRESHOLD` dans `dtm.py`) avant rastérisation. Offsets calculés **par tuile** (ils dérivent le long d'une ligne de vol : jamais de table globale), mémoïsés par LAS sol, consignés dans `DTM/*_dtm*_stripalign.json` (sidecar de cache : absent, ou version/seuil/paramètres différents ⇒ régénération du DTM). Désactivable : `--no-strip-align`.
|
||||
- **Gigue intra-faisceau (2ᵉ passe du calage)** : les lignes de balayage successives d'une MÊME passe peuvent être décalées verticalement de façon aléatoire (vibration capteur / bruit haute fréquence de trajectoire) — un offset constant par faisceau n'y suffit pas. `_strip_jitter_offsets` découpe chaque faisceau en fenêtres de temps GPS (`STRIP_JITTER_BIN` = 0,1 s, origine de temps propre à chaque faisceau), mesure l'offset robuste de chaque fenêtre contre la surface médiane des AUTRES faisceaux (maille 1 m partagée, ≥ `STRIP_JITTER_MIN_CELLS` = 40 cellules), lisse la série (médiane glissante `STRIP_JITTER_SMOOTH` = 5 fenêtres), la borne à ± `STRIP_JITTER_MAX` (10 cm) puis l'interpole au temps GPS de chaque point (`_apply_strip_jitter`) ; en recouvrement à deux faisceaux, chacun reçoit une série (chacun absorbe sa part). Requiert la dimension `gps_time` (silencieusement ignorée sinon). Sidecar version 2 (séries dans `jitter`), couverte par `--no-strip-align`.
|
||||
- **Couches produites et servies = `PANEL_VIZ` (`index.py`, aujourd'hui `('relief_oriente',)`)** : pipeline sans `--only`/`--skip` (`panel_steps()`), génération lancée depuis la carte (`_panel_viz_steps` dans `mapserve.py`) et couches servies (`tiles.available_layers` filtre : panneau, `/tiles/…`, TileJSON, WMTS, JOSM). Les autres visualisations restent calculables avec `--only`. Les tests de la carte lèvent la restriction via `_setup(..., panel=None)`.
|
||||
- **Relief orienté (`relief_oriente`, seule couche de la carte)** : image RGB unique (GeoTIFF uint8 3 bandes, rendue telle quelle comme ortho/topo via `RGB_KEYWORDS` dans `rendering.py`) — clarté CIELAB = openness positive **locale** (MNT − gaussienne `RELIEF_DETREND_M` = 10 m, rayons `RELIEF_RADII_M` = 5/10/20 m, 16 directions) 65 % + ombrage 35 % ; teinte = aspect, chroma fixe (`RELIEF_CHROMA`). Échelle log fixe `RELIEF_OPEN_RANGE` (pas de statistique par dalle) et support total 40 m < bande de raccord 100 m : dalles jointives. Rapide : détendance + rayons sur grille décimée ~0,8 m (`RELIEF_GRID_M`), noyau dédié qui n'accumule que la moyenne des angles (`_mean_horizon_*` : CuPy `RawKernel` sur GPU, numba parallèle sur CPU, numpy en repli), colorisation fusionnée (numba) ou vectorisée sans trigonométrie (CuPy) via une table L* × teinte (`_relief_lut`). ~5 s de calcul hors préparation sur CPU 12 cœurs. Tout changement de constante change le rendu : régénérer les dalles (`--only relief_oriente --force`).
|
||||
- **Openness sous-échantillonnée** : `generate_openness` calcule le lancé de rayons (l'étape la plus coûteuse : 532 s/tuile à 0,2 m sur CPU) sur une grille décimée par blocs (`OPENNESS_DOWNSAMPLE = 2` : max par bloc en positive, min en négative — préserve les reliefs qui bornent l'horizon) puis rééchantillonne en bilinéaire. Coût ÷ facteur³ : 532 s → 40 s (×13). Signal archéologique préservé (corr. 0,93 après lissage) ; la texture de bruit sub-métrique disparaît. `--openness-downsample 1` = pleine résolution. SVF et openness anisotrope ne sont PAS concernées.
|
||||
- **Raccord des bords entre tuiles** : les rendus à grand noyau (openness/SVF : rayons 100 m ; LRM : 15 m) tronquent leur fenêtre au bord de dalle — bandes d'artefacts à chaque changement de tuile. `--edge-buffer N` (défaut 0 = off ; case « Raccord des bords » de la carte, `EDGE_BUFFER_METERS` = 100 m dans `mapserve.py`) fait rastériser le DTM sur la **dalle nominale 1 km alignée sur la grille** plus une bande de N m remplie avec les points sol des 8 LAZ voisines (`_neighbor_ground_points` dans `dtm.py` : PDAL en flux, découpe + filtre de classes IGN ; voisine absente = téléchargement automatique depuis le catalogue IGN avant le run, **isolée dans `input/edge_neighbors/`** pour ne pas gonfler le corpus des passes globales (dédupliqué sur tout le lot, `_fetch_edge_neighbors` dans `pipeline.py`) ; introuvable ou échec = bande vide). Les visualisations calculent sur l'emprise étendue puis `rendering.py` (`_core_tile_window`, via `tif_to_crop`/`tif_to_png`) recadre les sorties sur la dalle 1 km exacte lue dans le nom LHD — les AVIF restent des carrés 1 km alignés dans la mosaïque. Tampon consigné dans le tag GeoTIFF `LIDAR_EDGE_BUFFER` du DTM : changer `--edge-buffer` invalide le cache DTM automatiquement (tag absent = 0). Bandes voisines non calées par faisceaux (contexte seul, recadrée hors image finale). Coût : ~7 s de lecture par voisine + ~44 % de pixels en plus à 100 m/0,2 m. Nom hors pattern LHD : option ignorée (bornes d'en-tête, pas de recadrage).
|
||||
- **Raccord des bords entre tuiles** : les rendus à grand noyau (openness/SVF : rayons 100 m ; LRM : 15 m) tronquent leur fenêtre au bord de dalle — bandes d'artefacts à chaque changement de tuile. `--edge-buffer N` (défaut 100 ; toujours appliqué par la génération depuis la carte, `EDGE_BUFFER_METERS` = 100 m dans `mapserve.py`, plus de case à cocher ; 0 = off en ligne de commande) fait rastériser le DTM sur la **dalle nominale 1 km alignée sur la grille** plus une bande de N m remplie avec les points sol des 8 LAZ voisines (`_neighbor_ground_points` dans `dtm.py` : PDAL en flux, découpe + filtre de classes IGN ; voisine absente = téléchargement automatique depuis le catalogue IGN avant le run, **isolée dans `input/edge_neighbors/`** pour ne pas gonfler le corpus des passes globales (dédupliqué sur tout le lot, `_fetch_edge_neighbors` dans `pipeline.py`) ; introuvable ou échec = bande vide). Les visualisations calculent sur l'emprise étendue puis `rendering.py` (`_core_tile_window`, via `tif_to_crop`/`tif_to_png`) recadre les sorties sur la dalle 1 km exacte lue dans le nom LHD — les AVIF restent des carrés 1 km alignés dans la mosaïque. Tampon consigné dans le tag GeoTIFF `LIDAR_EDGE_BUFFER` du DTM : changer `--edge-buffer` invalide le cache DTM automatiquement (tag absent = 0). Bandes voisines non calées par faisceaux (contexte seul, recadrée hors image finale). Coût : ~7 s de lecture par voisine + ~44 % de pixels en plus à 100 m/0,2 m. Nom hors pattern LHD : option ignorée (bornes d'en-tête, pas de recadrage).
|
||||
- **Tests use lazy imports inside each test function**, never at module top, to avoid importing CuPy/GDAL at import time.
|
||||
- **`_`-prefixed names are critical private**: `_create_ground_pipeline`, `_fallback_to_smrf`, `_fill_nans`, `_init_gpu`, `_process_file_standalone` — do not call from outside their module.
|
||||
- **`build_index()` écrit l'inventaire + les paliers sources** : `output/index_tiles.json` (dalles, couches, URLs versionnées — servi par `/api/tiles`), vignettes `index_thumbs/` (≈3,9 m/px + `_mid` 1,56 m/px) et quadrants `index_subtiles/` — paliers de la pyramide XYZ (`tiles.py`). Chaque tuile du run en cours porte ses coins WGS84 pour les cadres de progression. Plus d'HTML : l'interface vit dans `mapui.py`.
|
||||
|
||||
Reference in New Issue
Block a user