Ajouter la couche relief orienté et une échelle d'openness fixe
Nouvelle visualisation relief_oriente : une image RGB unique qui fusionne l'openness positive locale (MNT détendancé σ 10 m, rayons 5/10/20 m, 16 directions) portée par la clarté CIELAB et l'orientation des pentes portée par la teinte. Échelle log fixe et support de 40 m sous la bande de raccord de 100 m : dalles jointives. Calcul sur grille décimée à 0,8 m, noyau dédié (CuPy RawKernel, numba parallèle, repli numpy) et colorisation par table L* × teinte : ~8 s par dalle sur CPU au lieu de ~50 s. L'openness positive et négative est normalisée par des références figées mesurées sur 15 dalles au lieu d'un z-score par dalle, qui rendait l'échelle de couleur non jointive. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@ -638,6 +638,19 @@ def generate_svf(dem_file, basename, vis_dir, resolution, shared=None):
|
||||
# ; facteur 2 ≈ ×8 plus rapide, rendu quasi identique. 1 = pleine résolution.
|
||||
OPENNESS_DOWNSAMPLE = 2
|
||||
|
||||
# Références de normalisation de l'openness (degrés : moyenne, écart-type).
|
||||
# Le z-score par tuile rendait l'échelle non jointive — même ouverture
|
||||
# physique, couleur différente d'une dalle à l'autre selon le relief
|
||||
# environnant. Médianes inter-tuiles mesurées à 0,2 m (décimation ×2) sur
|
||||
# des dalles réparties sur le territoire (plaine, bocage, forêt, montagne,
|
||||
# volcans, delta, urbain, littoral). Références FIGÉES : même ouverture =
|
||||
# même couleur sur toutes les tuiles.
|
||||
# Rayons du lancé de rayons (mètres), moyennés à poids égaux.
|
||||
OPENNESS_RADII_M = (25, 50, 100)
|
||||
|
||||
OPENNESS_POS_REF = (5.571, 4.112)
|
||||
OPENNESS_NEG_REF = (5.974, 4.070)
|
||||
|
||||
|
||||
def generate_openness(dem_file, basename, vis_dir, resolution, positive=True, shared=None):
|
||||
"""Positive/Negative Openness - multi-radius ray-tracing with std normalization.
|
||||
@ -645,7 +658,8 @@ def generate_openness(dem_file, basename, vis_dir, resolution, positive=True, sh
|
||||
Traces rays in 8 directions at 3 radii (25, 50, 100m) on a block-decimated
|
||||
grid (cf. OPENNESS_DOWNSAMPLE), then bilinearly resamples the result back
|
||||
to the requested resolution. Results are combined with equal weight across
|
||||
radii, then normalized by standard deviation for cross-tile comparability.
|
||||
radii, then normalized by FIXED references (OPENNESS_POS_REF /
|
||||
OPENNESS_NEG_REF, degrees) so that adjacent tiles share one colour scale.
|
||||
"""
|
||||
name = "positive_openness" if positive else "negative_openness"
|
||||
gpu_tag = " [GPU]" if _gpu_mod.HAS_GPU else ""
|
||||
@ -657,7 +671,7 @@ def generate_openness(dem_file, basename, vis_dir, resolution, positive=True, sh
|
||||
dem, dem_np, rows, cols, res, nan_mask, transform, crs = \
|
||||
_prepare_dem_for_raycast(dem_file, shared, resolution)
|
||||
|
||||
radii_m = [25, 50, 100]
|
||||
radii_m = list(OPENNESS_RADII_M)
|
||||
n_dirs = 8
|
||||
full_rows, full_cols = rows, cols
|
||||
|
||||
@ -699,12 +713,10 @@ def generate_openness(dem_file, basename, vis_dir, resolution, positive=True, sh
|
||||
openness_result = zoomed.astype(np.float32)
|
||||
openness_result[nan_mask] = np.nan
|
||||
|
||||
# Z-score (écarts locaux en sigmas) : unités comparables entre tuiles,
|
||||
# plage de rendu fixe → mosaïque de couleur homogène
|
||||
valid = openness_result[~nan_mask]
|
||||
if len(valid) > 0:
|
||||
std_val = max(np.nanstd(valid), 0.01)
|
||||
openness_result = (openness_result - np.nanmean(valid)) / std_val
|
||||
# Écart aux références figées, en sigmas : même ouverture physique =
|
||||
# même valeur sur toutes les tuiles → mosaïque de couleur jointive
|
||||
ref_mean, ref_std = OPENNESS_POS_REF if positive else OPENNESS_NEG_REF
|
||||
openness_result = (openness_result - ref_mean) / ref_std
|
||||
|
||||
_save_tif(output, openness_result, transform, crs)
|
||||
logger.info(f" ✓ {name} terminé ({time.time()-t0:.1f}s){' [GPU]' if _gpu_mod.HAS_GPU else ''}")
|
||||
@ -714,6 +726,384 @@ def generate_openness(dem_file, basename, vis_dir, resolution, positive=True, sh
|
||||
return None
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Relief orienté : openness locale × aspect en une seule image RGB
|
||||
# ============================================================
|
||||
#
|
||||
# Clarté (CIELAB L*) = micro-relief : openness positive calculée sur le MNT
|
||||
# détendancé (MNT − gaussienne 10 m), rayons courts 5/10/20 m, plus un léger
|
||||
# ombrage directionnel. Teinte = orientation de la pente (aspect) sur le
|
||||
# cercle CIELAB, à clarté constante : aucune couleur ne crée de faux relief.
|
||||
# Pas de statistique par dalle (échelle log FIXE) et un support total
|
||||
# (rayon 20 m + lissage 4σ = 40 m) inférieur à la bande de raccord de 100 m :
|
||||
# les dalles adjacentes se raccordent sans couture.
|
||||
#
|
||||
# Coût maîtrisé : détendance et lancé de rayons tournent sur une grille
|
||||
# décimée à ~0,8 m (bloc max, cf. OPENNESS_DOWNSAMPLE) — seule l'openness
|
||||
# finale est agrandie ; noyau dédié qui n'accumule que la moyenne des angles
|
||||
# (CuPy RawKernel sur GPU, numba parallèle sur CPU, numpy en dernier
|
||||
# recours) ; couleurs par table pré-calculée (L* × teinte) au lieu d'une
|
||||
# conversion Lab → sRGB par pixel.
|
||||
RELIEF_DETREND_M = 10.0 # σ de la gaussienne retirée au MNT
|
||||
RELIEF_RADII_M = (5, 10, 20) # rayons du lancé de rayons, poids égaux
|
||||
RELIEF_N_DIRS = 16 # 16 directions : pas de losanges à 8 branches
|
||||
RELIEF_GRID_M = 0.8 # pas de la grille de calcul décimée
|
||||
RELIEF_OPEN_RANGE = (0.5, 12.0) # degrés, échelle log fixe (jointive)
|
||||
RELIEF_CHROMA = 60.0 # chroma CIELAB max (atteint vers L* = 50)
|
||||
RELIEF_SHADE_WEIGHT = 0.35 # part de l'ombrage directionnel dans L*
|
||||
# Azimut de l'ombrage, exprimé dans le repère de l'aspect ci-dessous
|
||||
# (arctan2(dy, dx), dy vers le sud) — réglage validé sur la galerie de rendus.
|
||||
RELIEF_SHADE_AZIMUTH = 315.0
|
||||
RELIEF_SHADE_ALTITUDE = 45.0
|
||||
RELIEF_NODATA_RGB = (38, 38, 41)
|
||||
|
||||
_RELIEF_LUT = None
|
||||
|
||||
|
||||
def _lab_to_srgb(L, a, b):
|
||||
"""CIELAB (D65) → sRGB [0, 1], écrêté."""
|
||||
fy = (L + 16) / 116
|
||||
fx = fy + a / 500
|
||||
fz = fy - b / 200
|
||||
finv = lambda t: np.where(t > 6 / 29, t ** 3, 3 * (6 / 29) ** 2 * (t - 4 / 29))
|
||||
X, Y, Z = 0.95047 * finv(fx), finv(fy), 1.08883 * finv(fz)
|
||||
rgb = np.stack([3.2406 * X - 1.5372 * Y - 0.4986 * Z,
|
||||
-0.9689 * X + 1.8758 * Y + 0.0415 * Z,
|
||||
0.0557 * X - 0.2040 * Y + 1.0570 * Z], -1)
|
||||
rgb = np.where(rgb <= 0.0031308, 12.92 * rgb,
|
||||
1.055 * np.clip(rgb, 0, None) ** (1 / 2.4) - 0.055)
|
||||
return np.clip(rgb, 0, 1)
|
||||
|
||||
|
||||
def _relief_lut():
|
||||
"""Table (256 niveaux de L*, 360 teintes) → sRGB uint8, calculée une fois.
|
||||
|
||||
La chroma décroît vers le noir et le blanc (L*(100−L*)/2500) : les
|
||||
couleurs restent dans la gamme sRGB au lieu d'être écrêtées.
|
||||
"""
|
||||
global _RELIEF_LUT
|
||||
if _RELIEF_LUT is None:
|
||||
L = np.linspace(0, 100, 256)[:, None]
|
||||
h = np.radians(np.arange(360))[None, :]
|
||||
C = RELIEF_CHROMA * np.clip(L * (100 - L) / 2500.0, 0, 1)
|
||||
_RELIEF_LUT = (_lab_to_srgb(np.broadcast_to(L, (256, 360)), C * np.cos(h), C * np.sin(h))
|
||||
* 255 + 0.5).astype(np.uint8)
|
||||
return _RELIEF_LUT
|
||||
|
||||
|
||||
def _horizon_rays(res, n_dirs, radii_m):
|
||||
"""Décalages (col, ligne) de chaque pas de rayon, distances et checkpoints.
|
||||
|
||||
Même géométrie que _ray_trace_horizons_core : direction k à l'angle
|
||||
2πk/n, pas arrondis au pixel, distance = pas × résolution.
|
||||
"""
|
||||
max_step = max(1, int(max(radii_m) / res))
|
||||
angles = np.linspace(0, 2 * np.pi, n_dirs, endpoint=False)
|
||||
steps = np.arange(1, max_step + 1)
|
||||
offs = np.empty((n_dirs, max_step, 2), dtype=np.int32)
|
||||
offs[:, :, 0] = np.rint(np.cos(angles)[:, None] * steps)
|
||||
offs[:, :, 1] = np.rint(np.sin(angles)[:, None] * steps)
|
||||
dist = (steps * res).astype(np.float32)
|
||||
cps = np.array(sorted(max(1, min(int(r / res), max_step)) for r in radii_m), dtype=np.int32)
|
||||
return offs, dist, cps
|
||||
|
||||
|
||||
_HORIZON_CUDA_SRC = r'''
|
||||
extern "C" __global__
|
||||
void mean_horizon(const float* dem, const int* offs, const float* dist,
|
||||
const int* cps, const int rows, const int cols,
|
||||
const int nd, const int ns, const int nr, float* out) {
|
||||
long idx = (long)blockDim.x * blockIdx.x + threadIdx.x;
|
||||
if (idx >= (long)rows * cols) return;
|
||||
int i = idx / cols, j = idx % cols;
|
||||
float z0 = dem[idx], acc = 0.f;
|
||||
for (int d = 0; d < nd; ++d) {
|
||||
float run = 0.f;
|
||||
int k = 0;
|
||||
for (int s = 0; s < ns; ++s) {
|
||||
int ii = i + offs[(d * ns + s) * 2 + 1];
|
||||
int jj = j + offs[(d * ns + s) * 2];
|
||||
if (ii >= 0 && ii < rows && jj >= 0 && jj < cols)
|
||||
run = fmaxf(run, (dem[(long)ii * cols + jj] - z0) / dist[s]);
|
||||
while (k < nr && s + 1 >= cps[k]) { acc += atanf(run); ++k; }
|
||||
}
|
||||
while (k < nr) { acc += atanf(run); ++k; }
|
||||
}
|
||||
out[idx] = acc / (float)(nd * nr);
|
||||
}
|
||||
'''
|
||||
_horizon_cuda_kernel = None
|
||||
_horizon_numba_kernel = None
|
||||
|
||||
|
||||
def _mean_horizon_gpu(dem, offs, dist, cps):
|
||||
"""Un thread CUDA par pixel ; tout reste en registres (aucun tableau
|
||||
intermédiaire par direction ou par rayon)."""
|
||||
global _horizon_cuda_kernel
|
||||
cp = _gpu_mod._cp
|
||||
if _horizon_cuda_kernel is None:
|
||||
_horizon_cuda_kernel = cp.RawKernel(_HORIZON_CUDA_SRC, 'mean_horizon')
|
||||
rows, cols = dem.shape
|
||||
d_dem = cp.ascontiguousarray(cp.asarray(dem, dtype=cp.float32))
|
||||
out = cp.empty((rows, cols), dtype=cp.float32)
|
||||
n = rows * cols
|
||||
threads = 256
|
||||
_horizon_cuda_kernel(((n + threads - 1) // threads,), (threads,),
|
||||
(d_dem, cp.asarray(offs), cp.asarray(dist), cp.asarray(cps),
|
||||
np.int32(rows), np.int32(cols), np.int32(offs.shape[0]),
|
||||
np.int32(offs.shape[1]), np.int32(len(cps)), out))
|
||||
return out
|
||||
|
||||
|
||||
def _mean_horizon_numba(dem, offs, dist, cps):
|
||||
"""Même noyau en numba parallèle (une ligne de pixels par thread CPU)."""
|
||||
global _horizon_numba_kernel
|
||||
if _horizon_numba_kernel is None:
|
||||
from numba import njit, prange
|
||||
|
||||
@njit(parallel=True, cache=True, fastmath=True)
|
||||
def _kernel(dem, offs, dist, cps):
|
||||
rows, cols = dem.shape
|
||||
nd, ns, nr = offs.shape[0], offs.shape[1], cps.shape[0]
|
||||
out = np.empty((rows, cols), dtype=np.float32)
|
||||
for i in prange(rows):
|
||||
for j in range(cols):
|
||||
z0 = dem[i, j]
|
||||
acc = 0.0
|
||||
for d in range(nd):
|
||||
run = 0.0
|
||||
k = 0
|
||||
for s in range(ns):
|
||||
ii = i + offs[d, s, 1]
|
||||
jj = j + offs[d, s, 0]
|
||||
if ii >= 0 and ii < rows and jj >= 0 and jj < cols:
|
||||
t = (dem[ii, jj] - z0) / dist[s]
|
||||
if t > run:
|
||||
run = t
|
||||
while k < nr and s + 1 >= cps[k]:
|
||||
acc += math.atan(run)
|
||||
k += 1
|
||||
while k < nr:
|
||||
acc += math.atan(run)
|
||||
k += 1
|
||||
out[i, j] = acc / (nd * nr)
|
||||
return out
|
||||
_horizon_numba_kernel = _kernel
|
||||
return _horizon_numba_kernel(np.ascontiguousarray(dem, dtype=np.float32), offs, dist, cps)
|
||||
|
||||
|
||||
def _mean_horizon_numpy(dem, offs, dist, cps):
|
||||
"""Repli vectorisé (sans numba ni GPU) : un décalage de tableau par pas."""
|
||||
rows, cols = dem.shape
|
||||
pad = int(np.abs(offs).max())
|
||||
padded = np.pad(dem.astype(np.float32), pad, constant_values=np.nan)
|
||||
acc = np.zeros((rows, cols), dtype=np.float64)
|
||||
for d in range(offs.shape[0]):
|
||||
run = np.zeros((rows, cols), dtype=np.float32)
|
||||
k = 0
|
||||
for s in range(offs.shape[1]):
|
||||
px, py = offs[d, s]
|
||||
view = padded[pad + py:pad + py + rows, pad + px:pad + px + cols]
|
||||
run = np.fmax(run, (view - dem) / dist[s])
|
||||
while k < len(cps) and s + 1 >= cps[k]:
|
||||
acc += np.arctan(run)
|
||||
k += 1
|
||||
while k < len(cps):
|
||||
acc += np.arctan(run)
|
||||
k += 1
|
||||
return (acc / (offs.shape[0] * len(cps))).astype(np.float32)
|
||||
|
||||
|
||||
def _mean_horizon_angle(dem, res, n_dirs, radii_m):
|
||||
"""Moyenne (directions × rayons) de l'angle d'horizon positif, en radians.
|
||||
|
||||
GPU (CuPy RawKernel) si disponible — le résultat reste alors sur le GPU —,
|
||||
sinon numba, sinon numpy. Un échec GPU (compilation NVRTC, VRAM) bascule
|
||||
sur le CPU pour ce calcul sans perdre la visualisation.
|
||||
"""
|
||||
offs, dist, cps = _horizon_rays(res, n_dirs, radii_m)
|
||||
if _gpu_mod.is_gpu_active():
|
||||
try:
|
||||
return _mean_horizon_gpu(dem, offs, dist, cps), "GPU"
|
||||
except Exception as e:
|
||||
logger.warning(f" ⚠ Noyau GPU relief orienté indisponible ({e}) — repli CPU")
|
||||
dem = to_cpu(dem)
|
||||
try:
|
||||
return _mean_horizon_numba(dem, offs, dist, cps), "numba"
|
||||
except ImportError:
|
||||
return _mean_horizon_numpy(dem, offs, dist, cps), "numpy"
|
||||
|
||||
|
||||
def _pad_to(m, arr, rows, cols):
|
||||
"""Complète par répétition du bord (dimensions non multiples du facteur)."""
|
||||
if arr.shape == (rows, cols):
|
||||
return arr
|
||||
return m.pad(arr[:rows, :cols], ((0, max(0, rows - arr.shape[0])), (0, max(0, cols - arr.shape[1]))),
|
||||
mode='edge')
|
||||
|
||||
|
||||
def _relief_params():
|
||||
"""Constantes scalaires du rendu (ombrage sans trigonométrie par pixel).
|
||||
|
||||
cos(pente) = 1/√(1+g²) et sin(pente)·cos(az − aspect) = (cos az·dx +
|
||||
sin az·dy)/√(1+g²), avec aspect = arctan2(dy, dx) : l'ombrage ne coûte
|
||||
qu'une racine par pixel.
|
||||
"""
|
||||
zen = math.radians(90.0 - RELIEF_SHADE_ALTITUDE)
|
||||
az = math.radians(RELIEF_SHADE_AZIMUTH)
|
||||
lo, hi = RELIEF_OPEN_RANGE
|
||||
return (math.cos(zen), math.sin(zen) * math.cos(az), math.sin(zen) * math.sin(az),
|
||||
lo, 1.0 / math.log(hi / lo), RELIEF_SHADE_WEIGHT)
|
||||
|
||||
|
||||
def _relief_colorize_xp(m, openness, dx, dy, lut):
|
||||
"""Colorisation vectorisée (CuPy sur GPU, numpy en repli)."""
|
||||
cz, sa_x, sa_y, lo, inv_log, w = _relief_params()
|
||||
inv = 1.0 / m.sqrt(1.0 + dx * dx + dy * dy)
|
||||
shade = m.clip((cz + sa_x * dx + sa_y * dy) * inv / cz, 0, 1.25) / 1.25
|
||||
t = m.clip(m.log(m.maximum(openness, 1e-3) / lo) * inv_log, 0, 1)
|
||||
L = 12.0 + 84.0 * ((1 - w) * (1 - t) + w * shade)
|
||||
del inv, shade, t
|
||||
li = m.clip(m.rint(L * 2.55), 0, 255).astype(m.int32)
|
||||
hue = m.mod(m.rint(m.degrees(m.arctan2(dy, dx))), 360).astype(m.int32)
|
||||
return lut[li, hue]
|
||||
|
||||
|
||||
_relief_color_numba_kernel = None
|
||||
|
||||
|
||||
def _relief_colorize_numba(open_c, f, dx, dy, lut):
|
||||
"""Colorisation CPU en une passe (numba parallèle) : l'openness est lue
|
||||
sur la grille décimée par interpolation bilinéaire alignée sur les centres
|
||||
de pixels (équivalent de xp_zoom), sans tableau pleine résolution
|
||||
intermédiaire."""
|
||||
global _relief_color_numba_kernel
|
||||
if _relief_color_numba_kernel is None:
|
||||
from numba import njit, prange
|
||||
|
||||
@njit(parallel=True, cache=True, fastmath=True)
|
||||
def _kernel(open_c, f, dx, dy, lut, cz, sa_x, sa_y, lo, inv_log, w):
|
||||
rows, cols = dx.shape
|
||||
rc, cc = open_c.shape
|
||||
out = np.empty((rows, cols, 3), dtype=np.uint8)
|
||||
for i in prange(rows):
|
||||
u = (i + 0.5) / f - 0.5
|
||||
u = min(max(u, 0.0), rc - 1.0)
|
||||
i0 = int(u)
|
||||
i1 = min(i0 + 1, rc - 1)
|
||||
fu = u - i0
|
||||
for j in range(cols):
|
||||
v = (j + 0.5) / f - 0.5
|
||||
v = min(max(v, 0.0), cc - 1.0)
|
||||
j0 = int(v)
|
||||
j1 = min(j0 + 1, cc - 1)
|
||||
fv = v - j0
|
||||
o = ((1 - fu) * ((1 - fv) * open_c[i0, j0] + fv * open_c[i0, j1])
|
||||
+ fu * ((1 - fv) * open_c[i1, j0] + fv * open_c[i1, j1]))
|
||||
gx = dx[i, j]
|
||||
gy = dy[i, j]
|
||||
inv = 1.0 / math.sqrt(1.0 + gx * gx + gy * gy)
|
||||
sh = (cz + sa_x * gx + sa_y * gy) * inv / cz
|
||||
sh = min(max(sh, 0.0), 1.25) / 1.25
|
||||
t = math.log(max(o, 1e-3) / lo) * inv_log
|
||||
t = min(max(t, 0.0), 1.0)
|
||||
L = 12.0 + 84.0 * ((1 - w) * (1 - t) + w * sh)
|
||||
li = min(max(int(round(L * 2.55)), 0), 255)
|
||||
h = int(round(math.degrees(math.atan2(gy, gx)))) % 360
|
||||
out[i, j, 0] = lut[li, h, 0]
|
||||
out[i, j, 1] = lut[li, h, 1]
|
||||
out[i, j, 2] = lut[li, h, 2]
|
||||
return out
|
||||
_relief_color_numba_kernel = _kernel
|
||||
return _relief_color_numba_kernel(np.ascontiguousarray(open_c), float(f),
|
||||
np.ascontiguousarray(dx, dtype=np.float32),
|
||||
np.ascontiguousarray(dy, dtype=np.float32),
|
||||
lut, *_relief_params())
|
||||
|
||||
|
||||
def generate_relief_oriente(dem_file, basename, vis_dir, resolution, shared=None):
|
||||
"""Relief orienté : image RGB unique fusionnant openness locale et aspect.
|
||||
|
||||
L* (clarté) = openness positive locale (MNT détendancé, rayons courts)
|
||||
65 % + ombrage directionnel 35 % ; teinte = orientation de la pente ;
|
||||
chroma CIELAB fixe. Sortie : GeoTIFF RGB uint8 (rendu tel quel, comme
|
||||
les fonds IGN).
|
||||
"""
|
||||
gpu_tag = " [GPU]" if _gpu_mod.HAS_GPU else ""
|
||||
logger.info(f" → Relief orienté (openness locale × aspect){gpu_tag}...")
|
||||
t0 = time.time()
|
||||
output = vis_dir / f"{basename}_relief_oriente.tif"
|
||||
|
||||
try:
|
||||
dem, dem_np, rows, cols, res, nan_mask, transform, crs = \
|
||||
_prepare_dem_for_raycast(dem_file, shared, resolution)
|
||||
res = float(res)
|
||||
use_gpu = _gpu_mod.is_gpu_active()
|
||||
filled = (shared.filled_gpu if shared is not None and use_gpu else None)
|
||||
if filled is None:
|
||||
filled = to_gpu(dem) if use_gpu else dem.astype(np.float32)
|
||||
|
||||
# 1. Grille décimée : bloc max (préserve les reliefs qui bornent
|
||||
# l'horizon) et bloc moyen (support de la tendance à retirer)
|
||||
f = max(1, int(round(RELIEF_GRID_M / res)))
|
||||
if rows < 2 * f or cols < 2 * f:
|
||||
f = 1
|
||||
r2, c2 = (rows // f) * f, (cols // f) * f
|
||||
blocks = filled[:r2, :c2].reshape(r2 // f, f, c2 // f, f)
|
||||
coarse_max = blocks.max(axis=(1, 3))
|
||||
coarse_mean = blocks.mean(axis=(1, 3))
|
||||
res_c = res * f
|
||||
|
||||
# 2. Détendance (MNT − gaussienne) sur la grille décimée
|
||||
trend = xp_gaussian_filter(coarse_mean, RELIEF_DETREND_M / res_c)
|
||||
detrended = coarse_max - trend
|
||||
del blocks, coarse_max, coarse_mean, trend
|
||||
t_prep = time.time() - t0
|
||||
|
||||
# 3. Openness locale : angle d'horizon moyen (radians) sur la grille décimée
|
||||
t1 = time.time()
|
||||
angle, engine = _mean_horizon_angle(detrended, res_c, RELIEF_N_DIRS, RELIEF_RADII_M)
|
||||
t_ray = time.time() - t1
|
||||
on_gpu = engine == "GPU"
|
||||
del detrended
|
||||
|
||||
# 4. Couleurs : agrandissement de l'openness, ombrage, table L* × teinte
|
||||
t2 = time.time()
|
||||
if shared is not None:
|
||||
dy, dx = shared.dy, shared.dx
|
||||
else:
|
||||
dy, dx = np.gradient(to_cpu(filled).astype(np.float32), res)
|
||||
lut = _relief_lut()
|
||||
if on_gpu:
|
||||
openness = _gpu_mod.xp_zoom(xp.degrees(angle), f, order=1) if f > 1 else xp.degrees(angle)
|
||||
openness = _pad_to(xp, xp.asarray(openness), rows, cols)
|
||||
rgb = to_cpu(_relief_colorize_xp(xp, openness, to_gpu(dx), to_gpu(dy), xp.asarray(lut)))
|
||||
del openness
|
||||
gpu_cleanup()
|
||||
engine_color = "GPU"
|
||||
else:
|
||||
angle = to_cpu(angle)
|
||||
try:
|
||||
rgb = _relief_colorize_numba(np.degrees(angle).astype(np.float32), f, dx, dy, lut)
|
||||
engine_color = "numba"
|
||||
except ImportError:
|
||||
openness = _gpu_mod.xp_zoom(np.degrees(angle), f, order=1) if f > 1 else np.degrees(angle)
|
||||
rgb = _relief_colorize_xp(np, _pad_to(np, openness, rows, cols), dx, dy, lut)
|
||||
engine_color = "numpy"
|
||||
del angle, dx, dy
|
||||
t_color = time.time() - t2
|
||||
|
||||
rgb[nan_mask] = RELIEF_NODATA_RGB
|
||||
_save_tif(output, np.moveaxis(rgb, -1, 0), transform, crs, dtype='uint8', count=3)
|
||||
logger.info(f" ✓ Relief orienté terminé ({time.time()-t0:.1f}s : "
|
||||
f"préparation {t_prep:.1f}s, rayons {t_ray:.1f}s [{engine}], "
|
||||
f"couleurs {t_color:.1f}s [{engine_color}])")
|
||||
return output
|
||||
except Exception as e:
|
||||
logger.error(f" ✗ Erreur relief orienté: {e}", exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
def generate_mslrm(dem_file, basename, vis_dir, resolution, shared=None):
|
||||
"""Multi-Scale Relief Model (MSRM) - LRM at adaptive scales combined (GPU if available).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user