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

@ -128,6 +128,31 @@ Exemples:
default="auto",
help="Méthode de classification du sol : auto (détection), smrf, csf (défaut: auto)"
)
parser.add_argument(
"--quality",
type=int,
default=85,
help="Qualité WebP (1-100, défaut: 85). Utilisez 100 pour lossless."
)
parser.add_argument(
"--lossless",
action="store_true",
help="Forcer la compression WebP lossless (équivalent à --quality 100)"
)
parser.add_argument(
"--only",
nargs="+",
type=str,
default=None,
help="Générer uniquement ces visualisations (ex: --only hillshade svf lrm)"
)
parser.add_argument(
"--skip",
nargs="+",
type=str,
default=None,
help="Exclure ces visualisations (ex: --skip ortho topo)"
)
parser.add_argument(
"--file",
nargs="+",
@ -168,6 +193,7 @@ Exemples:
log_gpu_status()
try:
quality = 100 if args.lossless else args.quality
pipeline = LidarArchaeoPipeline(
input_dir=args.input,
output_dir=args.output,
@ -177,6 +203,9 @@ Exemples:
ground_method=args.ground_classification,
force_classify=args.force_classification,
keep_tif=args.keep_tif,
quality=quality,
only_viz=args.only,
skip_viz=args.skip,
)
# If --file is specified, process only matching files

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)

View File

@ -271,7 +271,7 @@ def _nice_scale(extent_m):
return chosen, f"{chosen} m"
def tif_to_png(tif_file, vis_dir, resolution, keep_tif=False, source_info=None):
def tif_to_png(tif_file, vis_dir, resolution, keep_tif=False, source_info=None, quality=85):
"""Convert GeoTIFF to visualization WebP with GPS coordinates, legend, and scale bar.
Args:
@ -279,6 +279,8 @@ def tif_to_png(tif_file, vis_dir, resolution, keep_tif=False, source_info=None):
vis_dir: Output directory for the WebP file.
resolution: Grid resolution in m/px.
keep_tif: If True, keep the source TIFF after conversion.
source_info: Dict with method/date/basename for metadata.
quality: WebP quality (1-100). Use 100 for lossless. Default 85.
Returns:
Path to output WebP file, or None on failure.
@ -589,7 +591,10 @@ def tif_to_png(tif_file, vis_dir, resolution, keep_tif=False, source_info=None):
plt.close()
img = PILImage.open(str(png_temp))
img.save(str(webp_file), format='WEBP', lossless=True)
if quality >= 100:
img.save(str(webp_file), format='WEBP', lossless=True)
else:
img.save(str(webp_file), format='WEBP', quality=quality)
png_temp.unlink(missing_ok=True)
# Delete source TIFF (unless --keep-tif)

38
run.sh
View File

@ -68,6 +68,9 @@ FILE_ARGS=""
GROUND_METHOD=""
FORCE_CLASSIFY_FLAG=""
KEEP_TIF_FLAG=""
QUALITY=""
ONLY_FLAG=""
SKIP_FLAG=""
# Parse arguments manually (more robust than getopts for mixed short/long options)
while [ $# -gt 0 ]; do
@ -83,6 +86,10 @@ while [ $# -gt 0 ]; do
--keep-tif) KEEP_TIF_FLAG="--keep-tif"; shift ;;
--ground-classification) GROUND_METHOD="$2"; shift 2 ;;
--ground-classification=*) GROUND_METHOD="${1#--ground-classification=}"; shift ;;
--quality) QUALITY="--quality $2"; shift 2 ;;
--lossless) QUALITY="--lossless"; shift ;;
--only) shift; ONLY_FLAG="--only"; while [ $# -gt 0 ] && [[ ! "$1" =~ ^- ]]; do ONLY_FLAG="$ONLY_FLAG $1"; shift; done ;;
--skip) shift; SKIP_FLAG="--skip"; while [ $# -gt 0 ] && [[ ! "$1" =~ ^- ]]; do SKIP_FLAG="$SKIP_FLAG $1"; shift; done ;;
--file) shift; while [ $# -gt 0 ] && [[ ! "$1" =~ ^- ]]; do FILE_ARGS="$FILE_ARGS $1"; shift; done ;;
--test) ;; # Handled below
-h|--help|-help)
@ -102,17 +109,28 @@ while [ $# -gt 0 ]; do
echo " --keep-tif Conserver les TIFF pour régénérer les WebP"
echo " --ground-classification {auto,smrf,csf}"
echo " Méthode de classification du sol (défaut: auto)"
echo " --file NOM... Traiter un ou plusieurs fichiers LAZ (nom complet sans .laz/.las)"
echo " --quality N Qualité WebP 1-100 (défaut: 85, 100=lossless)"
echo " --lossless Compression WebP lossless (équivalent à --quality 100)"
echo " --only VIZ... Générer uniquement ces visualisations"
echo " --skip VIZ... Exclure ces visualisations"
echo " --file NOM... Traiter un ou plusieurs fichiers LAZ"
echo " --test Exécuter les tests unitaires"
echo " -h Afficher cette aide"
echo ""
echo "Visualisations disponibles:"
echo " hillshade slope aspect curvature svf lrm pos_open neg_open"
echo " mslrm tpi sailore roughness anomalies wavelet flow local_dominance ortho topo"
echo ""
echo "Exemples:"
echo " $0 -g # GPU, auto"
echo " $0 -g -w 4 # GPU + 4 workers"
echo " $0 -g -v # GPU + verbeux"
echo " $0 -g -r 0.2 # Haute résolution"
echo " $0 -g --force # Régénérer WebP (DTM conservé si --keep-tif)"
echo " $0 -g --force-classification # Reclassifier le sol seulement"
echo " $0 -g --force # Régénérer WebP"
echo " $0 -g --only hillshade svf lrm # Seulement 3 visualisations"
echo " $0 -g --skip ortho topo # Sans les overlays IGN"
echo " $0 -g --lossless # WebP lossless"
echo " $0 -g --quality 90 # WebP qualité 90"
echo " $0 -g --ground-classification csf # Forcer CSF (terrain complexe)"
echo " $0 -g --file LHD_...IGN69.copc # Un fichier"
exit 0
@ -160,16 +178,28 @@ echo " Verbeux : $([ -n "$VERBOSE_FLAG" ] && echo 'OUI' || echo 'non')"
echo " Force : $([ -n "$FORCE_FLAG" ] && echo 'OUI' || echo 'non')"
echo " Force classif.: $([ -n "$FORCE_CLASSIFY_FLAG" ] && echo 'OUI' || echo 'non')"
echo " Keep TIFF : $([ -n "$KEEP_TIF_FLAG" ] && echo 'OUI' || echo 'non')"
echo " Qualité WebP : $([ -n "$QUALITY" ] && echo "$QUALITY" || echo '85')"
echo " Classification sol : $([ -n "$GROUND_METHOD" ] && echo "$GROUND_METHOD" || echo 'auto')"
if [ -n "$ONLY_FLAG" ]; then
echo " Visualisations: uniquement${ONLY_FLAG#--only}"
elif [ -n "$SKIP_FLAG" ]; then
echo " Visualisations: tout sauf${SKIP_FLAG#--skip}"
fi
if [ -n "$FILE_ARGS" ]; then
echo " Fichiers :${FILE_ARGS}"
fi
echo "============================================"
CMD_ARGS="-o /data/output -r $RESOLUTION -w $WORKERS $VERBOSE_FLAG $FORCE_FLAG $FORCE_CLASSIFY_FLAG $KEEP_TIF_FLAG"
CMD_ARGS="-o /data/output -r $RESOLUTION -w $WORKERS $VERBOSE_FLAG $FORCE_FLAG $FORCE_CLASSIFY_FLAG $KEEP_TIF_FLAG $QUALITY"
if [ -n "$GROUND_METHOD" ]; then
CMD_ARGS="$CMD_ARGS --ground-classification $GROUND_METHOD"
fi
if [ -n "$ONLY_FLAG" ]; then
CMD_ARGS="$CMD_ARGS $ONLY_FLAG"
fi
if [ -n "$SKIP_FLAG" ]; then
CMD_ARGS="$CMD_ARGS $SKIP_FLAG"
fi
if [ -n "$FILE_ARGS" ]; then
CMD_ARGS="$CMD_ARGS --file $FILE_ARGS"
fi