Add WebP quality control and selective visualization (--only / --skip)

- WebP output now uses quality=85 by default (down from lossless),
  reducing file size by ~75% (35MB → 5-8MB per visualization)
- Added --quality N (1-100) and --lossless flags in CLI and run.sh
- Added --only and --skip to select/exclude specific visualizations
  (e.g., --only hillshade,svf,lrm or --skip ortho,topo)
- VIZ_STEPS filtering is done in LidarArchaeoPipeline.__init__
- SharedDEM is skipped when all selected visualizations already exist
- Invalid visualization names are validated at startup with clear error

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jacquin Antoine
2026-05-14 21:15:21 +02:00
parent 6ed4972afc
commit 34b79ac2c2
4 changed files with 103 additions and 15 deletions

View File

@ -108,7 +108,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, keep_tif=False):
def __init__(self, input_dir, output_dir, resolution=0.5, workers=1, force=False, ground_method='auto', force_classify=False, keep_tif=False, quality=85, only_viz=None, skip_viz=None):
self.input_dir = Path(input_dir)
self.output_dir = Path(output_dir)
self.resolution = resolution
@ -117,6 +117,9 @@ class LidarArchaeoPipeline:
self.ground_method = ground_method
self.force_classify = force_classify
self.keep_tif = keep_tif
self.quality = quality
self.only_viz = only_viz
self.skip_viz = skip_viz
self.temp_dir = self.output_dir / "temp"
if not self.input_dir.exists():
@ -132,6 +135,21 @@ class LidarArchaeoPipeline:
for d in [self.dtm_dir, self.vis_dir, self.pdf_dir]:
d.mkdir(exist_ok=True)
# Filter visualizations based on --only / --skip
all_viz_names = [name for name, _ in VIZ_STEPS]
if only_viz:
invalid = set(only_viz) - set(all_viz_names)
if invalid:
raise ValueError(f"Visualisations inconnues: {', '.join(invalid)}. Disponibles: {', '.join(all_viz_names)}")
self.viz_steps = [(n, f) for n, f in VIZ_STEPS if n in only_viz]
elif skip_viz:
invalid = set(skip_viz) - set(all_viz_names)
if invalid:
raise ValueError(f"Visualisations inconnues: {', '.join(invalid)}. Disponibles: {', '.join(all_viz_names)}")
self.viz_steps = [(n, f) for n, f in VIZ_STEPS if n not in skip_viz]
else:
self.viz_steps = VIZ_STEPS
logger.info("Pipeline initialisé")
logger.info(f" Entrée : {self.input_dir}")
logger.info(f" Sortie : {self.output_dir}")
@ -141,6 +159,12 @@ class LidarArchaeoPipeline:
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'}")
logger.info(f" Qualité WebP: {self.quality if self.quality < 100 else 'lossless'}")
if only_viz:
logger.info(f" Visualisations: uniquement {', '.join(only_viz)}")
elif skip_viz:
logger.info(f" Visualisations: tout sauf {', '.join(skip_viz)}")
logger.info(f" Visualisations: {len(self.viz_steps)}/{len(VIZ_STEPS)}")
def find_laz_files(self):
"""Find all LAZ/LAS files in input directory."""
@ -188,11 +212,11 @@ class LidarArchaeoPipeline:
# Create per-file subdirectory
file_vis_dir = self.vis_dir / basename
file_vis_dir.mkdir(exist_ok=True)
total = len(VIZ_STEPS)
total = len(self.viz_steps)
# Phase 1: determine which visualizations need generation
needs_generation = {} # name -> True/False
for name, func in VIZ_STEPS:
for name, func in self.viz_steps:
if self.force:
needs_generation[name] = True
else:
@ -207,7 +231,7 @@ class LidarArchaeoPipeline:
logger.info(" Toutes les visualisations déjà existantes — ignorées")
# Still need to return results dict for PDF check
vis_results = {}
for name, func in VIZ_STEPS:
for name, func in self.viz_steps:
vis_results[name] = self._expected_webp_path(name, basename, file_vis_dir)
return vis_results
@ -221,7 +245,7 @@ class LidarArchaeoPipeline:
# Phase 3: generate visualizations
vis_results = {}
for idx, (name, func) in enumerate(VIZ_STEPS, 1):
for idx, (name, func) in enumerate(self.viz_steps, 1):
if not needs_generation[name]:
logger.info(f" [{idx}/{total}] {name}: déjà existant, ignoré")
vis_results[name] = self._expected_webp_path(name, basename, file_vis_dir)
@ -271,7 +295,7 @@ class LidarArchaeoPipeline:
}
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, resolution, keep_tif=self.keep_tif, source_info=source_info)
webp_file = tif_to_png(tif_file, file_vis_dir, resolution, keep_tif=self.keep_tif, source_info=source_info, quality=self.quality)
if webp_file:
logger.info(f"{webp_file.name}")
@ -393,7 +417,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, self.keep_tif): 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, self.quality, self.only_viz, self.skip_viz): laz_file
for laz_file in files
}
done = 0
@ -456,7 +480,7 @@ class LidarArchaeoPipeline:
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, keep_tif=False):
def _process_file_standalone(laz_file_str, input_dir, output_dir, resolution, force=False, ground_method='auto', force_classify=False, keep_tif=False, quality=85, only_viz=None, skip_viz=None):
"""Standalone function for multiprocessing — creates its own pipeline instance.
Each worker gets its own temp directory to avoid file conflicts.
@ -477,7 +501,7 @@ 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, keep_tif=keep_tif)
pipeline = LidarArchaeoPipeline(input_dir, output_dir, resolution=resolution, workers=1, force=force, ground_method=ground_method, force_classify=force_classify, keep_tif=keep_tif, quality=quality, only_viz=only_viz, skip_viz=skip_viz)
basename = _file_basename(laz_file_str)
pipeline.temp_dir = pipeline.output_dir / "temp" / basename
pipeline.temp_dir.mkdir(exist_ok=True)