我第一次读这篇论文时,最直接的反应是:原来多相机感知还能这么搞。六路相机拍回来的图像,不再是各自做2D检测再硬凑到俯视图上,而是把每一个像素沿着相机射线“举到”三维空间,再“拍扁”到地面栅格里——这正是标题里的 Lift 和 Splat。而 Shoot 则是最后用一条路径穿过这张 BEV 地图,把规划的目标直接接回感知网络,形成端到端的训练闭环。标题其实已经把整个架构说完了。
这篇工作提出了一种“隐式逆投影”的思路。与其先估一个精确深度、再往3D空间反投影,不如让网络给每个像素在所有离散深度上同时留下特征,把“深度估计”这件事融进端到端学习里。这篇论文发布之后,很快成了BEV感知绕不开的baseline,直到今天,很多后续工作的对比表格里仍然能看到 Lift-Splat 的影子。如果你做自动驾驶多传感器融合、视觉感知、或者纯视觉BEV建图,这篇论文值得反复读。下面我从原理、实现到复现心得一层层拆开聊。
1. 拉开的序幕:多相机融合为什么这么难
1.1 几何逆投影的两条老路,各自卡在哪
几年前业界做多相机鸟瞰感知,最常规的做法是“图像坐标到地面坐标”的几何投影。第一条路是 IPM(逆透视映射),假设地面是一个平面,用单应性矩阵把像素铺到俯视图上。这个办法在平整的停车场里看着还行,一旦遇到坡道、路拱、桥面,地面不再是平面,像素就会开始“撕裂”。车身上的立体障碍物更是麻烦——一辆和你并排的车,在IPM里会被拉成一长条影子,后续检测根本没法用。
第二条路是先做单目深度估计,拿到每个像素的深度,然后反投影成3D点云,最后落到BEV栅格里。这条路的瓶颈在深度估计本身。单目深度天生是病态问题:同一块路面纹理,远处和近处在深度上相差几十倍,网络稍微犹豫一下,远处的车辆就可能错位几米。而且这种“深度估计→反投影”是两条独立管线,前一步的误差会直接传导到后一步,很难端到端修正。
LSS的转变在于:它不做“先猜一个精确深度再硬投影”,而是让每个像素在所有离散深度槽上都保留一份特征,由网络自己决定每个深度上“放多少置信度”。这就是标题里 implicitly unprojecting(隐式逆投影)的含义。它放弃了“我一定要在某一个深度上做对”这条思路,改成“我把特征沿光线铺开,后续的拍扁操作会替我选出有用部分”。
1.2 统一坐标系:所有相机先放到一个“自车参考系”里
多相机融合本质上要回答一个问题:不同相机里的同一个像素,到底分别对应自车周围的哪个位置?答案依赖内外参。LSS的做法是把所有相机放在以自车为中心的局部坐标系里,每个相机的图像特征先沿着各自的光线抬到3D,再用外参统一变换到自车坐标系,最后落到同一个BEV栅格上。
这个选择很朴素,但它解决了工程里的一个关键痛点:不需要手动拼接多路相机的视野,也不需要判断哪些像素重叠。网络只需要对“自车坐标系下的BEV栅格”负责,所有相机都往这个栅格里填特征,重叠区域自然累积,空隙区域自然空缺。你回头看很多后续BEV感知工作,它们虽然换了更花哨的注意力机制、更复杂的深度网络,但这个“所有相机映射到统一BEV坐标”的大前提一直没变。
2. Lift:把图像像素沿着光线铺进三维空间
2.1 前向网络的两个头:上下文与深度分布
Lift阶段的前向过程很简单。输入是若干张图像,先过一个图像编码器(常见的是 EfficientNet 这类主干),得到分辨率降低后的特征图,尺寸是 H×W×C。然后这个特征图会被送进两个分支:
- 上下文分支(context head)输出 C 维语义特征,表示“这个像素是什么”;
- 深度分支(depth head)输出 D 维向量,经过 softmax 之后表示“这个像素在不同深度上的概率分布”。
这两个输出做外积:每个像素得到 D×C 的张量。用代码视角看,这就是把高度为 H、宽度为 W 的特征图,扩展成 D×H×W×C 的特征体。每一份语义特征都被复制到了 D 个深度槽位上,每个槽位再用对应的概率加权。这个“复制→加权”的操作用爱因斯坦求和就能写清楚,但直觉上更好理解:网络把每个像素“不确定地”放进了三维空间中沿光线的一串位置,而不是强行选一个点。
2.2 为什么是“软深度”,而不是回归一个深度值
我见过很多第一次接触LSS的人会问:为什么不直接让网络回归深度值?答案在训练和推理的不同阶段都有体现。
训练阶段,深度真值往往来自激光雷达投影,稀疏且带噪声。如果让网络回归一个精确深度,标签里的噪声会被放大;而把深度做成 D 类的分类问题,天然对噪声更宽容,网络只需把质量放到正确的槽位附近就行。推理阶段,相机视角里存在大量“其实分不清远近”的像素,比如雨雾、暗光、透明挡风玻璃后面的物体。硬回归会把所有不确定性压缩到一个点上,而软分布允许网络同时在两个可能的深度上都留下响应,把最终裁决交给后续的聚合过程。
这有点像你在黑房间里摸开关:与其赌它一定在某个位置,不如把整个墙面都摸一遍,虽然浪费一些动作,但不会漏掉。LSS的 Lift 就是在“摸墙”。
2.3 相机标定如何进入计算图
很多人在读论文时忽略了一个工程细节:外积完成后,特征体里的每个点还需要从相机坐标系转换到自车坐标系。这个过程依赖内参 K 和相机外参 R、t。LSS在代码实现里通常会提前算好一个映射矩阵:对每个相机、每个像素、每个深度槽位,它的自车坐标系下的3D坐标是多少。前向时直接按这个坐标往BEV里填。
这套做法的好处是,图像编码器和深度网络只需要被优化,坐标变换本身是一个固定的索引映射。但这也意味着标定质量直接决定BEV几何是否正确。我的实测经验是,外参哪怕只有1到2度的偏差,几十米外的物体在BEV图上就会错位1米以上。所以用LSS做工程,第一项要检查的永远不是网络结构,而是相机标定。
3. Splat:把视锥点云拍扁到地面栅格
3.1 从三维点到 BEV pillar
Lift 阶段结束之后,我们得到一堆散布在三维空间中的特征点。这些点还不能直接进入二维卷积网络,因为BEV感知需要的是一张规整的俯视图。于是就有了 Splat。
具体操作是:把自车周围三维空间划分成固定大小的柱子(pillar),论文里一般使用 0.5m 分辨率的栅格,自车周围 100m×100m 的范围对应 200×200 的BEV网格。每个三维点根据它的 x、y 坐标整除到对应柱子里,然后把同一个柱子里所有点的特征求和。这个操作叫 sum pooling。
为什么用 sum 而不是 mean?关键在重叠区域。多个相机看同一个地方时,那片区域的响应会出现多次;如果用 mean,相机数量越多,响应反而被稀释;用 sum,多相机共识区域会自然累积出更高的响应强度,更多视角看到同一个物体时,特征会变得更“响亮”。这也解释了为什么LSS在相机少的时候会变弱——它不是靠几何一致性来消歧的,而是靠多个视图的信息叠加来获得置信度。
3.2 工程实现:排序+前缀和搞定 sum pooling
真正在 GPU 上实现 Splat 时,一个直观的做法是用 scatter_add:对每个 pillar 的索引做原子累加。但原子操作在多线程争用时会拖慢速度,而且显存访问不连续。
我在复现时发现,LSS 开源实现里的排序法更聪明。思路是把所有三维点按 pillar 索引排序,然后对排序后的特征做 cumsum(前缀和),再在 pillar 索引变化的位置做差分,一次性拿到每个 pillar 的特征总和。整个过程像这样:
# 伪代码:排序+前缀和实现 pillar sum pooling # points: (N, 3),features: (N, C) # 1. 根据 x, y 坐标量化到 pillar 索引 idx = quantize(points) # (N,) # 2. 按 pillar 索引排序 order = argsort(idx) sorted_idx = idx[order] sorted_feats = features[order] # 3. 前缀和 cumsum_feats = cumsum(sorted_feats, dim=0) # 4. 找到每个 pillar 的最后一个点,用差分还原每个 pillar 的总和 boundary = where(diff(sorted_idx) != 0) pillar_sums = diff(cumsum_feats[boundary])| 实现方式 | 特点 | 适用场景 |
|---|---|---|
| scatter_add | 实现简单,但原子操作多,显存访问不连续 | 小规模点云、原型验证 |
| 排序+前缀和 | 连续内存访问,GPU利用率高,几乎无锁竞争 | 大规模相机点云、批量训练 |
| 自定义CUDA核(fast-BEV-pool) | 速度最快,能进一步省显存 | 工程部署、实时推理 |
我自己在跑 6 相机、41 个深度槽位时,朴素 scatter_add 的前向时间能占整个 pipeline 的相当大比例;换成排序+前缀和之后,体感速度明显提升。后来一些仓库把这一步写成了 fused CUDA kernel,效果更好,但原理还是同一套。
3.3 重叠区域、遮挡和置信度问题
sum pooling 固然巧妙,但它也有明显的毛病:当两个相机的视角差异过大时,同一个物体在不同图像里的特征可能差异很大,直接相加会把语义搞混。光照变化、角度变化都会影响特征的一致性。
LSS在论文里没有做额外的置信度加权,直接把特征相加。后续工作们看中了这个短板:BEVDepth 在 Splat 之前引入深度校正,用显式深度监督把深度分布校准得更准;BEVFormer 则用可变形注意力在 BEV query 和图像特征之间做跨视图交互,替代了简单的求和。如果你在工程里发现重叠区域的特征“糊”在一起,可以尝试在相加前给每个相机的特征加一个可学习的置信度权重,往往能缓解问题。
4. Shoot:从 BEV 表示到可微路径规划
4.1 把 BEV 语义图变成一张“能开车的成本图”
Shoot 是 LSS 里最容易被跳过的一节,因为很多人只关心 BEV 感知结果,而不关心规划闭环。但 Shoot 恰恰是这篇论文最有启发的部分:它把 BEV 语义分割的输出接进了一个可微的路径规划器。
做法很简单。BEV 语义概率图里每个格子都对应“这里有车”“这里有行人”“这里是道路”等类别的概率。给定一组类别对应的成本权重,我们把这些概率加权求和,得到一张成本图。行人和车辆格子的成本高,道路格子的成本低。网络最后输出的这张成本图是平滑的,意味着我们可以对它的空间坐标求梯度。
4.2 轨迹栅格化与反向传播
训练时,我们手里有真值轨迹(来自数据录制时的自车行驶路径)。把轨迹“画”到成本图上——也就是对轨迹覆盖到的格子进行采样并求和——就得到这条轨迹的总成本。我们希望真值轨迹的成本尽量低,理想情况下网络应该学会把障碍物放到真值轨迹以外的地方。
由于轨迹只是索引,整个链路是可微的:成本矩阵从感知网络来,轨迹采样不切断梯度,所以梯度可以一路传回 BEV 特征,再传回图像编码器。这就是 Shoot 名字的由来——你可以想象成在网络里“朝地图射出一条射线”。
我复现时发现一个实际操作上的坑:这个损失的梯度作用范围非常窄,只有轨迹覆盖到的格子会影响更新。如果训练轨迹太短、覆盖格子太少,感知网络的监督信号会很稀疏。解决办法一是把轨迹做得足够长,二是随机采样多条候选轨迹,把它们的成本一起纳入损失,让网络学会“即使走不同路线,也不能撞上障碍物”。
4.3 端到端到底有没有必要
论文里有一个非常有意思的分析:当成本函数给得合理时,分开训练感知网络和规划器,效果和端到端差不多;但当成本函数故意给得很差时,端到端训练能明显缓解错误。也就是说,Shoot 的真正价值不是替代下游规划器,而是给感知表示施加了一个“驾驶偏置”——网络会倾向于把障碍物放到路径之外,让 BEV 特征不再只是客观的“有什么东西”,而是“该往哪开”的潜在答案。
这也是后来很多工作讨论端到端自动驾驶时反复引用的论据:感知和规划之间的接口,不该只是一堆检测框。LSS把成本图作为接口,等于让感知网络提前考虑了下游决策的需要。
5. 实验设定与NMN评估:这篇论文如何证明自己
5.1 数据与配置清单
LSS使用的数据集是公开的自动驾驶多模态数据集,包含 6 个相机、激光雷达、毫米波雷达和高精地图。论文里几个关键配置:
| 配置项 | 常见取值 | 说明 |
|---|---|---|
| 相机数量 | 6(前视、后视、侧视等) | 形成 360 度覆盖 |
| 深度槽位 | 41 个,范围 4m~45m | 每隔 1m 一个槽位 |
| BEV范围 | 100m × 100m | 以自车为中心 |
| BEV分辨率 | 0.5m/格 | 对应 200×200 栅格 |
| 图像编码器 | EfficientNet 系列 | 输出下采样特征图 |
| BEV编码器 | ResNet 或 EfficientNet | 处理 BEV 特征 |
| 监督信号 | 2D/3D语义分割、可选深度监督 | 图像和BEV两个层面 |
这里有个容易忽略的设计:深度槽位从 4m 开始,而不是从 0m 开始。近处的盲区通常由保险杠附近的其他传感器负责,视觉这一路没必要在自车正下方浪费计算量;而 45m 之外的深度槽位如果继续加密,显存开销会爆炸。这个取舍在后来的 BEV 感知工作中被广泛沿用。
5.2 为什么不用 IoU:NMN 的设计哲学
做BEV语义分割评估时,最直接的想法是拿预测图与真值图算 IoU。但 LSS 作者提出了一个值得思考的问题:在自动驾驶场景里,所有像素点的误差真的同等重要吗?
NMN 是论文配套数据集上定义的新评测指标,它的核心思想是重新标定错误的权重。车辆前方几米内“该看到的障碍没看到”,远比远处“多预测了一个幽灵目标”更致命。NMN 会把“我们本应知道却不知道”的错误和“我们预测多了但真值里没有”的错误分开计分,而不是像 IoU 那样把它们都当作对称的 FP/FN 处理。换句话说,IoU 是一个通用视觉指标,NMN 则是一个“驾驶安全”导向的指标。
论文里几组对比实验显示,光有 BEV 分割损失时模型已经能学到一部分结构;加上图像监督(IS)和深度监督(DS)之后,NMN 的提升非常明显。尤其深度监督,等于直接把雷达点云投影到图像上,生成稀疏的深度标签,用二值交叉熵约束深度分布。这个操作让 Lift 阶段生成的视锥点云从一开始就落在正确的位置附近,而不必靠后续的 BEV 网络慢慢摸索。
5.3 图像监督和深度监督各自贡献了什么
我在阅读和复现时对这两个监督的感受是:图像监督解决的是“语义”问题,它让图像编码器输出的 context 更有辨识度,知道这是车、那是路;深度监督解决的是“位置”问题,它让 depth 分支输出的密度分布更锐利、更准确。两者一个管“是什么”,一个管“在哪”,加起来正好对应 Lift 阶段外积的两个因子。
如果你在工程中只能加一个,我的建议是先加深度监督。因为 BEV 几何的准确性对下游规划影响更直接,而且雷达投影生成深度标签的成本很低,几乎不需要额外标注。
6. 复现LSS时踩过的坑,以及保留的改进空间
6.1 第一坑:Lift 阶段的张量爆炸
Lift 阶段生成的特征体非常大。以 6 个相机、41 个深度槽位、特征图尺寸 H/8×W/8、特征通道 C 为例,一次前向的张量是 6×41×(H/8)×(W/8)×C。如果你直接把通道数设成 256,Batch Size 设成 8,显存会瞬间见底。
我的建议是分步走:先把特征图降到 1/16 分辨率,把 BEV 通道压到 64 或 128,Batch Size 从 2 开始跑通流程,再逐步放大。后来社区里出现的 fast-BEV-pool 等自定义算子能把 Splat 阶段提速一个数量级,但 Lift 阶段巨大的中间张量仍然是显存瓶颈,这一步只能靠分辨率和通道数来平衡。
6.2 第二坑:数据增强必须保持“整车”同步
LSS 的数据增强极其讲究。多相机图像会一起做随机缩放、旋转、翻转、裁剪和颜色抖动,同时相机外参也必须同步变换;BEV 真值也要做同样的平移、旋转和缩放。很多早期复现者在这里翻过车——只对图像做增强,不对相机外参和 BEV 真值做同步变换,结果训练出来的 BEV 图在自车附近出现各种扭曲。
这个坑的根因是:相机位姿变了,图像内容变了,但外参还停留在原地,网络就会学到“图像特征和 BEV 位置之间的错误映射”。解决方式并不复杂,但需要写数据管线时格外小心,保证图像、外参、BEV 真值三者使用同一个几何变换矩阵。
提示:如果发现 BEV 分割图在自车附近出现扭曲,第一件事不是调网络结构,而是检查增强代码里的几何变换是否同步。
6.3 第三坑:坐标系约定
LSS 默认的坐标系是“前 x、左 y、上 z”的自车右手系。但很多相机标定工具使用的是 OpenCV 那种“右 x、下 y、前 z”的相机坐标系。如果你从某个现成标定流程拿到的外参直接塞进 LSS,BEV 图很容易出现整体翻转或错位。
排查方法很土但很有效:跑一次前向,把 BEV 特征可视化,看自车周围的形状是否合理。如果前方车辆特征跑到了后方,或者左右侧的马路沿翻转了,直接检查内外参变换里的坐标轴是否对齐。这种问题可能花掉你大半天,但一旦理顺,后面就很顺。
6.4 这套思想到现在还能用吗
LSS 之后出现了大量改进:BEVDepth 用深度监督和显式深度校准,把 Lift 阶段的深度分布做得更准;BEVFormer 用可变形注意力替换了简单的 splat 求和,让 BEV 查询点直接去图像里取特征;Fast-BEV 则在工程速度上做了大规模优化。但很有趣的是,几乎所有方案在最底层仍然没有离开“把图像特征沿光线映射到 BEV 栅格”这个内核。
这也是我把这篇论文列为多相机感知“必读第一篇”的原因。它不一定在每个指标上都是最强的,但它把一个原本要靠手工几何、先验假设和复杂 calibration 才能解决的问题,变成了一个干净、可微、可扩展的学习管线。
我自己的经验是:第一次复现 LSS 时,跑通之后并没有马上看到“惊艳”的 BEV 图,但把数据增强和外参修好之后,效果立刻上一个台阶。它给我的启发是,在感知融合这件事上,几何约束和网络学习不是替代关系,而是乘法关系。如果现在让我给一个新项目选型,我仍然会先拿这套 Lift-Splat 管线做底,再根据场景往上加深度监督、图像监督以及后续的工程优化,而不是一上来就上大模型。调这些细节的过程,比读十篇综述都管用。