Deriving Superelevation from Point-Cloud Cross-Sections
Superelevation — the lateral banking a road carries through a curve — is a safety-relevant attribute the planner uses for lateral acceleration limits, and it can only be measured from 3D surface data, not the 2D centerline. This task recovers it from a registered LiDAR point cloud: step along the centerline, cut thin cross-sections normal to the heading, robustly fit the cross-slope with RANSAC, and gate each station against curvature-consistent banking. It is the 3D companion to curvature in the road curvature and superelevation mapping stage, and it depends on a well-aligned cloud from point cloud registration.
At each station a normal slab of points is RANSAC-fit to recover the cross-slope, then checked against curvature:
Prerequisites #
- Python 3.10+, NumPy 1.24+, SciPy 1.11+, scikit-learn 1.3+ (
RANSACRegressor), open3d 0.18+ for slab selection. - Input: a registered, ground-classified LiDAR point cloud in the map's projected CRS, plus the lane centerline with per-station heading.
- Upstream stage: the cloud is aligned by point cloud registration with the ICP algorithm; curvature per station is already computed.
- Output: a per-station superelevation profile (cross-slope in percent) with an inlier count and fit residual per station.
Step-by-Step #
1. Cut a cross-section slab normal to the centerline #
At each station, project points into the local frame (along-track, cross-track, up) and keep those within a thin along-track slab.
import numpy as np
def cross_section(points: np.ndarray, station: np.ndarray, heading: float,
half_thick=0.25, half_width=6.0):
"""Points within a thin slab normal to the heading at `station`."""
t = np.array([np.cos(heading), np.sin(heading)]) # along-track
n = np.array([-np.sin(heading), np.cos(heading)]) # cross-track
d = points[:, :2] - station
along = d @ t
cross = d @ n
m = (np.abs(along) <= half_thick) & (np.abs(cross) <= half_width)
return cross[m], points[m, 2] # (offset, height)
Key parameters: half_thick (0.25 m) keeps the slab thin so longitudinal grade does not leak in; half_width bounds it to the roadway. Expected output: lateral offsets and heights for the slab.
2. RANSAC-fit the cross-slope #
Fit a robust line to (offset, height); its slope is the banking. RANSAC rejects curb and vegetation returns.
from sklearn.linear_model import RANSACRegressor
def fit_cross_slope(offset: np.ndarray, height: np.ndarray, residual_thr=0.03):
r = RANSACRegressor(residual_threshold=residual_thr, min_samples=0.5)
r.fit(offset.reshape(-1, 1), height)
slope = float(r.estimator_.coef_[0]) # rise / run
inliers = int(r.inlier_mask_.sum())
return slope * 100.0, inliers # superelevation in %
Key parameters: residual_threshold (0.03 m) sets the inlier band around the pavement; min_samples=0.5 requires half the slab to agree, biasing toward the dominant surface. Expected output: cross-slope in percent and the inlier count.
3. Assemble the per-station profile #
Walk the stations and record slope, inliers, and residual, so weak stations can be flagged.
def superelevation_profile(points, stations, headings):
prof = []
for st, hd in zip(stations, headings):
off, h = cross_section(points, st, hd)
if len(off) < 20: # too sparse to trust
prof.append({"slope_pct": np.nan, "inliers": len(off)})
continue
slope_pct, inliers = fit_cross_slope(off, h)
prof.append({"slope_pct": slope_pct, "inliers": inliers})
return prof
Verification & Acceptance Criteria #
Gate the profile on curvature consistency and fit quality.
def assert_superelevation(prof, kappa, tol_pct=0.3):
for p, k in zip(prof, kappa):
if np.isnan(p["slope_pct"]):
continue
# banking should share sign with curvature on real curves
if abs(k) > 0.005:
assert np.sign(p["slope_pct"]) == np.sign(k) or abs(p["slope_pct"]) < tol_pct, \
"banking sign inconsistent with curvature"
assert p["inliers"] >= 20, "too few inliers for a trusted slope"
Acceptance gate: on curved stations (|κ| > 0.005 m⁻¹) the banking sign matches curvature; cross-slope smooth station-to-station within ±0.3%; each accepted station has ≥20 inliers. Sign mismatches or spikes flag a slab corrupted by a curb or barrier — widen the inlier rejection or narrow the slab.
Common Errors & Fixes #
Cross-slope tilts the wrong way on a clear curve. The slab clipped a curb or barrier and RANSAC locked onto it. Narrow half_width to the pavement and lower residual_threshold so only the flat surface is inlier.
Longitudinal grade contaminates the banking. The slab is too thick, so uphill grade adds a false lateral slope. Reduce half_thick to ≤0.25 m and confirm the heading used is the true along-track direction.
Noisy, jumpy profile between stations. Station spacing is finer than the surface noise supports. Increase the station step, or smooth the profile with a short median filter after fitting.
RANSACRegressor raises on degenerate slabs. A near-empty or collinear slab has no consensus set. Guard with the len(off) < 20 skip in step 3 and mark those stations for interpolation.
FAQ #
Why cut cross-sections normal to the centerline? #
Superelevation is the road's lateral tilt, so it must be measured across the road, perpendicular to the direction of travel. A slab cut at any other angle mixes longitudinal grade into the measurement and biases the cross-slope. Orienting the slab along the centerline normal isolates the lateral height change from the longitudinal one, which is what makes the fitted slope the true banking angle.
Why RANSAC instead of a plain least-squares fit? #
A cross-section slab contains more than the road surface — curbs, vegetation, passing vehicles, and barrier returns all land in it. Least squares is pulled off the true surface by those outliers. RANSAC fits the line to the largest consensus set of inliers and ignores the rest, so the recovered cross-slope reflects the pavement, not a curb the slab happened to clip.
How is superelevation cross-checked against curvature? #
Roads bank into curves, so the sign of the cross-slope should follow the sign of the curvature and its magnitude should scale with design speed and curve radius. A station whose fitted banking tilts the wrong way, or is large where the road is straight, is almost always a fit corrupted by an outlier surface. Comparing the profile to the curvature signal catches those stations before the superelevation attribute is written.
Related #
- Fitting Clothoid Transitions to Lane Centerlines — the curvature model this cross-slope profile is checked against.
- Calculating Road Curvature with Python Shapely — the curvature signal used in the consistency gate.
- Point Cloud Registration with ICP Algorithm in Python — the alignment the cross-sections depend on.
Up one level: Road Curvature & Superelevation Mapping — the parent workflow that pairs cross-slope with curvature for HD map serialization.