Handle empty point clouds gracefully at every pipeline stage
Fixes "zero-size array to reduction operation" crash on corrupt/incomplete LAZ files. Added checks at each step: - validate_laz(): check point_count > 0 via laspy header, parse PDAL info JSON for point count when using PDAL fallback - detect_ground_method(): return 'smrf' default if point cloud is empty after PDAL conversion instead of crashing on np.max(empty_array) - _read_with_pdal(): log warning and return None if converted file has 0 points - create_dtm_fast(): fail gracefully if ground file has 0 points - classify_ground(): check output file size after PDAL pipeline to catch empty ground classifications early Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@ -128,17 +128,21 @@ def validate_laz(laz_file):
|
||||
"""Quick integrity check for a LAZ/LAS file.
|
||||
|
||||
Tries laspy first (fast header read), then PDAL as fallback for COPC files
|
||||
that laspy cannot read.
|
||||
that laspy cannot read. Also checks that the file contains points.
|
||||
|
||||
Returns:
|
||||
True if file is readable, False otherwise.
|
||||
True if file is readable and contains points, False otherwise.
|
||||
"""
|
||||
# Try laspy first (fast)
|
||||
import laspy
|
||||
try:
|
||||
with laspy.open(str(laz_file)) as f:
|
||||
header = f.header
|
||||
_ = header.point_count
|
||||
point_count = header.point_count
|
||||
if point_count == 0:
|
||||
logger.error(f" ✗ Fichier vide (0 points): {laz_file.name}")
|
||||
logger.error(f" → Re-télécharger depuis https://ign.fr/lidar-hd")
|
||||
return False
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
@ -151,6 +155,17 @@ def validate_laz(laz_file):
|
||||
capture_output=True, text=True, timeout=30
|
||||
)
|
||||
if result.returncode == 0:
|
||||
# Check point count from PDAL info output
|
||||
import json as _json
|
||||
try:
|
||||
info = _json.loads(result.stdout)
|
||||
count = info.get('summary', {}).get('num_points', 0)
|
||||
if count == 0:
|
||||
logger.error(f" ✗ Fichier vide (0 points PDAL): {laz_file.name}")
|
||||
logger.error(f" → Re-télécharger depuis https://ign.fr/lidar-hd")
|
||||
return False
|
||||
except Exception:
|
||||
pass # Can't parse — assume valid
|
||||
return True
|
||||
logger.error(f" ✗ Fichier illisible: {laz_file.name}")
|
||||
logger.error(f" PDAL: {result.stderr.strip()[:200]}")
|
||||
@ -200,6 +215,9 @@ def _read_with_pdal(laz_file):
|
||||
os.unlink(tmp_path)
|
||||
except Exception:
|
||||
pass
|
||||
if len(las.points) == 0:
|
||||
logger.warning(f" PDAL: conversion réussie mais 0 points")
|
||||
return None
|
||||
return las
|
||||
|
||||
except Exception as e:
|
||||
@ -238,6 +256,10 @@ def detect_ground_method(laz_file):
|
||||
return 'smrf'
|
||||
|
||||
total_points = len(las.points)
|
||||
if total_points == 0:
|
||||
logger.warning(f" Nuage vide (0 points) — méthode par défaut: SMRF")
|
||||
return 'smrf'
|
||||
|
||||
z = np.array(las.z)
|
||||
|
||||
# Height variance (always available)
|
||||
@ -321,6 +343,11 @@ def classify_ground(laz_file, temp_dir, method='auto', force=False):
|
||||
["pdal", "pipeline", str(pipeline_file)],
|
||||
capture_output=True, check=True
|
||||
)
|
||||
# Verify that ground file has points
|
||||
if output_las.exists() and output_las.stat().st_size < 100:
|
||||
logger.error(f" ✗ Fichier ground vide (taille < 100 octets)")
|
||||
output_las.unlink(missing_ok=True)
|
||||
return None
|
||||
logger.info(f" ✓ Classification sol {method.upper()} terminée")
|
||||
return output_las
|
||||
except subprocess.CalledProcessError as e:
|
||||
@ -409,6 +436,10 @@ def create_dtm_fast(las_file, basename, dtm_dir, resolution, force=False):
|
||||
logger.error(f" ✗ Impossible de lire {las_file.name}")
|
||||
return None
|
||||
|
||||
if len(las.points) == 0:
|
||||
logger.error(f" ✗ Fichier vide (0 points): {las_file.name}")
|
||||
return None
|
||||
|
||||
try:
|
||||
|
||||
min_x, max_x = float(las.header.min[0]), float(las.header.max[0])
|
||||
|
||||
Reference in New Issue
Block a user