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:
@ -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:
|
||||
|
||||
@ -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"
|
||||
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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)
|
||||
"""
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user