Add visualization picker to zone generation and unify tile colors

- Web map: multi-select picker in the generation bar (aspect, slope,
  positive openness, anisotropic openness, wavelet) passed to the API;
  the layer panel is restricted to the same shortlist (PANEL_VIZ) and
  a refresh button rebuilds the index when new layers appear on disk;
  jobs started outside the UI are now adopted into the visible queue
- Uniform colors across tiles: openness/anisotropic/sailore now store
  local z-scores, and all renderers use fixed ranges (0-3 sigma, SVF
  0-1 physical, slope 0-30 deg) instead of per-tile percentile stretches
- Ray-tracing falls back to CPU when VRAM is exhausted so openness and
  SVF no longer fail silently on shared GPUs
- build_index merges visualizations available at only one resolution
  into the displayed tile so in-progress layers stay visible
- 11 new tests (142 passing)
This commit is contained in:
Antoine Jacquin
2026-08-31 19:02:14 +02:00
parent 8ca65155db
commit ed3e90ea89
7 changed files with 412 additions and 37 deletions

View File

@ -79,3 +79,97 @@ def test_build_command_ground_classification():
assert cmd[i + 1] == method
assert ("--force" in cmd) == regenerate
assert ("--force-classification" in cmd) == regenerate
def test_build_command_default_viz_aspect():
"""Sans choix de visualisation, la commande génère uniquement aspect."""
from lidar_pipeline.webapp import _build_command
cmd = _build_command([(1054, 6882)])
i = cmd.index("--only")
assert cmd[i + 1] == "aspect"
def test_build_command_custom_viz():
"""Les visualisations demandées sont passées à --only dans l'ordre."""
from lidar_pipeline.webapp import _build_command
cmd = _build_command([(1054, 6882)], viz=["aspect", "wavelet", "aniso_open"])
i = cmd.index("--only")
assert cmd[i + 1:i + 4] == ["aspect", "wavelet", "aniso_open"]
def test_viz_step_names_match_pipeline():
"""Les noms acceptés par l'API sont exactement les étapes VIZ_STEPS du pipeline."""
from lidar_pipeline.webapp import _viz_step_names
from lidar_pipeline.pipeline import VIZ_STEPS
assert _viz_step_names() == [n for n, _ in VIZ_STEPS]
def test_generate_rejects_unknown_viz():
"""L'API refuse un nom de visualisation inconnu (HTTPException 400)."""
from fastapi import HTTPException
from lidar_pipeline.webapp import generate, GenerateRequest
req = GenerateRequest(tiles=[[1054, 6882]], viz=["wavelet", "inconnu"])
try:
generate(req)
assert False, "une HTTPException était attendue"
except HTTPException as e:
assert e.status_code == 400
assert "inconnu" in e.detail
def test_generate_accepts_wavelet_and_aniso():
"""L'API accepte wavelet + aniso_open et les transmet à --only."""
import subprocess
import lidar_pipeline.webapp as webapp
captured = {}
def fake_popen(cmd, **kwargs):
captured["cmd"] = cmd
class _P:
def wait(self):
return 0
def poll(self):
return 0
return _P()
webapp.subprocess.Popen = fake_popen
try:
req = webapp.GenerateRequest(tiles=[[1054, 6882]],
viz=["wavelet", "aniso_open"])
result = webapp.generate(req)
assert result["demarré"] is True
i = captured["cmd"].index("--only")
assert captured["cmd"][i + 1:i + 3] == ["wavelet", "aniso_open"]
finally:
webapp.subprocess.Popen = subprocess.Popen
def test_available_layers_scans_disk(tmp_path, monkeypatch):
"""/api/layers retourne les couches présentes (clé → label)."""
import lidar_pipeline.webapp as webapp
monkeypatch.setattr(webapp, "OUTPUT_DIR", tmp_path)
vis = tmp_path / "visualisations" / "LHD_FXX_1054_6882_PTS_LAMB93_IGN69"
vis.mkdir(parents=True)
(vis / "LHD_FXX_1054_6882_PTS_LAMB93_IGN69_wavelet.avif").write_bytes(b"x")
layers = webapp.available_layers()
assert layers.get("wavelet")
assert "aspect" not in layers
def test_rebuild_index_background(tmp_path, monkeypatch):
"""POST /api/rebuild lance build_index en arrière-plan puis termine."""
import time as _time
import lidar_pipeline.webapp as webapp
import lidar_pipeline.index as index_mod
calls = []
monkeypatch.setattr(index_mod, "build_index", lambda out: calls.append(out))
assert webapp.rebuild_index()["demarré"] is True
for _ in range(100):
if not webapp.rebuild_status()["running"]:
break
_time.sleep(0.05)
assert calls # build_index a bien été appelé
assert webapp.rebuild_status()["running"] is False
assert webapp.rebuild_status()["done"] is not None