4、数据模型设计:高精地图数据模型(NDS、OpenDRIVE)、车道模型、路口模型、拓扑关系
聊到高精地图的数据模型,我脑子里第一个蹦出来的词就是「混乱」。
为什么这么说?因为市面上主流的格式太多了。NDS、OpenDRIVE、还有各家图商自己的私有格式。你想想看,一个行业连数据怎么存都没统一,这活儿能好干吗?
但话说回来,作为嵌入式工程师,我们没得选。车端芯片就那么点资源,存储、算力、实时性,哪个都是硬约束。所以今天这节,我就把我在项目中踩过的坑、总结的经验,掰开了揉碎了讲给你听。
4.1 两大主流标准:NDS 与 OpenDRIVE
先说说 NDS。NDS 全称是 Navigation Data Standard,它最早是给导航用的。但后来发现,导航那套数据模型太「糙」了——只有道路级别,没有车道级别。高精地图需要的是厘米级的精度,所以 NDS 后来出了个扩展版本,叫 NDS.Live。
我个人习惯把 NDS 理解成一个「数据库容器」。它不规定你存什么,只规定你怎么存。比如数据分块、版本管理、增量更新,这些都是 NDS 的强项。我在做量产项目时,用的就是 NDS 格式。原因很简单:它支持差分更新。你想想看,全国地图几十个 GB,每次 OTA 都全量下载?车机那点流量根本扛不住。
再来看 OpenDRIVE。这个标准是 ASAM 组织搞的,主要用在仿真和测试领域。它的核心是「道路网络描述」。说白了,就是告诉你这条路有几条车道、车道多宽、曲率多少、坡度多少。
OpenDRIVE 用 XML 格式存储。嗯,这里要注意:XML 解析在嵌入式平台上非常吃资源。我见过一个项目,直接把 OpenDRIVE 文件丢给车机解析,结果启动时间从 2 秒变成了 20 秒。后来我们改成预编译成二进制格式,才把时间压下来。
| 特性 | NDS | OpenDRIVE |
|---|---|---|
| 存储格式 | SQLite 数据库 | XML 文件 |
| 更新方式 | 差分更新(增量) | 全量替换 |
| 适用场景 | 量产车端 | 仿真、测试 |
| 车道级精度 | 支持(扩展后) | 原生支持 |
| 嵌入式友好度 | 高(二进制存储) | 低(XML 解析开销大) |
4.2 车道模型:从「线」到「面」的思维转变
做传统导航时,我们只关心道路。道路就是一条线。但高精地图不一样,它关心的是车道。一条路可能有 3 条车道,每条车道都有自己的属性:宽度、限速、行驶方向、是否允许变道。
我建议你把车道模型想象成一个「管道」。这个管道有起点、终点、左右边界。每个边界都是一条曲线。嗯,说白了就是 4 条线围起来的一个面。
// 车道模型的数据结构(简化版)
typedef struct {
uint64_t lane_id; // 车道唯一 ID
uint64_t road_id; // 所属道路 ID
int32_t lane_index; // 车道序号(从右到左:-1, 0, 1...)
float width; // 车道宽度(米)
float speed_limit; // 限速(km/h)
Polyline3D left_boundary; // 左边界线
Polyline3D right_boundary; // 右边界线
LaneType type; // 车道类型(行车道、应急车道、公交车道...)
} LaneModel;
我在项目中遇到过一个问题:车道边界线怎么存?最直接的办法是存一堆离散点。但这样数据量太大了。一条车道几百米,每 10 厘米一个点,那就是几千个点。全国几千万条车道,你算算数据量?
后来我们改用三次样条曲线。只存控制点,运行时实时插值。这样数据量能压缩 90% 以上。代价就是计算量上去了。不过对于现在的车规级芯片来说,这点算力还是扛得住的。
4.3 路口模型:最复杂的拓扑节点
路口是高精地图里最复杂的地方。为什么?因为路口里没有车道线。或者说,车道线是断开的。车辆从一条路进入路口,然后从另一条路出去。中间这段怎么走?全靠拓扑关系。
我习惯把路口模型拆成三部分:
- 进入连接:从哪条车道进入路口
- 内部路径:在路口内部的行驶轨迹(通常是多条曲线)
- 退出连接:从路口进入哪条车道
你想想看,一个十字路口,每个方向有 3 条车道。左转、直行、右转,再加上掉头。那这个路口内部就有 3×4×3 = 36 条可能的行驶路径。每条路径都是一条曲线。这数据量,啧啧。
// 路口连接模型
typedef struct {
uint64_t junction_id; // 路口 ID
uint64_t in_lane_id; // 进入车道 ID
uint64_t out_lane_id; // 退出车道 ID
Polyline3D path; // 内部行驶路径
float path_length; // 路径长度
TurnType turn_type; // 转向类型(左转、右转、直行、掉头)
uint32_t priority; // 优先级(用于路径规划)
} JunctionConnection;
4.4 拓扑关系:把「死」数据变成「活」路网
数据模型再漂亮,如果没有拓扑关系,那就是一堆死数据。拓扑关系说白了就是「谁跟谁连着」。车道和车道之间、车道和路口之间、路口和道路之间,都得连起来。
我建议你把拓扑关系想象成一个有向图。每个节点是一条车道,每条边是一个连接关系。这个连接关系包含:前驱车道、后继车道、可变更车道(左变道、右变道)。
// 车道拓扑关系
typedef struct {
uint64_t lane_id; // 当前车道
uint64_t predecessor_id; // 前驱车道(进入当前车道的前一条车道)
uint64_t successor_id; // 后继车道(从当前车道出去的下一条车道)
uint64_t left_neighbor_id; // 左侧相邻车道(用于变道)
uint64_t right_neighbor_id; // 右侧相邻车道
bool can_change_left; // 是否允许向左变道
bool can_change_right; // 是否允许向右变道
uint64_t junction_id; // 如果当前车道连接路口,记录路口 ID
} LaneTopology;
这里有个坑:前驱和后继不是一一对应的。一条车道可能有多条前驱(比如两条车道汇入一条),也可能有多条后继(比如一条车道分叉成两条)。所以实际存储时,前驱和后继通常是一个列表,而不是单个 ID。
4.5 总结一下
数据模型设计,说白了就是「怎么把现实世界的路网,用计算机能理解的方式存下来」。NDS 和 OpenDRIVE 是两种主流思路,一个偏量产、一个偏仿真。车道模型要关注边界和宽度,路口模型要关注内部路径,拓扑关系要把所有东西串起来。
嗯,这节内容就到这。下一节我们会聊数据存储与索引——怎么在嵌入式平台上高效地读写这些数据。到时候我会分享一个我亲手优化的 B+ 树索引方案,性能提升 5 倍以上。