Skip to content

Generate a tree inventory from TreeMap

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

A tree inventory is a list of individual stems — location, species, DBH, height, crown ratio — and it’s the basis for 3D canopy fuels. FastFuels can generate one for any CONUS domain from the USGS/USFS TreeMap product in two steps:

  1. Build a PIM grid — a per-cell summary of species composition and stem density from TreeMap (pim/treemap).
  2. Realize individual trees — a point process places individual stems consistent with that grid (tree/pim).

If you also have a canopy height model (CHM) of the domain, a third, optional step places those stems only where the CHM shows canopy (tree/pim/fusion/chm) — see Condition the trees on a CHM.

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

  2. A domain within CONUS: your-domain-id. See Create a domain. TreeMap covers forested lands across the conterminous US.

POST grids/pim/treemap
curl -X 'POST' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/grids/pim/treemap' \
-H 'accept: application/json' \
-H 'api-key: my-api-key' \
-H 'Content-Type: application/json' \
-d '{
"name": "TreeMap PIM grid"
}'

Poll it to completed (TreeMap is coarse — the grid is small and fast), then record its id: your-pim-grid-id.

{
"id": "your-pim-grid-id",
"domain_id": "your-domain-id",
"status": "completed",
"source": {
"name": "pim",
"bands": ["tm_id"],
"description": "TreeMap plot imputation raster (FIA plot IDs at 30m)",
"alignment": {
"target": "domain",
"method": null,
"resolution": null
},
"version": "2022",
"product": "treemap",
"extent_buffer_cells": 0
},
"georeference": {
"crs": "EPSG:32611",
"shape": [30, 45]
}
}

Point source_pim_grid_id at the completed PIM grid. seed fixes the stochastic point process so the inventory is reproducible — same seed, same trees.

POST inventories/tree/pim
curl -X 'POST' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/inventories/tree/pim' \
-H 'accept: application/json' \
-H 'api-key: my-api-key' \
-H 'Content-Type: application/json' \
-d '{
"name": "TreeMap tree inventory",
"source_pim_grid_id": "your-pim-grid-id",
"seed": 42
}'

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'

The completed inventory’s columns describe each tree: x, y, fia_species_code, fia_status_code, dbh (cm), height (m), and crown_ratio. On the Blue Mountain domain this seed produces ≈44,000 stems.

Step 3 — Condition the trees on a CHM (optional)

Section titled “Step 3 — Condition the trees on a CHM (optional)”

TreeMap assigns a plot to every 30 m cell it can match to an FIA plot and leaves the cells it cannot match empty. Neither decision looks at the canopy on the ground: a matched cell receives its plot’s trees whether or not the cell is forested, and an unmatched cell receives none even where it is. If you have a canopy height model of the domain, tree/pim/fusion/chm expands the same PIM grid but decides cell by cell, from the CHM, whether trees belong there:

  • Where the CHM shows canopy over a cell TreeMap left empty, the nearest plot is imputed and its trees are placed. This fills TreeMap’s gaps.
  • Where the CHM shows no canopy over a cell TreeMap matched, the plot is dropped and no trees are placed. This removes trees from open ground.

The trees keep TreeMap’s full attribute set — species, DBH, height, crown ratio — and are placed only where the CHM shows canopy.

Any completed grid in this domain with a chm band in meters will do. The examples below use the NAIP canopy height model, the same grid Generate a tree inventory from a CHM builds in its first step; a CHM built from a point cloud or uploaded as a GeoTIFF works the same way. Poll it to completed and record its id: your-chm-grid-id.

Point source_pim_grid_id at the PIM grid from Step 1 and source_chm_grid_id at the CHM grid. method names the fusion algorithm and its parameters; the values below are the defaults, so you can omit method entirely to get the same result. seed works as in Step 2.

POST inventories/tree/pim/fusion/chm
curl -X 'POST' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/inventories/tree/pim/fusion/chm' \
-H 'accept: application/json' \
-H 'api-key: my-api-key' \
-H 'Content-Type: application/json' \
-d '{
"name": "TreeMap tree inventory conditioned on CHM",
"source_pim_grid_id": "your-pim-grid-id",
"source_chm_grid_id": "your-chm-grid-id",
"seed": 42,
"method": {
"name": "reimputation",
"resolution": 7.5,
"min_height": 2.0,
"cover_threshold": 0.2
}
}'

The response’s source records both grids and their checksums, fusion: ["chm"], and the method that was applied. Record the inventory id: your-inventory-id, and poll it to completed exactly as in Step 2:

{
"id": "your-inventory-id",
"domain_id": "your-domain-id",
"type": "tree",
"name": "TreeMap tree inventory conditioned on CHM",
"description": "",
"status": "completed",
"progress": {
"percent": 100,
"message": "Complete"
},
"created_on": "2026-09-08T15:22:33.558252Z",
"modified_on": "2026-09-08T15:22:42.331321Z",
"checksum": "f342cdb5bd134174be919556f47cac86",
"source": {
"fusion": ["chm"],
"method": {
"min_height": 2.0,
"name": "reimputation",
"resolution": 7.5,
"cover_threshold": 0.2
},
"seed": 42,
"name": "pim",
"source_chm_grid_checksum": "fcb2e6e7437b451b8d76cc3b9bb1c4aa",
"source_pim_grid_id": "your-pim-grid-id",
"source_chm_grid_id": "your-chm-grid-id",
"point_process": "inhomogeneous_poisson",
"source_pim_grid_checksum": "887f565ae7754a0a8361a94705f61db9"
},
"modifications": [],
"treatments": [],
"columns": [
{
"key": "x",
"type": "continuous",
"unit": "m",
"summary": {
"type": "continuous",
"count": 53282,
"null_count": 0,
"min": 720226.0003150188,
"max": 721530.8123194346,
"mean": 720811.6062926742,
"std": 351.7727544731188
}
},
{
"key": "y",
"type": "continuous",
"unit": "m",
"summary": {
"type": "continuous",
"count": 53282,
"null_count": 0,
"min": 5189762.002975728,
"max": 5190645.978815561,
"mean": 5190211.076738614,
"std": 260.54825423663846
}
},
{
"key": "fia_species_code",
"type": "categorical",
"unit": null,
"summary": {
"type": "categorical",
"count": 53282,
"null_count": 0,
"unique_count": 12
}
},
{
"key": "fia_status_code",
"type": "categorical",
"unit": null,
"summary": {
"type": "categorical",
"count": 53282,
"null_count": 0,
"unique_count": 2
}
},
{
"key": "dbh",
"type": "continuous",
"unit": "cm",
"summary": {
"type": "continuous",
"count": 53282,
"null_count": 0,
"min": 2.54,
"max": 93.218,
"mean": 13.835692053601592,
"std": 10.644656399565706
}
},
{
"key": "height",
"type": "continuous",
"unit": "m",
"summary": {
"type": "continuous",
"count": 53282,
"null_count": 0,
"min": 1.8287999999999998,
"max": 44.50079999999999,
"mean": 8.735635959611125,
"std": 5.466895967382896
}
},
{
"key": "crown_ratio",
"type": "continuous",
"unit": null,
"summary": {
"type": "continuous",
"count": 53282,
"null_count": 0,
"min": 0.0,
"max": 0.95,
"mean": 0.39849893022033706,
"std": 0.24383459451164752
}
}
],
"forestry_metrics": {
"type": "tree",
"tree_count": 53282,
"basal_area_per_area": 48.04181715446447,
"tree_density": 186.48334163528986,
"quadratic_mean_diameter": 6.872678003448064,
"dominant_species_groups": [
{
"spgrpcd": 4,
"name": "Pine",
"basal_area_share": 0.7646448602486037
},
{
"spgrpcd": 2,
"name": "Douglas-fir",
"basal_area_share": 0.15155054891271058
},
{
"spgrpcd": 6,
"name": "Aspen/alder/cottonwood-willow",
"basal_area_share": 0.029818598290393978
},
{
"spgrpcd": 1,
"name": "Cedar/larch",
"basal_area_share": 0.018902794754553313
},
{
"spgrpcd": 3,
"name": "True fir/hemlock",
"basal_area_share": 0.016015520858422342
}
]
},
"georeference": {
"crs": "EPSG:32611",
"bounds": [720226.0, 5189762.0, 721534.0, 5190646.0]
},
"error": null,
"tags": []
}

The columns are the same seven as a tree/pim inventory. On the Blue Mountain domain this seed produces ≈53,300 stems against ≈44,000 from Step 2 — more, not fewer, because TreeMap has no plot for roughly half of this domain’s cells and the CHM shows canopy over much of that area.

Five maps of the same 1.3 by 0.9 kilometre domain in three rows. Top left, NAIP aerial imagery showing dense forest on the west and open grassland with scattered trees on the east. Top right, the NAIP canopy height model in a purple-to-yellow scale, blank where the canopy is under 2 metres. Middle left, the tree/pim inventory as stems per 7.5 metre cell, with large grey blocks where TreeMap has no plot. Middle right, the tree/pim/fusion/chm inventory as stems per cell, now following the canopy pattern of the imagery. Bottom, spanning the width, a categorical map: green cells kept from TreeMap, orange cells filled where TreeMap had no plot but the CHM shows canopy, red cells removed where TreeMap placed trees over ground the CHM shows as open.

Top: NAIP aerial imagery of the domain (USGS National Map) and the NAIP CHM built from it, with canopy below min_height (2 m) left blank. Middle: the tree/pim inventory from Step 2 and the same PIM grid conditioned on the CHM, both as stems per 7.5 m cell; grey is a cell with no trees. Bottom: how each 7.5 m cell changed relative to tree/pim — kept, filled from the CHM over a TreeMap gap, or removed where the CHM shows no canopy.

method has three fields. The decision is made per cell at resolution, and a cell counts as canopy-covered when at least cover_threshold of the CHM cells inside it are taller than min_height:

FieldDefaultWhat it does
resolution7.5 mCell size at which the keep-or-drop decision is made. Must be no finer than the CHM’s cell and no coarser than the PIM’s (0.6 m and 30 m for NAIP and TreeMap).
min_height2.0 mCHM height a cell must reach to count as canopy. Raise it to ignore shrubs and regeneration.
cover_threshold0.2Fraction of a resolution cell that must be canopy for the cell to receive trees. Cells at or below it get none.

cover_threshold sets the balance between the two effects. A low value fills almost every TreeMap gap the CHM shows any canopy over and removes plots only from cells that are nearly bare; a high value fills only well-covered gaps and removes plots from every cell with broken canopy. On the Blue Mountain domain, same seed:

cover_thresholdTrees
0.0556,645
0.2 (default)53,282
0.544,995
0.7537,068
tree/pim, no CHM44,099
Four categorical maps of the same domain, one per cover threshold of 0.05, 0.2, 0.5 and 0.75. Each shows 7.5 metre cells as green where TreeMap's trees were kept, orange where trees were filled in over a TreeMap gap, and red where TreeMap's trees were removed. From left to right and top to bottom the orange area shrinks and the red area grows, especially along the edges of the forest patches.

Each 7.5 m cell’s change relative to tree/pim at four values of cover_threshold. As the threshold rises, fewer TreeMap gaps are filled (orange) and more TreeMap plots are removed (red), starting at the edges of the forest patches and in cells with scattered cover.

Raise the threshold when TreeMap places trees on ground you know is open — recent clearings, burns, meadows the CHM resolves — and lower it when the domain has many TreeMap gaps over real forest. There is no correct value; check the result against the CHM by exporting both and overlaying them, as you would for a detected inventory.

Other ways to get a tree inventory: from a canopy height model (from a CHM) or by uploading your own tree list.

  • Domain outside CONUS. TreeMap is a conterminous-US product.
  • Creating the inventory before the PIM grid is completed. tree/pim reads the PIM grid’s data — poll step 1 to completed first.
  • Expecting identical trees without a seed. Omit seed and each run draws a fresh random realization. Set it when you need reproducibility.
  • Expecting a conditioned inventory to have fewer trees. Fusion fills TreeMap’s gaps as well as pruning its plots, so on a domain where TreeMap left cells unmatched it can produce more trees than tree/pim. Compare the two inventories on the map, not by count; raise cover_threshold if you want pruning to dominate.
  • A resolution outside the grids’ cell sizes. tree/pim/fusion/chm returns 422 if method.resolution is finer than the CHM’s cell or coarser than the PIM’s. The default 7.5 m sits between every CHM source and TreeMap’s 30 m.
  • A CHM grid that isn’t a 2D height surface in meters. The CHM grid must carry a chm band with unit m; a grid in feet, or a 3D grid, is rejected with 422. Uploaded GeoTIFFs are the usual cause — set the band unit when you upload.
  • No canopy above the threshold anywhere. If no cell clears cover_threshold, the inventory fails with EMPTY_AFTER_FUSION and the error message reports the highest cover observed. Lower cover_threshold or min_height, or check that the CHM actually covers vegetated ground.