BEVFormer+Det/MaptrV2多任务感知map真值生成
前言:本文围绕自定义多任务感知模型(bevformer+det/maptrv2)的真值生成链路,系统说明 MapTRv2 风格地图 GT 是如何从 nuScenes 中间格式、地图 JSON 和位姿信息中在线生成的。文章重点回答三个问题:
- 生成 MapTRv2 的 GT 需要哪些数据;
- 这些数据在生成时分别怎么用;
- 对应到当前工程,代码分别落在哪些文件里。
现阶段代码见 https://github.com/HankerSia/Apollo-Vision-Net.git
目录
- 总述
- 1.当前配置到底在做什么
- 2.需要哪些数据
- 3.完整真值生成流程
- 4.位姿信息的具体作用
- 5.地图 JSON 的实际组织方式
- 6.中心线拓扑是怎么做出来的
- 7.训练时最终写进 batch 的 GT 长什么样
- 8.常见问题
总述
本文面向在线生成MapTRv2 模型需要的监督数据构建,即在给定样本位姿和 map_location 的前提下,如何从静态地图 JSON 中裁取局部地图,并将其中的几何结构转成监督信号。其核心流程可以概括为四步:
- 离线整理 nuScenes 原始样本,生成
nuscenes_infos_temporal_{train,val}.pkl等中间索引文件; - 训练时根据
map_location和位姿信息,从data/nuscenes/maps/expansion/<location>.json中裁取局部地图 patch; - 将 patch 内的线、面、拓扑结构转成固定点数的向量监督,得到
gt_map_vecs_label和gt_map_vecs_pts_loc; - 由 map head 在 one-to-one / one-to-many 查询框架下完成匹配、回归和损失计算。
1. 配置说明
当前配置 projects/configs/bevformer/bev_tiny_det_mapv2.py,在 base 配置 projects/configs/bevformer/bev_tiny_det_map_apollo.py 的maptr基础上新增了含有拓扑信息的centerline类别,新增了seg损失,修改了decoder等,对齐Maptrv2核心升级点。
概括而言,官方 MapTRv2 与当前配置的差异主要体现在以下几项:
- MapTRv2 的常见 nuScenes 配置是
ResNet50 + FPN + LSSTransform; - 当前 base 配置采用Apollo-Vision-Net典型结构:
DLA + SECONDFPNV2 + BEVFormerEncoder; - 类别配置对齐Maptrv2,新增
centerline; - 对齐MapTRv2 采用
one-to-one + one-to-many的 query 组织,增加map_aux_seg辅助分割用于模型监督训练。
2. 需要哪些数据
MapTRv2 的 GT 生成,真正依赖的是三类数据:
2.1 nuScenes 原始地图文件
目录结构:
/home/nuvo/Apollo-Vision-Net/data/nuscenes/maps/expansion/
├── boston-seaport.json
├── singapore-hollandvillage.json
├── singapore-onenorth.json
├── singapore-queenstown.json
这里的 JSON 是 nuScenes map expansion 格式,文件后缀是 .json。
当前工程通过以下方式读取它们:
NuScenesMap(dataroot=self.data_root, map_name=location)- 其中
self.data_root通常是data/nuscenes location则来自 infos 里的map_location
这部分代码落在:
2.2 nuScenes 中间格式 infos
目录:
/home/nuvo/Apollo-Vision-Net/data/nuscenes/
├── nuscenes_infos_temporal_train.pkl
├── nuscenes_infos_temporal_val.pkl
└── nuscenes_infos_temporal_test.pkl # 如果你生成 test split
这些 pkl 是中间格式,不是地图真值本身,而是“每个 sample 的元信息索引表”。
MapTRv2 生成 GT 时主要使用的字段包括:
tokenscene_tokenscene_namemap_locationlidar2ego_translationlidar2ego_rotationego2global_translationego2global_rotationcan_buscamssweeps
这些字段由离线 converter 生成,入口在:
2.3 当前样本的时序图像与标定信息
训练时数据集还会读到:
- 6 相机图像
- lidar top 传感器位姿
- ego pose
- can bus
- 历史 sweep
这些信息在 prepare_train_data(...) 里被整理成样本,然后再注入 map GT。
3. 完整真值生成流程
这一节按代码执行顺序讲。
3.1 第一步:离线生成 infos
入口命令:
python tools/create_data.py nuscenes \
--root-path ./data/nuscenes \
--out-dir ./data/nuscenes \
--extra-tag nuscenes \
--version v1.0 \
--canbus ./data
该命令最终会调用:
tools/data_converter/nuscenes_converter.py::create_nuscenes_infos(...)
这里会把每个 sample 的原始信息整理成一条 info,写入:
data/nuscenes/nuscenes_infos_temporal_train.pkldata/nuscenes/nuscenes_infos_temporal_val.pkl
其中最重要的是:
map_location- 通过
scene_token -> scene -> log -> location得到。
- 通过
lidar2ego_*/ego2global_*- 位姿信息。
can_bus- 由 nuScenes can bus 数据读取。
3.2 第二步:数据集读取 infos,准备训练样本
数据集主入口是:
训练时会进入:
prepare_train_data(index)
其流程是:
- 从
self.data_infos[index]读出 info; - 补齐
scene_name、map_location、lidar2ego_*; - 执行原有 pipeline;
- 在 pipeline 后调用
_add_vectormap_gt(...); - 把地图真值写回 sample。
关键注入字段:
gt_map_vecs_labelgt_map_vecs_pts_loc
3.3 第三步:根据位姿选地图并裁剪 patch
在 _add_vectormap_gt(...) 中会先做:
- 解析当前 sample 的地图位置
location - 读取位姿:
lidar2ego_rotationlidar2ego_translationego2global_rotationego2global_translation
- 组合出
lidar2global
然后调用:
self.vector_map.gen_vectorized_samples(location, lidar2global_translation, lidar2global_rotation)
这里的 vector_map 在 V2 中是:
VectorizedLocalMapV2
代码位置:
3.4 第四步:在地图里生成四类向量 GT
VectorizedLocalMapV2.gen_vectorized_samples(...) 会依次生成:
- divider
- ped_crossing
- boundary
- centerline
最后把它们统一打包成:
gt_vecs_labelgt_vecs_pts_loc
随后在 dataset 里写成:
gt_map_vecs_labelgt_map_vecs_pts_loc
4. 位姿信息的具体作用
位姿信息不是“直接变成标签”,而是用于选地图、裁 patch、做时序对齐。
4.1 位姿公式要怎么理解
- 采用的列向量约定:
lidar2global = ego2global @ lidar2ego
也就是:
p_global = ego2global @ lidar2ego @ p_lidar
这与代码一致:
- tools/data_converter/nuscenes_converter.py
- projects/mmdet3d_plugin/datasets/nuscenes_det_occ_map_dataset.py
4.2 位姿如何用于选地图
在 _add_vectormap_gt(...) 里:
- 先决定当前 sample 属于哪张地图。
- 优先用
map_location - 若缺失,则用
scene_name反查
- 优先用
- 用
map_location在self.vector_map.map_explorer中找到对应地图实例:
self.vector_map.map_explorer[location].map_api
- 用位姿构造 patch:
map_pose = lidar2global_translation[:2]
rotation = Quaternion(lidar2global_rotation)
patch_box = (map_pose[0], map_pose[1], self.patch_size[0], self.patch_size[1])
patch_angle = quaternion_yaw(rotation) / np.pi * 180
- 在这个 patch 里取地图几何,生成 vector GT。
4.3 位姿如何用于历史帧对齐
在 detector 前向里:
训练时:
len_queue = img.size(1)
prev_img = img[:, :-1, ...]
img = img[:, -1, ...]
prev_img_metas = copy.deepcopy(img_metas)
if self.keep_bev_history:
prev_bev = self.obtain_all_history_bev(prev_img, prev_img_metas)
else:
prev_bev = self.obtain_history_bev(prev_img, prev_img_metas)
测试时:
if self.can_bus_in_dataset:
tmp_pos = copy.deepcopy(img_metas[0][0]['can_bus'][:3])
tmp_angle = copy.deepcopy(img_metas[0][0]['can_bus'][-1])
if self.prev_frame_info['prev_bev'] is not None:
img_metas[0][0]['can_bus'][:3] -= self.prev_frame_info['prev_pos']
img_metas[0][0]['can_bus'][-1] -= self.prev_frame_info['prev_angle']
作用是:
- 使用
can_bus和历史保存的位姿 - 计算当前帧与上一帧的相对运动
- 把历史 BEV 对齐到当前帧坐标系
位姿信息主要用于两类任务:
- 地图 GT 生成:定位当前样本在地图中的位置
- 历史 BEV 对齐:把上一帧 BEV 变换到当前帧
5. 地图 JSON 的实际组织方式
当前这套代码依赖的是 nuScenes map expansion 风格的 JSON。
这类 JSON 不是单一表,而是由点、线、面、语义对象和拓扑关系共同组成的层级结构。
以 data/nuscenes/maps/expansion/boston-seaport.json 为例,顶层会包含:
versionpolygonlinenoderoad_segmentroad_blocklaneped_crossingwalkwaystop_linecarpark_arearoad_dividerlane_dividertraffic_lightcanvas_edgeconnectivityarcline_path_3lane_connector
实际字段可以从样例中直接看到。例如:
token、polygon_token、node_tokens、line_token本质上都是 UUID (python import uuid;uuid.uuid4())风格的唯一标识符,用于在不同表之间建立引用关系。它们由 nuScenes 地图导出或标注流程生成。road_segment记录包含自身token,并通过polygon_token引用对应面对象;例如:token = 00683936-1a08-4861-9ce5-bb4fc753dadapolygon_token = bea6cf31-59e5-48d9-8ca1-28312b5313d1
connectivity记录以对象token作为 key,并显式保存incoming/outgoing;例如:d190c816-c71b-4db2-9913-5f58d0b2c72dincoming = [5c4ddfe1-21d3-4e91-bc85-23b8a4e6f855]outgoing = [5e13747e-4ea8-422f-b286-ff3cd0a0f941, 8b7f8488-703b-4b0d-8de9-871bc0393ea7, f83b33f4-f455-4801-b38e-fded988784c3]
5.1 点、线、面三层
node- 最底层点,包含
x/y,带token
- 最底层点,包含
line- 一串
node_tokens组成的线
- 一串
polygon- 一串
exterior_node_tokens组成的面,可能还有holes
- 一串
5.2 语义对象层
lane- 引用
polygon_token - 还带
from_edge_line_token/to_edge_line_token
- 引用
lane_connector- 引用
polygon_token
- 引用
road_divider/lane_divider- 引用
line_token
- 引用
ped_crossing- 引用
polygon_token
- 引用
road_segment- 引用
polygon_token
- 引用
5.3 拓扑层
connectivity- 为每个对象 token 记录
incoming/outgoing
- 为每个对象 token 记录
arcline_path_3- 提供中心线的参数化路径,供离散化和采样使用
这些 token / 拓扑结构的作用,就是支持:
map_api.extract_polygon(...)map_api.discretize_lanes(...)map_api.get_incoming_lane_ids(...)map_api.get_outgoing_lane_ids(...)
6. 中心线拓扑是怎么做出来的
V2 版本和普通 MapTR 之间最大的差异之一,就是 centerline 类。
6.1 centerline 的语义来源
当前 V2 实现把 centerline 定义为:
lane_connectorlane
也就是这两类的中心路径。相关代码在:
6.2 单条中心线是怎么拿到的
在 _get_centerline(...) 里:
- 找到当前 patch:
patch = self.map_explorer[location].get_patch_coord(patch_box, patch_angle)
- 从 map API 中取对应 layer 的 records:
records = getattr(map_api, layer_name)
- 取
polygon_token并裁剪到 patch:map_api.extract_polygon(polygon_token)
- 调用:
map_api.discretize_lanes([record['token']], 0.5)
- 变成
LineString,再裁剪到 patch - 做坐标变换
6.3 拓扑如何把多段中心线串起来
这一段是核心:
- 每条中心线记录都会带:
incoming_tokensoutgoing_tokens
union_centerline(...)会:- 把每条线自身的点顺序加入有向图
nx.DiGraph() - 再把前驱/后继 lane 的端点也连进来
- 最终从 root 到 leaf 搜索完整路径
- 把每条线自身的点顺序加入有向图
这样做的结果是:
- 不只是“局部 lane 片段”
- 而是“按拓扑拼接后的完整中心路径”
拓扑关系在 GT 构造中的体现如下:
- 拓扑不是额外输出一个 graph tensor
- 而是直接影响 GT 折线的构造方式
6.4 最终中心线 GT 的类别
centerline 最终被写成类别 3:
CLASS2LABEL = {
'centerline': 3,
}
因此,训练 batch 中的 gt_map_vecs_label 会包含 3,对应 centerline 类别。
7. 训练时最终写进 batch 的 GT 长什么样
经过数据集和 pipeline,最终会写进训练 batch 的字段主要是:
gt_bboxes_3dgt_labels_3dgt_map_vecs_labelgt_map_vecs_pts_locimgimg_metas
7.1 gt_map_vecs_label
这是每条向量对应的类别 id。
当前 V2 的类别编号是:
- 0:divider
- 1:ped_crossing
- 2:boundary
- 3:centerline
7.2 gt_map_vecs_pts_loc
这是每条向量的点序列。
当前约定是:
- 每条 polyline 固定采样
map_num_pts = 20个点 - 因此其形状通常为:
N × 20 × 2
7.3 训练中的实际使用方式
在 head 的 loss 里:
- 主分支做 one-to-one matching
- 辅分支做 one-to-many 监督
- 辅助分割头还会用
gt_map_vecs_pts_loc构建 BEV / PV segmentation target
相关代码:
8. 常见问题
8.1 为什么一定要 map_location?
因为 nuScenes 有多张地图,boston-seaport、singapore-onenorth 等不是统一一张图。没有 map_location,数据集不知道该加载哪一个 maps/expansion/<location>.json。
8.2 为什么 scene_name 还能用?
因为部分 legacy infos 里可能没有 map_location,代码里会用 scene_name 反查 map_location,作为兜底。
8.3 为什么要用 ego2global @ lidar2ego?
因为当前实现采用列向量约定:先 lidar 到 ego,再 ego 到 global。顺序不能反。
8.4 为什么 centerline 要用拓扑拼接?
因为单个 lane 片段不足以表达道路连续结构。centerline 的目标是把 lane / lane_connector 的连接关系变成更完整的中心路径。
8.5 这个流程里有没有离线 map GT pkl?
当前这条链路没有必须的离线 map GT pkl。主要是:
- 离线生成 infos
- 在线生成 vector GT
附录:最关键的文件路径
- 配置:
- 数据集:
- 检测器:
- 离线转换:
- 可视化:
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)