Skip to content

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:

  1. Build a CHM grid — worked through below with NAIP aerial imagery (canopy/naip), a ~0.6 m canopy height model covering the conterminous US. It is one of several sources; see Choosing a source.
  2. 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.

  1. An API key: my-api-key.

  2. A domain: your-domain-id. See Create a domain.

Four canopy constructors emit a chm band, and a fifth route lets you bring your own raster. Any of them can feed Step 2:

SourceEndpointCell sizeWhat it is
NAIPgrids/canopy/naip~0.6 mA deep-learning canopy height model derived from aerial imagery. CONUS only. Used in Step 1 below.
Metagrids/canopy/meta~1 mA deep-learning canopy height model with global coverage.
Airborne lidargrids/canopy/point_cloud1 m or coarserRasterized from a point cloud you fetched from 3DEP or uploaded. A direct measurement rather than a model.
LANDFIREgrids/canopy/landfire30 mA national canopy layer carrying chm alongside cbd, cbh, and cc. Too coarse for individual-tree detection — a 30 m cell is wider than a crown.
Your own rastergrids/upload/geotiffwhatever you supplyAny CHM the API does not produce itself.

Two things decide the choice more often than quality does:

  • Coverage. The lidar route needs a point cloud, and 3DEP does not cover everywhere. Of six 1 km² boxes we tried around Missoula, Montana, three had 3DEP lidar and three had none — including the Blue Mountain domain used throughout these guides. Check coverage before you plan around it. NAIP and Meta have no such gap.
  • Cell size against crown size. Detection resolves peaks in a raster, so a cell must be well inside a crown for a crown to have a peak. That rules LANDFIRE out for this job and makes it the wrong tool here, however good it is for stand-scale canopy fuel.

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.

POST grids/canopy/naip
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",
"status": "completed",
"source": {
"tile_metadata": {
"acquisition_dates": null,
"tiles": [
"https://rangeland.ntsg.umt.edu/data/naip-chm/2023/11/m_4611416_nw_11_060_20231019_20240103_chm.tif"
],
"tile_source": null,
"tile_count": 1,
"native_crs": "EPSG:32611"
},
"alignment": {
"method": null,
"target": "domain",
"resolution": null
},
"extent_buffer_cells": 0,
"product": "naip",
"description": "NAIP high-resolution canopy height model at ~0.6m resolution (CONUS)",
"name": "canopy"
},
"bands": [
{
"key": "chm",
"type": "continuous",
"index": 0,
"name": "Canopy Height",
"description": "Height above ground of the canopy top.",
"unit": "m",
"nodata": null,
"summary": {
"type": "continuous",
"count": 3216975,
"nodata_count": 0,
"min": 0.0,
"max": 32.689998626708984,
"mean": 5.950183918105713,
"std": 7.25633903389633
}
}
],
"name": "NAIP CHM",
"description": "",
"progress": {
"message": "Complete",
"percent": 100
},
"created_on": "2026-08-27T13:07:17.371609+00:00",
"modified_on": "2026-08-27T13:07:20.783515+00:00",
"checksum": "1a1b879885dc42e084e294c58e8ebe1c",
"modifications": [],
"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).

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 fixed footprint_size window; ignores anything below min_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.

POST inventories/tree/chm — lmf
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
}
}'

Record the inventory id: your-inventory-id. Poll until completed:

GET inventory status
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'

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.

Both counts are specific to this CHM at this cell size. footprint_size is measured in pixels, so a 3-pixel footprint is 1.8 m on this 0.6 m NAIP grid and would be 3 m on a 1 m lidar grid — and the surfaces differ in more than their lattice. The source changes the answer shows how far apart the same request lands on two sources.

A close-up of the NAIP canopy height model with white dots marking each detected treetop sitting on a canopy peak.

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.

Those counts belong to a NAIP CHM at 0.6 m. Run the same request against a CHM built from lidar and you get a different inventory of the same forest.

Here is that comparison on a domain that has both — a 136 ha stand on the Blackfoot River near Bonner, Montana, with a NAIP CHM and a 1 m CHM built from 3DEP lidar, detected with identical parameters (lmf, footprint_size 3, min_height 2 m):

Two canopy height maps of the same 120 metre stand side by side, both green-on-cream with white treetop dots. The NAIP panel on the left shows smooth, round crown mounds with one dot each. The lidar panel on the right shows sharp, blocky, textured crowns carrying several dots each, plus many dots scattered over low vegetation.

The same 120 m stand. NAIP detects 23,513 treetops across the domain; the lidar CHM detects 64,317 — 2.7× more, from the same request body.

The reason is visible in the panels. NAIP is a modeled surface: a network predicts a height for every pixel, and it predicts smoothly, so a crown is a round mound with one summit. The lidar CHM is a measurement: each cell holds the tallest return that happened to land in it, so a crown is a rough cluster of peaks and troughs. A local-maxima filter counts summits, and one of these surfaces has far more of them.

That roughness is measurable. Over canopy cells, in a matched 3 m ground window, the mean local height standard deviation is 2.0 m for NAIP and 3.2 m for the lidar CHM.

A line chart of two canopy height profiles along a 120 metre transect. The green NAIP curve is smooth and rounded; the brown lidar curve is blockier and consistently taller on the same crowns. Two shaded bands mark places where one source records a tree and the other records bare ground.

One transect across that stand. The lidar CHM reads taller on the same crowns — 19.6 m against 16.2 m on the tallest — and the two sources disagree outright twice in 120 m, in both directions.

footprint_size is measured in pixels, so the same number is a different distance on the ground: 3 px is 1.8 m on a 0.6 m NAIP grid and 3.0 m on a 1 m lidar grid. That matters, and it is worth correcting for — but correcting for it does not close the gap.

A horizontal bar chart of three detection runs. NAIP with footprint 3 detects 23,513 treetops with a 1.8 metre ground window; NAIP with footprint 5 detects 19,795 with a 3 metre window; the lidar CHM with footprint 3 detects 64,317 with the same 3 metre window.

Widening NAIP’s footprint to 5 px matches the lidar run’s 3 m search window and drops NAIP to 19,795 — still 3.2× fewer than the lidar CHM. The difference is in the surfaces, not only in the lattice.

Nor is the gap confined to short vegetation that one source resolves and the other smooths away. Every height band gains:

Grouped horizontal bars comparing detected treetop counts by height band for the two sources. The lidar CHM leads in every band, by 6.7 times in the 2 to 4 metre band, around 2.1 times through the main canopy, and 20.8 times in the 25 to 40 metre band.

Detected treetops by height. The gap is widest at both ends — 6.7× among 2–4 m stems, and 20.8× above 25 m, where NAIP’s modeled surface rarely reaches (its 99th percentile height is 20.4 m against the lidar’s 23.6 m).

What to do with this. Neither count is the true stem count, and neither is “the correct one” — 472 detections per hectare from the lidar CHM is over-detection at these settings just as surely as NAIP’s smoothing hides real stems. Treat the source and the parameters as one choice, not two:

  1. Re-tune whenever you change source. Parameters carried over from another CHM are not a starting point, they are a different question. Inspect the overlay and adjust until the dots track the crowns in this raster.

  2. Convert footprint_size to metres before comparing settings — multiply by the grid’s cell size. Two guides quoting “footprint 3” may mean different windows.

  3. Never compare stem counts across sources. A change in detected trees between two inventories is only meaningful if both came from the same CHM source at the same cell size.

  • Too many or too few trees. footprint_size and min_height control detection sensitivity: a smaller footprint finds more (and more spurious) treetops; a higher min_height drops understory. Tune them to your stand, or switch to vwf for height-varying crowns. See How tree detection from a CHM works for the trade-offs.
  • Detecting structures as trees. This is a property of surface models in general, not of any one source: a CHM records what is physically above the ground, not what is vegetation. Buildings, powerlines, and towers all appear as tall cells, and detection will place treetops on them. Lidar is if anything more exposed to it than imagery — a conductor spanning a valley holds its elevation while the ground falls away beneath it, producing a line of very tall cells, and it sits in ASPRS class 1 whenever the vendor did not classify wires. Over developed or wildland-urban-interface land, mask it (for example, with building footprints) or restrict the domain to vegetated areas.
  • Detecting on a CHM whose ground was inferred rather than measured. A canopy height is a difference — a canopy return’s elevation minus the ground beneath it — so a CHM is only as good as the ground surface under it. A point-cloud CHM reports how that surface was established in source.ground: ground_source (classification or derived), ground_coverage, and max_ground_distance_m. Read them before detecting. Nothing downstream re-checks: detection will place stems on interpolated heights without complaint, and the resulting inventory looks exactly like a well-grounded one. See Read source.ground.
  • Comparing inventories built from different CHM sources. The counts are not comparable — see The source changes the answer. Neither is footprint_size across different cell sizes, since it is expressed in pixels.
  • 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/chm reads the grid’s data; poll step 1 to completed first.