Map Format Interoperability: OpenDRIVE, Lanelet2 & NDS.Live
Every production autonomous-vehicle stack eventually has to move an HD map across a format boundary — simulation authored in OpenDRIVE schema, a planner that consumes Lanelet2, and a distribution tier serialized as NDS.Live — and the naive approach of translating geometry primitive-for-primitive loses either metric fidelity or lane connectivity at the seam. The three formats disagree at the most fundamental level: OpenDRIVE describes roads as a parametric reference line with lateral width polynomials, Lanelet2 describes lanes as explicit left/right boundary polylines, and NDS.Live packs lane groups into tiled binary blobs indexed for runtime lookup. Bridging them is a schema-mapping problem, not a file-conversion one, and it sits at the junction of the HD mapping and lane geometry extraction pillars. This guide fixes a common metric intermediate representation, maps identity and topology across all three conventions, and holds the whole conversion to a ≤0.05 m round-trip RMSE gate so no lane silently disconnects.
The three formats normalize to one metric intermediate model, then rebuild into any target, gated on round-trip fidelity:
The engineering rule is to never convert format-to-format directly. Instead, project every source into one metric intermediate representation — arc-length-sampled boundary polylines plus an explicit lane connectivity graph, all in a single projected coordinate reference system — then rebuild the target from that neutral model. This collapses an N×N matrix of pairwise converters into N readers and N writers, and gives one place to enforce fidelity.
Format Model Comparison #
The three formats differ across geometry model, identity scheme, and topology encoding. Choosing the intermediate representation means understanding exactly what each one commits to:
| Format | Geometry model | Lane identity | Topology encoding | Best fit |
|---|---|---|---|---|
| OpenDRIVE 1.7/1.8 | Parametric reference line (line, arc, spiral, poly3) + lateral width polynomials | Signed integer offset from reference line (0 = centre) | <link> predecessor/successor + <junction> connection matrix |
Simulation authoring, ASAM toolchains |
| Lanelet2 | Explicit left/right boundary LineString polylines |
Opaque integer relation id | Routing graph from shared boundary points + regulatory relations | Planning, ROS-based stacks |
| NDS.Live | Tiled lane-group geometry, quantized to tile grid | Lane-group index + lane position | Connector features across tile borders | Runtime distribution, embedded fleets |
OpenDRIVE's parametric model is the most compact and the most lossy to sample; Lanelet2's polylines are the most planner-friendly and the natural shape for the intermediate model; NDS.Live's tiled quantization introduces a grid-snapping error that must be budgeted explicitly. The intermediate representation therefore uses arc-length-sampled polylines (Lanelet2-like) as its geometric backbone, with a side table carrying the OpenDRIVE parametric parameters when a loss-free OpenDRIVE→OpenDRIVE round trip is required.
Stage-by-Stage Implementation #
Stage 1 — Sample every geometry to a common polyline #
The mathematical constraint: two geometry models can only be compared or merged once they live in the same space. Sample the OpenDRIVE parametric reference line at a fixed arc-length step, evaluate the lateral width polynomials to recover left/right boundaries, and leave Lanelet2 polylines as-is after reprojecting them into the shared CRS. The sampling step ds sets the geometry error floor — 0.5 m oversmooths tight arcs, so use ≤0.2 m through curvature above 0.05 m⁻¹.
import numpy as np
def sample_opendrive_geometry(ref_line, width_poly, s_max: float, ds: float = 0.2):
"""Sample an OpenDRIVE road to left/right boundary polylines.
ref_line(s) -> (x, y, heading) evaluator for the parametric primitives
width_poly(s) -> (w_left, w_right) lateral offsets at arc length s
Returns two (N, 2) arrays in the projected CRS.
"""
s = np.arange(0.0, s_max + ds, ds)
left, right = [], []
for si in s:
x, y, hdg = ref_line(si)
nx, ny = -np.sin(hdg), np.cos(hdg) # left normal (unit)
wl, wr = width_poly(si)
left.append((x + nx * wl, y + ny * wl))
right.append((x - nx * wr, y - ny * wr))
return np.asarray(left), np.asarray(right)
Key parameters: ds is the arc-length sampling step (≤0.2 m in curvature); the left normal (-sin hdg, cos hdg) follows OpenDRIVE's right-handed convention where positive lane ids sit left of the reference line. Expected output is a pair of (N, 2) float64 arrays in the projected CRS, ready to store as the intermediate boundary geometry.
Stage 2 — Map lane identity across conventions #
Identity must be carried by an explicit, reversible table — re-deriving ids from geometry breaks the moment two lanes overlap at a junction. Translate each source id space into a canonical key and keep the inverse so a forward-then-backward conversion is exact.
from dataclasses import dataclass, field
@dataclass
class LaneIdMap:
"""Reversible mapping between source lane ids and canonical keys."""
fwd: dict = field(default_factory=dict) # source id -> canonical key
inv: dict = field(default_factory=dict) # canonical key -> source id
def bind(self, source_id, canonical: str) -> None:
self.fwd[source_id] = canonical
self.inv[canonical] = source_id
def opendrive_key(road_id: str, lane_section: int, lane_id: int) -> str:
# signed lane_id: negative = right of reference line, positive = left
side = "L" if lane_id > 0 else "R"
return f"{road_id}:{lane_section}:{side}{abs(lane_id)}"
Key parameters: the canonical key encodes road, lane-section index, side, and magnitude, so an OpenDRIVE signed id and an NDS.Live lane-group position resolve to the same string when they describe the same lane. Store the LaneIdMap alongside the geometry so the writer can address target features by canonical key.
Stage 3 — Rebuild connectivity in the target schema #
Topology is reconstructed from the source graph, never from geometric proximity — two lanes that touch at a junction are not necessarily connected, and two connected lanes may be metres apart across a gap. Walk the source predecessor/successor links, translate endpoints through the LaneIdMap, and emit the target's native connectivity, whether that is Lanelet2's shared boundary points or an NDS.Live cross-tile connector. This is the same graph the lane-level topology modeling stage produces, so the intermediate representation reuses its adjacency structure directly.
import networkx as nx
def build_connectivity(links, id_map: LaneIdMap) -> nx.DiGraph:
"""Rebuild a canonical successor graph from source link records."""
g = nx.DiGraph()
for rec in links:
a = id_map.fwd[rec.from_id]
b = id_map.fwd[rec.to_id]
g.add_edge(a, b, kind=rec.kind) # kind: 'successor' | 'junction'
return g
Validation & QC Automation #
Interoperability is a gated stage. Convert forward into the target, then back into the source, and compare. Enforce these thresholds in CI:
- Boundary RMSE ≤0.05 m and Hausdorff ≤0.15 m between source and round-tripped boundary polylines, resampled to a common arc-length grid.
- Topology preservation = 100%: every predecessor, successor, and junction edge in the source graph must exist in the round-tripped graph; a single dropped edge fails the gate.
- Identity bijection:
id_map.inv[id_map.fwd[x]] == xfor every source lane.
def roundtrip_rmse(src: np.ndarray, back: np.ndarray) -> float:
"""RMSE between a source boundary and its round-tripped counterpart,
after resampling both to the same number of arc-length points."""
n = min(len(src), len(back))
src_r = _resample(src, n) # linear arc-length resample to n points
back_r = _resample(back, n)
return float(np.sqrt(np.mean(np.sum((src_r - back_r) ** 2, axis=1))))
def assert_roundtrip(src, back, graph_src, graph_back, tol=0.05):
assert roundtrip_rmse(src, back) <= tol, "boundary drift exceeds budget"
missing = set(graph_src.edges) - set(graph_back.edges)
assert not missing, f"dropped {len(missing)} connectivity edges"
Run this against a corpus of representative tiles — straight segments, tight junctions, multi-lane-section roads — so grid-snapping and parametric-sampling error are both exercised. The geometry checks share tolerances with the topological validation rules applied to native maps.
Edge Cases & Failure Patterns #
- Lane-numbering sign flips. OpenDRIVE increments lane ids outward from the reference line with sign by side; a converter that assumes monotonically increasing ids across the road mislabels the far side. Always resolve side from the sign, not the magnitude order.
- Junction modeling mismatch. OpenDRIVE encodes intersections as a connection matrix between incoming and connecting roads; Lanelet2 has no first-class junction and models the same thing as overlapping lanelets with regulatory elements. A direct edge copy loses the turn semantics — rebuild junction membership explicitly.
- NDS.Live tile-grid snapping. Geometry quantized to the tile grid returns with a systematic offset at tile borders. Budget the quantization step into the RMSE gate, and stitch cross-tile boundaries before comparing.
- CRS drift between sources. A Lanelet2 map in a local metric frame and an OpenDRIVE map in UTM will appear to convert cleanly but sit metres apart. Normalize every source into one projected CRS in Stage 1 before any comparison.
- Regulatory-element loss. Speed limits, traffic-light bindings, and turn restrictions have no OpenDRIVE↔Lanelet2 one-to-one mapping. Carry them as typed attributes on the canonical key so they survive even when the target has no native slot.
Performance & Scale Notes #
The intermediate representation is the memory hot spot: a full metropolitan map sampled at 0.2 m holds hundreds of millions of boundary points. Stream per-tile rather than loading the whole map — read one NDS.Live tile or one OpenDRIVE road at a time, convert, and release. Cache the LaneIdMap per tile and persist it so incremental re-conversion after an edit does not re-derive the whole id space. Sampling and RMSE comparison are trivially parallel across tiles; distribute them with the same worker pattern as async data pipeline architecture, bounding each worker to a fixed RAM ceiling so a dense downtown tile does not OOM the pool. For repeated OpenDRIVE→OpenDRIVE round trips, keep the parametric side table so the reference line is restored exactly instead of re-fitted from samples.
FAQ #
Why not convert directly between OpenDRIVE and Lanelet2 geometry? #
OpenDRIVE stores a parametric reference line with lateral lane-width polynomials, while Lanelet2 stores explicit left and right boundary polylines. There is no closed-form map between a cubic-spiral reference line and a pair of sampled polylines, so a direct translation accumulates error at every primitive boundary. Sampling both models to arc-length points in one projected CRS gives a single intermediate representation both formats can be rebuilt from, which keeps the round trip inside the RMSE budget.
How is lane identity preserved across formats? #
It is preserved with an explicit id table, never by re-deriving ids from geometry. OpenDRIVE numbers lanes by signed offset from the reference line, Lanelet2 assigns opaque integer relation ids, and NDS.Live groups lanes under a lane group with its own indices. The converter writes a reversible mapping between these id spaces so a feature can be traced through a forward and backward conversion and land on the same identity.
What tolerance should the round-trip gate enforce? #
Boundary geometry should return within 0.05 m RMSE and 0.15 m Hausdorff after a forward-then-backward conversion, and every predecessor, successor, and junction link present in the source must survive. Geometry within budget but with a dropped connection still fails, because a lost successor link silently breaks routing even though the lane polylines look correct.
Related #
- How to Parse OpenDRIVE XML with Python — reading the parametric reference line this converter samples in Stage 1.
- Validating OpenDRIVE Files Against the XSD Schema — structural checks that must pass before a file is safe to convert.
- Converting Lanelet2 Maps to Routing Graphs — turning the rebuilt Lanelet2 connectivity into a navigable graph.
- Lane-Level Topology Modeling — the connectivity model the intermediate representation reuses.
- Building Lane Successor Graphs from OpenDRIVE — extracting the source link records Stage 3 rebuilds.
Up one level: HD Mapping Architecture & Spatial Data Standards — the parent domain whose serialization and topology stages this interchange layer bridges to the lane-geometry stack.