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.
|
"""Quick integrity check for a LAZ/LAS file.
|
||||||
|
|
||||||
Tries laspy first (fast header read), then PDAL as fallback for COPC files
|
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:
|
Returns:
|
||||||
True if file is readable, False otherwise.
|
True if file is readable and contains points, False otherwise.
|
||||||
"""
|
"""
|
||||||
# Try laspy first (fast)
|
# Try laspy first (fast)
|
||||||
import laspy
|
import laspy
|
||||||
try:
|
try:
|
||||||
with laspy.open(str(laz_file)) as f:
|
with laspy.open(str(laz_file)) as f:
|
||||||
header = f.header
|
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
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@ -151,6 +155,17 @@ def validate_laz(laz_file):
|
|||||||
capture_output=True, text=True, timeout=30
|
capture_output=True, text=True, timeout=30
|
||||||
)
|
)
|
||||||
if result.returncode == 0:
|
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
|
return True
|
||||||
logger.error(f" ✗ Fichier illisible: {laz_file.name}")
|
logger.error(f" ✗ Fichier illisible: {laz_file.name}")
|
||||||
logger.error(f" PDAL: {result.stderr.strip()[:200]}")
|
logger.error(f" PDAL: {result.stderr.strip()[:200]}")
|
||||||
@ -200,6 +215,9 @@ def _read_with_pdal(laz_file):
|
|||||||
os.unlink(tmp_path)
|
os.unlink(tmp_path)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
if len(las.points) == 0:
|
||||||
|
logger.warning(f" PDAL: conversion réussie mais 0 points")
|
||||||
|
return None
|
||||||
return las
|
return las
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -238,6 +256,10 @@ def detect_ground_method(laz_file):
|
|||||||
return 'smrf'
|
return 'smrf'
|
||||||
|
|
||||||
total_points = len(las.points)
|
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)
|
z = np.array(las.z)
|
||||||
|
|
||||||
# Height variance (always available)
|
# Height variance (always available)
|
||||||
@ -321,6 +343,11 @@ def classify_ground(laz_file, temp_dir, method='auto', force=False):
|
|||||||
["pdal", "pipeline", str(pipeline_file)],
|
["pdal", "pipeline", str(pipeline_file)],
|
||||||
capture_output=True, check=True
|
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")
|
logger.info(f" ✓ Classification sol {method.upper()} terminée")
|
||||||
return output_las
|
return output_las
|
||||||
except subprocess.CalledProcessError as e:
|
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}")
|
logger.error(f" ✗ Impossible de lire {las_file.name}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if len(las.points) == 0:
|
||||||
|
logger.error(f" ✗ Fichier vide (0 points): {las_file.name}")
|
||||||
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
||||||
min_x, max_x = float(las.header.min[0]), float(las.header.max[0])
|
min_x, max_x = float(las.header.min[0]), float(las.header.max[0])
|
||||||
|
|||||||
Reference in New Issue
Block a user