Tuning CSF Cloth Resolution and Rigidness

TL;DR: Sweep resolution (0.5–2.0 m), rigidness (1, 2, 3) and threshold (0.3–0.7 m) on a tile with a trusted reference classification, score each run by type I error (reference ground rejected) and type II error (reference non-ground accepted), and choose the setting with the lowest total error — then confirm it on a second tile of the same landscape before using it project-wide.

# Context and Motivation

This guide is part of CSF Cloth Simulation Ground Filtering. CSF has few parameters, which tempts people to guess them. The trouble is that its two main knobs interact: a fine cloth with low rigidness follows every dip, including gaps between trees; a coarse, stiff cloth rejects vegetation well but floats over ditches and banks. The best combination depends on point density, terrain and land cover, and the only reliable way to find it is to measure against something you trust.

A sweep of three parameters at a few values each is 20–60 runs on one tile — minutes of compute — and turns guessing into a documented choice.

Two errors pulling in opposite directions Two curves against cloth resolution from 0.5 to 2.0 metres at rigidness 2. Type II error, non-ground accepted as ground, falls as the cloth coarsens because it bridges vegetation. Type I error, ground rejected, rises because the coarse cloth floats over small terrain features. Total error is lowest around 1.0 to 1.25 metres, shaded as the sweet spot. sweet spot type II: vegetation as ground type I: ground missed 0.51.01.52.0 cloth resolution, m (rigidness 2, illustrative)

# Prerequisites and Assumptions

  • A reference tile per landscape type with trusted ground classification — manually edited, or a vendor classification that passed QA.
  • PDAL 2.1+ with Python bindings; pandas.
  • Enough cores to run the sweep in parallel; each run is single-threaded.
  • The same noise handling as production, applied before every run.

# Step-by-Step Implementation

# Step 1 — Freeze the reference

Store the reference classification in a separate dimension (for example with filters.ferry to RefClass) so every run can be compared point by point in one array.

# Step 2 — Define the grid

Resolution 0.5, 0.75, 1.0, 1.5, 2.0 m; rigidness 1, 2, 3; threshold 0.3, 0.5, 0.7 m. Forty-five runs.

# Step 3 — Run and score

For each combination, run CSF on the reference tile and compute type I = reference ground not classified as ground ÷ reference ground, type II = reference non-ground classified as ground ÷ reference non-ground.

# Step 4 — Pick by total error, check the balance

Minimize type I + type II, but look at both: a DTM tolerates small type I errors (missing a few ground points) far better than type II errors (bumps from vegetation or buildings).

# Step 5 — Validate on a held-out tile

Run the chosen setting on a second reference tile of the same landscape. If errors rise sharply, the first tile was not representative.

# Complete Working Example

python
"""Grid sweep of filters.csf parameters against a reference classification."""
from __future__ import annotations

import itertools
import json
from concurrent.futures import ProcessPoolExecutor

import numpy as np
import pandas as pd

REF_TILE = "reference/forest_ref_0822.laz"      # classification is the trusted reference
GRID = {"resolution": [0.5, 0.75, 1.0, 1.5, 2.0], "rigidness": [1, 2, 3], "threshold": [0.3, 0.5, 0.7]}


def score(params: dict) -> dict:
    import pdal
    spec = {"pipeline": [
        REF_TILE,
        {"type": "filters.range", "limits": "Classification![7:7],Classification![18:18]"},
        {"type": "filters.ferry", "dimensions": "Classification=>RefClass"},
        {"type": "filters.assign", "value": ["Classification = 1"]},
        {"type": "filters.csf", "smooth": True, **params},
    ]}
    p = pdal.Pipeline(json.dumps(spec))
    p.execute()
    a = p.arrays[0]
    ref_g = a["RefClass"] == 2
    got_g = a["Classification"] == 2
    t1 = float((ref_g & ~got_g).sum() / max(ref_g.sum(), 1))
    t2 = float((~ref_g & got_g).sum() / max((~ref_g).sum(), 1))
    return {**params, "type1": round(t1, 4), "type2": round(t2, 4), "total": round(t1 + t2, 4)}


if __name__ == "__main__":
    combos = [dict(zip(GRID, v)) for v in itertools.product(*GRID.values())]
    with ProcessPoolExecutor() as pool:
        results = pd.DataFrame(list(pool.map(score, combos)))
    results = results.sort_values("total")
    results.to_csv("tuning/csf_sweep_forest.csv", index=False)
    print(results.head(8).to_string(index=False))
    best = results.iloc[0].to_dict()
    print("best:", {k: best[k] for k in ("resolution", "rigidness", "threshold")})

Illustrative top of the table for a mixed forest tile at about 12 pts/m²:

text
 resolution  rigidness  threshold   type1   type2   total
       1.00          2        0.5  0.0312  0.0088  0.0400
       1.00          2        0.3  0.0421  0.0051  0.0472
       0.75          2        0.5  0.0247  0.0241  0.0488
       1.50          2        0.5  0.0493  0.0063  0.0556
       1.00          1        0.5  0.0206  0.0379  0.0585
Total error across the grid A grid with rigidness 1, 2 and 3 as rows and resolution 0.5, 0.75, 1.0, 1.5 and 2.0 metres as columns, at threshold 0.5. Each cell shows total error. The lowest value, 0.040, is at rigidness 2 and resolution 1.0 and is outlined. Rigidness 3 with fine resolution and rigidness 1 with coarse resolution are the worst corners. 0.50.751.01.52.0 rigidness 1rigidness 2rigidness 3 0.0710.0620.0590.0740.098 0.0660.0490.0400.0560.069 0.1040.0710.0520.0580.077 threshold 0.5 m; cloth resolution in metres across the top

# Key Parameter Table

Parameter Sweep values What it trades
resolution 0.5–2.0 m Following terrain detail vs bridging vegetation gaps
rigidness 1, 2, 3 Following slopes vs rejecting low objects
threshold 0.3–0.7 m Accepting rough ground vs accepting low vegetation
smooth on (fixed) Slope post-processing; turn off only on flat land
iterations 500 (fixed) Increase only if the cloth has not settled

# Verification

  • Held-out tile. The chosen setting’s total error on a second reference tile should be within about 20 percent of the first.
  • Hillshade. Visual inspection of the DTM from the chosen setting catches error types the rates average away, such as a single large building left in the ground.
  • Stability. The top few settings in the sweep should be neighbours in parameter space. A best result isolated among poor ones is likely noise.

# Gotchas and Edge Cases

Reference quality bounds your tuning. A vendor reference with its own ground errors trains CSF to reproduce them. Spot-check the reference, especially around bridges, dense shrubs and steep banks, before sweeping.

Error rates depend on landscape mix. A tile that is 90 percent forest and 10 percent town weights errors accordingly. Tune per landscape type — flat farmland, rolling woodland, mountains, towns — and select settings per tile by land cover.

Density changes the optimum. Settings tuned on 12 pts/m² data do not transfer to 4 or 40 pts/m². Retune when density differs by more than a factor of about two.

Optimum resolution follows density Three points showing the best cloth resolution found in sweeps at different densities: 0.5 metres at 40 points per square metre, 1.0 metre at 12, and 2.0 metres at 3. A rising line through them shows that optimum resolution grows roughly with ground point spacing. 40 pts/m²: 0.5 m 12 pts/m²: 1.0 m 3 pts/m²: 2.0 m ground point spacing → (illustrative)

Overfitting the threshold. Very small thresholds reduce type II errors on the reference tile by being strict, and fail on rougher terrain elsewhere. Prefer settings that are good across several tiles over the best on one.

# Frequently Asked Questions

How do I tune the cloth simulation filter?

Run filters.csf over a small grid of resolution, rigidness and threshold values on a tile with trusted ground classification, score each run by the share of ground missed and non-ground accepted, and pick the setting with the lowest total error. Confirm it on a second tile.

What are type I and type II errors in ground filtering?

Type I errors are true ground points rejected as non-ground; type II errors are non-ground points, such as vegetation or buildings, accepted as ground. Type II errors are usually more harmful to a DTM because they create bumps.

How many runs does a tuning sweep need?

A grid of five resolutions, three rigidness values and three thresholds is 45 runs, which takes minutes on one tile with a few cores. Coarse grids followed by a finer grid around the best result work well.

Should I use one setting for the whole project?

Only if the project is homogeneous. For mixed landscapes, tune per landscape type and choose the setting per tile based on its dominant land cover.