Commit Graph

83 Commits

Author SHA1 Message Date
d9a4ea9a4e Add tests for priority-flood numba/Python parity and _strip_lidar_ext 2026-09-07 21:23:52 +02:00
cd5bf54066 Fix dangerous pkill, circular import, and dedup _res_suffix
- Scope pkill to 'pdal pipeline' (avoids killing unrelated PDAL jobs)
- Inline _strip_lidar_ext in dtm.py to remove circular import from pipeline
- _res_suffix_str now delegates to LidarArchaeoPipeline._res_suffix
2026-09-07 21:21:57 +02:00
d27b92a26a Add wall-clock timeout to parallel worker pool
Prevents indefinite hang if a worker gets stuck (deadlocked PDAL,
I/O stall). 2-hour safety net for the entire batch; remaining
futures are cancelled on timeout.
2026-09-07 21:20:19 +02:00
b9ab13c2d1 Add numba JIT for priority-flood sink filling
Replaces the pure-Python heapq implementation with a compiled binary
min-heap (~200x faster for large grids). Falls back to Python when
numba is unavailable. Uses a flat array view for heap elevation
comparisons to avoid 2D indexing issues in nopython mode.
2026-09-07 21:18:59 +02:00
74580a922b Fix NaN artifacts in tif_to_crop tile rendering
Replace NaN values with 0 before colormap application so no-data
zones render as the darkest color instead of undefined pixels.
2026-09-07 21:15:29 +02:00
23969c9e14 Add Leaflet interactive map, tile generator compose, auto-sync cache, deploy docs
index.py rewritten as a continuous Leaflet map: rotated L93 tiles, stackable
visualization layers (per-layer opacity, drag-reorder persisted in
localStorage), tile info panel, live rebuild after each tile during a run.
Leaflet is vendored in assets/vendor/ so the map works fully offline;
georeferencing falls back rasterio -> pyproj -> affine so the lightweight
webapp (no GDAL) is supported.

docker-compose.worker.yml adds the tile generator service (full image + GPU)
that remote webapps call via LIDAR_GENERATION_URL, plus a one-shot process
profile. webapp.py gains LIDAR_AUTO_SYNC_SECONDS periodic cache refresh and
LIDAR_REGEN_CIDR restricting generation to the local network. run.sh
--serve-webapp now mounts ~/.ssh read-only so the rsync sync works.

docs/DEPLOY_WEBAPP.md completed for Raspberry Pi deployment: prerequisites,
git clone install, SSH key setup, first sync, update procedure and
troubleshooting.
2026-09-04 21:36:14 +02:00
422f58d772 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.
2026-09-02 19:44:39 +02:00
ed3e90ea89 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)
2026-08-31 19:02:14 +02:00
8ca65155db Add web map with zone generation API, side job queue, and restore historical DTM hole rendering
- webapp.py: FastAPI serving the continuous map (port 8973) with
  /api/preview, /api/generate and /api/status; tiles are downloaded
  from IGN and processed in a logged subprocess, tracked live in a
  side "File de génération" panel that survives page reloads
- fetch_ign.py: download missing 1 km LiDAR HD tiles from the IGN
  geoplateforme before processing
- index.py: tile thumbnails and 500 m subtiles are now invalidated by
  mtime so regenerating a tile refreshes its cached images; progress
  logging per tile
- dtm.py: back to the historical gap handling (small gaps filled by
  fillnodata only, larger holes left as nodata rendered black);
  lowest-return floor only via --bare-earth, IGN class selection via
  --ign-classes
- cli.py: positional input now optional (--rebuild-index works alone)
- docker-compose.yml: serve (GPU, port 8973) and process services;
  launch via docker compose only (documented in AGENTS.md/AGENTS.md)
- tests: 131 passing, incl. regressions for thumbnail staleness,
  --rebuild-index without input, and nodata rendering
2026-08-31 18:07:14 +02:00
35bd827790 fix: modal click handler + colormap gradient color format 2026-07-29 00:11:50 +02:00
3d79a5ee72 carte continue: cropped images + continuous map + PDF download via jsPDF 2026-07-28 23:37:40 +02:00
54dbec145e prepare: add index module, update pipeline, tests, Dockerfile, and run.sh 2026-07-28 23:20:20 +02:00
c58ca3f477 Fix multi-GPU detection, VRAM leak on disable, and orphaned GPU array handling
_gpu_candidates was never populated (typo: _candidate_gpus), breaking
num_gpus()/available_gpu_ids() and the multi-GPU round-robin. available_gpu_ids
also indexed tuples with dict syntax (TypeError). disable_gpu() now frees the
memory pool before clearing refs (was leaking VRAM). to_cpu() and safe_gpu_call()
survive disable_gpu() via a persistent _cupy_ndarray type reference so orphaned
GPU arrays are recovered to CPU. log_gpu_status() now uses Device(0) instead of
the host index (CuPy renumbers to 0 after CUDA_VISIBLE_DEVICES).
2026-07-23 20:06:14 +02:00
4dafae6d02 Refine MSRM: add small scales (3,8,15m), drop 100/200m, clip |z| to 3.0
Small archaeological features (ditches, walls, post-holes) were drowned out
by large-scale topography (100-200m). Fix: replace 100/200m with finer
scales (3, 8, 15m), weight 5-10m heaviest, clip absolute z-score to 3.0
before combination to prevent large-scale outliers from dominating.
2026-06-01 23:18:15 +02:00
8478106e51 Fix GPU bugs, restore aspect viz, fix anomaly mask, revert flow_acc to vectorized D8
GPU: num_gpus() returns real count via _gpu_candidates (was always 0/1),
available_gpu_ids() added. Pipeline: round-robin on real GPU host indices
instead of file enumerate index. _process_file_standalone signature simplified.

Restore generate_aspect using SharedDEM gradient (dy, dx). Colormap twilight
0-360 fixed range. VIZ_STEPS back to 16.

Flow accumulation: revert to vectorized numpy D8 direction + module-level
numba accumulator (cached, top-down sort) with Python fallback. Priority-flood
NaN-aware. Log1p transform.

Anomaly mask: replace RMS+fixed 2sigma threshold (was blank) with weighted
sum of |z-score| + adaptive percentile threshold. Absolute z-score captures
both positive and negative deviations. 6% signal detected vs 0% before.
2026-06-01 23:03:19 +02:00
618cd620e3 Fix GPU auto-detection for RTX 5060 Ti (sm_120) — CUDA_VISIBLE_DEVICES via run.sh -e + warm-up in Python 2026-06-01 00:07:40 +02:00
67d024a64e Auto-detect best GPU with sm_120 skip: prefer RTX 5060 Ti, fall back to 4060 Ti when nvcc/CuPy does not support sm_120 yet 2026-05-31 23:14:23 +02:00
8490b5ddf0 Use CuPy Device API instead of CUDA_VISIBLE_DEVICES for JIT compatibility on sm_120 2026-05-31 22:14:18 +02:00
a5af50c043 Remove sm_120 filter: CuPy 13.4 JIT supports RTX 5060 Ti 2026-05-31 22:04:20 +02:00
04eac7eded Support RTX 5060 Ti: CuPy 13.4 JIT compiles kernels at runtime for sm_120 2026-05-31 22:01:08 +02:00
17fd96fdf5 Remove broken CuPy 13.x memory pool setup 2026-05-31 21:35:27 +02:00
5a9cdddcfb Auto-detect usable GPU (skip sm_120 RTX 5060, fallback to RTX 4060 Ti) 2026-05-31 21:32:34 +02:00
9119d63bc3 Auto-detect best GPU (RTX 5060 preferred) + build CuPy from source for sm_120 2026-05-31 20:55:06 +02:00
d2f382c94d Per-GPU warm-up with fallback CPU on NO_BINARY + memory pool limit per worker 2026-05-31 20:30:22 +02:00
a8fd8addb7 Limit GPU memory pool per worker to prevent OOM with multi-worker 2026-05-31 19:51:32 +02:00
deed10ea62 Fix ray-tracing OOM + NO_BINARY: pad DEM on CPU, free GPU intermediates 2026-05-31 19:36:29 +02:00
2ccedbb9e0 Fix ray-tracing GPU OOM: process dirs sequentially on CPU, free GPU memory between dirs 2026-05-31 18:44:54 +02:00
89f3333b65 Add anomaly mask: automatic threshold detection across all viz layers 2026-05-31 18:34:45 +02:00
4193174196 Fix GPU tag: recalculate at log end so fallback shows no [GPU] 2026-05-31 17:48:32 +02:00
65631b18b0 Fix colormap legends: remove (deg) on normalized openness, correct flow_acc YlGn direction, remove LRM references, fix wavelet norm mode 2026-05-31 17:33:01 +02:00
07d2a8aea9 Update docs for GPU selection, fix test assertions + test_pipeline 2026-05-31 17:19:53 +02:00
eb9545b56d Document GPU selection in CLI help + epilog examples 2026-05-31 17:19:38 +02:00
929fac9aa0 Remove LRM, TPI, aspect, curvature, paths + add flow accumulation, directional Gabor wavelets, multi-radius ray-tracing 2026-05-31 17:19:31 +02:00
b5b6787956 Add --gpu flag to select specific GPU(s) for processing 2026-05-31 16:06:58 +02:00
266214fe3e Fix 12 bugs: D8 flow accumulation, PDF AVIF support, GPU memory leaks, dead code, SAILORE sigma scaling 2026-05-31 15:13:11 +02:00
30122c71ed Performance optimizations and rendering improvements
GPU multi-processing fix:
- gpu.py: revert to CUDA_VISIBLE_DEVICES approach with lazy CuPy init
  (Device.use() caused CUDA_ERROR_NO_BINARY_FOR_GPU on GPU 1)
- CuPy is imported lazily on first to_gpu() call, allowing
  CUDA_VISIBLE_DEVICES to be set before CUDA context creation
- nvidia-smi used for GPU count detection (no CUDA import needed)
- pipeline.py: add tip message suggesting -w N when multiple GPUs detected

Rendering improvements:
- Title: split into bold title (14pt) + italic description (10pt)
- North arrow: moved inside data area (top-right) with transparent
  background — no longer overlaps title
- Colorbar: full height (compass gap removed), ScalarFormatter with
  useOffset=False to prevent scientific notation on small values

Performance:
- rendering.py: save matplotlib figure to BytesIO instead of temp PNG
  file — eliminates disk I/O between matplotlib and PIL
- visualizations.py: cap max_dist at 300 for ray-tracing (SVF,
  openness, aniso_open) — avoids 500+ iterations at 0.2m resolution
- pipeline.py: deduplicate n_gpus calculation in parallel path
2026-05-15 12:32:51 +02:00
a3f7b44874 Fix multi-GPU with lazy CuPy init + rendering improvements
GPU fix:
- Revert to CUDA_VISIBLE_DEVICES approach but with lazy CuPy init
- gpu.py: CuPy is no longer imported at module level; _init_gpu()
  imports it lazily on first to_gpu() call. This allows workers to
  set CUDA_VISIBLE_DEVICES before CuPy creates a CUDA context.
- gpu.py: detect GPU count via nvidia-smi (no CUDA context needed)
- pipeline.py: each worker sets CUDA_VISIBLE_DEVICES=N before CuPy
  init, so each process uses only its assigned GPU

Rendering improvements:
- Title: split into bold title (14pt) + italic description (10pt)
  instead of single 15pt bold block
- North arrow: moved inside data area (top-right corner) with
  semi-transparent white background for readability over data
- Colorbar: full height (no gap for compass rose), added
  ScalarFormatter(useOffset=False) to avoid scientific notation
- Colorbar compass rose gap removed since north arrow is now
  inside the data area
2026-05-15 12:24:57 +02:00
b4a0e384c9 Change minimap zoom to level 10 for wider view and faster download
Zoom 10 gives ~150m/px with 30km context (274px image), ideal for a
small inset. Much faster to download than zoom 12 (1100px).
2026-05-15 12:03:19 +02:00
9e89686ac5 Reduce location map context to 30km and use zoom 12
The 80km context made the red LiDAR zone rectangle too small to see.
30km (15km radius) at zoom 12 gives a clear view where the 1km data
zone is visible as a distinct red rectangle in the inset map.
2026-05-15 12:01:37 +02:00
5af53a390f Add multi-GPU support and fix scale bar / location map overlap
Multi-GPU:
- gpu.py: lazy CuPy initialization so CUDA_VISIBLE_DEVICES takes
  effect before context creation in worker processes
- gpu.py: detect GPU count via nvidia-smi (no CUDA import needed)
- gpu.py: add set_active_gpu() to assign workers to specific GPUs
- pipeline.py: distribute files across GPUs (file % num_gpus) in
  parallel mode so both GPUs are used simultaneously
- pipeline.py: log GPU count when multiple GPUs detected

Layout fixes:
- rendering.py: move scale bar left of location map to avoid overlap
  (scale bar ends at fig_x=0.78, map starts at 0.82)
- rendering.py: expand location map inset to 0.16x0.13 fig coords
- rendering.py: return bounds from _download_location_map so imshow
  extent matches the actual IGN tile coverage (80km context)
- ign.py: add min_zoom parameter to download_ign_tiles, fixing the
  location map that was broken (zoom 10 blocked by hardcoded min_zoom=15)
2026-05-15 12:00:34 +02:00
4848f25326 Upgrade colormaps to Crameri scientific palette and fix IGN location map
- Replace RdBu_r with Crameri roma for all 6 relief-family colormaps
  (curvature, mslrm, lrm, tpi, sailore, aniso_open): perceptually
  uniform, CVD-friendly, dark center makes near-zero values visible
- Replace hsv with twilight for aspect: perceptually uniform cyclic
  colormap with no hue discontinuity
- Add cmcrameri dependency (falls back to RdBu_r if unavailable)
- Fix IGN location map: pass min_zoom=8 to download_ign_tiles so zoom
  10 context maps work (was hardcoded to min_zoom=15, blocking the
  loop entirely for zoom_level=10)
- Expand location map context from 3x extent to 80km fixed radius for
  better regional context at zoom 10
- Remove dead code: flow colormap entry, _nice_scale dead loop,
  aspect='auto' in location map imshow, outdated PDF report order list
- Fix SVF docstring: 8 directions → 16 directions
2026-05-15 11:50:15 +02:00
c634db573a Fix corrupted COPC detection, add CSF→SMRF fallback, improve MSRM colormap, add SVF and anisotropic openness
- validate_laz: verify point data accessibility (not just headers) to detect
  corrupted COPC files that pass header checks but fail on data reads
- classify_ground: fallback from CSF to SMRF when CSF produces no ground
  points or PDAL errors (fixes 2/9 failing tiles)
- MSRM: preserve sign in weighted combination so RdBu_r colormap shows
  both red (elevated) and blue (depressed) instead of red only
- Add Sky-View Factor (SVF) visualization: cos²(horizon angle) over 16
  directions, excellent for archaeological earthwork detection
- Add Anisotropic Openness: directional weighting (NW-SE/NE-SW) enhances
  linear feature detection aligned with common settlement patterns
- Remove anomalies and flow visualizations (replaced by SVF + aniso_open)
- Location inset: use IGN topographic map at zoom 10 instead of simplified
  France outline, with red rectangle marker and fallback
- Remove flow (hydrological accumulation) from VIZ_STEPS
2026-05-15 01:38:09 +02:00
da454bd23e Improve visualizations: adaptive scales, revert z-score to std normalization
- MSRM/TPI/roughness/anomalies: revert z-score (x-mean)/std to std normalization x/std
  to preserve contrast and visibility of linear features (paths, ditches, trenches)
- MSRM: adaptive scales based on resolution, archaeological weight combination
- TPI: extend from 2 to 4 scales (3m/15m/50m/200m) with weighted combination
- Hillshade: 8 directions instead of 4, altitude 35° instead of 30°
- LRM: adaptive sigma based on resolution
- Openness: doubled radius (100m instead of 50m)
- Roughness: multi-scale (3m fine + 15m broad) instead of single 5x5 window
- Anomalies: uses MSRM multi-scale relief instead of single LRM 15m
- Wavelet: 8 adaptive scales, std normalization, archaeological weights
- Remove svf (Sky-View Factor) and local_dominance visualizations
- Add AVIF format support (default), quality 98
- Add multi-resolution support (-r 0.5,0.2)
- Improve Ctrl+C handling for immediate process termination
- Update rendering.py descriptions for all modified visualizations
2026-05-14 23:12:08 +02:00
57ffdbae67 Add multi-resolution support and remove PDF generation
- --resolution now accepts comma-separated values (e.g. 0.5,0.2)
- Additional resolutions get suffixed output dirs: basename_r0p2/
- DTM files are named basename_dtm_r0p2.tif for extra resolutions
- Ground classification is done once and shared across resolutions
- PDF report generation removed per user request
- Fix --file argument to accept full filenames with extensions
2026-05-14 21:29:45 +02:00
eccec302fe Fix --file argument to accept full filenames with extensions
Previously --file LHD_FXX_...copc.laz would fail because it appended
extensions. Now tries exact filename match first, then falls back to
adding extensions.
2026-05-14 21:17:03 +02:00
6aef7b8af3 Add WebP quality control and selective visualization (--only / --skip)
- WebP output now uses quality=85 by default (down from lossless),
  reducing file size by ~75% (35MB → 5-8MB per visualization)
- Added --quality N (1-100) and --lossless flags in CLI and run.sh
- Added --only and --skip to select/exclude specific visualizations
  (e.g., --only hillshade,svf,lrm or --skip ortho,topo)
- VIZ_STEPS filtering is done in LidarArchaeoPipeline.__init__
- SharedDEM is skipped when all selected visualizations already exist
- Invalid visualization names are validated at startup with clear error
2026-05-14 21:15:21 +02:00
5faa85b54b Handle empty point clouds gracefully at every pipeline stage
Fixes "zero-size array to reduction operation" crash on corrupt/incomplete
LAZ files. Added checks at each step:

- validate_laz(): check point_count > 0 via laspy header, parse PDAL
  info JSON for point count when using PDAL fallback
- detect_ground_method(): return 'smrf' default if point cloud is empty
  after PDAL conversion instead of crashing on np.max(empty_array)
- _read_with_pdal(): log warning and return None if converted file has
  0 points
- create_dtm_fast(): fail gracefully if ground file has 0 points
- classify_ground(): check output file size after PDAL pipeline to
  catch empty ground classifications early
2026-05-14 20:56:52 +02:00
96920b95cc List all processed LAZ files with status in pipeline summary
Show each file with ✓/✗ before the success/fail counts, so the user
can see at a glance which files succeeded and which failed.
2026-05-14 20:49:30 +02:00
a98d0836b7 Skip SharedDEM computation when all visualizations already exist
Two optimizations to avoid ~2min wasted per file on re-runs:

1. pipeline.py: Check which visualizations need regeneration before
   computing SharedDEM. If all WebP outputs exist, skip SharedDEM
   entirely. If only IGN overlays need updating, also skip SharedDEM.

2. visualizations.py: Make SharedDEM attributes lazy (filled, gradient,
   lrm_15) so only the data actually needed is computed. For example,
   if only hillshade is regenerated, LRM at 15m is never calculated.
2026-05-14 20:40:51 +02:00
0d9e444118 Upgrade PDAL to 2.10 via conda-forge, add COPC v1.1 support
- Dockerfile: install PDAL 2.10.1 from conda-forge (was 2.3 from apt)
  Ubuntu 22.04's PDAL 2.3 cannot read COPC v1.1 files from IGN LiDAR HD
- dtm.py: add _read_with_pdal() fallback for COPC files that laspy can't read
- dtm.py: validate_laz() now tries PDAL when laspy fails
- dtm.py: create_dtm_fast() and detect_ground_method() use PDAL fallback
- ign.py: auto-retry at lower zoom on 404 errors
- pipeline.py: check DTM resolution mismatch and regenerate if needed
- pipeline.py: propagate actual DTM resolution to visualizations
- pipeline.py: add --init to docker run for proper Ctrl+C signal handling
- Remove RRIM and Multi-Hillshade RGB visualizations
2026-05-14 19:01:05 +02:00