Écrire le sidecar qualité aussi quand le DTM primaire vient du cache
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@ -589,6 +589,17 @@ class LidarArchaeoPipeline:
|
|||||||
logger.info(f"[1/5] Classification du sol — sautée (DTM existant)")
|
logger.info(f"[1/5] Classification du sol — sautée (DTM existant)")
|
||||||
logger.info(f"[2/5] Génération DTM {res}m/px — sautée (DTM existant)")
|
logger.info(f"[2/5] Génération DTM {res}m/px — sautée (DTM existant)")
|
||||||
self._report(basename, "classif", "skip", "DTM existant")
|
self._report(basename, "classif", "skip", "DTM existant")
|
||||||
|
# Sidecar qualité (densité sol, dates de vol) pour
|
||||||
|
# l'encart de l'export PDF : DTM réutilisé depuis le
|
||||||
|
# cache — la classification sol a été sautée, donc
|
||||||
|
# mesure directement sur le LAZ d'entrée avec les
|
||||||
|
# classes IGN du run (ensure_quality ne recalcule
|
||||||
|
# rien si le sidecar existe déjà : coût en régime
|
||||||
|
# établi = une lecture JSON).
|
||||||
|
from .dtm import parse_ign_classes
|
||||||
|
from .quality import ensure_quality
|
||||||
|
ensure_quality(laz_file, basename, self.output_dir,
|
||||||
|
codes=tuple(parse_ign_classes(self.ign_classes)))
|
||||||
else:
|
else:
|
||||||
logger.info(f" DTM {res}m/px déjà existant — ignoré")
|
logger.info(f" DTM {res}m/px déjà existant — ignoré")
|
||||||
self._report(basename, "dtm", "skip", res=res)
|
self._report(basename, "dtm", "skip", res=res)
|
||||||
@ -651,7 +662,9 @@ class LidarArchaeoPipeline:
|
|||||||
|
|
||||||
if i == 0:
|
if i == 0:
|
||||||
# Sidecar qualité (densité sol, dates de vol) pour l'encart de
|
# Sidecar qualité (densité sol, dates de vol) pour l'encart de
|
||||||
# l'export PDF : calculé une fois, même si le DTM vient du cache.
|
# l'export PDF : DTM fraîchement généré — mesure sur le LAS sol
|
||||||
|
# déjà filtré (le cas DTM réutilisé depuis le cache est couvert
|
||||||
|
# plus haut, avant le `continue`).
|
||||||
from .quality import ensure_quality
|
from .quality import ensure_quality
|
||||||
ensure_quality(las_file, basename, self.output_dir)
|
ensure_quality(las_file, basename, self.output_dir)
|
||||||
|
|
||||||
|
|||||||
@ -287,6 +287,77 @@ class TestEffectiveGroundMethod:
|
|||||||
assert p._effective_ground_method() == "smrf"
|
assert p._effective_ground_method() == "smrf"
|
||||||
|
|
||||||
|
|
||||||
|
class TestQualityCacheHit:
|
||||||
|
"""Sidecar qualité écrit même quand le DTM primaire est réutilisé depuis
|
||||||
|
le cache (classification sol sautée, pas de LAS sol disponible)."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _write_las(path, x, y, cls, t):
|
||||||
|
import laspy
|
||||||
|
import numpy as np
|
||||||
|
header = laspy.LasHeader(point_format=6, version="1.4")
|
||||||
|
header.scales = [0.01, 0.01, 0.01]
|
||||||
|
header.offsets = [652000.0, 6861000.0, 0.0]
|
||||||
|
header.global_encoding.gps_time_type = laspy.header.GpsTimeType.STANDARD
|
||||||
|
las = laspy.LasData(header)
|
||||||
|
las.x = np.asarray(x, float); las.y = np.asarray(y, float)
|
||||||
|
las.z = np.zeros(len(x))
|
||||||
|
las.classification = np.asarray(cls, np.uint8)
|
||||||
|
las.return_number = np.ones(len(x), np.uint8)
|
||||||
|
las.number_of_returns = np.ones(len(x), np.uint8)
|
||||||
|
las.gps_time = np.asarray(t, float)
|
||||||
|
las.write(str(path))
|
||||||
|
|
||||||
|
def test_cache_hit_writes_quality_sidecar(self, tmp_path, monkeypatch):
|
||||||
|
import numpy as np
|
||||||
|
import rasterio
|
||||||
|
from rasterio.transform import from_bounds
|
||||||
|
from lidar_pipeline.pipeline import LidarArchaeoPipeline
|
||||||
|
import lidar_pipeline.pipeline as pipeline_mod
|
||||||
|
from lidar_pipeline.dtm import GAP_FILL_TAG, GAP_FILL_VERSION
|
||||||
|
from lidar_pipeline.quality import read_quality
|
||||||
|
|
||||||
|
input_dir = tmp_path / "input"
|
||||||
|
input_dir.mkdir()
|
||||||
|
base = "LHD_FXX_0652_6862_PTS_LAMB93_IGN69"
|
||||||
|
laz = input_dir / f"{base}.laz"
|
||||||
|
import datetime
|
||||||
|
epoch = datetime.datetime(1980, 1, 6, tzinfo=datetime.timezone.utc)
|
||||||
|
gps = (datetime.datetime(2022, 6, 1, 12, tzinfo=datetime.timezone.utc) - epoch).total_seconds() - 1e9
|
||||||
|
self._write_las(laz, [652100, 652200, 652300], [6861100, 6861200, 6861300],
|
||||||
|
[2, 2, 6], [gps, gps, gps])
|
||||||
|
|
||||||
|
pipeline = LidarArchaeoPipeline(
|
||||||
|
str(input_dir), str(tmp_path / "output"), ground_method='ign',
|
||||||
|
ign_classes="sol", strip_align=False, edge_buffer=0.0)
|
||||||
|
pipeline.viz_steps = [] # aucune visualisation à calculer (hors périmètre)
|
||||||
|
|
||||||
|
# DTM déjà en cache, compatible avec la config du run (pas de calage,
|
||||||
|
# pas de raccord, comblement à la version courante) : le run doit
|
||||||
|
# emprunter la branche « DTM existant » sans reclassifier ni régénérer.
|
||||||
|
dtm_path = pipeline.dtm_dir / f"{base}_dtm.tif"
|
||||||
|
with rasterio.open(
|
||||||
|
dtm_path, "w", driver="GTiff", height=10, width=10, count=1,
|
||||||
|
dtype="float32", crs="EPSG:2154",
|
||||||
|
transform=from_bounds(652000.0, 6861995.0, 652005.0, 6862000.0, 10, 10),
|
||||||
|
) as dst:
|
||||||
|
dst.write(np.zeros((10, 10), dtype="float32"), 1)
|
||||||
|
dst.update_tags(**{GAP_FILL_TAG: GAP_FILL_VERSION})
|
||||||
|
|
||||||
|
def boom(*a, **k):
|
||||||
|
raise AssertionError("cache hit attendu : ne doit pas reclassifier/régénérer le DTM")
|
||||||
|
|
||||||
|
monkeypatch.setattr(pipeline_mod, "classify_ground", boom)
|
||||||
|
monkeypatch.setattr(pipeline_mod, "create_dtm_fast", boom)
|
||||||
|
|
||||||
|
assert pipeline.process_file(laz) is True
|
||||||
|
|
||||||
|
data = read_quality(pipeline.output_dir, base)
|
||||||
|
assert data is not None
|
||||||
|
assert abs(data["ground_density"] - 2e-6) < 1e-9 # 2 points de classe 2 sur 1 km²
|
||||||
|
assert data["acq_start"] == "2022-06-01" and data["acq_source"] == "gps"
|
||||||
|
|
||||||
|
|
||||||
class TestResolveWorkers:
|
class TestResolveWorkers:
|
||||||
def test_auto_scales_with_cpus(self):
|
def test_auto_scales_with_cpus(self):
|
||||||
from lidar_pipeline.pipeline import resolve_workers
|
from lidar_pipeline.pipeline import resolve_workers
|
||||||
|
|||||||
Reference in New Issue
Block a user