论文链接:https://arxiv.org/pdf/2112.11790.pdf;
Github仓库源码:https://github.com/HuangJunJie2017/BEVDet;
BEVDet这篇论文主要是提出了一种基于BEV空间下的3D目标检测范式,BEVDet算法模型的整体流程图如下:
整体流程如下:先对多视角图像进行特征提取(Image-view Encoder),再通过基于LSS的视角转换(View Transformer)将多视角特征投影到bev空间下,再用和第一步类似的backbone对bev特征进行编码,最后进行目标检测。这种方法虽然在LSS这一步存在不少冗余的计算,但好处是得到了显式的bev特征,可以做bev视角下的特征提取和数据增强,并且可以使用任意的目标检测头。
BEVDet算法模型的前向过程主要包括以下四个部分:
一、Image-View Encoder模块
利用主干网络和特征融合网络提取输入环视图片的图像特征。
论文中采用ResNet-50作为主干网络,首先主干网络会对输入的环视图像,记作Tensor([bs, N, 3, H, W]),提取多尺度特征;其中bs = batch size,N = 环视图像的个数,H, W = 输入图像的宽和高。
多尺度特征的分辨率如下:
L0 = Tensor([bs * N,1024,H / 16,W / 16])
L1 = Tensor([bs * N,2048,H / 32,W / 32])
然后,采用FPN、FPN-LSS来融合多尺度特征,融合后特征为:
Tensor([bs,N,512,H / 16,W / 16])
二、View Transformer模块
借鉴LSS算法的思想构建BEV空间特征。
2.1 深度估计网络构建3D视锥特征
与LSS算法相似,利用深度估计网络预测特征图每个单元格的离散深度信息和语义特征,用外积运算构建3D视锥特征,对应的逻辑用伪代码表示:
输入特征: Tensor([bs * N, 512, H / 16, W / 16])
输出特征: Tensor([bs * N, 139, H / 16, W / 16])
1) 输出特征的前59维代表深度估计网络预测出来的59个离散深度, 然后用Softmax()函数获得概率分布;
2) 输出特征的后80维代表深度估计网络预测出来的80维的语义特征;
3) 对深度概率分布和语义特征利用外积运算得到3D视锥特征
2.2 生成视锥3D坐标点并完成向BEV空间的投影映射
1)生成3D视锥点(源码中的create_frustum()函数,与LSS一样)
需要注意的是,3D坐标中的横纵坐标是基于特征图每个单元格映射回原始图像的位置,深度是预先设置好的深度范围。
def create_frustum(self):
# 原始图片大小, ogfH:128 ogfW:352
ogfH, ogfW = self.data_aug_conf['final_dim']
# 下采样16倍后图像大小,fH: 8 fW: 22
fH, fW = ogfH // self.downsample, ogfW // self.downsample
# self.grid_conf['dbound'] = [4, 45, 1]
# 在深度方向上划分网格,ds:D×fH×fW (41*8*22)
ds = torch.arange(*self.grid_conf['dbound'], dtype=torch.float).view(-1, 1, 1).expand(-1, fH, fW)
D, _, _ = ds.shape # D:41 表示深度方向上网格的数量
# 在0到351上划分22个格子 xs:D×fH×fW (41×8×22)
xs = torch.linspace(0, ogfW - 1, fW, dtype=torch.float).view(1, 1, fW).expand(D, fH, fW)
# 在0到127上划分8个格子 ys:D×fH×fW (41×8×22)
ys = torch.linspace(0, ogfH - 1, fH, dtype=torch.float).view(1, fH, 1).expand(D, fH, fW)
# D x H x W x 3
# 堆积起来形成网格坐标,frustum[i,j,k,0]就是(i,j)的位置
# 深度为k的像素的宽度方向上的栅格坐标frustum:D×fH×fW×3
frustum = torch.stack((xs, ys, ds), -1)
return nn.Parameter(frustum, requires_grad=False)
2)视锥点向BEV空间投影(源码中的get_lidar_coor()函数,对应LSS源码中的get_geometry()函数)
def get_geometry(self, rots, trans, intrins, post_rots, post_trans):
"""
rots:相机外参旋转, trans:相机外参平移, intrins:相机内参, post_rots:数据增强旋转, post_trans:数据增强平移
Determine the (x,y,z) locations (in the ego frame) of the points in the point cloud.
Returns B x N x D x H/downsample x W/downsample x 3
"""
B, N, _ = trans.shape # B: batch size N:环视相机个数
# undo post-transformation
# B x N x D x H x W x 3
# 抵消数据增强及预处理对像素的变化,即还原数据增强中旋转和平移对坐标的影响
points = self.frustum - post_trans.view(B, N, 1, 1, 1, 3)
points = torch.inverse(post_rots).view(B, N, 1, 1, 1, 3, 3).matmul(points.unsqueeze(-1))
# 图像坐标系 -> 归一化相机坐标系 -> 相机坐标系 -> 车身坐标系
# 但是自认为由于转换过程是线性的,所以反归一化是在图像坐标系完成的,然后再利用
# 求完逆的内参投影回相机坐标系
points = torch.cat((points[:, :, :, :, :, :2] * points[:, :, :, :, :, 2:3],
points[:, :, :, :, :, 2:3]
), 5) # 反归一化
combine = rots.matmul(torch.inverse(intrins))
points = combine.view(B, N, 1, 1, 1, 3, 3).matmul(points).squeeze(-1)
points += trans.view(B, N, 1, 1, 1, 3)
# (bs, N, depth, H, W, 3):其物理含义
# 每个batch中的每个环视相机图像特征点,其在不同深度下位置对应
# 在ego坐标系下的坐标
return points #维度不变,坐标值从相机坐标系->世界坐标系
2.3 构建BEV空间特征
利用上述得到的投影后的3D坐标点以及预测出来的3D视锥特征,利用类似Voxel Pooling的方式得到BEV空间特征,BEV空间特征记作Tensor([bs, C=80, BEV_X=128, BEV_Y=128])。
注:此处与LSS的区别:
LSS中获取BEV feature,采用VoxelPooling操作,即:通过对每一个pillar中的feature进行 cumsum
实现的sum_pooling
,但是该操作的推理延迟与总体点数成正比,最后得到的feature越大,需要遍历的点就越多,LSS需要的时间也就越多。
BEVDet:引入了一个辅助索引来记录相同的体素索引之前出现过的次数
有了这个辅助索引和体素索引,将点分配到一个二维矩阵中,并通过单独的辅助轴的求和运算将同一体素内的特征组合起来。
在推理时相机内外参数固定的前提下,辅助索引和体素索引固定,可以在初始化阶段计算;
(如上加黑部分,没有理解,有理解的朋友可留言交流。)
值得注意的是,这种修改需要额外的内存,该内存由体素的数量和辅助索引的最大值决定。在实践中,我们将辅助指标的最大值限制为300,并删除剩余的点。该操作对模型精度的影响可以忽略不计。
三、BEV Encoder模块
该模块的作用是对View Transformer模块输出的BEV空间特征进行进一步的多尺度特征提取和融合,包括BEV主干网络和BEV特征融合网络,得到增强的BEV特征。
尽管该结构类似于Image-View Encoder模块,但它可以高精度地感知一些关键信息,比如大小、方向和速度,因为这些信息都是在BEV
空间中定义的。BEV
编码器的主干网络采用ResNet
,颈部网络采用FPN-LSS
。
BEV主干网络:
BEV主干网络是对输出的BEV特征提取多尺度特征,多尺度特征的分辨率如下:
B0 = Tensor([bs,160,64,64])
B1 = Tensor([bs,320,32,32])
B2 = Tensor([bs,640,16,16])
BEV特征融合网络:
BEV特征融合网络主要是对BEV主干网络输出的多尺度特征进行融合,特征融合网络输出的特征对应Tensor([bs, 256, 128, 128])。
四、3D Object Detection Head模块
对增强的BEV特征接3D检测头实现3D检测任务。
需要注意的是,BEVDet对不同的检测类别会设置有独立的检测头,每组独立的检测头都会有六个分支来预测物体的不同属性,同时在检测头前又添加一个
共享卷积层提取特征。具体信息如下:
BEVDet一共设置了六个互不共享的检测头,对应六个检测分支:
tasks=[
1)dict(num_class=1, class_names=['car']),
2)dict(num_class=2, class_names=['truck', 'construction_vehicle']),
3)dict(num_class=2, class_names=['bus', 'trailer']),
4)dict(num_class=1, class_names=['barrier']),
5)dict(num_class=2, class_names=['motorcycle', 'bicycle']),
6)dict(num_class=2, class_names=['pedestrian', 'traffic_cone']),
]
4.1 共享卷积层
共享卷积层的结构如下:
ConvModule(
(conv): Conv2d(256, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(activate): ReLU(inplace=True)
)
4.2 3D检测头
针对每个检测分支头,分别设置如下六个检测属性:
reg分支:用于预测BEV下物体相对于每个单元格左上角的xy偏移量;
height分支:用于预测物体的高度信息;
dim分支:用于预测物体的尺寸大小信息;
rot分支:用于预测物体偏航角的正、余弦值;
vel分支:用于预测物体沿xy方向的速度;
heatmap分支:用于预测不同物体的类别概率;
每个类别的检测头检测的内容都是一样的,如下:
1. 沿着x,y轴方向的偏移量(reg分支)
Sequential(
(0): ConvModule(
(conv): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(activate): ReLU(inplace=True)
)
(1): Conv2d(64, 2, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
)
2. z轴也就是预测物体的高度信息(height分支)
Sequential(
(0): ConvModule(
(conv): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(activate): ReLU(inplace=True)
)
(1): Conv2d(64, 1, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
)
3. 物体的尺寸大小信息(长-宽-高)(dim分支)
Sequential(
(0): ConvModule(
(conv): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(activate): ReLU(inplace=True)
)
(1): Conv2d(64, 3, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
)
4. 物体偏航角的正、余弦(一般车辆在行驶过程中不会涉及俯仰角和滚动角)(rot分支)
Sequential(
(0): ConvModule(
(conv): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(activate): ReLU(inplace=True)
)
(1): Conv2d(64, 2, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
)
5. 物体沿x,y轴方向的速度(vel分支)
Sequential(
(0): ConvModule(
(conv): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(activate): ReLU(inplace=True)
)
(1): Conv2d(64, 2, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
)
6. 分类置信度(heatmap分支)
Sequential(
(0): ConvModule(
(conv): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(activate): ReLU(inplace=True)
)
(1): Conv2d(64, 1, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
)
五、数据增强
具体参照如下链接,此处不作详细解释: