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:
- a hosted
.tifCloud-Optimized GeoTIFF with the right CRS, tiling and overviews, - a
lidar-local-ndsm-*YAML snippet ready to paste into the Helios card config, - 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.
Comments