Rendu: zones NaN transparentes au lieu d'interpolation DTM
L'interpolation fillnodata du DTM générait des artefacts visuels (zones lissées artificielles). Revenu au DTM avec NaN conservés. Nouvelle approche pour le rendu: - NaN remplacés par la médiane des valeurs valides (valeur neutre) - Après application de la colormap, un masque alpha rend les zones NaN transparentes au lieu de les afficher - interpolation='bilinear' sur imshow pour un rendu lisse - Figure adaptative: 20-40 pouces selon la taille des données - DPI 200 pour les images > 3000px Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@ -285,25 +285,12 @@ def create_dtm_fast(las_file, basename, dtm_dir, resolution):
|
|||||||
dtm = stat.statistic.T
|
dtm = stat.statistic.T
|
||||||
dtm = dtm[::-1, :] # Flip Y so north is at top
|
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))
|
nan_count = np.count_nonzero(np.isnan(dtm))
|
||||||
if nan_count > 0:
|
if nan_count > 0:
|
||||||
total = dtm.size
|
total = dtm.size
|
||||||
nan_pct = 100.0 * nan_count / total
|
nan_pct = 100.0 * nan_count / total
|
||||||
logger.info(f" {nan_count:,} pixels sans données ({nan_pct:.1f}%) — interpolation...")
|
logger.info(f" {nan_count:,} pixels sans données ({nan_pct:.1f}%)")
|
||||||
|
|
||||||
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
|
# Save as GeoTIFF
|
||||||
output_tif = dtm_dir / f"{basename}_dtm.tif"
|
output_tif = dtm_dir / f"{basename}_dtm.tif"
|
||||||
|
|||||||
@ -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 = np.asarray(data.compressed() if hasattr(data, 'compressed') else data.flatten())
|
||||||
valid_data = valid_data[~np.isnan(valid_data)]
|
valid_data = valid_data[~np.isnan(valid_data)]
|
||||||
|
|
||||||
# Convert MaskedArray to plain ndarray (NaN-filled) to avoid numpy warnings
|
# Track NaN mask before converting to plain ndarray
|
||||||
if isinstance(data, np.ma.MaskedArray):
|
nan_mask = None
|
||||||
data = np.ma.filled(data, np.nan)
|
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
|
# Apply colormap
|
||||||
data, cmap, title, legend_label, description, is_rgb_result = _apply_colormap(data, tif_file)
|
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
|
# 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
|
||||||
fig_width = max(20, width / 150)
|
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)
|
left=0.06, right=0.88, top=0.93, bottom=0.08)
|
||||||
|
|
||||||
ax = fig.add_subplot(gs[0])
|
ax = fig.add_subplot(gs[0])
|
||||||
if is_rgb:
|
if is_rgba or is_rgb:
|
||||||
im = ax.imshow(data, aspect='equal', origin='upper',
|
im = ax.imshow(data, aspect='equal', origin='upper',
|
||||||
interpolation='bilinear')
|
interpolation='bilinear')
|
||||||
else:
|
else:
|
||||||
|
|||||||
Reference in New Issue
Block a user