3、车道模型与拓扑:车道中心线、车道边界线、车道连接关系(Link)、车道宽度与曲率

好,咱们进入正题。车道模型,说白了就是高精地图里最核心的「骨架」。你想想看,自动驾驶车在路上跑,它得知道自己该走哪条道,哪条道能拐弯,哪条道会变窄。这些信息,全藏在车道模型里。

我个人习惯把车道模型拆成三块来看:几何形状、拓扑关系、属性参数。今天咱们一个一个啃。

3.1 车道中心线:车的「导航线」

车道中心线,就是车道上那条虚拟的引导线。它不是实体的标线,而是我们人为定义的一条参考线。车控模块会盯着这条线做横向控制。

中心线的表示方式,业内主流有两种:

  • 离散点序列:每隔一定距离(比如0.5米或1米)采一个点,存成(x, y, z)坐标。简单粗暴,但数据量大。
  • 参数化曲线:用三次样条或贝塞尔曲线拟合。我更喜欢用三次样条,因为它在连接处能保证曲率连续,车跑起来不抖。

核心要点:中心线必须保证C2连续(二阶导数连续),否则控制模块会算出跳变的转向角,车会「画龙」。

举个例子,OpenDRIVE格式里,中心线是用planViewelevationProfile组合描述的。代码里长这样:

<!-- OpenDRIVE 车道中心线示例 -->
<planView>
  <geometry s="0.0" x="0.0" y="0.0" hdg="0.0" length="100.0">
    <line/>
  </geometry>
  <geometry s="100.0" x="100.0" y="0.0" hdg="0.1" length="50.0">
    <spiral curvStart="0.0" curvEnd="0.02"/>
  </geometry>
</planView>

这里spiral就是缓和曲线,用来平滑过渡曲率。嗯,这个细节很多新手会忽略。

3.2 车道边界线:别压线,别越界

边界线就是车道的物理或虚拟边界。它决定了车能不能变道、能不能借道。

边界线分两种:

  • 物理边界:路缘石、护栏、混凝土墙。这些是硬约束,车绝对不能碰。
  • 标线边界:白实线、白虚线、双黄线。虚线可以跨越,实线不行。

我在项目中遇到过一个问题:某段路的标线因为磨损,地图里标的是实线,但实际已经模糊不清了。结果规划模块死活不让变道,车就在那堵着。后来我们加了一个「标线置信度」字段,低于阈值的按虚线处理。

避坑指南:边界线的类型一定要和交通规则绑定。比如「实线变道扣3分」,这个逻辑必须在路径规划里写死。我曾经见过一个demo,车压着实线变道,虽然技术上做到了,但上路就是违章。

3.3 车道连接关系(Link):谁跟谁连着

车道不是孤岛。前一个车道到后一个车道怎么接?左转车道连到哪条路上?这些就是Link要干的事。

Link关系通常用「前驱-后继」模型来描述:

  • 前驱(Predecessor):当前车道从哪来
  • 后继(Successor):当前车道到哪去
  • 左邻/右邻(Left/Right Neighbor):相邻车道是谁

你想想看,如果路口有5条车道,每条车道可能对应3个不同的出口方向。那Link关系就是一张网。我习惯用邻接表来存,查询效率高。

举个例子,一个十字路口左转车道的Link配置:

车道ID: lane_3
类型: 左转专用道
前驱: lane_2 (直行车道)
后继: lane_101 (目标路段的最左侧车道)
左邻: lane_4 (直行车道)
右邻: lane_2 (直行车道)
转向类型: left_turn

这里要注意,转向类型必须和交通灯相位匹配。否则会出现「绿灯亮了但规划模块还在等」的尴尬情况。

3.4 车道宽度:不是所有车道都一样宽

车道宽度是个容易被低估的参数。你以为都是3.5米?错。高速车道、城市快速路、匝道、停车带,宽度都不一样。

道路类型 标准宽度 最小宽度 备注
高速公路 3.75m 3.5m 大型车多,需要更宽
城市主干道 3.5m 3.25m 混行路段要注意
匝道 3.0m 2.8m 弯道处要加宽
停车带 2.5m 2.0m 仅限临时停车

宽度数据怎么用?路径规划时,如果车道宽度小于车辆宽度+安全余量,那就不能通行。我见过一个案例,某辆宽体SUV在窄匝道上蹭了护栏,就是因为地图里宽度字段写错了。

个人经验:车道宽度最好存成「分段函数」。因为有些车道在路口会渐变加宽,比如从3.5米变成4.5米。用s_offsetwidth两个字段就能描述这种变化。

3.5 曲率:弯道快慢全靠它

曲率决定了车过弯时的最大安全速度。曲率越大,弯越急,速度就得越低。

曲率的计算很简单:曲率 = 1 / 转弯半径。单位是 1/m。比如半径100米的弯,曲率就是0.01。

但实际应用中,曲率不是常数。一个弯道从直道进入,曲率从0逐渐增大到最大值,再逐渐减小到0。这就是为什么需要缓和曲线(clothoid)。

我建议在车道模型里存两个曲率值:

  • 起始曲率:车道起点处的曲率
  • 终止曲率:车道终点处的曲率

中间的变化,用线性插值或者样条插值算出来。这样规划模块就能提前知道「前面有个弯,该减速了」。

举个例子,一段匝道的曲率变化:

车道ID: ramp_05
长度: 200m
起始曲率: 0.001 (半径1000m,几乎直道)
终止曲率: 0.02 (半径50m,急弯)
曲率变化率: 0.000095 /m (线性变化)

有了这个数据,速度规划就能算出:在哪个位置开始刹车,刹到多少速度,过了弯心再加速。

核心要点:曲率变化率(曲率对弧长的导数)比曲率本身更重要。如果曲率变化率太大,车会感觉「突然被拽了一把」,乘客体验极差。我一般要求曲率变化率不超过0.001/m²。

3.6 把这些串起来:一个完整的车道模型

好了,咱们把上面这些元素拼起来,就是一个完整的车道模型了。我习惯用JSON格式来存,方便调试:

{
  "lane_id": "lane_001",
  "type": "driving",
  "center_line": [
    {"s": 0.0, "x": 0.0, "y": 0.0, "z": 0.0},
    {"s": 1.0, "x": 0.0, "y": 1.0, "z": 0.0}
  ],
  "left_boundary": {
    "type": "solid_white",
    "points": [...]
  },
  "right_boundary": {
    "type": "dashed_white",
    "points": [...]
  },
  "predecessor": "lane_000",
  "successor": "lane_002",
  "left_neighbor": null,
  "right_neighbor": "lane_003",
  "width": {
    "s_start": 0.0,
    "s_end": 100.0,
    "width_start": 3.5,
    "width_end": 3.5
  },
  "curvature": {
    "s_start": 0.0,
    "s_end": 100.0,
    "curv_start": 0.0,
    "curv_end": 0.005
  }
}

这个结构虽然简单,但足够支撑90%的路径规划场景了。剩下的10%,比如多叉路口、环形岛、潮汐车道,需要在这个基础上加一些特殊字段。

最后说一句,车道模型的质量直接决定了自动驾驶的上限。地图数据不准,算法再牛也白搭。所以,做地图的同学,请善待每一条车道线。