Fix corrupted COPC detection, add CSF→SMRF fallback, improve MSRM colormap, add SVF and anisotropic openness
- validate_laz: verify point data accessibility (not just headers) to detect corrupted COPC files that pass header checks but fail on data reads - classify_ground: fallback from CSF to SMRF when CSF produces no ground points or PDAL errors (fixes 2/9 failing tiles) - MSRM: preserve sign in weighted combination so RdBu_r colormap shows both red (elevated) and blue (depressed) instead of red only - Add Sky-View Factor (SVF) visualization: cos²(horizon angle) over 16 directions, excellent for archaeological earthwork detection - Add Anisotropic Openness: directional weighting (NW-SE/NE-SW) enhances linear feature detection aligned with common settlement patterns - Remove anomalies and flow visualizations (replaced by SVF + aniso_open) - Location inset: use IGN topographic map at zoom 10 instead of simplified France outline, with red rectangle marker and fallback - Remove flow (hydrological accumulation) from VIZ_STEPS
This commit is contained in:
@ -125,15 +125,15 @@ def create_csf_pipeline(input_laz, output_las):
|
||||
|
||||
|
||||
def validate_laz(laz_file):
|
||||
"""Quick integrity check for a LAZ/LAS file.
|
||||
"""Integrity check for a LAZ/LAS file.
|
||||
|
||||
Tries laspy first (fast header read), then PDAL as fallback for COPC files
|
||||
that laspy cannot read. Also checks that the file contains points.
|
||||
Verifies that both the header AND point data are readable. Some corrupted
|
||||
COPC files have valid headers but inaccessible point data (LazrsError:
|
||||
failed to fill whole buffer). Such files must be re-downloaded.
|
||||
|
||||
Returns:
|
||||
True if file is readable and contains points, False otherwise.
|
||||
True if file is readable and contains accessible points, False otherwise.
|
||||
"""
|
||||
# Try laspy first (fast)
|
||||
import laspy
|
||||
try:
|
||||
with laspy.open(str(laz_file)) as f:
|
||||
@ -143,6 +143,15 @@ def validate_laz(laz_file):
|
||||
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
|
||||
# Verify point data is actually accessible (not just header metadata)
|
||||
try:
|
||||
for _ in f.chunk_iterator(1000):
|
||||
break # Read just one chunk to confirm data integrity
|
||||
except Exception as chunk_err:
|
||||
logger.error(f" ✗ Données inaccessibles (fichier corrompu?): {laz_file.name}")
|
||||
logger.error(f" Erreur: {chunk_err}")
|
||||
logger.error(f" → Re-télécharger depuis https://ign.fr/lidar-hd")
|
||||
return False
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
@ -166,6 +175,18 @@ def validate_laz(laz_file):
|
||||
return False
|
||||
except Exception:
|
||||
pass # Can't parse — assume valid
|
||||
# Verify PDAL can actually read point data (not just header)
|
||||
try:
|
||||
test_result = subprocess.run(
|
||||
["pdal", "info", str(laz_file), "--point", "1"],
|
||||
capture_output=True, text=True, timeout=60
|
||||
)
|
||||
if test_result.returncode != 0:
|
||||
logger.error(f" ✗ Données inaccessibles (PDAL): {laz_file.name}")
|
||||
logger.error(f" → Re-télécharger depuis https://ign.fr/lidar-hd")
|
||||
return False
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
pass # Timeout — assume valid, will fail later if corrupted
|
||||
return True
|
||||
logger.error(f" ✗ Fichier illisible: {laz_file.name}")
|
||||
logger.error(f" PDAL: {result.stderr.strip()[:200]}")
|
||||
@ -347,6 +368,9 @@ def classify_ground(laz_file, temp_dir, method='auto', force=False):
|
||||
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)
|
||||
# Fallback: if CSF produced no ground points, retry with SMRF
|
||||
if method == 'csf':
|
||||
return _fallback_to_smrf(laz_file, temp_dir, laz_base, force)
|
||||
return None
|
||||
logger.info(f" ✓ Classification sol {method.upper()} terminée")
|
||||
return output_las
|
||||
@ -354,6 +378,10 @@ def classify_ground(laz_file, temp_dir, method='auto', force=False):
|
||||
error_msg = e.stderr.decode() if e.stderr else str(e)
|
||||
logger.warning(f" ✗ Erreur classification PDAL ({method.upper()}): {error_msg}")
|
||||
|
||||
# Fallback: if CSF failed, retry with SMRF
|
||||
if method == 'csf':
|
||||
return _fallback_to_smrf(laz_file, temp_dir, laz_base, force)
|
||||
|
||||
# Try repairing file with laspy if PDAL fails on EVLR/VLR
|
||||
if 'VLR' in error_msg or 'Invalid' in error_msg:
|
||||
logger.info(f" → Tentative de réparation du fichier avec laspy...")
|
||||
@ -378,6 +406,58 @@ def classify_ground(laz_file, temp_dir, method='auto', force=False):
|
||||
return None
|
||||
|
||||
|
||||
def _fallback_to_smrf(laz_file, temp_dir, laz_base, force=False):
|
||||
"""Retry ground classification with SMRF when CSF fails.
|
||||
|
||||
CSF (Cloth Simulation Filter) can fail on certain terrain types where
|
||||
SMRF (Simple Morphological Filter) succeeds. This fallback ensures
|
||||
processing continues even when auto-detection selects CSF incorrectly.
|
||||
|
||||
Args:
|
||||
laz_file: Path to input LAZ/LAS file.
|
||||
temp_dir: Directory for temporary files.
|
||||
laz_base: Base name for the file.
|
||||
force: If True, reclassify even if output exists.
|
||||
|
||||
Returns:
|
||||
Path to classified ground LAS file, or None on failure.
|
||||
"""
|
||||
logger.info(f" → Basculement CSF → SMRF (fallback)")
|
||||
|
||||
# Clean up failed CSF output if it exists
|
||||
csf_output = temp_dir / f"{laz_base}_ground_csf.las"
|
||||
if csf_output.exists():
|
||||
csf_output.unlink(missing_ok=True)
|
||||
|
||||
output_las = temp_dir / f"{laz_base}_ground_smrf.las"
|
||||
|
||||
if output_las.exists() and not force:
|
||||
logger.info(f" Classification SMRF déjà existante — fichier réutilisé")
|
||||
return output_las
|
||||
|
||||
pipeline_json = _create_ground_pipeline(laz_file, output_las, 'smrf')
|
||||
pipeline_file = temp_dir / "pipeline_smrf.json"
|
||||
|
||||
with open(pipeline_file, 'w') as f:
|
||||
f.write(pipeline_json)
|
||||
|
||||
try:
|
||||
subprocess.run(
|
||||
["pdal", "pipeline", str(pipeline_file)],
|
||||
capture_output=True, check=True
|
||||
)
|
||||
if output_las.exists() and output_las.stat().st_size < 100:
|
||||
logger.error(f" ✗ Fichier ground SMRF vide (taille < 100 octets)")
|
||||
output_las.unlink(missing_ok=True)
|
||||
return None
|
||||
logger.info(f" ✓ Classification sol SMRF terminée (fallback depuis CSF)")
|
||||
return output_las
|
||||
except subprocess.CalledProcessError as e:
|
||||
error_msg = e.stderr.decode() if e.stderr else str(e)
|
||||
logger.error(f" ✗ Échec classification SMRF (fallback): {error_msg}")
|
||||
return None
|
||||
|
||||
|
||||
def _repair_laz_with_laspy(input_laz, output_las):
|
||||
"""Try to repair a corrupt LAZ file by re-reading with laspy and saving as LAS.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user