前言:本文围绕 det(3D 检测) 与 map(MapTR 实时地图生成) 两个感知任务并行模型设计,系统梳理 Apollo-Vision-Net 中 det+map 多任务模型的“设计 → 配置 → 源码实现 → 评测闭环 → 复现结果”。目标是让读者能够对照文档完成模型复现、测试,并明确每个 cfg 字段对应的源码位置、推理输出的接口规范与设计理由。
-
[Modified]projects/mmdet3d_plugin/maptrv2/dense_heads/bevformer_det_map_head_apollo_v2.py- MapTRv2 decoder 改为默认严格模式:配置了
map_decoder后,若 decoder 构建或执行失败,将直接报错,不再静默退回 fallback MLP head。 - 新增 decoder 合约校验:
map_num_query == num_map_vec * map_num_pts、cls/reg branches数量覆盖num_dec、dec_states/dec_references必须为 4 维,并明确提示return_intermediate=True缺失问题。 - fallback 仅允许白名单初始化类异常;非白名单异常一律抛出,避免实验配置与实际执行路径不一致。
- 修复
loss_anchor兜底路径在one2many_preds is None时可能二次报错的问题。
- MapTRv2 decoder 改为默认严格模式:配置了
-
[Modified]projects/mmdet3d_plugin/bevformer/dense_heads/bevformer_det_map_head_apollo.py- 将 map 主分支聚合项从
loss_map调整为仅用于日志展示的map_main_total,避免与子项同时参与_parse_losses时发生重复累计。
- 将 map 主分支聚合项从
-
[Modified]projects/mmdet3d_plugin/maptrv2/dense_heads/bevformer_det_map_head_apollo_v2.py- 统一 map 损失记账方式:保留真正参与反传的
loss_map_*项,将汇总项拆为日志字段,新增map_total用于观察地图分支总体趋势。
- 统一 map 损失记账方式:保留真正参与反传的
-
[Modified]projects/mmdet3d_plugin/bevformer/hooks/det_map_text_logger_hook.py- logger 兼容新的
map_*汇总字段,保证map_main_total、map_total、map_o2m_*仍按 map 分类输出。
- logger 兼容新的
-
[Modified]projects/mmdet3d_plugin/core/evaluation/eval_hooks.py- 修复训练期分布式评估阶段的 rank 同步问题:此前
CustomDistEvalHook只在 rank 0 执行dataset.evaluate()/evaluate_map(),其他 rank 在结果收集后会提前回到训练循环。 - 当 rank 0 仍在执行耗时 bbox/map 评估时,非 0 rank 进入下一轮训练 loss,触发
reduce_mean/all_reduce等 collective;由于 rank 0 未参与同一 collective,最终出现Watchdog caught collective operation timeout: WorkNCCL(OpType=ALLREDUCE, Timeout(ms)=1800000)。 - 新增基于
.eval_hook_sync的文件哨兵同步:rank 0 完整完成 bbox/map 评估与save_best后写入.done;若评估异常则写入.fail;非 0 rank 在 CPU 侧轮询等待,不再提前进入下一轮训练。 - 该同步刻意不使用
dist.barrier():map 评估可能超过 NCCL 默认 30 分钟 watchdog,长时间占用 NCCL collective 仍可能触发超时;文件哨兵只用于训练循环重入前的跨 rank 对齐。 - 可通过环境变量调整等待行为:
AVN_EVAL_SYNC_TIMEOUT控制最长等待秒数(默认 24 小时),AVN_EVAL_SYNC_POLL_INTERVAL控制轮询间隔秒数(默认 5 秒)。
- 修复训练期分布式评估阶段的 rank 同步问题:此前
-
评估阶段 NCCL timeout 的判定与处理建议
- 典型日志特征:det bbox 评估已完成并打印
mAP/NDS,随后进入[map_eval] formatting class-wise inputs或 map 指标计算;同时非 0 rank 报ALLREDUCEtimeout,并伴随[det_map][det] loss failed/[det_map][maptr_official] map loss failed。 - 这类报错不表示 loss 本身先出错,而是非 0 rank 已经提前进入下一轮训练 loss,同步等待不到仍在评估的 rank 0。
- 若异常退出后
nvidia-smi显示部分 GPU 显存未释放但 Processes 为空,通常是容器/驱动侧残留 CUDA 上下文;先用fuser -v /dev/nvidia*查占用,必要时重启容器,或在确认无其他任务时执行nvidia-smi --gpu-reset -i <gpu_ids>。
- 典型日志特征:det bbox 评估已完成并打印
-
当前建议读取的 map 日志字段
map_total:地图分支综合损失map_main_total:one-to-one 主分支损失和loss_map_o2m:one-to-many 分支损失和loss_map_seg/loss_map_pv_seg:辅助分割损失
- 0.复现准备(环境、数据、权重)
- 1.目标、边界与关键工程决策
- 2.总体设计:det 与 map 的数据流与输出接口与格式规范
- 3.配置设计:如何在 cfg 中表达 det+map(以及如何映射到代码)
- 4.实现改动:按模块拆解实际代码如何落地
- 5.评测协议与落盘产物:bbox 与 map 各自怎么评、产物在哪里
- 6.复现命令与测试结果
- 7.常见坑与排障清单
- 8.后续迭代路线
- 9.工程化对账表与回归清单
这一节的目标是:让第一次接触本工程的人也能按步骤把 det+map 多任务链路跑通。
coda环境名不限,下文用 apollo_vnet 作为示例。我自己环境实际安装步骤如下:
conda create -n apollo_vnet python=3.8
conda activate apollo_vnet
export PYTHONPATH=$PYTHONPATH:"/" #报错ModuleNotFoundError: No module named 'tools'时执行
pip install torch==1.10.0+cu113 torchvision==0.11.0+cu113 torchaudio==0.10.0 -f https://download.pytorch.org/whl/torch_stable.html
pip install mmcv-full==1.4.0 -f https://download.openmmlab.com/mmcv/dist/cu113/torch1.10.0/index.html
pip install mmdet==2.14.0
pip install mmsegmentation==0.14.1
# 源码安装mmdet3d-v0.17.1版本
cd bevformer
# 下载mmdetection3d github加速代理https://ghproxy.com/
git clone https://github.com/open-mmlab/mmdetection3d.git
# 进入mmdetection3d目录
cd mmdetection3d
# 切换v0.17.1
git checkout v0.17.1
# 安装mmdet3d-v0.17.1版本
python setup.py install
pip install numba==0.48.0
pip install numpy==1.21.0 # 报错 numpy has no module "long"时执行
pip install lyft_dataset_sdk如果你本机已经安装了 mmdet3d(或正在使用 MapTR 仓库内的 mmdet3d),建议先打印一次实际 import 的来源,避免“import 到了另一个 mmdet3d”导致的 registry/PIPELINES 不一致:
python -c "import mmdet3d; print('mmdet3d from:', mmdet3d.__file__)"将 nuScenes v1.0 数据与 can_bus 按如下组织(与 docs/prepare_dataset.md 一致):
Apollo-Vision-Net
├── data/
│ ├── can_bus/
│ ├── nuscenes/
│ │ ├── maps/
│ │ ├── samples/
│ │ ├── sweeps/
│ │ ├── v1.0-test/
│ │ ├── v1.0-trainval/
det+map 配置默认使用时序 infos(nuscenes_infos_temporal_train.pkl / nuscenes_infos_temporal_val.pkl)。若你还没有这些文件,执行:
conda activate apollo_vnet
python tools/create_data.py nuscenes \
--root-path ./data/nuscenes \
--out-dir ./data/nuscenes \
--extra-tag nuscenes \
--version v1.0 \
--canbus ./data执行完成后应至少包含:
data/nuscenes/nuscenes_infos_temporal_train.pkldata/nuscenes/nuscenes_infos_temporal_val.pkl
本工程的 det+map 配置不要求你提前离线生成一份“maptr 训练标注 pkl/json”。
训练阶段 map GT 的生成逻辑是:
- 输入:nuScenes 原始地图(
data/nuscenes/maps)+ 每个 sample 的位姿(lidar2ego_*/ego2global_*)+ 当前场景的地图 location(map_location或可由scene_name推导)。 - 处理:以
pc_range推导的patch_size为裁剪窗口,在全局地图中裁剪并提取 divider/ped_crossing/boundary 三类几何,再变换到 LiDAR 局部坐标系。 - 输出(写回 sample):
gt_map_vecs_label:每条 polyline 的类别(0/1/2)gt_map_vecs_pts_loc:每条 polyline 的点序列(在评测/训练协议里会重采样为固定点数)
对应源码入口(强烈建议读一遍,避免“照文档跑但不理解 GT 从哪来”):
- dataset:
projects/mmdet3d_plugin/datasets/nuscenes_det_occ_map_dataset.pyprepare_train_data(...):在 pipeline 完成后调用self._add_vectormap_gt(example, input_dict)把 map GT 注入到样本。_add_vectormap_gt(...):计算lidar2global,然后调用self.vector_map.gen_vectorized_samples(...)完成在线矢量化。
简化版关键代码(与当前实现一致,便于你定位调试点):
# projects/mmdet3d_plugin/datasets/nuscenes_det_occ_map_dataset.py
example = self.pipeline(input_dict)
example = self._add_vectormap_gt(example, input_dict)补充说明(避免被 pipeline 顺序误导):
- 由于注入发生在
self.pipeline(...)之后,gt_map_vecs_*可能并不是由 pipeline 的Collect3D/CustomCollect3D“收集出来”的,而是 dataset 在返回前额外补充到example里。 - 因此:
- 如果你的收集算子是严格版(缺 key 直接
KeyError),不要在Collect3D/CustomCollect3D.keys里硬性要求gt_map_vecs_*; - 本工程使用的
CustomCollect3D允许 keys 缺失跳过(见docs/changes/2026-01-15-det-map-port.md的说明),用于兼容这种“pipeline 后注入”的在线 GT。
- 如果你的收集算子是严格版(缺 key 直接
# projects/mmdet3d_plugin/datasets/nuscenes_det_occ_map_dataset.py
anns_results = self.vector_map.gen_vectorized_samples(location, lidar2global_translation, lidar2global_rotation)
example['gt_map_vecs_label'] = ...
example['gt_map_vecs_pts_loc'] = ..._add_vectormap_gt(...) 至少需要以下字段(来自 get_data_info(index) 或 data_infos[index]):
- 地图 location:
map_location(优先)或scene_name(可推导为真实 location) - 位姿:
lidar2ego_rotation/lidar2ego_translation/ego2global_rotation/ego2global_translation
如果你遇到类似报错:
Missing map_location/scene_name in input_dict:说明 infos 不包含 location 信息,通常需要重新生成 infos 或检查 converter。Unknown nuScenes map location:说明 location 不在 nuScenes 支持的 4 个地图名里(boston-seaport / singapore-xxx)。
cfg 里的 fixed_ptsnum_per_line(例如 20)不仅影响 pred 的格式,也影响 GT 的重采样协议。在线矢量化得到的 shapely LineString 会被重采样为固定点数,保证训练/评测的输入输出可对齐。
本文的 smoke 配置 projects/configs/bevformer/bev_tiny_det_map_apollo.py 引用了预训练权重与待评测 checkpoint:
- 预训练权重(示例路径):
ckpts/depth_pretrained_dla34-y1urdmir-20210422_165446-model_final-remapped_bev.pth
- 待评测 checkpoint:
- 你可以用自己的训练产物(如
work_dirs/.../epoch_1.pth),也可以复用已有实验目录下的 checkpoint。
- 你可以用自己的训练产物(如
如果缺少权重,tools/test.py 会在 load_checkpoint 阶段直接报错;这是最常见的“照文档跑不起来”的原因之一。
在正式跑 dist_test 前,建议先确认 cfg 能 build dataset 与 model(排除 registry 未注册、数据路径错误):
conda activate apollo_vnet
python -c "from mmcv import Config; from mmdet3d.datasets import build_dataset; from mmdet3d.models import build_model; cfg=Config.fromfile('projects/configs/bevformer/bev_tiny_det_map_apollo.py'); ds=build_dataset(cfg.data.val); m=build_model(cfg.model, test_cfg=cfg.get('test_cfg')); print(type(ds), 'evaluate_map' in dir(ds), type(m))"额外建议:在训练前先验证一次“在线 map GT 是否真的生成且被 pipeline 收集到”。
conda activate apollo_vnet
python - <<'PY'
from mmcv import Config
from mmdet3d.datasets import build_dataset
cfg = Config.fromfile('projects/configs/bevformer/bev_tiny_det_map_apollo.py')
ds = build_dataset(cfg.data.train)
sample = ds[0]
keys = list(sample.keys())
print('sample keys:', keys)
assert 'gt_map_vecs_label' in sample, 'missing gt_map_vecs_label'
assert 'gt_map_vecs_pts_loc' in sample, 'missing gt_map_vecs_pts_loc'
lab = sample['gt_map_vecs_label'].data
pts = sample['gt_map_vecs_pts_loc'].data
print('gt_map_vecs_label type:', type(lab))
print('gt_map_vecs_pts_loc type:', type(pts))
if hasattr(pts, 'fixed_num_sampled_points'):
t = pts.fixed_num_sampled_points
print('fixed_num_sampled_points:', t.shape)
else:
print('gt_map_vecs_pts_loc tensor shape:', getattr(pts, 'shape', None))
PY若提示 dataset/head “not in registry”,优先检查 cfg 是否启用了 plugin=True,以及你的 PYTHONPATH 是否包含仓库根目录。
训练测试:
conda activate apollo_vnet && ./tools/dist_test.sh ./projects/configs/bevformer/bev_tiny_det_map_apollo.py ./ckpts/depth_pretrained_dla34-y1urdmir-20210422_165446-model_final-remapped_bev.pth 1- 默认多任务:同一次推理同时产出 det 与 map 两条结果。
- 同一次测试闭环:同一次
dist_test同时跑通- nuScenes bbox evaluator(NDS/mAP/ATE/ASE/AOE/AVE/AAE 等)
- MapTR 协议 map evaluator(Chamfer mAP + IoU mAP)
- 不追求 map 任务的最终精度:当前以“链路闭环 + 协议对齐”为第一优先级。
- 不一次性引入 MapTR 全量 head/assigner/loss/CUDA op:先以 scaffold 骨架保证可训练、可推理、可评测。
bbox_results与map_results必须严格分离
- bbox evaluator 只接受检测结构(
boxes_3d/scores_3d/labels_3d),map 的 dict 混进来就会导致 bbox evaluator 误解析并报错(典型:KeyError: 'boxes_3d')。
- 默认就是 det+map;不再保留“stage 分支”
- 推理层面默认返回
{'bbox_results': ..., 'map_results': ...}。 - 是否启用 det/map 分支由 head 内部开关控制(cfg 中
enabling_det/enabling_map),而不是靠外层脚本分 stage。
- 离线评测是否可靠,关键在 registry,不在 mmdet3d 来源
- 离线失败最常见原因:cfg 的 plugin/custom_imports 没有执行,导致自定义 dataset/head 没注册。
- 所以离线入口必须复刻
tools/test.py的 plugin 导入逻辑(本工程用tools/eval_map_offline.py固化)。
运行约束:本文所有命令默认以激活你的 conda 环境为前置(下文示例为 conda activate apollo_vnet)。
这里的 “baseline” 指 Apollo-Vision-Net 里 仅 det(或 det+occ) 能跑通测试闭环,但 map(向量地图) 还未形成“推理产物 → 落盘 → 评测”的完整链路的状态。
-
输出接口规范层(首要)
- BEVFormer 推理返回值从“单一 bbox 结果”升级为“bbox_results + map_results 并行返回”。
- 强制
bbox_results与map_results分离,避免 bbox evaluator 误解析 map dict。
-
DDP 收集层(multi-gpu test collect)
- DDP 测试收集逻辑扩展为支持
map_results的独立收集,避免与 bbox/occ/mask 的 tmpdir 分片互相覆盖。
- DDP 测试收集逻辑扩展为支持
-
测试入口层(tools/test.py)
- 增加
map_results.pkl的 dump(与 bbox 结果分开落盘)。 - 当
--eval包含chamfer/iou时,调用 dataset 的evaluate_map(...)完成 MapTR 协议评测。
- 增加
-
Dataset & Evaluator 层(MapTR protocol)
- Dataset 增加
evaluate_map/format_map_results/_format_map_gt:- pred:把
map_results统一写成 MapTR 兼容的nuscmap_results.json。 - gt:必要时自动生成
nuscenes_map_anns_val.json。
- pred:把
- 引入/复用
map_utils的 chamfer/iou mAP 计算协议(阈值、match 规则、加速策略)。
- Dataset 增加
-
配置层(cfg)
- 新增 det+map 的 smoke 配置
projects/configs/bevformer/bev_tiny_det_map_apollo.py。 - 通过 head 内部开关
enabling_det/enabling_map控制分支启用,不再靠外层 stage 分支。
- 新增 det+map 的 smoke 配置
-
离线复测入口(可复现性)
- 新增离线 map 评测脚本
tools/eval_map_offline.py,固化“plugin import/registry 注册”的前置条件,避免离线评测随机失败。
- 新增离线 map 评测脚本
BEVFormer detector 负责把同一次 forward 的输出拆成两条并行产物:
- det:走原有
get_bboxes→ 生成 nuScenes evaluator 需要的结构 - map:调用 head 的
get_map_results→ 生成 MapTR evaluator 能格式化的结构
BEVFormer 在推理阶段返回:
({
'bbox_results': bbox_results, # list[dict(pts_bbox=...)] 或 None
'map_results': map_results, # list[dict(vectors,scores,labels,...)] 或 None
}, occ_results)其中:
bbox_results- 维持 MMDet3D/nuScenes evaluator 的传统结构。
- 下游只允许 bbox evaluator 读取它。
map_results- 与
bbox_results并行、同样按 sample 对齐。 - 最小字段:
vectors:(N, P, 2)的 polyline 点序列(P 通常为fixed_ptsnum_per_line)scores:(N,)(由sigmoid(map_cls_logits).max(-1)得到)labels:(N,)(同上,取 argmax 类别)- 可选:
cls_logits
- 与
备注:当前阶段 map 分支是 scaffold,vectors 的坐标语义还没有严格对齐 MapTR(例如是否映射到米制坐标系、是否裁剪到 pc_range 等),所以指标偏低是预期现象;本文关注的是链路闭环与协议对齐。
- bbox evaluator 只看
bbox_results,并且读取的是固定 key(boxes_3d/...)。 - map evaluator 只看
map_results,并且由 dataset 的format_map_results写成 MapTR json,再交给map_utils计算。 - 因为两条链路从 detector 返回时就分开,所以不会出现“map dict 被当 bbox dict 解析”的问题。
本次闭环使用的配置:projects/configs/bevformer/bev_tiny_det_map_apollo.py。
- plugin 与 registry(不然很多类找不到)
plugin=Trueplugin_dir='projects/mmdet3d_plugin/'
- 模型结构(det+map head + 双 decoder)
model.type='BEVFormer'model.pts_bbox_head.type='BEVFormerDetMapHeadApollo'model.pts_bbox_head.transformer.det_decoder=...model.pts_bbox_head.transformer.map_decoder=...(type='MapTRDecoder')model.pts_bbox_head.enabling_det=True/enabling_map=True
- dataset 与 map 评测需要的上下文
dataset_type='CustomNuScenesDetMapDataset'fixed_ptsnum_per_line=20(点数协议)queue_length=3(时序 BEV)
-
model.type='BEVFormer'- 代码:
projects/mmdet3d_plugin/bevformer/detectors/bevformer.py - 作用:推理时拆分并返回
bbox_results/map_results。
- 代码:
-
model.pts_bbox_head.type='BEVFormerDetMapHeadApollo'- 代码:
projects/mmdet3d_plugin/bevformer/dense_heads/bevformer_det_map_head_apollo.py - 作用:det 分支复用
BEVFormerHead,map 分支提供 decoder 优先、MLP fallback,并实现get_map_results。
- 代码:
-
transformer.map_decoder.type='MapTRDecoder'- 代码:
projects/mmdet3d_plugin/maptr/modules/decoder.py - 作用:让 map decoder 至少“能 build、能跑通接口”,当前阶段不依赖上游 CUDA op。
- 代码:
-
dataset_type='CustomNuScenesDetMapDataset'- 代码:
projects/mmdet3d_plugin/datasets/nuscenes_det_occ_map_dataset.py - 作用:提供
evaluate_map/format_map_results/_format_map_gt,把map_results评成 MapTR 协议的 mAP。
- 代码:
-
detector 级
model.only_det=False:允许 forward 同时产出 det + map(否则 map 分支不会走到)。model.video_test_mode=True/queue_length=3:时序 BEV;影响prev_bev缓存逻辑。
-
head 级(det+map 分支开关 + 输出规模)
model.pts_bbox_head.enabling_det/enabling_map:控制 det/map 分支是否启用。fixed_ptsnum_per_line=20:map 评测协议中的每条 polyline 固定采样点数(同时影响 GT 生成与 pred 格式化)。- (可选)
map_num_classes/map_num_pts/num_map_vec:当前 scaffold 支持这些参数(不配则走默认值)。
-
transformer 级(双 decoder)
transformer.det_decoder:用于 det。transformer.map_decoder:用于 map(type='MapTRDecoder')。当前实现会“能 build 则用;失败就 fallback 到 MLP”。
-
dataset 级(map evaluator 的关键上下文)
dataset_type='CustomNuScenesDetMapDataset':别名类,实际继承CustomNuScenesDetOccMapDataset。map_eval_nproc:map 评测多进程数(默认 8;离线复测可用--nproc 0强制单进程排障)。pc_range/map_ann_file/eval_use_same_gt_sample_num_flag:控制 MapTR 协议评测行为与 GT json 的位置。
- det+map 多任务 + scaffold 阶段,某些参数可能在某些 iteration 不参与反向(例如 map decoder 构建失败时走 MLP fallback,或某些 loss 暂时占位)。
- DDP 默认会把“未使用参数”当成错误,因此 cfg 中开启
find_unused_parameters=True能让 smoke 阶段更稳。
这一节不追求把每行代码抄出来,而是把“改动点 → 关键函数 → 输入输出 → 作用”讲清楚,方便你二次开发时快速下手。
- 文件:
projects/mmdet3d_plugin/bevformer/detectors/bevformer.py - 关键函数:
forward_test(...):调用simple_test(...)后返回{'bbox_results': ..., 'map_results': ...}。simple_test_pts(...):- det:
self.pts_bbox_head.get_bboxes(...)→bbox3d2result(...) - map:若 head 存在
get_map_results则调用;异常只打印一次并回退为None,防止刷屏导致日志不可读。
- det:
说明:forward_test(...) 的真实返回形态是:
bbox_results:simple_test(...)内部把每个 sample 的pts_bbox包装成dict(pts_bbox=...),所以最终是list[dict(pts_bbox=...)]。map_results:由 head 的get_map_results(outs, img_metas)返回,结构为list[dict(vectors,scores,labels,...)]。
- 文件:
projects/mmdet3d_plugin/bevformer/apis/test.py - 关键点:
custom_multi_gpu_test(...)识别 model 返回的 dict,并把map_results收集到独立 list。- CPU collect 时为不同产物使用不同的 tmpdir 后缀,避免与 bbox/mask/occ/flow 的分片结果混写。
- 注意:当前实现会在 tmpdir 名上按序追加后缀(例如先
_mask再_map),因此最终目录名可能呈现为..._mask_map之类的组合形式。
- 注意:当前实现会在 tmpdir 名上按序追加后缀(例如先
- 文件:
tools/test.py - 关键点:
map_results.pkl单独落盘到jsonfile_prefix/map_results.pkl。--eval中包含chamfer/iou才会调用dataset.evaluate_map(...)。- bbox 与 map 各自走各自 evaluator,不互相干扰。
- 文件:
tools/dist_test.sh - 关键点:
- 扫描
${@:4}是否含--eval。 - 只有未显式提供时才注入默认
--eval iou bbox。
- 扫描
- 文件:
projects/mmdet3d_plugin/datasets/nuscenes_det_occ_map_dataset.py - 关键方法:
format_map_results(results, jsonfile_prefix):- 逐 sample 写
nuscmap_results.json,并把labels映射到divider/ped_crossing/boundary。
- 逐 sample 写
evaluate_map(results, metric, jsonfile_prefix, ...):- 调用
format_map_results得到 pred json - 调用
_evaluate_map_single(...)用map_utils计算 AP/mAP。
- 调用
_format_map_gt():- 当
map_ann_file不存在时自动生成 GT json(默认data/nuscenes/nuscenes_map_anns_val.json)。
- 当
说明:以下为工程上需注意的两个关键细节:
-
样本顺序对齐要求:
format_map_results通过enumerate(results)读取self.data_infos[sample_id]['token']生成sample_token,因此results必须与数据集 / dataloader 的样本顺序严格一致(本工程的 DDP 收集逻辑已保证此点)。 -
map_results 的输入容错:dataset 在
_map_det_to_vector_list中做统一归一化,支持两种输入形式:{'vectors': (N,P,2), 'scores': (N,), 'labels': (N,)}{'vectors': [ {'pts':..., 'label':..., 'score':...}, ... ]}若缺失字段或结果为空,函数会返回空列表以保证评测过程不会中断。
- 目录:
projects/mmdet3d_plugin/datasets/map_utils/ - 关键点:
- Chamfer 阈值
[0.5, 1.0, 1.5]。 - IoU 阈值
0.5..0.95 step 0.05(polyline buffer 后求 IoU)。 - 按置信度排序,一个 GT 只匹配一次。
- multiprocessing 的 worker 需要可 pickling(已按顶层函数规避常见报错)。
- Chamfer 阈值
- 文件:
projects/mmdet3d_plugin/bevformer/dense_heads/bevformer_det_map_head_apollo.py - 核心思路:
- det:继承
BEVFormerHead,最大化复用现有 det 训练/推理。 - map:
- 优先尝试构建并运行
map_decoder(cfg 中transformer.map_decoder) - 失败则走 MLP fallback(用 BEV global 特征 + learnable query embedding 预测
map_cls_logits/map_pts)
- 优先尝试构建并运行
- 推理输出通过
get_map_results(outs, img_metas)标准化为(vectors,scores,labels)。
- det:继承
关键张量形状(便于二次开发与替换为完整 MapTR):
-
head forward 产物:
outs['bev_embed']:[bs, bev_h*bev_w, C]outs['map_cls_logits']:[bs, num_map_vec, map_num_classes]outs['map_pts']:[bs, num_map_vec, map_num_pts, 2]
-
get_map_results(...):prob = map_cls_logits.sigmoid(),然后scores, labels = prob.max(dim=-1)。- 每个 sample 返回一个 dict:
vectors:map_pts[i](numpy)scores:scores[i](numpy)labels:labels[i](numpy)cls_logits:map_cls_logits[i](numpy,可选)
- 目录:
projects/mmdet3d_plugin/maptr/ - 作用:注册
MapTRDecoder,让build_from_cfg(..., TRANSFORMER_LAYER_SEQUENCE)能工作。
- 文件:
tools/eval_map_offline.py - 关键点:
- Apollo root 放入
sys.path最前。 - 复刻 cfg 的 plugin/custom_imports 导入逻辑,确保 registry 注册。
- 打印
mmdet3d.__file__帮你确认实际 import 的来源。
- Apollo root 放入
- 主要产物:
pts_bbox/metrics_summary.jsonpts_bbox/results_nusc.json
- 主要产物:
map_results.pkl:推理的原始 map 结果(并行于 bbox)。nuscmap_results.json:由format_map_results写出的 MapTR 协议预测 json。data/nuscenes/nuscenes_map_anns_val.json:GT json(若不存在会自动生成)。
说明:smoke 阶段指标偏低是正常现象,本节只关注“链路闭环正确”。
conda activate apollo_vnet
PORT=29513 bash tools/dist_test.sh \
projects/configs/bevformer/bev_tiny_det_map_apollo.py \
work_dirs/smoke_det_map_retrain_20260206_v4/epoch_1.pth 1 \
--eval bbox chamfer iou历史输出目录示例:
test/bev_tiny_det_map_apollo/Fri_Feb__6_17_42_22_2026/
提示:tools/dist_test.sh 只有在你没有显式传入 --eval 时才会默认注入 --eval iou bbox,所以要跑 map 指标必须显式加 chamfer/iou。
bbox 指标(读取 pts_bbox/metrics_summary.json):
mean_ap = 0.0nd_score = 0.01823157683703287
map 指标(MapTR 协议,控制台输出或离线复测一致):
NuscMap_chamfer/mAP = 4.098719873329375e-06NuscMap_iou/mAP = 0.0
输入:test/bev_tiny_det_map_apollo/Fri_Feb__6_17_42_22_2026/map_results.pkl。
chamfer:
conda activate apollo_vnet
python tools/eval_map_offline.py \
projects/configs/bevformer/bev_tiny_det_map_apollo.py \
test/bev_tiny_det_map_apollo/Fri_Feb__6_17_42_22_2026/map_results.pkl \
--eval chamfer --nproc 0 \
--out-dir test/bev_tiny_det_map_apollo/Fri_Feb__6_17_42_22_2026/map_eval_offline_retest_20260209_chamfer关键输出:
mmdet3d.__file__ = /home/nuvo/MapTR/mmdetection3d/mmdet3d/__init__.pyNuscMap_chamfer/mAP = 4.098719873329375e-06
iou:
conda activate apollo_vnet
python tools/eval_map_offline.py \
projects/configs/bevformer/bev_tiny_det_map_apollo.py \
test/bev_tiny_det_map_apollo/Fri_Feb__6_17_42_22_2026/map_results.pkl \
--eval iou --nproc 0 \
--out-dir test/bev_tiny_det_map_apollo/Fri_Feb__6_17_42_22_2026/map_eval_offline_retest_20260209_iou关键输出:
NuscMap_iou/mAP = 0.0
目标检测:
|
地图生成:
|
下表列出训练/评测中常见的“症状 → 可能原因 → 快速修复”对照,便于现场排障:
| 症状 | 可能原因 | 修复 |
|---|---|---|
缺少 gt_map_vecs_label / gt_map_vecs_pts_loc(训练样本中) |
infos 中缺 map_location 或位姿字段;或 dataset 在 pipeline 后注入但 Collect3D 严格要求 keys |
重新生成 nuscenes_infos_temporal_*.pkl(确保包含 map_location 与位姿);或使用允许缺键的 CustomCollect3D;运行自检脚本验证 ds[0] 中存在键 |
报 Missing map_location/scene_name |
infos converter 未写入 location 字段 | 检查 tools/create_data.py 的 converter 参数并重新生成 infos |
报 Unknown nuScenes map location |
location 值拼写错误或不在 nuScenes 支持的地图名列表 | 校正 infos 中的 location 值为 nuScenes 的标准名称(例如 boston-seaport) |
pipeline/Collect3D 报 KeyError(因缺少 map 字段) |
收集算子在注入之前运行且严格要求 keys | 不在 Collect3D.keys 中强制加入 gt_map_vecs_*;使用本工程的 CustomCollect3D 或调整注入时序 |
| map 评测 mAP = 0 或结果全 0 | 预测和 GT 的坐标系/采样数不匹配(fixed_ptsnum_per_line)或裁剪窗口不一致 |
检查 cfg 中 fixed_ptsnum_per_line 与 pc_range;打印并比对 gt_map_vecs_pts_loc 与预测 vectors 的形状和坐标系 |
| 离线评测报 registry/ModuleNotFound(自定义 dataset/head 未注册) | plugin 没启用或 import 路径错误;import 到另一个 mmdet3d |
确认 cfg 中 plugin=True 且 plugin_dir='projects/mmdet3d_plugin/';运行 python -c "import mmdet3d; print(mmdet3d.__file__)" 验证来源 |
Shapely 报 TopologicalError/invalid geometry |
地图数据中存在自相交等不合法几何 | 在生成前对 geometry 做 geom = geom.buffer(0) 修复,或跳过该 patch 并记录 |
- bbox evaluator 报
KeyError: 'boxes_3d'
- 原因:把 map 结果混进 bbox 的结构。
- 解决:严格保持
bbox_results与map_results分离,并分开落盘。
- 离线 build_dataset 报“xxx not in registry”
- 原因:没有执行 cfg 的 plugin/custom_imports 导入。
- 解决:使用
tools/eval_map_offline.py,或在你自己的脚本里复刻tools/test.py的导入逻辑。
dist_test.sh覆盖--eval
- 原因:脚本硬编码追加默认 eval。
- 解决:本工程已修复为“未传
--eval才注入默认”。
- Shapely
TopologicalError/ invalid geometry
- 解决:对 geometry 做
buffer(0)修复;仍失败则跳过,保证评测不中断。
- 多进程评测 pickling 失败
- 解决:
map_utils中 worker 需要定义在模块顶层;必要时将map_eval_nproc设置为 0 先单进程验证。
- 把 map scaffold(MLP fallback)替换为完整 MapTR head/assigner/loss,并把 loss 与 GT 对齐到论文实现。
- 为 map 指标增加回归基线(固定输入 pkl 的离线评测 + 指标落盘),便于 CI 做 diff。
- 把 smoke 配置升级为可收敛的训练配置(epochs、lr、data aug、map loss 权重与采样策略)。
本节目标是把“代码改动 → 文档位置 → 如何回归验证”做成对账表,便于后续持续迭代时快速检查:
| 文件/目录 | 主要职责 | 关键符号/产物 | 文档对应章节 |
|---|---|---|---|
projects/configs/bevformer/bev_tiny_det_map_apollo.py |
det+map smoke cfg(模型结构、数据管线、评测开关) | plugin=True、dataset_type、fixed_ptsnum_per_line、--eval bbox chamfer iou |
§3 配置设计、§6 复现命令 |
projects/mmdet3d_plugin/__init__.py |
plugin 入口:导入并注册自定义 dataset/head/pipeline/maptr 等 | registry 可见性(避免 not in registry) | §3.1 plugin 与 registry |
projects/mmdet3d_plugin/datasets/nuscenes_det_occ_map_dataset.py |
dataset:在线生成 map GT;map 评测与格式化 | prepare_train_data/_add_vectormap_gt/evaluate_map/format_map_results;注入 gt_map_vecs_* |
§0.4 在线 GT、§4.5 dataset、§5 map 产物 |
projects/mmdet3d_plugin/datasets/pipelines/transform_3d.py |
pipeline:CustomCollect3D 对缺失 keys 容错 |
CustomCollect3D(缺键跳过) |
§0.4(pipeline 注入时序说明)、§7 排障 |
projects/mmdet3d_plugin/bevformer/detectors/bevformer.py |
detector:forward/test 输出拆分 det 与 map;训练透传 map GT | 推理返回 {bbox_results, map_results};训练透传 gt_map_vecs_* |
§2 输出接口规范、§4.1 detector |
projects/mmdet3d_plugin/bevformer/dense_heads/bevformer_det_map_head_apollo.py |
head:det 分支复用 + map scaffold;get_map_results 标准化输出 |
outs['map_cls_logits']/'map_pts' → vectors/scores/labels |
§4.7 head、§2.2 map_results 规范 |
projects/mmdet3d_plugin/bevformer/apis/test.py |
DDP 测试收集:map 作为独立产物收集 | multi-gpu collect,tmpdir 后缀隔离 | §4.2 DDP 收集 |
tools/test.py |
测试入口:落盘 bbox + map;按 --eval 触发 evaluator |
map_results.pkl;调用 evaluate_map |
§4.3 测试入口、§5 产物 |
tools/dist_test.sh |
分布式测试脚本:不覆盖用户显式 --eval |
--eval 注入逻辑 |
§4.4 dist_test |
projects/mmdet3d_plugin/datasets/map_utils/ |
MapTR 协议 evaluator(Chamfer/IoU mAP) | NuscMap_chamfer/mAP、NuscMap_iou/mAP |
§4.6 map_utils、§6 指标 |
projects/mmdet3d_plugin/maptr/ |
轻量 MapTR 组件(用于 build decoder / scaffold) | MapTRDecoder |
§4.8 MapTRDecoder |
tools/eval_map_offline.py |
离线 map 评测:固化 plugin 导入,避免 registry 漏导入 | --eval chamfer/iou;打印 mmdet3d.__file__ |
§4.9 离线入口、§6.2 离线复测 |
- 确认 import 来源与 plugin 生效(避免“导入了另一个 mmdet3d/没注册到自定义组件”):
conda activate apollo_vnet
python -c "import mmdet3d; print('mmdet3d from:', mmdet3d.__file__)"- cfg 能 build dataset/model(最快排除 registry 与数据路径问题):
conda activate apollo_vnet
python -c "from mmcv import Config; from mmdet3d.datasets import build_dataset; from mmdet3d.models import build_model; cfg=Config.fromfile('projects/configs/bevformer/bev_tiny_det_map_apollo.py'); ds=build_dataset(cfg.data.val); m=build_model(cfg.model, test_cfg=cfg.get('test_cfg')); print(type(ds), 'evaluate_map' in dir(ds), type(m))"- 在线 map GT 真的生成(训练样本必须包含
gt_map_vecs_*):
conda activate apollo_vnet
python - <<'PY'
from mmcv import Config
from mmdet3d.datasets import build_dataset
cfg = Config.fromfile('projects/configs/bevformer/bev_tiny_det_map_apollo.py')
ds = build_dataset(cfg.data.train)
sample = ds[0]
assert 'gt_map_vecs_label' in sample and 'gt_map_vecs_pts_loc' in sample
print('ok: online map gt exists')
PY- 端到端测试闭环(同一次 dist_test 同时产出 bbox + map,并能评测):
conda activate apollo_vnet
PORT=29513 bash tools/dist_test.sh \
projects/configs/bevformer/bev_tiny_det_map_apollo.py \
/path/to/your_ckpt.pth 1 \
--eval bbox chamfer iou- 离线 map 复测(排除 dist_test/分布式收集干扰):
conda activate apollo_vnet
python tools/eval_map_offline.py \
projects/configs/bevformer/bev_tiny_det_map_apollo.py \
/path/to/map_results.pkl \
--eval chamfer --nproc 0本节记录一个未来工程:把当前仓库中的 projects/configs/bevformer/bev_tiny_det_mapv2.py
迁移到 Apollo-Vision-Net-Deployment/ 的 TensorRT/ONNX/Apollo 部署链路中。
说明:
- 当前
Apollo-Vision-Net-Deployment/已经打通的是 det+occ 部署链路,而不是 det+mapv2。 - 因此,这里记录的是一个“等模型稳定后再启动”的迁移方案,不代表当前代码已经支持。
- 目标不是先做完整地图后处理,而是先做 可导出、可跑通、可验证的 raw map tensor 输出。
当前部署仓库的导出链路是:
- 用 TRT 专用 cfg 构建模型,例如
configs/apollo_bev/bev_tiny_det_occ_apollo_trt.py - 在
tools/pth2onnx.py中调用det2trt.convert.pytorch2onnx(...) - 导出时强制切换到部署版前向,例如
model.forward = model.forward_trt_occ_det - 依赖
det2trt/下的 TRT 版 detector / head / transformer / attention 模块 - 生成的是 面向 TensorRT/Apollo 的 ONNX,而不是纯通用推理图
当前已经存在、可复用的部署骨架:
det2trt/models/detector/bevformer.pydet2trt/models/dense_heads/bevformer_head.pydet2trt/models/modules/transformer.pydet2trt/models/modules/encoder.pydet2trt/models/modules/temporal_self_attention.pydet2trt/models/modules/spatial_cross_attention.pydet2trt/models/modules/decoder.py
当前尚不存在的关键能力:
BEVFormerDetMapHeadApolloTRTBEVFormerDetMapHeadApolloV2TRTMapTRv2DecoderTRTMapTRv2DecoupledDetrTransformerDecoderLayerTRT- det+mapv2 对应的 TRT cfg、导出前向、Apollo 侧 map 输出协议
分阶段目标定义如下:
阶段 A:最小导出闭环
- 复用现有 BEVFormer TRT encoder/det decoder
- 新增 mapv2 TRT 头
- 成功导出 ONNX
- ONNX 输出只包含原始 map 结果张量,不做复杂 Python 后处理
阶段 B:TensorRT 闭环
- 成功从 ONNX 构建 TensorRT engine
- 在 x86 上完成 PyTorch / ONNX / TRT 数值对齐
- 保持与 det+occ 相同的在线时序输入协议:
prev_bev + use_prev_bev + can_bus
阶段 C:Apollo 接入
- 更新 Apollo 侧模型描述、输出解析与 postprocess
- 支持 det+mapv2 输出替换 det+occ 的现有接口
- 验证 Orin 上的实时性与稳定性
建议严格按下面顺序推进,而不要一开始同时做导出、TRT、Apollo 接入:
- 先做 det+mapv2 的 TRT 配置与 ONNX 导出
- 再做 TRT engine 构建与数值对齐
- 最后做 Apollo 侧 map 输出接入
理由:
det+mapv2最先会卡在 decoder/head 导出兼容性,而不是 Apollo 侧逻辑- 如果 ONNX 和 TRT 都未稳定,Apollo 侧适配会反复返工
- map 后处理天然更适合放在部署侧实现,不适合一开始强塞进导出图
如果正式启动,建议最少新增以下文件:
| 路径 | 作用 |
|---|---|
Apollo-Vision-Net-Deployment/configs/apollo_bev/bev_tiny_det_mapv2_apollo_trt.py |
det+mapv2 的 TRT 配置,定义模型类型、输入输出 shape、导出前向名 |
Apollo-Vision-Net-Deployment/det2trt/models/dense_heads/bevformer_det_map_head_apollo.py |
TRT 版 det+map head(基础版) |
Apollo-Vision-Net-Deployment/det2trt/models/dense_heads/bevformer_det_map_head_apollo_v2.py |
TRT 版 det+mapv2 head |
Apollo-Vision-Net-Deployment/det2trt/models/modules/maptrv2_decoder.py |
TRT 版 MapTRv2 decoder 与 decoupled decoder layer |
可选新增:
| 路径 | 作用 |
|---|---|
Apollo-Vision-Net-Deployment/det2trt/models/detector/bevformer_det_map.py |
若不想继续扩展通用 BEVFormerTRT,可单独放 det+map detector wrapper |
参考现有:
Apollo-Vision-Net-Deployment/configs/apollo_bev/bev_tiny_det_occ_apollo_trt.py
新增 bev_tiny_det_mapv2_apollo_trt.py 时,建议:
_base_指向一个普通 det+mapv2 cfg 的部署镜像model.type切到BEVFormerTRTpts_bbox_head.type切到BEVFormerDetMapHeadApolloV2TRT- 补充
default_shapes/input_shapes/output_shapes - 增加一个显式字段,例如:
trt_forward = "forward_trt_det_map"避免继续在导出脚本里写死 forward_trt_occ_det
当前部署仓库里:
model.forward = model.forward_trt_occ_det建议改为按 cfg 选择:
trt_forward = getattr(config, "trt_forward", "forward_trt_occ_det")
model.forward = getattr(model, trt_forward)这样 det+occ 和 det+mapv2 可以共用一套导出脚本。
现有 det2trt/models/detector/bevformer.py 已经支持:
forward_trt(...)forward_trt_occ_det(...)
建议新增:
def forward_trt_det_map(self, image, prev_bev, use_prev_bev, can_bus, lidar2img, no_pad_image_shape):
...
return bev_embed, outputs_classes, outputs_coords, map_cls_logits, map_pts注意事项:
- 只返回张量,不返回 Python dict/list
- 保持和现有 det+occ 一样的在线时序输入协议
- 让
bev_embed继续作为下一帧prev_bev
建议不要直接从训练版 BEVFormerDetMapHeadApolloV2 原样继承,而是采用“det TRT 基底 + map 分支迁移”的方式:
- 以
Apollo-Vision-Net-Deployment/det2trt/models/dense_heads/bevformer_head.py中的BEVFormerHeadTRT为检测基底 - 从训练仓库的
projects/mmdet3d_plugin/bevformer/dense_heads/bevformer_det_map_head_apollo.py和projects/mmdet3d_plugin/maptrv2/dense_heads/bevformer_det_map_head_apollo_v2.py迁移 map 分支逻辑
最小可行版本只需要导出:
map_cls_logitsmap_pts
第一版先不要导出:
one2many_outsmap_segmap_pv_seg- Python 风格
get_map_results()
训练版 V2 decoder 位于:
projects/mmdet3d_plugin/maptrv2/modules/decoder.py
部署时至少需要迁移:
MapTRv2DecoderMapTRv2DecoupledDetrTransformerDecoderLayer
建议实现时优先复用现有部署仓库能力:
CustomMSDeformableAttentionTRTGroup/MultiheadAttention的 TRT 兼容路径- 已经存在的 reshape/permute 风格
最关键的工作是保证下面这条点级 query 路径在 TRT 版中仍成立:
num_map_query = num_map_vec * map_num_pts
第一版建议保持输入与 det+occ 一致,只替换输出。
输入:
image:[1, 6, 3, 480, 800]prev_bev:[2500, 1, 256]use_prev_bev:[1]can_bus:[18]lidar2img:[1, 6, 4, 4]no_pad_image_shape:[2]
建议第一版输出:
bev_embed:[2500, 1, 256]outputs_classes:[6, 1, 900, 10]outputs_coords:[6, 1, 900, 10]map_cls_logits:[1, 50, 4]map_pts:[1, 50, 20, 2]
说明:
50来自 mapv2 的one-to-onemap queries300条one-to-many只服务训练,不建议作为部署输出20来自map_num_pts4类对应divider/ped_crossing/boundary/centerline
训练仓库中 map_results 是 Python 风格结构:
vectorsscoreslabelscls_logits
这适合测试脚本,不适合部署图。
部署第一版建议只导 raw tensors:
map_cls_logitsmap_pts
原因:
- ONNX/TRT 图更稳定
- Apollo 侧可用 C++ 实现阈值、筛选、重采样和发布协议
- 后处理策略后续可独立迭代,不必重新导图
为了把工程边界压小,第一阶段建议明确不做:
- 不导出
one-to-manymap 输出 - 不导出
map_aux_seg(map_seg/map_pv_seg) - 不在 ONNX 中实现 map postprocess
- 不做动态 batch
- 不做动态 camera 数
- 不把 Apollo 侧地图发布协议一起塞进第一轮开发
这项工程的主要风险不是 backbone,而是 map 分支:
MapTRv2DecoupledDetrTransformerDecoderLayer的双 self-attention 重排逻辑在 TensorRT 上可能出现 shape/性能问题MultiheadAttention、CustomMSDeformableAttentionTRT与点级 query 组合后,可能出现 ONNX 或 TRT parser 不兼容- Apollo 侧现有模型包和后处理明显按 det+occ 的 4 输出接口设计,切换到 det+mapv2 意味着 runtime 也要同步改
因此,如果正式启动,必须遵守一个原则:
- 先完成 ONNX 与 TRT 闭环,再做 Apollo 侧接入
在真正启动这项工程前,建议先满足下面条件:
bev_tiny_det_mapv2.py的训练与测试接口稳定mapv2的推理输出 contract 不再频繁改动one-to-one输出是否足够部署使用有明确结论- 是否保留
centerline类在部署侧已经达成一致 - Apollo 侧是否愿意接 raw map tensors 而不是现成
map_results已经确认
建议到时候直接按下面顺序推进:
- 新增
bev_tiny_det_mapv2_apollo_trt.py - 改造
tools/pth2onnx.py支持按 cfg 选择trt_forward - 在部署仓库中补
forward_trt_det_map - 实现最小
BEVFormerDetMapHeadApolloV2TRT - 先完成
pth -> onnx - 再完成
onnx -> trt - 最后改 Apollo runtime 接入新输出
建议暂停点:
- 如果第 5 步无法稳定导出,先不要进入 Apollo 侧改造
- 如果第 6 步 TRT 性能或兼容性不达标,优先缩小输出范围,不要先做后处理
未来如果要把 bev_tiny_det_mapv2.py 做成 Apollo 可部署版本,推荐路线不是“直接导训练模型”,而是:
- 复用现有 det+occ 的 TRT 化 BEVFormer 骨架
- 新增 TRT 版 mapv2 head 和 decoder
- 第一版只导出 raw map tensors
- 等 ONNX/TRT 稳定后,再改 Apollo 侧接口



