Suppression éclairage solaire, GPU accéléré, --file multi, tests unitaires
- Suppression de generate_solar (éclairage solaire) des visualisations - Accélération GPU de hillshade, slope, aspect, curvature, depressions, anomalies, roughness, texture GLCM, flow (sink filling) - Nettoyage mémoire GPU entre visualisations (gpu_cleanup) - Correction OOM texture GLCM: calcul entropie bin par bin au lieu d'un tableau 3D massif sur GPU - Correction bug: xp_minimum_filter manquant dans imports visualizations - Option --file accepte plusieurs noms complets sans extension - run.sh affiche l'aide si appelé sans arguments - Option --test pour exécuter les tests unitaires dans Docker - Filtre ReturnNumber>=1 intégré dans le pipeline PDAL (plus d'erreur SMRF) - 60 tests unitaires: GPU, visualisations, rendering, DTM, pipeline, CLI - Ajout pytest au Dockerfile Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@ -10,11 +10,10 @@ from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import rasterio
|
||||
from scipy import ndimage
|
||||
from scipy.ndimage import generic_filter, gaussian_filter, uniform_filter, minimum_filter
|
||||
from scipy.ndimage import generic_filter
|
||||
from scipy.stats import binned_statistic_2d
|
||||
|
||||
from .gpu import HAS_GPU, to_gpu, to_cpu, xp_gaussian_filter, xp_uniform_filter
|
||||
from .gpu import HAS_GPU, to_gpu, to_cpu, xp_gaussian_filter, xp_uniform_filter, xp_minimum_filter
|
||||
|
||||
logger = logging.getLogger("lidar")
|
||||
|
||||
@ -58,31 +57,38 @@ def _read_dem(dem_file):
|
||||
# ============================================================
|
||||
|
||||
def generate_hillshade(dem_file, basename, vis_dir, resolution):
|
||||
"""Generate multi-directional hillshade (NW, NE, SW, SE)."""
|
||||
logger.info(" → Hillshade multidirectionnel...")
|
||||
"""Generate multi-directional hillshade (NW, NE, SW, SE) — GPU if available."""
|
||||
gpu_tag = " [GPU]" if HAS_GPU else ""
|
||||
logger.info(f" → Hillshade multidirectionnel{gpu_tag}...")
|
||||
t0 = time.time()
|
||||
output = vis_dir / f"{basename}_hillshade_multi.tif"
|
||||
|
||||
try:
|
||||
dem, transform, crs = _read_dem(dem_file)
|
||||
dy, dx = np.gradient(dem)
|
||||
dem_np, transform, crs = _read_dem(dem_file)
|
||||
dem = to_gpu(dem_np)
|
||||
dy, dx = xp.gradient(dem)
|
||||
|
||||
azimuts = [315, 45, 225, 135]
|
||||
altitude = 30
|
||||
hillshades = []
|
||||
|
||||
for az in azimuts:
|
||||
az_rad = np.radians(az)
|
||||
alt_rad = np.radians(altitude)
|
||||
slope = np.arctan(np.sqrt(dx**2 + dy**2))
|
||||
aspect = np.arctan2(dy, dx)
|
||||
hs = np.sin(alt_rad) * np.sin(slope) + \
|
||||
np.cos(alt_rad) * np.cos(slope) * np.cos(az_rad - aspect)
|
||||
hillshades.append(np.clip(hs, 0, 1))
|
||||
slope = xp.arctan(xp.sqrt(dx**2 + dy**2))
|
||||
aspect = xp.arctan2(dy, dx)
|
||||
sin_slope = xp.sin(slope)
|
||||
cos_slope = xp.cos(slope)
|
||||
|
||||
combined = np.mean(hillshades, axis=0)
|
||||
_save_tif(output, combined, transform, crs)
|
||||
logger.info(f" ✓ Hillshade terminé ({time.time()-t0:.1f}s)")
|
||||
alt_rad = xp.radians(xp.array(altitude))
|
||||
sin_alt = xp.sin(alt_rad)
|
||||
cos_alt = xp.cos(alt_rad)
|
||||
|
||||
for az in azimuts:
|
||||
az_rad = xp.radians(xp.array(az))
|
||||
hs = sin_alt * sin_slope + cos_alt * cos_slope * xp.cos(az_rad - aspect)
|
||||
hillshades.append(xp.clip(hs, 0, 1))
|
||||
|
||||
combined = xp.mean(xp.array(hillshades), axis=0)
|
||||
_save_tif(output, to_cpu(combined), transform, crs)
|
||||
logger.info(f" ✓ Hillshade terminé ({time.time()-t0:.1f}s){gpu_tag}")
|
||||
return output
|
||||
except Exception as e:
|
||||
logger.error(f" ✗ Erreur hillshade: {e}", exc_info=True)
|
||||
@ -90,17 +96,19 @@ def generate_hillshade(dem_file, basename, vis_dir, resolution):
|
||||
|
||||
|
||||
def generate_slope(dem_file, basename, vis_dir, resolution):
|
||||
"""Generate slope map (degrees)."""
|
||||
logger.info(" → Pente (Slope)...")
|
||||
"""Generate slope map (degrees) — GPU if available."""
|
||||
gpu_tag = " [GPU]" if HAS_GPU else ""
|
||||
logger.info(f" → Pente (Slope){gpu_tag}...")
|
||||
t0 = time.time()
|
||||
output = vis_dir / f"{basename}_slope.tif"
|
||||
|
||||
try:
|
||||
dem, transform, crs = _read_dem(dem_file)
|
||||
dy, dx = np.gradient(dem)
|
||||
slope = np.arctan(np.sqrt(dx**2 + dy**2)) * 180 / np.pi
|
||||
_save_tif(output, slope, transform, crs)
|
||||
logger.info(f" ✓ Pente terminée ({time.time()-t0:.1f}s)")
|
||||
dem_np, transform, crs = _read_dem(dem_file)
|
||||
dem = to_gpu(dem_np)
|
||||
dy, dx = xp.gradient(dem)
|
||||
slope = xp.arctan(xp.sqrt(dx**2 + dy**2)) * 180 / xp.pi
|
||||
_save_tif(output, to_cpu(slope), transform, crs)
|
||||
logger.info(f" ✓ Pente terminée ({time.time()-t0:.1f}s){gpu_tag}")
|
||||
return output
|
||||
except Exception as e:
|
||||
logger.error(f" ✗ Erreur slope: {e}", exc_info=True)
|
||||
@ -108,18 +116,20 @@ def generate_slope(dem_file, basename, vis_dir, resolution):
|
||||
|
||||
|
||||
def generate_aspect(dem_file, basename, vis_dir, resolution):
|
||||
"""Generate aspect (slope orientation) map."""
|
||||
logger.info(" → Aspect (Orientation)...")
|
||||
"""Generate aspect (slope orientation) map — GPU if available."""
|
||||
gpu_tag = " [GPU]" if HAS_GPU else ""
|
||||
logger.info(f" → Aspect (Orientation){gpu_tag}...")
|
||||
t0 = time.time()
|
||||
output = vis_dir / f"{basename}_aspect.tif"
|
||||
|
||||
try:
|
||||
dem, transform, crs = _read_dem(dem_file)
|
||||
dy, dx = np.gradient(dem)
|
||||
aspect = np.arctan2(dy, dx) * 180 / np.pi
|
||||
aspect = np.mod(aspect, 360)
|
||||
_save_tif(output, aspect, transform, crs)
|
||||
logger.info(f" ✓ Aspect terminé ({time.time()-t0:.1f}s)")
|
||||
dem_np, transform, crs = _read_dem(dem_file)
|
||||
dem = to_gpu(dem_np)
|
||||
dy, dx = xp.gradient(dem)
|
||||
aspect = xp.arctan2(dy, dx) * 180 / xp.pi
|
||||
aspect = xp.mod(aspect, 360)
|
||||
_save_tif(output, to_cpu(aspect), transform, crs)
|
||||
logger.info(f" ✓ Aspect terminé ({time.time()-t0:.1f}s){gpu_tag}")
|
||||
return output
|
||||
except Exception as e:
|
||||
logger.error(f" ✗ Erreur aspect: {e}", exc_info=True)
|
||||
@ -127,49 +137,28 @@ def generate_aspect(dem_file, basename, vis_dir, resolution):
|
||||
|
||||
|
||||
def generate_curvature(dem_file, basename, vis_dir, resolution):
|
||||
"""Generate curvature (terrain concavity/convexity) map."""
|
||||
logger.info(" → Courbure (Curvature)...")
|
||||
"""Generate curvature (terrain concavity/convexity) map — GPU if available."""
|
||||
gpu_tag = " [GPU]" if HAS_GPU else ""
|
||||
logger.info(f" → Courbure (Curvature){gpu_tag}...")
|
||||
t0 = time.time()
|
||||
output = vis_dir / f"{basename}_curvature.tif"
|
||||
|
||||
try:
|
||||
dem, transform, crs = _read_dem(dem_file)
|
||||
dz_dx = np.gradient(dem, axis=1)
|
||||
dz_dy = np.gradient(dem, axis=0)
|
||||
d2z_dx2 = np.gradient(dz_dx, axis=1)
|
||||
d2z_dy2 = np.gradient(dz_dy, axis=0)
|
||||
dem_np, transform, crs = _read_dem(dem_file)
|
||||
dem = to_gpu(dem_np)
|
||||
dz_dx = xp.gradient(dem, axis=1)
|
||||
dz_dy = xp.gradient(dem, axis=0)
|
||||
d2z_dx2 = xp.gradient(dz_dx, axis=1)
|
||||
d2z_dy2 = xp.gradient(dz_dy, axis=0)
|
||||
curvature = (d2z_dx2 + d2z_dy2) / 2
|
||||
_save_tif(output, curvature, transform, crs)
|
||||
logger.info(f" ✓ Courbure terminée ({time.time()-t0:.1f}s)")
|
||||
_save_tif(output, to_cpu(curvature), transform, crs)
|
||||
logger.info(f" ✓ Courbure terminée ({time.time()-t0:.1f}s){gpu_tag}")
|
||||
return output
|
||||
except Exception as e:
|
||||
logger.error(f" ✗ Erreur curvature: {e}", exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
def generate_solar(dem_file, basename, vis_dir, resolution):
|
||||
"""Generate solar irradiance simulation."""
|
||||
logger.info(" → Éclairage Solaire...")
|
||||
t0 = time.time()
|
||||
output = vis_dir / f"{basename}_solar.tif"
|
||||
|
||||
try:
|
||||
dem, transform, crs = _read_dem(dem_file)
|
||||
dy, dx = np.gradient(dem)
|
||||
slope = np.arctan(np.sqrt(dx**2 + dy**2))
|
||||
aspect = np.arctan2(dy, dx)
|
||||
az_rad = np.radians(90)
|
||||
alt_rad = np.radians(30)
|
||||
solar = np.sin(alt_rad) * np.sin(slope) + \
|
||||
np.cos(alt_rad) * np.cos(slope) * np.cos(az_rad - aspect)
|
||||
solar = np.clip(solar, 0, 1)
|
||||
_save_tif(output, solar, transform, crs)
|
||||
logger.info(f" ✓ Solaire terminé ({time.time()-t0:.1f}s)")
|
||||
return output
|
||||
except Exception as e:
|
||||
logger.error(f" ✗ Erreur solar: {e}", exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
# ============================================================
|
||||
# GPU-accelerated visualizations
|
||||
@ -390,45 +379,46 @@ def generate_tpi(dem_file, basename, vis_dir, resolution):
|
||||
# ============================================================
|
||||
|
||||
def generate_depressions(dem_file, basename, vis_dir, resolution):
|
||||
"""Depression detection using hydrological sink filling."""
|
||||
logger.info(" → Détection dépressions (hydrologique)...")
|
||||
"""Depression detection using hydrological sink filling — GPU if available."""
|
||||
gpu_tag = " [GPU]" if HAS_GPU else ""
|
||||
logger.info(f" → Détection dépressions (hydrologique){gpu_tag}...")
|
||||
t0 = time.time()
|
||||
output = vis_dir / f"{basename}_depressions.tif"
|
||||
|
||||
try:
|
||||
dem, transform, crs = _read_dem(dem_file)
|
||||
|
||||
from scipy.ndimage import binary_dilation, generate_binary_structure
|
||||
|
||||
dem_filled = dem.copy()
|
||||
nodata_mask = np.isnan(dem_filled)
|
||||
dem_filled[nodata_mask] = np.nanmax(dem) + 1000
|
||||
dem_np, transform, crs = _read_dem(dem_file)
|
||||
dem = to_gpu(dem_np)
|
||||
|
||||
from scipy.ndimage import generate_binary_structure
|
||||
struct = generate_binary_structure(2, 2)
|
||||
|
||||
dem_filled = xp.copy(dem)
|
||||
nodata_mask = xp.isnan(dem_filled)
|
||||
dem_filled[nodata_mask] = xp.nanmax(dem) + 1000
|
||||
|
||||
changed = True
|
||||
iterations = 0
|
||||
max_iter = 100
|
||||
|
||||
while changed and iterations < max_iter:
|
||||
from scipy.ndimage import minimum_filter as scipy_min_filter
|
||||
neighbor_min = scipy_min_filter(dem_filled, footprint=struct)
|
||||
neighbor_min = xp_minimum_filter(dem_filled, footprint=struct)
|
||||
sinks = (dem_filled < neighbor_min) & ~nodata_mask
|
||||
|
||||
if not np.any(sinks):
|
||||
if not xp.any(sinks):
|
||||
break
|
||||
|
||||
new_dem = np.maximum(dem_filled, neighbor_min)
|
||||
new_dem[nodata_mask] = np.nan
|
||||
changed = np.any(new_dem != dem_filled)
|
||||
new_dem = xp.maximum(dem_filled, neighbor_min)
|
||||
new_dem[nodata_mask] = xp.nan
|
||||
changed = bool(xp.any(new_dem != dem_filled))
|
||||
dem_filled = new_dem
|
||||
iterations += 1
|
||||
|
||||
depressions = dem_filled - dem
|
||||
depressions[nodata_mask] = np.nan
|
||||
depressions = to_cpu(dem_filled - dem)
|
||||
depressions[to_cpu(nodata_mask)] = np.nan
|
||||
depressions = np.where(depressions > 0.01, depressions, 0)
|
||||
|
||||
_save_tif(output, depressions, transform, crs)
|
||||
logger.info(f" ✓ Dépressions terminé ({time.time()-t0:.1f}s)")
|
||||
logger.info(f" ✓ Dépressions terminé ({time.time()-t0:.1f}s){gpu_tag}")
|
||||
return output
|
||||
except Exception as e:
|
||||
logger.error(f" ✗ Erreur dépressions: {e}", exc_info=True)
|
||||
@ -489,24 +479,27 @@ def generate_sailore(dem_file, basename, vis_dir, resolution):
|
||||
# ============================================================
|
||||
|
||||
def generate_roughness(dem_file, basename, vis_dir, resolution):
|
||||
"""Surface roughness - standard deviation of elevation in a window."""
|
||||
logger.info(" → Rugosité de surface...")
|
||||
"""Surface roughness - standard deviation of elevation in a window (GPU-accelerated)."""
|
||||
gpu_tag = " [GPU]" if HAS_GPU else ""
|
||||
logger.info(f" → Rugosité de surface{gpu_tag}...")
|
||||
t0 = time.time()
|
||||
output = vis_dir / f"{basename}_roughness.tif"
|
||||
|
||||
try:
|
||||
dem, transform, crs = _read_dem(dem_file)
|
||||
dem_np, transform, crs = _read_dem(dem_file)
|
||||
dem = to_gpu(dem_np.astype(np.float64))
|
||||
window_size = int(5 / resolution)
|
||||
if window_size % 2 == 0:
|
||||
window_size += 1
|
||||
|
||||
def std_filter(arr):
|
||||
return np.nanstd(arr)
|
||||
# Vectorized std: sqrt(E[X²] - (E[X])²) via uniform_filter (GPU-accelerated)
|
||||
local_mean = xp_uniform_filter(dem, size=window_size)
|
||||
local_mean_sq = xp_uniform_filter(dem * dem, size=window_size)
|
||||
roughness = xp.sqrt(local_mean_sq - local_mean * local_mean)
|
||||
|
||||
roughness = generic_filter(dem.astype(np.float64), std_filter,
|
||||
size=window_size, mode='nearest')
|
||||
roughness = to_cpu(roughness)
|
||||
_save_tif(output, roughness, transform, crs)
|
||||
logger.info(f" ✓ Rugosité terminée ({time.time()-t0:.1f}s)")
|
||||
logger.info(f" ✓ Rugosité terminée ({time.time()-t0:.1f}s){gpu_tag}")
|
||||
return output
|
||||
except Exception as e:
|
||||
logger.error(f" ✗ Erreur rugosité: {e}", exc_info=True)
|
||||
@ -518,29 +511,33 @@ def generate_roughness(dem_file, basename, vis_dir, resolution):
|
||||
# ============================================================
|
||||
|
||||
def generate_anomalies(dem_file, basename, vis_dir, resolution):
|
||||
"""Statistical anomaly detection - z-score of local relief + Local Moran's I."""
|
||||
logger.info(" → Détection anomalies statistiques...")
|
||||
"""Statistical anomaly detection - z-score of local relief + Local Moran's I — GPU if available."""
|
||||
gpu_tag = " [GPU]" if HAS_GPU else ""
|
||||
logger.info(f" → Détection anomalies statistiques{gpu_tag}...")
|
||||
t0 = time.time()
|
||||
output = vis_dir / f"{basename}_anomalies.tif"
|
||||
|
||||
try:
|
||||
dem, transform, crs = _read_dem(dem_file)
|
||||
dem_np, transform, crs = _read_dem(dem_file)
|
||||
dem = to_gpu(dem_np)
|
||||
|
||||
lrm = dem - gaussian_filter(dem, sigma=15 / resolution)
|
||||
lrm_mean = np.nanmean(lrm)
|
||||
lrm_std = max(np.nanstd(lrm), 0.01)
|
||||
lrm = dem - xp_gaussian_filter(dem, sigma=15 / resolution)
|
||||
lrm_mean = xp.nanmean(lrm)
|
||||
lrm_std = max(float(xp.nanstd(lrm)), 0.01)
|
||||
z_score = (lrm - lrm_mean) / lrm_std
|
||||
|
||||
window = int(10 / resolution)
|
||||
if window % 2 == 0:
|
||||
window += 1
|
||||
|
||||
local_mean = uniform_filter(z_score, size=window)
|
||||
morans_i = z_score * (local_mean - np.nanmean(z_score)) / np.nanstd(z_score)
|
||||
anomaly_score = np.abs(z_score) * np.sign(morans_i)
|
||||
local_mean = xp_uniform_filter(z_score, size=window)
|
||||
z_mean = xp.nanmean(z_score)
|
||||
z_std = max(float(xp.nanstd(z_score)), 0.01)
|
||||
morans_i = z_score * (local_mean - z_mean) / z_std
|
||||
anomaly_score = xp.abs(z_score) * xp.sign(morans_i)
|
||||
|
||||
_save_tif(output, anomaly_score, transform, crs)
|
||||
logger.info(f" ✓ Anomalies terminé ({time.time()-t0:.1f}s)")
|
||||
_save_tif(output, to_cpu(anomaly_score), transform, crs)
|
||||
logger.info(f" ✓ Anomalies terminé ({time.time()-t0:.1f}s){gpu_tag}")
|
||||
return output
|
||||
except Exception as e:
|
||||
logger.error(f" ✗ Erreur anomalies: {e}", exc_info=True)
|
||||
@ -595,21 +592,24 @@ def generate_wavelet(dem_file, basename, vis_dir, resolution):
|
||||
# ============================================================
|
||||
|
||||
def generate_texture(dem_file, basename, vis_dir, resolution):
|
||||
"""GLCM texture analysis on hillshade - contrast, entropy, homogeneity."""
|
||||
logger.info(" → Texture GLCM...")
|
||||
"""GLCM-inspired texture analysis — contrast, entropy, homogeneity (GPU-accelerated)."""
|
||||
gpu_tag = " [GPU]" if HAS_GPU else ""
|
||||
logger.info(f" → Texture GLCM{gpu_tag}...")
|
||||
t0 = time.time()
|
||||
output = vis_dir / f"{basename}_texture.tif"
|
||||
|
||||
try:
|
||||
dem, transform, crs = _read_dem(dem_file)
|
||||
dem_np, transform, crs = _read_dem(dem_file)
|
||||
|
||||
gy, gx = np.gradient(dem, resolution)
|
||||
# Hillshade — compute on CPU to avoid holding DEM on GPU during texture
|
||||
gy, gx = np.gradient(dem_np, resolution)
|
||||
slope = np.arctan(np.sqrt(gx**2 + gy**2))
|
||||
alt_rad = np.radians(45)
|
||||
az_rad = np.radians(315)
|
||||
aspect = np.arctan2(gy, gx)
|
||||
shading = (np.sin(alt_rad) * np.cos(slope) +
|
||||
np.cos(alt_rad) * np.sin(slope) *
|
||||
np.cos(az_rad - np.arctan2(gy, gx)))
|
||||
np.cos(az_rad - aspect))
|
||||
hillshade = np.clip(shading, 0, 1)
|
||||
|
||||
valid = hillshade[~np.isnan(hillshade)]
|
||||
@ -617,31 +617,39 @@ def generate_texture(dem_file, basename, vis_dir, resolution):
|
||||
raise ValueError("No valid data for texture analysis")
|
||||
lo, hi = np.percentile(valid, (1, 99))
|
||||
img = np.clip((hillshade - lo) / max(hi - lo, 0.001), 0, 1)
|
||||
img_uint8 = (img * 255).astype(np.uint8)
|
||||
del hillshade, shading, slope, aspect, gy, gx # free memory
|
||||
|
||||
window = int(5 / resolution)
|
||||
if window % 2 == 0:
|
||||
window += 1
|
||||
|
||||
def local_variance(arr):
|
||||
return np.var(arr.astype(np.float64))
|
||||
# Contrast (variance) — GPU-accelerated
|
||||
img_gpu = to_gpu(img.astype(np.float32))
|
||||
local_mean = xp_uniform_filter(img_gpu, size=window)
|
||||
local_mean_sq = xp_uniform_filter(img_gpu * img_gpu, size=window)
|
||||
contrast = to_cpu(local_mean_sq - local_mean * local_mean).astype(np.float64)
|
||||
del img_gpu, local_mean, local_mean_sq # free GPU memory
|
||||
|
||||
def local_entropy(arr):
|
||||
hist, _ = np.histogram(arr.astype(np.float64), bins=16, range=(0, 256))
|
||||
hist = hist / max(hist.sum(), 1)
|
||||
hist = hist[hist > 0]
|
||||
return -np.sum(hist * np.log2(hist))
|
||||
# Entropy — compute bin-by-bin to avoid large 3D allocation
|
||||
n_bins = 16
|
||||
img_uint8 = np.clip(img * 255, 0, 255).astype(np.uint8)
|
||||
quantized = (img_uint8 // (256 // n_bins)).astype(np.int32)
|
||||
entropy = np.zeros_like(img, dtype=np.float64)
|
||||
win_area = max(window * window, 1)
|
||||
|
||||
def local_homogeneity(arr):
|
||||
arr_f = arr.astype(np.float64)
|
||||
return np.mean(1.0 / (1.0 + (arr_f - np.mean(arr_f)) ** 2))
|
||||
for b in range(n_bins):
|
||||
plane = (quantized == b).astype(np.float32)
|
||||
plane_gpu = to_gpu(plane)
|
||||
prob_plane = to_cpu(xp_uniform_filter(plane_gpu, size=window))
|
||||
prob_val = prob_plane / win_area
|
||||
prob_val = np.clip(prob_val, 1e-10, None)
|
||||
entropy -= prob_val * np.log2(prob_val)
|
||||
del plane_gpu # free GPU memory per bin
|
||||
|
||||
contrast = generic_filter(img_uint8.astype(np.float64), local_variance,
|
||||
size=window, mode='nearest')
|
||||
entropy = generic_filter(img_uint8.astype(np.float64), local_entropy,
|
||||
size=window, mode='nearest')
|
||||
homogeneity = generic_filter(img_uint8.astype(np.float64), local_homogeneity,
|
||||
size=window, mode='nearest')
|
||||
del quantized, img_uint8 # free CPU memory
|
||||
|
||||
# Homogeneity — 1 / (1 + variance)
|
||||
homogeneity = 1.0 / (1.0 + contrast)
|
||||
|
||||
def norm(arr):
|
||||
valid_arr = arr[~np.isnan(arr)]
|
||||
@ -650,12 +658,10 @@ def generate_texture(dem_file, basename, vis_dir, resolution):
|
||||
std_val = max(np.std(valid_arr), 0.01)
|
||||
return (arr - np.mean(valid_arr)) / std_val
|
||||
|
||||
texture_combined = (0.4 * norm(contrast) +
|
||||
0.4 * norm(entropy) -
|
||||
0.2 * norm(homogeneity))
|
||||
texture_combined = 0.4 * norm(contrast) + 0.4 * norm(entropy) - 0.2 * norm(homogeneity)
|
||||
|
||||
_save_tif(output, texture_combined, transform, crs)
|
||||
logger.info(f" ✓ Texture terminée ({time.time()-t0:.1f}s)")
|
||||
logger.info(f" ✓ Texture terminée ({time.time()-t0:.1f}s){gpu_tag}")
|
||||
return output
|
||||
except Exception as e:
|
||||
logger.error(f" ✗ Erreur texture GLCM: {e}", exc_info=True)
|
||||
@ -667,55 +673,58 @@ def generate_texture(dem_file, basename, vis_dir, resolution):
|
||||
# ============================================================
|
||||
|
||||
def generate_flow(dem_file, basename, vis_dir, resolution):
|
||||
"""Flow accumulation using D8 algorithm.
|
||||
|
||||
Identifies drainage patterns, ditches, and enclosure boundaries.
|
||||
"""
|
||||
logger.info(" → Accumulation de flux D8...")
|
||||
"""Flow accumulation using D8 algorithm — sink filling on GPU, accumulation on CPU."""
|
||||
gpu_tag = " [GPU]" if HAS_GPU else ""
|
||||
logger.info(f" → Accumulation de flux D8{gpu_tag}...")
|
||||
t0 = time.time()
|
||||
output = vis_dir / f"{basename}_flow.tif"
|
||||
|
||||
try:
|
||||
dem, transform, crs = _read_dem(dem_file)
|
||||
rows, cols = dem.shape
|
||||
nodata_mask = np.isnan(dem)
|
||||
dem_np, transform, crs = _read_dem(dem_file)
|
||||
rows, cols = dem_np.shape
|
||||
nodata_mask = np.isnan(dem_np)
|
||||
|
||||
from scipy.ndimage import minimum_filter as scipy_min_filter, generate_binary_structure
|
||||
|
||||
dem_filled = dem.copy()
|
||||
dem_filled[nodata_mask] = np.nanmax(dem) + 1000
|
||||
# Sink filling — GPU-accelerated
|
||||
dem_gpu = to_gpu(dem_np)
|
||||
nodata_mask_gpu = xp.isnan(dem_gpu)
|
||||
dem_filled = xp.copy(dem_gpu)
|
||||
dem_filled[nodata_mask_gpu] = xp.nanmax(dem_gpu) + 1000
|
||||
|
||||
from scipy.ndimage import generate_binary_structure
|
||||
struct = generate_binary_structure(2, 2)
|
||||
|
||||
for _ in range(50):
|
||||
neighbor_min = scipy_min_filter(dem_filled, footprint=struct)
|
||||
sinks = (dem_filled < neighbor_min) & ~nodata_mask
|
||||
if not np.any(sinks):
|
||||
neighbor_min = xp_minimum_filter(dem_filled, footprint=struct)
|
||||
sinks = (dem_filled < neighbor_min) & ~nodata_mask_gpu
|
||||
if not xp.any(sinks):
|
||||
break
|
||||
dem_filled = np.where(sinks, neighbor_min, dem_filled)
|
||||
dem_filled = xp.where(sinks, neighbor_min, dem_filled)
|
||||
|
||||
dem_filled[nodata_mask] = np.nan
|
||||
dem_filled[nodata_mask_gpu] = xp.nan
|
||||
dem_filled_np = to_cpu(dem_filled)
|
||||
|
||||
# D8 slope + accumulation — CPU (sequential by nature)
|
||||
dx8 = [1, 1, 0, -1, -1, -1, 0, 1]
|
||||
dy8 = [0, 1, 1, 1, 0, -1, -1, -1]
|
||||
dist8 = [1.0, np.sqrt(2), 1.0, np.sqrt(2), 1.0, np.sqrt(2), 1.0, np.sqrt(2)]
|
||||
|
||||
flow_dir = np.full((rows, cols), -1, dtype=np.int8)
|
||||
max_slope = np.full((rows, cols), 0.0)
|
||||
max_slope = np.zeros((rows, cols), dtype=np.float64)
|
||||
|
||||
padded = np.pad(dem_filled, 1, mode='constant',
|
||||
constant_values=np.nanmax(dem_filled) + 10000)
|
||||
padded = np.pad(dem_filled_np, 1, mode='constant',
|
||||
constant_values=np.nanmax(dem_filled_np[~np.isnan(dem_filled_np)]) + 10000)
|
||||
|
||||
for d in range(8):
|
||||
nx = 1 + dx8[d]
|
||||
ny = 1 + dy8[d]
|
||||
neighbor_elev = padded[ny:ny + rows, nx:nx + cols]
|
||||
slope = (dem_filled - neighbor_elev) / (dist8[d] * resolution)
|
||||
slope = (dem_filled_np - neighbor_elev) / (dist8[d] * resolution)
|
||||
slope[nodata_mask] = -1
|
||||
better = slope > max_slope
|
||||
flow_dir[better] = d
|
||||
max_slope[better] = slope[better]
|
||||
|
||||
flat_dem = dem_filled[~nodata_mask].flatten()
|
||||
flat_dem = dem_filled_np[~nodata_mask].flatten()
|
||||
valid_indices = np.where(~nodata_mask.flatten())[0]
|
||||
sort_order = valid_indices[np.argsort(-flat_dem)]
|
||||
|
||||
@ -733,7 +742,7 @@ def generate_flow(dem_file, basename, vis_dir, resolution):
|
||||
|
||||
flow_log = np.log1p(flow_acc)
|
||||
_save_tif(output, flow_log, transform, crs)
|
||||
logger.info(f" ✓ Flux terminé ({time.time()-t0:.1f}s)")
|
||||
logger.info(f" ✓ Flux terminé ({time.time()-t0:.1f}s){gpu_tag}")
|
||||
return output
|
||||
except Exception as e:
|
||||
logger.error(f" ✗ Erreur flux: {e}", exc_info=True)
|
||||
|
||||
Reference in New Issue
Block a user