diff --git a/lidar_pipeline/index.py b/lidar_pipeline/index.py index e2cce1b..5f29b3a 100644 --- a/lidar_pipeline/index.py +++ b/lidar_pipeline/index.py @@ -137,7 +137,7 @@ VIZ_LEGENDS = { }, 'roughness': { 'title': 'Rugosité Multi-Échelle (3m + 15m)', - 'legend': 'Irrégularité du terrain combinée fine + large\nViolet foncé = Surface lisse (route, mur, sol plat)\nJaune vif = Surface rugueuse (végétation, ruines, pierres)\nCombine rugosité fine 3m (70%) + large 15m (30%)', + 'legend': 'Irrégularité du terrain combinée fine + large\nViolet foncé = Surface lisse (route, mur, sol plat)\nJaune vif = Surface rugueuse (végétation, ruines, pierres)\nCombine rugosité fine 3m (70%) + large 15m (30%)\nÉchelle physique commune à toutes les tuiles (références\nfigées mesurées sur dalles réelles) : mosaïque jointive,\nmême valeur = même couleur', 'description': 'Mesure la variabilité locale — surfaces anthropiques lisses vs naturelles rugueuses', 'cmap': 'plasma', 'gradient': ('#0d0887', '#4c02a1', '#7e03a8', '#aa2395', '#cc4778', diff --git a/lidar_pipeline/rendering.py b/lidar_pipeline/rendering.py index b4815fc..b2b7864 100644 --- a/lidar_pipeline/rendering.py +++ b/lidar_pipeline/rendering.py @@ -128,8 +128,10 @@ COLORMAPS = { }, 'roughness': { 'cmap': 'plasma', + # vmax fixe (p98 médian mesuré sur 20 dalles réelles) : un vmax au + # percentile par tuile rendait l'échelle non jointive entre tuiles. 'vmin_mode': 'fixed', 'vmin_val': 0, - 'vmax_mode': 'percentile', 'vmax_pct': 98, + 'vmax_mode': 'fixed', 'vmax_val': 3.8, }, 'wavelet': { 'cmap': 'inferno', diff --git a/lidar_pipeline/visualizations.py b/lidar_pipeline/visualizations.py index 13e5390..3851f26 100644 --- a/lidar_pipeline/visualizations.py +++ b/lidar_pipeline/visualizations.py @@ -880,6 +880,15 @@ def _box_std_from_integral(Sx, Sx2, size): return xp.sqrt(xp.maximum(mean_sq - mean * mean, 0)) +# Références de normalisation de la rugosité (mètres d'écart-type local). +# Médianes inter-tuiles mesurées sur 20 dalles réelles à 0,2 m : la +# normalisation par tuile (z-score) rendait l'échelle non jointive — l'écart +# variait de 0,05 à 0,58 m selon la tuile pour l'échelle fine. References +# FIGÉES : même rugosité physique = même valeur sur toutes les tuiles. +ROUGHNESS_FINE_REF_M = 0.156 +ROUGHNESS_BROAD_REF_M = 0.475 + + def generate_roughness(dem_file, basename, vis_dir, resolution, shared=None): """Surface roughness - multi-scale standard deviation (GPU-accelerated). @@ -889,6 +898,9 @@ def generate_roughness(dem_file, basename, vis_dir, resolution, shared=None): deux échelles, extraction par 4 coins — coût constant quelle que soit la fenêtre (un uniform_filter coûte proportionnellement à sa taille, 75 px à 0,2 m pour l'échelle large). + + Normalisation par références FIXÉES (médianes mesurées) et non par + tuile : les mosaïques sont jointives, même valeur = même couleur. """ gpu_tag = " [GPU]" if _gpu_mod.HAS_GPU else "" logger.info(f" → Rugosité de surface{gpu_tag}...") @@ -930,13 +942,10 @@ def generate_roughness(dem_file, basename, vis_dir, resolution, shared=None): roughness_fine[nan_mask] = np.nan roughness_broad[nan_mask] = np.nan - # Std normalization per scale then weighted combination - fine_valid = roughness_fine[~nan_mask] - broad_valid = roughness_broad[~nan_mask] - fine_std = max(np.nanstd(fine_valid), 0.01) if len(fine_valid) > 0 else 0.01 - broad_std = max(np.nanstd(broad_valid), 0.01) if len(broad_valid) > 0 else 0.01 - - roughness = 0.7 * roughness_fine / fine_std + 0.3 * roughness_broad / broad_std + # Combinaison pondérée, échelle physique commune (références fixées : + # jointive entre tuiles — cf. constantes module) + roughness = (0.7 * roughness_fine / ROUGHNESS_FINE_REF_M + + 0.3 * roughness_broad / ROUGHNESS_BROAD_REF_M) roughness[nan_mask] = np.nan _save_tif(output, roughness, transform, crs)