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

363 阅读14分钟

 

一、介绍​

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

论文:arxiv.org/abs/2112.11…

官方代码:github.com/HuangJunJie…

二、核心亮点

  • 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.py
def 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.py
bda_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.py
class 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.py
class 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