Pas d'interpolation dans le DTM: les zones sans données restent NaN

- Suppression de l'interpolation NearestNDInterpolator dans create_dtm_fast
- Les pixels sans données LiDAR restent NaN dans le DTM et les
  visualisations — pas de valeurs fictives qui faussent les calculs
- nodata=float('nan') dans le GeoTIFF de sortie pour identifier les vides
- _save_tif() détecte automatiquement les NaN et écrit le flag nodata

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jacquin Antoine
2026-05-10 01:17:48 +02:00
parent 72b1437c1e
commit 52409a6510
2 changed files with 15 additions and 35 deletions

View File

@ -1,7 +1,7 @@
"""DTM generation from classified LiDAR point clouds.
Handles ground classification via PDAL/SMRF and DTM rasterisation
using scipy binned_statistic_2d with gap-filling interpolation.
using scipy binned_statistic_2d. Zones without LiDAR data remain as NaN.
"""
import json
@ -12,10 +12,7 @@ from pathlib import Path
import numpy as np
import rasterio
from rasterio.transform import from_bounds
from scipy import ndimage as scipy_ndimage
from scipy.ndimage import distance_transform_edt, gaussian_filter
from scipy.stats import binned_statistic_2d
from scipy import interpolate
logger = logging.getLogger("lidar")
@ -137,34 +134,10 @@ def create_dtm_fast(las_file, basename, dtm_dir, resolution):
dtm = stat.statistic.T
dtm = dtm[::-1, :] # Flip Y so north is at top
# Fill gaps using interpolation
mask = np.isnan(dtm)
if np.any(mask):
logger.info(" Remplissage des zones vides...")
dist_to_nearest = distance_transform_edt(mask)
max_fill_distance = max(20, int(10 / resolution))
y_coords, x_coords = np.where(~mask)
z_values = dtm[~mask]
if len(y_coords) > 100:
interp = interpolate.NearestNDInterpolator(
np.column_stack((y_coords, x_coords)),
z_values
)
y_missing, x_missing = np.where(mask)
dtm[y_missing, x_missing] = interp(y_missing, x_missing)
dtm_smooth = gaussian_filter(dtm, sigma=2.0)
fill_mask = mask & (dist_to_nearest <= max_fill_distance)
dtm[fill_mask] = dtm_smooth[fill_mask]
far_mask = mask & (dist_to_nearest > max_fill_distance)
dtm[far_mask] = np.nan
# No interpolation: keep NaN for zones without LiDAR data
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)")
# Save as GeoTIFF
output_tif = dtm_dir / f"{basename}_dtm.tif"
@ -175,6 +148,7 @@ def create_dtm_fast(las_file, basename, dtm_dir, resolution):
driver='GTiff', height=height, width=width,
count=1, dtype='float32',
crs='EPSG:2154', transform=transform,
nodata=float('nan'),
compress='lzw'
) as dst:
dst.write(dtm.astype('float32'), 1)

View File

@ -25,14 +25,19 @@ else:
xp = np
def _save_tif(output_path, data, transform, crs, dtype='float32', count=1):
def _save_tif(output_path, data, transform, crs, dtype='float32', count=1, nodata=None):
"""Helper to save a 2D or 3D array as GeoTIFF."""
# Auto-detect nodata for float types with NaN
if nodata is None and dtype.startswith('float') and np.any(np.isnan(data)):
nodata = float('nan')
if data.ndim == 2:
height, width = data.shape
with rasterio.open(
output_path, 'w', driver='GTiff',
height=height, width=width, count=count,
dtype=dtype, crs=crs, transform=transform, compress='lzw'
dtype=dtype, crs=crs, transform=transform,
compress='lzw', nodata=nodata
) as dst:
dst.write(data.astype(dtype), 1)
elif data.ndim == 3:
@ -40,7 +45,8 @@ def _save_tif(output_path, data, transform, crs, dtype='float32', count=1):
with rasterio.open(
output_path, 'w', driver='GTiff',
height=height, width=width, count=bands,
dtype=dtype, crs=crs, transform=transform, compress='lzw'
dtype=dtype, crs=crs, transform=transform,
compress='lzw', nodata=nodata
) as dst:
for i in range(bands):
dst.write(data[i].astype(dtype), i + 1)