文章目录
BEVFormer 是什么
BEVFormer 是将Transformer架构的自注意机制与BEV视图中3D检测结合起来的一种纯视觉目标检测方案。
将Transformer架构的自注意力机制应用到目标检测方案有 DETR,deformable DETR,其中deformable DETR 首次将可变形稀疏局部自注意力代替Transform原版的全局自注意机制,使得训练推理计算更加高效。
基于BEV视图中3D检测方案起初均是为点云目标检测服务,流行的方法有PointNet++,PointPillar,centerPoint,大致的思路就是将3D点云拍扁的2D的BEV栅格上,然后类似图像的特征上进行分类和回归。
对于纯视觉的BEV检测方案,其中的重中之重就是如何将2D的图像特征映射到3D空间的BEV栅格,既然是映射关系,那就有前行投影和反向查询两种机制。前向投影是基于深度估计的方法,参考基于深度估计的BEV视图转换方法,典型代表为LSS。反向查询方法思路为先将BEV栅格在Z方向上进行lift提升,然后再映射到图像特征图上进行特征查询。BEVFormer 就是基于这种机制进行2D图像特征到3D空间的BEV栅格映射。
至于为什么要用BEV,什么是自注意力,什么是deformable attention,为什么要用纯视觉方案等等问题,这里不做详细解释,但是在看BEVformer之前,这些问题都是必现先搞明白的,特别是multiscale deformable attention ,因为整个BEVFormer最重要的encoder部分说白了就是在不停地做 deformable attention。

BEVFormer 流程
BEVFormer 的 Pipeline 分成几个部分:
1、Backbone: 主干网络提取图像多尺度特征。Backbone + FPN Neck 提取多个视角相机的多尺度特征。常用的有resnet50, resNet101-DCN等。
2、encoder: 完成2D图像多尺度特征映射到3D空间的BEV栅格。(包括 Temporal Self-Attention 模块和 Spatial Cross-Attention 模块。
3、Decoder: 在BEV栅格上完成 3D 目标检测的分类和定位任务, 类似Deformable DETR 的 Decoder
4、正负样本定义: 采用 Transformer 中常用的匈牙利匹配算法,Focal Loss + L1 Loss 的总损失和最小
5、损失函数: Focal Loss 分类损失 + L1 Loss 回归损失
BEVFormer 输入
输入的数据是一个 6 维的张量:(bs,queue,cam,C,H,W);
bs: batchsize
queue:连续帧的个数;相当加入时序信息,起到多帧特征融合效果,对遮挡起到很好效果。所以加入网络的数据除当前帧之外,还有之前几帧的数据。
cam: 相机的个数,即每一帧包含图像的数量。例如nuScenes数据集而言,由于一辆车带有六个环视相机传感器,可以实现 360° 全场景的覆盖。cam = 6
C,H,W:图像通道数,图像高度,图像宽度
backbone 特征提取
提取某一帧对应的所有环视图像的多尺度特征。
对应代码如下,取出对应历史帧图像,将 bs * cam 作为新的bs送到backbone网络提取多尺度特征,返回后,再把 bs * cam 的维度再拆成 bs,cam。即可得到某一queue的特征。
# 输入图片信息 tensor: (bs, queue, cam, c, h, w)
# 通过 for loop 方式一次获取单帧对应的六张环视图像
# 送入到 Backbone + Neck 网络提取多尺度的图像特征
for idx in range(tensor.size(1) - 1): # 利用除当前帧之外的所有帧迭代计算 `prev_bev` 特征
single_frame = tensor[:, idx, :] # (bs, cam, c, h, w)
# 将 bs * cam 看作是 batch size,将原张量 reshape 成 4 维的张量
# 待 Backbone + Neck 网络提取多尺度特征后,再把 bs * cam 的维度再拆成 bs,cam
single_frame = single_frame.reshape(bs * cam, c, h, w)
feats = Backbone(FPN(single_frame))
""" feats 是一个多尺度的特征列表 """
[0]: (bs, cam, 256, h / 8, w / 8)
[1]: (bs, cam, 256, h / 16, w / 16)
[2]: (bs, cam, 256, h / 32, w / 32)
[3]: (bs, cam, 256, h / 64, w / 64)
encoder 核心

包含两个子模块 Temporal Self-Attention模块 以及 Spatial Cross-Attention模块;
Temporal Self-Attention 模块
主要做时序特征融合。
BEV Query 是什么?
可以理解 BEV Query 就是当前BEV特征。首先它是预定义的一组栅格(grid-shaped)可学习参数, Q ∈ R H × W × C Q \in R^{H \times W \times C} Q∈RH×W×C,它是对BEV感知空间的特征描述,当它完成学习以后,就变成了BEV 特征。
在Temporal Self-Attention模块,我们把历史BEV特征作为输入,与当前时刻的 BEV Query 进行融合,输出当前的 deformable attention 的Query特征。
对于一个deformable attention 模块,它处理数据的pipline 大致如下:

Deformable Attention Module CUDA 模块的输入分别是采样位置(Sample Location)、注意力权重(Attention Weights)、映射后的 Value 特征、多尺度特征每层特征起始索引位置、多尺度特征图的空间大小(便于将采样位置由归一化的值变成绝对位置);
对于 Temporal Self-Attention 模块而言,需要 bev_query、bev_pos、prev_bev、ref_point、value等参数(需要用到的参数参考 Deformable Attention Pipeline 图解)
代码实现:
bev_query:
一个完全 learnable parameter,通过 nn.Embedding() 函数得到,形状 shape = (200 * 200,256);200,200 分别代表 BEV 特征平面的长和宽。
bev_pose:
一个完全 learnable parameter。先分别产生行、列编码,再通过 cat 形成二维位置编码,相比直接生成二维位置编码节省了大量参数。
""" bev_pose 的生成过程 """
# w, h 分别代表 bev 特征的空间尺寸 200 * 200
x = torch.arange(w, device=mask.device)
y = torch.arange(h, device=mask.device)
# self.col_embed 和 self.row_embed 分别是两个 Linear 层,将(200, )的坐标向高维空间做映射
x_embed = self.col_embed(x) # (200, 128)
y_embed = self.row_embed(y) # (200, 128)
# pos shape: (bs, 256, 200, 200)
pos = torch.cat((x_embed.unsqueeze(0).repeat(h, 1, 1), y_embed.unsqueeze(1).repeat(1, w, 1)), dim=-1).permute(2, 0, 1).unsqueeze(0).repeat(mask.shape[0], 1, 1, 1)
prev_bev
历史时刻BEV特征
ref_point
ref_point 是参考点,也就是说当前BEV特征需要参考前一时刻的 BEV 特征的位置。
如果没有输入 prev_bev(第一帧没有前一时刻的 BEV 特征)的情况,其 ref_point = ref_2d;
对于存在输入 prev_bev 的情况,其 ref_point = ref_2d + shift;
ref_2d 是在相对BEV网格下的归一化坐标位置,即对于每一个BEV网格整型索引(i,j)都对应一个归一化的二维坐标位置。
shift 是历史帧BEV 相对当前帧 BEV的偏移
由于车身不断运动,而BEV是相对自车建立起来的坐标系,所以前后 BEV 在空间上的特征是不对齐的。为了实现两个时刻特征的空间对齐,需要用到 can_bus 数据中有关车自身旋转角度和偏移的信息。根据车辆的CAN_bus数据,计算从上一帧到当前帧的平移距离(translation_length)、平移方向(translation_angle)和旋转角度(bev_angle)。
对于相对车也在运动的物体的特征,这部分对齐需要自注意力模块去学习和修正。
"""shift 参数的生成"""
# obtain rotation angle and shift with ego motion
delta_x = kwargs['img_metas'][0]['can_bus'][0]
delta_y = kwargs['img_metas'][0]['can_bus'][1]
ego_angle = kwargs['img_metas'][0]['can_bus'][-2] / np.pi * 180
rotation_angle = kwargs['img_metas'][0]['can_bus'][-1]
grid_length_y = grid_length[0]
grid_length_x = grid_length[1]
translation_length = np.sqrt(delta_x ** 2 + delta_y ** 2)
translation_angle = np.arctan2(delta_y, delta_x) / np.pi * 180
if translation_angle < 0:
translation_angle += 360
bev_angle = ego_angle - translation_angle
shift_y = translation_length * \
np.cos(bev_angle / 180 * np.pi) / grid_length_y / bev_h
shift_x = translation_length * \
np.sin(bev_angle / 180 * np.pi) / grid_length_x / bev_w
shift_y = shift_y * self.use_shift
shift_x = shift_x * self.use_shift
shift = bev_queries.new_tensor([shift_x, shift_y]) # shape (2,)
# 通过`旋转`和`平移`变换实现 BEV 特征的对齐,对于平移部分是通过对参考点加上偏移量`shift`体现的
if prev_bev is not None:
if prev_bev.shape[1] == bev_h * bev_w:
prev_bev = prev_bev.permute(1, 0, 2)
if self.rotate_prev_bev:
num_prev_bev = prev_bev.size(1)
prev_bev = prev_bev.reshape(bev_h, bev_w, -1).permute(2, 0, 1) # sequence -> grid
prev_bev = rotate(prev_bev, rotation_angle, center=self.rotate_center)
prev_bev = prev_bev.permute(1, 2, 0).reshape(bev_h * bev_w, num_prev_bev, -1)
"""ref_2d 参数的生成,常规的 2D 网格生成的规则坐标点"""
ref_y, ref_x = torch.meshgrid(torch.linspace(0.5, H - 0.5, H, dtype=dtype, device=device


5988

被折叠的 条评论
为什么被折叠?



