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:
@ -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)
|
||||
|
||||
Reference in New Issue
Block a user