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 <noreply@anthropic.com>
This commit is contained in:
Jacquin Antoine
2026-05-14 19:01:05 +02:00
parent 7ac08f75dc
commit 02218b2cfc
2 changed files with 92 additions and 12 deletions

View File

@ -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

View File

@ -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])