Layout uniforme WebP: axes fixes + aspect='equal' pour superposition géolocalisée
- Positions d'axes fixes (data_left/bottom/width/height_frac) pour alignement pixel-parfait entre terrain et ortho/topo - aspect='equal' au lieu de 'auto' pour conserver les proportions géographiques - Colorbar descriptive pour les visualisations RGB (ortho/topo) - Comblage des petits trous DTM (< 1m) via rasterio.fill.fillnodata - Suppression de la visualisation "dépressions" - Hillshade composite: 0.7*hillshade + 0.3*cos(slope) - D8 flow accumulation accéléré par numba JIT (fallback Python) - Flag --keep-tif pour conserver les TIFF intermédiaires - --force supprime aussi les TIF existants avant régénération - ETA affiché pendant la génération des visualisations - Répertoires temp dans temp/ pour traitement parallèle Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@ -3,7 +3,7 @@
|
||||
LidarArchaeoPipeline coordinates the full processing chain:
|
||||
1. Ground classification (PDAL/SMRF)
|
||||
2. DTM generation
|
||||
3. Visualization generation (18 products)
|
||||
3. Visualization generation (17 products)
|
||||
4. Rendering (WebP + PDF report)
|
||||
"""
|
||||
|
||||
@ -57,7 +57,7 @@ from .dtm import classify_ground, create_dtm_fast
|
||||
from .visualizations import (
|
||||
generate_hillshade, generate_slope, generate_aspect, generate_curvature,
|
||||
generate_lrm, generate_svf, generate_openness,
|
||||
generate_mslrm, generate_tpi, generate_depressions, generate_sailore,
|
||||
generate_mslrm, generate_tpi, generate_sailore,
|
||||
generate_roughness, generate_anomalies, generate_wavelet,
|
||||
generate_flow,
|
||||
)
|
||||
@ -80,7 +80,6 @@ VIZ_STEPS = [
|
||||
('neg_open', lambda d, b, v, r: generate_openness(d, b, v, r, positive=False)),
|
||||
('mslrm', generate_mslrm),
|
||||
('tpi', generate_tpi),
|
||||
('depressions', generate_depressions),
|
||||
('sailore', generate_sailore),
|
||||
('roughness', generate_roughness),
|
||||
('anomalies', generate_anomalies),
|
||||
@ -106,7 +105,7 @@ VIZ_STEPS = [
|
||||
class LidarArchaeoPipeline:
|
||||
"""Orchestrates the LiDAR archaeological analysis pipeline."""
|
||||
|
||||
def __init__(self, input_dir, output_dir, resolution=0.5, workers=1, force=False, ground_method='auto', force_classify=False):
|
||||
def __init__(self, input_dir, output_dir, resolution=0.5, workers=1, force=False, ground_method='auto', force_classify=False, keep_tif=False):
|
||||
self.input_dir = Path(input_dir)
|
||||
self.output_dir = Path(output_dir)
|
||||
self.resolution = resolution
|
||||
@ -114,6 +113,7 @@ class LidarArchaeoPipeline:
|
||||
self.force = force
|
||||
self.ground_method = ground_method
|
||||
self.force_classify = force_classify
|
||||
self.keep_tif = keep_tif
|
||||
self.temp_dir = self.output_dir / "temp"
|
||||
|
||||
if not self.input_dir.exists():
|
||||
@ -137,6 +137,7 @@ class LidarArchaeoPipeline:
|
||||
logger.info(f" Force : {'OUI' if self.force else 'non (skip existing)'}")
|
||||
logger.info(f" Classification sol : {self.ground_method}")
|
||||
logger.info(f" Force classif.: {'OUI' if self.force_classify else 'non'}")
|
||||
logger.info(f" Keep TIFF : {'OUI' if self.keep_tif else 'non'}")
|
||||
|
||||
def find_laz_files(self):
|
||||
"""Find all LAZ/LAS files in input directory."""
|
||||
@ -171,8 +172,24 @@ class LidarArchaeoPipeline:
|
||||
|
||||
vis_results = {}
|
||||
total = len(VIZ_STEPS)
|
||||
elapsed_times = []
|
||||
|
||||
for idx, (name, func) in enumerate(VIZ_STEPS, 1):
|
||||
# When --force, delete existing TIF to ensure clean regeneration
|
||||
if self.force:
|
||||
for tif in file_vis_dir.glob(f"{basename}_{name}.tif"):
|
||||
tif.unlink(missing_ok=True)
|
||||
# Special cases for differently-named TIFs
|
||||
if name == 'pos_open':
|
||||
for tif in file_vis_dir.glob(f"{basename}_positive_openness.tif"):
|
||||
tif.unlink(missing_ok=True)
|
||||
elif name == 'neg_open':
|
||||
for tif in file_vis_dir.glob(f"{basename}_negative_openness.tif"):
|
||||
tif.unlink(missing_ok=True)
|
||||
elif name == 'hillshade':
|
||||
for tif in file_vis_dir.glob(f"{basename}_hillshade_multi.tif"):
|
||||
tif.unlink(missing_ok=True)
|
||||
|
||||
# Check if output WebP already exists (skip unless --force)
|
||||
if not self.force:
|
||||
# Determine expected WebP filename from the viz name
|
||||
@ -199,8 +216,14 @@ class LidarArchaeoPipeline:
|
||||
result = func(dtm_file, basename, file_vis_dir, self.resolution)
|
||||
vis_results[name] = result
|
||||
elapsed = time.time() - t0
|
||||
elapsed_times.append(elapsed)
|
||||
if result:
|
||||
logger.info(f" [{idx}/{total}] ✓ {name} ({elapsed:.1f}s)")
|
||||
eta = ""
|
||||
if len(elapsed_times) > 1:
|
||||
avg_time = sum(elapsed_times) / len(elapsed_times)
|
||||
remaining = (total - idx) * avg_time
|
||||
eta = f" — ETA: {remaining:.0f}s"
|
||||
logger.info(f" [{idx}/{total}] ✓ {name} ({elapsed:.1f}s){eta}")
|
||||
else:
|
||||
logger.warning(f" [{idx}/{total}] ✗ {name} — no output ({elapsed:.1f}s)")
|
||||
except Exception as e:
|
||||
@ -214,7 +237,7 @@ class LidarArchaeoPipeline:
|
||||
logger.info(" Conversion images WebP:")
|
||||
for name, tif_file in vis_results.items():
|
||||
if tif_file and isinstance(tif_file, Path) and tif_file.suffix == '.tif' and tif_file.exists():
|
||||
webp_file = tif_to_png(tif_file, file_vis_dir, self.resolution)
|
||||
webp_file = tif_to_png(tif_file, file_vis_dir, self.resolution, keep_tif=self.keep_tif)
|
||||
if webp_file:
|
||||
logger.info(f" ✓ {webp_file.name}")
|
||||
|
||||
@ -293,7 +316,7 @@ class LidarArchaeoPipeline:
|
||||
logger.info(f"Fichiers: {len(files)}")
|
||||
with ProcessPoolExecutor(max_workers=self.workers) as executor:
|
||||
future_to_file = {
|
||||
executor.submit(_process_file_standalone, str(laz_file), str(self.input_dir), str(self.output_dir), self.resolution, self.force, self.ground_method, self.force_classify): laz_file
|
||||
executor.submit(_process_file_standalone, str(laz_file), str(self.input_dir), str(self.output_dir), self.resolution, self.force, self.ground_method, self.force_classify, self.keep_tif): laz_file
|
||||
for laz_file in files
|
||||
}
|
||||
done = 0
|
||||
@ -346,16 +369,16 @@ class LidarArchaeoPipeline:
|
||||
try:
|
||||
if self.temp_dir.exists():
|
||||
shutil.rmtree(self.temp_dir)
|
||||
# Also clean up per-file temp directories from parallel workers
|
||||
for d in self.output_dir.glob("temp_*"):
|
||||
if d.is_dir():
|
||||
shutil.rmtree(d, ignore_errors=True)
|
||||
# Also clean up any subdirectories inside temp/
|
||||
temp_base = self.output_dir / "temp"
|
||||
if temp_base.exists():
|
||||
shutil.rmtree(temp_base)
|
||||
logger.info(" ✓ Fichiers temporaires supprimés")
|
||||
except Exception as e:
|
||||
logger.warning(f" Note: Impossible de supprimer les fichiers temporaires: {e}")
|
||||
|
||||
|
||||
def _process_file_standalone(laz_file_str, input_dir, output_dir, resolution, force=False, ground_method='auto', force_classify=False):
|
||||
def _process_file_standalone(laz_file_str, input_dir, output_dir, resolution, force=False, ground_method='auto', force_classify=False, keep_tif=False):
|
||||
"""Standalone function for multiprocessing — creates its own pipeline instance.
|
||||
|
||||
Each worker gets its own temp directory to avoid file conflicts.
|
||||
@ -371,9 +394,9 @@ def _process_file_standalone(laz_file_str, input_dir, output_dir, resolution, fo
|
||||
worker_logger.addHandler(handler)
|
||||
worker_logger.addFilter(_file_filter)
|
||||
|
||||
pipeline = LidarArchaeoPipeline(input_dir, output_dir, resolution=resolution, workers=1, force=force, ground_method=ground_method, force_classify=force_classify)
|
||||
pipeline = LidarArchaeoPipeline(input_dir, output_dir, resolution=resolution, workers=1, force=force, ground_method=ground_method, force_classify=force_classify, keep_tif=keep_tif)
|
||||
basename = _file_basename(laz_file_str)
|
||||
pipeline.temp_dir = pipeline.output_dir / f"temp_{basename}"
|
||||
pipeline.temp_dir = pipeline.output_dir / "temp" / basename
|
||||
pipeline.temp_dir.mkdir(exist_ok=True)
|
||||
laz_file = Path(laz_file_str)
|
||||
result = pipeline.process_file(laz_file)
|
||||
|
||||
Reference in New Issue
Block a user