From c3a8fe7b79b977f9e9fe3e39d6012799a56ee871 Mon Sep 17 00:00:00 2001 From: Jacquin Antoine Date: Sun, 10 May 2026 11:34:41 +0200 Subject: [PATCH] =?UTF-8?q?Am=C3=A9lioration=20du=20rendu:=20interpolation?= =?UTF-8?q?=20DTM=20et=20affichage=20haute=20r=C3=A9solution?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DTM: interpolation des NaN avec rasterio.fill.fillnodata après binned_statistic_2d — comble les trous entre les cellules sans données - Rendering: interpolation='bilinear' sur imshow pour lisser le sous-échantillonnage des données haute résolution - Rendering: fig_width adaptatif (20-40 pouces) selon la taille des données - Rendering: DPI 200 pour les images > 3000px de large Co-Authored-By: Claude Opus 4.6 --- lidar_pipeline/dtm.py | 19 +++++++++++++++++-- lidar_pipeline/rendering.py | 17 +++++++++++------ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/lidar_pipeline/dtm.py b/lidar_pipeline/dtm.py index 82b5e98..c298955 100644 --- a/lidar_pipeline/dtm.py +++ b/lidar_pipeline/dtm.py @@ -285,10 +285,25 @@ def create_dtm_fast(las_file, basename, dtm_dir, resolution): dtm = stat.statistic.T dtm = dtm[::-1, :] # Flip Y so north is at top - # No interpolation: keep NaN for zones without LiDAR data + # Interpolate NaN gaps using distance-weighted nearest-neighbor fill nan_count = np.count_nonzero(np.isnan(dtm)) if nan_count > 0: - logger.info(f" {nan_count:,} pixels sans données (conservés en NaN)") + 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") # Save as GeoTIFF output_tif = dtm_dir / f"{basename}_dtm.tif" diff --git a/lidar_pipeline/rendering.py b/lidar_pipeline/rendering.py index 5aadc48..eaae0c0 100644 --- a/lidar_pipeline/rendering.py +++ b/lidar_pipeline/rendering.py @@ -324,8 +324,10 @@ def tif_to_png(tif_file, vis_dir, resolution): # Apply colormap data, cmap, title, legend_label, description, is_rgb_result = _apply_colormap(data, tif_file) - # Create figure - fig_width = 20 + # 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) + fig_width = min(fig_width, 40) # cap at 40 inches map_aspect = height / width fig = plt.figure(figsize=(fig_width, fig_width * map_aspect * 0.7 + 2.5), facecolor='white') @@ -335,9 +337,11 @@ def tif_to_png(tif_file, vis_dir, resolution): ax = fig.add_subplot(gs[0]) if is_rgb: - im = ax.imshow(data, aspect='equal', origin='upper') + im = ax.imshow(data, aspect='equal', origin='upper', + interpolation='bilinear') else: - im = ax.imshow(data, cmap=cmap, aspect='equal', origin='upper') + im = ax.imshow(data, cmap=cmap, aspect='equal', origin='upper', + interpolation='bilinear') ax.set_title(f"{title}\n{description}", fontsize=15, fontweight='bold', pad=10) @@ -450,9 +454,10 @@ def tif_to_png(tif_file, vis_dir, resolution): fig.patch.set_facecolor('white') - # Save as PNG then convert to WebP + # 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=150, bbox_inches='tight', pad_inches=0.15, + plt.savefig(png_temp, dpi=save_dpi, bbox_inches='tight', pad_inches=0.15, facecolor='white', format='png') plt.close()