MATLAB 里的驾驶场景设计器(Driving Scenario Designer,下文简称 DSD)是我这几年做自动驾驶弯道仿真的默认起点。它不神秘,本质上就是一个带道路编辑器的场景装配台:你画出中心线、拉出车道、摆上几台车、配好速度轨迹,按下运行就能看到车沿着你画的线跑起来。真正卡住人的从来不是“按钮在哪”,而是“为什么这么点”——为什么中心点要多密、为什么速度剖面必须跟路径点一一对应、为什么仿真步长稍大一点车就切出车道。下面我把自己反复折腾出来的一整套流程摊开讲,从环境自查到代码级拆解,弯道怎么画、速度怎么算、坑怎么填,全部是可复制的东西。
这篇内容适合两类人看:刚接手驾驶仿真、需要在一小时内跑出第一条弯道的新手,已经在用 DSD 但总被“车飞出去”“传感器啥也测不到”“导出 Simulink 后场景不动”折磨的同行。全文以 MATLAB 脚本为主线,图形界面只作为校验和兜底手段,因为脚本可以版本管理、可以参数化、可以批量跑,这三点在真实测试流程里比“点得爽”重要得多。
1. 先把工具的能力边界摸清楚
1.1 弯道为什么是仿真里最“贵”的场景
直线段仿真几乎没有技术含量,车速恒定、航向恒定、车道边界平行,任何一套逻辑都能跑出漂亮结果。弯道就不一样了,它同时把四个维度的复杂度拧在一起:几何上曲率随弧长变化,车辆模型需要持续输出横向加速度;感知上车道线从直线变成圆弧,拟合算法要用二次或三次曲线去逼近;控制上预瞄距离和前视点必须随曲率调整,否则纯跟踪算法会切内道;评价上横向误差、航向误差、舒适性指标全都跟曲率强相关。这就是为什么很多人直线跑得挺顺,一进弯就原形毕露。
DSD 在这方面提供的价值不是“画个弯”,而是把道路几何、车辆运动学和传感器模型绑在同一个时间轴上。你画的中心线会被拟合成带曲率连续性的道路,车辆沿轨迹行驶时,每一帧的位置、朝向、速度都是自洽的,雷达和摄像头模型拿到的也是同一套真值。这一点很关键——如果道路用一套坐标、车辆用另一套坐标、传感器再用第三套,你后面排查问题会疯掉。
另一个容易被忽略的点是弯道对时步的敏感性。直线上车辆朝某个方向匀速走,步长 0.1 秒的影响无非是位置跳了一米;弯道上同样的步长会让航向角每次跳好几度,视觉上就是车在一格一格地“拐硬弯”,传感器打出去的光束也会跟着抖动。所以弯道场景做得好不好,很大程度取决于你对采样率的把控,而不是画得多漂亮。
1.2 图形界面和脚本各自该干哪一半
我见过不少人一上来就打开 App,鼠标拖着中心点一个个点,点完发现想改半径,只能全部推倒重来。这属于用错了工具的分工。我的习惯是:探索性设计用界面,可复现场景用脚本。第一次试某个弯道形态、想快速看看车道宽度和转弯半径比例是否合理,打开drivingScenarioDesigner拖两下确实快;但一旦形态确定,立刻把中心点坐标抄进脚本,后面所有迭代都在代码里做。
界面的另一个正确用法是反向校验。脚本跑出来的场景可以一键喂回 App:drivingScenarioDesigner(scenario)把变量加载进去,你就能在图形界面里看到道路边界、车道线、车辆轨迹、传感器覆盖范围叠在一起的样子。有些问题在命令行里看不出来,比如两条车道线在弯心处交叉、车辆轨迹贴着路缘擦过去,图形界面上一眼就能发现。这种“脚本生成 + 界面审查”的双向流程,比纯界面或纯脚本都高效。
还有一个功能值得单独提:App 的 Tools 菜单里有一个针对道路和车辆时步的检查项。它会把你的速度、采样时间、道路曲率放在一起算一遍,告诉你当前配置下车辆每步移动的距离是否超出了合理范围。这个检查我在项目里救过好几次场——用高车速配 0.1 秒步长时,它会直接标红提示,省得你后面花两小时去查“为什么车会瞬移”。
2. 动手前的环境自查与最小验证
2.1 工具箱、版本与常见依赖
DSD 不是一个独立产品,它挂在自动驾驶工具箱体系下,同时依赖场景建模的基础设施。装环境时至少确认这几块:自动驾驶相关工具箱(提供drivingScenario、vehicle、trajectory、各类传感器生成器)、导航与地图相关工具箱(部分道路拟合和曲线工具在这里)、以及基础的 MATLAB 本体。如果你的流程要接 Simulink 做闭环,那 Simulink 和对应的场景读取模块也得在。
版本差异是个真实的坑。同一个函数在不同年份的版本里参数名会变,尤其是传感器生成器那一块,属性名和返回值顺序调整过好几次。我的经验是:先锁定一个版本再写代码,把版本号写在脚本头部注释里,别今天用新版本跑通、明天换台机器用旧版本报错,然后花半天怀疑人生。另外,函数签名不确定的时候,别硬猜,直接在命令行敲help 函数名看当前这个版本到底怎么写的,比在网上翻三年前的帖子靠谱得多。
道路拟合部分需要说明一下它的原理。DSD 在根据你给的中心点生成道路时,用的是回旋线(clothoid)拟合,也就是曲率随弧长线性变化的曲线。这个东西在公路设计里是标准做法——直线段曲率是 0,圆曲线曲率是常数 1/R,中间用回旋线过渡,保证曲率连续。好处是车辆沿路行驶时方向盘转角不会突变,坏处是你给的中心点如果本身跳变很大,拟合出来的道路可能偏离你预期的形状。理解这一点,你就知道为什么中心点不能随手点了。
2.2 三行代码验证环境是否真的可用
在正式画弯道之前,我建议先跑一个最小闭环,确认整条链路是通的。三行代码就够:
scenario = drivingScenario('SampleTime', 0.05, 'StopTime', 5); road(scenario, [0 0 0; 50 0 0], 'Lanes', lanespec([1 1])); drivingScenarioDesigner(scenario);这三行的意思是:建一个采样时间 0.05 秒、总时长 5 秒的场景;铺一条从原点出发、沿 x 轴方向长 50 米的直路,中心线左右各一条车道;然后把它丢进图形界面里看一眼。如果界面正常打开、道路显示为两条平行车道线,说明基础环境没问题。
这一步的价值在于把所有环境问题前置。我见过太多次,兴冲冲写了两百行弯道代码,一运行报“函数不存在”,才开始排查是哪个工具箱没装。用最小用例先探路,成本三十秒,收益是后面省掉半小时。如果这三行都跑不通,那问题一定在环境层面,跟你的弯道逻辑毫无关系,方向很明确。
顺手再验证一下推演机制:
while advance(scenario) p = pose(scenario.Actors(1)); fprintf('t=%.2f x=%.2f y=%.2f\n', scenario.CurrentTime, p(1), p(2)); end这段用来说明advance是怎么推进时间的——每次调用把场景往前推一个采样步长,返回 false 表示到StopTime了。后面所有的仿真循环都是这个骨架。
3. 弯道场景的三层建模:道路、车辆、传感器
3.1 道路层:中心点密度决定了一切
道路是场景的地基,弯道尤其如此。核心接口是road(scenario, roadCenters, 'Lanes', lanespec([...])),其中roadCenters是一个 N×3 的矩阵,每行是一个中心点的 [x, y, z] 坐标,z 用来表达高程和横坡。DSD 会按顺序把这些点连成一条带曲率连续性的中心线,再按lanespec往两侧铺车道。
中心点密度这里有两条经验值。第一,弯道上相邻中心点的间距控制在曲率半径的 1/10 到 1/5 之间。半径 60 米的弯,点间距 6 到 12 米比较合适。间距太大,回旋线拟合出来的形状跟你脑子里想的不一样;间距太小,拟合会放大数值噪声,道路出现细微抖动,车辆的航向角跟着抖。第二,直线段不需要这么密,30 米一个点完全够用,因为直线曲率恒为零,拟合结果没有任何自由度。
lanespec的参数形式是lanespec([左车道数, 右车道数])。lanespec([1 1])是双向两车道,lanespec([0 1])是只有右侧一条车道。这里有个容易搞混的地方:车道的左右是相对于中心线的前进方向定义的,右车道的中心线在中心线右侧偏移半个车道宽(默认车道宽 3.6 米,所以偏移是 1.8 米)。如果你的车要跑在右车道正中间,初始位置就不能放在中心线上。
高程那一列别随便填。很多人图省事全部写 0,平面场景下没问题;但如果你想验证车辆在带横坡的弯道上是否会有姿态变化,第三列就要按横向坡率填。常规公路弯道超高在 2% 到 6% 之间,半径 60 米这种中速弯,取 4% 比较常见。具体做法是让外弯侧比内弯侧高,高差 = 车道宽 × 坡率。
3.2 车辆层:位置、朝向与速度剖面
车辆用vehicle(scenario, 'Position', [x y z])创建,能配置的包括尺寸、类别、初始朝向。位置坐标是世界坐标系下的米,朝向是相对 x 轴逆时针的角度(弧度)。
初始位姿的坑在于它必须跟轨迹的第一个点一致。trajectory的行为是让车辆从当前位姿出发去追击你给的路径点,如果第一个路径点跟初始位置差了五米,车辆会在第一步就瞬移到那个点上,后续的轨迹全部错位。我踩过这个坑,现象是车辆在起点闪一下,然后整条轨迹看起来像整体平移了一段。
真正决定弯道表现的是速度剖面。trajectory(actor, waypoints, speed)的第三个参数可以是标量(全程匀速),也可以是与waypoints行数相同的向量(每点一个速度)。用向量的时候必须严格对齐行数,多一个少一个都会报错,这个报错信息还挺模糊的,容易让人卡住。
速度剖面的设计原则是从横向加速度约束倒推。车辆过弯时受到的横向加速度近似为 a = v²/R,这个值直接决定乘客的舒适感。工程上取 1.5 到 2.5 m/s² 作为舒适上限,超过 3 m/s² 就明显不适了。所以给定半径 R,弯中允许的最大车速是 v = √(a·R)。这个公式是整个速度设计的锚点,后面具体算的时候会用到。
3.3 传感器层:装在哪、看多远
传感器模型不是必须的,但弯道场景不加传感器,价值至少少一半。常用的两个是雷达数据生成器和视觉检测生成器。
雷达的参数重点是安装位置、视场角和量程。安装位置用MountingLocation,单位是米,坐标是相对车身、以车辆中心为原点的车身坐标系,x 向前、y 向左、z 向上。量产车的毫米波雷达一般装在前保险杠,纵向 3 到 4 米、横向 0、高度 0.4 到 0.8 米。视场角用FieldOfView,格式是 [水平角, 俯仰角],角雷达通常水平 90 度、俯仰 10 到 20 度。
视觉检测器的关键参数是量程、检测概率和误检率。量程要跟你的场景尺度匹配,弯道半径 60 米、总长一百多米的话,量程 60 米是个合理起点。检测概率默认 0.9 左右,意味着每帧有 10% 概率漏检,如果你想验证跟踪算法的鲁棒性,可以调到 0.7 制造压力。
弯道下传感器有个特有现象:内弯侧目标被遮挡,外弯侧目标提前暴露。这是几何决定的,雷达和摄像头都一样。如果你的场景里放了多辆车,做评估时要意识到这个不对称性——同一辆目标车在弯道入口和弯道出口的检测稳定性可能完全不同,别把它当成算法的稳定性问题。
4. 完整实操:五分钟搭出一条 90 度弯道
4.1 第一步 用脚本生成中心线
先定义基本几何参数。我要的是一条从原点出发、走 30 米直线、然后左转 90 度的弯,弯道半径 60 米。这几个数字决定了整条路的位置。
R = 60; % 弯道半径,单位 m lineLen = 30; % 入弯前直线长度,单位 m nLine = 7; % 直线段中心点数量 nArc = 10; % 弯道段中心点数量 % 直线段:沿 +x 方向,y 恒为 0 sLine = linspace(0, lineLen, nLine)'; ptsLine = [sLine, zeros(nLine, 1), zeros(nLine, 1)]; % 弯道段:圆心位于 (lineLen, R),从 θ=0 转到 θ=π/2 theta = linspace(0, pi/2, nArc)'; ptsArc = [lineLen + R*sin(theta), R - R*cos(theta), zeros(nArc, 1)]; % 拼接时去掉重复的接缝点 roadCenters = [ptsLine; ptsArc(2:end, :)];这段代码的几何关系值得说清楚。圆心选在 (30, 60),起点是 (30, 60-60) = (30, 0),正好接上直线段的终点;终点是 (30+60, 60) = (90, 60)。参数方程用 sin 和 (1-cos) 的组合,起点处切线方向是 +x,终点处切线方向是 +y,也就是标准的左转 90 度。拼接时去掉ptsArc的第一个点,因为那一点和直线段最后一点完全重合,重复的中心点会让后续的弧长计算出现零长度段,某些函数会直接报错。
如果换成右转、或者转 45 度,只需要改theta的范围,比如linspace(0, -pi/2, nArc)就是右转。半径和直线长度改了,圆心位置要跟着改,这个是唯一的联动关系。
4.2 第二步 铺车道与摆车
scenario = drivingScenario('SampleTime', 0.02, 'StopTime', 25); road(scenario, roadCenters, 'Lanes', lanespec([1 1]));采样时间我选了 0.02 秒,比默认的 0.05 更细。原因后面第 5 节会展开,简单说就是弯道上每步位移要控制住,不然航向角跳变明显。lanespec([1 1])是双向两车道,视觉上完整一些。
接着摆车。为了让车跑在右车道中心而不是压着中心线,先用中心点的法向量往外偏 1.8 米:
dx = gradient(roadCenters(:,1)); dy = gradient(roadCenters(:,2)); nor = [-dy, dx] ./ vecnorm([-dy dx], 2, 2); % 单位法向量,指向左侧 laneCenters = roadCenters(:,1:2) - 1.8 * nor; % 右车道中心 laneCenters(:,3) = 0;gradient算的是每个点处切线的差分近似,[-dy, dx]旋转 90 度得到法向。注意方向:这个法向指向左侧,所以减去 1.8 才是右侧车道中心。这个偏移用在整个路径上,而不是只偏起点——弯道里法向是连续变化的,只偏起点会让车辆越跑越往外。
然后创建车辆并把初始位置设到路径第一个点:
ego = vehicle(scenario, ... 'ClassID', 1, ... 'Position', laneCenters(1,:), ... 'Length', 4.6, 'Width', 1.9, 'Height', 1.5);4.3 第三步 配轨迹与速度
速度剖面按前面说的横向加速度倒推。我取舒适上限 1.5 m/s²,半径 60 米:
aLatMax = 1.5; vCorner = sqrt(aLatMax * R); % 约 9.49 m/s,折合 34 km/h vEnter = 14; % 入弯前车速,m/s sDecelFrom = 10; % 从弧长 10 m 处开始减速vCorner = sqrt(1.5 × 60) = 9.49米每秒,大概 34 公里每小时。入弯前 14 米每秒(50 公里每小时),所以需要在直线段内减速。
按弧长重采样路径点,再逐点填速度:
s = [0; cumsum(vecnorm(diff(roadCenters), 2, 2))]; sUniform = linspace(0, s(end), 240)'; waypoints = [interp1(s, laneCenters(:,1), sUniform), ... interp1(s, laneCenters(:,2), sUniform), ... zeros(numel(sUniform),1)]; speedProfile = vCorner * ones(size(sUniform)); idx1 = sUniform < sDecelFrom; speedProfile(idx1) = vEnter; idx2 = sUniform >= sDecelFrom & sUniform < lineLen; speedProfile(idx2) = vEnter + (vCorner - vEnter) * ... (sUniform(idx2) - sDecelFrom) / (lineLen - sDecelFrom); trajectory(ego, waypoints, speedProfile);这里有两个关键动作。一是按弧长等间距重采样,把原来疏密不均的中心点变成间隔均匀的点(总长约 124 米,240 个点,间距约 0.52 米),这样速度剖面在空间上的分布才是均匀的。二是速度向量长度必须等于路径点行数,代码里都用sUniform驱动,天然对齐。
入弯的减速段是线性的,实际车上会做成加加速度受限的 S 形曲线,但用于仿真验证横向控制,线性减速已经够用,而且更容易分析。
4.4 第四步 运行与可视化
bep = birdsEyePlot('XLimits', [-20 130], 'YLimits', [-40 100]); lp = laneBoundaryPlotter(bep, 'DisplayName', '车道边界'); tp = trackPlotter(bep, 'DisplayName', '自车', 'MarkerFaceColor', 'red'); plotLaneBoundary(lp, scenario.LaneBoundaries); while advance(scenario) p = pose(ego); tp(p(1), p(2), p(3)); drawnow limitrate end跑起来之后,你应该能看到车从原点出发,沿直线跑 30 米,在弯道入口减速,然后贴着弯道内侧平稳转过 90 度。如果车辆在中途跳出车道或者航向角剧烈摆动,直接跳到第 6 节找对应症状。
5. 代码逐段拆解与参数怎么算
5.1 圆弧参数化与切线方向
圆弧参数方程用的是(圆心 + R·sinθ, 圆心_y - R·cosθ)这种组合,而不是常见的(R·cosθ, R·sinθ)。这个选择不是随意的,是因为后者在 θ=0 处的切线方向是 +y,起点朝向不对,你还得额外加一个旋转矩阵。用 sin 和 (1-cos) 的形式,天然满足“起点在原点方向、切线沿 +x”的需求,省去一堆坐标变换。
对于任意起始朝向的弯道,更通用的做法是先算一个旋转矩阵再作用到点上:
heading0 = pi/6; % 起始朝向,30 度 Rmat = [cos(heading0) -sin(heading0); sin(heading0) cos(heading0)]; ptsArcRotated = (Rmat * ptsArc(:,1:2)')';不过实际项目里我一般不用这套通用解,因为场景都是围绕自车坐标系搭建的,让自车从原点沿 +x 出发,后面所有几何都能用最简单的表达式写。保持这个约定,能省掉大量调试坐标系的时间。
还有一个细节:trajectory计算车辆朝向是靠前后路径点的差分,所以路径点的顺序必须是从起点到终点单调推进。如果你不小心把点排反了,车辆会试图倒着走,或者干脆原地打转。中心点拼接顺序错了是新手常见的失误,症状是车辆在起点来回抽搐。
5.2 弧长重采样为什么必须做
不重采样会发生什么?你给的中心点可能是 7 个直线点加 10 个弯道点,间距分别是 5 米和 10.5 米,而速度向量如果按点数配,每个点之间代表的空间距离完全不同。线性插值速度的话,减速的“陡峭程度”就会随点间距变化,弯道入口那一段的减速度可能比你设计的大两三倍。
重采样的本质是把时间维度的设计转换到空间维度。速度剖面本质上是 s(弧长)的函数,只有等间距采样,速度曲线在空间上的斜率才有物理意义。cumsum(vecnorm(diff(...)))这段就是算累积弧长,然后interp1在弧长域上做线性插值,把原来不均匀的点映射到均匀网格上。
采样密度怎么定?我给的经验值是空间分辨率 0.5 米左右,对应总长 124 米就是 240 个点。这个密度下,60 米半径的弯道每两点之间的转角约 0.5 度,视觉上足够平滑。密度再高收益不大,反而增加计算量;低于 0.3 米的间距就要注意数值噪声了。
5.3 横向加速度反推限速
这张表是我常用的速查,取横向加速度上限 1.5 m/s²:
| 弯道半径 R (m) | 弯中限速 v (m/s) | 换算 (km/h) | 典型场景 |
|---|---|---|---|
| 20 | 5.5 | 19.7 | 停车场掉头 |
| 30 | 6.7 | 24.1 | 城市小半径路口 |
| 50 | 8.7 | 31.2 | 城市主干道转弯 |
| 60 | 9.5 | 34.2 | 郊区匝道 |
| 80 | 11.0 | 39.4 | 快速路匝道 |
| 100 | 12.2 | 44.1 | 高速互通 |
如果你的场景是验证紧急工况,可以把上限提到 2.5 m/s²,车速相应提高约 29%(因为 √(2.5/1.5) ≈ 1.29)。但要注意,此时车辆模型的轮胎侧偏特性可能进入非线性区,仿真结果的参考价值会下降。
反过来用也很有价值:给定你希望跑的车速,反推需要多大的半径。比如验证某个弯道 60 km/h 的通过性,v = 16.7 m/s,在 1.5 m/s² 约束下需要 R = v²/a = 278 米。这个数字一算出来你就知道,60 km/h 过 60 米半径的弯,横向加速度会到 4.6 m/s²,属于相当激进的工况。
5.4 仿真主循环的时序控制
采样时间选 0.02 秒不是拍脑袋。判断标准是单步位移量与弯道曲率的关系:每步车辆前进的距离 Δs = v·Δt,如果 Δs 大到让离散化后的轨迹明显偏离圆弧,航向角就会出现阶跃。
拿弯中 9.5 m/s 算,0.02 秒步长对应 Δs = 0.19 米,圆弧上对应的圆心角是 0.19/60 ≈ 0.18 度,完全可以忽略。如果用 0.05 秒,Δs = 0.475 米,转角 0.45 度,还凑合;用 0.1 秒,Δs = 0.95 米,转角接近 1 度,视觉上就能看出车辆“一格一格拐”。高速工况下这个差异更明显——30 m/s 配 0.1 秒步长,单步走了 3 米,弯道上基本没法看。
代价是计算量。0.02 秒步长、25 秒场景,就是 1250 步,比 0.05 秒多了 2.5 倍。如果场景里有多个传感器、每帧都要做检测,跑一次可能要几十秒。我的折中方案是:调试阶段用 0.02,批量回归用 0.05,前者保证可视化正确,后者保证吞吐量。真要卡精度,可以在弯道段动态调整步长,但这就复杂了,一般场景用不上。
另外StopTime要留余量。按 124 米总长、平均速度 10 m/s 估,大约 12.4 秒能跑完,我给 25 秒是有富裕的。如果StopTime设小了,车辆跑到一半仿真就结束,你可能误以为是轨迹断了。
6. 踩坑实录:弯道仿真最常见的六类问题
6.1 车切出车道或“飞出去”
这是最高频的问题,原因通常有三个,按出现频率排序。
第一,路径点顺序错乱或存在重复点。拼接中心线时如果接缝点没去重,cumsum里会出现零长度段,interp1在这一点上会产生 NaN,轨迹就断了。表现是车辆在某个位置突然消失或跳到远处。检查方法很简单:跑之前打一行any(diff(s) <= 0),返回 true 就说明有非递增的弧长。
第二,速度剖面长度与路径点数不匹配。这个报错通常直接报“向量长度不一致”,但如果你用了广播机制不小心让它跑通了,速度就会错位到别的点上,车辆会在弯心附近莫名其妙加速。
第三,车辆初始位姿与路径首点偏离。前面提过,偏离超过一两米车辆就会瞬移。检查方法是打印ego.Position和waypoints(1,:)对比,差的应该是零。
还有一种“飞出去”是真的物理原因——速度太快,轨迹函数算出来的转向角跟不上。这时候车辆不是瞬移,而是沿着切线方向冲出去,路径变成一条直线。看到这个现象,先降速再谈别的。
6.2 传感器检测不到目标
传感器返回空结果,排查顺序建议这样走。先确认调用时机:传感器生成器需要在advance之后、用当前时刻的scenario.Actors调用,如果你在循环外面调,那永远拿到的是初始状态。再确认坐标系:MountingLocation用的是车身坐标系,不是世界坐标系,写错了传感器就装到车顶上或者车尾去了。
然后是视场角。弯道场景里,如果你的传感器水平视场只有 30 度,那内弯侧的目标在很长一段距离里都进不了视野,这是几何决定的,不是 bug。想验证这一点,把视场角调到 120 度再跑一遍,如果检测突然出现了,说明就是视野问题。
最后是版本的返回值顺序。不同年份的版本里,视觉检测器的返回值可能是[detections, numDetections, image]也可能是别的排列,用之前help一下确认,别照着旧帖子抄。
6.3 导出 Simulink 之后的断层
场景在脚本里跑得好好的,导出成模型之后车不动或者场景不变,问题通常出在这几处。
场景数据没跟着模型走。导出时场景是保存成一个数据文件或工作区变量的,Simulink 里的场景读取模块需要指向这个变量。如果你换了机器或者清了工作区,模型找不到数据就会用默认空场景,表现就是路没画出来、车也不动。
采样时间不匹配。模型里的求解步长跟场景的SampleTime如果不是整数倍关系,车辆位置会出现插值误差,弯道上表现为轨迹抖动。建议把模型的固定步长设成与场景采样时间一致,或者成整数倍。
道路参数没有同步。有些导出方式只把车辆轨迹带过去,道路几何要单独配置。这时候模型里车辆是沿着一条不存在的路在跑,视觉上就是悬浮。检查方法是在模型里把道路预览打开,看边界线是否显示。
| 现象 | 最可能原因 | 快速验证 |
|---|---|---|
| 车辆起点闪烁后整体偏移 | 初始位姿与首路径点不一致 | 打印两者差值 |
| 弯道中车辆沿切线冲出 | 速度超过曲率允许上限 | 按 √(aR) 重算限速 |
| 轨迹在某个位置断裂 | 路径点重复导致弧长非递增 | any(diff(s)<=0) |
| 传感器全程无检测 | 调用时机在循环外或视场过窄 | 加大视场角重跑 |
| 导出后场景空白 | 场景数据未随模型加载 | 检查模块指向的变量 |
| 弯道上航向角一跳一跳 | 采样步长过大 | 步长减半观察 |
7. 从单车弯道走向多车交互与批量测试
7.1 编排一辆对向车
单车跑通之后,加一辆对向车是验证感知和决策逻辑的下一步。做法是复制路径生成逻辑,把车道偏移反过来:
oppLane = roadCenters(:,1:2) + 1.8 * nor; % 左侧车道 oppLane(:,3) = 0; % 对向车从弯道出口往入口跑,路径需要反转 oppWaypoints = flipud(oppLane); oppSpeed = 8 * ones(size(oppWaypoints,1), 1); opp = vehicle(scenario, 'ClassID', 1, 'Position', oppWaypoints(1,:)); trajectory(opp, oppWaypoints, oppSpeed);关键点是路径必须反转。对向车的行驶方向跟自车相反,如果不翻转,它会从弯道出口倒着开回入口,虽然位置对但朝向完全错,看起来像在倒车。翻转之后还要检查速度向量的长度也跟着变,这一点容易漏。
时间上的编排更有讲究。你想让两车在弯心交会,还是错开?交会时机取决于两车进入场景的时间差。trajectory支持延迟启动,可以在路径前面补一段静止点,或者干脆调整初始位置。我一般用初始位置控制,直观一些:把对向车的起点往远处挪,让它晚一点进入交互区域。
7.2 把场景参数化以便批量跑
单次仿真说明不了什么,真正的价值在于跑一批参数组合。把整个搭场景过程包成函数是最省事的做法:
function scenario = buildCurveScenario(R, vEnter, aLatMax, dt) if nargin < 4, dt = 0.02; end vCorner = sqrt(aLatMax * R); ... % 内部按前面的逻辑生成道路、车辆、轨迹 end这样外层就能用嵌套循环扫参数:
results = []; for R = [40 60 80 100] for v0 = [10 12 14 16] sc = buildCurveScenario(R, v0, 1.5, 0.05); metric = runAndEvaluate(sc); % 自己写的评估函数 results = [results; R, v0, metric]; end end评估函数里我通常记三个指标:最大横向误差(车辆实际位置到车道中心线的距离峰值)、最大横向加速度(从速度平方除以曲率算)、航向误差收敛时间(从入弯到误差稳定在设计值以内的时间)。这三个放在一起,基本能判断某个控制参数在弯道上的适应性。
批量跑有个坑:drivingScenario对象是有状态的,跑完之后不能直接复用,必须重新创建。循环里每次都调buildCurveScenario就是因为这个,别图省事在循环外建一次然后反复advance。另外每次创建场景的随机种子要固定,传感器模型里有随机成分,种子不固定的话同一组参数跑两次结果不一样,你会误以为算法不稳定。
我个人在实际操作中的体会是,弯道仿真这件事,八成的时间花在“把几何和速度对上”,两成花在“看结果”。第一版场景能跑起来之后,别急着加传感器、加多车,先把单车轨迹跑顺、把横向误差曲线画出来,确认车辆确实贴着车道中心走。这个基线稳了,后面加什么都是增量验证;基线不稳,后面每加一个元素你都要重新怀疑一遍到底是谁的问题。