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:
@ -960,12 +960,21 @@ def generate_solar(dem_file, basename, vis_dir, resolution, shared=None):
|
||||
def generate_wavelet(dem_file, basename, vis_dir, resolution, shared=None):
|
||||
"""Mexican Hat wavelet multi-scale analysis (GPU if available).
|
||||
|
||||
CWT 2D at multiple scales adapted to resolution.
|
||||
- At 0.5m/px: [1, 2, 5, 10, 20, 50, 100]m
|
||||
- At 0.2m/px: [0.5, 1, 2, 5, 10, 20, 50, 100]m
|
||||
Focused on small archaeological structures (paths, ditches, ramparts).
|
||||
CWT 2D at scales [1, 2, 5, 10, 20, 50]m (0.5m added below 0.25m/px).
|
||||
The 100m scale was dropped: it mostly responds to landforms (hills,
|
||||
valleys), not to structures.
|
||||
|
||||
Uses std normalization per scale and weighted RMS combination
|
||||
with emphasis on archaeologically relevant scales (2-50m).
|
||||
Large landforms are removed first by subtracting a Gaussian local-mean
|
||||
trend (~35m). The residual is analyzed relative to its ~35m neighborhood,
|
||||
so a ditch on a hilltop or slope does not stand out more than the same
|
||||
ditch on flat ground (topographic-position independence). A Gaussian
|
||||
smoothing preserves locally planar slopes, so slopes are removed too.
|
||||
|
||||
Uses robust per-scale normalization (MAD) and median-centered weighted RMS
|
||||
combination with emphasis on small scales (1-10m).
|
||||
The output is an index relative to the tile's own median level (≈ 1):
|
||||
comparable from tile to tile, so a single fixed color stretch works.
|
||||
"""
|
||||
gpu_tag = " [GPU]" if _gpu_mod.HAS_GPU else ""
|
||||
logger.info(f" → Ondelette Mexican Hat multi-échelle{gpu_tag}...")
|
||||
@ -985,18 +994,44 @@ def generate_wavelet(dem_file, basename, vis_dir, resolution, shared=None):
|
||||
filled, _ = _fill_nans(dem_np.astype(np.float64))
|
||||
|
||||
min_scale = max(resolution * 2, 1.0)
|
||||
candidate_scales = [0.5, 1, 2, 5, 10, 20, 50, 100]
|
||||
# 100m retirée : elle répond surtout aux grandes formes du terrain,
|
||||
# pas aux structures. Accent sur 1-10m (petites structures).
|
||||
candidate_scales = [0.5, 1, 2, 5, 10, 20, 50]
|
||||
scales = [s for s in candidate_scales if s >= min_scale]
|
||||
|
||||
scale_weights = {
|
||||
0.5: 0.6, 1.0: 0.8, 2.0: 1.5, 5.0: 2.0,
|
||||
10.0: 1.8, 20.0: 1.5, 50.0: 1.0, 100.0: 0.6,
|
||||
0.5: 0.7, 1.0: 1.2, 2.0: 1.8, 5.0: 2.2,
|
||||
10.0: 2.0, 20.0: 1.5, 50.0: 0.8,
|
||||
}
|
||||
weights = np.array([scale_weights.get(s, 1.0) for s in scales])
|
||||
|
||||
logger.info(f" Échelles CWT: {scales}m (résolution {resolution}m/px)")
|
||||
|
||||
from scipy.ndimage import gaussian_laplace
|
||||
from scipy.ndimage import gaussian_laplace, gaussian_filter
|
||||
|
||||
# Retrait des grands volumes (collines, vallées) : on soustrait une
|
||||
# moyenne locale gaussienne avant la CWT. Un lissage gaussien préserve
|
||||
# les pentes planes, donc le résidu est analysé par rapport à son
|
||||
# voisinage ~35m : un fossé en sommet ou en flanc de colline ne
|
||||
# ressort pas plus que le même fossé à plat.
|
||||
# Fraction conservée pour une structure gaussienne de largeur σ_f :
|
||||
# σ_t²/(σ_f²+σ_t²) → 10m : 92%, 20m : 75%, colline 150m : 5%.
|
||||
# Mesuré sur MNT synthétique bruité : le contraste des petites
|
||||
# structures est insensible à σ_t ; seul le fond sommet/plat varie
|
||||
# (1.71 sans détendage → 1.21 à 35m).
|
||||
detrend_sigma_m = 35.0
|
||||
detrend_sigma_px = detrend_sigma_m / resolution
|
||||
if _gpu_mod.HAS_GPU:
|
||||
try:
|
||||
from cupyx.scipy.ndimage import gaussian_filter as gpu_gaussian_filter
|
||||
trend = to_cpu(gpu_gaussian_filter(to_gpu(filled), sigma=detrend_sigma_px))
|
||||
except Exception:
|
||||
trend = gaussian_filter(filled, sigma=detrend_sigma_px)
|
||||
else:
|
||||
trend = gaussian_filter(filled, sigma=detrend_sigma_px)
|
||||
residual = filled - trend
|
||||
del trend
|
||||
logger.info(f" Retrait des grands volumes (tendance gaussienne {detrend_sigma_m:.0f}m)")
|
||||
|
||||
wavelet_stack = []
|
||||
|
||||
@ -1005,16 +1040,20 @@ def generate_wavelet(dem_file, basename, vis_dir, resolution, shared=None):
|
||||
if _gpu_mod.HAS_GPU:
|
||||
try:
|
||||
from cupyx.scipy.ndimage import gaussian_laplace as gpu_gaussian_laplace
|
||||
response = -gpu_gaussian_laplace(to_gpu(filled), sigma=sigma_px)
|
||||
response = -gpu_gaussian_laplace(to_gpu(residual), sigma=sigma_px)
|
||||
response = to_cpu(response)
|
||||
except Exception:
|
||||
response = -gaussian_laplace(filled, sigma=sigma_px)
|
||||
response = -gaussian_laplace(residual, sigma=sigma_px)
|
||||
else:
|
||||
response = -gaussian_laplace(filled, sigma=sigma_px)
|
||||
response = -gaussian_laplace(residual, sigma=sigma_px)
|
||||
response[nan_mask] = np.nan
|
||||
valid = response[~nan_mask]
|
||||
std_val = max(np.nanstd(valid), 0.01) if len(valid) > 0 else 0.01
|
||||
response = response / std_val
|
||||
# σ robuste (MAD) : le std classique est gonflé par les queues
|
||||
# (structures marquées, bords de tuile) et varie fortement d'une
|
||||
# tuile à l'autre — cause première des dominantes de couleur
|
||||
# par tuile sur la carte.
|
||||
mad = np.nanmedian(np.abs(valid - np.nanmedian(valid))) if len(valid) > 0 else 0.0
|
||||
response = response / max(1.4826 * mad, 0.01)
|
||||
wavelet_stack.append(response)
|
||||
|
||||
stack = np.array(wavelet_stack)
|
||||
@ -1025,6 +1064,15 @@ def generate_wavelet(dem_file, basename, vis_dir, resolution, shared=None):
|
||||
combined = np.sqrt(np.nansum((stack ** 2) * weights_3d, axis=0) / np.sum(weights))
|
||||
combined[nan_mask] = np.nan
|
||||
|
||||
# Recentrage par la médiane de la tuile : la RMS devient un indice
|
||||
# relatif au niveau moyen de la tuile (médiane = 1). La distribution
|
||||
# est alors comparable d'une tuile à l'autre — condition pour un
|
||||
# étirement couleur global fixe et homogène entre tuiles.
|
||||
finite = combined[np.isfinite(combined)]
|
||||
if finite.size:
|
||||
combined = combined / max(float(np.median(finite)), 0.01)
|
||||
combined[nan_mask] = np.nan
|
||||
|
||||
_save_tif(output, combined.astype(np.float32), transform, crs)
|
||||
logger.info(f" ✓ Ondelette terminée ({time.time()-t0:.1f}s){' [GPU]' if _gpu_mod.HAS_GPU else ''}")
|
||||
return output
|
||||
@ -1227,69 +1275,6 @@ def generate_flow_accumulation(dem_file, basename, vis_dir, resolution, shared=N
|
||||
return None
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Anisotropic Openness
|
||||
# ============================================================
|
||||
|
||||
def generate_aniso_open(dem_file, basename, vis_dir, resolution, shared=None):
|
||||
"""Anisotropic Openness - weighted directional openness, multi-radius (GPU if available).
|
||||
|
||||
Computes positive and negative openness with anisotropic weighting:
|
||||
NW/SE directions weighted more heavily to enhance detection of structures
|
||||
aligned NE-SW (common in French archaeological sites: villas, enclosures).
|
||||
|
||||
Multi-radius (25, 50, 100m) with equal weight, results std-normalized
|
||||
for cross-tile comparability.
|
||||
"""
|
||||
gpu_tag = " [GPU]" if _gpu_mod.HAS_GPU else ""
|
||||
logger.info(f" → Openness Anisotropique (multi-rayon){gpu_tag}...")
|
||||
t0 = time.time()
|
||||
output = vis_dir / f"{basename}_aniso_open.tif"
|
||||
|
||||
try:
|
||||
dem, dem_np, rows, cols, res, nan_mask, transform, crs = \
|
||||
_prepare_dem_for_raycast(dem_file, shared, resolution)
|
||||
|
||||
n_dirs = 8
|
||||
# Anisotropic weights: emphasize NW-SE and NE-SW directions
|
||||
weights = np.array([1.0, 1.5, 1.0, 1.5, 1.0, 1.5, 1.0, 1.5])
|
||||
|
||||
radii_m = [25, 50, 100]
|
||||
max_dist = min(int(100 / res), 300)
|
||||
|
||||
pos_angles, neg_angles = _ray_trace_horizons(dem, rows, cols, res, n_dirs, max_dist, radii_m)
|
||||
|
||||
# Weighted combination across directions and radii
|
||||
weight_total = np.sum(weights)
|
||||
n_radii = len(radii_m)
|
||||
|
||||
pos_combined = np.zeros((rows, cols), dtype=np.float64)
|
||||
neg_combined = np.zeros((rows, cols), dtype=np.float64)
|
||||
|
||||
for r_idx in range(n_radii):
|
||||
for d_idx in range(n_dirs):
|
||||
w = weights[d_idx]
|
||||
pos_combined += pos_angles[d_idx, r_idx] * w / (n_radii * weight_total)
|
||||
neg_combined += neg_angles[d_idx, r_idx] * w / (n_radii * weight_total)
|
||||
|
||||
aniso_result = np.degrees(pos_combined - neg_combined).astype(np.float32)
|
||||
aniso_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 = aniso_result[~nan_mask]
|
||||
if len(valid) > 0:
|
||||
std_val = max(np.nanstd(valid), 0.01)
|
||||
aniso_result = (aniso_result - np.nanmean(valid)) / std_val
|
||||
|
||||
_save_tif(output, aniso_result, transform, crs)
|
||||
logger.info(f" ✓ Openness anisotropique terminé ({time.time()-t0:.1f}s){' [GPU]' if _gpu_mod.HAS_GPU else ''}")
|
||||
return output
|
||||
except Exception as e:
|
||||
logger.error(f" ✗ Erreur openness anisotropique: {e}", exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Anomaly Mask — automatic threshold detection
|
||||
# ============================================================
|
||||
@ -1342,7 +1327,6 @@ def generate_anomaly_mask(dem_file, basename, vis_dir, resolution, shared=None,
|
||||
("wavelet", 1.5), # Circular + linear structures
|
||||
("svf", 1.3), # Sky-view depressions
|
||||
("flow_acc", 1.2), # Drainage channels / ditches
|
||||
("aniso_open", 1.0), # Anisotropic structures
|
||||
("positive_openness", 0.8), # Surélevations
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user