LSS是NVIDIA在ECCV2020上发表的文章。

理解一下论文标题中的Lift, Splat, Shoot三个单词。

这三个单词对应模型中三个核心步骤。

  • Lift:提升。2D图像特征提升到3D视锥空间特征。

  • Splat:泼溅。所有相机生成的3D视锥特征,泼洒到统一的BEV平面网格。

  • Shoot:发射。向BEV代价图中发射预生成的轨迹模板,实现E2E的运动规划。

如果使用多个单目相机传感器,要使用多个单图像的Detector,然后根据相机内外参,将每个Detector的结果平移到自车坐标系。这类方法具备三大对称性,也是优势:

  • 平移等变性:若图像内的像素坐标整体平移,Detector的输出也同步平移。

  • 置换不变性:最终自车系得预测结果,与相机位置无关。

  • 自车系等距等变性:相机相对自车的空间位置,不会影响检测结果;若自车系旋转平移,则预测结果也旋转平移。

这类方法的缺陷是:

  • 自车系的预测结果无法通过反向传播到原始传感器输入,模型无法通过数据学习跨相机信息融合的策略

  • 没法利用运动规划模块的反馈,感知和规划模块依然是独立的

LSS

Lift

对相机中的每张图像单独处理,将单张图像从2D像素坐标系Lift到所有相机共享的3D世界坐标系,生成视锥(frustum)3D特征点云。

每个像素不是预测单一深度值,而是生成所有可能深度下的特征表征。

Step1

为每个像素生成离散深度的3D点云,这步没有需要学习的参数。

对于图像XR3×H×WX \in \mathbb{R}^{3 ×H ×W},对应内参II、外参EE;取图像中任意像素pp,其像素坐标为(h,w)(h, w)

定义一个离散的深度集合D={d0+Δ,d0+2Δ,...,d0+DΔ}D = \{d_0+\Delta, d_0+2\Delta, ..., d_0+|D|\Delta\}d0d_0为最小深度,Δ\Delta 为深度步长,D|D|为深度离散数量。在论文中,深度范围为4m到45m,步长为1m,共41个档。

像素点pp关联D|D| 个3D点(h,w,d)(h, w,d)

遍历像素点,得到该相机图像的3D点云。点云的规模为H×W×DH\times W \times D,形状是视锥,也就是相机可观测的3D空间。

这部分没有可学习参数,仅通过设计规则,即相机内外参和离散深度生成。

Step2

为3D点云赋予深度相关特征,这步Learnable。

对像素点pp,模型使用双分支预测,得到俩输出:

  • cRCc \in \mathbb{R}^{C}上下文特征向量,其中C=64{C} =64 为特征通道数,表征该像素的2D特征,比如深度,纹理。

  • 深度概率分布αD1\alpha \in \triangle^{|D|-1},也就是对这个像素,在深度集合中每个深度的置信度,也可以说是权重。

最终像素点pp在深度 dd 处的最终特征定义为cd=αdcc_{d}=\alpha_{d} c

如何理解这一步呢,假设深度分布置信度(权重)α=[a1,a2,...,a41]\alpha = [a_1, a_2, ... , a_{41}] ,其中每个元素都是标量。还有一个C=64C=64 维的特征向量c=[c1,c2,...,c64]c = [c_1,c_2,...,c_{64}]。相乘就是在加权,加权之前每个像素只有一个dim=Cdim=Ccc 向量,加权后,每个深度dd都有了一个对应的cdc_{d}向量。用αd\alpha_{d}cc加权,那当αd\alpha_{d} 越大,该向量cc 也就越大,模型更会专注于αd\alpha_{d} 深度下的特征向量,因为它置信度高啊。

看下面右图中α2c\alpha_{2}c 深度更高,更多的信息就被编码在了α2\alpha_{2} 这个深度。

LSS-Lift.webp

如果某个像素深度比较确定,那么会得到近似one-hot的深度分布,把所有特征都集中到这个真实深度点上,效果类似pseudolidar。

反之,这个像素深度比较模糊,那么网络会预测一个均匀的深度分布,把特征平摊到整个射线上,效果类似OFT (Orthographic feature transform)。OFT不做深度选择,直接把特征扩散到整个射线上。这样的好处是,当深度明确时,特征集中到准确的深度上,提升3D定位精度;若深度不确定,均匀分布,避免错误深度带来估计的偏差。

Lift阶段最终输出的3D特征点云维度为C×H×W×DC\times H\times W \times D

Splat

所有相机经过Lift步骤后得到对应的Frustum3D特征点云,将这些点云融合并栅格化为统一的BEV特征张量。

弄成BEV张量有啥好处呢,BEV张量是2D的,可以直接输入2D的CNN进行下游任务。

LLS借鉴了PointPillars (PP) 的方法,使用Pillar Pooling方法,实现了多相机的3D特征到BEV的2D特征的转换。

Step1

在BEV 3D坐标系中,定义柱体也就是Pillar——也就是BEV平面上每个栅格对应一个Pillar,每个Pillar是高度无限的Voxel。

所有相机组成的3D特征点云中的每个点,分配到其在BEV平面上的最近的Pillar,根据 (x,y)(x,y) 坐标分配。坐标是世界坐标系,通过41个深度分布,结合内外参进行坐标系逆运算得来。

Step2

然后对每个Pillar内所有3D特征点执行按通道求和,最终将所有Pillar的Pooling特征按BEV排列,生成BEV 2D的特征张量:RC×HBEV×WBEV\mathbb{R}^{C ×H_{BEV} ×W_{BEV}},其中CC 为特征通道,HBEV/WBEV{H_{BEV} /W_{BEV}} 为BEV的栅格的空间维度,有量纲,单位为米,注意和前面特征点云维度区分。文中设置为50米,grid分辨率=0.5米。

为了避免逐个求和(太慢),作者使用了前缀和方法。注意Pytorch中数组索引是左闭右开。

具体做法是把所有 3D 点排成一个连续的长列表,不补零、不浪费空间;再用两个极小的数组,记住每个 Pillar 的点,在长列表里从第几个开始、到第几个结束,这两个数组就是起始(start)、结束(end)索引。

假设Pillar的分配结果为:

  • P0:点0,1

  • P1:点2,3,4

  • P2:点5,6

那么可以得到如下连续的长列表:

长列表 = [点0, 点1, 点2, 点3, 点4, 点5, 点6]
索引:    0    1    2    3    4    5    6

然后生成索引数组,意思就是每个Pillar在长列表中的哪里开始哪里结束

start = [0, 2, 5]
end   = [2, 5, 7]

我们假设长列表里存的是数字(向量同理):

长列表 points = [1, 2, 3, 4, 5, 6, 7]
索引:          0  1  2  3  4  5  6

计算前缀和数组:

  • S[0] = 1

  • S[1] = 1+2 = 3

  • S[2] = 1+2+3 = 6

  • S[3] = 1+2+3+4 = 10

  • S[4] = 1+2+3+4+5 = 15

  • S[5] = 1+2+3+4+5+6 = 21

  • S[6] = 1+2+3+4+5+6+7 = 28

最终前缀和数组和索引数组

S = [0, 1, 3, 6, 10, 15, 21, 28]
Pillar 0: start=0, end=2  → 点0点1
Pillar 1: start=2, end=5  → 点2点3点4
Pillar 2: start=5, end=7  → 点5点6

直接计算:

  • Pillar 0 的和(1+2)

S[end] - S[start] = S[2] - S[0] = 3 - 0 = 3

  • Pillar 1 的和(3+4+5)

S[5] - S[2] = 15 - 3 = 12

  • Pillar 2 的和(6+7)

S[7] - S[5] = 28 - 21 = 13

Shoot

Shoot阶段的输入是BEV特征图和轨迹模板,输出是最优轨迹τ\tau和Cost Map。

先对驾驶轨迹进行K-Means聚类,得到1000个轨迹模板。

然后使用BEV特征图作为输入,输出得到Cost Map。每个网格输出p(τio)p(\tau_i|o),索引为ii的Grid代价越大,那么置信度p(τio)p(\tau_i|o)越小,让模型各个倾向于走代价小的轨迹。

Model

超参

符号

含义

B

Batch size

4

N

摄像头数量

6

C

输入图像通道数

3

imH, imW

输入图像分辨率

128, 352

fH, fW

特征图分辨率

8, 22

D

离散深度分布

41

camC

相机特征维度

64

X, Y

BEV平面Grid数

200, 200

outC

最终分割输出通道

1

模型输入

最终输入图像的尺寸resize为[128, 352]

张量

Shape

说明

x (images)

[B, N, 3, imH, imW] = [4, 6, 3, 128, 352]

N 路相机图像

rots

[B, N, 3, 3]

相机到 ego 旋转矩阵

trans

[B, N, 3]

相机到 ego 平移向量

intrins

[B, N, 3, 3]

相机内参矩阵

post_rots

[B, N, 3, 3]

数据增强后的旋转(图像空间)

post_trans

[B, N, 3]

数据增强后的平移(图像空间

Lift

  • Frustum生成,下采样16倍

ds: [D, fH, fW]  →  [41, 8, 22]
xs: [D, fH, fW]  →  [41, 8, 22]
ys: [D, fH, fW]  →  [41, 8, 22]
frustum = stack(xs, ys, ds, dim=-1)  →  [D, fH, fW, 3] = [41, 8, 22, 3]
# xs, ys, ds 可以理解为(x_pixel, y_pixel, depth)

  • 反投影到World Frame,获取点云

geom = rots @ inv(intrins) @ points + trans → [B, N, D, fH, fW, 3] = [B, N, 41, 8, 22, 3]
  • 相机特征提取

# 合并BN
[B, N, 3, H, W] → [B*N, 3, 128, 352]
  • EfficientNet-B0:

input:       [B*N, 3, 128, 352]
reduction_5  [B*N, 320, 4, 11]  # 最深层
reduction_4  [B*N, 112, 8, 22]  # 倒数第二层
reduction_5 upsample x 2 → [B*N, 320, 8, 22] 
res = concat(reduction_4, reduction_5) → [B*N, 432, 8, 22]
Conv2d(res) → [B*N, 512, 8, 22]
  • depthnet

depthnet: [B*N, 512, 8, 22] → [B*N, 105, 8, 22]
  • 外积

depth  = softmax(x[:, :D])         → [24, 41, 8, 22]       # 深度概率分布
feat   = x[:, D:D+C]               → [24, 64, 8, 22]       # 图像语义特征

# outer product: 每个像素 × D个深度
depth.unsqueeze(1) * feat.unsqueeze(2)
= [B*N, 1, 41, 8, 22] * [B*N, 64, 1, 8, 22]
→ [B*N, 64, 41, 8, 22]    
# 恢复B, N
= [B, N, 41, 8, 22, 64]

Splat

  • Voxel Pooling

input = [B, N, 41, 8, 22, 64]
total= B*N*D*H*W = 4×6×41×8×22 = 173184

input.reshape(total, 64)     → [173184, 64]
geom.view(total, 3)     → [173184, 3]    # ego(x,y,z) 
geom → [173184, 4]    # ego(x,y,z,batch_id) 

# 过滤点云(越界/重复删除)
total → kept[]
input → [kept, 64] 
geom → [kept, 4] 

# 压缩高度,投影到BEV
[B, 64, 200, 200]
  • BEV Encoder

B = 4
input:          [4, 64, 200, 200]

conv1 (7×7, s=2): [4, 64, 100, 100]
bn1 + relu:       [4, 64, 100, 100]

layer1 (x1):      [4, 64, 100, 100]   ← skip connection
layer2:           [4, 128, 50, 50]
layer3:           [4, 256, 25, 25]

Up1(64+256→256, scale=4):
  layer3 upsample ×4: [4, 256, 100, 100]
  cat(x1):            [4, 320, 100, 100]   # 256+64=320
  conv → out:         [4, 256, 100, 100]

Up2:
  upsample ×2:        [4, 256, 200, 200]
  conv(256→128):      [4, 128, 200, 200]
  conv(128→outC=1):   [4, 1, 200, 200] # 最终输出

参考文章

[1] [2008.05711] Lift, Splat, Shoot: Encoding Images From Arbitrary Camera Rigs by Implicitly Unprojecting to 3D