4、LSS原理详解:Lift-Splat-Shoot核心思想、深度分布预测、视锥点云构建、BEV池化操作
好,咱们今天来聊聊LSS。这个模型在BEV感知领域,可以说是里程碑式的存在。
我第一次看到LSS论文的时候,说实话,有点懵。它把2D图像转成3D空间的方式,跟之前的方法完全不一样。但后来我在实际项目中跑通之后,不得不服——这思路确实巧妙。
LSS的全称是Lift-Splat-Shoot。名字很形象:
- Lift:把2D图像特征“提升”到3D空间
- Splat:把3D特征“拍平”到BEV网格
- Shoot:在BEV空间做规划(原论文里用这个做路径规划)
咱们今天重点讲前两步,也就是Lift和Splat。这是LSS最核心的贡献。
4.1 核心思想:从图像到BEV的“可微分”映射
LSS要解决什么问题?说白了,就是让模型学会“看图识位置”。
你想想看,一张2D图片里,一个像素点可能对应3D空间中的无数个点。怎么确定它到底在哪儿?传统做法是用几何约束,比如双目视觉。但LSS走了一条新路——让网络自己学出深度分布。
我个人觉得,LSS最牛的地方在于:它把整个映射过程做成了可微分的。这意味着什么?意味着你可以把LSS塞进一个端到端的训练框架里,用梯度反传来优化整个流程。
嗯,这里要注意:可微分不是LSS独有的,但LSS把“深度估计”和“特征投影”这两个步骤融合得特别好。
4.2 深度分布预测:让网络学会“猜深度”
深度分布预测,是LSS的Lift阶段的核心。
具体怎么做?我直接上代码逻辑,这样更直观:
# 伪代码:深度分布预测
def predict_depth_distribution(image_features):
# image_features: [B, C, H, W]
# 输出深度分布: [B, D, H, W],D是离散深度bin的数量
# 1. 用一个小网络预测深度分布
depth_logits = conv2d(image_features, out_channels=D)
# 2. 用softmax归一化,得到每个像素在不同深度上的概率
depth_probs = softmax(depth_logits, dim=1)
return depth_probs
这里有个关键点:深度是离散化的。LSS把连续深度空间划分成D个bin,比如从1米到50米,每隔0.5米一个bin。然后网络预测每个像素落在每个bin上的概率。
我在项目中遇到过一个问题:D设多大合适?设小了,深度分辨率不够;设大了,计算量爆炸。我个人的经验是:D=41或61比较常用,覆盖1-50米范围,步长1米或0.8米。
4.3 视锥点云构建:从2D到3D的“桥梁”
有了深度分布,下一步就是构建视锥点云。这个名字听起来高大上,其实原理很简单。
每个像素点,沿着相机光线方向,在不同深度上“撒”一堆点。每个点都带有一个特征向量(来自图像特征)和一个深度概率。
数学上可以这样理解:
# 伪代码:视锥点云构建
def build_frustum_points(depth_probs, image_features, camera_params):
# depth_probs: [B, D, H, W]
# image_features: [B, C, H, W]
# camera_params: 内参、外参
# 1. 生成所有像素在所有深度上的3D坐标
# 用相机内参把像素坐标(u,v) + 深度d 投影到相机坐标系
# 再用外参转到世界坐标系
points_3d = project_to_3d(u_grid, v_grid, depth_bins, camera_params)
# points_3d: [B, D, H, W, 3]
# 2. 把图像特征广播到每个深度上
features_3d = image_features.unsqueeze(1) * depth_probs.unsqueeze(2)
# features_3d: [B, D, C, H, W]
return points_3d, features_3d
你想想看,这一步其实就是在做“特征提升”。每个2D像素点,变成了D个3D点,每个点都带着特征和概率。
这里有个细节:特征和概率是相乘的。这意味着深度概率高的位置,特征贡献就大;概率低的位置,特征被抑制。这个设计很巧妙,相当于让网络自己决定“哪些深度上的特征值得保留”。
4.4 BEV池化操作:把散点“拍”成网格
视锥点云构建完之后,我们得到了一堆散乱的3D点。但BEV感知需要的是规整的网格特征。怎么办?
LSS的做法是:BEV池化。说白了,就是把3D空间划分成一个个网格,然后把落在同一个网格里的点特征聚合起来。
具体流程:
- 定义BEV网格:比如200x200的网格,每个网格代表0.5米x0.5米的实际空间
- 分配点云到网格:根据每个点的3D坐标,找到它属于哪个网格
- 特征聚合:对每个网格内的所有点特征,做求和或平均
# 伪代码:BEV池化
def bev_pooling(points_3d, features_3d, grid_size):
# points_3d: [B, N, 3] N = D*H*W
# features_3d: [B, N, C]
# 1. 把3D坐标映射到网格索引
grid_x = (points_3d[..., 0] - x_min) / grid_resolution
grid_y = (points_3d[..., 1] - y_min) / grid_resolution
grid_indices = stack([grid_x, grid_y], dim=-1).long()
# 2. 用scatter_add做求和池化
bev_feature = zeros([B, C, H_grid, W_grid])
for b in range(B):
bev_feature[b] = scatter_add(
features_3d[b],
grid_indices[b],
dim=2 # 在空间维度上累加
)
return bev_feature
我在实际部署时发现,BEV池化这一步的计算量其实不小。尤其是当D、H、W都比较大时,点云数量会达到百万级别。我建议用CUDA自定义算子来加速,或者用稀疏卷积的思路来优化。
4.5 整体流程总结
咱们把LSS的Lift-Splat流程串起来看:
| 步骤 | 输入 | 输出 | 关键操作 |
|---|---|---|---|
| 1. 特征提取 | 多视角图像 | 2D图像特征 [B, C, H, W] | CNN/Transformer |
| 2. 深度预测 | 2D图像特征 | 深度分布 [B, D, H, W] | Softmax归一化 |
| 3. 视锥构建 | 深度分布 + 图像特征 | 3D点云特征 [B, D*H*W, C] | 外积 + 坐标变换 |
| 4. BEV池化 | 3D点云特征 | BEV特征 [B, C, X, Y] | Scatter求和 |
说白了,LSS就是一套“从图像到BEV”的完整流水线。它用深度分布预测解决了“2D到3D”的歧义性问题,用BEV池化解决了“散点到网格”的规整化问题。
嗯,LSS的原理就讲到这里。下一章咱们聊聊LSS的训练技巧和工程化落地。到时候我会分享一些我在实际项目中踩过的坑,比如多相机标定误差怎么处理、深度分布不收敛怎么办等等。