Isoler les dalles voisines de raccord dans input/edge_neighbors/

This commit is contained in:
Antoine Jacquin
2026-09-19 23:43:50 +02:00
parent 5ef0232c80
commit 3236d56a4b
4 changed files with 50 additions and 12 deletions

View File

@ -1043,8 +1043,14 @@ def _interpolate_holes(dtm, downsample=8):
# images finales sont recadrées sur la dalle exacte (cf. rendering.py).
EDGE_BUFFER_TAG = "LIDAR_EDGE_BUFFER" # tag GeoTIFF : tampon utilisé (m)
# Sous-dossier de input/ où sont isolées les dalles voisines téléchargées
# pour le seul raccord des bords : elles ne font pas partie du corpus de
# tuiles à rendre (les scans globaux n'énumèrent que input/ à plat).
EDGE_NEIGHBORS_DIRNAME = "edge_neighbors"
# Décalages des 8 voisines d'une dalle (col, row) en km
_NEIGHBOR_OFFSETS = [(-1, -1), (0, -1), (1, -1), (-1, 0),
(1, 0), (-1, 1), (0, 1), (1, 1)]
@ -1055,12 +1061,19 @@ def _tile_coords(name):
def _neighbor_laz_files(source_laz):
"""Liste les 8 fichiers LAZ/LAS adjacents à `source_laz` dans son dossier."""
"""Liste les 8 fichiers LAZ/LAS adjacents à `source_laz`.
Recherche dans le dossier de la source, puis dans le sous-dossier
edge_neighbors/ : les dalles voisines téléchargées pour le seul raccord
des bords y sont isolées — sinon elles s'accumulent à plat dans input/ et
chaque passe globale annexe un anneau de tuiles à rendre en plus.
"""
coords = _tile_coords(source_laz)
if coords is None:
return []
col, row = coords
directory = Path(source_laz).parent
search_dirs = [directory, directory / EDGE_NEIGHBORS_DIRNAME]
neighbors = []
for dcol, drow in _NEIGHBOR_OFFSETS:
nc, nr = col + dcol, row + drow
@ -1068,9 +1081,10 @@ def _neighbor_laz_files(source_laz):
# on accepte aussi la variante sans remplissage pour les dalles exotiques.
names = {f"LHD_FXX_{nc:04d}_{nr:04d}", f"LHD_FXX_{nc}_{nr}"}
matches = []
for name in names:
matches += list(directory.glob(f"{name}_*.las"))
matches += list(directory.glob(f"{name}_*.laz"))
for search_dir in search_dirs:
for name in names:
matches += list(search_dir.glob(f"{name}_*.las"))
matches += list(search_dir.glob(f"{name}_*.laz"))
if matches:
neighbors.append(sorted(matches)[0])
else:

View File

@ -456,17 +456,20 @@ class LidarArchaeoPipeline:
"""Télécharge les dalles LAZ voisines manquantes (raccord des bords).
La bande de raccord lit les 8 LAZ adjacentes de chaque tuile ; celles
absentes de input/ sont téléchargées depuis le catalogue IGN avant de
lancer les workers, pour que la bande soit remplie jusqu'au bord de la
zone même pour des tuiles jamais rendues. La liste est dédupliquée sur
absentes sont téléchargées depuis le catalogue IGN avant de lancer
les workers, dans le sous-dossier input/edge_neighbors/ : elles ne
doivent PAS rejoindre input/ à plat, sinon les passes globales
(« tout input/ ») les comptent comme des tuiles à rendre et la zone
grandit d'un anneau à chaque relance. La liste est dédupliquée sur
tout le lot : la couronne d'un bloc contigu ne coûte qu'un passage.
Une dalle introuvable (zone non publiée) ou en échec laisse simplement
la bande vide — le rendu continue (best-effort).
"""
if self.edge_buffer <= 0:
return
from .dtm import _tile_coords, _NEIGHBOR_OFFSETS
from .dtm import _tile_coords, _NEIGHBOR_OFFSETS, EDGE_NEIGHBORS_DIRNAME
from .fetch_ign import fetch_tiles, tile_filename
edge_dir = self.input_dir / EDGE_NEIGHBORS_DIRNAME
wanted = set()
for laz_file in files:
coords = _tile_coords(Path(laz_file).name)
@ -477,16 +480,19 @@ class LidarArchaeoPipeline:
wanted.add((col + dcol, row + drow))
missing = sorted(
cr for cr in wanted
if not (self.input_dir / tile_filename(cr[0], cr[1])).exists())
if not (self.input_dir / tile_filename(cr[0], cr[1])).exists()
and not (edge_dir / tile_filename(cr[0], cr[1])).exists())
if not missing:
return
edge_dir.mkdir(parents=True, exist_ok=True)
logger.info(f"Raccord des bords : {len(missing)} dalle(s) voisine(s) "
f"absente(s) de input/ — téléchargement depuis le catalogue IGN")
f"absente(s) — téléchargement dans {EDGE_NEIGHBORS_DIRNAME}/ "
f"(catalogue IGN)")
t0 = time.time()
from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=4) as pool:
fetched = [path for batch in pool.map(
lambda spec: fetch_tiles(self.input_dir, [spec]), missing)
lambda spec: fetch_tiles(edge_dir, [spec]), missing)
for path in batch]
logger.info(f"Raccord des bords : {len(fetched)}/{len(missing)} dalle(s) "
f"voisine(s) téléchargée(s) ({time.time() - t0:.0f} s)")

View File

@ -720,6 +720,24 @@ class TestEdgeBuffer:
src.touch()
assert _neighbor_laz_files(src) == []
def test_neighbor_found_in_edge_subdir(self, tmp_output_dir):
"""Une voisine isolée dans edge_neighbors/ est trouvée ; la priorité
reste à une dalle à plat dans input/."""
from lidar_pipeline.dtm import _neighbor_laz_files, EDGE_NEIGHBORS_DIRNAME
base = "LHD_FXX_0637_6627_PTS_LAMB93_IGN69"
(tmp_output_dir / f"{base}.copc.laz").touch()
edge = tmp_output_dir / EDGE_NEIGHBORS_DIRNAME
edge.mkdir()
# Voisine uniquement dans le sous-dossier de raccord
(edge / "LHD_FXX_0638_6628_PTS_LAMB93_IGN69.copc.laz").touch()
# Voisine présente aux deux endroits : la version input/ gagne
(edge / "LHD_FXX_0636_6626_PTS_LAMB93_IGN69.copc.laz").touch()
(tmp_output_dir / "LHD_FXX_0636_6626_PTS_LAMB93_IGN69.copc.laz").touch()
found = _neighbor_laz_files(tmp_output_dir / f"{base}.copc.laz")
by_name = {f.name: f for f in found}
assert by_name["LHD_FXX_0638_6628_PTS_LAMB93_IGN69.copc.laz"].parent == edge
assert by_name["LHD_FXX_0636_6626_PTS_LAMB93_IGN69.copc.laz"].parent == tmp_output_dir
def test_buffered_dtm_extends_into_neighbor(self, tmp_output_dir):
"""MNT 24x24 (dalle 20x20 + bande 100 m), bande EST remplie à z=20 par la voisine."""
from lidar_pipeline.dtm import create_dtm_fast, read_dtm_edge_buffer, EDGE_BUFFER_TAG