Fix 12 bugs: D8 flow accumulation, PDF AVIF support, GPU memory leaks, dead code, SAILORE sigma scaling
This commit is contained in:
@ -80,57 +80,64 @@ _FRANCE_OUTLINE_L93 = np.array([
|
||||
|
||||
COLORMAPS = {
|
||||
# === Famille RELIEF : rouge=surélévation, bleu=dépression ===
|
||||
# Roma (Crameri): perceptually uniform, CVD-friendly, dark center → near-zero values visible
|
||||
# Falls back to RdBu_r if cmcrameri unavailable
|
||||
# Diverging: rouge vif=positif, bleu vif=négatif, blanc=plat
|
||||
'curvature': {
|
||||
'cmap': 'roma' if HAS_CMCRAmeri else 'RdBu_r',
|
||||
'cmap': 'bwr',
|
||||
'title': 'Courbure (Convexité/Concavité du terrain)',
|
||||
'legend': 'Changement de pente (1/m)\nRouge = Convexe (sommet de mur, levée)\nBleu = Concave (fond de fossé, dépression)',
|
||||
'description': 'Détecte les ruptures de pente — utile pour bords de terrasses et levées',
|
||||
'vmin_mode': 'symmetric', 'sym_pct': (5, 95),
|
||||
},
|
||||
'mslrm': {
|
||||
'cmap': 'roma' if HAS_CMCRAmeri else 'RdBu_r',
|
||||
'cmap': 'seismic',
|
||||
'title': 'MSRM - Multi-Scale Relief Model (échelles adaptatives)',
|
||||
'legend': 'Relief combiné multi-échelles\nRouge = Surélévation (mur, tumulus, levée)\nBleu = Dépression (fossé, douve)\n\nLRM = 1 échelle (15m)\nMSRM = échelles combinées pondérées\nDétecte du micro au macro',
|
||||
'description': 'Combine LRM à 5 échelles — détecte structures de 5m à 100m simultanément',
|
||||
'vmin_mode': 'symmetric', 'sym_pct': (2, 98),
|
||||
},
|
||||
'lrm': {
|
||||
'cmap': 'roma' if HAS_CMCRAmeri else 'RdBu_r',
|
||||
'cmap': 'seismic',
|
||||
'title': 'LRM - Local Relief Model (échelle unique 15m)',
|
||||
'legend': 'Écart local par rapport au terrain moyen (m)\nRouge = Surélévation (+{vmax:.2f}m)\nBleu = Dépression ({vmin:.2f}m)\nNoyau gaussien unique de 15m',
|
||||
'description': 'Micro-relief à 15m seulement — voir MSRM pour toutes les échelles',
|
||||
'vmin_mode': 'symmetric', 'sym_pct': (2, 98),
|
||||
},
|
||||
'tpi': {
|
||||
'cmap': 'roma' if HAS_CMCRAmeri else 'RdBu_r',
|
||||
'cmap': 'seismic',
|
||||
'title': 'TPI - Topographic Position Index (4 échelles)',
|
||||
'legend': 'Position dans le paysage\nRouge = Plus haut que le voisinage (crête, plateau)\nBleu = Plus bas que le voisinage (fossé, vallée)\nCombine 4 échelles : 3m, 15m, 50m, 200m',
|
||||
'description': 'Identifie la position topographique — utile pour repérer crêtes vs vallées à grande échelle',
|
||||
'vmin_mode': 'symmetric', 'sym_pct': (2, 98),
|
||||
},
|
||||
'sailore': {
|
||||
'cmap': 'roma' if HAS_CMCRAmeri else 'RdBu_r',
|
||||
'cmap': 'seismic',
|
||||
'title': 'SAILORE - LRM Auto-Adaptatif',
|
||||
'legend': 'Relief local adaptatif\nRouge = Surélévation | Bleu = Dépression\n\nLRM = noyau fixe 15m\nMSRM = 5 noyaux fixes\nSAILORE = noyau adapté à la pente\nPlat=grand noyau | Pente=petit noyau',
|
||||
'description': 'Noyau qui s\'adapte à la pente locale — terrain plat=grand noyau, pente=petit noyau',
|
||||
'vmin_mode': 'symmetric', 'sym_pct': (2, 98),
|
||||
},
|
||||
'aniso_open': {
|
||||
'cmap': 'roma' if HAS_CMCRAmeri else 'RdBu_r',
|
||||
'cmap': 'seismic',
|
||||
'title': 'Openness Anisotropique (pondération directionnelle)',
|
||||
'legend': 'Openness positive - négative pondérée (degrés)\nRouge = Surélévation dominante (mur, levée)\nBleu = Dépression dominante (fossé, doline)\nPondère les directions NW-SE et NE-SW davantage',
|
||||
'description': 'Openness avec pondération anisotropique — détecte mieux les structures alignées NW-SE et NE-SW',
|
||||
'vmin_mode': 'symmetric', 'sym_pct': (2, 98),
|
||||
},
|
||||
# === Famille OUVERTURE : clair=ouvert, sombre=fermé ===
|
||||
'paths': {
|
||||
'cmap': 'inferno',
|
||||
'title': 'Cheminement (chemins et sentiers)',
|
||||
'legend': 'Openness directionnelle maximale\nJaune/vif = Chemin ou sentier\nNoir = Terrain plat\n\nDétecte les structures linéaires dans toutes les directions',
|
||||
'description': 'Différence openness positive-négative maximale sur 8 directions — chemins, sentiers, ornières',
|
||||
'vmin_mode': 'percentile', 'vmin_pct': 5,
|
||||
'vmax_mode': 'percentile', 'vmax_pct': 99,
|
||||
},
|
||||
# === Famille OUVERTURE : séquentiel, toujours positif ===
|
||||
'positive_openness': {
|
||||
'cmap': 'YlOrBr',
|
||||
'title': 'Openness Positive (ouverture vers le haut)',
|
||||
'legend': 'Angle d\'ouverture vers le haut (deg)\nClair = Vue dégagée vers le ciel (sommets, plateaux)\nSombre = Vue bloquée (vallées encaissées)',
|
||||
'legend': 'Angle d\'ouverture vers le ciel (deg)\nClair = Vue dégagée vers le ciel (sommets, plateaux)\nSombre = Vue bloquée (vallées encaissées)',
|
||||
'description': 'Ray-tracing 8 directions — complémentaire de la négative pour détecter crêtes',
|
||||
'vmin_mode': 'percentile', 'vmin_pct': 10,
|
||||
'vmin_mode': 'percentile', 'vmin_pct': 2,
|
||||
'vmax_mode': 'percentile', 'vmax_pct': 98,
|
||||
},
|
||||
'negative_openness': {
|
||||
@ -138,14 +145,14 @@ COLORMAPS = {
|
||||
'title': 'Openness Negative (ouverture vers le bas)',
|
||||
'legend': 'Angle d\'ouverture vers le bas (deg)\nClair = Surplomb (bords de fossé, grottes)\nSombre = Terrain plat (fonds de vallée)\nMeilleur détecteur de cavités et dolines',
|
||||
'description': 'Ray-tracing 8 directions — détecte fossés, dolines, souterrains',
|
||||
'vmin_mode': 'percentile', 'vmin_pct': 10,
|
||||
'vmin_mode': 'percentile', 'vmin_pct': 2,
|
||||
'vmax_mode': 'percentile', 'vmax_pct': 98,
|
||||
},
|
||||
'svf': {
|
||||
'cmap': 'bone_r',
|
||||
'cmap': 'hot_r',
|
||||
'title': 'Sky-View Factor (fraction de ciel visible)',
|
||||
'legend': 'Proportion de ciel visible depuis chaque point\nClair = Ciel dégagé (sommet, plateau, levée)\nSombre = Ciel masqué (vallée, fossé, tranchée)\nContraste adapté aux valeurs réelles (percentiles 2-98)',
|
||||
'description': 'Détection de micro-relief — fossés sombres, levées claires, complémentaire de l\'openness',
|
||||
'legend': 'Proportion de ciel visible depuis chaque point\nBlanc/jaune = Ciel masqué (vallée, fossé, tranchée)\nNoir = Ciel dégagé (sommet, plateau)\nLes fossés ressortent en vif — excellent pour structures linéaires',
|
||||
'description': 'Détection de micro-relief — fossés en jaune/blanc, levées en sombre',
|
||||
'vmin_mode': 'percentile', 'vmin_pct': 2,
|
||||
'vmax_mode': 'percentile', 'vmax_pct': 98,
|
||||
},
|
||||
@ -155,16 +162,16 @@ COLORMAPS = {
|
||||
'title': 'Hillshade Multidirectionnel',
|
||||
'legend': 'Illumination combinée de 6 directions\nBlanc = Face éclairée | Noir = Zone d\'ombre',
|
||||
'description': 'Ombres portées révélant micro-relief (murs, fossés, terrasses)',
|
||||
'vmin_mode': 'fixed', 'vmin_val': 0,
|
||||
'vmax_mode': 'fixed', 'vmax_val': 1,
|
||||
'vmin_mode': 'percentile', 'vmin_pct': 1,
|
||||
'vmax_mode': 'percentile', 'vmax_pct': 99,
|
||||
},
|
||||
'slope': {
|
||||
'cmap': 'inferno',
|
||||
'title': 'Pente (Inclinaison du terrain)',
|
||||
'legend': 'Inclinaison en degrés\nMin: {vmin:.1f}° | Max: {vmax:.1f}°\nClair = Forte pente | Sombre = Terrain plat',
|
||||
'description': 'Murs, talus et bords ressortent en clair — terrain plat en sombre',
|
||||
'legend': 'Inclinaison en degrés\nMin: {vmin:.1f}° | Max: {vmax:.1f}°\nJaune = Forte pente | Violet foncé = Terrain plat',
|
||||
'description': 'Murs, talus et bords ressortent en jaune — terrain plat en sombre',
|
||||
'vmin_mode': 'fixed', 'vmin_val': 0,
|
||||
'vmax_mode': 'percentile', 'vmax_pct': 95,
|
||||
'vmax_mode': 'percentile', 'vmax_pct': 97,
|
||||
},
|
||||
'aspect': {
|
||||
'cmap': 'twilight',
|
||||
@ -175,12 +182,12 @@ COLORMAPS = {
|
||||
'vmax_mode': 'fixed', 'vmax_val': 360,
|
||||
},
|
||||
'roughness': {
|
||||
'cmap': 'magma',
|
||||
'cmap': 'plasma',
|
||||
'title': 'Rugosité Multi-Échelle (3m + 15m)',
|
||||
'legend': 'Irrégularité du terrain combinée fine + large\nSombre = Surface lisse (route, mur, sol plat)\nClair = 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%)',
|
||||
'description': 'Mesure la variabilité locale — surfaces anthropiques lisses vs naturelles rugueuses',
|
||||
'vmin_mode': 'fixed', 'vmin_val': 0,
|
||||
'vmax_mode': 'percentile', 'vmax_pct': 97,
|
||||
'vmax_mode': 'percentile', 'vmax_pct': 98,
|
||||
},
|
||||
'wavelet': {
|
||||
'cmap': 'cividis',
|
||||
@ -345,12 +352,15 @@ def _nice_scale(extent_m):
|
||||
|
||||
Returns (scale_m, label) where label is like '100 m' or '500 m' or '1 km'.
|
||||
"""
|
||||
nice_scales = [50, 100, 200, 500, 1000, 2000, 5000, 10000]
|
||||
# Pick the largest scale <= 20% of extent
|
||||
nice_scales = [10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000]
|
||||
# Pick the largest scale that fits within 20% of extent
|
||||
max_scale = extent_m * 0.20
|
||||
chosen = nice_scales[0]
|
||||
for s in nice_scales:
|
||||
if s <= extent_m * 0.20:
|
||||
if s <= max_scale:
|
||||
chosen = s
|
||||
else:
|
||||
break
|
||||
if chosen >= 1000:
|
||||
return chosen, f"{chosen // 1000} km"
|
||||
return chosen, f"{chosen} m"
|
||||
@ -496,10 +506,11 @@ def tif_to_png(tif_file, vis_dir, resolution, keep_tif=False, source_info=None,
|
||||
im = ax.imshow(data, cmap=cmap, aspect='equal', origin='upper',
|
||||
interpolation='bilinear')
|
||||
|
||||
ax.set_title(f"{title}", fontsize=14, fontweight='bold', pad=8)
|
||||
ax.text(0.5, 1.01, description, transform=ax.transAxes,
|
||||
fontsize=10, fontstyle='italic', color='#555555',
|
||||
ha='center', va='bottom')
|
||||
ax.set_title(f"{title}", fontsize=14, fontweight='bold', pad=10)
|
||||
if description:
|
||||
ax.text(0.5, 1.04, description, transform=ax.transAxes,
|
||||
fontsize=10, fontstyle='italic', color='#555555',
|
||||
ha='center', va='bottom')
|
||||
|
||||
# Colorbar/legend area — full height alongside data
|
||||
cbar_left = data_left + data_width_frac + 0.02
|
||||
@ -569,34 +580,35 @@ def tif_to_png(tif_file, vis_dir, resolution, keep_tif=False, source_info=None,
|
||||
spine.set_color('black')
|
||||
spine.set_linewidth(0.8)
|
||||
|
||||
# North arrow — compass rose style, inside the data area (top-right corner)
|
||||
# North arrow — compass rose in bottom-right corner of data area
|
||||
# Semi-transparent background for readability over any data
|
||||
north_ax = fig.add_axes([data_left + data_width_frac - 0.06,
|
||||
data_bottom + data_height_frac - 0.12,
|
||||
0.05, 0.10],
|
||||
north_ax = fig.add_axes([data_left + data_width_frac - 0.07,
|
||||
data_bottom + 0.01,
|
||||
0.06, 0.14],
|
||||
facecolor='none')
|
||||
north_ax.set_xlim(-1.2, 1.2)
|
||||
north_ax.set_ylim(-0.3, 1.5)
|
||||
north_ax.set_xlim(-1.5, 1.5)
|
||||
north_ax.set_ylim(-1.5, 1.5)
|
||||
north_ax.axis('off')
|
||||
north_ax.set_aspect('equal')
|
||||
# Compass rose centered at (0, 0) — all 4 cardinals equidistant from center
|
||||
# Semi-transparent white background circle
|
||||
circle_bg = plt.Circle((0, 0.5), 0.85, facecolor='white', edgecolor='#888888',
|
||||
circle_bg = plt.Circle((0, 0), 1.0, facecolor='white', edgecolor='#888888',
|
||||
linewidth=0.5, alpha=0.7, zorder=1)
|
||||
north_ax.add_patch(circle_bg)
|
||||
# N arrow
|
||||
north_ax.annotate('N', xy=(0, 1.3), fontsize=9, fontweight='bold',
|
||||
# N arrow (pointing up = North)
|
||||
north_ax.annotate('N', xy=(0, 1.35), fontsize=9, fontweight='bold',
|
||||
ha='center', va='bottom', color='#b22222', zorder=10)
|
||||
north_ax.plot([0, 0], [0.0, 1.0], color='#b22222', linewidth=2.0, zorder=10)
|
||||
north_ax.add_patch(MplPolygon([[0, 0.3], [-0.2, 0.7], [0, 1.0], [0.2, 0.7]],
|
||||
north_ax.plot([0, 0], [-0.5, 1.0], color='#b22222', linewidth=2.0, zorder=10)
|
||||
north_ax.add_patch(MplPolygon([[0, 0.5], [-0.2, 0.7], [0, 1.0], [0.2, 0.7]],
|
||||
closed=True, facecolor='#b22222', edgecolor='#b22222', zorder=9))
|
||||
# Cardinal ticks
|
||||
for angle, label in [(90, ''), (0, 'E'), (180, 'O'), (270, 'S')]:
|
||||
# Cardinal ticks — all centered at (0, 0)
|
||||
for angle, label in [(90, 'N'), (0, 'E'), (180, 'O'), (270, 'S')]:
|
||||
rad = np.radians(angle)
|
||||
north_ax.plot([0.85*np.cos(rad), 1.05*np.cos(rad)],
|
||||
[0.85*np.sin(rad), 1.05*np.sin(rad)],
|
||||
north_ax.plot([1.0*np.cos(rad), 1.2*np.cos(rad)],
|
||||
[1.0*np.sin(rad), 1.2*np.sin(rad)],
|
||||
color='#555555', linewidth=0.8, zorder=5)
|
||||
if label:
|
||||
north_ax.text(1.15*np.cos(rad), 1.15*np.sin(rad), label,
|
||||
north_ax.text(1.35*np.cos(rad), 1.35*np.sin(rad), label,
|
||||
fontsize=6, ha='center', va='center', color='#555555', zorder=5)
|
||||
|
||||
# Bottom info bar — enriched with source, method, date
|
||||
@ -621,7 +633,9 @@ def tif_to_png(tif_file, vis_dir, resolution, keep_tif=False, source_info=None,
|
||||
else:
|
||||
line1_parts.append(f"X: {min_x:.0f}–{max_x:.0f} Y: {min_y:.0f}–{max_y:.0f}")
|
||||
line1_parts.append(f"EPSG:2154")
|
||||
line1_parts.append(f"Res: {resolution}m/px")
|
||||
# Round resolution to avoid ugly decimals like 0.499999959
|
||||
res_display = round(resolution, 2) if resolution < 1 else round(resolution, 1)
|
||||
line1_parts.append(f"Res: {res_display}m/px")
|
||||
line1_parts.append(f"Emprise: {extent_km_x:.1f}×{extent_km_y:.1f}km")
|
||||
if not is_rgb:
|
||||
line1_parts.append(f"Alt: {alt_min:.1f}–{alt_max:.1f}m")
|
||||
@ -793,15 +807,24 @@ def generate_pdf_report(basename, vis_dir, pdf_dir, resolution):
|
||||
logger.info(f" → Génération rapport PDF A3: {pdf_file.name}")
|
||||
t0 = time.time()
|
||||
|
||||
# Look for WebPs in per-file subdirectory first, then fallback to main dir
|
||||
# Look for images in per-file subdirectory first, then fallback to main dir
|
||||
file_vis_dir = vis_dir / basename
|
||||
png_files = []
|
||||
if file_vis_dir.exists():
|
||||
png_files = sorted(file_vis_dir.glob("*.webp"))
|
||||
png_files = sorted(file_vis_dir.glob("*.avif")) + sorted(file_vis_dir.glob("*.webp"))
|
||||
else:
|
||||
png_files = sorted(vis_dir.glob(f"{basename}_*.webp"))
|
||||
if not png_files:
|
||||
png_files = sorted(vis_dir.glob(f"{basename}_*.avif")) + sorted(vis_dir.glob(f"{basename}_*.webp"))
|
||||
# Deduplicate in case both formats exist
|
||||
seen = set()
|
||||
unique_files = []
|
||||
for f in png_files:
|
||||
if f not in seen:
|
||||
seen.add(f)
|
||||
unique_files.append(f)
|
||||
if not unique_files:
|
||||
logger.warning(f" ✗ Aucune image trouvée pour {basename}")
|
||||
return None
|
||||
png_files = unique_files
|
||||
|
||||
# Categorize
|
||||
situ_files = []
|
||||
|
||||
Reference in New Issue
Block a user