Skip to content

Check whether a point cloud is fit to build on

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

A completed point cloud is not automatically a usable point cloud. Four fields on GET /domains/{domain_id}/pointclouds/{point_cloud_id} decide whether it is worth building on, and reading them takes one request.

Every downstream disappointment in this pipeline is diagnosable here — before you spend a grid job, a detection run, and an afternoon on it.

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

  2. A completed point cloud: your-point-cloud-id in your-domain-id.

GET the point cloud
curl -X 'GET' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/pointclouds/your-point-cloud-id' \
-H 'accept: application/json' \
-H 'api-key: my-api-key'

All four checks in one script:

The four checks, in order
"""Decide whether a completed point cloud is worth building on."""
import fastfuels_sdk.v2 as ff
ff.set_api_key("my-api-key")
domain = ff.Domain.from_id("your-domain-id")
point_cloud = ff.get_point_cloud("your-domain-id", "your-point-cloud-id")
if point_cloud.status != "completed":
raise SystemExit(f"{point_cloud.status}: {point_cloud.error}")
summary = point_cloud.summary
# 1. Does the cloud actually cover the domain? coverage_fraction is computed
# from catalog boundaries; these bounds come from the points themselves.
min_x, min_y, _, max_x, max_y, _ = point_cloud.georeference.bounds
dxmin, dymin, dxmax, dymax = domain.bbox
cloud_area = (max_x - min_x) * (max_y - min_y)
domain_area = (dxmax - dxmin) * (dymax - dymin)
print(f"Cloud extent covers ~{cloud_area / domain_area:.0%} of the domain bbox")
# 2. Did it return roughly what the pre-flight estimated?
print(f"Points: {summary.point_count:,}")
# 3. Is it dense enough for the cell size you intend to rasterize at?
print(f"Density: {summary.density:.1f} pts/m2")
# 4. Will a derived CHM get measured ground, or inferred ground?
has_ground = 2 in summary.point_classes
print(f"Classes: {summary.point_classes}")
print(f"Ground class present: {has_ground} -> CHM ground will be "
f"{'classification' if has_ground else 'derived'}")

Compare georeference.bounds[min_x, min_y, min_z, max_x, max_y, max_z] in the domain’s CRS — against the domain’s own bbox.

This is the check that coverage_fraction cannot do for you. Coverage is computed from the catalog’s published boundary polygons; bounds comes from the points themselves. A cloud that reports coverage_fraction: 1.0 and arrives with a gap is a documented failure mode (API #481).

2. Did you get roughly what was estimated?

Section titled “2. Did you get roughly what was estimated?”

Compare summary.point_count against the estimated_point_count from the coverage pre-flight:

{
"available": true,
"coverage_fraction": 1.0,
"datasets": [
{
"name": "MT_Statewide_P3_4_B21",
"url": "https://s3-us-west-2.amazonaws.com/usgs-lidar-public/MT_Statewide_P3_4_B21/ept.json",
"contribution_fraction": 1.0,
"estimated_density": 14.811981421134373,
"estimated_points": 7847291
}
],
"estimated_point_count": 7847291
}
Two bar charts comparing the pre-flight estimate with the delivered cloud. Points: 7,835,003 estimated versus 10,496,309 delivered. Density: 14.8 estimated versus 19.8 points per square metre delivered.

A third more than estimated, which is normal — the catalog averages density over an acquisition’s full published extent, including parts holding no points. Treat a large shortfall as the signal, not a modest surplus.

3. Is it dense enough for the cell size you want?

Section titled “3. Is it dense enough for the cell size you want?”

summary.density is points per square metre. Multiply by your intended cell area to get the returns per cell:

Densityat 1 m cellsat 0.5 m cells
20 pts/m²~20 per cell~5 per cell
8 pts/m²~8 per cell~2 per cell
2 pts/m²~2 per cell~0.5 per cell — mostly empty

A cell receiving one or two returns is described by luck rather than by measurement. Note that a point-cloud CHM cannot go below 1 m anyway.

4. Will a derived CHM get measured or inferred ground?

Section titled “4. Will a derived CHM get measured or inferred ground?”

summary.point_classes lists the ASPRS classes present. Look for 2 — ground. Its presence decides whether a canopy height model built from this cloud measures the ground beneath the canopy or infers it.

The cloud above reports [1, 2, 7, 9, 18, 20]. Two things to read from that:

  • Class 2 is present, so a derived CHM will report ground_source: "classification".
  • There are no vegetation classes (3, 4, 5) at all. This is normal and not a problem: many acquisitions classify only ground and leave vegetation in class 1. It does not mean the cloud has no vegetation.

Here is what that looks like once the CHM is built — the reported ground statistics beside the cells that actually received a canopy return:

Left: a map of cells with a canopy return, showing a solid green domain with a wide pale band winding through it where the river is. Right: a log-scale histogram of canopy heights peaking near zero and tailing off past 35 metres.

A CHM from this cloud: 93.1% of cells carry a canopy return, and the pale band is the Blackfoot River — water absorbs the pulse, so it returns nothing. Nodata is not automatically a coverage gap; here it is the river doing what rivers do.

When status is failed, error carries a code, a message, and usually a suggestion:

{
"id": "your-point-cloud-id",
"domain_id": "your-domain-id",
"type": "als",
"name": "Scan with no CRS",
"description": "",
"status": "failed",
"progress": {
"percent": 100,
"message": "Failed"
},
"created_on": "2026-08-03T19:51:16.253376Z",
"modified_on": "2026-08-03T19:51:18.051700Z",
"checksum": "2b0bea9318fc4a4dba360bbdf764c0d7",
"source": {
"name": "upload",
"object_name": "pointclouds/your-point-cloud-id/upload"
},
"georeference": null,
"summary": null,
"error": {
"code": "MISSING_CRS",
"message": "The point cloud has no coordinate reference system. Assign a CRS before uploading.",
"suggestion": "Set the CRS in your processing software, e.g. `pdal translate in.laz out.laz --writers.las.a_srs=EPSG:<code>`."
},
"tags": []
}

georeference and summary stay null. A failed cloud still occupies a slot against your total quota until you delete it.

  • Reading summary before status is completed. It is null until then, and so is georeference.
  • Trusting density as evidence of completeness. It is measured only where points exist.
  • Reading a missing class 3/4/5 as missing vegetation. It usually means the vendor did not label it.
  • Skipping this page and discovering the problem after detection. A sparse or badly grounded cloud produces a plausible-looking CHM and an inventory full of trees that are not there.