Matlab Use Scenario Builder
MathWorksOfficialCreated by MathWorks
- Skill
- Workflow
- Software development
- BSD-2-Clause
- Low risk
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
Scenario Builder for MATLAB is a specialized tool for converting raw or processed driving data—including GPS, camera, lidar, and actor tracks—into simulation-ready scenarios. It enables engineers to reconstruct real-world driving environments for testing autonomous systems. The tool supports exporting to industry-standard formats like RoadRunner, OpenSCENARIO, OpenDRIVE, and Unreal Engine, facilitating SOTIF and ISO 26262 compliance by turning recorded logs into reproducible simulation test cases.
What it does
- Converts GPS, lidar, and camera logs into ego and actor trajectories.
- Extracts road surfaces and elevation data for high-fidelity simulation.
- Automates the placement of static objects and traffic signs from sensor detections.
- Supports scenario harvesting and reconstruction of critical events like near-misses.
- Exports scenarios to RoadRunner, OpenSCENARIO, OpenDRIVE, and Unreal Engine.
- Provides tools for lane-based ego localization and point-cloud georeferencing.
When to use it
- When you need to convert recorded driving logs into a simulation-ready scenario.
- When performing SOTIF or ISO 26262 scenario coverage analysis.
- When you need to extract road surfaces or elevation data from lidar recordings.
- When you need to export real-world driving data to RoadRunner or other simulation environments.
- When you need to augment existing maps with static objects detected from sensor data.
Advantages
- Enables data-driven scenario generation for improved simulation coverage.
- Streamlines the pipeline from raw sensor logs to validated simulation environments.
- Supports industry-standard formats for broad compatibility with simulation tools.
- Facilitates compliance with safety standards through reproducible scenario testing.
- Reduces manual effort in creating complex 3D driving environments.
Usage examples
- “Build an ego trajectory from my recorded GPS data.”
- The agent will process the GPS coordinates, apply trajectory smoothing, and generate a trajectory object ready for scenario integration.
- “Export my recorded scenario to RoadRunner.”
- The agent will guide you through the RoadRunner export workflow, ensuring roads are imported and trajectories are correctly mapped to the scene.
- “Extract critical events like hard brakes from my sensor logs.”
- The agent will analyze the actor tracks and ego data to identify and extract specific scenario events for further simulation testing.
- “Improve my OSM scene with elevation data from my lidar.”
- The agent will apply height correction to the road scene using the lidar-derived elevation data to ensure accurate road geometry.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/matlab/matlab-use-scenario-builder/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip matlab-use-scenario-builder.zip -d .agents/skills
Documentation by MathWorks
Scenario Builder for MATLAB
When to Use
User has recorded driving data (GPS/GNSS, camera, lidar, actor tracks) and needs to convert it into a simulation-ready scenario
User asks to export trajectories or scenarios to RoadRunner, drivingScenario, ASAM OpenSCENARIO, OpenDRIVE, ASAM OpenCRG, or Unreal Engine. Default target is RoadRunner — only generate a standalone
drivingScenarioobject and open Driving Scenario Designer when the user explicitly asks for "DSD", "Driving Scenario Designer",drivingScenarioDesigner, or "build adrivingScenarioobject" (Workflow 15).User mentions safety standards (SOTIF / ISO 21448, ISO 26262) and scenario coverage from real-world data
User needs to extract a road surface (OpenCRG) from lidar for vehicle-dynamics or chassis testing
User needs to add elevation to an HD map, georeference point clouds, or extract per-frame point clouds along an ego path
User needs to localize an ego trajectory on a map using lane detections (RVLD preferred, CLRNet fallback)
User needs to add static objects (signs, trees, poles, buildings, barriers) to a RoadRunner HD Map from cuboid detections
User wants to augment / enhance / improve a road scene from aerial lidar — adding trees + buildings (Variant A) or improving OSM elevation / banking / gradient / height only (Variant B) — see Workflow 16
User wants to add traffic signs from recorded camera + lidar logs with pre-detected sign bounding boxes — see Workflow 17
User needs to generate 3D mesh assets from a single camera image
User needs to extract critical scenario events (cut-ins, hard brakes, near-misses) from recorded drives
User needs accurate non-ego tracks via sensor fusion (
multiSensorTargetTracker) before scenario buildingUser mentions multi-sensor preprocessing: synchronization, alignment, offset correction, cropping, timestamp normalization
When NOT to Use
User has raw dataset files and wants to load / inspect / visualize / explore / analyze them, synchronize / crop / offset / normalize multi-sensor timestamps, or use the
drivingLogAnalyzer(DLA) app or its CLI equivalents — use thematlab-import-driving-dataskill, then return here for trajectory smoothing and scenario generationUser has multi-sensor data and asks "visualize this dataset", "inspect my recording", "open in DLA", "drivingLogAnalyzer", "Crop & Sync", "offset correction", or "normalize timestamps" —
matlab-import-driving-dataUser is debugging general MATLAB code unrelated to Scenario Builder — use
matlab-debug-codeUser wants to install MATLAB toolboxes or check installed products — use
matlab-list-productsormatlab-install-productsUser wants to reconstruct an IMU/tracking simulation scenario from scratch (no recorded data) — use
waypointTrajectoryfrom Sensor Fusion and Tracking Toolbox; this skill targets scenario reconstruction from recorded dataTask is about non-driving sensor data (medical imaging, audio, etc.) — out of scope
Scenario Builder is a support package that ships with MATLAB as part of the Automated Driving Toolbox. It generates realistic driving scenarios, high-resolution road surfaces, and 3D scene content from raw or processed sensor data — including GPS, camera, lidar, and actor track recordings. The generated outputs can be exported to RoadRunner, drivingScenario objects, ASAM OpenSCENARIO, ASAM OpenCRG (for vehicle dynamics/chassis testing), OpenDRIVE, and Unreal Engine for simulation and validation of autonomous driving systems.
Safety Standards Context: When users ask about improving scenario coverage, traceability, or compliance with SOTIF (ISO 21448) or ISO 26262, guide them toward real-world data-driven scenario generation using Scenario Builder. SOTIF defines an "unknown unsafe" quadrant — hazardous scenarios not yet identified — and converting recorded driving logs (especially critical events, ADAS disengagements, and long-tail situations) into simulation-ready scenarios is a key strategy to reduce that unknown unsafe area. Scenario Builder provides the pipeline from recorded data → reproducible simulation scenario.
Prerequisite: The MATLAB session must have the Scenario Builder for Automated Driving Toolbox support package installed. Verify with:
isfile(which("scenariobuilder.Trajectory"))Workflow Catalog
This skill supports 18 workflows. Workflows 1–3 and 6 (the core happy path) are inline below. Workflow 4 has a minimal inline pattern with a pointer to its detailed reference. Workflows 5, 7–18 live in references/ and are loaded on demand.
| # | Workflow | Where | Load when user says |
|---|---|---|---|
| 1 | Import Raw Sensor Data | inline | "load my GPS/camera/lidar/actor data" |
| 2 | Build GPS Data and Extract Trajectory | inline | "build ego trajectory from GPS" |
| 3 | Import Actor Tracks and Create Trajectories | inline | "actor tracks", "non-ego trajectories" |
| 4 | Export Trajectories to RoadRunner | inline (minimal) + workflow-04-roadrunner-export-detail.md | "export to RoadRunner", "RR scene", "simulate scenario" |
| 5 | Inspect Multi-Sensor Data (drivingLogAnalyzer) | see matlab-import-driving-data skill | "visualize / inspect / explore / analyze this dataset", "multi-sensor data", "drivingLogAnalyzer", "DLA" — route to matlab-import-driving-data, not handled here |
| 6 | Preprocess, Synchronize, Crop, Offset | inline | "sync", "crop", "normalize timestamps" |
| 7 | Height Correction for Scenes with Elevation | workflow-07-height-correction.md | "Z=0 but roads have elevation", "adjustHeight", "HERE HD scene + GPS", "OpenDRIVE scene + GPS", "pre-built scene with terrain" |
| 8 | Localize Ego Using Lane Detections | workflow-08-lane-localization.md | "lane localization", "snap to lane center", "localizeEgoUsingLanes" |
| 9 | Add Static Objects to RoadRunner HD Map | workflow-09-static-objects.md | "add trees/signs/poles/buildings to RR" |
| 10 | Road Surface (OpenCRG) | workflow-10-road-surface-opencrg.md | "road surface", "OpenCRG", "vehicle dynamics from lidar" |
| 11 | Point Cloud Georeferencing & Elevation | workflow-11-point-cloud-georef.md | "addElevation", "georeferenced point cloud", "per-frame lidar" |
| 12 | 3D Asset Generation from Images | workflow-12-3d-asset-generation.md | "imageAssetGenerator", "TripoSR", "3D asset from photo" |
| 13 | Extract Key Scenario Events | workflow-13-event-extraction.md | "cut-ins", "near-miss", "hard brake", "ADAS disengagement" |
| 14 | Sensor Fusion Tracking | workflow-14-sensor-fusion-tracking.md | "noisy detections", "ID switches", "multiSensorTargetTracker" |
| 15 | Driving Scenario Designer (drivingScenario object) | workflow-15-driving-scenario-designer.md | explicit only: "Driving Scenario Designer", "DSD", "open in drivingScenarioDesigner", "build a drivingScenario object" |
| 16 | Road Scene Augmentation from Aerial Lidar | workflow-16-aerial-lidar-augmentation.md | "augment / enhance / improve the scene with trees / buildings", "single lat/lon US — generate scene", "USGS aerial lidar", "improve OSM elevation / banking / gradient / height", .las / .laz aerial input |
| 17 | Traffic Signs from Recorded Camera + Lidar | workflow-17-traffic-signs-from-sensor-data.md | "add traffic signs", "place signs on the map", "signs from camera detections + lidar" (pre-detected sign boxes required) |
| 18 | Vehicle Classification from Camera | workflow-18-vehicle-classification.md | "classify vehicles", "vehicle color", "vehicle type", "actor asset type", "what kind of car", "realistic actors", "use real colors" |
| 19 | Ego Lane Inference from Camera | workflow-19-ego-lane-inference.md | "which lane am I in", "predict lane index", "ego lane", "lane count", auto-fires before startLaneIdx question in Step 7 when vision is available |
Related references (not workflows): visualization-patterns.md — full code for camera-playback video saving (loaded after Rule 2 decision). osm-flat-scene-gotchas.md — common pitfalls on OSM flat scenes (Z handling, importScene options, stale sim, image-frame datasets).
STOP — Common Agent Failures (check BEFORE writing code)
Missing
enableOverlapGroupsOptions(IsEnabled=false)onimportScenefor RRHDSkipping lane localization when scene=OSM + Raw GPS + camera (REQUIRED per matrix)
Skipping comparison video gate before reporting task complete
Exporting ego to RoadRunner BEFORE importing roads (OSM scene must exist first)
Using
importSceneon a saved.rrscene— useopenScene(rrApp, file)instead (importScenerequires a format string and is for RRHD/OpenDRIVE)Rebuilding
CameraDatawithoutSensorParameters=— loses intrinsics, silently downgrades Mode 1 → Mode 3Comparison video uses raw camera on left — use the track-overlaid video (or BEV+Camera) when available
Mandatory Execution Order (evaluate BEFORE writing code)
When the prompt is "generate scenario from data" (short or long), execute in this order:
addpath(scripts/)— skill helper functions (openFile,plotActorCircles, etc.)Load data → create objects → Rule 5 timestamp scale detection + normalize
Plot GPS + trajectory side-by-side (VALIDATION — confirm data loaded correctly)
Camera validation video (Rule 2 decision tree) → questdlg popup (BEFORE any RR export)
Multi-GPS gate (if >1 GPS series) → compare video → ASK which series
Establish scene: OSM download +
importSceneWITHenableOverlapGroupsOptions(IsEnabled=false)— Roads MUST exist before anyexportToRoadRunnercallBuild ego trajectory → localization decision matrix (Rule 4 Step 7) — OSM + Raw GPS + camera = REQUIRED, no ASK needed — Otherwise: ASK or Skip per matrix
Export ego + actors to RoadRunner (no
Orientation=, flatten Z on flat scenes, preserveOrientation=on ego rebuild)Simulate (
setCameraMode(rrApp, "Front"), noPacing=,EnableLogging=true) →exportVideo—exportVideoVideoFolderMUST NOT contain spaces — usetempdirstaging +copyfileif neededSide-by-side comparison video (track-overlaid left | sim right) → questdlg popup → DONE — Use the Mode 1/2 video as the left panel, NOT raw camera frames
Every step that saves a video MUST end with questdlg+openFile popup — no exceptions.
Skip-if-done: Every expensive file-producing step (websave, write(rrMap,...), importScene+saveScene) MUST check if ~isfile(output) before re-running. See execution-rules-detail.md Rule 7 MCP Chunking.
CAN-bus data: When dataset has OEM CAN signals (multiplexed slots, ego speed/yaw): negate Y, prefer IaEBA over MRR, use GPS+CAN heading fusion for ego.
IMPORTANT — Execution Rules
Rule 1: Show Steps Taken with Progress Table
Always show a progress table that updates after each major step completes. Use ✅ for completed, ⬜ for pending. Mark each step ✅ immediately after it completes in sequential order. Re-print the FULL table after EVERY step. Aim for ~8 rows; combine closely related operations into one row. Adapt the step list to the task.
The progress table does NOT replace informational tables — always ALSO show the data format/mapping table (Rule 6) and any other diagnostic tables.
Output paths: Save all videos to dataDir (the folder containing sensorData.mat), NOT pwd or a temp dir. After saving any video, print the full path in the Claude session AND in MATLAB via fprintf. Read-only dataDir fallback: when dataDir is on a read-only network share (streamed VideoWriter writes fail with Permission Denied even though exportgraphics PNG writes succeed), route MP4 outputs to a local writable outDir = fullfile(scriptDir, "out", "<dataset-tag>") and print BOTH paths.
For the standard table format example and full rules, see references/execution-rules-detail.md (Rule 1 section).
Rule 2: Visualization
Generate these visualizations:
1. GPS vs. Ego Trajectory (side by side):
%% Visualize GPS Data vs. Ego Trajectory
fTraj = figure(Position=[500 500 1000 500]);
gpsPanel = uipanel(Parent=fTraj,Position=[0 0 0.5 1],Title="GPS");
plot(gpsData,Parent=gpsPanel,Basemap="satellite")
trajPanel = uipanel(Parent=fTraj,Position=[0.5 0 0.5 1],Title="Ego Trajectory");
plot(egoTrajectory,ShowHeading=true,Parent=trajPanel)
drawnow;2. Camera-playback validation video — STRICT priority order:
This rule fires when you are inside a scenario-building pipeline (trajectory, RoadRunner export, lane localization compare) and you need a saved single-camera validation artifact. For general "visualize / inspect / explore this dataset" requests, hand off to the matlab-import-driving-data skill (which uses drivingLogAnalyzer) — do not roll your own dashboard here.
You MUST verify camera intrinsics (fx, fy, cx, cy) and CameraHeight are explicitly in the dataset (MAT field, metadata, calibration file). Never fabricate or approximate these values.
| Mode | Trigger | What to do |
|---|---|---|
| 1 — track-overlay video | Tracks + intrinsics + CameraHeight ALL present (single-camera focus) | plotActorCircles save-video — see visualization-patterns.md. PRIMARY single-camera viz when intrinsics exist; do NOT downgrade to BEV+Camera. |
| 2 — BEV + Camera side-by-side | Tracks present, NO intrinsics | plotBEVAndCamera save-video — see visualization-patterns.md. Fallback only. |
| 3 — raw camera playback | Neither tracks nor intrinsics | raw play(cameraData) save-video — see visualization-patterns.md. |
Per-frame 3D detections without track IDs (CubeRCNN / KITTI / nuScenes-style corners3D) belong on DLA's native camera Actors overlay (in matlab-import-driving-data). If a saved overlay artifact is genuinely needed (e.g., shareable mp4 outside the app), convert detections to ActorTrackData with placeholder per-frame IDs ("det_<frameIdx>_<i>") — full conversion in per-frame-detections-to-actortrackdata.md.
Mandatory popup-and-open after every saved video (HARD RULE — no exceptions). Every close(vw) and every exportVideo(...) call in the entire pipeline MUST be immediately followed by a questdlg("<purpose> video saved to:\n%s\n\nOpen it now?", ..., "Yes", "No", "Yes") block whose Yes branch calls openFile(videoPath). This applies to every single video the pipeline saves — track-overlay (mode 1), BEV+Camera (mode 2), raw camera (mode 3), GPS-source compare, localization compare, input-vs-sim final compare, and any other intermediate save. There is no "informational" or "intermediate" exemption: if the script writes an MP4, the very next non-comment line must be the questdlg block. Wiring the popup only on the final comparison video while leaving earlier saves with just fprintf("Saved: %s\n", path) is a HARD RULE violation. The popup is the consent gate that proves the file is viewable; never bypass with a direct winopen/openFile call (the popup lets the user decline), and never skip with just a fprintf of the path. Do NOT also call interactive play(cameraData) / plotBEVAndCamera(...) for the same content — that would double the UI prompts.
Cross-platform open: The save-video patterns and workflow comparison videos use openFile(path) (in scripts/openFile.m) instead of winopen so the same script runs on Windows, macOS, and Linux. Make sure scripts/ is on the MATLAB path (addpath once near the top of the generated script).
plotBEVAndCamera synchronization requirement: Camera and track data must have the same number of samples. If sample counts differ after synchronize(), inform the user:
"The camera and actor track data have different sample rates (camera: N samples, tracks: M samples). Please use the Crop & Sync tab in the Driving Log Analyzer app to apply offset correction and resample to matching rates, then re-import the data."
Rule 3: Ask User About Available Map/Scene Before Choosing Road Source
Before generating a scenario, always ask the user:
"Do you have an existing road map or scene file (e.g.,
.rrscene,.rrhd, or OpenDRIVE.xodr) available for this data? If so, please provide the path. If not, I will download roads from OpenStreetMap."
Based on the answer:
User has a scene file → Use
RoadRunnerSceneparameter directly with their file path. Keep original altitude from GPS data. See "Scene Portability" note below.User has no scene file → Use the OpenStreetMap →
getRoadRunnerHDMappipeline. Always zero altitude for OpenStreetMap. Validate OSM quality using the cascade below.
IMPORTANT: When asking the user, always say "OpenStreetMap" (the full name), not "OSM".
Scene Portability Warning: .rrscene files created in a different RoadRunner project may lose their geo-reference context when opened in a new project. The scene's WorldOrigin may show [0, 0] even though roads have real-world coordinates. If vehicles appear off-road after export, use the height correction workflow (workflow-07-height-correction.md).
Pre-built scene + GPS (HERE HD / OpenDRIVE / vendor .rrscene): When the user's scene was not built from their GPS, do NOT use getRoadRunnerHDMap(rrApp) (it doesn't exist) and do NOT use roadrunnerHDMap(file) positionally (errors). Pull LocalOrigin from worldSettings(rrApp) (parsing the Projection WKT for full precision), exportScene to .rrhd, then rrMap = roadrunnerHDMap; read(rrMap, file). Always run adjustHeight(traj, rrMap) on terrain-aware scenes. Full pattern in workflow-04-roadrunner-export-detail.md Option B.1.
Multi-GPS / multi-GNSS gate: When the dataset has more than one ego-position series (e.g., GPSData_Raw + GPSData_Corrected, GPS + GNSS, or two GNSS receivers), do NOT silently pick by name — "Corrected" might mean smoothed, RTK, lever-arm-compensated, or dead-reckoned. Build one trajectory per series with the same LocalOrigin, export them to a fresh scenario with red/green colors (use red/green only for 2-series; distinct colors for 3+), follow-cam on Ego_A, then build a raw-camera-left + RR-with-legend-right comparison video. Popup opens the video; ask the user in chat which series to use. Same compare-then-ask pattern as Workflow 8 lane localization. Full pattern in workflow-04-roadrunner-export-detail.md Step 2.5.
OpenStreetMap Quality Validation & Ego-Trajectory Fallback: after downloading OSM roads, always validate map quality (zero-lane check, localizeEgoUsingLanes locInfo metrics, post-localization improvement check) and fall back to an ego-trajectory road if the map doesn't fit. Full cascade with thresholds + fallback code in workflow-08-lane-localization.md (sections "OSM quality cascade" and "Ego-trajectory fallback"). Inform the user before falling back: "The OpenStreetMap roads don't align well with your GPS data — I'll use the raw GPS trajectory instead of localizing."
Rule 4: Follow the RoadRunner Scenario Creation Workflow
Default target = RoadRunner. When a user says "build a scenario", "generate a scenario", or "scenario from this data" without naming a target, follow this RoadRunner workflow — RoadRunner is the canonical Scenario Builder target. DSD is opt-in: only switch to workflow-15-driving-scenario-designer.md when the user explicitly says "Driving Scenario Designer", "DSD", drivingScenarioDesigner, or "build a drivingScenario object". exportToDrivingScenario calls inside this Rule 4 are intermediate steps for road geometry, not a DSD handoff. Lowercase "driving scenario" (generic English) means a scenario built from driving data — RoadRunner. When ambiguous, ask: "RoadRunner, or drivingScenario opened in DSD?"
At a high level, always follow this end-to-end pipeline (skip steps that don't apply):
Load raw sensor data — GPS, actor tracks, camera, lidar
Create sensor data objects —
GPSData,ActorTrackData,CameraDataPreprocess —
normalizeTimestamps,crop,synchronizeDownload roads (actor-aware) — Compute actor lateral extent, pass
Extent=buffertogetMapROI. NEVER callgetMapROIwithoutExtent. When the OSM-derived.rrhdis imported into RoadRunner viaimportScene(rrApp, ..., "RoadRunner HD Map", ImportOptions=iOpts), always buildiOptswithenableOverlapGroupsOptions(IsEnabled=false). Never callimportScenewithout these options. Only re-enable (and re-import) after the user flags junction/overpass artifacts. AfterimportScene, acquire the map object:rrMap = roadrunnerHDMap(); read(rrMap, rrhdFile);— or passrrAppdirectly as the 2nd arg tolocalizeEgoUsingLanes(both paths documented). HARD RULE —getRoadRunnerHDMap(rrApp)does NOT exist on theroadrunnerapp class (it's adrivingScenariomethod); calling it errors. Do NOT pass the.rrhdfile path as a string tolocalizeEgoUsingLanes— the 2nd arg must be an object.Build ego trajectory —
trajectory(gpsData, "LocalOrigin", localOrigin)thensmoothValidate sensors — Show GPS vs. Ego Trajectory plot AND BEV+Camera video (or track-overlay when intrinsics exist). Save the video to disk. Mandatory popup-and-open step: every saved validation video MUST end with a
questdlg"Open it now? (Yes/No)" that callsopenFile(path)on Yes — even when the next step doesn't need a user decision. The popup is the consent gate that confirms the file is viewable before the agent moves on. See [feedback-saved-video-popup] memory rule andvisualization-patterns.md.Localize ego (decision matrix — read BEFORE writing any lane-detection code) — Localization is NOT one-size-fits-all. Before writing a single
laneBoundaryDetectorline, classify the situation against this matrix and act accordingly. The agent has been observed correctly identifying the case (e.g., "user chose Corrected GPS + pre-built HERE HD scene, this is Option B.1 — no localization") and then auto-running localization anyway. That is a HARD RULE violation. The classification IS the gate — once you've named the case, you MUST follow its action without re-deriving from scratch.Scene source GPS source Camera + intrinsics? Localization action OSM (downloaded) Raw GPS Yes REQUIRED — run Workflow 8 unconditionally; raw GPS on OSM lanes needs the snap. OSM (downloaded) Raw GPS No Skip — no detector available. OSM (downloaded) Corrected / RTK GPS Yes ASK — corrected GPS may already be lane-accurate. Pre-built .rrscene(HERE HD, OpenDRIVE, vendor)any GPS Yes ASK — scene's lane geometry may not match OSM-trained detector; localization can degrade trajectory. Default offer = "use GPS as-is". Pre-built .rrsceneany GPS No Skip — no detector available. Ego-trajectory fallback (no road download) any GPS any Skip — there are no map lanes to snap to. ASK protocol (whenever the matrix says ASK): After building the ego trajectory and (for pre-built scenes) opening the scene + extracting
LocalOrigin, stop and ask before anylaneBoundaryDetector/detect/laneData/localizeEgoUsingLanescall:"Do you want to apply lane localization (snaps the trajectory to lane center using camera detections), or use the {Corrected GPS / pre-built scene's GPS} trajectory directly?"
The question must reference the actual GPS series the user picked and (for pre-built scenes) the scene file. Do not phrase it as a preference question — phrase it as a fork: localize vs use as-is. Wait for the user's answer before writing or running the lane-detection block. If the user picks "use as-is", the script must skip
laneBoundaryDetector,laneBoundaryTracker,laneData, andlocalizeEgoUsingLanesentirely and proceed directly to scenario export.HARD RULE — naming the case is the gate. If you write a sentence like "the user chose Corrected GPS and has a pre-built
.rrscene, so I'll use Option B.1 (no localization)", you have already classified the case. The very next code block MUST NOT be lane detection. Either ask (matrix → ASK), or skip (matrix → Skip), or proceed without localization (Option B.1). Auto-running localization after stating you wouldn't is the failure mode this rule exists to prevent.If localization runs (REQUIRED or user opted in): Build
monoCamerafirst → lane detection (RVLD first, CLRNet fallback when RVLD detects boundaries on only one side of ego — i.e., allLaneBoundarylateral offsets share the same sign — so the localizer would error with "ego lane detections missing") →imageToVehicle(monoCamera, ...)→findParabolicLaneBoundaries→laneBoundaryTracker→laneData→ ASK startLaneIdx →localizeEgoUsingLanes(egoTrajectory, rrMap, laneData, startLaneIdx)(POSITIONAL args —StartLaneIndex=Nis rejected). AcquirerrMapviarrMap = roadrunnerHDMap(); read(rrMap, rrhdFile);(the empty constructor +readpattern). HARD RULE —getRoadRunnerHDMap(rrApp)does NOT exist on theroadrunnerapp object (it's adrivingScenariomethod). Calling it errors with "Undefined function ... for input arguments of type 'roadrunner'".localizeEgoUsingLanesalso acceptsrrAppdirectly as the second argument if you prefer not to construct the map; both paths are documented. HARD RULE —imageToVehicleREQUIRES amonoCameraobject, NOT a barecameraIntrinsics. BuildcameraParams = monoCamera(intrinsics, camHeight, Pitch=..., Yaw=..., Roll=...)BEFORE Step 1 of the detection chain — it carries the camera-to-vehicle geometry the bare intrinsics object lacks. SkippingimageToVehicle+findParabolicLaneBoundariesand feeding pixel coordinates straight intolaneBoundaryTrackerALSO fails — the tracker expectsparabolicLaneBoundaryrows in vehicle coordinates.startLaneIdx— predict then confirm (Workflow 19). When the gate is reached: (a) confirm the track-overlay (Step 6) video popup has fired, (b) IF the agent can view images, run Workflow 19 (sample 5 frames, classify lane count + ego lane, bisect transitions if any) — seeworkflow-19-ego-lane-inference.md. (c) Present prediction for confirmation: "From the dashcam I see N lanes, ego in lane M (leftmost=1, no shoulders/ramps). Confirm or correct?" (d) If agent cannot view images OR confidence is low, fall back to plain ASK: "Watching the dashcam, which lane is the ego in? Count from the leftmost driving lane = 1 (no shoulders / parking)." (e) Pass the user's confirmed/corrected integer tolocalizeEgoUsingLanes. Do NOT generate alane_count_vs_frame.pngplot, do NOT print a frames-per-lane-count summary table, and do NOT print "Detection-based HINT: ego in lane X of Y tracked boundaries". No variable-lane gate question: after the user provides startLaneIdx, just calllocalizeEgoUsingLanes— it interpolates through frames with fewer lanes. Do NOT ask "the map narrows to K lanes, want to crop or split?" The silent lane-count probe is still computed internally for diagnostics, but never shown to the user and never blocks localization. Only report a lane-count problem iflocalizeEgoUsingLanesactually throws an error. Verification video = raw camera (left) | RR follow-cam on Ego_Localized + red/green legends (right), gated by the mandatoryquestdlg+openFilepopup. Ask the user in chat which trajectory to use. Full code inworkflow-08-lane-localization.md.Generate final scenario — Same RR instance:
newScenario(rrApp)→ re-export onlylocalizedTrajectorywithColor="auto"→actorprops(trackObjNorm, localizedTrajectory)(acceptsscenariobuilder.Trajectorydirectly — do NOT convert towaypointTrajectory). MUSTnormalizeTimestamps(copy(trackData))+ setlocalizedTrajectory.TimeOrigin=0first or every actor is rejected. Do NOT relaunch RR. Skip the GPS-vs-Trajectory plot — the red/green compare already covers it. Non-ego actors: run Workflow 18 (vehicle classification) automatically — build best-crop grid, view it, classify by color + type, map toAssetPath+ RR namedColor. If prerequisites are missing (no intrinsics / no camera / text-only agent) or confidence is low, fall back toAssetPath="Vehicles/Sedan.fbx",Color="auto". HARD RULE — do NOT passOrientation=when constructingscenariobuilder.Trajectoryfor non-ego actors.actorpropsreturns yaw in compass convention (e.g., ~340° for northbound) which is incompatible with RoadRunner's heading convention; passing it asOrientationproduces actors facing sideways or yaw-flickering across frames. Omit the parameter entirely and let RR auto-derive heading from the waypoint sequence:scenariobuilder.Trajectory(t, wp, Name=tid)— noOrientation, noSpeed. On flat scenes (OSM, OpenDRIVE without elevation), flatten waypoint Z to 0 before constructing the actorTrajectory—actorInfo.Waypoints(:,3)carries sensor-mount offset (~1 m), not road height, so without flattening every actor floats above the road. Keep Z and useadjustHeighton ego only when the rrhd has real terrain elevation (HERE HD, Zenrin, OpenDRIVE+CRG, point-cloud-derived). Seeworkflow-04-roadrunner-export-detail.mdStep 4. Post-export asset replacement (when iterating on classification without re-exporting trajectories):rrAPI = roadrunnerAPI(rrApp); allActors = rrAPI.Scenario.Actors;thenallActors(k).ActorAsset = "<PROJECT>/Assets/Vehicles/Suv.fbx_rrx";— CRITICAL: always use<PROJECT>/Assets/...relative paths, NEVER absolute paths (absolute silently produces white placeholder boxes). Seeosm-flat-scene-gotchas.mdfor common flat-scene pitfalls.Simulate & export video — Disable collision (
setFailCondition("DurationCondition")),setCameraMode(rrApp, "front", FocusActorID=1)(NEVER follow for the final scenario),simulateScenario(rrApp, EnableLogging=true)— HARD RULE: do NOT passPacing=tosimulateScenariofor the final capture;Pacingcaps the logged simulation duration and silently produces a 1-second video instead of the full clip.exportVideo, build the comparison video. All four are mandatory.
Camera-mode rule (do NOT mix up): Step 7 (localization compare) = "follow" on localized ego (keeps red/green both in frame). Step 9 (final scenario) = "front" on ego ID=1 (mirrors the dashcam). Export-order HARD RULE — FocusActorID=1 follows whatever was exported FIRST. RoadRunner assigns ActorID in exportToRoadRunner call order (1st→ID=1, 2nd→ID=2, …). Export the actor the camera should track FIRST: localization compare = Ego_Localized first then Ego_Raw; multi-GPS compare (workflow-04 Step 2.5) = the series whose perspective matters first; final scenario = ego first then non-egos. Inverting silently produces a video that follows the wrong actor — looks like a "video bug" but root cause is export sequence.
Ego localization priority: When the decision matrix above says REQUIRED (OSM + Raw GPS + camera-with-intrinsics) — i.e., the agent downloaded roads from OSM, the user did NOT specify a corrected/RTK GPS series, and cameraData.SensorParameters carries fx, fy, cx, cy + CameraHeight — run Workflow 8 unconditionally, immediately after building the ego trajectory and before any export. Do NOT ask in this case; localization on raw GPS over OSM lanes is the canonical happy path. Skipping Workflow 8 here (e.g., going straight from trajectory(gpsData,...) to exportToRoadRunner without ever calling laneBoundaryDetector / detect / laneData / localizeEgoUsingLanes) is a regression — it produces a scenario where the ego drifts off the lane it was actually in. Common false-skip patterns to avoid:
Building
cameraDatabut never callinglaneBoundaryDetector(...)on it.Treating intrinsics + raw GPS as "good enough" because the GPS plot looks reasonable on a satellite basemap. The OSM lane offset can be 1–3 m even when GPS looks correct at 2-D map zoom.
Confusing this case with the pre-built-scene case (matrix row 4) — pre-built scenes are ASK; OSM + Raw GPS is REQUIRED.
Rule 5: Ensure Timestamps Are in Seconds and All Sensors Are Synchronized
Before any trajectory creation or export: (1) detect raw timestamp scale and convert to seconds, (2) normalize so all sensors share t=0, (3) synchronize to a reference sensor's timeline.
Timestamp scale detection (always convert to seconds BEFORE creating sensor objects):
| Raw value magnitude | Unit | Conversion |
|---|---|---|
| ~1e9 (e.g., 1.557e9) | POSIX seconds | none |
| ~1e12 | milliseconds | /1e3 |
| ~1e15 | microseconds | /1e6 |
| ~1e18 | nanoseconds | /1e9 |
Span check (catch relative timestamps): Some datasets (notably HERE HD / OpenDRIVE exports) store timestamps in microseconds-since-trip-start, so the first sample is small but the span is huge. After the magnitude check above, also guard on span: if ts(end) - ts(1) > 1e6, ts = ts / 1e6; end (microseconds) or > 1e3 for milliseconds. Without the span check you silently get a 23-million-second trajectory. Apply the same conversion to every sensor's raw timestamps before constructing the scenariobuilder.* objects.
normalizeTimestamps pitfall: convertTimestamps(obj, "numeric") may internally normalize some sensors but not others. Check whether timestamps already start near 0 before calling normalizeTimestamps with a shared reference. If one is normalized and another still has POSIX values, normalize each independently first, then synchronize. Start from the earliest sensor: normalizeTimestamps(sensorB, timeRef) errors with "Time origin must be less than or equal to the minimum timestamp" if sensorB starts before the reference sensor. Always call normalizeTimestamps first on the sensor with the smallest Timestamps(1), capture its timeRef, then pass that to all others.
For the full conversion + sync code block, see references/execution-rules-detail.md (Rule 5 section).
Rule 6: Explain Expected Data Format Before Importing
Before importing any sensor data, always tell the user the exact format/structure their data must be in. Inspect the user's data first (load it, check variable names, sizes, types), then present a sensor-format mapping table: Sensor | Required Fields | Expected Format | Your Data | Raw Magnitude / Scale. Fill in what you detect and flag mismatches or required transformations (e.g., "timestamps are POSIX — will convert", "positions are 2D — will add z=0").
HARD RULE — Raw Magnitude column is mandatory. Run max(rawTs) (and span rawTs(end)-rawTs(1) if the first sample is small) BEFORE constructing any scenariobuilder.* object; write the magnitude AND Rule 5 scale factor (/1, /1e3, /1e6, /1e9) into the table. A blank magnitude cell is not acceptable — Polysync (~1.46e15 µs) silently builds million-second trajectories that only fail at simulateScenario. Full table template + known-dataset quick reference (Pandaset, Polysync, nuScenes, KITTI, HERE HD) in references/execution-rules-detail.md (Rule 6 section).
Field name case sensitivity: MATLAB table variable names are case-sensitive. Datasets vary (timestamp vs timeStamp, TrackID vs trackID). Always disp(data.X.Properties.VariableNames') before accessing any field.
Y-axis convention (CAN/OEM data — HARD RULE): OEM radar (MRR_*) and camera AEB (IaEBA_*) signals use +Y = right. MATLAB/SAE/ISO use +Y = left. ALWAYS negate Y from CAN object sources: posY = -rawLatDist. Without negation actors on the left appear on the right — silent, no error. Add a "Coordinate flip needed?" column to the format table when CAN data is detected.
Rule 7: Generate and Run a MATLAB Script
Always generate a well-documented .m script and run it (via run_matlab_file or evaluate_matlab_code) — do not execute snippets piecemeal. Include %% section headers, comments, and a header block stating purpose, inputs, and outputs. Always set a dataDir variable for video outputs and clean up rrApp before re-launching. For the standard header template, see references/execution-rules-detail.md (Rule 7 section).
Rule 8: Ask User for RoadRunner Paths
Always ask the user for the RoadRunner installation folder (containing AppRoadRunner.exe) and project folder. Auto-discovery is unreliable. If you already have the paths from earlier in this conversation, reuse without re-asking. Single-instance guard: never call roadrunner(...) unconditionally — always check first:
if ~exist('rrApp', 'var') || ~isvalid(rrApp)
rrApp = roadrunner(rrProjectPath, InstallationFolder=rrAppPath);
endThis prevents opening multiple RoadRunner windows when the script is re-run or when Part 2 follows Part 1 in the same MATLAB session.
Multi-script pipeline pitfall: the default pipeline shape is single-script — every reference template opens RoadRunner once and keeps it alive through export. If you split a pipeline across scripts to checkpoint between user-gated pauses, do NOT start each part with clear; clc; then roadrunner(...) — clear wipes the handle but not the GUI process, so each part spawns a new RoadRunner window. Either keep one script with inline uiwait/questdlg pauses, or close(rrApp); clear rrApp at the end of each part and skip the outright clear at the top of the next. Full patterns in references/execution-rules-detail.md (Rule 8 — Multi-Script Pipeline Pitfall).
Clean-slate before export: After the guard, always call newScenario(rrApp) before exporting trajectories — this ensures a previous run's actors/scenario don't accumulate. When loading a pre-built scene (openScene), call newScenario(rrApp) after the scene is loaded (scene stays, scenario clears). When importing an HD map (importScene), the import itself opens a new scene so no explicit newScenario is needed before import — but call it after import and before exportToRoadRunner.
Rule 9: Create Your Own Helper Functions When Needed
Do not rely on pre-existing helper functions (e.g., helperSmoothWaypoints, helperPlotActors). Write helpers as local functions or separate .m files alongside the script.
Rule 10: Always Use scenariobuilder.* APIs for Timestamped Sensor Data
Read the matching reference file in references/ BEFORE the first call. The references/*-api.md and workflow-*.md files have the exact constructor signatures. Do NOT guess from memory — scenariobuilder.ActorTrackData(timestamps, trackID, position) has 3 positional args (no ClassID), and similar pitfalls exist for the other constructors. Open the reference once at the top of the session, then proceed.
Whenever the user's data has timestamps + a sensor type, use the corresponding scenariobuilder.* object:
| User has | Use |
|---|---|
| Timestamps + lat/lon/GPS/GNSS | scenariobuilder.GPSData |
| Timestamps + camera images/video | scenariobuilder.CameraData |
| Timestamps + lidar point clouds | scenariobuilder.LidarData |
| Timestamps + actor/object tracks | scenariobuilder.ActorTrackData |
| Timestamps + waypoints (local XY) | scenariobuilder.Trajectory |
Whenever the user mentions these operations, use the built-in methods:
| User says | Use |
|---|---|
| Synchronize, sync, align sensors, multi-sensor alignment | synchronize(sensorObj, referenceSensor) |
| Crop, trim, cut, extract segment | crop(sensorObj, startTime, endTime) |
| Offset correction, time offset, shift timestamps | normalizeTimestamps(sensorObj) or normalizeTimestamps(sensorObj, timeRef) |
| Normalize, common time base, t=0 reference | normalizeTimestamps(sensorObj) |
| Convert timestamps, datetime to numeric | convertTimestamps(sensorObj, "numeric") |
| Visualize, plot, play, replay | plot(sensorObj) or play(sensorObj) |
Do NOT implement these manually (e.g., interp1 for sync, manual indexing for crop). The scenariobuilder.* objects handle edge cases that manual code often gets wrong. OpenStreetMap roads: always use getMapROI for the bounding box, URL, and georeference — never manually construct OSM API URLs. Standard OSM import pattern in references/execution-rules-detail.md (Rule 11).
Rule 11: Preferred APIs — Use / Don't Use
NEVER use these functions:
| Don't Use | Use Instead | Reason |
|---|---|---|
roadrunnerLaneInfo | laneBoundarySegment → laneBoundaryGroup → getLanesInRoadRunnerHDMap | Struct-heavy I/O, error-prone |
actorTracklist | scenariobuilder.ActorTrackData | Legacy — no sync/crop/normalize/viz support |
waypointTrajectory vs scenariobuilder.Trajectory: For scenario building, always use scenariobuilder.Trajectory. waypointTrajectory is a Sensor Fusion System object for IMU simulation — NOT for scenario reconstruction.
Preferred pipelines (full code in references/execution-rules-detail.md):
Programmatic roads:
laneBoundarySegment→laneBoundaryGroup→ (optionalsmoothBoundaries) →getLanesInRoadRunnerHDMap→write(rrMap, filePath).Static objects: Always
roadrunnerStaticObjectInfo. Never manually buildroadrunner.hdmap.StaticObject/Signstructs. Seeworkflow-09-static-objects.md.GPS/GNSS data:
GPSData→trajectory(...)→smooth(...)→exportToDrivingScenarioorexportToRoadRunner. Never combinelatlon2local+vehicle()+trajectory()on adrivingScenario.Legacy migration: Convert
actorTracklist/objectTrackviaimportFromObjectTrack()on anActorTrackData.
Rule 12: Minimum-Change Reruns
When fixing a defect, change ONLY what the defect requires. Do not rebuild the scene/scenario/actors when only a color, orientation, camera mode, or a scene-flag changed. Each unnecessary rerun wastes 30-60s and risks file-lock issues. Before re-running anything, write a one-line plan: "the defect is X; the minimum change is Y." For the matrix of defect → minimum change, see references/execution-rules-detail.md (Rule 12 section).
Quick Reference — Key APIs
| API | Purpose |
|---|---|
| Sensor Data Objects | |
scenariobuilder.GPSData | Store GPS coordinates with timestamps |
scenariobuilder.ActorTrackData | Store actor track detections with timestamps |
scenariobuilder.CameraData | Store camera image data with timestamps |
scenariobuilder.LidarData | Store lidar point cloud data with timestamps |
scenariobuilder.Trajectory | Create trajectory from timestamps + waypoints |
recordedSensorData | Convenience factory to create any sensor data object |
drivingLogAnalyzer | Launch the interactive app for sensor data analysis |
| Trajectory & Actor Processing | |
actorprops | Extract actor properties and trajectories from track data |
importFromObjectTrack | Import objectTrack array (sensor fusion) into ActorTrackData |
writeCSV | Export trajectory to CSV (world-coord waypoints + optional orientation) |
selectActorRoads | Filter roads in an actor's path from roadprops output |
| Road Network & Map | |
getMapROI | Compute geographic bounding box from GPS for OSM download |
roadprops | Extract road properties and geo-reference from OSM/OpenDRIVE file |
updateLaneSpec | Update lane widths/markings using lane detections |
| Lane Detection & Localization | |
laneBoundaryDetector | Detect lane boundaries. Constructor: laneBoundaryDetector(Model="RVLD") — RVLD is preferred. Fall back to laneBoundaryDetector() (CLRNet default) only if the RVLD constructor errors. WRONG: checking which("rvldLaneDetector") or exist("rvldLaneDetector") — no such function exists. RVLD is a model option on laneBoundaryDetector, not a separate class. Also fall back to CLRNet if tracker output has boundaries on only one side of ego (all same-sign lateral offsets). |
laneBoundaryTracker | Track lane boundaries across frames |
laneData | Store recorded lane boundary data with timestamps |
egoToWorldLaneBoundarySegments | Convert tracked ego-frame lane boundaries → world-frame laneBoundarySegment |
laneBoundarySegment | Store lane boundary info for a road segment (world coords) |
laneBoundaryGroup | Connect multiple lane boundary segments |
getLanesInRoadRunnerHDMap | Create RoadRunner HD Map from lane boundary objects |
localizeEgoUsingLanes | Snap ego trajectory to lane center using lane detections |
| Static Objects & 3D Assets | |
roadrunnerStaticObjectInfo | Convert cuboid detections to RR HD Map static objects/signs |
imageAssetGenerator | Generate 3D mesh assets from a single camera image (TripoSR) |
| Point Cloud & Elevation | |
egoPointCloudExtractor | Extract local point clouds along ego trajectory |
addElevation | Add elevation to roadrunnerHDMap, 2D lane points, or cuboids |
roadSurface | Extract high-res road surface from lidar → OpenCRG, elevation, mesh, boundaries |
| Export | |
exportToDrivingScenario | Export trajectory + road network → drivingScenario |
exportToRoadRunner | Export trajectory + scene directly to RoadRunner |
exportScenario(rrApp,file,"OpenSCENARIO XML") | Export RR scenario to ASAM OpenSCENARIO XML |
exportScene(rrApp,file,"OpenDRIVE") | Export RR scene to ASAM OpenDRIVE |
openScenarioExportOptions | Configure OpenSCENARIO export (version, OpenDRIVE pairing, catalogs) |
openDriveExportOptions | Configure OpenDRIVE export (version, signals, objects, CRG) |
exportToASAMOpenCRG | Export roadSurface to ASAM OpenCRG .crg file |
enableOverlapGroupsOptions | Hard default IsEnabled=false for every importScene(rrApp, ..., "RoadRunner HD Map", ...) call. Flip to true and re-import (asking the user) ONLY if junctions/overpasses look wrong. |
Workflow 1 — Import Raw Sensor Data
Use recordedSensorData (factory) or construct directly: scenariobuilder.GPSData(timestamps, lat, lon, alt), scenariobuilder.ActorTrackData(timestamps, trackID, position, ...), scenariobuilder.CameraData(camTimestamps, imageFileNames, ...), scenariobuilder.LidarData(lidarTimestamps, pcdFiles, ...). For per-schema field-mapping see the matlab-import-driving-data skill. No wrapper exists for radar or IMU — do NOT fabricate scenariobuilder.RadarData / scenariobuilder.IMUData.
Workflow 2 — Build GPS Data and Extract Trajectory
altitude = zeros(size(latitude)); % OSM has no elevation — zero it
gpsData = scenariobuilder.GPSData(timestamps, latitude, longitude, altitude);
[roadProperties, localOrigin] = roadprops(OpenStreetMap="drive_map.osm");
egoTrajectory = trajectory(gpsData, "LocalOrigin", localOrigin);
smooth(egoTrajectory); % remove GPS noiseWorkflow 3 — Import Actor Track Data and Create Trajectories
%% Step 1: Create ActorTrackData
trackData = scenariobuilder.ActorTrackData(timestamps, trackID, position, ...
Category=category, Dimension=dimension, Orientation=orientation);
%% Step 2: Create ego trajectory (needed as reference)
egoTrajectory = trajectory(gpsData, "LocalOrigin", localOrigin);
smooth(egoTrajectory);
%% Step 3: Extract actor properties using actorprops
actorInfo = actorprops(trackData, egoTrajectory); % basic
actorInfo = actorprops(trackData, egoTrajectory, AgeThreshold=20); % filter short tracks
actorInfo = actorprops(trackData, egoTrajectory, SaveAs="none"); % in-memory onlyactorprops returns a table with Age, TrackID, ClassID, EntryTime, ExitTime, Mesh, Time, Waypoints, Speed, Roll, Pitch, Yaw, IsStationary.
Lightweight off-ramp: world-coordinate trajectories as CSV
If the user just wants trajectories in world coords (no RoadRunner, no drivingScenario), use actorprops → Trajectory → writeCSV(traj, FileName="ego_trajectory.csv", IncludeOrientation=true).
Step 4: Ask User About Actor Track Smoothness
After generating actor trajectories, always ask:
"Are the actor trajectories smooth and consistent? If tracks appear jittery or IDs are not persistent, I can run a JPDA smoother (
smootherJIPDA) on the raw detections for better track quality."
If the user reports jittery/noisy tracks, apply offline smoothing using trackerJPDA + smootherJIPDA, then importFromObjectTrack into a fresh ActorTrackData. Full pattern in workflow-14-sensor-fusion-tracking.md.
Workflow 4 — Export Trajectories to RoadRunner (minimal inline)
This is the minimal happy-path pattern. For the full workflow (4 scene-source options including OpenStreetMap with actor-aware buffer, HERE HD Live Map, OpenDRIVE; stale-cosim-fix; advanced simulation control; side-by-side comparison video composition; OpenSCENARIO/OpenDRIVE export), load workflow-04-roadrunner-export-detail.md.
%% Step 1: Connect to RoadRunner (single-instance guard — Rule 8)
if ~exist('rrApp', 'var') || ~isvalid(rrApp)
rrApp = roadrunner(rrProjectPath, InstallationFolder=rrAppPath);
end
%% Step 2: Get scene from OSM (most common path) — full version in reference
mapROI = getMapROI(gpsData.Latitude, gpsData.Longitude, Extent=buffer);
osmFile = fullfile(tempdir, "drive_map.osm");
websave(osmFile, mapROI.osmUrl, weboptions(ContentType="xml"));
[roadProperties, localOrigin] = roadprops(OpenStreetMap=osmFile);
egoTrajectory = trajectory(gpsData, "LocalOrigin", localOrigin);
smooth(egoTrajectory);
scenario = exportToDrivingScenario(egoTrajectory, ...
RoadNetworkSource="OpenStreetMap", FileName=osmFile, Name="Ego");
rrMap = getRoadRunnerHDMap(scenario);
%% Step 3: Import HD Map into RoadRunner — overlap groups DISABLED by default
% (user can re-enable later if junctions/overpasses look wrong — see workflow-04)
rrhdFile = fullfile(tempdir, "roads.rrhd");
write(rrMap, rrhdFile);
copyfile(rrhdFile, fullfile(rrProjectPath, "Assets", "RoadFromOSM.rrhd"));
oOpts = enableOverlapGroupsOptions(IsEnabled=false);
bOpts = roadrunnerHDMapBuildOptions(EnableOverlapGroupsOptions=oOpts);
iOpts = roadrunnerHDMapImportOptions(BuildOptions=bOpts);
importScene(rrApp, "RoadFromOSM.rrhd", "RoadRunner HD Map", ImportOptions=iOpts);
tempSceneFile = fullfile(tempdir, "osm_roads_temp.rrscene");
saveScene(rrApp, tempSceneFile);
%% Step 4: Clean slate + export ego (with the saved scene)
newScenario(rrApp);
exportToRoadRunner(egoTrajectory, rrApp, ...
RoadRunnerScene=tempSceneFile, Name="Ego", SetupSimulation=true);
%% Step 5: Classify vehicles (Workflow 18 — automatic when intrinsics exist)
% Builds best-crop grid, agent views + classifies, maps to AssetPath + Color.
% See references/workflow-18-vehicle-classification.md for full pipeline.
% Self-gating: skipped if no intrinsics/CameraHeight or agent can't view images.
classificationMap = containers.Map(); % populated by agent vision pass
%% Step 6: Export non-ego actors with classified asset types
for i = 1:height(actorInfo)
actorTraj = scenariobuilder.Trajectory( ...
actorInfo.Time{i}, actorInfo.Waypoints{i}, Name=actorInfo.TrackID(i));
smooth(actorTraj);
tid = char(actorInfo.TrackID(i));
if isKey(classificationMap, tid)
cls = classificationMap(tid);
assetPath = cls.AssetPath; rrColor = cls.Color;
else
assetPath = "Vehicles/Sedan.fbx"; rrColor = "auto";
end
exportToRoadRunner(actorTraj, rrApp, ...
Name=actorInfo.TrackID(i), AssetPath=assetPath, ...
Color=rrColor, SetupSimulation=false);
end
%% Step 6: Disable collision fail condition + simulate (see Rule 11 snippet)
%% Step 7: exportVideo + side-by-side comparison — see workflow-04 referenceexportToRoadRunner name-value args (full list in reference): Name, Color, AssetPath, SetupSimulation, RoadRunnerScene.
Comparison video requirements: insertText timestamp burn on BOTH halves, questdlg popup after close(vw), defensive field-name inspect on input struct. Full pattern in workflow-04-roadrunner-export-detail.md.
Workflow 5 — handled in matlab-import-driving-data
DLA (drivingLogAnalyzer) routing for "visualize / inspect / explore / analyze this dataset" → matlab-import-driving-data.
Workflow 6 — Preprocess, Synchronize, Crop, and Correct Offset
Use normalizeTimestamps, crop, synchronize, convertTimestamps on the scenariobuilder data objects. Route complex preprocessing to matlab-import-driving-data.
Workflows 7–17, Object APIs, End-to-End Examples
Workflows 7–18 are reference-only — see the Workflow Catalog above for triggers and references/workflow-NN-<slug>.md. Workflow 15 (DSD) is opt-in. Workflows 16–17 cover scene augmentation. Workflow 18 (vehicle classification) runs automatically inside Workflow 4 Step 4 when the prerequisites are met.
Workflow 15 (DSD) actor safeguards — NEVER use smoothTrajectory on recorded actor waypoints. Use trajectory(veh, wpu, spdu) instead. smoothTrajectory's accumulateDistance is brittle on sparse, near-stationary, or noisy paths and throws "Unable to create smooth trajectory." Also: dedup consecutive identical (x,y) waypoints, compute per-waypoint speed via finite differences, clamp speed to max(spdu, 0.1) (zero stalls DSD), and flatten actor Z to 0 on OSM scenes. Full pattern in workflow-15-driving-scenario-designer.md.
Object APIs (load on demand): trajectory-api.md, actortrackdata-api.md, gpsdata-api.md. For raw-dataset wrapping → matlab-import-driving-data.
End-to-end examples (read one before writing scripts): example-roadrunner-scenario.md (direct RR export, Pandaset) · example-scenario-from-gps-and-tracks.md (OSM roads + getMapROI + roadprops).
Copyright 2026 The MathWorks, Inc.
Files
- ATTRIBUTION.mdAdded by AutoGineer1.0 KiB
- LICENSELicence file added by AutoGineer1.4 KiB
- SKILL.md56.6 KiB
- manifest.yaml704 B
- references/actortrackdata-api.md3.3 KiB
- references/example-roadrunner-scenario.md4.3 KiB
- references/example-scenario-from-gps-and-tracks.md9.6 KiB
- references/execution-rules-detail.md16.7 KiB
- references/gpsdata-api.md2.4 KiB
- references/osm-flat-scene-gotchas.md8.8 KiB
- references/per-frame-detections-to-actortrackdata.md7.8 KiB
- references/python-spkg-install-troubleshooting.md5.8 KiB
- references/trajectory-api.md13.7 KiB
- references/visualization-patterns.md12.4 KiB
- references/workflow-04-roadrunner-export-detail.md32.6 KiB
- references/workflow-07-height-correction.md3.3 KiB
- references/workflow-08-lane-localization.md39.1 KiB
- references/workflow-09-static-objects.md9.3 KiB
- references/workflow-10-road-surface-opencrg.md4.7 KiB
- references/workflow-11-point-cloud-georef.md4.5 KiB
- references/workflow-12-3d-asset-generation.md2.0 KiB
- references/workflow-13-event-extraction.md5.3 KiB
- references/workflow-14-sensor-fusion-tracking.md6.9 KiB
- references/workflow-15-driving-scenario-designer.md7.6 KiB
- references/workflow-16-aerial-lidar-augmentation.md14.4 KiB
- references/workflow-17-traffic-signs-from-sensor-data.md12.5 KiB
- references/workflow-18-vehicle-classification.md12.8 KiB
- references/workflow-19-ego-lane-inference.md10.0 KiB
- scripts/buildBestCropGrid.m5.2 KiB
- scripts/openFile.m716 B
- scripts/plotActorCircles.m3.5 KiB
- scripts/plotBEVAndCamera.m4.2 KiB
references/workflow-08-lane-localization.md
name: workflow-08-lane-localization description: Snap ego trajectory onto correct lane center using lane boundary detections — simplified path with monoCamera detect, advanced manual pixel-filtering path, GPS-vs-localized verification in RoadRunner, and final scenario generation in the same session. Loaded when user has camera intrinsics and needs lane-level positioning.
Workflow 8 — Localize Ego Trajectory on Map Using Lane Detections
Parent skill:
SKILL.md— Loaded when GPS accuracy is insufficient for lane-level positioning AND camera intrinsics are available.Related references:
workflow-04-roadrunner-export-detail.mdfor the upstream OSM/HD-map setup. The localization step replaces direct GPS export.Official MathWorks docs:
Lane extraction: https://www.mathworks.com/help/driving/ug/extract-lane-information-from-recorded-camera-data-for-scene-generation.html
localizeEgoUsingLanesreference: https://www.mathworks.com/help/driving/ref/localizeegousinglanes.html
When GPS accuracy is insufficient for lane-level positioning, use localizeEgoUsingLanes to snap the ego trajectory onto the correct lane center using lane boundary detections.
IMPORTANT — Localization is OPTIONAL (ask before running)
Do NOT auto-run this workflow. Before starting lane detection, ask the user:
"Do you want to apply lane localization (snaps the trajectory to the lane center using camera detections), or use the GPS trajectory directly?"
Skip the question and localize by default only when:
User explicitly says "localize", "snap to lane", "lane-level accuracy"
User says "build scenario" with no pre-built scene (OSM-only — GPS typically needs help)
Always ask when:
Dataset has a pre-built scene (HERE HD, vendor
.rrscene) — GPS may already be accurate enoughUser chose "Corrected GPS" — implies they trust the correction
Dataset explicitly has RTK or post-processed GNSS
Why: Choosing Corrected GPS implies the user trusts it. Running localization on top without asking is wasted time and can even degrade the trajectory if the map's lane geometry doesn't perfectly match.
Canonical signature (R2026a)
% Four positional arguments — order matters
[localizedTrajectory, info] = localizeEgoUsingLanes( ...
trajectory, rrMap, lanedata, startLaneIndex);
% Optional: ApproxLaneWidth (default 3.5 m)
[localizedTrajectory, info] = localizeEgoUsingLanes( ...
trajectory, rrMap, lanedata, startLaneIndex, ApproxLaneWidth=3.5);trajectory—scenariobuilder.Trajectory(ego from GPS)rrMap—roadrunnerHDMapobject (preferred) orrrApp(roadrunnerobject)lanedata—laneDataobject built from tracked lane boundaries (see Step 3 below)startLaneIndex— positive integer (left-to-right, excluding shoulders). Must be scalar. Always ask the user —egoLaneIndexis detection-only and routinely undercounts on multi-lane roads.
DO NOT use egoToWorldLaneBoundarySegments for localization
egoToWorldLaneBoundarySegments converts tracked boundaries to world-coordinate segments for static lane-geometry reconstruction (feeds into laneBoundaryGroup for HD-map building). It is not part of the ego-localization path. Pass the laneData object directly into localizeEgoUsingLanes — never go through egoToWorldLaneBoundarySegments first.
Prerequisites
A
scenariobuilder.Trajectoryobject (from GPS)A
roadrunnerHDMapobject or connectedrrAppwith a loaded sceneA
laneDataobject with tracked lane boundaries (from lane detection +laneBoundaryTracker)The starting lane index of the ego vehicle (left-to-right numbering, excluding shoulders)
Scene must have been imported with
enableOverlapGroupsOptions(IsEnabled=false)— the upstreamimportScene(rrApp, ..., "RoadRunner HD Map", ImportOptions=iOpts)in workflow-04 MUST pass this option block. If you launch this workflow with a freshly built scene, build it with overlap groups disabled. The defaultIsEnabled=trueproduces false-positive overlap groups at junctions/overpasses on OSM-derived maps — flip totrueonly if the user inspects the imported scene and reports junction/overpass artifacts.Variable-lane roads are handled automatically —
localizeEgoUsingLanesinterpolates through frames where the map has fewer lanes thanstartLaneIdx. No pre-emptive crop/split logic needed.
Map lane-count gate (silent — run BEFORE localizeEgoUsingLanes)
localizeEgoUsingLanes needs startLaneIdx ≤ availableLanes at every frame along the route. This gate is silent by default — do not generate plots, tables, or printed summaries for the user. It runs internally and only surfaces a question if the map actually has a coverage problem or the user's chosen startLaneIdx exceeds the map's minimum lane count. Generating a lane_count_vs_frame.png and a LaneCount/Frames/PercentOfRoute summary up-front clutters the chat and biases the user's answer to the next gate (which lane is the ego in?).
Note: roadrunnerHDMap does not expose findLanes. Use a proximity loop on rrMap.Lanes(li).Geometry instead.
%% Silent lane-count probe — used internally, NOT shown to the user up-front
ego2D = egoTrajectory.Position(:,1:2);
radius = 5;
laneXY = arrayfun(@(L) L.Geometry(:,1:2), rrMap.Lanes, UniformOutput=false);
laneCount = zeros(size(ego2D,1),1);
for k = 1:size(ego2D,1)
p = ego2D(k,:);
cnt = 0;
for li = 1:numel(laneXY)
d = vecnorm(laneXY{li} - p, 2, 2);
if any(d < radius), cnt = cnt + 1; end
end
laneCount(k) = cnt;
end
maxValidStartLaneIdx = min(laneCount); % keep — used after the user answersNo variable-lane gate question. After the user provides startLaneIdx, just call localizeEgoUsingLanes — it interpolates through frames where the map has fewer lanes. Do NOT pre-emptively ask "the map narrows to K lanes, want to crop or split?" The user already chose their lane from the dashcam; the localizer handles it.
Only surface a problem if:
min(laneCount) < 1→ the map has zero lane coverage for some frames. Tell the user: "OpenStreetMap doesn't have road geometry for some frames — localization may not work. Want to fall back to raw GPS?"localizeEgoUsingLanesactually throws an error — then report the error, include lane-count diagnostics, and offer the user: (a) crop/split, (b) try a differentstartLaneIdx, or (c) skip localization and use GPS directly.
NEVER silently skip localization. If it fails, always tell the user what happened and ask how to proceed. Proceeding with raw GPS without informing the user is a failure mode.
The crop recipe below is kept for reference in case the localizer does error on a specific dataset:
Crop recipe — Trajectory + laneData to a frame window
scenariobuilder.Trajectory has a crop(traj, tStart, tEnd) method, but laneData does not. Rebuild it.
% Inputs from the gate above:
% cropStartFrame, cropEndFrame — frame indices into trackedLanes
tStart = trackedLanes.TimeStamp(cropStartFrame); % singular .TimeStamp
tEnd = trackedLanes.TimeStamp(cropEndFrame);
% Crop ego (Trajectory has crop)
egoTrajectory = crop(egoTrajectory, tStart, tEnd);
% Rebuild laneData on the cropped window (laneData has NO crop method)
trackedLanes = laneData( ...
trackedLanes.TimeStamp(cropStartFrame:cropEndFrame), ...
trackedLanes.LaneBoundaryData(cropStartFrame:cropEndFrame), ...
TrackIDs=trackedLanes.TrackIDs(cropStartFrame:cropEndFrame));
% Re-zero both timelines so they share t=0 again
normalizeTimestamps(egoTrajectory); egoTrajectory.TimeOrigin = 0;
trackedLanes.updateTime(trackedLanes.TimeStamp - tStart);
% Crop the camera arrays the same way before composing the compare video.Why this gate matters: without it, the agent runs detect → track → laneData → localize, the localizer crashes, the user has no diagnostic, and you waste a 5-15 min lane-detection pass per retry. The probe takes <1 s.
Recommended path (mirrors the official MathWorks example)
Follow the same five stages as the doc "Extract Lane Information from Recorded Camera Data for Scene Generation": build monoCamera sensor → detect in image coords → imageToVehicle to get vehicle-coord points → findParabolicLaneBoundaries to fit parabolas → laneBoundaryTracker to track over time, then build laneData and call localizeEgoUsingLanes.
HARD RULE —
imageToVehicleREQUIRES amonoCameraobject, NOT a barecameraIntrinsics. PassingcameraIntrinsics(the object you build fromfx, fy, cx, cy, imageSize) directly intoimageToVehicleis the most common failure mode in this workflow.imageToVehicleneeds the camera-to-vehicle geometry (mounting Height + Pitch + Yaw + Roll) that lives onmonoCamera, not on the bare intrinsics. BuildcameraParams = monoCamera(intrinsics, height, Pitch=..., Yaw=..., Roll=...)BEFORE Step 1 and reuse it everywhereimageToVehicleis called. SamemonoCamerais what you assemble into the 4-fieldcameraData.SensorParametersstruct (MountingLocation/MountingAngles/Intrinsics/EgoOriginHeight) for DLA-style overlays — seesensor-import-api. SkippingimageToVehicle+findParabolicLaneBoundariesand trying to feed pixel coordinates straight intolaneBoundaryTrackerALSO fails — the tracker expectsparabolicLaneBoundaryrows in vehicle coordinates.
%% Step 0 — Build the monoCamera sensor (MUST come before Step 2 imageToVehicle)
% Use the dataset's calibration: intrinsics from K (and D if available),
% mounting Height from CameraHeight (m, ground → camera optical center),
% mounting angles from the calibration's R/Euler. Do NOT fabricate.
intrinsics = cameraIntrinsics([fx fy], [cx cy], [imgH imgW]); % from dataset
camHeight = CameraHeight; % m, from dataset
cameraParams = monoCamera(intrinsics, camHeight, ...
Pitch=pitchDeg, Yaw=yawDeg, Roll=rollDeg); % from dataset, deg
% If only Height is in the dataset, default Pitch/Yaw/Roll to 0 and tell
% the user — same disclosure rule as in sensor-import-api.md.
%% Step 1 — Detect lane boundary points in image coordinates
% RVLD first (preferred). Two fallback conditions trigger CLRNet:
% (a) RVLD constructor throws — model not installed.
% (b) After tracking, all LaneBoundary lateral offsets share one sign —
% i.e., RVLD only saw boundaries on ONE side of the ego. CLRNet
% typically returns boundaries on both sides on the same data.
% (Detected post-tracking — see Step 4 sanity check below.)
try
detector = laneBoundaryDetector(Model="RVLD");
catch ME
warning("RVLD not installed (%s). Falling back to CLRNet.", ME.identifier);
detector = laneBoundaryDetector(); % CLRNet default
end
laneBoundaryPoints = detect(detector, cameraData, ...
DetectionThreshold=0.3, OverlapThreshold=0.1, ShowProgress=true);
% laneBoundaryPoints: cell-of-cell — outer = frames, inner = Nx2 pixel coords per boundary
%% Step 2 — Convert image → vehicle coordinates (imageToVehicle)
% MANDATORY: filter out-of-bounds pixels first. RVLD/CLRNet sometimes
% produce points at or above the horizon (y <= cy) and outside [1..W, 1..H].
% imageToVehicle errors with "Locations specified in imagePoints must be
% inside image bounds" if you pass these through unfiltered.
imageSize = cameraParams.Intrinsics.ImageSize; % [H W]
H = imageSize(1); W = imageSize(2);
cy = cameraParams.Intrinsics.PrincipalPoint(2);
laneBoundaryPointsVehicle = cell(numel(laneBoundaryPoints), 1);
for i = 1:numel(laneBoundaryPoints)
cellIn = laneBoundaryPoints{i};
cellOut = cell(1, numel(cellIn));
for j = 1:numel(cellIn)
pts = cellIn{j};
if isempty(pts), cellOut{j} = []; continue; end
validMask = pts(:,2) > (cy + 1) & ...
pts(:,1) >= 1 & pts(:,1) <= W & ...
pts(:,2) >= 1 & pts(:,2) <= H;
validPts = pts(validMask, :);
if size(validPts, 1) < 5
cellOut{j} = [];
else
cellOut{j} = imageToVehicle(cameraParams, validPts);
end
end
laneBoundaryPointsVehicle{i, 1} = cellOut;
end
%% Step 3 — Fit parabolic boundaries (findParabolicLaneBoundaries)
% Do NOT pass ValidateBoundaryFcn=@(b) length(b.Strength)>5 && b.Strength>0.1 —
% the validate fcn is called by RANSAC on raw model parameters (a numeric
% vector), so referencing `b.Strength` errors with "Dot indexing is not
% supported for variables of type double". Defaults are sufficient.
numFrames = numel(laneBoundaryPointsVehicle);
laneBoundaries = cell(numFrames, 1);
for i = 1:numFrames
pts = laneBoundaryPointsVehicle{i};
pts = pts(~cellfun(@isempty, pts));
if isempty(pts), laneBoundaries{i,1} = []; continue; end
boundaries = cellfun(@(x) findParabolicLaneBoundaries(x, 0.3, MaxNumBoundaries=1), ...
pts, UniformOutput=false);
idices = cellfun(@(x) ~isempty(x), boundaries);
boundaries = cellfun(@(x) x(1), boundaries(idices));
laneBoundaries{i, 1} = boundaries;
end
%% Step 4 — Track lane boundaries (laneBoundaryTracker)
% UseSmoother=true gives better results but can fail with
% "Expected StateCovariance to be positive-semidefinite" on long sequences
% or sparse detections. Fall back to UseSmoother=false if it errors.
lbTracker = laneBoundaryTracker( ...
LaneBoundaryModel="Parabolic", ...
DetectionProbability=0.8);
try
trackedLanesRaw = lbTracker(laneBoundaries, cameraData.Timestamps, ...
ShowProgress=true, UseSmoother=true);
catch ME
if contains(ME.message, "positive-semidefinite") || contains(ME.message, "StateCovariance")
warning("Smoother failed (%s). Retrying without smoother.", ME.message);
lbTracker = laneBoundaryTracker(LaneBoundaryModel="Parabolic", DetectionProbability=0.8);
trackedLanesRaw = lbTracker(laneBoundaries, cameraData.Timestamps, ...
ShowProgress=true, UseSmoother=false);
else
rethrow(ME);
end
end
% trackedLanesRaw is a cell-of-cell: outer = frames, inner = tracks. Convert
% to a laneData object (sorted left-to-right by lateral offset) before localizing.
nF = numel(cameraData.Timestamps);
lbDataSorted = cell(nF, 1);
trackIDsStr = cell(nF, 1);
for i = 1:nF
frameTracks = trackedLanesRaw{i};
bounds = []; ids = [];
for j = 1:numel(frameTracks)
bounds = [bounds; frameTracks{j}.LaneBoundary]; %#ok<AGROW>
ids = [ids; frameTracks{j}.TrackID]; %#ok<AGROW>
end
if ~isempty(bounds)
offsets = arrayfun(@(b) b.Parameters(3), bounds);
[~, sortIdx] = sort(offsets, 'descend'); % left -> right
lbDataSorted{i} = bounds(sortIdx)';
trackIDsStr{i} = string(ids(sortIdx))';
else
lbDataSorted{i} = bounds';
trackIDsStr{i} = string([])';
end
end
trackedLanes = laneData(cameraData.Timestamps, lbDataSorted, TrackIDs=trackIDsStr);
%% Step 4b — Single-side sanity check (RVLD → CLRNet fallback)
% localizeEgoUsingLanes needs the ego BRACKETED — boundaries on both sides.
% RVLD on some datasets returns same-sign offsets only (typically all
% positive). The localizer would then error with "ego lane detections
% missing for the frame N" on every frame. CLRNet usually catches the
% other side; re-run detection with CLRNet if RVLD output is single-sided.
allOffsets = [];
for i = 1:numel(lbDataSorted)
if ~isempty(lbDataSorted{i})
allOffsets = [allOffsets; arrayfun(@(b) b.Parameters(3), lbDataSorted{i}(:))]; %#ok<AGROW>
end
end
if ~isempty(allOffsets) && (all(allOffsets >= 0) || all(allOffsets <= 0)) ...
&& detector.Model == "RVLD"
warning("RVLD detected boundaries on a single side of ego only. " + ...
"Re-running with CLRNet.");
detector = laneBoundaryDetector(); % CLRNet default
laneBoundaryPoints = detect(detector, cameraData, ...
DetectionThreshold=0.3, OverlapThreshold=0.1, ShowProgress=true);
% Re-run Steps 2–4 on the new detections (omitted here — see canonical script).
end
%% Step 5 — Get start lane index from the user (DO NOT auto-pick)
% mode(egoLaneIndex(trackedLanes)) is detection-only — it counts the
% boundaries the tracker actually held onto, not the lanes on the road.
% On any road wider than 2 lanes (or where outer boundaries are weakly
% detected), it systematically undercounts and the localizer snaps the
% ego into a parallel lane. Always ask the user.
laneIdxPerFrame = egoLaneIndex(trackedLanes);
validIdx = laneIdxPerFrame(~isnan(laneIdxPerFrame));
if isempty(validIdx)
hintIdx = NaN; hintNum = NaN;
else
[~, numLanesPerFrame] = egoLaneIndex(trackedLanes);
hintIdx = mode(validIdx);
hintNum = max(numLanesPerFrame, [], "omitnan");
fprintf(['Detection-based hint: ego in lane %d of %d tracked boundaries.\n' ...
'NOTE: this counts only what the camera tracked, not the actual\n' ...
'road. Outer boundaries are often missed on >2-lane roads.\n'], ...
hintIdx, hintNum);
end
% startLaneIdx must come from the user — they know how many lanes the
% road actually has and which one the ego is in. The agent must ASK in chat
% (use AskUserQuestion or the equivalent), not infer.
startLaneIdx = <ASK USER — leftmost lane = 1, excluding shoulders>;
%% Step 6 — Localize. Pass laneData directly — NOT egoToWorldLaneBoundarySegments output.
[localizedTrajectory, locInfo] = localizeEgoUsingLanes( ...
egoTrajectory, rrMap, trackedLanes, startLaneIdx);Why ask instead of auto-pick: egoLaneIndex counts tracked boundaries in the camera frame and adds 1 — it has no view of the rrMap or how many lanes the road actually has. When the tracker only holds the two boundaries immediately bracketing the ego (typical on 3+ lane roads with weak outer markings), egoLaneIndex returns 1 every frame and mode(...) cheerfully agrees, even though the ego may be in lane 3 of a 4-lane road. The localizer then snaps the trajectory two lanes to the left and the user sees a multi-lane offset in the compare video. The fastest, most reliable fix is just to ask — the user already knows the answer from the dashcam.
IMPORTANT — Common pitfalls:
imageToVehicleREQUIRES amonoCameraobject, NOT a barecameraIntrinsics. This is the most common failure mode. BuildcameraParams = monoCamera(intrinsics, camHeight, Pitch=..., Yaw=..., Roll=...)and passcameraParamstoimageToVehicle. Passing the barecameraIntrinsicserrors withInvalid argument at position 1becauseimageToVehicleneeds the camera→vehicle geometry (mounting Height + angles) that onlymonoCameracarries.Do NOT skip
imageToVehicle+findParabolicLaneBoundaries— pass detection pixel coords through BOTH before tracking. The tracker expectsparabolicLaneBoundaryrows in vehicle coordinates. Skipping either step (e.g., feeding raw pixel cells directly intolaneBoundaryTracker) errors out at the first frame.Do filter out-of-bounds pixels before
imageToVehicle(mask shown in Step 2). RVLD/CLRNet near-horizon points routinely fall abovecyand outside[1..W,1..H]; the function errors on those.Do NOT pass a custom
ValidateBoundaryFcnthat touchesb.Strength— RANSAC calls it with raw model parameters (a numeric vector), not a struct. Defaults work.Do NOT call
egoToWorldLaneBoundarySegments— that function is for static lane-geometry reconstruction (feedslaneBoundaryGroup), not ego localization. Localization takes thelaneDataobject directly.RVLD first, CLRNet as fallback (two trigger conditions): Always try
laneBoundaryDetector(Model="RVLD")first — RVLD is the preferred model for this workflow. Fall back tolaneBoundaryDetector()(CLRNet default) when either (a) the RVLD constructor throws (not installed) or (b) after tracking, allLaneBoundarylateral offsets share the same sign — meaning RVLD only saw boundaries on ONE side of ego, andlocalizeEgoUsingLaneswould error with "ego lane detections missing" on every frame. CLRNet typically returns boundaries on both sides on the same data. Do NOT default to CLRNet for performance reasons; only swap on (a) or (b).RVLD availability — WRONG way to check: Do NOT use
which("rvldLaneDetector")orexist("rvldLaneDetector")— no such standalone function exists. RVLD is a model option onlaneBoundaryDetector, not a separate class. The ONLY correct check is the try-catch onlaneBoundaryDetector(Model="RVLD"). If you grep for function names and find nothing, that does NOT mean RVLD is unavailable.Flat OSM scene — flatten ego Z + preserve Orientation after localization:
localizedTrajectory.Position(:,3)carries GPS altitude (~40m+). On flat OSM scenes (Z=0 roads), you MUST zero the ego Z before export — otherwise ego floats above the road. When rebuilding the trajectory withZ=0, always passOrientation=localizedTrajectory.Orientationto preserve the lane-aligned heading from localization. Without it, heading is re-derived from waypoint deltas (less accurate at low speed/curves).Python-SPKG install errors (RVLD is Python-backed): if the RVLD constructor or first run fails with a
proxyError,SSL: CERTIFICATE_VERIFY_FAILED,SSLConnectionError, orReadTimeoutError: HTTPSConnectionPool(host='download.pytorch.org', ...), this is a known customer-network issue, not a MATLAB bug. Loadpython-spkg-install-troubleshooting.mdfor the symptom→fix matrix (proxy / SSL → sethttp_proxy+https_proxy; PyTorch read timeout → reinstall the SPKG).Always ASK the user for
startLaneIdx(1 = leftmost lane, excluding shoulders). Never auto-pick.egoLaneIndexis detection-only and undercounts on 3+ lane roads, somode(egoLaneIndex(...))is unreliable as a startLaneIdx source. Compute it as a hint to show the user, but the value passed tolocalizeEgoUsingLanesMUST come from the user.localizeEgoUsingLanesacceptsroadrunnerHDMaporroadrunnerapp — NOT an OSM file path. Do NOT callgetRoadRunnerHDMap(rrApp)— that function does not exist on theroadrunnerclass (it's adrivingScenariomethod only) and errors with "Undefined function for input arguments of type 'roadrunner'". Use one of: (a)rrMap = roadrunnerHDMap(); read(rrMap, rrhdFile);then passrrMap, or (b) passrrAppdirectly as the second argument tolocalizeEgoUsingLanes. Both paths are documented inhelp localizeEgoUsingLanes. For a pre-built.rrscenealready opened inrrApp, useexportScene(rrApp, file, "RoadRunner HD Map")to extract the rrhd, then path (a) — seeworkflow-04-roadrunner-export-detail.mdOption B.1.Warnings about missing frames are normal — the function interpolates through gaps.
HD Map import option is mandatory — every
importScene(rrApp, ..., "RoadRunner HD Map", ...)call in this workflow's upstream MUST passImportOptions=iOptsbuilt withenableOverlapGroupsOptions(IsEnabled=false). Forgetting this is a recurring miss; treat it as a hard default.laneBoundaryTrackerreturns cell-of-structs, NOT a struct array. Each element is a cell:tracked = trackerObj(boundaries, ts)→tracked{k}is a struct with.TrackID,.UpdateTime,.LaneBoundary. Extract with curly braces:tracked{k}.LaneBoundary. Using parenthesestracked(k).LaneBoundaryerrors becausetrackedis a cell, not a struct array.Empty frames need typed empty, not
[]. Passing[](class double) tolaneBoundaryTrackererrors with "Expected input to be one of these types: cell, parabolicLaneBoundary". UseparabolicLaneBoundary.empty(1,0)for frames with no detections:if isempty(allBoundaries{i}) tracked = trackerObj(parabolicLaneBoundary.empty(1,0), ts(i)); else tracked = trackerObj(allBoundaries{i}, ts(i)); endDo NOT name your boundary cell array
laneData— this shadows thelaneDataclass.localizeEgoUsingLanesrequires alaneDataobject (constructed vialaneData(timestamps, lbCell)), not a raw cell. If you shadow the class,clear laneDatais needed before you can construct the object. UselbCellorlaneBoundsPerFramefor the intermediate cell array.localizeEgoUsingLanespasses Z through unchanged — it localizes XY (snaps to lane center) but copiesPosition(:,3)from the input trajectory verbatim. On flat OSM scenes, GPS altitude (~30–50 m) survives into the localized output. Always zero Z after localization for flat scenes (and preserveOrientation— see gotcha above).
Advanced Path (Manual pixel filtering — use when simplified path fails)
Step 1: Run RVLD lane detector on camera data
detector = laneBoundaryDetector(Model="RVLD");
roi = round(cameraIntrinsics.cy) + 50; % Below horizon
[lanePoints, ~] = detect(detector, cameraData, ...
ROI=roi, DetectionThreshold=0.3, OverlapThreshold=0.1, ...
MaxBoundaries=6, ShowProgress=true, ExecutionEnvironment="cpu");
% Convert to vehicle coords (filter out-of-bounds points)
numFrames = numel(lanePoints);
laneBoundaries = cell(numFrames, 1);
for i = 1:numFrames
frameBounds = [];
for j = 1:numel(lanePoints{i})
pts = lanePoints{i}{j};
if isempty(pts), continue; end
validMask = pts(:,2) > (cameraIntrinsics.cy + 1) & ...
pts(:,1) >= 1 & pts(:,1) <= imageSize(2) & ...
pts(:,2) >= 1 & pts(:,2) <= imageSize(1);
validPts = pts(validMask, :);
if size(validPts, 1) < 5, continue; end
vehPts = imageToVehicle(cameraParams, validPts); % monoCamera, NOT cameraIntrinsics
bounds = findParabolicLaneBoundaries(vehPts, 0.3, MaxNumBoundaries=1);
if ~isempty(bounds)
frameBounds = [frameBounds; bounds];
end
end
laneBoundaries{i} = frameBounds;
endStep 2: Track lane boundaries
tracker = laneBoundaryTracker(LaneBoundaryModel="Parabolic");
trackedLanes = tracker(laneBoundaries, timestamps, ShowProgress=true, UseSmoother=true);
% Convert to laneData (sorted left-to-right by lateral offset)
lbDataSorted = cell(numFrames, 1);
trackIDsStr = cell(numFrames, 1);
for i = 1:numFrames
frameTracks = trackedLanes{i};
bounds = []; ids = [];
for j = 1:numel(frameTracks)
bounds = [bounds; frameTracks{j}.LaneBoundary];
ids = [ids; frameTracks{j}.TrackID];
end
if ~isempty(bounds)
offsets = arrayfun(@(b) b.Parameters(3), bounds);
[~, sortIdx] = sort(offsets, 'descend'); % Left-to-right
lbDataSorted{i} = bounds(sortIdx)';
trackIDsStr{i} = string(ids(sortIdx))';
else
lbDataSorted{i} = bounds';
trackIDsStr{i} = string([])';
end
end
laneDetections = laneData(timestamps, lbDataSorted, TrackIDs=trackIDsStr);Step 3: Localize ego trajectory
% Using roadrunnerHDMap object (preferred — no rrApp dependency)
[localizedTrajectory, info] = localizeEgoUsingLanes(egoTrajectory, rrMap, ...
laneDetections, startLaneIndex, ApproxLaneWidth=detectedLaneWidth);
% OR using rrApp (requires scene loaded with at least one actor)
[localizedTrajectory, info] = localizeEgoUsingLanes(egoTrajectory, rrApp, ...
laneDetections, startLaneIndex, ApproxLaneWidth=detectedLaneWidth);Step 4: Visualize comparison (GPS vs Localized) — MANDATORY user gate
This is a hard pause. Immediately after localizeEgoUsingLanes returns:
Print the offset metrics
Export both trajectories to RoadRunner (red=raw GPS, green=localized) and simulate
Export the simulation video, then burn a legend onto the video itself so the user can tell red vs green without external context
Show a
questdlgpopup with the video file path and an "Open video" button — this popup is only for opening the video, NOT for choosing a trajectoryAfter the user has reviewed the video, ask in the agent text channel which trajectory to use (raw GPS or localized) — do not pick automatically
HARD RULE — Export
Ego_LocalizedFIRST. RoadRunner assignsActorIDin the orderexportToRoadRunneris called (1st call → ID=1).setCameraMode(..., "follow", FocusActorID=1)therefore tracks whichever actor was exported first. Inverting the order silently produces a follow-cam locked on the red raw trajectory instead of the green localized one — the symptom looks like a camera-mode bug but the root cause is the export sequence. Same rule applies in workflow-04's multi-GPS compare and SKILL.md Rule 4 final scenario.
% Export both trajectories — Ego_Localized FIRST so it gets ActorID=1.
% (Order matters — follow-cam tracks ID=1.)
newScene(rrApp);
exportToRoadRunner(localizedTrajectory, rrApp, ...
RoadRunnerScene=tempSceneFile, Name="Ego_Localized", ...
Color="green", SetupSimulation=true);
exportToRoadRunner(egoTrajectory, rrApp, ...
Name="Ego_GPS_Raw", Color="red", SetupSimulation=false);
% MANDATORY: Delete stale sim object from SetupSimulation=true, then set fail condition
rrSim = createSimulation(rrApp); delete(rrSim); clear rrSim;
rootPhase = roadrunnerAPI(rrApp).Scenario.PhaseLogic.RootPhase;
failCond = rootPhase.setFailCondition("DurationCondition");
failCond.Duration = localizedTrajectory.Duration + 10;
% FOLLOW view focused on the green (localized) ego — keeps both ego cars in
% frame and shows the perspective the user actually drove. NEVER use orbit
% for the localization comparison: it loses the on-road context the user needs.
setCameraMode(rrApp, "follow", FocusActorID=1); % FocusActorID=1 → Ego_Localized
simulateScenario(rrApp, EnableLogging=true);
% Export simulation video (FileName WITHOUT extension)
locVideoBase = "ego_localization_compare";
exportVideo(rrApp, VideoFolder=videoDir, FileName=locVideoBase, VideoResolution="HD");
locVideoSrc = fullfile(videoDir, locVideoBase + ".mp4");
if ~isfile(locVideoSrc)
locVideoSrc = fullfile(videoDir, locVideoBase + ".avi");
end
% MANDATORY: side-by-side composite — RAW CAMERA on the LEFT, RR follow view on the RIGHT.
% The raw camera anchors the user to where the ego was in the original recording so
% they can judge whether the green trajectory matches reality.
locVideoOut = fullfile(videoDir, "ego_localization_compare_labeled.mp4");
% Build the raw-camera-as-video on the fly (cameraData is the scenariobuilder.CameraData)
rawVideoTmp = fullfile(tempdir, "raw_camera_for_localization.mp4");
camPaths = cameraData.Frames; % string array of full paths — NOT `.Filenames`
ts = cameraData.Timestamps;
camWriter = VideoWriter(rawVideoTmp, "MPEG-4");
camWriter.FrameRate = numel(ts) / (ts(end) - ts(1));
open(camWriter);
for k = 1:numel(camPaths)
img = imread(camPaths(k));
writeVideo(camWriter, img);
end
close(camWriter);
% Now compose: raw camera (left) | RR follow view with legend (right)
rawReader = VideoReader(rawVideoTmp);
simReader = VideoReader(locVideoSrc);
targetH = 480;
maxFrames = 100; % side-by-side = heavier I/O per frame; matches visualization-patterns.md
totalDur = min(rawReader.Duration, simReader.Duration);
timeSteps = linspace(0, totalDur - 0.01, maxFrames);
vw = VideoWriter(locVideoOut, "MPEG-4");
vw.FrameRate = round(maxFrames / totalDur);
open(vw);
for k = 1:maxFrames
t = timeSteps(k);
rawReader.CurrentTime = t;
simReader.CurrentTime = t;
imgRaw = readFrame(rawReader);
imgSim = readFrame(simReader);
imgRawR = imresize(imgRaw, targetH / size(imgRaw, 1));
imgSimR = imresize(imgSim, targetH / size(imgSim, 1));
imgRawR = insertText(imgRawR, [10 10], ...
sprintf("Raw camera (t=%.1fs) — original recording", t), ...
FontSize=18, BoxColor="black", TextColor="white", BoxOpacity=0.7);
imgSimR = insertText(imgSimR, [10 10], "Red = Raw GPS", ...
FontSize=18, BoxColor="red", TextColor="white", BoxOpacity=0.75);
imgSimR = insertText(imgSimR, [10 45], "Green = Localized", ...
FontSize=18, BoxColor="green", TextColor="white", BoxOpacity=0.75);
writeVideo(vw, [imgRawR, imgSimR]);
end
close(vw);
% MATLAB popup: ONLY for opening the video — NOT for choosing the trajectory
ans1 = questdlg(sprintf("Lane-localization comparison video saved to:\n%s\n\nOpen it now?", locVideoOut), ...
"Open localization video", "Yes", "No", "Yes");
if strcmp(ans1, "Yes")
openFile(locVideoOut);
endAfter the popup, the agent must ask the user in the chat channel which trajectory to use (raw GPS or localized) before exporting non-ego actors or running the final simulation. Do not derive the choice from medLoc < medRaw — the smaller-offset trajectory is not always the user's preferred one (e.g., when localization snaps to the wrong lane).
Step 5: Generate final scenario (same RoadRunner session)
After user confirms localization looks good, reuse the same RoadRunner instance. The cleanest path is:
newScenario(rrApp)— clears the red+green compare scenario (the per-actoractor.delete()API is unreliable)Re-export only the localized ego with
Color="auto"(no red/green leftover)Normalize timestamps so ego trajectory and tracklist share the same time origin
Call
actorpropswith thescenariobuilder.Trajectorydirectly — it accepts that typeExport each non-ego actor with
Color="auto"anddeg2radorientationsDisable collision fail-condition, set front camera on the ego, simulate
Save scene and scenario
% (1) Fresh scenario — wipes the red/green compare actors cleanly
newScenario(rrApp);
% (2) Re-export only the localized ego
exportToRoadRunner(localizedTrajectory, rrApp, ...
RoadRunnerScene=tempSceneFile, Name="Ego", ...
Color="auto", SetupSimulation=true);
% (3) MANDATORY: normalize timestamps — actorprops compares ego and track
% absolute times. Localized trajectory keeps `TimeOrigin=0` after
% localizeEgoUsingLanes; track data has the original epoch start. They MUST
% share an origin. Easiest: zero both sides.
trackObjNorm = copy(trackData);
normalizeTimestamps(trackObjNorm); % sets track time origin to 0
localizedTrajectory.TimeOrigin = 0; % already 0 in practice; keep explicit
% (4) Extract non-ego actors — scenariobuilder.Trajectory works directly
nonEgoActorInfo = actorprops(trackObjNorm, localizedTrajectory, SaveAs="none");
% (5) Export each non-ego actor.
% HARD RULE — do NOT pass Orientation= here. actorprops Yaw uses compass
% convention (~340° for northbound) which is incompatible with RR's ENU frame.
% RR auto-derives heading from waypoint deltas — that matches recorded motion.
% On flat scenes (OSM), flatten waypoint Z to 0 — per-track Z is sensor-mount
% offset, not road height. See workflow-04 "Flatten waypoint Z" note.
egoLocalOrigin = localizedTrajectory.LocalOrigin;
for i = 1:height(nonEgoActorInfo)
wp = nonEgoActorInfo.Waypoints{i};
wp(:,3) = 0; % flat scene only
actorTraj = scenariobuilder.Trajectory( ...
nonEgoActorInfo.Time{i}, wp, ...
Name=nonEgoActorInfo.TrackID(i), ...
LocalOrigin=egoLocalOrigin);
smooth(actorTraj);
exportToRoadRunner(actorTraj, rrApp, ...
Name=nonEgoActorInfo.TrackID(i), Color="auto", SetupSimulation=false);
end
% (6) MANDATORY: Delete stale sim object, then disable collision fail condition
rrSim = createSimulation(rrApp); delete(rrSim); clear rrSim;
rootPhase = roadrunnerAPI(rrApp).Scenario.PhaseLogic.RootPhase;
failCond = rootPhase.setFailCondition("DurationCondition");
failCond.Duration = localizedTrajectory.Duration + 10;
% Final-scenario camera: ALWAYS "front" on ego (NOT follow). Follow view is
% reserved for the localization compare in Step 4. Front view shows what the
% driver sees and what the recorded camera shows — directly comparable.
setCameraMode(rrApp, "front", FocusActorID=1);
simulateScenario(rrApp, EnableLogging=true);
% (7) Save
saveScene(rrApp, fullfile(rrProjectPath, "Scenes", "MyScenario.rrscene"));
saveScenario(rrApp, fullfile(rrProjectPath, "Scenarios", "MyScenario.rrscenario"));Camera-mode rule (do NOT mix up):
| Stage | Camera | FocusActor | Why |
|---|---|---|---|
| Step 4 — localization compare | "follow" | localized ego | Both ego copies (red/green) need to stay in frame side-by-side with the road |
| Step 5 — final scenario | "front" | ego | Mirror what driver/dashcam saw — directly comparable to raw camera input |
actorprops gotchas (R2026a):
Accepts
scenariobuilder.Trajectorydirectly — do NOT convert towaypointTrajectoryfirstThe internal timestamp-overlap check uses absolute timestamps. If the ego trajectory has
TimeOrigin=0but track timestamps are still on the recording's epoch (e.g.1.55e9), the check fails for every actor. AlwaysnormalizeTimestamps(trackObj)and setlocalizedTrajectory.TimeOrigin = 0before callingThe error surface looks like: "Actor tracklist start timestamp must be greater than or equal to the ego start timestamp..." followed by an internal
actorParamsassignment failure — both are the same root cause
Determining the starting lane index — ASK the user
Do NOT try to derive startLaneIdx from the detections. The ego camera typically only tracks the two boundaries immediately bracketing the vehicle, so a frame-1 inspection (or mode(egoLaneIndex(...))) systematically returns 1 even on a 3-lane or 4-lane road. The dashcam recording is what disambiguates — the user has already seen it; just ask.
Pattern:
Run Steps 1–4 (detect → imageToVehicle → fit → track →
laneData).Print the detection-only hint:
mode(egoLaneIndex(trackedLanes))andmax(numLanesPerFrame).Save state, stop the script, and ASK the user in chat for
startLaneIdx(1 = leftmost driving lane, excluding shoulders).Resume with the user's value into
localizeEgoUsingLanes.
Splitting the script at this gate (e.g., part2a_lane_detect.m → user answers → part2b_localize_compare.m) is the cleanest way to enforce the pause without a blocking input() prompt that hides in MATLAB's command window.
Staged-script pattern. When a pipeline has more than one human-in-the-loop gate (e.g. Multi-GPS pick + lane-index ASK + raw-vs-localized pick), do not stuff them into one monolithic script. Split into numbered parts that save state to a .mat between gates (part1_state.mat, part2a_state.mat, ...). Each part reloads the state, does one section's work, prints a hint, saves new state, and exits. This makes reruns cheap (no re-detecting lanes to fix a downstream bug) and matches the agent's "one tool call ↔ one user answer" loop.
Benign warnings to ignore
These are expected and do not indicate a problem:
Warning: Duplicate data points have been detected and removed - corresponding values have been averaged.— emitted byadjustHeighton dense GPS where two raw samples land on the same point.Warning: Ego lane detections are missing for the frame N.— emitted bylocalizeEgoUsingLaneswhen the tracker dropped a frame; it interpolates through. Common at frame 1 before the tracker has converged.Warning: The video's width and height has been padded to be a multiple of two as required by the H.264 codec.— emitted byVideoWriterMPEG-4 on odd-pixel canvases. Cosmetic.
Copyright 2026 The MathWorks, Inc.
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Security
What our scanner noticed· Low risk
- Phrasing that asks for actions without confirmation
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- MathWorks
- Publisher
- MathWorks (Official)
- Source
- matlab/matlab-agentic-toolkit · skills-catalog/automotive/matlab-use-scenario-builder · commit 2a7f215
- Licence
- BSD 2-Clause License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE.md)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by MathWorks or MathWorks.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download matlab-use-scenario-builder.zip