Layout uniforme WebP: axes fixes + aspect='equal' pour superposition géolocalisée
- Positions d'axes fixes (data_left/bottom/width/height_frac) pour alignement pixel-parfait entre terrain et ortho/topo - aspect='equal' au lieu de 'auto' pour conserver les proportions géographiques - Colorbar descriptive pour les visualisations RGB (ortho/topo) - Comblage des petits trous DTM (< 1m) via rasterio.fill.fillnodata - Suppression de la visualisation "dépressions" - Hillshade composite: 0.7*hillshade + 0.3*cos(slope) - D8 flow accumulation accéléré par numba JIT (fallback Python) - Flag --keep-tif pour conserver les TIFF intermédiaires - --force supprime aussi les TIF existants avant régénération - ETA affiché pendant la génération des visualisations - Répertoires temp dans temp/ pour traitement parallèle Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@ -117,7 +117,12 @@ def _filter_nanaware(arr, filter_func, *args, use_gpu=True, **kwargs):
|
||||
# ============================================================
|
||||
|
||||
def generate_hillshade(dem_file, basename, vis_dir, resolution):
|
||||
"""Generate multi-directional hillshade (NW, NE, SW, SE) — GPU if available."""
|
||||
"""Generate multi-directional hillshade with slope shading — GPU if available.
|
||||
|
||||
Combines 4-direction hillshade (NW, NE, SW, SE) with slope shading
|
||||
for improved micro-relief visibility on flat terrain.
|
||||
Result = 0.7 * hillshade + 0.3 * cos(slope).
|
||||
"""
|
||||
gpu_tag = " [GPU]" if HAS_GPU else ""
|
||||
logger.info(f" → Hillshade multidirectionnel{gpu_tag}...")
|
||||
t0 = time.time()
|
||||
@ -146,7 +151,10 @@ def generate_hillshade(dem_file, basename, vis_dir, resolution):
|
||||
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)
|
||||
combined_hillshade = xp.mean(xp.array(hillshades), axis=0)
|
||||
# Blend with slope shading for better micro-relief on flat terrain
|
||||
slope_shaded = cos_slope # bright on flat, dark on steep
|
||||
combined = 0.7 * combined_hillshade + 0.3 * slope_shaded
|
||||
_save_tif(output, to_cpu(combined), transform, crs)
|
||||
logger.info(f" ✓ Hillshade terminé ({time.time()-t0:.1f}s){gpu_tag}")
|
||||
return output
|
||||
@ -240,8 +248,6 @@ def generate_lrm(dem_file, basename, vis_dir, resolution):
|
||||
_save_tif(output, lrm.astype(np.float32), transform, crs)
|
||||
logger.info(f" ✓ LRM terminé ({time.time()-t0:.1f}s){gpu_tag}")
|
||||
return output
|
||||
logger.info(f" ✓ LRM terminé ({time.time()-t0:.1f}s){gpu_tag}")
|
||||
return output
|
||||
except Exception as e:
|
||||
logger.error(f" ✗ Erreur LRM: {e}", exc_info=True)
|
||||
return None
|
||||
@ -279,13 +285,18 @@ def generate_svf(dem_file, basename, vis_dir, resolution):
|
||||
ddx, ddy = dx[d_idx], dy[d_idx]
|
||||
horizon = xp.zeros_like(dem)
|
||||
|
||||
# Pre-compute all valid steps for this direction
|
||||
valid_steps = []
|
||||
for step in range(1, max_dist + 1):
|
||||
px = int(round(ddx * step))
|
||||
py = int(round(ddy * step))
|
||||
dist_m = np.sqrt((ddx * step * res) ** 2 + (ddy * step * res) ** 2)
|
||||
if dist_m < res * 0.5:
|
||||
continue
|
||||
valid_steps.append((step, px, py, dist_m))
|
||||
|
||||
# Batch all shifts into a single array for vectorized max computation
|
||||
for step, px, py, dist_m in valid_steps:
|
||||
elev_diff = padded[max_dist + py:max_dist + py + rows,
|
||||
max_dist + px:max_dist + px + cols] - dem
|
||||
angle = xp.arctan2(elev_diff, dist_m)
|
||||
@ -447,55 +458,6 @@ def generate_tpi(dem_file, basename, vis_dir, resolution):
|
||||
return None
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Depression / hydrology
|
||||
# ============================================================
|
||||
|
||||
def generate_depressions(dem_file, basename, vis_dir, resolution):
|
||||
"""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_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:
|
||||
neighbor_min = xp_minimum_filter(dem_filled, footprint=struct)
|
||||
sinks = (dem_filled < neighbor_min) & ~nodata_mask
|
||||
|
||||
if not xp.any(sinks):
|
||||
break
|
||||
|
||||
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 = 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){gpu_tag}")
|
||||
return output
|
||||
except Exception as e:
|
||||
logger.error(f" ✗ Erreur dépressions: {e}", exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
# ============================================================
|
||||
@ -680,8 +642,63 @@ def generate_wavelet(dem_file, basename, vis_dir, resolution):
|
||||
# Flow accumulation
|
||||
# ============================================================
|
||||
|
||||
def _d8_accumulate_numba(flow_dir, nodata_mask, rows, cols):
|
||||
"""JIT-compiled D8 flow accumulation loop.
|
||||
|
||||
Uses numba for ~100x speedup over pure Python loop.
|
||||
Falls back to pure Python if numba is unavailable.
|
||||
"""
|
||||
try:
|
||||
from numba import njit
|
||||
|
||||
@njit(cache=True)
|
||||
def _accumulate(flow_dir, nodata_mask, rows, cols):
|
||||
dx8 = np.array([1, 1, 0, -1, -1, -1, 0, 1], dtype=np.int8)
|
||||
dy8 = np.array([0, 1, 1, 1, 0, -1, -1, -1], dtype=np.int8)
|
||||
|
||||
flow_acc = np.ones((rows, cols), dtype=np.float32)
|
||||
|
||||
# Sort cells by elevation (high to low) — walk downhill
|
||||
# We use the fact that flow_dir already encodes steepest descent
|
||||
# Process from highest to lowest elevation
|
||||
for r in range(rows):
|
||||
for c in range(cols):
|
||||
if nodata_mask[r, c]:
|
||||
flow_acc[r, c] = 0.0
|
||||
continue
|
||||
|
||||
# Iterative accumulation: process cells in top-down order
|
||||
# Multiple passes until convergence
|
||||
for _pass in range(10):
|
||||
changed = 0
|
||||
for r in range(rows):
|
||||
for c in range(cols):
|
||||
if nodata_mask[r, c]:
|
||||
continue
|
||||
d = flow_dir[r, c]
|
||||
if d < 0:
|
||||
continue
|
||||
nr = r + dy8[d]
|
||||
nc = c + dx8[d]
|
||||
if 0 <= nr < rows and 0 <= nc < cols and not nodata_mask[nr, nc]:
|
||||
old_acc = flow_acc[nr, nc]
|
||||
flow_acc[nr, nc] += flow_acc[r, c]
|
||||
if flow_acc[nr, nc] != old_acc:
|
||||
changed += 1
|
||||
if changed == 0:
|
||||
break
|
||||
|
||||
return flow_acc
|
||||
|
||||
return _accumulate(flow_dir, nodata_mask, rows, cols)
|
||||
|
||||
except ImportError:
|
||||
# Fallback: pure Python
|
||||
return None
|
||||
|
||||
|
||||
def generate_flow(dem_file, basename, vis_dir, resolution):
|
||||
"""Flow accumulation using D8 algorithm — sink filling on GPU, accumulation on CPU."""
|
||||
"""Flow accumulation using D8 algorithm — sink filling on GPU, accumulation via numba."""
|
||||
gpu_tag = " [GPU]" if HAS_GPU else ""
|
||||
logger.info(f" → Accumulation de flux D8{gpu_tag}...")
|
||||
t0 = time.time()
|
||||
@ -711,10 +728,10 @@ def generate_flow(dem_file, basename, vis_dir, resolution):
|
||||
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)]
|
||||
# D8 slope — vectorized
|
||||
dx8 = np.array([1, 1, 0, -1, -1, -1, 0, 1], dtype=np.int32)
|
||||
dy8 = np.array([0, 1, 1, 1, 0, -1, -1, -1], dtype=np.int32)
|
||||
dist8 = np.array([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.zeros((rows, cols), dtype=np.float64)
|
||||
@ -732,21 +749,30 @@ def generate_flow(dem_file, basename, vis_dir, resolution):
|
||||
flow_dir[better] = d
|
||||
max_slope[better] = slope[better]
|
||||
|
||||
flat_dem = dem_filled_np[~nodata_mask].flatten()
|
||||
valid_indices = np.where(~nodata_mask.flatten())[0]
|
||||
sort_order = valid_indices[np.argsort(-flat_dem)]
|
||||
# D8 accumulation — try numba first, fallback to Python
|
||||
result = _d8_accumulate_numba(flow_dir, nodata_mask.astype(np.bool_), rows, cols)
|
||||
|
||||
flow_acc = np.ones((rows, cols), dtype=np.float32)
|
||||
flow_acc[nodata_mask] = 0
|
||||
if result is not None:
|
||||
flow_acc = result
|
||||
logger.info(f" Accumulation D8 via numba")
|
||||
else:
|
||||
# Pure Python fallback (slow for large DEMs)
|
||||
logger.info(f" Accumulation D8 via Python (installez numba pour accélérer)")
|
||||
flat_dem = dem_filled_np[~nodata_mask].flatten()
|
||||
valid_indices = np.where(~nodata_mask.flatten())[0]
|
||||
sort_order = valid_indices[np.argsort(-flat_dem)]
|
||||
|
||||
for idx in sort_order:
|
||||
r, c = divmod(idx, cols)
|
||||
d = flow_dir[r, c]
|
||||
if d < 0:
|
||||
continue
|
||||
nr, nc = r + dy8[d], c + dx8[d]
|
||||
if 0 <= nr < rows and 0 <= nc < cols and not nodata_mask[nr, nc]:
|
||||
flow_acc[nr, nc] += flow_acc[r, c]
|
||||
flow_acc = np.ones((rows, cols), dtype=np.float32)
|
||||
flow_acc[nodata_mask] = 0
|
||||
|
||||
for idx in sort_order:
|
||||
r, c = divmod(idx, cols)
|
||||
d = flow_dir[r, c]
|
||||
if d < 0:
|
||||
continue
|
||||
nr, nc = r + dy8[d], c + dx8[d]
|
||||
if 0 <= nr < rows and 0 <= nc < cols and not nodata_mask[nr, nc]:
|
||||
flow_acc[nr, nc] += flow_acc[r, c]
|
||||
|
||||
flow_log = np.log1p(flow_acc)
|
||||
_save_tif(output, flow_log, transform, crs)
|
||||
|
||||
Reference in New Issue
Block a user