Split webapp for Raspberry Pi deployment, remote generation API and sync
La webapp (carte + vignettes) et la génération de tuiles se déploient sur deux machines : image légère Dockerfile.webapp (FastAPI + Pillow AVIF natif + pyproj) sur Raspberry Pi, pipeline complet sur la machine de traitement. LIDAR_GENERATION_URL délègue /api/generate, /api/preview et /api/status ; /api/sync ramène les tuiles par rsync puis régénère vignettes et index localement. Token partagé optionnel (LIDAR_API_TOKEN/LIDAR_REMOTE_TOKEN). Retire du dépôt les journaux internes (.swival, audit-findings) et les données (data/, notebooks/). Doc : docs/DEPLOY_WEBAPP.md.
This commit is contained in:
@ -97,14 +97,6 @@ COLORMAPS = {
|
||||
'vmin_mode': 'fixed', 'vmin_val': -3,
|
||||
'vmax_mode': 'fixed', 'vmax_val': 3,
|
||||
},
|
||||
'aniso_open': {
|
||||
'cmap': 'seismic',
|
||||
'title': 'Openness Anisotropique (pondération directionnelle)',
|
||||
'legend': 'Openness positive − négative pondérée (z-score local)\nRouge = Surélévation dominante (mur, levée)\nBleu = Dépression dominante (fossé, doline)\nÉchelle fixe ±3σ — couleurs homogènes entre tuiles\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': 'fixed', 'vmin_val': -3,
|
||||
'vmax_mode': 'fixed', 'vmax_val': 3,
|
||||
},
|
||||
# === Famille OUVERTURE : séquentiel, valeurs normalisées ===
|
||||
'positive_openness': {
|
||||
'cmap': 'YlOrBr',
|
||||
@ -164,12 +156,26 @@ COLORMAPS = {
|
||||
'vmax_mode': 'percentile', 'vmax_pct': 98,
|
||||
},
|
||||
'wavelet': {
|
||||
'cmap': 'cividis',
|
||||
'cmap': 'inferno',
|
||||
'title': 'Ondelette Mexican Hat (CWT multi-échelle)',
|
||||
'legend': 'Réponse RMS multi-échelles (σ locales)\nÉchelles adaptées à la résolution\n\nClair = Structure détectée à cette échelle\nSombre = Pas de structure\nÉchelle fixe 0–3σ — couleurs homogènes entre tuiles\n\nOptimisé pour formes circulaires:\ntumulus, enclos, fossés annulaires',
|
||||
'description': 'Transformée en ondelette 2D — excellente pour détecter structures circulaires',
|
||||
'vmin_mode': 'fixed', 'vmin_val': 0,
|
||||
'vmax_mode': 'fixed', 'vmax_val': 3,
|
||||
'legend': 'Indice RMS multi-échelles, centré sur la médiane de la\ntuile (1 = niveau moyen, plus haut = structure)\n\nGrands volumes retirés (moyenne locale 35 m) :\nun fossé en sommet ou en pente ne ressort\npas plus qu\'un fossé à plat\n\nÉtirement quantile global figé (calibré sur un\néchantillon de tuiles) : même valeur = même couleur\nsur toutes les tuiles et résolutions\n\nOptimisé petites structures :\nchemins, fossés, ramparts',
|
||||
'description': 'Transformée en ondelette 2D : détection des petites structures (chemins, fossés, ramparts)',
|
||||
# Nœuds (percentile → valeur) mesurés sur 20 tuiles réelles par
|
||||
# résolution avec l'algorithme actuel (détendage 35 m, échelles
|
||||
# 1-50 m) : médiane inter-tuiles des percentiles par tuile, robuste
|
||||
# aux tuiles très structurées. Distribution plus large qu'avant
|
||||
# détendage : le fond macro-relief supprimé, les petites structures
|
||||
# percent bien plus au-dessus du bruit (p98 ≈ 9 vs 1,65 avant).
|
||||
'knots': {
|
||||
0.5: ([0.089, 0.213, 0.296, 0.445, 0.602, 0.783, 1.0, 1.274,
|
||||
1.661, 2.32, 3.842, 5.661, 8.936, 11.93, 15.06],
|
||||
[0.01, 0.05, 0.10, 0.20, 0.30, 0.40, 0.50, 0.60,
|
||||
0.70, 0.80, 0.90, 0.95, 0.98, 0.99, 0.995]),
|
||||
0.2: ([0.088, 0.213, 0.296, 0.445, 0.602, 0.783, 1.0, 1.287,
|
||||
1.694, 2.358, 3.846, 5.687, 8.971, 11.928, 14.971],
|
||||
[0.01, 0.05, 0.10, 0.20, 0.30, 0.40, 0.50, 0.60,
|
||||
0.70, 0.80, 0.90, 0.95, 0.98, 0.99, 0.995]),
|
||||
},
|
||||
},
|
||||
'flow_acc': {
|
||||
'cmap': 'YlGn',
|
||||
@ -212,7 +218,7 @@ RGB_LEGENDS = {
|
||||
}
|
||||
|
||||
|
||||
def _apply_colormap(data, tif_file):
|
||||
def _apply_colormap(data, tif_file, resolution=None):
|
||||
"""Apply the registered colormap normalization to data based on filename.
|
||||
|
||||
Returns (data, cmap, title, legend_label, description, is_rgb).
|
||||
@ -238,31 +244,45 @@ def _apply_colormap(data, tif_file):
|
||||
|
||||
vmin = vmax = None
|
||||
|
||||
# Compute vmin/vmax based on mode
|
||||
if info['vmin_mode'] == 'fixed':
|
||||
vmin = info['vmin_val']
|
||||
elif info['vmin_mode'] == 'percentile':
|
||||
vmin = np.percentile(valid_data, info['vmin_pct'])
|
||||
elif info['vmin_mode'] == 'symmetric':
|
||||
vmax_abs = max(abs(np.percentile(valid_data, info['sym_pct'][0])),
|
||||
abs(np.percentile(valid_data, info['sym_pct'][1])), 0.001)
|
||||
vmin = -vmax_abs
|
||||
vmax = vmax_abs # symmetric mode sets both vmin and vmax
|
||||
|
||||
if vmax is None:
|
||||
# Only compute vmax if not already set by symmetric mode
|
||||
if info.get('vmax_mode') == 'fixed':
|
||||
vmax = info['vmax_val']
|
||||
elif info.get('vmax_mode') == 'percentile':
|
||||
vmax = np.percentile(valid_data, info['vmax_pct'])
|
||||
elif info.get('vmax_mode') == 'symmetric':
|
||||
knots = info.get('knots')
|
||||
if knots is not None:
|
||||
# Étalonnage quantile figé (appariement d'histogramme global,
|
||||
# cf. normalisation radiométrique des mosaïques) : fonction de
|
||||
# transfert en nœuds mesurés une fois sur un échantillon de
|
||||
# tuiles — même valeur → même couleur sur toutes les tuiles,
|
||||
# toute la palette utilisée, insensible aux queues locales.
|
||||
if isinstance(knots, dict):
|
||||
key = min(knots, key=lambda r: abs(float(r) - float(resolution or 0.5)))
|
||||
knots = knots[key]
|
||||
kv, kt = knots
|
||||
data = np.interp(np.asarray(data, dtype=float), kv, kt, left=0.0, right=1.0)
|
||||
vmin, vmax = kv[0], kv[-1]
|
||||
else:
|
||||
# Compute vmin/vmax based on mode
|
||||
if info['vmin_mode'] == 'fixed':
|
||||
vmin = info['vmin_val']
|
||||
elif info['vmin_mode'] == 'percentile':
|
||||
vmin = np.percentile(valid_data, info['vmin_pct'])
|
||||
elif info['vmin_mode'] == 'symmetric':
|
||||
vmax_abs = max(abs(np.percentile(valid_data, info['sym_pct'][0])),
|
||||
abs(np.percentile(valid_data, info['sym_pct'][1])), 0.001)
|
||||
vmax = vmax_abs
|
||||
abs(np.percentile(valid_data, info['sym_pct'][1])), 0.001)
|
||||
vmin = -vmax_abs
|
||||
vmax = vmax_abs # symmetric mode sets both vmin and vmax
|
||||
|
||||
# Apply normalization
|
||||
if vmin is not None and vmax is not None:
|
||||
data = np.clip((data - vmin) / max(vmax - vmin, 0.001), 0, 1)
|
||||
if vmax is None:
|
||||
# Only compute vmax if not already set by symmetric mode
|
||||
if info.get('vmax_mode') == 'fixed':
|
||||
vmax = info['vmax_val']
|
||||
elif info.get('vmax_mode') == 'percentile':
|
||||
vmax = np.percentile(valid_data, info['vmax_pct'])
|
||||
elif info.get('vmax_mode') == 'symmetric':
|
||||
vmax_abs = max(abs(np.percentile(valid_data, info['sym_pct'][0])),
|
||||
abs(np.percentile(valid_data, info['sym_pct'][1])), 0.001)
|
||||
vmax = vmax_abs
|
||||
|
||||
# Apply normalization
|
||||
if vmin is not None and vmax is not None:
|
||||
data = np.clip((data - vmin) / max(vmax - vmin, 0.001), 0, 1)
|
||||
|
||||
legend = info['legend'].format(vmin=vmin or 0, vmax=vmax or 0)
|
||||
return data, info['cmap'], info['title'], legend, info['description'], False
|
||||
@ -463,7 +483,7 @@ def tif_to_png(tif_file, vis_dir, resolution, keep_tif=False, source_info=None,
|
||||
nan_mask = nan_mask # keep for later
|
||||
|
||||
# Apply colormap
|
||||
data, cmap, title, legend_label, description, is_rgb_result = _apply_colormap(data, tif_file)
|
||||
data, cmap, title, legend_label, description, is_rgb_result = _apply_colormap(data, tif_file, resolution=resolution)
|
||||
|
||||
# Apply NaN mask: make zones without data transparent
|
||||
has_nan_mask = nan_mask is not None and not is_rgb_result
|
||||
@ -818,7 +838,7 @@ def tif_to_crop(tif_file, vis_dir, resolution, keep_tif=False, quality=98, outpu
|
||||
data = src.read(1)
|
||||
|
||||
# Apply colormap normalization
|
||||
data, cmap_name, title, legend_label, description, is_rgb_result = _apply_colormap(data, tif_file)
|
||||
data, cmap_name, title, legend_label, description, is_rgb_result = _apply_colormap(data, tif_file, resolution=resolution)
|
||||
|
||||
# Convert to RGB using colormap
|
||||
if is_rgb_result:
|
||||
@ -907,7 +927,7 @@ def generate_pdf_report(basename, vis_dir, pdf_dir, resolution):
|
||||
|
||||
# Sort analysis files by archaeological priority
|
||||
order = ['mslrm', 'svf', 'negative_openness',
|
||||
'positive_openness', 'aniso_open', 'sailore', 'hillshade_multi',
|
||||
'positive_openness', 'sailore', 'hillshade_multi',
|
||||
'flow_acc', 'solar', 'slope', 'roughness', 'wavelet']
|
||||
|
||||
def sort_key(f):
|
||||
|
||||
Reference in New Issue
Block a user