Audit: corrections de bugs identifiés

- rendering.py: colorbar cassée quand NaN mask actif — créer un
  ScalarMappable avec le cmap sauvegardé au lieu de rely sur
  l'image RGBA qui n'a plus de cmap
- rendering.py: nettoyage du PNG temporaire avec try/finally et
  missing_ok=True pour éviter les fichiers orphelins
- gpu.py: to_gpu() convertit en float32 au lieu de float64 pour
  réduire la consommation mémoire GPU
- dtm.py: utiliser _file_basename() de pipeline.py au lieu de
  dupliquer la logique d'extraction du basename
- pipeline.py: docstring corrigé (18 visualisations, pas 19)
- cli.py: --file supporte aussi les noms sans .copc
  (recherche .copc.laz et .copc.las en plus de .laz et .las)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jacquin Antoine
2026-05-10 12:11:13 +02:00
parent 47c20a319a
commit e31d3f0e2b
5 changed files with 37 additions and 20 deletions

View File

@ -171,9 +171,13 @@ Exemples:
from pathlib import Path from pathlib import Path
input_dir = Path(args.input) input_dir = Path(args.input)
# Each pattern is the full filename without extension (e.g. LHD_FXX_1000_6882_PTS_LAMB93_IGN69.copc) # 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 = [] selected_files = []
for pattern in args.file: 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 # Remove duplicates
matches = list(dict.fromkeys(matches)) matches = list(dict.fromkeys(matches))
if not matches: if not matches:

View File

@ -218,12 +218,9 @@ def classify_ground(laz_file, temp_dir, method='auto', force=False):
else: else:
logger.info(f" Classification sol: {method.upper()} (forcé)") logger.info(f" Classification sol: {method.upper()} (forcé)")
# Strip all known LiDAR extensions (.copc.laz, .laz, .las) # Use shared basename extraction function
laz_base = laz_file.name from .pipeline import _file_basename
for ext in ['.copc.laz', '.copc.las', '.laz', '.las']: laz_base = _file_basename(laz_file)
if laz_base.lower().endswith(ext):
laz_base = laz_base[:-len(ext)]
break
output_las = temp_dir / f"{laz_base}_ground_{method}.las" output_las = temp_dir / f"{laz_base}_ground_{method}.las"

View File

@ -57,16 +57,17 @@ def log_gpu_status():
def to_gpu(arr): 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(): if _gpu_available():
try: try:
return _cp.asarray(arr.astype(np.float64)) return _cp.asarray(arr.astype(np.float32))
except Exception: except Exception:
pass # Fall back to CPU pass # Fall back to CPU
return arr.astype(np.float64) return arr.astype(np.float32)
def to_cpu(arr): def to_cpu(arr):

View File

@ -3,7 +3,7 @@
LidarArchaeoPipeline coordinates the full processing chain: LidarArchaeoPipeline coordinates the full processing chain:
1. Ground classification (PDAL/SMRF) 1. Ground classification (PDAL/SMRF)
2. DTM generation 2. DTM generation
3. Visualization generation (19 products) 3. Visualization generation (18 products)
4. Rendering (WebP + PDF report) 4. Rendering (WebP + PDF report)
""" """

View File

@ -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 has_nan_mask = nan_mask is not None and not is_rgb_result
if has_nan_mask: if has_nan_mask:
# data is normalized 0-1 from _apply_colormap; apply cmap to get RGBA # 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 # Save the colormap for colorbar before converting to RGBA
rgba = cmap_obj(data) # (H, W, 4) float 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 rgba[nan_mask, 3] = 0.0 # transparent where no data
data = rgba data = rgba
is_rgba = True is_rgba = True
else: else:
is_rgba = False is_rgba = False
saved_cmap = None
saved_vmin = None
saved_vmax = None
# Create figure — adapt width to data resolution for sharp rendering # 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 # 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) ax.set_title(f"{title}\n{description}", fontsize=15, fontweight='bold', pad=10)
if not is_rgb: 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.ax.tick_params(labelsize=9, width=1.5)
cbar.outline.set_linewidth(1.5) cbar.outline.set_linewidth(1.5)
cbar.set_label(legend_label, fontsize=10, fontweight='bold') 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 as PNG then convert to WebP — use higher DPI for large data
save_dpi = 200 if width > 3000 else 150 save_dpi = 200 if width > 3000 else 150
png_temp = vis_dir / f"{tif_file.stem}_temp.png" png_temp = vis_dir / f"{tif_file.stem}_temp.png"
plt.savefig(png_temp, dpi=save_dpi, bbox_inches='tight', pad_inches=0.15, try:
facecolor='white', format='png') plt.savefig(png_temp, dpi=save_dpi, bbox_inches='tight', pad_inches=0.15,
plt.close() facecolor='white', format='png')
finally:
plt.close()
img = PILImage.open(str(png_temp)) img = PILImage.open(str(png_temp))
img.save(str(webp_file), format='WEBP', lossless=True) img.save(str(webp_file), format='WEBP', lossless=True)
png_temp.unlink() png_temp.unlink(missing_ok=True)
# Delete source TIFF # Delete source TIFF
tif_file.unlink() tif_file.unlink(missing_ok=True)
return webp_file return webp_file