Generate a tree inventory from a CHM
You are viewing in-progress documentation for v2 (Beta). Switch to the stable version for the current production release.
When you have a canopy height model (CHM) — a raster of vegetation height — you can detect individual trees from it directly, rather than sampling them statistically from TreeMap. FastFuels does this in two steps:
- Build a CHM grid — here from NAIP aerial imagery (
canopy/naip), a ~0.6 m canopy height model covering the conterminous US. - Isolate stems — run a tree-detection algorithm over the CHM to place
one tree at each detected treetop (
tree/chm).
For the ideas behind step 2 — why a treetop is a local maximum, and how the two algorithms differ — see How tree detection from a CHM works.
Prerequisites
Section titled “Prerequisites”-
An API key: my-api-key.
-
A domain: your-domain-id. See Create a domain.
The whole flow in one script:
import time
import requests
API_KEY = "my-api-key"DOMAIN_ID = "your-domain-id"BASE = "https://api-v2-prod-nyvjyh5ywa-uw.a.run.app"HEADERS = {"api-key": API_KEY}
def poll(kind: str, resource_id: str) -> dict: while True: r = requests.get( f"{BASE}/domains/{DOMAIN_ID}/{kind}/{resource_id}", headers=HEADERS ).json() if r["status"] in ("completed", "failed"): return r time.sleep(5)
# Step 1: a canopy height model from NAIP aerial imagery (~0.6 m, CONUS).chm = requests.post( f"{BASE}/domains/{DOMAIN_ID}/grids/canopy/naip", headers=HEADERS, json={"name": "NAIP CHM"},).json()chm = poll("grids", chm["id"])
# Step 2: isolate individual stems from the CHM with a local-maxima filter.inv = requests.post( f"{BASE}/domains/{DOMAIN_ID}/inventories/tree/chm", headers=HEADERS, json={ "name": "Tree inventory from CHM", "source_chm_grid_id": chm["id"], "algorithm": {"name": "lmf", "min_height": 2, "footprint_size": 3}, },).json()inv = poll("inventories", inv["id"])print(inv["id"], inv["status"]) # -> <inventory id> completedStep 1 — Build the CHM grid
Section titled “Step 1 — Build the CHM grid”NAIP imagery is processed by a deep-learning model into a ~0.6 m canopy height surface, so this grid is fine-grained. It is a modeled surface (≈1 m typical height error) that also captures buildings and other tall structures — see Common pitfalls. No version or band selection is needed — the source resolves the NAIP-CHM tiles that cover your domain.
curl -X 'POST' \ 'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/grids/canopy/naip' \ -H 'accept: application/json' \ -H 'api-key: my-api-key' \ -H 'Content-Type: application/json' \ -d '{ "name": "NAIP CHM"}'Poll to completed and record the grid id: your-chm-grid-id.
{ "id": "your-chm-grid-id", "domain_id": "your-domain-id", "name": "NAIP CHM", "description": "", "status": "completed", "progress": { "percent": 100, "message": "Complete" }, "created_on": "2026-06-05T13:46:28.404212Z", "modified_on": "2026-06-05T13:46:50.517489Z", "source": { "product": "naip", "name": "canopy", "tile_metadata": { "native_crs": "EPSG:32611", "tile_count": 1, "tiles": [ "https://rangeland.ntsg.umt.edu/data/naip-chm/2023/11/m_4611416_nw_11_060_20231019_20240103_chm.tif" ], "tile_source": null, "acquisition_dates": null }, "description": "NAIP high-resolution canopy height model at ~0.6m resolution (CONUS)", "alignment": { "resolution": null, "target": "domain", "method": null }, "extent_buffer_cells": 0 }, "modifications": [], "bands": [ { "key": "chm", "name": "Canopy Height", "description": "Height of the canopy top above ground.", "type": "continuous", "unit": "m", "index": 0, "nodata": null } ], "georeference": { "crs": "EPSG:32611", "transform": [ 0.5997260672151095, 0.0, 720226.0, 0.0, -0.5997260672150779, 5190646.595949142 ], "shape": [1475, 2181] }, "error": null, "chunks": { "shape": [512, 512], "count": 15, "count_by_axis": { "x": 5, "y": 3 } }, "tags": []}The grid carries a single chm band (canopy height, in meters).
Step 2 — Isolate stems
Section titled “Step 2 — Isolate stems”Point source_chm_grid_id at the completed CHM grid and pick a stem-isolation
algorithm:
lmf(local maxima filter) — finds treetops as local height maxima within a fixedfootprint_sizewindow; ignores anything belowmin_height.vwf(variable window filter) — scales the search window with canopy height, which separates large and small crowns better in mixed stands.
Pick your algorithm below. For the full request schema and every field default, see the live API reference.
curl -X 'POST' \ 'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/inventories/tree/chm' \ -H 'accept: application/json' \ -H 'api-key: my-api-key' \ -H 'Content-Type: application/json' \ -d '{ "name": "Tree inventory from CHM", "source_chm_grid_id": "your-chm-grid-id", "algorithm": { "name": "lmf", "min_height": 2, "footprint_size": 3 }}'{ "id": "your-inventory-id", "domain_id": "your-domain-id", "type": "tree", "name": "Tree inventory from CHM", "description": "", "status": "pending", "progress": null, "created_on": "2026-06-05T13:46:55.133410", "modified_on": "2026-06-05T13:46:55.133410", "source": { "name": "chm", "source_chm_grid_id": "your-chm-grid-id", "algorithm": { "name": "lmf", "min_height": 2.0, "footprint_size": 3 } }, "modifications": [], "treatments": [], "columns": [ { "key": "x", "type": "continuous", "unit": "m" }, { "key": "y", "type": "continuous", "unit": "m" }, { "key": "height", "type": "continuous", "unit": "m" } ], "georeference": null, "error": null, "tags": []}curl -X 'POST' \ 'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/inventories/tree/chm' \ -H 'accept: application/json' \ -H 'api-key: my-api-key' \ -H 'Content-Type: application/json' \ -d '{ "name": "Tree inventory from CHM", "source_chm_grid_id": "your-chm-grid-id", "algorithm": { "name": "vwf", "min_height": 2, "crown_ratio": 0.1, "crown_offset": 1.0 }}'{ "id": "your-inventory-id", "domain_id": "your-domain-id", "type": "tree", "name": "Tree inventory from CHM", "description": "", "status": "pending", "progress": null, "created_on": "2026-06-05T14:25:45.963971", "modified_on": "2026-06-05T14:25:45.963971", "source": { "name": "chm", "source_chm_grid_id": "your-chm-grid-id", "algorithm": { "name": "vwf", "min_height": 2.0, "spatial_resolution": null, "crown_ratio": 0.1, "crown_offset": 1.0 } }, "modifications": [], "treatments": [], "columns": [ { "key": "x", "type": "continuous", "unit": "m" }, { "key": "y", "type": "continuous", "unit": "m" }, { "key": "height", "type": "continuous", "unit": "m" } ], "georeference": null, "error": null, "tags": []}Record the inventory id: your-inventory-id. Poll until completed:
curl -X 'GET' \ 'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/inventories/your-inventory-id' \ -H 'accept: application/json' \ -H 'api-key: my-api-key'{ "id": "your-inventory-id", "domain_id": "your-domain-id", "type": "tree", "name": "Tree inventory from CHM", "description": "", "status": "completed", "progress": { "percent": 100, "message": "Complete" }, "created_on": "2026-06-05T13:46:55.133410Z", "modified_on": "2026-06-05T13:47:26.356066Z", "source": { "name": "chm", "algorithm": { "min_height": 2.0, "footprint_size": 3, "name": "lmf" }, "source_chm_grid_id": "your-chm-grid-id" }, "modifications": [], "treatments": [], "columns": [ { "key": "x", "type": "continuous", "unit": "m" }, { "key": "y", "type": "continuous", "unit": "m" }, { "key": "height", "type": "continuous", "unit": "m" } ], "georeference": { "crs": "EPSG:32611", "bounds": [720226.0, 5189762.0, 721534.0, 5190646.0] }, "error": null, "tags": []}On the Blue Mountain domain, lmf with a 3-pixel footprint and a 2 m minimum
detects ≈9,200 treetops; vwf with the default crown scaling detects ≈8,500.
These are overstory detections, not a full stem count — trees beneath the
dominant canopy aren’t seen (why).
The inventory’s data columns are x, y, and height — position and treetop
height, the observables a CHM provides.

Detected treetops (LMF) over the NAIP CHM, a 175 m window of the Blue Mountain domain. Each dot is one row of the inventory, placed at a local height maximum. Greener cells are taller canopy.
Next steps
Section titled “Next steps”- Fill in morphology, then voxelize. A CHM inventory carries only
x,y, andheight, so it can’t be voxelized as-is — building 3D crowns needsdbh,crown_ratio, and species. Run GDAM allometry to impute those columns, then voxelize the inventory into a 3D canopy fuel grid. This is the intended path from a CHM to fuel. - Understand it — how tree detection from a CHM works: why a treetop is a local maximum, fixed vs. variable windows, and the over-/under-detection trade-off.
- Tune it — inspect the detection and adjust the parameters until the treetops track the crowns.
- Trim it — remove trees near roads or water.
- Read the treetops — fetch and stream the inventory data.
Common pitfalls
Section titled “Common pitfalls”- Too many or too few trees.
footprint_sizeandmin_heightcontrol detection sensitivity: a smaller footprint finds more (and more spurious) treetops; a highermin_heightdrops understory. Tune them to your stand, or switch tovwffor height-varying crowns. See How tree detection from a CHM works for the trade-offs. - Detecting buildings as trees. NAIP-CHM is a surface model that includes buildings, powerlines, and other tall structures. Over developed or wildland-urban-interface land, detection places treetops on infrastructure — mask it (for example, with building footprints) or restrict the domain to vegetated areas.
- Expecting species or DBH. A CHM doesn’t observe them — the inventory has
height only, so anything that needs per-tree morphology (voxelization,
silvicultural treatments) can’t run on it directly. Don’t hand-roll a
height–diameter relationship or abandon the CHM for TreeMap: fill in
dbh,crown_ratio, and species with the purpose-built GDAM allometry step, which imputes them from each tree’s position and height. - Treating the canopy as a complete fuel column. A CHM sees the overstory tops, not the sub-canopy and ladder fuels that carry fire into the crowns. For physics-based fire simulation, pair the CHM canopy with a modeled sub-canopy and surface layer (why).
- Creating the inventory before the CHM grid is
completed.tree/chmreads the grid’s data; poll step 1 tocompletedfirst.