diff --git a/lidar_pipeline/cli.py b/lidar_pipeline/cli.py index 8587892..29bfa7f 100644 --- a/lidar_pipeline/cli.py +++ b/lidar_pipeline/cli.py @@ -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 diff --git a/lidar_pipeline/pipeline.py b/lidar_pipeline/pipeline.py index a0d20c0..cfcc5e5 100644 --- a/lidar_pipeline/pipeline.py +++ b/lidar_pipeline/pipeline.py @@ -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) diff --git a/lidar_pipeline/rendering.py b/lidar_pipeline/rendering.py index b751cfe..3279a7d 100644 --- a/lidar_pipeline/rendering.py +++ b/lidar_pipeline/rendering.py @@ -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) diff --git a/run.sh b/run.sh index fa44b63..ec1d551 100755 --- a/run.sh +++ b/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