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:
@ -121,6 +121,77 @@ class TestWavelet:
|
||||
assert result is not None
|
||||
assert result.exists()
|
||||
|
||||
def test_output_median_centered(self, synthetic_dem, tmp_output_dir):
|
||||
"""Recentrage robuste : médiane ≈ 1 quel que soit le terrain.
|
||||
|
||||
C'est la condition pour qu'un étirement couleur global fixe donne
|
||||
des couleurs homogènes entre tuiles (cf. knots dans rendering.py).
|
||||
"""
|
||||
import rasterio
|
||||
from lidar_pipeline.visualizations import generate_wavelet
|
||||
result = generate_wavelet(synthetic_dem, "test", tmp_output_dir, 5.0)
|
||||
with rasterio.open(result) as src:
|
||||
data = src.read(1)
|
||||
valid = data[np.isfinite(data)]
|
||||
assert abs(np.median(valid) - 1.0) < 0.05
|
||||
|
||||
def test_ditch_on_hilltop_not_amplified(self, tmp_path, tmp_output_dir):
|
||||
"""Un fossé en sommet de colline ne ressort pas plus qu'à plat.
|
||||
|
||||
Le détendage (moyenne locale gaussienne ~35 m) doit neutraliser la
|
||||
position topographique :
|
||||
- le pic d'indice sur le fossé en sommet reste comparable au même
|
||||
fossé sur terrain plat ;
|
||||
- le fond du sommet (sans structure) reste comparable au fond plat.
|
||||
|
||||
MNT synthétique bruité (σ=8 cm) : sans détendage, le fond sommet
|
||||
ressort ~1,7× le fond plat (sommets jaunes sur la carte).
|
||||
"""
|
||||
import rasterio
|
||||
from rasterio.transform import from_bounds
|
||||
from lidar_pipeline.visualizations import generate_wavelet
|
||||
|
||||
size = 600
|
||||
res = 1.0
|
||||
x = np.arange(size) * res
|
||||
y = np.arange(size) * res
|
||||
X, Y = np.meshgrid(x, y)
|
||||
dem = 100.0 + 0.01 * X
|
||||
|
||||
# Colline réaliste (sigma 80 m, 25 m de haut) à gauche + bruit capteur
|
||||
dem += 25.0 * np.exp(-((X - 150)**2 + (Y - 300)**2) / (2 * 80**2))
|
||||
rng = np.random.default_rng(42)
|
||||
dem += rng.normal(0, 0.08, dem.shape)
|
||||
|
||||
# Deux fossés identiques : l'un au sommet de la colline, l'autre à plat
|
||||
for xc in (150, 480):
|
||||
dem -= 1.5 * np.exp(-((X - xc)**2) / (2 * 1.2**2))
|
||||
|
||||
dem_file = tmp_path / "ditch_dem.tif"
|
||||
transform = from_bounds(660000, 6700000, 660600, 6700600, size, size)
|
||||
with rasterio.open(
|
||||
dem_file, 'w', driver='GTiff', height=size, width=size,
|
||||
count=1, dtype='float32', crs='EPSG:2154', transform=transform,
|
||||
) as dst:
|
||||
dst.write(dem.astype('float32'), 1)
|
||||
|
||||
result = generate_wavelet(dem_file, "ditch", tmp_output_dir, res)
|
||||
assert result is not None and result.exists()
|
||||
with rasterio.open(result) as src:
|
||||
data = src.read(1)
|
||||
|
||||
# Pics sur une bande verticale autour de chaque fossé
|
||||
peak_hilltop = np.nanmax(data[:, 145:156])
|
||||
peak_flat = np.nanmax(data[:, 475:486])
|
||||
assert peak_flat > 5.0 # le fossé ressort nettement au-dessus du fond
|
||||
# Pas d'amplification du fossé par la position topographique
|
||||
assert peak_hilltop < 1.5 * peak_flat
|
||||
|
||||
# Fond du sommet (35 m à l'est du fossé sommital) vs fond plat
|
||||
bg_hilltop = np.nanmedian(data[250:350, 185:226])
|
||||
bg_flat = np.nanmedian(data[250:350, 500:561])
|
||||
assert bg_hilltop < 1.4 * bg_flat # sans détendage : ~1,7×
|
||||
|
||||
|
||||
class TestFlowAccumulation:
|
||||
def test_generates_tif(self, synthetic_dem, tmp_output_dir):
|
||||
|
||||
Reference in New Issue
Block a user