泊车语义建图算法复现全解析:从鱼眼BEV到时序融合与部署
年初接到一个任务把泊车场景下的语义建图算法完整复现一遍用于后续的记忆泊车预研。当时以为这只是把几个开源模型跑通的事真正动手才发现泊车语义建图算法最费时间的环节不是网络训练而是标定、数据、时序融合与部署验证的整条链路。这篇博文围绕我实际复现这个算法的全过程从问题定义、链路拆解、数据准备、代码实现到训练调参和实车验证把能直接参考的细节都写出来。如果你也在做泊车感知或相关方向的算法复现这篇文章应该能帮你少走不少弯路。1. 泊车语义建图到底在解决什么问题从一段泊车视频说起地下车库里光线暗、环氧地坪反光厉害车位线被旁边的车挡掉一半。如果人坐在驾驶位靠眼睛就能知道“前面是路沿右边有个空车位后方柱子后面是墙”。但要让车辆自己完成这件事就必须在车体坐标系下维护一张带语义的地图哪个区域可以走哪些线条是车位边界哪里是固定障碍物。这张地图就是泊车语义地图。1.1 泊车场景下的语义地图是什么语义地图最常见的载体是BEV栅格图把车身周围一个矩形区域按固定分辨率划分成格子每个格子保存一个类别标签。比如前7米、后3米、左右5米分辨率0.1米输出就是一个大约110x160的二维栅格。类别通常包括背景、可行驶区域、路沿、车位线、静态障碍物、动态障碍物。这和导航用的高精地图不一样。高精地图是全局尺度的泊车语义地图是局部、实时、以自车为中心的。它不需要告诉我“下一个路口怎么走”但必须告诉我“车头右侧50厘米处有一条车位的边界线当前车辆后轮距离路沿还有80厘米”。本质上它回答的是三个问题我周围哪里能走哪里不能走车位到底在哪。1.2 为什么不能用通用语义分割直接替代很多人一开始会想直接用一个语义分割网络对鱼眼图像做像素级分类再把像素点投影到地面不就行了听起来顺理成章但实测下来问题非常多。第一通用语义分割的输出在相机坐标系里也就是图像平面。要把像素点变成地面坐标得假设地面是平面再用外参做逆透视变换。这个假设在近距离大体成立一旦距离超过两三米哪怕地面有一点坡度或者车身有轻微颠簸投影出来的位置就会偏出十几厘米。泊车要求的是厘米级精度这种误差直接让人不敢用。第二图像平面分割结果往往无法给出稳定的车位角点。车位规划需要的是“线段的端点和交点”而不是一团密集的像素。通用分割网络可以给出车位的mask轮廓但轮廓上哪一个像素才是角点还需要额外后处理而且鱼眼畸变下角点的重投影精度很难保证。第三单帧图像信息有限。地上一条虚线车位线被车身遮住只凭当前帧根本无法判断它该不该延续。语义建图要解决的恰恰是“利用历史信息补全当前感知”的问题这天然需要时序。所以真正要做的不是一个更高精度的分割网络而是一条完整链路多路鱼眼图像输入变换到BEV按车辆位姿拼接历史信息最后输出一份带置信度的语义栅格图。1.3 复现前必须搞清的四个技术前提在动手写任何代码前有几个前提如果没想清楚后面大概率白跑。坐标系约定像素坐标系、相机坐标系、车体坐标系、栅格坐标系从图像到栅格会反复使用这些坐标变换写代码时最好统一用齐次坐标。标定精度外参如果偏差超过2厘米在0.1米分辨率的栅格上就会错开一个格子所以相机到车体的外参标定不能省。传感器同步图像、IMU、轮速计的时间戳要对齐否则时序融合时历史帧的位姿是错的地图必然会重影。位姿来源泊车场景没有卫星信号位姿通常来自轮速计加IMU的融合结果。纯里程计会漂移需要靠回环或特征匹配修正这一点后面专门讲。这些不是论文里的公式推导而是工程上最容易先踩坑的地方。我当时因为外参标定没做验证导致整个模型的BEV输出偏移了好几十个像素排查了三天才发现是标定板的角点坐标写错了。2. 算法整体链路拆解从多路鱼眼到BEV语义栅格整个算法的数据流可以分成四段传感器采集与标定、图像特征提取与视图变换、时序融合、输出后处理。理解这四段的顺序和依赖关系比理解某个网络模块更重要。2.1 输入传感器配置与标定关系我复现时用的是典型配置四路鱼眼摄像头分别朝前、后、左、右视场角接近190度加上IMU和轮速计。鱼眼覆盖范围大但是畸变严重标定必不可少。标定分两步。第一步是单相机内参用棋盘格对着镜头摆不同姿态采集几十帧后用通用鱼眼畸变模型做优化。第二步是外参要确定每个相机在车体坐标系下的位姿。常见的做法是把车停在一个布置好标记物的标定车间让相机看到地面上的标靶再通过这些標靶点的像素坐标和车体坐标建立对应关系。外参求解其实就是一个PnP问题优化目标是所有标靶点的重投影误差。标定做完一定要做验证不能只看重投影误差小就完事。我习惯在车前方和左右放几个已知间距的标记点用外参把像素点投影到地面上再用卷尺量实际位置。间距50厘米的标记点投影误差应该在1厘米以内。误差大了问题大概率出在标靶坐标或者车辆姿态估计上。2.2 视图变换的几种实现视图变换是把相机图像或图像特征映射到BEV视角常见有三种路线。第一种是纯几何的逆透视映射利用地面平面假设做单应性变换。给定相机内参K、相机到车体外参[R|t]地面上的点(X,Y,0)投影到像素(u,v)的关系是一个3x3单应矩阵HK[r1 r2 t]。反过来也能把像素坐标反算回地面坐标。这种方法实现快缺点是对非平面地面非常敏感而且远距离拉伸严重分辨率很低。但是它速度快可以作为辅助监督或者数据预对齐。第二种是基于Transformer的可变形注意力机制。网络在BEV空间放一组可学习的位置编码作为查询每个BEV查询从视角图像的局部特征区域采样信息学习从图像到BEV的映射。这个方案能处理非平面地面和遮挡鲁棒性强是目前主流做法。代价是训练资源消耗大工程链路复杂。第三种是几何与学习混合。先用IPM把图像映射到BEV作为粗特征再用小网络修正几何误差。这种方法在数据量不足的时候比纯Transformer更容易收敛我在复现项目初期用的就是这种方案。我的建议是如果你是为了快速验证泊车语义建图的上层逻辑先用IPM跑通确认后面的时序融合、位姿修正没有问题再升级到可变形注意力模型。千万不要一上来就啃大模型否则问题出来了都不知道是感知的问题、位姿的问题还是融合的问题。2.3 时序融合与位姿漂移矫正单帧BEV语义图噪声大、遮挡多必须把连续几帧甚至几十帧的信息融合起来。融合方式很简单利用车辆在相邻帧间的位姿变换把上一帧的语义栅格图投影到当前帧的车体坐标系下再做逐像素的置信度叠加。我用的是带遗忘因子的指数滑动平均当前帧融合结果 遗忘系数 * 历史融合结果 (1 - 遗忘系数) * 当前帧预测。遗忘系数取0.7左右相当于有效历史窗口大约是几秒。车位线是静态目标历史信息能帮助补齐被遮挡部分动态障碍物则要尽快遗忘否则会拖出轨迹残影。位姿漂移是时序融合最大的敌人。轮速计加IMU的航位推算在几十米内尚可几百米后必然漂移。漂移的表现是同一个车位线在语义地图里出现双线重影。缓解手段有两个层面。一是在建图端提取车位线和路沿的角点作为路标当车辆经过同一区域时做回环匹配用位姿图优化修正累计误差二是在感知端对动态障碍物单独做实例掩膜不让它的模糊轨迹参与地图更新。2.4 输出定义输出栅格的分辨率和类别定义要提前定死因为它影响标注、模型输出头、下游规划的所有环节。项目数值栅格分辨率0.1米/像素范围前方7米后方3米左右各5米输出尺寸140(H) x 120(W) 像素类别背景、可行驶区域、路沿、车位线、静态障碍物、动态障碍物输出通道每个类别一个概率通道关于分辨率我见过有人用0.05米想让建图更精细但训练难度和标注成本会翻好几倍。对泊车规划来说0.1米的分辨率通常够用而且地图尺寸小推理速度快。类别里把动态障碍物和静态障碍物分开非常重要因为时序融合时两类目标的处理策略完全不同。3. 数据准备与标注复现路上第一个大坑论文里的方法再漂亮没有好的训练数据也是白搭。泊车语义建图的标注数据比普通分割数据要复杂得多因为大量类别是地面上的线状目标对边界精度极为敏感。3.1 数据来源与自采方案公开可复现的泊车语义建图数据集不多即使有也很难覆盖你实际要用到的停车场景。自采数据是绕不开的一步。我的做法是选了一个固定的停车场作为主数据场地再额外采集三个不同类型的场地做泛化验证包括地下环氧地坪、露天水泥地、立体车库。采集设备就是量产方案的四路鱼眼加IMU加轮速计。时间同步上我用的是PTP同步机制图像触发时刻给IMU和轮速计打时间戳误差控制在几个毫秒内。车辆采集时尽量按照正常泊车速度行驶约15km/h以下先绕场跑几圈用于建图回环再做实际泊车动作采集车位场景。数据量方面我第一版训练集大概是5000帧关键帧覆盖白天、夜晚、阴天和轻微雨天。5000帧听起来不少但摊到不同场景、不同光照、不同车位形态后每个类别样本量依然不足。如果能想办法采集更多数据量建议翻一倍。3.2 标注工具与标注规范标注工具用的是开源的多边形标注工具支持点和线属性。泊车语义建图的标注不能直接用普通语义分割标注流程需要单独设计规范尤其是车位线。我的规范是车位线标注成闭合多边形但要额外记录端点属性。视野内能看到明确端点的标为“可见端点”被遮挡但按白色虚线的节奏可以推断出来的标为“推断端点”。路沿按线段标注断裂处不能自动连接线宽统一为2像素。可行驶区域扣除车辆自身区域和静态障碍物区域后标注册地面部分动态障碍物出现在画面上时可行驶区域仍然可以包含其下方地面但障碍物要单独标出。静态障碍物只包括柱、墙、停放车辆等不会移动的物体使用矩形或多边形。这里很容易踩的坑是车位线的箭头标识要不要标注。我发现很多标注规范把“车位内箭头”漏掉了但箭头对车位入口方向的判断很有价值建议单独增加一个“标识线”类别或者在标注说明里规定它与车位线相同的语义标签。我第一版数据就因为没有箭头标注导致后续对斜列车位判断不准。3.3 数据增强策略数据的增强不应只停留在图像层面。图像层面可以加亮度扰动、对比度扰动、模拟水渍、运动模糊、高斯噪声这些基本手段都能明显提升泛化能力。但泊车语义建图还需要BEV空间增强。BEV空间增强思路是对图像做透视扰动后同步调整真值BEV栅格。比如我对图像施加一个小的旋转和缩放对应的BEV真值也要做同样的旋转和平移。如果在图像上做了加雨滴模糊那么真值保持不变但该样本在训练中对雨天的鲁棒性会提升。车位线擦除增强很有效。车位线常常被车辆或行人遮挡训练时需要随机把图像中某一段车位线涂掉让网络学会利用周围语义线索推断出被遮挡的线条。类似CutOut但目标区域更聚焦在线目标上。4. 核心模块的代码级实现细节到了代码环节我把最核心的四个模块拆出来讲去畸变、BEV特征提取、损失函数、评价指标。这四个模块如果实现不到位整个项目根本跑不出能看的效果。4.1 鱼眼图像去畸变与ROI裁剪鱼眼图像不能直接作为普通透视模型输入。我的做法是在预处理阶段用通用鱼眼模型做去畸变把鱼眼图展开成近似针孔相机视角再做IPM或输入网络。import cv2 import numpy as np # 假设已经通过标定获得了相机内参和畸变系数 K np.array([[550.0, 0.0, 640.0], [0.0, 550.0, 360.0], [0.0, 0.0, 1.0]]) D np.array([-0.02, 0.01, 0.0, 0.0]) # 鱼眼畸变系数 DIM (1280, 720) # 原始图尺寸 # 计算去畸变映射表 map1, map2 cv2.fisheye.initUndistortRectifyMap( K, D, np.eye(3), K, DIM, cv2.CV_32FC1 ) def preprocess_image(raw_img): # 去畸变 undist cv2.remap(raw_img, map1, map2, interpolationcv2.INTER_LINEAR) # 车辆周边往往只需要车体附近区域超出范围的部分直接裁剪 roi undist[100:620, 100:1180] roi cv2.resize(roi, (512, 256)) return roi去畸变时要小心计算代价。全图remap很费算力在实车板子上每路相机都要跑一遍如果算力紧张可以把去畸变后的图缩小或者只在ROI区域内计算映射。另一个细节是去畸变之后相机内参其实会变必须重新定义新的虚拟相机内参否则后面的IPM投影会错位。最简单的方法是用去畸变后的图像中心和焦距构造新的K矩阵。4.2 基于Transformer的BEV特征提取简化版实现可变形注意力的完整实现比较复杂但核心思路可以简化成几行伪代码。每个BEV网格对应一个可学习查询向量与视角图像特征做cross-attention得到该网格的BEV特征。import torch import torch.nn as nn class SimpleBEVAttention(nn.Module): def __init__(self, bev_h140, bev_w120, embed_dim256, num_heads8): super().__init__() self.bev_h bev_h self.bev_w bev_w self.embed_dim embed_dim # BEV空间位置编码作为查询 self.bev_embed nn.Parameter(torch.randn(1, bev_h * bev_w, embed_dim)) self.cross_attn nn.MultiheadAttention(embed_dim, num_heads, batch_firstTrue) self.norm nn.LayerNorm(embed_dim) self.feed_forward nn.Sequential( nn.Linear(embed_dim, embed_dim * 4), nn.ReLU(), nn.Linear(embed_dim * 4, embed_dim) ) def forward(self, image_feats): # image_feats: [B, N, C, H, W]N是相机个数 B image_feats.size(0) N image_feats.size(1) _, C, Hf, Wf image_feats.shape # 压缩图像特征为序列并展开 image_feats image_feats.view(B, N * C, Hf * Wf) image_feats image_feats.permute(0, 2, 1) # [B, Hf*Wf, N*C] # 这里实际还需要一个视角编码和特征维度对齐略 query self.bev_embed.expand(B, -1, -1) # [B, bev_h*bev_w, embed_dim] attn_out, _ self.cross_attn(query, image_feats, image_feats) out self.norm(attn_out query) out self.feed_forward(out) out out out.view(B, self.bev_h, self.bev_w, -1) return out简化代码里省略了视角特征对齐、位置编码生成和可变形采样但足以表达“BEV查询与图像特征交互”的核心逻辑。实际部署的时候建议用现成的可变形注意力实现直接手写容易陷入细节。4.3 损失函数设计泊车语义建图的类别极不平衡。车位线和路沿只占整个BEV区域的很小比例如果用普通交叉熵网络很快把所有像素都预测成背景。加权交叉熵加Dice Loss是稳妥的组合。import torch.nn.functional as F def weighted_ce_loss(pred, target, class_weights): # pred: [B, num_classes, H, W] # target: [B, H, W] ce F.cross_entropy(pred, target, weightclass_weights, reductionmean) return ce def dice_loss(pred, target, smooth1.0): # 只在前景类别上计算背景不参与 n_classes pred.size(1) pred_softmax F.softmax(pred, dim1) total_dice 0.0 for cls in range(1, n_classes): pred_cls pred_softmax[:, cls] target_cls (target cls).float() intersection (pred_cls * target_cls).sum() union pred_cls.sum() target_cls.sum() smooth total_dice 2 * intersection / union return 1 - total_dice / (n_classes - 1)权重设置方面背景权重0.2可行驶区域权重1.0路沿权重3.0车位线权重5.0静态障碍物权重2.0动态障碍物权重1.5。整体loss用加权ce加0.5倍dice。车位线权重不能给得太高否则网络会过度关注车位线而忽略可行驶区域边界导致车辆压到路沿上。4.4 评价指标计算语义地图要用多个指标同时评价。单看mIoU不够因为mIoU会把背景的贡献算得很大掩盖车位线的小误差。我用了三类指标。mIoU直接按类别计算交并比后平均。车位角点F1是从预测的车位线mask里提取角点和线段端点与真值做最近邻匹配算准确率和召回率。线完整度IoU是只针对车位线和路沿这一类线目标的IoU不是全类的。实测下来mIoU达到0.65以上、车位角点F1达到0.85以上这个地图才能给下游规划使用。如果只是mIoU高但角点F1低说明语义分割“看起来还行”但边界精度不行实际泊车会倒不进去。5. 训练与调参那些文档里不会写的经验模型训练的过程没什么神秘但有几个细节非常影响最终精度。我把它们单独列出。5.1 模型大小与算力预算的取舍刚开始我直接套用了一个比较大的BEV感知模型参数量有接近3000万训练没问题但推到实车计算平台上帧率只有不到20FPS根本不够用。后来换成轻量级骨干网络参数量压到800万mIoU掉了大概2个点但帧率到40FPS总体收益更大。如果算力平台很紧张建议骨干网络用轻量结构BEV注意力层数控制在2到3层即可。更大模型带来的收益通常在泛化能力上而不是基础指示标上。如果想要高精度优先把数据和标注质量做扎实再加模型容量。5.2 学习率策略与Batch Size我用AdamW优化器学习率2e-4带1000步warmup之后cosine衰减。Batch Size方面单卡很难支撑大batch考虑用梯度累积等效到16。混合精度训练是必须的否则模型体积和显存占用都会很夸张。多次实验里的一个规律学习率对BEV Transformer类模型的敏感度远高于普通分割网络。学习率一旦大于5e-4训练比较容易出现震荡而且车位线的边界会变糊。宁可把学习率调低一点让训练跑得慢一点也不要贪快。5.3 过拟合与类别不均衡数据量不足时模型容易记住特定停车场的车位线形态换个场地就掉点。缓解办法是强增强加早停。训练过程里每两个epoch用验证集算一次角点F1如果连续三轮没有提升就停。类别不均衡还有一个隐藏问题动态障碍物的样本很少且姿态变化大模型容易把它预测成静态障碍物。在数据增强时我特意对“运动中的车辆”这一块做了更多随机遮挡和模糊让模型学会从外观之外的信息判断动态性。但即便如此动态障碍物的准确率也是所有类别里最低的实际部署时还是要用雷达做补充。5.4 泛化评测千万不要只在一个停车场验证。我最后评测时选了四个停车场两个环氧地坪、一个水泥地、一个立体车库地面。每个场景跑10遍不同的泊车动线统计地图的mIoU和车位线完整性。结果显示模型在环氧地坪亮度极高、反光严重的场景下路沿检测IoU下降了约12个百分点。这才是泛化能力最脆弱的点。泛化评测时还要记录车辆行驶里程和时间因为漂移会随时间累积。如果一个评测片段跑3分钟建图就出现了明显漂移那问题不一定在感知很可能在时序融合参数或位姿源上。6. 部署与实车验证复现不是跑通就够了模型在服务器上跑出指标只是第一步真正放到车上看效果才会暴露大量问题。我把部署和实车验证期间踩过的坑按类别理了一遍。6.1 模型导出与推理加速训练用的模型格式不能直接上板子要导出成ONNX再用推理引擎转成INT8量化模型。量化过程中车位线这类小目标很容易掉点因为INT8量化会把很小的特征值直接压缩掉。我的处理策略是先做层敏感度分析把每个层单独量化看最终角点F1掉多少。发现注意力模块的QKV投影对量化非常敏感这些层保留FP16其余卷基层用INT8。混合量化完成后整个模型体积减小了约60%帧率从22FPS提升到38FPS角点F1只掉了1个点。这个结果是可以接受的。6.2 工程化细节线程、队列与时间戳部署架构上我把程序拆成四个线程相机采集线程、感知推理线程、时序建图线程、结果发布线程。相机采集线程负责拉流和时间戳校准感知线程只负责BEV输出时序建图线程做位姿变换和地图更新结果发布线程把地图转成下游规划需要的格式。线程之间用带锁的环形队列通信队列深度不要超过3帧。感知线程如果有一次推理超时宁可跳过这一帧也不要让队列积压否则时序融合的位姿对应关系会乱掉。我还在建图线程里加了时间戳校验如果当前帧的时间戳跳变超过100毫秒就暂停地图更新等待下一帧。6.3 实车常见失败模式失败模式现象原因应对方案车位线重影地图里一条线变成两条位姿漂移或外参误差增加回环修正重新标定验证反光地面误检环氧地坪反射的顶部灯光被识别成路沿光照对比度过强增加反光地面训练样本降低路沿权重车位方向模糊斜列车位入口反了车位线标注没有方向性增加标识线标注类别训练时加入箭头信息动态车辆拖影运动的车在地图上留下轨迹时序遗忘系数过大对动态障碍物单独建mask不参与静态地图融合闸机杆误检闸机杆被识别为障碍物没有类似样本扩充路边设施样本这些失败模式在仿真和离线数据里几乎不会出现但实车一个多小时就能遇到好几个。建议预留至少两周的实车测试时间来调整这些细节。6.4 复现时踩过的坑清单最后把整个复现过程中印象最深的几个坑按影响程度排个序。外参标定验证不到位导致所有地图输出整体偏移排查时间最长影响最隐蔽。位姿源精度不足就做时序融合地图重影永远消不掉不管感知多准。标注规范没定死端点属性车位线被遮挡时网络完全不会推断在线指标提升缓慢。训练时只关注mIoU不看角点F1模型看起来指标不错但下游车位规划失败率很高。过早追求大模型算力扛不住最后还得回头压轻量模型白白浪费了两周。这些坑如果提前知道至少能省下三到四成的时间。复现泊车语义建图算法真正的考验不是看懂论文里那个网络结构图而是把数据、标定、时序、推理、部署这一整条链路都打通。我自己最大的感受是每一步的误差都是累积的外参差一点损失函数权重差一点位姿差一点最后地图上的效果会差非常多。做这个方向最好先建立一套从离线到实车的完整验证闭环再逐步迭代算法细节。希望这份记录能帮你少走弯路。