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