diff --git a/lidar_pipeline/cli.py b/lidar_pipeline/cli.py index 4bbc178..7042ece 100644 --- a/lidar_pipeline/cli.py +++ b/lidar_pipeline/cli.py @@ -171,9 +171,13 @@ Exemples: from pathlib import Path input_dir = Path(args.input) # Each pattern is the full filename without extension (e.g. LHD_FXX_1000_6882_PTS_LAMB93_IGN69.copc) + # Also supports bare name without .copc (e.g. LHD_FXX_1000_6882_PTS_LAMB93_IGN69) selected_files = [] for pattern in args.file: - matches = list(input_dir.glob(f"{pattern}.laz")) + list(input_dir.glob(f"{pattern}.las")) + matches = (list(input_dir.glob(f"{pattern}.laz")) + + list(input_dir.glob(f"{pattern}.las")) + + list(input_dir.glob(f"{pattern}.copc.laz")) + + list(input_dir.glob(f"{pattern}.copc.las"))) # Remove duplicates matches = list(dict.fromkeys(matches)) if not matches: diff --git a/lidar_pipeline/dtm.py b/lidar_pipeline/dtm.py index a1196e2..433fc98 100644 --- a/lidar_pipeline/dtm.py +++ b/lidar_pipeline/dtm.py @@ -218,12 +218,9 @@ def classify_ground(laz_file, temp_dir, method='auto', force=False): else: logger.info(f" Classification sol: {method.upper()} (forcé)") - # Strip all known LiDAR extensions (.copc.laz, .laz, .las) - laz_base = laz_file.name - for ext in ['.copc.laz', '.copc.las', '.laz', '.las']: - if laz_base.lower().endswith(ext): - laz_base = laz_base[:-len(ext)] - break + # Use shared basename extraction function + from .pipeline import _file_basename + laz_base = _file_basename(laz_file) output_las = temp_dir / f"{laz_base}_ground_{method}.las" diff --git a/lidar_pipeline/gpu.py b/lidar_pipeline/gpu.py index 151bc77..ed83b20 100644 --- a/lidar_pipeline/gpu.py +++ b/lidar_pipeline/gpu.py @@ -57,16 +57,17 @@ def log_gpu_status(): def to_gpu(arr): - """Send array to GPU if available, otherwise return as float64 numpy. + """Send array to GPU if available, otherwise return as float32 numpy. - Falls back to CPU if GPU is unavailable (e.g. in forked subprocess). + Uses float32 to reduce GPU memory usage. Falls back to CPU if GPU + is unavailable (e.g. in forked subprocess). """ if _gpu_available(): try: - return _cp.asarray(arr.astype(np.float64)) + return _cp.asarray(arr.astype(np.float32)) except Exception: pass # Fall back to CPU - return arr.astype(np.float64) + return arr.astype(np.float32) def to_cpu(arr): diff --git a/lidar_pipeline/pipeline.py b/lidar_pipeline/pipeline.py index c0a419f..bbd498f 100644 --- a/lidar_pipeline/pipeline.py +++ b/lidar_pipeline/pipeline.py @@ -3,7 +3,7 @@ LidarArchaeoPipeline coordinates the full processing chain: 1. Ground classification (PDAL/SMRF) 2. DTM generation -3. Visualization generation (19 products) +3. Visualization generation (18 products) 4. Rendering (WebP + PDF report) """ diff --git a/lidar_pipeline/rendering.py b/lidar_pipeline/rendering.py index 6c2dd8b..32a36ef 100644 --- a/lidar_pipeline/rendering.py +++ b/lidar_pipeline/rendering.py @@ -345,13 +345,19 @@ def tif_to_png(tif_file, vis_dir, resolution): has_nan_mask = nan_mask is not None and not is_rgb_result if has_nan_mask: # data is normalized 0-1 from _apply_colormap; apply cmap to get RGBA - cmap_obj = plt.get_cmap(cmap) if isinstance(cmap, str) else cmap - rgba = cmap_obj(data) # (H, W, 4) float RGBA + # Save the colormap for colorbar before converting to RGBA + saved_cmap = plt.get_cmap(cmap) if isinstance(cmap, str) else cmap + saved_vmin = float(np.nanmin(data)) if not nan_mask.all() else 0 + saved_vmax = float(np.nanmax(data)) if not nan_mask.all() else 1 + rgba = saved_cmap(data) # (H, W, 4) float RGBA rgba[nan_mask, 3] = 0.0 # transparent where no data data = rgba is_rgba = True else: is_rgba = False + saved_cmap = None + saved_vmin = None + saved_vmax = None # Create figure — adapt width to data resolution for sharp rendering # At high res (5000+px wide), we need a larger figure to avoid downsampling artifacts @@ -375,7 +381,14 @@ def tif_to_png(tif_file, vis_dir, resolution): ax.set_title(f"{title}\n{description}", fontsize=15, fontweight='bold', pad=10) if not is_rgb: - cbar = plt.colorbar(im, ax=ax, pad=0.02, shrink=0.85, aspect=30) + if is_rgba and saved_cmap is not None: + # Create a ScalarMappable for the colorbar from the saved colormap + sm = plt.cm.ScalarMappable(cmap=saved_cmap, + norm=plt.Normalize(vmin=saved_vmin, vmax=saved_vmax)) + sm.set_array([]) + cbar = plt.colorbar(sm, ax=ax, pad=0.02, shrink=0.85, aspect=30) + else: + cbar = plt.colorbar(im, ax=ax, pad=0.02, shrink=0.85, aspect=30) cbar.ax.tick_params(labelsize=9, width=1.5) cbar.outline.set_linewidth(1.5) cbar.set_label(legend_label, fontsize=10, fontweight='bold') @@ -486,16 +499,18 @@ def tif_to_png(tif_file, vis_dir, resolution): # Save as PNG then convert to WebP — use higher DPI for large data save_dpi = 200 if width > 3000 else 150 png_temp = vis_dir / f"{tif_file.stem}_temp.png" - plt.savefig(png_temp, dpi=save_dpi, bbox_inches='tight', pad_inches=0.15, - facecolor='white', format='png') - plt.close() + try: + plt.savefig(png_temp, dpi=save_dpi, bbox_inches='tight', pad_inches=0.15, + facecolor='white', format='png') + finally: + plt.close() img = PILImage.open(str(png_temp)) img.save(str(webp_file), format='WEBP', lossless=True) - png_temp.unlink() + png_temp.unlink(missing_ok=True) # Delete source TIFF - tif_file.unlink() + tif_file.unlink(missing_ok=True) return webp_file