Choosing SMRF Parameters for Ground Classification

filters.smrf has a handful of parameters and a reputation for needing trial and error. Most of that reputation comes from tuning them against how the output looks rather than deriving them from the site, because three of the four are physical measurements of the terrain and only one is a genuine trade-off.

This page derives each from the site, names the failure each wrong value produces, and gives a check that separates over-filtering from under-filtering.

What each parameter controls

window is the maximum size of the morphological opening, in the cloud’s horizontal units. It must exceed the largest non-ground object on the site: anything wider than the window is treated as terrain, because the opening cannot see across it to establish that the ground is lower.

slope is the maximum terrain gradient the filter will accept as ground, as a rise over run. It must exceed the steepest genuine slope on the site; set below it, real ground is rejected and the DTM is trenched.

threshold is the vertical distance above the provisional ground surface at which a point becomes non-ground. It should be a few times the cloud’s own vertical noise and well below the height of the shortest object to remove.

scalar scales the threshold with the local slope, allowing more tolerance where the surface is steep. On flat sites it does nothing; on steep ones it prevents the threshold from rejecting ground that is simply going uphill.

Each SMRF parameter against the site measurement that sets it A terrain cross-section with a building, a hedge and a steep bank, annotated with the four SMRF parameters and the physical quantity each derives from. The window parameter spans the building's width, labelled as needing to exceed the widest non-ground object. The slope parameter is drawn against the steepest natural bank on the site, labelled as needing to exceed it. The threshold parameter is a small vertical distance above the ground surface, labelled as a few times the cloud's vertical noise and below the shortest object to remove. The scalar parameter is shown widening the threshold on the steep bank. bare ground plateau building window > widest object slope > steepest real gradient hedge threshold Three of the four are measurements of this site; only scalar is a judgement.

Figure 1 — Each parameter has a physical referent on the ground. Measuring those four things is faster than trying values, and it produces a setting that transfers to the next survey of the same site.

Minimal reproducible solution

Derive the parameters from the cloud and from a description of the site, rather than from defaults.

import json
import subprocess

import numpy as np


def derive_smrf(cloud_path: str, widest_object_m: float,
                steepest_slope_pct: float, cell_m: float | None = None) -> dict:
    """SMRF parameters from site measurements and the cloud's own statistics."""
    info = json.loads(subprocess.run(
        ["pdal", "info", "--stats", cloud_path],
        capture_output=True, text=True, check=True).stdout)
    stats = {s["name"]: s for s in info["stats"]["statistic"]}

    count = int(info["stats"]["statistic"][0]["count"])
    bounds = info.get("summary", {}).get("bounds", {})
    area = max((bounds.get("maxx", 1) - bounds.get("minx", 0)) *
               (bounds.get("maxy", 1) - bounds.get("miny", 0)), 1.0)
    spacing = float(np.sqrt(area / max(count, 1)))

    # Vertical noise: a robust spread over a locally flat subsample would be
    # better, but the z standard deviation bounds it usefully for a threshold.
    z_std = float(stats.get("Z", {}).get("stddev", 0.05))

    return {
        "type": "filters.smrf",
        "cell": round(cell_m or max(spacing * 1.5, 0.5), 2),
        "window": round(widest_object_m * 1.2, 1),        # exceed the widest object
        "slope": round(steepest_slope_pct / 100.0 * 1.25, 3),
        "threshold": round(max(3 * z_std, 0.15), 2),
        "scalar": 1.25,
    }

The multipliers are margins rather than tuning. window at 1.2 times the widest object accounts for the fact that a building measured on a map is smaller than the point cluster on its roof. slope at 1.25 times the steepest gradient allows for the noise in the gradient estimate itself. threshold at three times the vertical noise makes a false rejection of ground a three-sigma event rather than a routine one.

Two of the three inputs are properties of the site rather than of the data, which is the point: they are stated once per project and reused across every flight of it.

Edge-case matrix

Wrong value Symptom in the DTM Correct derivation
window too small Large buildings become terrain 1.2 × the widest object
window too large Slow; real hills flattened Do not exceed the site’s landform scale
slope too small Trenches down steep banks 1.25 × the steepest real gradient
slope too large Low vegetation retained on slopes Measure the gradient, do not guess
threshold too small Ground points rejected as noise 3 × the cloud’s vertical noise
threshold too large Kerbs and low walls retained Below the shortest object to remove
cell too small Noisy provisional surface ≈ 1.5 × the mean point spacing
scalar = 0 Ground rejected on every slope 1.0–1.5 on terrain with relief

The two window rows are a genuine tension rather than a right answer. A window large enough to remove a warehouse is also large enough to flatten a small hill, because the filter cannot distinguish a wide flat-topped object from a landform. On sites with both, classifying in two passes — a large window to remove the buildings, a smaller one to recover the terrain detail — is more effective than a compromise value.

Verification snippet

The check that matters is against surveyed ground points, split by surface, because the two failure directions have opposite signs.

import numpy as np


def assert_classification_balanced(dtm_at_checks: np.ndarray,
                                   surveyed_z: np.ndarray,
                                   on_vegetation: np.ndarray,
                                   tol_m: float = 0.10) -> None:
    """Over- and under-filtering produce opposite biases; test for both.

    on_vegetation is a boolean array marking check points under or beside
    vegetation, which is where retained non-ground shows up.
    """
    dz = dtm_at_checks - surveyed_z

    open_bias = float(np.median(dz[~on_vegetation]))
    assert abs(open_bias) <= tol_m, (
        f"bias on open ground is {open_bias:+.3f} m — with no vegetation to "
        "retain, this is a datum or rasterisation problem, not classification")

    veg_bias = float(np.median(dz[on_vegetation]))
    if veg_bias > tol_m:
        raise AssertionError(
            f"surface sits {veg_bias:+.3f} m above ground under vegetation — "
            "under-filtered: lower the threshold or raise the window")
    if veg_bias < -tol_m:
        raise AssertionError(
            f"surface sits {veg_bias:+.3f} m below ground under vegetation — "
            "over-filtered: real ground was rejected and interpolated across")

Separating the two groups is what makes the direction readable. A single combined bias averages an over-filtered trench against an under-filtered canopy and can report zero while both faults are present at full strength.

Over- and under-filtering have opposite signatures Three terrain cross-sections with a hedge on a gentle slope. In the under-filtered case the classified ground surface rides over the base of the hedge, sitting above the true ground by a fraction of the vegetation height. In the correctly filtered case the surface follows the true ground beneath the hedge. In the over-filtered case the surface dips below the true ground on the slope beside the hedge, because real ground points on the gradient were rejected and the void was interpolated across. A note gives the sign of the check-point bias in each case: positive, near zero, and negative. under-filtered surface rides over the hedge base bias: positive correct surface follows the true ground bias: near zero over-filtered real ground rejected, interpolated across bias: negative A single combined bias averages the two and can report zero while both are present. Which is why the check points must be split by surface before anything is computed.

Figure 2 — The two failure directions cancel in a combined statistic. Splitting the check points by surface is what makes the sign — and therefore the fix — readable.

Recording the parameters as site properties

Because three of the four parameters describe the site rather than the flight, they belong in a per-site configuration rather than in a per-run one. A quarry’s widest structure and steepest bank do not change between Tuesday’s flight and next month’s, so the values derived once are the values every subsequent survey of that site should use.

That has a practical consequence worth stating: a change in the classification parameters between two surveys of the same site makes those surveys incomparable. A volume computed between two epochs differences two surfaces, and if one was classified with a 40 m window and the other with 60 m, part of the reported change is a change in the filter rather than in the ground. Pinning the parameters per site — and recording them in the run manifest so the pinning is visible — is what makes repeat surveys of a working quarry mean anything.

The one parameter that legitimately varies per flight is cell, because it follows from the point density, which follows from the flight altitude and the overlap. Deriving it from the cloud each time is correct; deriving the other three each time is how two comparable surveys become two incomparable ones.

When to escalate

  • No parameter set satisfies both surfaces. The site has objects at two very different scales, so a single window cannot serve both. Classify in two passes and merge, rather than compromising.
  • The DTM is correct on open ground and wrong under dense canopy regardless of settings. The cloud has no ground points there. This is the photogrammetric limit described in removing vegetation from DTM ground surfaces, and no filter recovers ground the camera never saw.
  • Classification is stable and the surface still has voids. Voids come from the rasterisation rather than the classification when the cell size is finer than the ground-point spacing; see fixing holes and voids in drone DSMs.

Generating DSM and DTM from Point Clouds with PDAL

Two-pass classification for a site with objects at two scales A site containing both a wide warehouse and a low kerb, which cannot be served by one window size. A first pass with a large window removes the warehouse but also flattens a small hill. A second pass with a small window run on the points the first pass kept recovers the hill and removes the kerb. The final classification takes the ground points that both passes agree on, plus the terrain the second pass recovered, producing a surface with neither the warehouse nor the flattened hill. A note gives the cost as one additional filter stage. pass 1 — window 60 m warehouse removed small hill flattened pass 2 — window 6 m kerb removed hill recovered merge ground where both agree, plus terrain pass 2 recovered A compromise window serves neither object; two passes serve both. The cost is one extra filter stage, which on a survey-sized cloud is a few minutes. Record both windows in the run manifest — the pair is the parameter, not either value alone.

Figure 3 — The escape from the window tension. Nothing about SMRF requires a single pass, and a site with a warehouse and a kerb is a two-pass site.