写点什么

BEVDet 算法详细解读 - 全网最全攻略

作者:十三Tech

​一、介绍​



BEVDet 算法是鉴智机器人21 年开源的 BEV 感知算法。该论文研究将 LSS 算法应用到 BEV 3D 目标检测中。提出四阶段的范式:图像编码器、视图转换器、BEV 编码器、3D 目标检测头。并针对 3D 实际场景,开先河的提出 BEV 数据增强Scale NMS。​


论文:https://arxiv.org/abs/2112.11790


官方代码:https://github.com/HuangJunJie2017/BEVDet


二、核心亮点

  • Framework 模块化的设计思想:和 BEV 语义分割框架类似,包括四个部分(Image-view Encoder、View Transformer、BEV Encoder、 Task-specific Head)

  • 改进策略 1:在 BEV 空间中进行额外的数据增强。

  • 改进策略 2: 改进 NMS,提高三维场景适应性。

三、 模块化结构设计


BEVDet 模块化设计: 由图像编码器、视图转换器、BEV编码器、 任务头四部分组成。

3.1. 图像编码器(Image-view Encoder)

作用:将图像编码为高维度特征。


方法:为了获得多分辨率特征,使用 backbone 提取高维特征,neck 对多分辨率特征进行融合。



多尺度特征的分辨率如下:


  • L0 = Tensor([bs * N,1024,H / 16,W / 16])

  • L1 = Tensor([bs * N,2048,H / 32,W / 32])


ResNet-50 提取多尺度图像特征图


# anaconda3/envs/bevdet/lib/python3.8/site-packages/mmdet/models/backbones/resnet.py    def forward(self, x):        """Forward function."""        if self.deep_stem:            x = self.stem(x)        else:            x = self.conv1(x)            x = self.norm1(x)            x = self.relu(x)        x = self.maxpool(x)        outs = []        for i, layer_name in enumerate(self.res_layers):            res_layer = getattr(self, layer_name)            x = res_layer(x)            if i in self.out_indices:                outs.append(x) #输出特征层layer3【6,1024,26,44】,layer4【6,2048,13,22】        return tuple(outs)
复制代码


neck 将多分辨率特征融合:


对主干网络提取的多尺度特征进行特征融合,融合后的特征为 Tensor([bs,N,512,H / 16,W / 16])。特征融合部分的数学描述如下:



其中 Up 代表上采样, Conv1×1 用于降维, Conv3×3 用于对融合后的特征图进一步提取特征。


# mmdet3d/models/detectors/bevdet.py  def image_encoder(self, img, stereo=False):  ...        # Neck层        if self.with_img_neck: #True            x = self.img_neck(x)            if type(x) in [list, tuple]:                x = x[0]        #_, 256, 16, 44        _, output_dim, ouput_H, output_W = x.shape         x = x.view(B, N, output_dim, ouput_H, output_W)
复制代码

3.2. 视图转换器(View Transformer)


作用:将图像特征映射到鸟瞰图坐标

3.2.1. 深度预测(Lift 操作)

对每个图像像素点,预测其深度分布概率。深度范围【1m, 60m】。


深度估计网络(DepthNet): 1x1 卷积;得到深度特征图和语义特征图,深度特征 softmax 得到深度概率分布。


# mmdet3d/models/necks/view_transformer.pydef forward():...    self.depth_net = nn.Conv2d(        in_channels, self.D + self.out_channels, kernel_size=1, padding=0)    #Conv2d(256, (D=59)+64, kernel_size=(1, 1), stride=(1, 1))        depth_digit = x[:, :self.D, ...]#前59层为深度图    tran_feat = x[:, self.D:self.D + self.out_channels, ...]#后64层为特征图    depth = depth_digit.softmax(dim=1) # 深度图softmax得到深度概率
复制代码
3.2.2. 视锥坐标点->车体的坐标转换

图像视锥生成见 LSS(略)




# mmdet3d/models/necks/view_transformer.py    def get_lidar_coor(self, sensor2ego, ego2global, cam2imgs, post_rots, post_trans,                       bda):        B, N, _, _ = sensor2ego.shape         #----------------------------------        #-1.图像坐标(增强后)->(减去平移)->(去除旋转)->矫正到图像增强前的坐标系)        # B x N x D x H x W x 3        #----------------------------------        points = self.frustum.to(sensor2ego) - 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)) #视锥去除图像增强的旋转         # --------------------------------        #-2.图像坐标系->透视逆变换*depth->相机坐标系        #---------------------------------        '''        points:(B, N, D, H, W, 3, 1)。视为 (..., 3, 1)        points[..., :2, :] :取前两个分量(u,v) ,points[..., 2:3, :] :取第三个分量(depth)        points[..., :2, :] * points[..., 2:3, :] : 将u,v与depth相乘,得到(u*depth, v*depth),得到相机坐标系下的坐标(立柱->视锥):Xc.Yc.Zc,Z轴沿着=光轴位置        torch.cat(..., 5) : 将Xc.Yc.Zc 拼接, 维度为(B, N, D, H, W, 3)        '''        points = torch.cat(            (points[..., :2, :] * points[..., 2:3, :], points[..., 2:3, :]), 5) #图像坐标系->相机坐标系        # --------------------------------        #-3. 相机坐标系->旋转平移->ego坐标系        #---------------------------------        combine = sensor2ego[:,:,:3,:3].matmul(torch.inverse(cam2imgs)) #  combine = R_{cam->ego} * 内参取逆K^-1,        points = combine.view(B, N, 1, 1, 1, 3, 3).matmul(points).squeeze(-1) # 相机坐标系->世界坐标系(旋转)        points += sensor2ego[:,:,:3, 3].view(B, N, 1, 1, 1, 3) # 加上平移        # --------------------------------        #-4.BEV 数据增强 车体坐标系->BDA(旋转+平移)        #---------------------------------        points = bda[:, :3, :3].view(B, 1, 1, 1, 1, 3, 3).matmul(            points.unsqueeze(-1)).squeeze(-1) # 在平面上旋转        points += bda[:, :3, 3].view(B, 1, 1, 1, 1, 3) # 加上平移        return points
复制代码
3.2.3. BEV 池化
(详细见 LSS)


# 调用CUDA内核,实现并行计算        bev_pool_v2_ext.bev_pool_v2_forward(            depth, #深度图            feat, #特征图            out, #输出            ranks_depth,  #排序后的点的原始空间索引            ranks_feat,   # ranks_feat: 特征回溯,排序后点的特征空间索引。核心作用: 至关重要! 在池化时,通过它可以找到该点对应的原始 2D 图像特征向量(来自哪个 Batch、哪个相机、哪个特征图位置 (h, w))。同一个 (b, n, h, w) 位置的所有深度采样点共享相同的 ranks_feat,方便后续操作。            ranks_bev, #排序后的体素唯一标识数组,相同值标识同一个体素            interval_lengths, #每个体素包含的点序列的长度            interval_starts,#排序后,每个体素包含的点序列的开始索引        )#每个CUDA线程处理一个体素         ctx.save_for_backward(ranks_bev, depth, feat, ranks_feat, ranks_depth)
复制代码

3.3. BEV 编码器(BEV Encoder)


 参数定义


# configs/bevdet/bevdet-r50.py    numC_Trans = 64    img_bev_encoder_backbone=dict(        type='CustomResNet',        numC_input=numC_Trans,        num_channels=[numC_Trans * 2, numC_Trans * 4, numC_Trans * 8]),    img_bev_encoder_neck=dict(        type='FPN_LSS',        in_channels=numC_Trans * 8 + numC_Trans * 2,        out_channels=256),
复制代码


BEV 编码器网络结构


#mmdet3d/models/detectors/bevdet.py  def bev_encoder(self, x):        x = self.img_bev_encoder_backbone(x)        x = self.img_bev_encoder_neck(x)        if type(x) in [list, tuple]:            x = x[0]        return x   提取多尺度特征:mmdet3d/models/backbones/resnet.py     def forward(self, x):        feats = []        x_tmp = x        for lid, layer in enumerate(self.layers):            if self.with_cp:                x_tmp = checkpoint.checkpoint(layer, x_tmp)            else:                x_tmp = layer(x_tmp)            if lid in self.backbone_output_ids:                feats.append(x_tmp)        return feats   多尺度特征融合:mmdet3d/models/necks/lss_fpn.py     def forward(self, feats):        x2, x1 = feats[self.input_feature_index[0]], #B0 = Tensor([bs,128,64,64])                 feats[self.input_feature_index[1]] #B1 = Tensor([bs,256,32,32])        if self.lateral:            x2 = self.lateral_conv(x2)        x1 = self.up(x1) #[([bs,256,32,32])] -> up-> x1=([bs,  512,  64,  64])        x = torch.cat([x2, x1], dim=1) #拼接: ([bs,  640,  64,  64])        if self.input_conv is not None:            x = self.input_conv(x)        x = self.conv(x) #通道变换:([bs, 512,64,64])        if self.extra_upsample:            x = self.up2(x) #上采样([bs, 256, 128,128])        return x
复制代码

3.4. 3D Detection Head

3.4.1 共享卷积层

一个标准的 3x3 卷积:


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))
复制代码
3.4.2 检测头
  • 作用:根据所执行任务设计输出头(3D 物体检测旨在检测行人、车辆、障碍物等可移动物体的位置、比例、方向和速度),代码如下:


1. reg分支:沿着x,y轴方向的偏移量[1,2,128,128]    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. height分支: z轴也就是预测物体的高度信息 [1,1,128,128]    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分支: 物体的尺寸大小信息(长-宽-高)[1,3,128,128]     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分支:物体偏航角的正、余弦(一般车辆在行驶过程中不会涉及俯仰角和滚动角 )[1,2,128,128]           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. vel分支: 物体沿x,y轴方向的速度 [1,2,128,128]          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分支:分类置信度 [1,10,128,128]          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)))
复制代码


通过对上述属性的预测,就可以实现最终的 3D 目标检测任务。

四、数据增强策略

4.1 图像增强

4.1.1 原理
4.1.2 数据增强对比图
4.1.3 代码详细解析
# configs/bevdet/bevdet-r50.py   data_config = {    'cams': [        'CAM_FRONT_LEFT', 'CAM_FRONT', 'CAM_FRONT_RIGHT', 'CAM_BACK_LEFT',        'CAM_BACK', 'CAM_BACK_RIGHT'    ],    'Ncams':6,    'input_size': (256, 704),    'src_size': (900, 1600),        'resize': (-0.06, 0.11),    'rot': (-5.4, 5.4),    'flip': True,    'crop_h': (0.0, 0.0),    'resize_test': 0.00,}  # mmdet3d/datasets/pipelines/loading.py(精华操作)#图像数据增强:(缩放、裁剪、翻转、旋转)def img_transform_core(self, img, resize_dims, crop, flip, rotate):        # adjust image        img = img.resize(resize_dims)         img = img.crop(crop)        if flip:            img = img.transpose(method=Image.FLIP_LEFT_RIGHT)        img = img.rotate(rotate)        return img     # 数据增强(缩放、裁剪、翻转、旋转)并更新对应的仿射变换矩阵# 该类支持两种变换方法def img_transform(self, img, post_rot, post_tran, resize, resize_dims,                      crop, flip, rotate):        '''        args: resize: 0.44,  resize_dims: (709, 399),              crop:(3, 143, 707, 399),  flip: 1,  rotate:-3.54        '''        # 不使用opencv的数据增强方法        if not self.opencv_pp:            img = self.img_transform_core(img, resize_dims, crop, flip, rotate)             #--------------------------------------------------        # 求变换对应的仿射矩阵:post—rot,post_tran        #--------------------------------------------------        #-1 缩放处理        post_rot *= resize  #【0.443,0】缩放变换累积到旋转矩阵        # -2 裁剪 (很有意思的取值范围,把1/3上半部分裁掉,去掉天空位置)        post_tran -= torch.Tensor(crop[:2])#【-3,143】#减去裁剪区域的左上角坐标(坐标系平移)        #-3 水平翻转处理        if flip:            A = torch.Tensor([[-1, 0], [0, 1]]) #翻转矩阵,实现x轴镜像            b = torch.Tensor([crop[2] - crop[0], 0])#平移补偿b:[crop宽度,0],保证翻转后位置正确            post_rot = A.matmul(post_rot) #更新旋转矩阵            post_tran = A.matmul(post_tran) + b #更新平移矩阵        #-4 旋转变换        A = self.get_rot(rotate / 180 * np.pi) #获取旋转矩阵 角度->弧度        b = torch.Tensor([crop[2] - crop[0], crop[3] - crop[1]]) / 2 #中心点        b = A.matmul(-b) + b #计算旋转补偿 先把中心点移到坐标原点->绕原点旋转->移回中心点位置        post_rot = A.matmul(post_rot)  #更新旋转矩阵        post_tran = A.matmul(post_tran) + b  #更新平移向量        if self.opencv_pp:            img = self.img_transform_core_opencv(img, post_rot, post_tran, crop)        return img, post_rot, post_tran
复制代码

4.2 BEV 空间增强

4.2.1 问题与策略(仅 train 用)

因训练时,BEV 空间特征学习容易过拟合,而图像空间的数据增强对 BEV 编码器无效。所以,设计了基于 BEV 空间的数据增强方法。这在之后的 BEV 算法中一直沿用。


  • 在鸟瞰图空间进行随机旋转、缩放、平移和翻转(水平/垂直)。

  • 同时保持 3D 检测框(gt_box)、点云(points)和图像数据的空间一致性。


4.2.2 BEV 增强前后视锥对比图

在 LSS 算法中(见上 3.2.2),对 ego 坐标系下视锥应用_bda_数据增强,并对应的改变 gt_box,实现空间一致性。



4.2.3 实现代码

参数设置


# configs/bevdet/bevdet-r50.pybda_aug_conf = dict(    rot_lim=(-22.5, 22.5), #旋转区间    scale_lim=(0.95, 1.05), #缩放区间    flip_dx_ratio=0.5,   #翻转x轴概率    flip_dy_ratio=0.5)   #翻转y轴概率
复制代码


3D 边界框数据


# mmdet3d/datasets/pipelines/loading.pyclass BEVAug(object):...    #--------------------------------    # 对3D框进行几何变换    #--------------------------------    def bev_transform(self, gt_boxes, rotate_angle, scale_ratio, flip_dx,                      flip_dy, tran_bda):        '''        gt_boxes: 形状为(N, 9)的张量, 表示N个3D边界框,                  每行包含9个属性:[x, y, z, l, w, h, yaw, vx, vy]        rotate_angle: 旋转角度        scale_ratio: 缩放比例        flip_dx: 是否沿x轴翻转(bool)        flip_dy: 是否沿y轴翻转(bool)        tran_dba: 平移向量[dx, dy, dz]        return : 变换后的gt_boxes, 变换矩阵rot_mat        '''        #-1. 旋转矩阵(绕Z轴的旋转矩阵)        rotate_angle = torch.tensor(rotate_angle / 180 * np.pi) # 度转弧度        rot_sin = torch.sin(rotate_angle)        rot_cos = torch.cos(rotate_angle)        rot_mat = torch.Tensor([[rot_cos, -rot_sin, 0], [rot_sin, rot_cos, 0],                                [0, 0, 1]]) #z坐标不随旋转改变        #-2. 缩放矩阵        scale_mat = torch.Tensor([[scale_ratio, 0, 0], [0, scale_ratio, 0],                                  [0, 0, scale_ratio]])        #-3. 翻转矩阵        flip_mat = torch.Tensor([[1, 0, 0], [0, 1, 0], [0, 0, 1]])        if flip_dx: #X轴翻转->X坐标取反            flip_mat = flip_mat @ torch.Tensor([[-1, 0, 0], [0, 1, 0],                                                [0, 0, 1]])        if flip_dy: #Y轴翻转->Y坐标取反            flip_mat = flip_mat @ torch.Tensor([[1, 0, 0], [0, -1, 0],                                                [0, 0, 1]])        #-4. 组合基础变换矩阵        rot_mat = flip_mat @ (scale_mat @ rot_mat)        #-------------------------------        #-5. 应用变换到3D框(核心)        #-------------------------------        if gt_boxes.shape[0] > 0: #非空边框            # 1.中心点变换            gt_boxes[:, :3] = (                rot_mat @ gt_boxes[:, :3].unsqueeze(-1)).squeeze(-1)            # 2.尺寸缩放 (w, l, h)->(w, l, w)*scale_ratio            gt_boxes[:, 3:6] *= scale_ratio            # 3.偏航角更新             gt_boxes[:, 6] += rotate_angle            if flip_dx: #X轴翻转:yaw = π - yaw                gt_boxes[:, 6] = 2 * torch.asin(torch.tensor(1.0)) - gt_boxes[:, 6]            if flip_dy: #Y轴翻转:yaw = -yaw                gt_boxes[:, 6] = -gt_boxes[:, 6]            # 4.速度向量变换 (仅需x,y平面的旋转、缩放、翻转)            gt_boxes[:, 7:] = (                rot_mat[:2, :2] @ gt_boxes[:, 7:].unsqueeze(-1)).squeeze(-1)            # 5.平移变换            gt_boxes[:, :3] = gt_boxes[:, :3] + tran_bda        return gt_boxes, rot_mat
复制代码


空间一致性:点云增强,图像增加 bda_mat


# mmdet3d/datasets/pipelines/loading.pyclass BEVAug(object):...        #--------------------        #-4. *^-^点云变换 ^-^*(对x,y,z,坐标旋转平移)        #--------------------        if 'points' in results:            points = results['points'].tensor            points_aug = (bda_rot @ points[:, :3].unsqueeze(-1)).squeeze(-1)#坐标旋转            points[:,:3] = points_aug + tran_bda #坐标平移            points = results['points'].new_point(points) #更新点云坐标            results['points'] = points        #---------------        #-5. 构建仿射变换矩阵        # [ R | T ]        # [ 0 | 1 ]         #---------------            bda_mat[:3, :3] = bda_rot        bda_mat[:3, 3] = torch.from_numpy(tran_bda)        if len(gt_boxes) == 0: # 无边界框时,返回空张量            gt_boxes = torch.zeros(0, 9)        results['gt_bboxes_3d'] =             LiDARInstance3DBoxes(gt_boxes, box_dim=gt_boxes.shape[-1],                                 origin=(0.5, 0.5, 0.5))            #使用LiDARInstance3DBoxes类封装边界框,并设置锚点(0.5, 0.5, 0.5)        if 'img_inputs' in results:            imgs, rots, trans, intrins = results['img_inputs'][:4]            post_rots, post_trans = results['img_inputs'][4:]            results['img_inputs'] = (imgs, rots, trans, intrins, post_rots,                                     post_trans, bda_mat) #添加bda_mat        #-----------------------        # -6. *^-^体素数据增强^-^*#未执行        #-----------------------        if 'voxel_semantics' in results:             if flip_dx: # X轴翻转->翻转对应的分割图,【::-1,...】X轴翻转                results['voxel_semantics'] = results['voxel_semantics'][::-1,...].copy()                results['mask_lidar'] = results['mask_lidar'][::-1,...].copy()                results['mask_camera'] = results['mask_camera'][::-1,...].copy()            if flip_dy: #【:,::-1,...】Y轴翻转                results['voxel_semantics'] = results['voxel_semantics'][:,::-1,...].copy()                results['mask_lidar'] = results['mask_lidar'][:,::-1,...].copy()                results['mask_camera'] = results['mask_camera'][:,::-1,...].copy()        return results
复制代码

五、Scale-NMS

5.1 问题与策略

BEV空间中,不同类别的物体占用的面积本质上是不同的,因此预测结果之间的IoU分布会因类别而异。如果对象占用的区域都很小,可能会使预测结果与真正的结果之间没有交集,这将使依赖IoU的经典NMS算法失效。


作者提出了Scale-NMSScale-NMS在执行经典NMS算法之前,会根据每个对象的类别对其大小进行缩放缩放的比例因子是特定于类别的,它们是通过在验证集上的超参数搜索生成的。通过这种方式,真阳性和冗余结果之间的IOU分布会被调整以匹配经典NMS算法。


如下图所示,在预测小目标的时候,Scale-NMS通过放大对象大小来构建结果之间的空间关系,这使得经典的NMS算法能够根据IOU指标丢弃冗余的预测结果。在实践中,作者将Scale-NMS应用于除barrier以外的所有类别,因为该类别包含的对象的大小各不相同。


5.2 实现代码

#configs/bevdet/bevdet-r50.py        # Scale-NMS    nms_type=['rotate'],    nms_thr=[0.2],    nms_rescale_factor=[[1.0, 0.7, 0.7, 0.4, 0.55,                         1.1, 1.0, 1.0, 1.5, 3.5]] #定义不同类的缩放因子
复制代码


#mmdet3d/models/dense_heads/centerpoint_head.pymmdet3d/models/dense_heads/centerpoint_head.py    predictions_dicts = []...       if isinstance(factor, list):           for cid in range(len(factor)):               box_preds[cls_labels == cid, 3:6] =                     box_preds[cls_labels == cid, 3:6] * factor[cid] #box,w,h,l *类别缩放因子        else:            box_preds[:, 3:6] = box_preds[:, 3:6] * factor             # Apply NMS in birdeye view....        if isinstance(factor, list):             for cid in range(len(factor)):                 box_preds[top_labels == cid, 3:6] =                      box_preds[top_labels == cid, 3:6] / factor[cid] # 除以缩放因子,去除影响       else:              box_preds[:, 3:6] = box_preds[:, 3:6] / factor
复制代码


用户头像

十三Tech

关注

还未添加个人签名 2019-04-24 加入

公众号:十三Tech

评论

发布
暂无评论
BEVDet 算法详细解读-全网最全攻略_十三Tech_InfoQ写作社区