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 Meta’s global 1 m canopy height model
(
canopy/meta). - Isolate stems — run a tree-detection algorithm over the CHM to place
one tree at each detected treetop (
tree/chm).
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 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> completedStep 1 — Build the CHM grid
Section titled “Step 1 — Build the CHM grid”Meta’s CHM is global at 1 m, so this grid is fine-grained. version selects
the Meta CHM release (1 or 2).
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).
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.
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-05-25T18:59:46.647758", "modified_on": "2026-05-25T18:59:46.647758", "source": { "name": "chm", "source_chm_grid_id": "your-chm-grid-id", "algorithm": { "name": "lmf", "min_height": 2.0, "footprint_size": 3 } }, "modifications": [], "columns": [ { "key": "x", "type": "continuous", "unit": "m" }, { "key": "y", "type": "continuous", "unit": "m" }, { "key": "fia_species_code", "type": "categorical", "unit": null }, { "key": "fia_status_code", "type": "categorical", "unit": null }, { "key": "dbh", "type": "continuous", "unit": "cm" }, { "key": "height", "type": "continuous", "unit": "m" }, { "key": "crown_ratio", "type": "continuous", "unit": null } ], "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-05-25T18:59:46.647758Z", "modified_on": "2026-05-25T19:00:30.100313Z", "source": { "algorithm": { "name": "lmf", "footprint_size": 3, "min_height": 2.0 }, "name": "chm", "source_chm_grid_id": "your-chm-grid-id" }, "modifications": [], "columns": [ { "key": "x", "type": "continuous", "unit": "m" }, { "key": "y", "type": "continuous", "unit": "m" }, { "key": "fia_species_code", "type": "categorical", "unit": null }, { "key": "fia_status_code", "type": "categorical", "unit": null }, { "key": "dbh", "type": "continuous", "unit": "cm" }, { "key": "height", "type": "continuous", "unit": "m" }, { "key": "crown_ratio", "type": "continuous", "unit": null } ], "georeference": { "crs": "EPSG:32611", "bounds": [ 720227.9398802927, 5189763.323999467, 721533.6406826023, 5190645.048516054 ] }, "error": null, "tags": []}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.
Next steps
Section titled “Next steps”- Trim it — remove trees near roads or water.
- Read the stems — 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. - 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/chmreads the grid’s data; poll step 1 tocompletedfirst.