Filters#

Raw DEM elevation profiles can have outliers and incorrect terrain. Causes include GPS position noise, DEM resolution, and bare-earth artifacts below road structures. GradeIT uses one or more ElevationFilter objects before it calculates grade.

from gradeit import BridgeFilter, Wood2014Filter, gradeit

gradeit(trace, elevation_model=model)  # Wood2014Filter, the default
gradeit(trace, elevation_model=model, elevation_filter=None)  # no filtering
gradeit(trace, elevation_model=model, elevation_filter=Wood2014Filter(savgol_window_ft=1200))
gradeit(
    trace,
    elevation_model=model,
    elevation_filter=[BridgeFilter(baseline_radius_ft=6000), Wood2014Filter()],
)

GradeIT applies filter sequences in order. Each filter uses the output from the last filter. GradeIT calculates grade from final elevation. All parameters use feet.

Wood2014Filter — the default#

This filter does steps B–E of the Wood et al. method. It resamples to a uniform distance grid, smooths, removes and fills unusual nodes, smooths again, and interpolates.

This is the default filter. It is suitable for most traces. It also removes ordinary bridge and overpass artifacts without bridge-specific logic.

Parameter

Default

What it controls

interval_ft

100.0

Uniform distance-grid spacing (step B). ~3× the DEM's 33 ft post spacing; below one post, adjacent nodes read the same cell.

savgol_window_ft

600.0

Savitzky-Golay width. Wider means a smoother grade signal and more attenuation of short real features.

savgol_polyorder

3

Polynomial order within the window.

binomial_sigma_ft

100.0

Binomial stage width, as a Gaussian-equivalent sigma.

residual_threshold_ft

8.0

Step D discard threshold on |pre − post|. Equals the DEM's 2.44 m vertical RMSE.

residual_grow_ratio

0.5

Hysteresis: a run above threshold × ratio is discarded whole if any node breaches the full threshold. Set to 1.0 to disable.

max_discard_len_ft

2000.0

Runs longer than this are treated as real topography, not artifacts.

max_discard_fraction

0.25

Safety valve so unusually noisy input cannot silently erase a quarter of the trace.

max_gap_ft

1000.0

Unobserved stretches longer than this split the trace into independently filtered segments; points inside such a gap return NaN.

min_node_occupancy

0.35

Guard on interval_ft: warn when fewer than this fraction of grid nodes contain a GPS point. Set to 0.0 to silence.

Tuning notes#

savgol_window_ft controls smoothness. A wider value reduces grade noise. It also reduces real short features.

residual_threshold_ft captures the size of a typical artifact. Raising or lowering this values balances between removing real artifacts and preserving genuine road features.

BridgeFilter — targeted bare-earth correction#

This filter is useful for large bridges that can't be detected by the residual method alone. To do this the BridgeFilter looks for a dip that is lower than the nearby road on both sides. It interpolates road elevation across each dip.

Parameter

Default

What it controls

baseline_radius_ft

5280.0 (1 mi)

Half-width of the rolling-max window on each side. This is the parameter to tune.

min_dip_depth_ft

5.0

Per-point threshold for inclusion in a candidate dip.

min_peak_depth_ft

10.0

At least one point in a run must reach this depth. Filters out wide, shallow noise.

min_bridge_len_ft

50.0

Minimum accepted span. Shorter runs are usually noise.

max_bridge_len_ft

7920.0 (1.5 mi)

Maximum accepted span. Longer runs are usually real terrain.

max_aspect_ratio

50.0

Reject runs whose span ÷ peak depth exceeds this. Bridges are short relative to their depth; valleys are long relative to theirs.

grade_plausibility_tol

0.05

Reject a correction whose recovered grade differs from the surrounding median segment grade by more than this.

Warning

Elevation does not distinguish a valley without a bridge from a valley with a bridge. The checks above cannot always separate them.

baseline_radius_ft defines the difference. Use hundreds of feet for typical overpasses and creek crossings. Use thousands of feet for a major water crossing. But, be aware that a value that is too wide can interpolate a straight line across a real valley.

These examples show both errors:

  • How Filtration Works — the one-mile default flattening 1.5 miles of real Colorado canyon by up to 130 ft, on a trace the default Wood2014Filter handles perfectly.

  • Bare-Earth Bridges — a 5,332 ft crossing of the Carquinez Strait that Wood2014Filter cannot touch at any setting, cleared once baseline_radius_ft covers the span.

Order matters. Put BridgeFilter first. A smoother reduces raw dip magnitude.

Writing your own#

ElevationFilter requires one method:

from typing import List

from gradeit import Coordinate, ElevationFilter


class ClampFilter(ElevationFilter):
    """Clip elevation to a plausible range."""

    def __init__(self, min_ft: float, max_ft: float):
        self.min_ft = min_ft
        self.max_ft = max_ft

    def filter(
        self,
        elevation_profile: List[float],
        coordinates: List[Coordinate],
    ) -> List[float]:
        return [min(max(e, self.min_ft), self.max_ft) for e in elevation_profile]

Take an elevation profile. Return an elevation profile of the same length in feet. Do not return grade. GradeIT calculates grade from final filtered elevation.