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