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