Skip to content

Tune CHM tree detection

You are viewing in-progress documentation for v2 (Beta). Switch to the stable version for the current production release.

Tree detection has no universally correct settings — the right footprint_size, min_height, or VWF window depends on how your stand is spaced. The way to choose is to look: export the CHM and the detected treetops, overlay them, and check whether each point lands on a distinct crown. This guide runs one round of that loop; repeat it, adjusting parameters, until the points track the crowns you can see.

For what over- and under-detection look like — and which knob fixes each — see How tree detection from a CHM works. For the full request schema and every field default, see the live API reference.

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

  2. A domain (your-domain-id) with a completed CHM grid (your-chm-grid-id) and at least one tree inventory (your-inventory-id) to inspect. See Generate a tree inventory from a CHM.

  3. A local environment with rasterio and matplotlib.

Step 1 — Export the CHM and the treetops

Section titled “Step 1 — Export the CHM and the treetops”

Export the CHM grid as a GeoTIFF and the inventory as GeoJSON. Both are asynchronous: the create call returns an export in status: "pending".

POST grids/{grid_id}/exports/geotiff
curl -X 'POST' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/grids/your-chm-grid-id/exports/geotiff' \
-H 'accept: application/json' \
-H 'api-key: my-api-key' \
-H 'Content-Type: application/json' \
-d '{
"name": "Blue Mountain NAIP CHM",
"bands": ["chm"]
}'

Each returns an export id: your-export-id. Poll /exports/{export_id} until status is completed, then read its signed_url.

GET export status
curl -X 'GET' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/exports/your-export-id' \
-H 'accept: application/json' \
-H 'api-key: my-api-key'
{
"id": "your-export-id",
"domain_id": "your-domain-id",
"name": "Blue Mountain NAIP CHM",
"description": "",
"status": "completed",
"progress": {
"percent": 100,
"message": "Complete"
},
"created_on": "2026-06-05T14:40:33.809172Z",
"modified_on": "2026-06-05T14:40:36.193701Z",
"source": {
"bands": ["chm"],
"name": "geotiff",
"grid_id": "your-chm-grid-id"
},
"signed_url": "https://storage.googleapis.com/silvx-fastfuels-exports-v2/your-export-id/Blue_Mountain_NAIP_CHM.tif?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Expires=604800&X-Goog-SignedHeaders=host&X-Goog-Signature=...",
"expires_on": "2026-06-12T14:40:33.809172Z",
"error": null,
"tags": []
}

The signed_url is a direct download — no API key needed, valid for seven days. Save the two files side by side:

Terminal window
# paste each signed_url from the completed responses above
curl -o chm.tif "<CHM_SIGNED_URL>"
curl -o treetops.geojson "<TREETOPS_SIGNED_URL>"

With chm.tif and treetops.geojson in hand (both in the domain CRS), plot the treetops over a close-up of the CHM:

inspect_detection.py
"""Overlay detected treetops on the CHM to judge detection quality.
Reads the two files you downloaded from the export signed URLs:
chm.tif — the exported CHM grid (geotiff)
treetops.geojson — the exported tree inventory (geojson, same CRS)
Writes overlay.png: detected treetops as dots over a close-up of the CHM.
Run it for each candidate inventory and compare the overlays.
"""
import json
import matplotlib.pyplot as plt
import numpy as np
import rasterio
WINDOW_M = 175.0 # side length of the close-up window, in CHM map units (m)
with rasterio.open("chm.tif") as src:
chm = src.read(1, masked=True)
b = src.bounds
extent = (b.left, b.right, b.bottom, b.top)
# A close-up window — small enough that individual treetops are legible.
x0, y0 = b.left + 175.0, b.bottom + 40.0
x1, y1 = x0 + WINDOW_M, y0 + WINDOW_M
pts = np.array([
f["geometry"]["coordinates"][:2]
for f in json.load(open("treetops.geojson"))["features"]
])
inside = (pts[:, 0] >= x0) & (pts[:, 0] <= x1) & (pts[:, 1] >= y0) & (pts[:, 1] <= y1)
fig, ax = plt.subplots(figsize=(7, 7))
ax.imshow(chm, extent=extent, origin="upper", cmap="Greens",
vmax=np.percentile(chm.compressed(), 99))
ax.scatter(pts[inside, 0], pts[inside, 1], s=14,
facecolor="white", edgecolor="black", linewidth=0.5)
ax.set_xlim(x0, x1)
ax.set_ylim(y0, y1)
ax.set_xticks([])
ax.set_yticks([])
ax.set_title(f"{int(inside.sum())} treetops in a {WINDOW_M:.0f} m window")
fig.savefig("overlay.png", dpi=150, bbox_inches="tight")
print(f"{int(inside.sum())} treetops in view -> overlay.png")
Detected treetops drawn as white dots over a close-up of the canopy height model; the canopy is nearly continuous and some connected crowns carry more than one dot.

A real, untuned result — not an ideal one. The dots track local height peaks, but in this dense canopy the crowns merge into a near-continuous surface, so some connected crowns carry several dots and the boundaries between trees are ambiguous. Weighing trade-offs like these is the point of inspecting the overlay.

Compare the dots to the canopy and re-create the inventory with new parameters:

  • Several dots on one crown (over-detection) → enlarge footprint_size, or switch to vwf.
  • Obvious trees with no dot (under-detection) → shrink footprint_size.
  • Dots on low shrubs or understory → raise min_height.
  • Dots on rooftops or powerlines → NAIP-CHM includes structures; mask them (e.g. with building footprints) or restrict the domain to vegetated land.

Each knob moves the count in a predictable direction. On the Blue Mountain domain (≈1 km², NAIP CHM, min_height 2 m), holding everything else fixed, the whole-domain treetop count responds like this:

lmf footprint_size (px)treetops
39,227
57,836
77,345
96,898

A wider footprint forces peaks farther apart, so neighboring crowns merge and the count falls. vwf instead grows the window with canopy height; its crown_ratio sets how fast (window = crown_offset + crown_ratio × height):

vwf crown_ratio (at crown_offset 1.0)treetops
0.0537,381
0.108,506
0.158,099
0.207,818

A small crown_ratio keeps the window narrow even under tall canopy, so it over-detects sharply — the 0.05 row finds roughly four times as many treetops as 0.10. Raising it widens the window for tall trees and brings the count down. These are one stand’s numbers, not targets: the right setting is the one whose dots track the crowns in your overlay.

Then overlay again. Side-by-side examples of each effect — fixed vs. variable window, and the footprint-size trade-off — are in How tree detection from a CHM works.

These knobs balance detection of the visible canopy. Trees occluded beneath the dominant crowns can’t be recovered by tuning — see Detection resolves the dominant canopy for why a detected count undercounts the true stems.