The coverage and scan tools use measured per-pixel canopy heights to refine
the ITU-R P.833-9 path-integrated vegetation loss. Tiles are pre-baked from
the ETH Global Canopy Height 2020 dataset (Lang et al. 2023, 10 m global) and
served as static PNGs under /tiles/canopy/{z}/{x}/{y}.png.
For the propagation model that consumes these tiles, see ../RF-MODEL.md.
The bake auto-downloads the relevant 3°×3° COGs from ETH's libdrive share into
output/canopy-source/, then bakes z=8..12 PNG tiles into output/canopy/.
docker compose --profile bake run --rm canopy-bakepip install -r scripts/requirements-canopy.txt
python scripts/canopy_tiles.pyDefault scope is CONUS (lower 48). Without canopy tiles the frontend falls back to class-nominal heights from NLCD (e.g. 20 m for Evergreen Forest) — the coverage tool still works, just less accurate over canopy variation.
First-run footprint (CONUS): ~30 GB downloaded COGs + ~1–2 GB output tiles.
Time: ~1–4 hours total depending on the ETH server and CPU. Idempotent — ctrl-C and re-run to resume.
# Single state / metro
python scripts/canopy_tiles.py --bbox -125 32 -113 43
# Full Earth (~50 GB download, hours of bake time)
python scripts/canopy_tiles.py --scope global
# AK only (uses ETH coverage extent N81..S57)
python scripts/canopy_tiles.py --bbox -170 51 -129 71ETH publishes a per-pixel standard-deviation file (*_Map_SD.tif) alongside
each height file. The frontend can use it to weight uncertainty: where σ
exceeds the height, the value is treated as low-confidence and the model
falls back toward the class-nominal value.
Off by default (it doubles the download). Enable with:
python scripts/canopy_tiles.py --include-stddev--scope conus|global Convenience preset for --bbox (default: conus).
--bbox W S E N Geographic bbox in EPSG:4326. Overrides --scope.
--src-dir PATH Cache for downloaded COGs (default: output/canopy-source).
--out PATH Tile output directory (default: output/canopy).
--zooms MIN MAX Zoom range (default: 8 12; ETH is 10 m native).
--workers N Bake worker count (default: cpu_count).
--download-workers N Parallel ETH downloads (default: 2; max 4 — be polite).
--include-stddev Also download std-dev and pack into B channel.
--force Overwrite existing tiles instead of skipping.
The Meshinfo API mounts output/canopy (configurable via canopy.tile_dir in
config.toml) at /tiles/canopy automatically when canopy.enabled = true.
Both compose files bind-mount ./output:/app/output, so tiles baked on the
host appear at /app/output/canopy inside the container with no extra config.
The frontend fetches via the Caddy /api/* proxy:
/api/tiles/canopy/{z}/{x}/{y}.png → meshinfo:9000/tiles/canopy/{z}/{x}/{y}.png
Tiles respond with Cache-Control: public, max-age=86400 (1 day).
Missing dependency: rasterio— installscripts/requirements-canopy.txt.- All cells absent — bbox doesn't overlap any ETH-published cell. Most often this means the bbox is over open ocean. Check the ETH tile index at https://langnico.github.io/globalcanopyheight/.
- Slow downloads — leave
--download-workersat 2; raising it does not speed things up much against a single server, and will get you rate-limited. - Slow bake — bump
--workersonce downloads are done.
Each output tile is a 256×256 RGBA PNG:
- R = high byte of canopy height in metres (uint16 packed into R/G).
- G = low byte of canopy height.
- B = std-dev in metres (uint8); 0 if
--include-stddevwas off. - A = 255 valid, 0 nodata. Frontend falls back to NLCD class-nominal on nodata.
A=255 with height=0 is "valid measurement, no canopy here" (e.g. clearing in a forest, fire scar) — distinct from A=0 (the bake didn't cover this region).
ETH has not yet published a v2 (as of 2026). When they do, update
ETH_BASE_URL and ETH_QUERY_PATH in
canopy_tiles.py to point at the new share, delete
output/canopy-source/ and output/canopy/, and re-run the bake.