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

The three formats do not disagree about the road; they disagree about what a lane is. Sampling every geometry to a common polyline is what makes them comparable at all:

One Road, Three Lane Models, One Common Polyline Three panels show the same road segment as an OpenDRIVE reference line with lane offsets, as Lanelet2 boundary way pairs, and as an NDS.Live tiled indexed geometry. An arrow from each panel converges on a sampled polyline used as the comparison frame. OpenDRIVE reference line + signed lane offsets Lanelet2 boundary way pairs, nodes shared NDS.Live tile-scoped ids + indexed geometry common sampled polyline dense vertices in one metric frame — where ≤0.05 m is measurable converting geometry directly between two of these compares a curve against a way against a tile index — three different questions

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.

The round trip is the only honest test, and its budget is not one number. Each hop resamples, so the tolerance that matters is the accumulated deviation after returning to the source schema:

Accumulated Deviation Across a Three-Hop Round Trip Three conversion hops each add up to 0.05 metres of deviation to a running total, drawn as a staircase against a 0.2 metre acceptance line at the right. 00.05 m 0.10 m0.15 m0.20 m round-trip gate 0.2 m OpenDRIVE → Lanelet2 Lanelet2 → NDS.Live NDS.Live → OpenDRIVE +0.05+0.05+0.05 0.15 m worst case measure the deviation after the last hop against the original vertices, not hop by hop — per-hop errors can cancel, and a gate that only checks each hop will pass a pair that has drifted twice as far

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.

Measuring what a conversion costs #

A conversion is trustworthy when you can state what it costs, and that statement has two halves that are measured differently. Geometric deviation is a distance and must be measured against the original, not hop by hop: per-hop errors are not independent, so three hops each reporting 0.04 m can leave a total of 0.04 m if they cancel or 0.12 m if they compound, and only the comparison with the source measures what a consumer experiences.

Attribute loss is a set difference and needs three buckets rather than one. Dropped attributes were present before and absent after, which is either an unmappable field with no sidecar or a converter bug. Added attributes are the bucket nobody expects: a converter that fills a default where the source was silent has made the map assert something nobody surveyed. Changed attributes are a bug for a direct field and acceptable for a derived one only inside its stated tolerance.

The gate that makes all of this useful asserts the dropped set equals the declared loss set rather than being a subset of it. That equality is what keeps "we know this conversion is lossy" from silently absorbing a newly lost field, which is the way an interoperability layer normally decays.

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.