4、BEV感知经典模型解析(上):LSS(Lift-Splat-Shoot)模型原理、深度分布估计与特征提升
各位同学,欢迎来到第四章。前面我们聊了BEV感知的整体框架,也聊了数据标注和坐标系。今天,我们正式进入经典模型环节。
第一个要啃的硬骨头,就是LSS。说实话,LSS这个名字起得挺形象的——Lift(提升)、Splat(拍平)、Shoot(射击)。你想想看,这三个词基本就把它的核心流程说清楚了。
我个人习惯把LSS看作是「2D到3D的桥梁」。为什么这么说?因为摄像头天然就是2D的,它没有深度信息。而BEV空间是3D的,我们需要把2D图像上的每个像素,映射到3D空间中去。LSS就是干这个活的。
4.1 LSS的整体思想:从2D到3D的逆透视变换
先问大家一个问题:给定一张2D图像,你怎么知道某个像素在3D空间中的位置?
答案很简单——你需要深度。有了深度,再加上相机内参和外参,就能把像素坐标反投影到3D空间。这就是所谓的「逆透视变换」。
但问题来了:单目相机没有深度信息。怎么办?
LSS的做法很直接:让网络自己去学深度分布。它不直接预测一个确定的深度值,而是预测一个深度分布——说白了,就是每个像素在多个离散深度区间上的概率。
我在项目中遇到过这样一个场景:用LSS做园区内的障碍物检测。园区里有个路灯杆,从2D图像上看,它和后面的墙壁几乎重叠在一起。如果只靠2D检测,你根本分不清哪个在前哪个在后。但LSS通过深度分布估计,能给出路灯杆在近处、墙壁在远处的概率分布,从而在BEV空间中把它们正确分开。嗯,这就是深度分布的魅力。
核心思想总结:LSS通过「深度分布估计」将2D图像特征提升到3D空间,再通过「体素池化」将3D特征拍平到BEV平面,最终在BEV空间中进行检测或分割。
4.2 Lift阶段:深度分布估计与特征提升
Lift阶段是整个LSS的精华所在。它要解决的核心问题是:如何把2D图像特征,变成3D空间中的特征点?
具体来说,Lift阶段做了两件事:
- 预测深度分布:对每个像素,预测它在D个离散深度区间上的概率分布。
- 特征提升:将2D图像特征与深度分布做外积,得到3D特征体。
我们一步步来看。
4.1.1 深度分布预测
假设我们有一张H×W的图像,经过主干网络提取特征后,得到C维的特征图。同时,我们还有一个并行的分支,专门用来预测深度分布。
这个深度分支的输出是什么?是一个H×W×D的张量,其中D是离散深度区间的数量。每个像素对应一个D维的向量,表示该像素在D个深度区间上的概率。
举个例子:假设D=41,深度范围是4m到45m,每个区间宽度1m。那么对于图像上的某个像素,深度分支会输出41个概率值,分别表示该像素落在4-5m、5-6m、...、44-45m的概率。
这里有个细节要注意:深度分布是经过softmax归一化的。也就是说,每个像素的41个概率值加起来等于1。
我曾经踩过一个坑:一开始训练时,深度分布总是收敛到某个固定深度,导致BEV空间中的特征非常稀疏。后来发现,是深度分支的初始化方式不对。我建议使用均匀分布初始化,让网络自己去学习哪些深度区间更重要。
避坑指南:深度区间的数量D和范围要根据实际场景来定。做高速场景,深度范围可以设大一些(比如100m);做园区场景,深度范围设小一些(比如30m)就够了。D太大,计算量会爆炸;D太小,深度分辨率不够。
4.1.2 特征提升:外积操作
有了深度分布,怎么把它和图像特征结合起来?
LSS的做法是:外积。
具体来说,对于每个像素位置(u, v),我们有一个C维的图像特征向量f(u,v),和一个D维的深度分布向量d(u,v)。将这两个向量做外积,得到一个C×D的矩阵。
这个矩阵的含义是什么?它表示:在深度区间k上,该像素的特征是f(u,v)乘以d_k(u,v)。说白了,就是用深度概率去加权图像特征。
对所有像素都做一遍外积,我们就得到了一个H×W×C×D的四维张量。这就是所谓的「特征提升」——把2D图像特征提升到了3D空间。
# 伪代码:Lift阶段的核心操作
# 输入:
# img_features: [B, C, H, W] 图像特征
# depth_probs: [B, D, H, W] 深度分布(已softmax)
# 输出:
# lifted_features: [B, C, D, H, W] 提升后的特征
# 外积操作(简化版)
lifted_features = img_features.unsqueeze(2) * depth_probs.unsqueeze(1)
# 形状变化:[B, C, 1, H, W] * [B, 1, D, H, W] -> [B, C, D, H, W]
你想想看,这个操作其实很优雅。它没有硬性地给每个像素分配一个深度值,而是保留了所有可能的深度假设,让网络自己去学习。这就是LSS的精髓所在。
4.3 Splat阶段:从3D到BEV的拍平操作
Lift阶段结束后,我们有了一个3D特征体。但这个特征体是稀疏的——因为大部分深度区间上,概率都很小,对应的特征也接近于0。
Splat阶段要做的,就是把这个3D特征体「拍平」到BEV平面。
具体步骤是这样的:
- 坐标变换:根据相机内参和外参,把每个像素的(u, v, d)坐标映射到3D空间中的(x, y, z)坐标。
- 体素化:把3D空间划分成一个个体素(voxel),每个体素对应BEV平面上的一个网格。
- 池化:把落在同一个体素内的所有特征点进行求和池化(sum pooling),得到该体素的特征。
这里有个关键点:求和池化 vs 平均池化。LSS用的是求和池化,为什么?
我个人理解是:求和池化能保留特征的「能量」。如果一个体素内落入了多个特征点,说明这个位置有多个像素都认为这里有物体,求和池化会放大这个信号。而平均池化会稀释掉这种信息。
我在项目中做过对比实验:用求和池化,BEV特征图上的障碍物响应更集中、更强烈;用平均池化,响应会变得模糊。所以,我建议你默认使用求和池化。
注意事项:Splat阶段的计算量很大,尤其是坐标变换和体素池化。在实际部署时,建议预先计算好坐标映射表,避免重复计算。另外,体素的大小要适中——太小了计算量大,太大了会丢失空间分辨率。
4.4 Shoot阶段:在BEV空间中进行检测
Shoot阶段相对简单。经过Splat之后,我们得到了一个C'×H_bev×W_bev的BEV特征图。这个特征图已经对齐到了BEV坐标系中,可以直接用2D检测头来做检测。
常用的检测头包括:
- CenterNet-style:预测目标的中心点、尺寸、朝向等。
- Anchor-based:在BEV空间上预设锚框,然后做分类和回归。
- Transformer-based:用DETR之类的结构做端到端检测。
我个人比较喜欢CenterNet-style,因为它简单、高效,而且不需要复杂的后处理。在嵌入式平台上,少一个NMS步骤,能省不少时间。
4.5 LSS的优缺点分析
聊了这么多,我们来总结一下LSS的优缺点。
| 优点 | 缺点 |
|---|---|
| 显式建模深度分布,可解释性强 | 深度分布预测精度有限,远距离误差大 |
| 能处理多相机输入,统一到BEV空间 | 计算量大,尤其是Lift阶段的外积操作 |
| 对相机外参变化有一定鲁棒性 | 需要预先定义深度区间,灵活性不足 |
| 结构清晰,易于理解和实现 | 在遮挡严重场景下,深度分布容易混淆 |
说实话,LSS不是最完美的方案。但它是一个里程碑式的作品,后续很多模型(比如BEVDet、BEVDepth)都是在它的基础上改进的。理解了LSS,你就掌握了BEV感知的一条核心脉络。
4.6 实战经验:LSS在嵌入式平台上的部署要点
最后,分享一些我在嵌入式平台上部署LSS的经验。
- 深度区间量化:在嵌入式平台上,浮点运算很慢。我建议把深度区间量化成整数,用查表代替浮点运算。
- 稀疏化处理:Lift阶段产生的3D特征体非常稀疏。可以用稀疏张量来存储和计算,能省不少内存。
- 预计算坐标映射:如果相机外参固定,可以提前算好每个像素对应的3D坐标,存成查找表。运行时直接查表,省去坐标变换的计算。
- 模型剪枝:深度分支其实可以做得更轻量。我试过把深度分支从ResNet18换成MobileNetV2,精度只掉了1%,但速度提升了30%。
小技巧:在嵌入式平台上,可以把Lift和Splat两个阶段合并成一个自定义算子。用CUDA或NPU的算子库去实现,能大幅减少中间张量的读写开销。我曾在Jetson Orin上做过优化,端到端延迟从120ms降到了45ms。
好了,这一章的内容就到这里。LSS的原理其实不复杂,但细节很多。我建议你动手实现一下,哪怕只是跑通一个最小demo,也能加深理解。下一章,我们会继续解析BEV感知的另一个经典模型——BEVDepth,看看它是怎么在LSS基础上改进深度估计的。
记住,纸上得来终觉浅,绝知此事要躬行。动手试试吧!