Web pipeline that turns user-uploaded LiDAR data into nDSM Cloud-Optimized GeoTIFFs ready to consume by the Helios Home Assistant card via its lidar-local-ndsm-* bring-your-own configuration.

Live at helios-lidar.org. The site version is kept in lock-step with the Helios card version.

Why

Helios renders LiDAR-derived shadows on the 3D map when the user lives inside a region covered by one of its built-in providers (France IGN HD, Defra UK, IGN Spain, AHN Netherlands, Kartverket Norway, Geobasis NRW, GUGiK Poland, NRCan HRDEM Canada, LGB Brandenburg / Berlin, VCGI Vermont). Outside those, the card can already read a self-hosted nDSM (height-above-ground) GeoTIFF through its lidar-local-ndsm-* config keys, but getting from raw open-data LiDAR (a .laz point cloud or a DSM + DTM pair) to a properly tiled COG is a multi-step GDAL dance most users don't want to learn.

Helios-Lidar takes the user's raw input through a browser upload, runs the conversion server-side, and hands back:

  1. a hosted .tif Cloud-Optimized GeoTIFF with the right CRS, tiling and overviews,
  2. a lidar-local-ndsm-* YAML snippet ready to paste into the Helios card config,
  3. a 3D preview of the result matching the card's own LiDAR View.

How it works

Browser
   |  HTTPS upload (one of: DSM + DTM raster pair, OR a LAS / LAZ file)
   v
nginx -> FastAPI app on 127.0.0.1:8000
   |
   v
Job worker
   |  raster_pair path: GDAL subtract DTM from DSM -> nDSM
   |  point_cloud path: laspy reads points, scipy KDTree finds
   |                    nearest ground per point, GDAL writes the
   |                    per-cell max height -> nDSM
   |  GDAL Translate -> Cloud-Optimized GeoTIFF with overviews
   v
COG saved under /var/helios-lidar/output/<job_id>.tif
   |  served back with CORS headers from
   |  https://helios-lidar.org/output/<job_id>.tif
   v
Browser polls /jobs/<job_id> for status, displays the COG link plus
the ready-to-paste YAML snippet, and renders a 3D preview of the
nDSM next to the snippet.

The browser auto-triggers the file save dialog as soon as the job lands done. The server-side .tif is wiped 10 minutes later by a threading.Timer (5-minute download window plus 5 minutes of slack). A 30-minute cron sweep is the backstop in case the in-process timer ever misses.

Layout

.
|-- LIDAR_SOURCES.md          Community-maintained list of national
|                             LiDAR portals (rendered live on the
|                             upload page).
|-- app/                      FastAPI server (routes, job tracking,
|                             markdown rendering for the README and
|                             sources page).
|-- pipeline/                 GDAL + laspy + scipy wrappers.
|-- frontend/                 Browser upload page + /helios-card page
|                             (vanilla HTML + ES modules, no build).
|-- deploy/                   nginx vhost, systemd unit, cleanup cron
|                             templates.
|-- tests/                    Pipeline + health tests with a tiny
|                             sample fixture.
|-- .github/                  Issue / PR templates.
|-- pyproject.toml            Project metadata + Python dependencies.
`-- README.md                 You are here.

Local development

Requires Python 3.11+ and the system GDAL library (apt install gdal-bin libgdal-dev on Debian / Ubuntu, plus the Python bindings via apt install python3-gdal and a venv created with --system-site-packages so the bindings are visible).

python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
uvicorn app.main:app --reload

The API will be available at http://127.0.0.1:8000; the OpenAPI documentation auto-generated by FastAPI sits at http://127.0.0.1:8000/docs.

Deploying

The deploy/ directory holds an example nginx vhost, a systemd unit and a cleanup cron entry. The production target is a single OVH VPS running uvicorn with --workers 1 (scipy + multiprocessing.fork deadlocks otherwise) and OMP_NUM_THREADS=4 for the KDTree query stage.