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:

Format-Neutral Intermediate Representation for HD Map Interchange Three source boxes — OpenDRIVE parametric reference line, Lanelet2 boundary polylines, and NDS.Live lane-group tiles — feed arrows down into a central intermediate representation box holding arc-length-sampled boundaries and a lane connectivity graph in one projected CRS. An arrow leads to a diamond gate testing round-trip RMSE within 0.05 metres and topology preserved. A pass branch goes to a target artifact terminal; a fail branch goes to a remediation box that fixes the lane id mapping and loops back to the intermediate representation. OpenDRIVE parametric reference line Lanelet2 left/right boundary polylines NDS.Live tiled lane-group blobs Metric intermediate representation arc-length boundaries + connectivity graph one projected CRS · signed lane-id table Round-trip gate RMSE ≤0.05 m · topology kept? pass Rebuild into target format fail Fix id mapping re-derive links

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⁻¹.

python
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.

python
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.

python
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]] == x for every source lane.
python
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.

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.