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米。

我曾经踩过的坑:深度分布预测的初始阶段非常不稳定。建议先用预训练好的深度估计模型做初始化,或者加一个深度监督信号。否则前几个epoch的BEV特征全是噪声。

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空间划分成一个个网格,然后把落在同一个网格里的点特征聚合起来。

具体流程:

  1. 定义BEV网格:比如200x200的网格,每个网格代表0.5米x0.5米的实际空间
  2. 分配点云到网格:根据每个点的3D坐标,找到它属于哪个网格
  3. 特征聚合:对每个网格内的所有点特征,做求和或平均
# 伪代码: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池化是LSS的“Splat”步骤。它把离散的3D点特征“拍”成了规整的BEV网格特征。这个操作是可微分的,所以整个模型可以端到端训练。

我在实际部署时发现,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,不要急着调参。先跑通一个最小demo,把整个流程走一遍。重点关注深度分布的可视化——看看网络到底在哪些深度上“犹豫不决”。这能帮你快速定位问题。

嗯,LSS的原理就讲到这里。下一章咱们聊聊LSS的训练技巧和工程化落地。到时候我会分享一些我在实际项目中踩过的坑,比如多相机标定误差怎么处理、深度分布不收敛怎么办等等。