diff --git a/lidar_pipeline/dtm.py b/lidar_pipeline/dtm.py index c298955..4464b8b 100644 --- a/lidar_pipeline/dtm.py +++ b/lidar_pipeline/dtm.py @@ -285,25 +285,12 @@ def create_dtm_fast(las_file, basename, dtm_dir, resolution): dtm = stat.statistic.T dtm = dtm[::-1, :] # Flip Y so north is at top - # Interpolate NaN gaps using distance-weighted nearest-neighbor fill + # Keep NaN for zones without LiDAR data — no interpolation nan_count = np.count_nonzero(np.isnan(dtm)) if nan_count > 0: total = dtm.size nan_pct = 100.0 * nan_count / total - logger.info(f" {nan_count:,} pixels sans données ({nan_pct:.1f}%) — interpolation...") - - from rasterio.fill import fillnodata - # rasterio.fill.fillnodata uses GDAL's interpolation: - # fills gaps from surrounding valid pixels with distance weighting - dtm_filled = fillnodata(dtm.astype(np.float32), mask=~np.isnan(dtm), - max_search_distance=max(width, height) // 4) - dtm = dtm_filled.astype(np.float64) - - remaining = np.count_nonzero(np.isnan(dtm)) - if remaining > 0: - logger.warning(f" {remaining:,} pixels encore sans données après interpolation") - else: - logger.info(f" ✓ Interpolation terminée — tous les trous comblés") + logger.info(f" {nan_count:,} pixels sans données ({nan_pct:.1f}%)") # Save as GeoTIFF output_tif = dtm_dir / f"{basename}_dtm.tif" diff --git a/lidar_pipeline/rendering.py b/lidar_pipeline/rendering.py index 626ce34..6c2dd8b 100644 --- a/lidar_pipeline/rendering.py +++ b/lidar_pipeline/rendering.py @@ -323,13 +323,36 @@ def tif_to_png(tif_file, vis_dir, resolution): valid_data = np.asarray(data.compressed() if hasattr(data, 'compressed') else data.flatten()) valid_data = valid_data[~np.isnan(valid_data)] - # Convert MaskedArray to plain ndarray (NaN-filled) to avoid numpy warnings - if isinstance(data, np.ma.MaskedArray): - data = np.ma.filled(data, np.nan) + # Track NaN mask before converting to plain ndarray + nan_mask = None + if not is_rgb: + if isinstance(data, np.ma.MaskedArray): + nan_mask = data.mask.copy() + data = np.ma.filled(data, np.nan) + elif np.any(np.isnan(data)): + nan_mask = np.isnan(data) + + # For rendering: replace NaN with neutral value to avoid interpolation halos + if nan_mask is not None and np.any(nan_mask) and len(valid_data) > 0: + fill_value = float(np.median(valid_data)) + data[nan_mask] = fill_value + nan_mask = nan_mask # keep for later # Apply colormap data, cmap, title, legend_label, description, is_rgb_result = _apply_colormap(data, tif_file) + # Apply NaN mask: make zones without data transparent + 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 + rgba[nan_mask, 3] = 0.0 # transparent where no data + data = rgba + is_rgba = True + else: + is_rgba = False + # 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 fig_width = max(20, width / 150) @@ -342,7 +365,7 @@ def tif_to_png(tif_file, vis_dir, resolution): left=0.06, right=0.88, top=0.93, bottom=0.08) ax = fig.add_subplot(gs[0]) - if is_rgb: + if is_rgba or is_rgb: im = ax.imshow(data, aspect='equal', origin='upper', interpolation='bilinear') else: