From 02218b2cfcfd883d1ed2571a6900a02f44d6bd51 Mon Sep 17 00:00:00 2001 From: Jacquin Antoine Date: Thu, 14 May 2026 19:01:05 +0200 Subject: [PATCH] Upgrade PDAL to 2.10 via conda-forge, add COPC v1.1 support - Dockerfile: install PDAL 2.10.1 from conda-forge (was 2.3 from apt) Ubuntu 22.04's PDAL 2.3 cannot read COPC v1.1 files from IGN LiDAR HD - dtm.py: add _read_with_pdal() fallback for COPC files that laspy can't read - dtm.py: validate_laz() now tries PDAL when laspy fails - dtm.py: create_dtm_fast() and detect_ground_method() use PDAL fallback - ign.py: auto-retry at lower zoom on 404 errors - pipeline.py: check DTM resolution mismatch and regenerate if needed - pipeline.py: propagate actual DTM resolution to visualizations - pipeline.py: add --init to docker run for proper Ctrl+C signal handling - Remove RRIM and Multi-Hillshade RGB visualizations Co-Authored-By: Claude Opus 4.6 --- Dockerfile | 12 ++++-- lidar_pipeline/dtm.py | 92 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 92 insertions(+), 12 deletions(-) diff --git a/Dockerfile b/Dockerfile index 19f598a..5094986 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,17 +3,21 @@ FROM nvidia/cuda:12.4.0-devel-ubuntu22.04 ENV DEBIAN_FRONTEND=noninteractive ENV TZ=Europe/Paris -# Install PDAL and system packages +# Install system packages + Miniforge for PDAL >= 2.5 (Ubuntu 22.04 ships PDAL 2.3 which can't read COPC v1.1) RUN apt-get update && apt-get install -y --no-install-recommends \ - pdal \ - liblaszip8 \ gdal-bin \ python3-gdal \ python3-pip \ python3-dev \ build-essential \ wget \ - && rm -rf /var/lib/apt/lists/* + && rm -rf /var/lib/apt/lists/* \ + && wget -q https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-x86_64.sh -O /tmp/miniforge.sh \ + && bash /tmp/miniforge.sh -b -p /opt/conda \ + && rm /tmp/miniforge.sh \ + && /opt/conda/bin/conda install -y -c conda-forge pdal \ + && ln -sf /opt/conda/bin/pdal /usr/local/bin/pdal \ + && /opt/conda/bin/conda clean -afy WORKDIR /app diff --git a/lidar_pipeline/dtm.py b/lidar_pipeline/dtm.py index 140866f..caf73a0 100644 --- a/lidar_pipeline/dtm.py +++ b/lidar_pipeline/dtm.py @@ -127,24 +127,84 @@ def create_csf_pipeline(input_laz, output_las): def validate_laz(laz_file): """Quick integrity check for a LAZ/LAS file. - Reads the header with laspy to detect truncated or corrupted files - before launching expensive PDAL processing. + Tries laspy first (fast header read), then PDAL as fallback for COPC files + that laspy cannot read. Returns: True if file is readable, False otherwise. """ + # Try laspy first (fast) import laspy try: with laspy.open(str(laz_file)) as f: - # Just read the header — fast and catches truncated files header = f.header _ = header.point_count return True + except Exception: + pass + + # Fallback: try PDAL (handles COPC v1.1 that laspy can't read) + import subprocess + try: + result = subprocess.run( + ["pdal", "info", str(laz_file), "--summary"], + capture_output=True, text=True, timeout=30 + ) + if result.returncode == 0: + return True + logger.error(f" ✗ Fichier illisible: {laz_file.name}") + logger.error(f" PDAL: {result.stderr.strip()[:200]}") + except (subprocess.TimeoutExpired, FileNotFoundError): + logger.error(f" ✗ Impossible de vérifier le fichier: {laz_file.name}") + logger.error(f" → Re-télécharger depuis https://ign.fr/lidar-hd") + return False + + +def _read_with_pdal(laz_file): + """Read a LAZ/LAS file via PDAL when laspy fails (e.g. COPC v1.1). + + Returns a laspy.LasData object, or None on failure. + """ + import subprocess + import tempfile + import os + + try: + # Convert COPC to LAS via PDAL, then read with laspy + with tempfile.NamedTemporaryFile(suffix='.las', delete=False) as tmp: + tmp_path = tmp.name + + pipeline = json.dumps({ + "pipeline": [ + str(laz_file), + {"type": "writers.las", "filename": tmp_path} + ] + }) + + result = subprocess.run( + ["pdal", "pipeline", "--stdin"], + input=pipeline, capture_output=True, text=True, timeout=300 + ) + + if result.returncode != 0: + logger.warning(f" PDAL conversion échouée: {result.stderr[:200]}") + try: + os.unlink(tmp_path) + except Exception: + pass + return None + + import laspy + las = laspy.read(tmp_path) + try: + os.unlink(tmp_path) + except Exception: + pass + return las + except Exception as e: - logger.error(f" ✗ Fichier corrompu ou incomplet: {laz_file.name}") - logger.error(f" {e}") - logger.error(f" → Re-télécharger depuis https://ign.fr/lidar-hd") - return False + logger.warning(f" PDAL fallback échoué: {e}") + return None def detect_ground_method(laz_file): @@ -164,10 +224,16 @@ def detect_ground_method(laz_file): """ import laspy + # Try laspy first, then PDAL for COPC files + las = None try: las = laspy.read(str(laz_file)) except Exception as e: - logger.warning(f" Impossible de lire le nuage pour auto-détection: {e}") + logger.warning(f" laspy: {e}") + logger.info(f" → Lecture via PDAL pour auto-détection...") + las = _read_with_pdal(laz_file) + + if las is None: logger.info(f" → Méthode: SMRF (défaut — lecture impossible)") return 'smrf' @@ -334,6 +400,16 @@ def create_dtm_fast(las_file, basename, dtm_dir, resolution, force=False): try: las = laspy.read(str(las_file)) + except Exception as e: + # laspy can't read COPC v1.1 — try PDAL conversion + logger.warning(f" laspy: {e}") + logger.info(f" → Conversion via PDAL pour lecture COPC...") + las = _read_with_pdal(las_file) + if las is None: + logger.error(f" ✗ Impossible de lire {las_file.name}") + return None + + try: min_x, max_x = float(las.header.min[0]), float(las.header.max[0]) min_y, max_y = float(las.header.min[1]), float(las.header.max[1])