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
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:

View File

@ -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"

View File

@ -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):

View File

@ -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)
"""

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
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,6 +381,13 @@ 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:
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)
@ -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"
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