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 — here from Meta’s global 1 m canopy height model (canopy/meta).
  2. Isolate stems — run a tree-detection algorithm over the CHM to place one tree at each detected treetop (tree/chm).
  1. An API key: my-api-key.

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

The whole flow in one script:

Detect trees from a CHM (both steps + polling)
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 Meta's global 1 m CHM.
chm = requests.post(
f"{BASE}/domains/{DOMAIN_ID}/grids/canopy/meta",
headers=HEADERS,
json={"name": "Meta CHM", "version": "2"},
).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> completed

Meta’s CHM is global at 1 m, so this grid is fine-grained. version selects the Meta CHM release (1 or 2).

POST grids/canopy/meta
curl -X 'POST' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/grids/canopy/meta' \
-H 'accept: application/json' \
-H 'api-key: my-api-key' \
-H 'Content-Type: application/json' \
-d '{
"name": "Meta CHM",
"version": "2"
}'

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": {
"name": "canopy",
"attribution": {
"citation": "Brandt et al. CHMv2: Improvements in Global Canopy Height Mapping using DINOv3. arXiv:2603.06382. Data accessed on 2026-05-25 from https://registry.opendata.aws/dataforgood-fb-forests. ",
"license_name": "DINOv3",
"access_url": "https://registry.opendata.aws/dataforgood-fb-forests",
"license_url": "https://github.com/facebookresearch/dinov3/blob/main/LICENSE.md",
"accessed_on": "2026-05-25"
},
"description": "Meta global canopy height model at ~1m resolution",
"alignment": {
"target": "domain",
"method": null,
"resolution": null
},
"extent_buffer_cells": 0,
"tile_metadata": {
"tile_source": null,
"acquisition_dates": null,
"tile_count": 1,
"native_crs": "EPSG:32611",
"tiles": [
"s3://dataforgood-fb-data/forests/v2/global/dinov3_global_chm_v2_ml3/chm/0212313011.tif"
]
},
"version": "2",
"product": "meta"
},
"bands": ["chm"],
"georeference": {
"crs": "EPSG:32611",
"shape": [1079, 1598]
}
}

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.
POST inventories/tree/chm
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-cell footprint and a 2 m minimum detects ≈31,800 stems. The inventory’s columns are x, y, and height — position and treetop height, the observables a CHM provides.

  • 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.
  • Expecting species or DBH. A CHM doesn’t observe them — the inventory has height only. If a downstream step needs DBH, derive it allometrically from height, or start from TreeMap instead.
  • Creating the inventory before the CHM grid is completed. tree/chm reads the grid’s data; poll step 1 to completed first.