Tuning PMF for Flat Agricultural Terrain
TL;DR: On flat farmland, set slope low (0.1–0.3), initial_distance small (0.1–0.15 m) and max_distance modest (1–1.5 m) so standing crops and hedges are rejected, choose max_window_size just larger than the biggest farm building (20–30 m at 1 m cells), and keep exponential: true. Check the DTM for crop-height bumps and for field drains that were filled in.
# Context and Motivation
This guide is part of PMF Ground Classification. Flat agricultural land looks like the easiest possible ground classification job, and for the morphological filter it mostly is: there are few steep slopes for the opening to cut off. The difficulty is subtler. Crops half a metre to two metres tall form dense, flat-topped canopies that return few ground points in summer; hedgerows and field margins sit a metre or two above fields; field drains and ditches are narrow and shallow. Default PMF settings, which allow a gentle slope and a generous height tolerance, accept crop tops as ground and flatten away drains — both of which matter for the drainage and flood studies that farmland DTMs are typically used for.
# Prerequisites and Assumptions
- PDAL 2.x with
filters.pmf. - Flat or gently undulating terrain (median slope under about 3°).
- Noise removed, and ideally a leaf-off or post-harvest collection; summer data over tall crops limits any filter.
- A few reference profiles or checkpoints across drains and field boundaries.
# Step-by-Step Implementation
# Step 1 — Lower the slope parameter
PMF’s slope scales how much the elevation threshold grows with window size. On flat land 0.1–0.3 prevents the threshold from growing large enough to accept crops.
# Step 2 — Tighten the distances
initial_distance is the threshold at the smallest window; max_distance caps it. 0.1–0.15 m and 1–1.5 m reject crops and hedges while tolerating ploughed soil roughness.
# Step 3 — Size the maximum window
max_window_size must exceed the largest object to remove. Farm buildings and machinery sheds are typically 15–30 m across; at 1 m cells, a 31-cell window is usually enough. Larger windows add time and flatten broad but real features.
# Step 4 — Keep exponential growth
exponential: true grows windows geometrically (1, 3, 7, 15, 31…), reaching the maximum quickly with few iterations. Linear growth offers finer control but is slower.
# Step 5 — Check drains and crops
Profile across ditches and field boundaries, and compare the DTM over cropped fields with a post-harvest collection if one exists.
# Complete Working Example
{
"pipeline": [
"tiles/fens_2206.laz",
{ "type": "filters.range", "limits": "Classification![7:7],Classification![18:18]" },
{ "type": "filters.assign", "value": ["Classification = 1 WHERE Classification == 2"] },
{ "type": "filters.pmf", "cell_size": 1.0, "slope": 0.2, "initial_distance": 0.12,
"max_distance": 1.2, "max_window_size": 31, "exponential": true },
{ "type": "writers.las", "filename": "out/fens_2206_pmf.laz", "minor_version": 4,
"dataformat_id": 6, "forward": "all", "tag": "classified" },
{ "type": "filters.range", "inputs": ["classified"], "limits": "Classification[2:2]" },
{ "type": "writers.gdal", "filename": "out/fens_2206_dtm.tif", "resolution": 1.0,
"output_type": "idw", "window_size": 4, "data_type": "float32" }
]
}A quick comparison of default and tuned settings on the same tile, measuring how high “ground” sits above the lowest returns in each cell — a crop-residue indicator:
"""Crop-residue indicator: ground points well above the cell minimum."""
import json
import numpy as np
import pdal
SETTINGS = {
"default": {"slope": 1.0, "initial_distance": 0.15, "max_distance": 2.5, "max_window_size": 33},
"tuned": {"slope": 0.2, "initial_distance": 0.12, "max_distance": 1.2, "max_window_size": 31},
}
for name, s in SETTINGS.items():
p = pdal.Pipeline(json.dumps({"pipeline": [
"tiles/fens_2206.laz",
{"type": "filters.range", "limits": "Classification![7:7],Classification![18:18]"},
{"type": "filters.assign", "value": ["Classification = 1"]},
{"type": "filters.pmf", "cell_size": 1.0, "exponential": True, **s},
]}))
p.execute()
a = p.arrays[0]
cell = (a["X"] // 2).astype(np.int64) * 1_000_000 + (a["Y"] // 2).astype(np.int64)
order = np.argsort(cell)
c, z = cell[order], a["Z"][order]
starts = np.r_[0, np.nonzero(np.diff(c))[0] + 1]
cell_min = np.repeat(np.minimum.reduceat(z, starts), np.diff(np.r_[starts, len(z)]))
g = a["Classification"][order] == 2
high = (z - cell_min)[g]
print(f"{name:>8}: ground {g.mean():.1%}, ground >0.4 m above cell min: {(high > 0.4).mean():.2%}")# Key Parameter Table
| Option | Default | Flat farmland | Effect |
|---|---|---|---|
slope |
1.0 | 0.1–0.3 | How fast the threshold grows with window size |
initial_distance |
0.15 m | 0.1–0.15 m | Tolerance at the smallest window |
max_distance |
2.5 m | 1.0–1.5 m | Cap on the tolerance; below crop and hedge heights |
max_window_size |
33 | 25–31 | Just above the largest building, in cells |
cell_size |
1.0 m | 1.0 m | Grid for the morphological surface |
exponential |
true | true | Window growth pattern |
# Verification
- Crop-residue indicator as in the script: a low share of ground points far above the local minimum.
- Ditch profiles. Depths across field drains should match the reference within a decimetre.
- Seasonal comparison. If a leaf-off or post-harvest flight exists, difference the two DTMs over fields; residual crop shows as positive differences confined to field boundaries.
# Gotchas and Edge Cases
Tall summer crops. Maize or oilseed rape at two metres or more with dense canopy may leave almost no ground returns. No setting recovers ground that was never measured; flag those fields and interpolate from margins.
Levees and embankments. Flood banks are narrow, raised and real. A small max_distance can reject their crests as non-ground. Check known embankments explicitly, and relax settings locally or use breaklines.
Glasshouses and polytunnels. Large, flat structures just above ground look like terrain to morphology. Their footprint needs the maximum window to exceed it; add a building mask if they are common.
Default slope of 1.0. PMF’s default slope was chosen for varied terrain; on flat land it lets the threshold reach max_distance quickly, which is why crops pass. Lowering slope is the most effective single change.
# Frequently Asked Questions
What PMF settings suit flat agricultural land?
A low slope around 0.1 to 0.3, a small initial distance around 0.12 metres, a maximum distance around 1 to 1.5 metres and a maximum window just larger than the largest farm building, with exponential window growth.
Why does PMF classify crops as ground?
With the default slope and maximum distance, the elevation threshold grows large enough to accept flat-topped crop canopies a metre or more above the soil. Lowering slope and max_distance keeps the threshold below crop height.
How do I preserve drainage ditches?
Keep max_window_size no larger than needed and max_distance small, and verify with profiles across known ditches. Narrow ditches can still be interpolated over if few ground returns fall inside them; breaklines help where they are critical.
Is PMF or SMRF better for farmland?
Both work well on flat land when tuned. PMF’s parameters are simple and map directly onto crop and building heights; SMRF offers more control on mixed terrain. Compare them on a reference tile if the project matters.
# Related
- PMF Ground Classification — the filter in general
- Tuning PMF Window and Slope — parameter behaviour in depth
- Classifying Ground with Progressive Morphological Filter — a complete PMF run
- SMRF vs PMF for Dense Urban LiDAR — the urban comparison
- Hydro-Flattening Water Bodies in a DTM — drainage-ready terrain models