Removing Noise Classes Before Processing
TL;DR: Put {"type": "filters.range", "limits": "Classification![7:7],Classification![18:18]"} directly after the reader in every processing pipeline, so low noise (class 7) and high noise (class 18) never reach SMRF, HAG, covariance features or rasterization. If the data has no noise classification yet, flag outliers first with filters.outlier or filters.elm, then exclude them the same way. When noise must stay in the delivered file, exclude it with where clauses instead of removing it.
# Context and Motivation
This guide is part of Pipeline Filtering Logic. Noise points are rare — a fraction of a percent of a typical tile — and disproportionately destructive. A single low-noise return 30 m below the terrain, from a multipath reflection or a sensor artefact, becomes the lowest point in its SMRF cell and drags the ground surface down into a pit. A high-noise return from a bird or a cloud edge sets a DSM cell 200 m above the canopy and ruins any hillshade or canopy height model built from it. Neighbourhood features computed near either are distorted too.
ASPRS reserves class 7 for low points (noise) and, in LAS 1.4, class 18 for high noise. Vendors routinely classify both, and removing them first is the single cheapest quality improvement available in almost every pipeline.
# Prerequisites and Assumptions
- PDAL 2.x.
- Data classified to LAS 1.4 conventions: class 7 low noise, class 18 high noise. Older LAS 1.2 deliveries use class 7 for all noise and have no class 18.
- A decision about the delivered file: are noise points removed, or kept and labelled?
# Step-by-Step Implementation
# Step 1 — Check what noise classes exist
pdal info --stats --enumerate Classification tile.laz lists the classes present. If 7 and 18 are absent, the data has not been noise-classified and Step 4 applies.
# Step 2 — Drop noise immediately after the reader
filters.range with negated ranges keeps everything except the listed classes:
{ "type": "filters.range", "limits": "Classification![7:7],Classification![18:18]" }The equivalent expression is Classification != 7 && Classification != 18.
# Step 3 — Keep withheld and overlap handling consistent
Points flagged withheld should usually be excluded in the same stage. Overlap points are a separate decision; see flagging overlap and withheld points.
# Step 4 — Classify noise when the vendor did not
Run filters.outlier (statistical) and filters.elm (extended local minimum, for low points) and let them write class 7, then exclude class 7 as in Step 2.
# Step 5 — Exclude instead of remove when delivering
If the output must contain every point, add where clauses to the processing stages — "where": "Classification != 7 && Classification != 18" — so noise is skipped but written.
# Complete Working Example
A pipeline that classifies noise when missing, excludes it from SMRF and HAG, and still writes every point:
{
"pipeline": [
{ "type": "readers.las", "filename": "tiles/t_0431.laz" },
{ "type": "filters.elm", "cell": 10.0, "threshold": 1.0, "class": 7 },
{ "type": "filters.outlier", "method": "statistical", "mean_k": 12, "multiplier": 3.0,
"class": 18, "where": "Classification != 7" },
{ "type": "filters.smrf", "slope": 0.15, "window": 18, "threshold": 0.5,
"where": "Classification != 7 && Classification != 18" },
{ "type": "filters.hag_nn", "count": 2,
"where": "Classification != 7 && Classification != 18" },
{ "type": "writers.las", "filename": "out/t_0431_classified.laz",
"minor_version": 4, "dataformat_id": 6, "forward": "all",
"extra_dims": "HeightAboveGround=float" }
]
}filters.outlier marks outliers with the class you give it — class 7 by default — without removing them; setting class: 18 here labels statistical outliers as high noise. Strictly, statistical outliers can be below the surface too; if your specification distinguishes, run a second pass that relabels class-18 points below the ground surface as class 7.
A quick Python check of noise share per tile across a batch:
import json
from pathlib import Path
import numpy as np
import pdal
for tile in sorted(Path("tiles").glob("*.laz")):
p = pdal.Pipeline(json.dumps({"pipeline": [str(tile)]}))
p.execute()
c = p.arrays[0]["Classification"]
share = np.isin(c, [7, 18]).mean()
flag = " <-- check" if share > 0.01 else ""
print(f"{tile.name:<24} noise {share:6.3%}{flag}")# Key Parameter Table
| Stage | Option | Typical value | Purpose |
|---|---|---|---|
filters.range |
limits |
Classification![7:7],Classification![18:18] |
Remove classified noise |
filters.elm |
cell |
10.0 m | Search cell for isolated low points |
filters.elm |
threshold |
1.0 m | Height gap marking a point as low noise |
filters.outlier |
mean_k |
8–16 | Neighbours for the statistical test |
filters.outlier |
multiplier |
2.5–3.0 | Standard deviations beyond which a point is an outlier |
filters.outlier |
class |
7 (default) or 18 | Class written to outliers |
# Verification
- No noise downstream. Add a check after the noise stage in development runs:
pdal info --stats --enumerate Classificationon an intermediate output should list neither 7 nor 18. - DTM minimum. The minimum of the DTM should be within a few metres of the lowest ground you expect. A minimum far below it points to a noise point that slipped through.
- Noise share per tile. Typical shares are 0.01–0.5 percent. A tile with several percent noise may have a sensor problem worth reporting.
# Gotchas and Edge Cases
LAS 1.2 noise. Older files have no class 18; all noise is class 7, and some vendors put high noise in class 7 too. The same range filter handles both.
Unclassified outliers. Many deliveries classify only ground and leave noise as class 1. Excluding classes 7 and 18 then removes nothing. Check the class histogram before assuming noise has been handled.
Removing too much. Statistical outlier removal with a small multiplier on sparse or edge areas removes legitimate points — isolated poles, wires, tree tops. Use conservative settings and inspect what was flagged before trusting it.
Removing noise from the deliverable. Some specifications require noise to remain in the file, classified. Use where clauses on processing stages rather than removing points when the output is a deliverable, as the example does.
# Frequently Asked Questions
What are ASPRS classes 7 and 18?
Class 7 is low point (noise), for returns below the true surface from multipath and sensor artefacts. Class 18, added in LAS 1.4, is high noise, for returns well above the surface from birds, clouds or atmospheric scatter.
Where in the pipeline should noise be removed?
Immediately after the reader, before any stage that uses neighbourhoods or builds surfaces. Removing it later cannot undo the effect it already had on ground classification or features.
How do I remove noise but keep it in the output file?
Instead of a range filter, add a where clause excluding classes 7 and 18 to each processing stage. The stages skip noise points while the writer still receives and writes them.
Which PDAL filter finds low noise points?
filters.elm, the extended local minimum filter, is designed for isolated low points and writes class 7 by default. filters.outlier catches statistical outliers in any direction.
# Related
- Pipeline Filtering Logic — filter order and cost
- Applying Statistical Outlier Filters in PDAL — tuning filters.outlier
- Filtering Points with filters.expression — the expression form of the same filter
- Understanding ASPRS Classification Codes — what each class means
- Tuning SMRF for Forested Terrain — the stage noise hurts most