CSF vs SMRF vs PMF: Choosing a Ground Filter for Dense Forest Canopy
Under a closed forest canopy the lowest laser return in a grid cell is often a shrub, a fallen log, or coarse woody debris rather than true bare earth, so the ground filter you pick decides whether your terrain surface is honest or systematically inflated. This guide compares the three ground-classification algorithms that ship with PDAL — filters.csf, filters.smrf, and filters.pmf — as one focused decision inside LiDAR Point Cloud Preprocessing, which itself sits within the wider Canopy Height Modeling & Terrain Extraction workflow. Once you have a clean Class 2 ground surface, the same classified cloud feeds both Digital Terrain Model Generation and the height-above-ground step in Normalizing LiDAR Point Clouds with PDAL. Getting this choice right up front is the single highest-leverage decision in the preprocessing chain.
All three filters solve the same problem — separate ground returns from vegetation — but they use fundamentally different physics. CSF (Cloth Simulation Filter) drapes a simulated elastic cloth over the inverted cloud. PMF (Progressive Morphological Filter) opens the surface with a monotonically growing window. SMRF (Simple Morphological Filter) is a refinement of PMF that adds an explicit, slope-aware elevation tolerance. Their differences only matter at the margins that define forestry work: steep slopes, dense multi-layered canopy, and sparse ground returns.
When to use each filter
The choice is driven by terrain gradient, canopy density, and ground-return density. The table below summarizes the behavior that matters for dense-canopy work.
| Criterion | filters.csf |
filters.smrf |
filters.pmf |
|---|---|---|---|
| Underlying model | Draped cloth simulation | Slope-bounded morphology | Progressive morphological opening |
| Key parameters | resolution, rigidness, threshold, step |
slope, window, scalar, threshold |
cell_size, max_window_size, slope, initial_distance, max_distance |
| Steep terrain (>30°) | Good if rigidness lowered to 1; can sag on cliffs |
Strong — explicit slope term tracks abrupt breaks | Weakest — fixed slope term over-clips terrace edges |
| Dense / multi-layer canopy | Strong — physics penetrates understory, preserves breaklines | Good with tuned scalar |
Fair — large windows bridge gaps and gouge ground |
| Low point density (<1 pt/m²) | Fair — coarsen resolution to interpolate across gaps |
Good — window growth spans gaps predictably | Fair — sensitive to initial_distance |
| Speed | Slower (iterative simulation) | Fast | Fast |
| Parameter intuition | Least intuitive (physics) | Moderate (slope + window) | Moderate but many coupled params |
| PDAL stage name | filters.csf |
filters.smrf |
filters.pmf |
The short version: reach for filters.csf first on forested, topographically complex sites; it penetrates dense understory while preserving breaklines. Switch to filters.smrf when the terrain has abrupt breaks — karst, terraces, road cuts — where its explicit slope tolerance beats a cloth that tends to bridge narrow gullies. Treat filters.pmf as the legacy baseline: it is well understood and fast, but its fixed morphological window makes it the most likely of the three to either bridge canopy gaps or over-clip real terrace edges in complex forest terrain.
Recommendation for dense canopy
For temperate and tropical closed-canopy forest over steep terrain — the hardest and most common case in ecological LiDAR — start with filters.csf, rigidness=2, resolution=0.5, and threshold=0.3. The cloth model settles through gaps in the overstory to reach genuine ground and, unlike morphology-based filters, it does not require you to guess a maximum window size that matches your largest canopy opening. Lower rigidness toward 1 on slopes above 30° so the cloth can follow the gradient rather than sagging across it. Reserve SMRF for sites dominated by sharp breaklines, and validate whichever you pick against RTK control points as described under Digital Terrain Model Generation.
Minimal reproducible examples
Each filter is a single PDAL stage. The pipelines below share the same structure — read, strip outliers, classify ground, write — so you can swap the classifier stage and compare Class 2 counts on an identical input. Statistical outlier removal must run before any ground filter so a high-noise return is never anchored as ground.
CSF for dense canopy on steep terrain:
{
"pipeline": [
"input_tile.laz",
{
"type": "filters.outlier",
"method": "statistical",
"mean_k": 12,
"multiplier": 2.2
},
{
"type": "filters.csf",
"resolution": 0.5,
"rigidness": 2,
"threshold": 0.3,
"step": 0.65,
"iterations": 500,
"classify": true
},
{
"type": "writers.las",
"filename": "ground_csf.laz",
"forward": "all"
}
]
}
SMRF for abrupt breaklines and terraces:
{
"pipeline": [
"input_tile.laz",
{
"type": "filters.outlier",
"method": "statistical",
"mean_k": 12,
"multiplier": 2.2
},
{
"type": "filters.smrf",
"slope": 0.2,
"window": 16.0,
"scalar": 1.2,
"threshold": 0.45
},
{
"type": "writers.las",
"filename": "ground_smrf.laz",
"forward": "all"
}
]
}
PMF as the legacy morphological baseline:
{
"pipeline": [
"input_tile.laz",
{
"type": "filters.outlier",
"method": "statistical",
"mean_k": 12,
"multiplier": 2.2
},
{
"type": "filters.pmf",
"cell_size": 1.0,
"max_window_size": 24,
"slope": 1.0,
"initial_distance": 0.5,
"max_distance": 3.0
},
{
"type": "writers.las",
"filename": "ground_pmf.laz",
"forward": "all"
}
]
}
Run any of them from Python and count the ground returns each produces, so the comparison is quantitative rather than visual:
import json
import pdal
def count_ground(pipeline_json: str) -> int:
"""Execute a PDAL pipeline and return the number of Class 2 ground points."""
p = pdal.Pipeline(pipeline_json)
p.execute()
arr = p.arrays[0]
return int((arr["Classification"] == 2).sum())
for name in ("ground_csf", "ground_smrf", "ground_pmf"):
with open(f"{name}.json") as fh:
n = count_ground(fh.read())
print(f"{name}: {n:,} ground returns")
Parameter reference
The parameters that most affect dense-canopy accuracy, with forested starting values and the ecological reason each matters.
| Parameter | Filter | Type | Default | Recommended range | Ecological rationale |
|---|---|---|---|---|---|
resolution |
filters.csf |
float (m) | 1.0 | 0.5–2.0 | Cloth grid spacing; tighten for fine breaklines, coarsen to interpolate across sparse ground |
rigidness |
filters.csf |
int | 3 | 1–2 | Cloth stiffness; lower to 1 on steep slopes so the cloth follows the gradient instead of sagging |
threshold |
filters.csf |
float (m) | 0.5 | 0.25–0.4 | Max cloth-to-point distance kept as ground; tighten so low shrubs stay out of Class 2 |
step |
filters.csf |
float | 0.65 | 0.5–0.9 | Cloth displacement per iteration; smaller is more stable but slower |
slope |
filters.smrf |
float | 0.15 | 0.15–0.3 | Slope tolerance (rise/run); raise on steep terrain to keep genuine slope returns |
window |
filters.smrf |
float (m) | 18.0 | 12–20 | Max morphological window; size to the widest canopy opening you must span |
scalar |
filters.smrf |
float | 1.25 | 0.5–1.5 | Multiplies the slope-based elevation threshold; lower rejects more aggressively |
threshold |
filters.smrf |
float (m) | 0.5 | 0.3–0.5 | Elevation tolerance floor; lower under dense canopy to reject low vegetation |
max_window_size |
filters.pmf |
int | 33 | 16–33 | Largest opening window; must exceed the largest canopy gap or ground is gouged |
slope |
filters.pmf |
float | 1.0 | 0.5–1.5 | Slope factor scaling the elevation-difference threshold as the window grows |
initial_distance |
filters.pmf |
float (m) | 0.15 | 0.15–0.5 | First-pass elevation tolerance; raise slightly in noisy understory |
max_distance |
filters.pmf |
float (m) | 2.5 | 2.5–3.5 | Ceiling on the elevation-difference threshold at the largest window |
Expected output and verification
Every one of these pipelines writes a LAZ in which terrain points carry ASPRS Classification value 2. A correct dense-canopy run classifies a plausible fraction of returns as ground — typically 5–25% depending on canopy closure — and, once you rasterize and normalize, produces near-zero height for bare-ground returns. The fastest cross-check is to grid each classifier’s ground returns to a DTM and compare RMSE against survey control, but a first-pass sanity check compares ground counts and looks for the failure signatures each filter is prone to:
import numpy as np
import laspy
def ground_fraction(laz_path: str) -> float:
"""Fraction of returns classified as ASPRS Class 2 ground."""
with laspy.open(laz_path) as fh:
las = fh.read()
cls = np.asarray(las.classification)
frac = (cls == 2).sum() / len(cls)
return float(frac)
for path in ("ground_csf.laz", "ground_smrf.laz", "ground_pmf.laz"):
frac = ground_fraction(path)
print(f"{path}: {frac:.1%} ground")
assert 0.02 < frac < 0.6, "implausible ground fraction — retune the classifier"
A ground fraction near zero means the filter rejected true terrain (window too small in PMF, or CSF threshold too tight). A fraction above ~50% under closed canopy means low vegetation leaked into the ground class (CSF rigidness too high, or SMRF scalar too loose). Grid each result to a bare-earth surface and validate against RTK points — the filter with the lowest RMSE and a mean error nearest zero wins for that stand type.
Common pitfalls
- Using PMF defaults under closed canopy. The default
max_window_sizeof 33 with a largeslopebridges wide canopy gaps and gouges genuine ground beneath them. Sizemax_window_sizeto your largest real opening and validate the bias. - Leaving CSF
rigidnessat 3 on steep slopes. A rigid cloth spans gullies and misclassifies the gully floor as vegetation. Lowerrigidnessto 1 andthresholdto 0.3 together on gradients above 30°. - Skipping outlier removal. All three filters can anchor on a single high-noise or low-noise return. Always place
filters.outlier(statistical) ahead of the classifier so noise is never a candidate ground point. - Comparing filters on different tiles. Ground fraction and RMSE only compare meaningfully on the same input with outlier removal held constant. Swap only the classifier stage between runs.
Frequently Asked Questions
Is SMRF just a faster version of PMF?
They share a morphological lineage, but SMRF is a distinct algorithm: it adds an explicit, slope-dependent elevation threshold and a net-cut infilling step that makes it markedly more robust on abrupt breaklines than classic PMF. In practice SMRF supersedes PMF for most forestry work, and PMF is kept mainly for reproducing older products or comparison baselines.
Why does CSF beat morphological filters under dense canopy?
CSF models ground as a physical cloth that settles from above, so it does not need a maximum-window parameter tuned to your largest canopy gap. Morphological filters must span the widest opening with their window, and a window large enough to bridge a big gap will also flatten real micro-relief. The cloth simply drapes to the lowest coherent surface, which is why it preserves breaklines in complex forest terrain.
Which filter should I use if I only have one shot at a regional survey?
Standardize on filters.csf with forest-tuned parameters (rigidness=2, resolution=0.5, threshold=0.3) and validate a representative sample of tiles against control points. A single consistent CSF configuration avoids the step changes at tile boundaries that appear when classification settings drift between batches, which matters most for multi-temporal change detection.
Related
- LiDAR Point Cloud Preprocessing — the parent workflow this ground-filter choice belongs to
- Normalizing LiDAR Point Clouds with PDAL — subtract the classified ground surface to get height-above-ground
- Digital Terrain Model Generation — rasterize and validate the ground returns these filters produce
- Canopy Height Modeling & Terrain Extraction — the full point-cloud-to-canopy pipeline
Up: LiDAR Point Cloud Preprocessing · Canopy Height Modeling & Terrain Extraction