PV-RCNN的推理实现和LOSS计算

Posted NNNNNathan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了PV-RCNN的推理实现和LOSS计算相关的知识,希望对你有一定的参考价值。

        前面已经完成了PV-RCNN网络的搭建和第一、第二阶段中关键点分割的label匹配,anchor与GT匹配、proposal与GT的target assignment,想了解的小伙伴可以我之前的博文

第一阶段:

PV-RCNN论文和逐代码解析(一)_NNNNNathan的博客-CSDN博客1、前言当前的点云3D检测主要分为两大类,第一类为grid-based的方法,第二类为point-based的方法。grid-based的方法将不规则的点云数据转换成规则的3D voxels (VoxelNet, SECOND , Fast PointRCNN, Part A^2 Net)或者转化成 2D的BEV特征图(PIXOR, HDNet,PointPillars),这种方法可以将不规则的数据转换后使用3D或者2D的CNN来高效的进行特征提取。...https://blog.csdn.net/qq_41366026/article/details/123349889?spm=1001.2014.3001.5501第二阶段:

PV-RCNN论文和逐代码解析(二)_NNNNNathan的博客-CSDN博客第一阶段:1、MeanVFE (voxel特征编码)2、VoxelBackBone8x(3D CNN 提取voxel特征)3、HeightCompression(高度方向Z轴堆叠)5、BaseBEVBackbone(SECOND中的RPN层)6、AnchorHeadSingle(anchor分类和回归头)4、VoxelSetAbstraction(VSA模块,对不同voxel特征层完成SA)第二阶段:7、PointHeadSimple Predicted Keypoint..https://blog.csdn.net/qq_41366026/article/details/123463717?spm=1001.2014.3001.5501这篇文章将根据前面生成的target assignment结果进行loss计算网络的推理以及PV-RCNN中的消融实验

1、Loss计算

        PV-RCNN是端到端训练的,一共需要计算三部分的损失:

1、anchor头的分类和回归损失 

2、Predicted Keypoint Weighting (PKW)分割损失

3、Refinement网络损失

总体的训练损失为三者相加,且三者权重相等

注:Loss计算的内容均在之前的博客中详细介绍过,这里再简单叙述一下;如果了解SECOND和PointRCNN的LOSS计算,不需要再看Loss计算部分。

1、anchor头的分类和回归损失

第一阶段建议框生成的损失如下:

        其中Lcls为每个acnhor的分类损失,采用focal loss进行计算(alpha和gamma与Retina一样,分别为0.25, 2),并采用SmoothL1函数优化anhcor box residual regression。

1.定位任务的回归残差定义如下:


                

        其中x^gt代表了标注框的x长度 ;x^a代表了先验框的长度信息,d^a表示先验框长度和宽度的对角线距离,定义为:

2.类别分类任务 


        对于每个先验框的物体类别分类,PV-RCNN使用了focal loss,来完成调节正负样本均衡,和难样本挖掘。公式定义如下:

        其中,aplha参数和gamma参数都和RetinaNet中的设置一样,分别为0.25和2。 

3.先验框方向分类


         由于在角度回归的时候,不可以完全区分两个两个方向完全相反的预测框,所以在实现的时候,作者加入了对先验框的方向分类,使用softmax函数预测方向的类别。

代码在:pcdet/models/dense_heads/anchor_head_template.py

    def get_cls_layer_loss(self):
        # (batch_size, 248, 216, 18) 网络类别预测
        cls_preds = self.forward_ret_dict['cls_preds']
        # (batch_size, 321408) 前景anchor类别
        box_cls_labels = self.forward_ret_dict['box_cls_labels']
        batch_size = int(cls_preds.shape[0])
        # [batch_szie, num_anchors]--> (batch_size, 321408)
        # 关心的anchor  选取出前景背景anchor, 在0.45到0.6之间的设置为仍然是-1,不参与loss计算
        cared = box_cls_labels >= 0
        # (batch_size, 321408) 前景anchor
        positives = box_cls_labels > 0
        # (batch_size, 321408) 背景anchor
        negatives = box_cls_labels == 0
        # 背景anchor赋予权重
        negative_cls_weights = negatives * 1.0
        # 将每个anchor分类的损失权重都设置为1
        cls_weights = (negative_cls_weights + 1.0 * positives).float()
        # 每个正样本anchor的回归损失权重,设置为1
        reg_weights = positives.float()
        # 如果只有一类
        if self.num_class == 1:
            # class agnostic
            box_cls_labels[positives] = 1

        # 正则化并计算权重     求出每个数据中有多少个正例,即shape=(batch, 1)
        pos_normalizer = positives.sum(1, keepdim=True).float()  # (4,1) 所有正例的和 eg:[[162.],[166.],[155.],[108.]]
        # 正则化回归损失-->(batch_size, 321408),最小值为1,根据论文中所述,用正样本数量来正则化回归损失
        reg_weights /= torch.clamp(pos_normalizer, min=1.0)
        # 正则化分类损失-->(batch_size, 321408),根据论文中所述,用正样本数量来正则化分类损失
        cls_weights /= torch.clamp(pos_normalizer, min=1.0)
        # care包含了背景和前景的anchor,但是这里只需要得到前景部分的类别即可不关注-1和0
        # cared.type_as(box_cls_labels) 将cared中为False的那部分不需要计算loss的anchor变成了0
        # 对应位置相乘后,所有背景和iou介于match_threshold和unmatch_threshold之间的anchor都设置为0
        cls_targets = box_cls_labels * cared.type_as(box_cls_labels)
        # 在最后一个维度扩展一次
        cls_targets = cls_targets.unsqueeze(dim=-1)

        cls_targets = cls_targets.squeeze(dim=-1)
        one_hot_targets = torch.zeros(
            *list(cls_targets.shape), self.num_class + 1, dtype=cls_preds.dtype, device=cls_targets.device
        )  # (batch_size, 321408, 4),这里的类别数+1是考虑背景

        # target.scatter(dim, index, src)
        # scatter_函数的一个典型应用就是在分类问题中,
        # 将目标标签转换为one-hot编码形式 https://blog.csdn.net/guofei_fly/article/details/104308528
        # 这里表示在最后一个维度,将cls_targets.unsqueeze(dim=-1)所索引的位置设置为1

        """
            dim=1: 表示按照列进行填充
            index=batch_data.label:表示把batch_data.label里面的元素值作为下标,
            去下标对应位置(这里的"对应位置"解释为列,如果dim=0,那就解释为行)进行填充
            src=1:表示填充的元素值为1
        """
        # (batch_size, 321408, 4)
        one_hot_targets.scatter_(-1, cls_targets.unsqueeze(dim=-1).long(), 1.0)
        # (batch_size, 248, 216, 18) --> (batch_size, 321408, 3)
        cls_preds = cls_preds.view(batch_size, -1, self.num_class)
        # (batch_size, 321408, 3) 不计算背景分类损失
        one_hot_targets = one_hot_targets[..., 1:]

        # 计算分类损失 # [N, M] # (batch_size, 321408, 3)
        cls_loss_src = self.cls_loss_func(cls_preds, one_hot_targets, weights=cls_weights)
        # 求和并除以batch数目
        cls_loss = cls_loss_src.sum() / batch_size
        # loss乘以分类权重 --> cls_weight=1.0
        cls_loss = cls_loss * self.model_cfg.LOSS_CONFIG.LOSS_WEIGHTS['cls_weight']
        tb_dict = 
            'rpn_loss_cls': cls_loss.item()
        
        return cls_loss, tb_dict

    @staticmethod
    def add_sin_difference(boxes1, boxes2, dim=6):
        # 针对角度添加sin损失,有效防止-pi和pi方向相反时损失过大
        assert dim != -1  # 角度=180°×弧度÷π   弧度=角度×π÷180°
        # (batch_size, 321408, 1)  torch.sin() - torch.cos() 的 input (Tensor) 都是弧度制数据,不是角度制数据。
        rad_pred_encoding = torch.sin(boxes1[..., dim:dim + 1]) * torch.cos(boxes2[..., dim:dim + 1])
        # (batch_size, 321408, 1)
        rad_tg_encoding = torch.cos(boxes1[..., dim:dim + 1]) * torch.sin(boxes2[..., dim:dim + 1])
        # (batch_size, 321408, 7) 将编码后的结果放回
        boxes1 = torch.cat([boxes1[..., :dim], rad_pred_encoding, boxes1[..., dim + 1:]], dim=-1)
        # (batch_size, 321408, 7) 将编码后的结果放回
        boxes2 = torch.cat([boxes2[..., :dim], rad_tg_encoding, boxes2[..., dim + 1:]], dim=-1)
        return boxes1, boxes2

    @staticmethod
    def get_direction_target(anchors, reg_targets, one_hot=True, dir_offset=0, num_bins=2):
        batch_size = reg_targets.shape[0]
        # (batch_size, 321408, 7)
        anchors = anchors.view(batch_size, -1, anchors.shape[-1])
        # (batch_size, 321408)在-pi到pi之间
        # 由于reg_targets[..., 6]是经过编码的旋转角度,如果要回到原始角度需要重新加回anchor的角度就可以
        rot_gt = reg_targets[..., 6] + anchors[..., 6]
        """
        offset_rot shape : (batch_size, 321408)
        rot_gt - dir_offset  由于在openpcdet中x向前,y向左,z向上,
        
        减去dir_offset(45度)的原因可以参考这个issue:
        https://github.com/open-mmlab/OpenPCDet/issues/818
        说的呢就是因为大部分目标都集中在0度和180度,270度和90度,
        这样就会导致网络在一些物体的预测上面不停的摇摆。所以为了解决这个问题,
        将方向分类的角度判断减去45度再进行判断,
        这里减掉45度之后,在预测推理的时候,同样预测的角度解码之后
        也要减去45度再进行之后测nms等操作
        
        common_utils.limit_period:
        将角度限制在0到2*pi之间 原数据的角度在-pi到pi之间
        """
        offset_rot = common_utils.limit_period(rot_gt - dir_offset, 0, 2 * np.pi)
        # (batch_size, 321408) 取值为0和1,num_bins=2
        dir_cls_targets = torch.floor(offset_rot / (2 * np.pi / num_bins)).long()
        # (batch_size, 321408)
        dir_cls_targets = torch.clamp(dir_cls_targets, min=0, max=num_bins - 1)

        if one_hot:
            # (batch_size, 321408, 2)
            dir_targets = torch.zeros(*list(dir_cls_targets.shape), num_bins, dtype=anchors.dtype,
                                      device=dir_cls_targets.device)
            # one-hot编码,只存在两个方向:正向和反向 (batch_size, 321408, 2)
            dir_targets.scatter_(-1, dir_cls_targets.unsqueeze(dim=-1).long(), 1.0)
            dir_cls_targets = dir_targets
        return dir_cls_targets

    def get_box_reg_layer_loss(self):
        # (batch_size, 248, 216, 42) anchor_box的7个回归参数
        box_preds = self.forward_ret_dict['box_preds']
        # (batch_size, 248, 216, 12) anchor_box的方向预测
        box_dir_cls_preds = self.forward_ret_dict.get('dir_cls_preds', None)
        # (batch_size, 321408, 7) 每个anchor和GT编码的结果
        box_reg_targets = self.forward_ret_dict['box_reg_targets']
        # (batch_size, 321408) 得到每个box的类别
        box_cls_labels = self.forward_ret_dict['box_cls_labels']
        batch_size = int(box_preds.shape[0])
        # 获取所有anchor中属于前景anchor的mask  shape : (batch_size, 321408)
        positives = box_cls_labels > 0
        # 设置回归参数为1.    [True, False] * 1. = [1., 0.]
        reg_weights = positives.float()  # (4, 211200) 只保留标签>0的值
        # 同cls处理
        pos_normalizer = positives.sum(1,
                                       keepdim=True).float()  # (batch_size, 1) 所有正例的和 eg:[[162.],[166.],[155.],[108.]]

        reg_weights /= torch.clamp(pos_normalizer, min=1.0)  # (batch_size, 321408)

        if isinstance(self.anchors, list):
            if self.use_multihead:
                anchors = torch.cat(
                    [anchor.permute(3, 4, 0, 1, 2, 5).contiguous().view(-1, anchor.shape[-1]) for anchor in
                     self.anchors], dim=0)
            else:
                anchors = torch.cat(self.anchors, dim=-3)  # (1, 248, 216, 3, 2, 7)
        else:
            anchors = self.anchors
        # (1, 248*216, 7) --> (batch_size, 248*216, 7)
        anchors = anchors.view(1, -1, anchors.shape[-1]).repeat(batch_size, 1, 1)
        # (batch_size, 248*216, 7)
        box_preds = box_preds.view(batch_size, -1,
                                   box_preds.shape[-1] // self.num_anchors_per_location if not self.use_multihead else
                                   box_preds.shape[-1])
        # sin(a - b) = sinacosb-cosasinb
        # (batch_size, 321408, 7)    分别得到sinacosb和cosasinb
        box_preds_sin, reg_targets_sin = self.add_sin_difference(box_preds, box_reg_targets)
        loc_loss_src = self.reg_loss_func(box_preds_sin, reg_targets_sin, weights=reg_weights)
        loc_loss = loc_loss_src.sum() / batch_size

        loc_loss = loc_loss * self.model_cfg.LOSS_CONFIG.LOSS_WEIGHTS['loc_weight']  # loc_weight = 2.0 损失乘以回归权重
        box_loss = loc_loss
        tb_dict = 
            # pytorch中的item()方法,返回张量中的元素值,与python中针对dict的item方法不同
            'rpn_loss_loc': loc_loss.item()
        

        # 如果存在方向预测,则添加方向损失
        if box_dir_cls_preds is not None:
            # (batch_size, 321408, 2) 此处生成每个anchor的方向分类
            dir_targets = self.get_direction_target(
                anchors, box_reg_targets,
                dir_offset=self.model_cfg.DIR_OFFSET,  # 方向偏移量 0.78539 = π/4
                num_bins=self.model_cfg.NUM_DIR_BINS  # BINS的方向数 = 2
            )
            # 方向预测值 (batch_size, 321408, 2)
            dir_logits = box_dir_cls_preds.view(batch_size, -1, self.model_cfg.NUM_DIR_BINS)
            # 只要正样本的方向预测值 (batch_size, 321408)
            weights = positives.type_as(dir_logits)
            # (4, 211200) 除正例数量,使得每个样本的损失与样本中目标的数量无关
            weights /= torch.clamp(weights.sum(-1, keepdim=True), min=1.0)
            # 方向损失计算
            dir_loss = self.dir_loss_func(dir_logits, dir_targets, weights=weights)
            dir_loss = dir_loss.sum() / batch_size
            # 损失权重,dir_weight: 0.2
            dir_loss = dir_loss * self.model_cfg.LOSS_CONFIG.LOSS_WEIGHTS['dir_weight']
            # 将方向损失加入box损失
            box_loss += dir_loss

            tb_dict['rpn_loss_dir'] = dir_loss.item()
        return box_loss, tb_dict

    def get_loss(self):
        # 计算classfiction layer的loss,tb_dict内容和cls_loss相同,形式不同,一个是torch.tensor一个是字典值
        cls_loss, tb_dict = self.get_cls_layer_loss()
        # 计算regression layer的loss
        box_loss, tb_dict_box = self.get_box_reg_layer_loss()
        # 在tb_dict中添加tb_dict_box,在python的字典中添加值,
        # 如果添加的也是字典,用update方法,如果是键值对则采用赋值的方式
        tb_dict.update(tb_dict_box)
        # rpn_loss是分类和回归的总损失
        rpn_loss = cls_loss + box_loss

        # 在tb_dict中添加rpn_loss,此时tb_dict中包含cls_loss,reg_loss和rpn_loss
        tb_dict['rpn_loss'] = rpn_loss.item()
        return rpn_loss, tb_dict

2、Predicted Keypoint Weighting (PKW)分割损失

         由于在一帧点云的关键点中属于前背景点的数量差异较大,作者在此处使用了Focal Loss:

        

 其中alpha和gamma都与RetinaNet中保持一致,分别为0.25、2。

注:在计算前背景点的分类loss时,对每个GT enlarge 0.2米后才包括的点,类别置为-1,不计算这些点的分类loss,来提高网络的泛化性,网络构建已经有提到过。
 

代码在:pcdet/models/dense_heads/point_head_template.py

    def get_cls_layer_loss(self, tb_dict=None):
        # 第一阶段点的GT类别
        point_cls_labels = self.forward_ret_dict['point_cls_labels'].view(-1)
        # 第一阶段点的预测类别
        point_cls_preds = self.forward_ret_dict['point_cls_preds'].view(-1, self.num_class)
        # 取出属于前景的点的mask,0为背景,1,2,3分别为前景,-1不关注
        positives = (point_cls_labels > 0)
        # 背景点分类权重置0
        negative_cls_weights = (point_cls_labels == 0) * 1.0
        # 前景点分类权重置0
        cls_weights = (negative_cls_weights + 1.0 * positives).float()
        # 使用前景点的个数来normalize,使得一批数据中每个前景点贡献的loss一样
        pos_normalizer = positives.sum(dim=0).float()
        # 正则化每个类别分类损失权重
        cls_weights /= torch.clamp(pos_normalizer, min=1.0)
        # 初始化分类的one-hot (batch * 16384, 4)
        one_hot_targets = point_cls_preds.new_zeros(*list(point_cls_labels.shape), self.num_class + 1)
        # 将目标标签转换为one-hot编码形式 https://blog.csdn.net/guofei_fly/article/details/104308528
        one_hot_targets.scatter_(-1, (point_cls_labels * (point_cls_labels >= 0).long()).unsqueeze(dim=-1).long(), 1.0)
        # 原来背景为[1, 0, 0, 0] 现在背景为[0, 0, 0]
        one_hot_targets = one_hot_targets[..., 1:]
        # 计算分类损失使用focal loss
        cls_loss_src = self.cls_loss_func(point_cls_preds, one_hot_targets, weights=cls_weights)
        # 各类别loss置求总数
        point_loss_cls = cls_loss_src.sum()
        # 分类损失权重
        loss_weights_dict = self.model_cfg.LOSS_CONFIG.LOSS_WEIGHTS
        # 分类损失乘以分类损失权重
        point_loss_cls = point_loss_cls * loss_weights_dict['point_cls_weight']

        if tb_dict is None:
            tb_dict = 
        # 使用.item()将tensor转换成标量,抛弃Backward属性,可以优化显存,
        tb_dict.update(
            'point_loss_cls': point_loss_cls.item(),
            'point_pos_num': pos_normalizer.item()
        )

        return point_loss_cls, tb_dict

3、Refinement网络损失

proposal refinement网络的损失计算如下:

 为预测的box残差,为target编码的残差,编码方式和第一阶段相同。

3.1 quality-aware confidence prediction

        在PV-RCNN中作者将二阶段的置信度预测改成了quality-aware confidence预测的形式,计算公式如下:

        yk = min (1, max (0, 2IoUk − 0.5))

       在target assignment的时候,已经完成了该计算。

代码在:pcdet/models/roi_heads/roi_head_template.py

    def get_box_cls_layer_loss(self, forward_ret_dict):
        loss_cfgs = self.model_cfg.LOSS_CONFIG
        # 每个proposal的预测置信度 shape (batch *128, 1)
        rcnn_cls = forward_ret_dict['rcnn_cls']
        """
        Point RCNN
        rcnn_cls_labels
        每个proposal与之对应的GT,
        其中IOU大于0.6为前景,数值为1 
        0.45-0.6忽略不计算loss,数值为-1 
        0.45为背景,数值为0
        rcnn_cls_labels shape (batch *128 ,)
        """
        """
        PV-RCNN
        每个rcnn_cls_labels的数值不再是1或者0,
        改为了预测yk = min (1, max (0, 2IoUk − 0.5))
        quality-aware confidence prediction的方式
        """
        rcnn_cls_labels = forward_ret_dict['rcnn_cls_labels'].view(-1)
        if loss_cfgs.CLS_LOSS == 'BinaryCrossEntropy':
            # shape (batch *128, 1)--> (batch *128, )
            rcnn_cls_flat = rcnn_cls.view(-1)
            batch_loss_cls = F.binary_cross_entropy(torch.sigmoid(rcnn_cls_flat), rcnn_cls_labels.float(),
                                                    reduction='none')
            # 生成前背景mask
            cls_valid_mask = (rcnn_cls_labels >= 0).float()
            # 求loss值,并根据前背景总数进行正则化
            rcnn_loss_cls = (batch_loss_cls * cls_valid_mask).sum() / torch.clamp(cls_valid_mask.sum(), min=1.0)
        elif loss_cfgs.CLS_LOSS == 'CrossEntropy':
            batch_loss_cls = F.cross_entropy(rcnn_cls, rcnn_cls_labels, reduction='none', ignore_index=-1)
            cls_valid_mask = (rcnn_cls_labels >= 0).float()
            rcnn_loss_cls = (batch_loss_cls * cls_valid_mask).sum() / torch.clamp(cls_valid_mask.sum(), min=1.0)
        else:
            raise NotImplementedError

        # 乘以分类损失权重
        rcnn_loss_cls = rcnn_loss_cls * loss_cfgs.LOSS_WEIGHTS['rcnn_cls_weight']

        tb_dict = 'rcnn_loss_cls': rcnn_loss_cls.item()
        return rcnn_loss_cls, tb_dict

3.2 box refinement loss

在 box refinement loss的计算过程中,

这里需要ROI于GT的3D IOU大于0.55的ROI计算回归loss。在OpenPCDet中,PointRCNN的第二阶段的回归loss由两部分组成;其中第一部分为前景ROI与GT的每个参数的SmoothL1 Loss,第二部分为前景ROI与GT的Corner Loss。

1 SmoothL1 Loss

        直接对前景roi的微调结果和GT计算Loss,这里的角度残差计算直接使用SmoothL1函数计算,原因是因为被认为属于前景的ROI其与GT的3D IOU大于0.55,所以两个box之间的角度偏差在正负45度以内

2 CORNER LOSS REGULARIZATION

        Corner Loss来源于F-PointNet,用于联合优化box的7个预测参数;在F-PointNet中指出,直接使用SmoothL1来回归box的参数,是直接对box的中心点,box的长宽高,box的朝向分别进行优化的。这样的优化可能会出现,box的中心点和长宽高已经可以十分准确的回归时,角度的预测却出现了偏差,导致3D IOU的降低的主要原因由角度预测错误引起。因此提出需要在(IOU metric)的度量方式下联合优化3D Box。为了解决这个问题,提出了一个正则化损失即Corner Loss,公式如下:

 Corner Loss是GTBox和预测Box的8个顶点的差值的和,因为一个box的顶点会被box的中心、box的长宽高、box的朝向所决定;因此 Corner Loss 可以作为这个多任务优化参数的正则项。

公式中,NS和NH分别代表了预测框和GT框,然后将box的坐标系都转换到以自身的中心坐标点上,P的i,j,k代表了box的不同类别的尺度,旋转角,和预定义的顶角顺序;在计算loss时,为了避免因为角度估计错误而导致的过大的正则化项,因此,会同时计算角度预测方向正确和完成相反的两种情况,并取其中最小值为该box的loss。δij为一个二维mask,用于选取需要计算loss的距离项。 
代码在:pcdet/models/roi_heads/roi_head_template.py

 def get_box_reg_layer_loss(self, forward_ret_dict):
        loss_cfgs = self.model_cfg.LOSS_CONFIG
        code_size = self.box_coder.code_size  # 7
        # (batch * 128, )#每帧点云中,有128个roi,只需要对iou大于0.55的roi计算loss
        reg_valid_mask = forward_ret_dict['reg_valid_mask'].view(
            -1)
        # 每个roi的gt_box  canonical坐标系下 (batch , 128, 7)
        gt_boxes3d_ct = forward_ret_dict['gt_of_rois'][..., 0:code_size]
        # 每个roi的gt_box 点云坐标系下 (batch * 128, 7)
        gt_of_rois_src = forward_ret_dict['gt_of_rois_src'][..., 0:code_size].view(-1, code_size)
        # 每个roi的调整参数 (rcnn_batch_size, C)  (batch * 128, 7)
        rcnn_reg = forward_ret_dict['rcnn_reg']
        # 每个roi的7个位置大小转向角参数 (batch , 128, 7)
        roi_boxes3d = forward_ret_dict['rois']
        rcnn_batch_size = gt_boxes3d_ct.view(-1, code_size).shape[0]  # 256
        # 获取前景mask
        fg_mask = (reg_valid_mask > 0)
        # 用于正则化
        fg_sum = fg_mask.long().sum().item()

        tb_dict = 

        if loss_cfgs.REG_LOSS == 'smooth-l1':
            rois_anchor = roi_boxes3d.clone().detach().view(-1, code_size)
            rois_anchor[:, 0:3] = 0
            rois_anchor[:, 6] = 0
            """
            编码GT和roi之间的回归残差  
            由于在第二阶段选出的每个roi都和GT的 3D_IOU大于0.55,
            所有roi_box和GT_box的角度差距只会在正负45度以内;
            因此,此处的角度直接使用SmoothL1进行回归,
            不再使用residual-cos-based的方法编码角度
            """
            reg_targets = self.box_coder.encode_torch(
                gt_boxes3d_ct.view(rcnn_batch_size, code_size), rois_anchor
            )
            # 计算第二阶段的回归残差损失 [B, M, 7]
            rcnn_loss_reg = self.reg_loss_func(
                rcnn_reg.view(rcnn_batch_size, -1).unsqueeze(dim=0),
                reg_targets.unsqueeze(dim=0),
            )
            # 这里只计算3D iou大于0.55的roi_box的loss
            rcnn_loss_reg = (rcnn_loss_reg.view(rcnn_batch_size, -1) * fg_mask.unsqueeze(dim=-1).float()).sum() / max(
                fg_sum, 1)
            rcnn_loss_reg = rcnn_loss_reg * loss_cfgs.LOSS_WEIGHTS['rcnn_reg_weight']
            tb_dict['rcnn_loss_reg'] = rcnn_loss_reg.item()

            # 此处使用了F-PointNet中的corner loss来联合优化roi_box的 中心位置、角度、大小
            if loss_cfgs.CORNER_LOSS_REGULARIZATION and fg_sum > 0:
                # TODO: NEED to BE CHECK
                # 取出对前景ROI的回归结果(num_of_fg_roi, 7)
                fg_rcnn_reg = rcnn_reg.view(rcnn_batch_size, -1)[fg_mask]
                # 取出所有前景ROI(num_of_fg_roi, 7)
                fg_roi_boxes3d = roi_boxes3d.view(-1, code_size)[fg_mask]

                # 前景ROI(1, num_of_fg_roi, 7)
                fg_roi_boxes3d = fg_roi_boxes3d.view(1, -1, code_size)
                # 前景ROI(1, num_of_fg_roi, 7)
                batch_anchors = fg_roi_boxes3d.clone().detach()
                # 取出前景ROI的角度
                roi_ry = fg_roi_boxes3d[:, :, 6].view(-1)
                # 取出前景ROI的xyz
                roi_xyz = fg_roi_boxes3d[:, :, 0:3].view(-1, 3)
                # 将前景ROI的xyz置0,转化到以自身中心为原点(CCS坐标系),
                # 用于解码第二阶段得到的回归预测结果
                batch_anchors[:, :, 0:3] = 0
                # 根据第二阶段的微调结果来解码出最终的预测结果
                rcnn_boxes3d = self.box_coder.decode_torch(
                    fg_rcnn_reg.view(batch_anchors.shape[0], -1, code_size), batch_anchors
                ).view(-1, code_size)

                # 将canonical坐标系下的角度转回到点云坐标系中 (num_of_fg_roi, 7)
                rcnn_boxes3d = common_utils.rotate_points_along_z(
                    rcnn_boxes3d.unsqueeze(dim=1), roi_ry
                ).squeeze(dim=1)
                # 将canonical坐标系的中心坐标转回原点云雷达坐标系中
                rcnn_boxes3d[:, 0:3] += roi_xyz

                # corner loss  根据前景的ROI的refinement结果和对应的GTBox 计算corner_loss
                loss_corner = loss_utils.get_corner_loss_lidar(
                    rcnn_boxes3d[:, 0:7],  # 前景的ROI的refinement结果
                    gt_of_rois_src[fg_mask][:, 0:7]  # GTBox
                )
                # 求出所有前景ROI corner loss的均值
                loss_corner = loss_corner.mean()
                loss_corner = loss_corner * loss_cfgs.LOSS_WEIGHTS['rcnn_corner_weight']
                # 将两个回归损失求和
                rcnn_loss_reg += loss_corner
                tb_dict['rcnn_loss_corner'] = loss_corner.item()
        else:
            raise NotImplementedError

        return rcnn_loss_reg, tb_dict
get_corner_loss_lidar代码在pcdet/utils/loss_utils.py
def get_corner_loss_lidar(pred_bbox3d: torch.Tensor, gt_bbox3d: torch.Tensor):
    """
    Args:
        pred_bbox3d: (N, 7) float Tensor.
        gt_bbox3d: (N, 7) float Tensor.
    Returns:
        corner_loss: (N) float Tensor.
    """
    assert pred_bbox3d.shape[0] == gt_bbox3d.shape[0]
    # 将预测box的7个坐标值转换到其在3D空间中对应的8个顶点
    pred_box_corners = box_utils.boxes_to_corners_3d(pred_bbox3d)
    # 将GTBox的7个坐标值转换到其在3D空间中对应的8个顶点
    gt_box_corners = box_utils.boxes_to_corners_3d(gt_bbox3d)
    # 再计算GTBox和预测的box的方向完全相反的情况
    gt_bbox3d_flip = gt_bbox3d.clone()
    gt_bbox3d_flip[:, 6] += np.pi
    gt_box_corners_flip = box_utils.boxes_to_corners_3d(gt_bbox3d_flip)
    #  所有的box和GT取距离最小值,防止因为距离相反产生较大的loss(N, 8)
    corner_dist = torch.min(torch.norm(pred_box_corners - gt_box_corners, dim=2),
                            torch.norm(pred_box_corners - gt_box_corners_flip, dim=2))
    # (N, 8)
    corner_loss = WeightedSmoothL1Loss.smooth_l1_loss(corner_dist, beta=1.0)
    # 对每个box的8个顶点的差距求均值
    return corner_loss.mean(dim=1)
boxes_to_corners_3d在pcdet/utils/box_utils.py
def boxes_to_corners_3d(boxes3d):
    """
        7 -------- 4
       /|         /|
      6 -------- 5 .
      | |        | |
      . 3 -------- 0
      |/         |/
      2 -------- 1
    Args:
        boxes3d:  (N, 7) [x, y, z, dx, dy, dz, heading], (x, y, z) is the box center
    Returns:
    """
    boxes3d, is_numpy = common_utils.check_numpy_to_torch(boxes3d)
    # shape (8, 3)
    template = boxes3d.new_tensor((
        [1, 1, -1], [1, -1, -1], [-1, -1, -1], [-1, 1, -1],
        [1, 1, 1], [1, -1, 1], [-1, -1, 1], [-1, 1, 1],
    )) / 2
 
    corners3d = boxes3d[:, None, 3:6].repeat(1, 8, 1) * template[None, :, :]
    corners3d = common_utils.rotate_points_along_z(corners3d.view(-1, 8, 3), boxes3d[:, 6]).view(-1, 8, 3)
    corners3d += boxes3d[:, None, 0:3]
 
    return corners3d.numpy() if is_numpy else corners3d

至此,PV-RCNN的所有loss计算就完成了,下面看看推理的实现。

2、网络推理实现

2.1 网络预测结果生成

    看回PV-RCNN head第二阶段中roi精调的代码,在预测阶段,需要根据前面提出的roi和第二阶段的精调结果生成最终的预测结果;分别是:

batch_cls_preds (100,1) 每个ROI Box的置信度得分
batch_box_preds (100,7) 每个ROI Box的7个参数 (x,y,z,l,w,h,theta)

注:在推理阶段,第一阶段的anchor得到的ROI的个数是100个且NMS阈值是0.7。

代码在:pcdet/models/roi_heads/pvrcnn_head.py

    def forward(self, batch_dict):
        """
        :param input_data: input dict
        :return:
        """
        # 根据所有的预测结果生成proposal, rois: (B, num_rois, 7+C)
        # roi_scores: (B, num_rois) roi_labels: (B, num_rois)
        targets_dict = self.proposal_layer(
            batch_dict, nms_config=self.model_cfg.NMS_CONFIG['TRAIN' if self.training else 'TEST']
        )
        # 训练模式下, 需要对选取的roi进行target assignment,并将ROI对应的GTBox转换到CCS坐标系下
        """
        xxxxxxxx
        """
            
        # (B, 1 or 2)     rcnn_cls proposal的置信度  (batch * 128,  1)
        rcnn_cls = self.cls_layers(shared_features).transpose(1, 2).contiguous().squeeze(dim=1)
        # (B, C)      rcnn_reg  proposal的box refinement结果 (batch * 128,  7)
        rcnn_reg = self.reg_layers(shared_features).transpose(1, 2).contiguous().squeeze(dim=1)
        # 推理模式下,根据微调生成预测结果
        if not self.training:
            batch_cls_preds, batch_box_preds = self.generate_predicted_boxes(
                batch_size=batch_dict['batch_size'], rois=batch_dict['rois'], cls_preds=rcnn_cls, box_preds=rcnn_reg
            )
            batch_dict['batch_cls_preds'] = batch_cls_preds
            batch_dict['batch_box_preds'] = batch_box_preds
            batch_dict['cls_preds_normalized'] = False
        else:
            targets_dict['rcnn_cls'] = rcnn_cls
            targets_dict['rcnn_reg'] = rcnn_reg

            self.forward_ret_dict = targets_dict

        return batch_dict

生成最终预测box的函数为generate_predicted_boxes

代码在:pcdet/models/roi_heads/roi_head_template.py

    def generate_predicted_boxes(self, batch_size, rois, cls_preds, box_preds):
        """
        Args:
            batch_size:
            rois: (B, N, 7)
            cls_preds: (BN, num_class)
            box_preds: (BN, code_size)

        Returns:

        """
        # 回归编码的7个参数 x, y, z, l, w, h, θ
        code_size = self.box_coder.code_size
        # 对ROI的置信度分数预测batch_cls_preds : (B, num_of_roi, num_class or 1)
        batch_cls_preds = cls_preds.view(batch_size, -1, cls_preds.shape[-1])
        # 对ROI Box的参数调整 batch_box_preds : (B, num_of_roi, 7)
        batch_box_preds = box_preds.view(batch_size, -1, code_size)
        # 取出每个roi的旋转角度,并拿出每个roi的xyz坐标,
        # local_roi用于生成每个点自己的bbox,
        # 因为之前的预测都是基于CCS坐标系下的,所以生成后需要将原xyz坐标上上去
        roi_ry = rois[:, :, 6].view(-1)
        roi_xyz = rois[:, :, 0:3].view(-1, 3)
        local_rois = rois.clone().detach()
        local_rois[:, :, 0:3] = 0
        # 得到CCS坐标系下每个ROI Box的经过refinement后的Box结果
        batch_box_preds = self.box_coder.decode_torch(batch_box_preds, local_rois).view(-1, code_size)

        # 完成CCS到点云坐标系的转换
        # 将canonical坐标系下的box角度转回到点云坐标系中
        batch_box_preds = common_utils.rotate_points_along_z(
            batch_box_preds.unsqueeze(dim=1), roi_ry
        ).squeeze(dim=1)
        # 将canonical坐标系下的box的中心偏移估计加上roi的中心,转回到点云坐标系中
        batch_box_preds[:, 0:3] += roi_xyz
        batch_box_preds = batch_box_preds.view(batch_size, -1, code_size)
        # batch_cls_preds 每个ROI Box的置信度得分
        # batch_box_preds 每个ROI Box的7个参数 (x,y,z,l,w,h,theta)
        return batch_cls_preds, batch_box_preds

box_decode的函数代码在:pcdet/utils/box_coder_utils.py

注:这里没有方向分类,因为角度已经在正负45度以内的,原因在训练的target assignment中已经说过。

class ResidualCoder(object):
    def __init__(self, code_size=7, encode_angle_by_sincos=False, **kwargs):
        """
        loss中anchor和gt的编码与解码
        7个参数的编码的方式为
            ∆x = (x^gt − xa^da)/d^a , ∆y = (y^gt − ya^da)/d^a , ∆z = (z^gt − za^ha)/h^a
            ∆w = log (w^gt / w^a) ∆l = log (l^gt / l^a) , ∆h = log (h^gt / h^a)
            ∆θ = sin(θ^gt - θ^a)
        """
        super().__init__()
        self.code_size = code_size
        self.encode_angle_by_sincos = encode_angle_by_sincos
        if self.encode_angle_by_sincos:
            self.code_size += 1

    def encode_torch(self, boxes, anchors):
        """
        Args:
            boxes: (N, 7 + C) [x, y, z, dx, dy, dz, heading, ...]
            anchors: (N, 7 + C) [x, y, z, dx, dy, dz, heading or *[cos, sin], ...]

        Returns:

        """
        # 截断anchors的[dx,dy,dz],每个anchor_box的l, w, h数值如果小于1e-5则为1e-5
        anchors[:, 3:6] = torch.clamp_min(anchors[:, 3:6], min=1e-5)
        # 截断boxes的[dx,dy,dz] 每个GT_box的l, w, h数值如果小于1e-5则为1e-5
        boxes[:, 3:6] = torch.clamp_min(boxes[:, 3:6], min=1e-5)
        # If split_size_or_sections is an integer type, then tensor will be split into equally sized chunks (if possible).
        # Last chunk will be smaller if the tensor size along the given dimension dim is not divisible by split_size.

        # 这里指torch.split的第二个参数   torch.split(tensor, split_size, dim=)  split_size是切分后每块的大小,不是切分为多少块!,多余的参数使用*cags接收
        xa, ya, za, dxa, dya, dza, ra, *cas = torch.split(anchors, 1, dim=-1)
        xg, yg, zg, dxg, dyg, dzg, rg, *cgs = torch.split(boxes, 1, dim=-1)
        # 计算anchor对角线长度
        diagonal = torch.sqrt(dxa ** 2 + dya ** 2)

        # 计算loss的公式,Δx,Δy,Δz,Δw,Δl,Δh,Δθ
        # ∆x = (x^gt − xa^da)/diagonal
        xt = (xg - xa) / diagonal
        # ∆y = (y^gt − ya^da)/diagonal
        yt = (yg - ya) / diagonal
        # ∆z = (z^gt − za^ha)/h^a
        zt = (zg - za) / dza
        # ∆l = log(l ^ gt / l ^ a)
        dxt = torch.log(dxg / dxa)
        # ∆w = log(w ^ gt / w ^ a)
        dyt = torch.log(dyg / dya)
        # ∆h = log(h ^ gt / h ^ a)
        dzt = torch.log(dzg / dza)
        # False
        if self.encode_angle_by_sincos:
            rt_cos = torch.cos(rg) - torch.cos(ra)
            rt_sin = torch.sin(rg) - torch.sin(ra)
            rts = [rt_cos, rt_sin]
        else:
            rts = [rg - ra]  # Δθ

        cts = [g - a for g, a in zip(cgs, cas)]
        return torch.cat([xt, yt, zt, dxt, dyt, dzt, *rts, *cts], dim=-1)

    def decode_torch(self, box_encodings, anchors):
        """
        Args:
            box_encodings: (B, N, 7 + C) or (N, 7 + C) [x, y, z, dx, dy, dz, heading or *[cos, sin], ...]
            anchors: (B, N, 7 + C) or (N, 7 + C) [x, y, z, dx, dy, dz, heading, ...]

        Returns:

        """
        # 这里指torch.split的第二个参数   torch.split(tensor, split_size, dim=)
        # split_size是切分后每块的大小,不是切分为多少块!,多余的参数使用*cags接收
        xa, ya, za, dxa, dya, dza, ra, *cas = torch.split(anchors, 1, dim=-1)
        # 分割编码后的box PointPillar为False
        if not self.encode_angle_by_sincos:
            xt, yt, zt, dxt, dyt, dzt, rt, *cts = torch.split(box_encodings, 1, dim=-1)
        else:
            xt, yt, zt, dxt, dyt, dzt, cost, sint, *cts = torch.split(box_encodings, 1, dim=-1)
        # 计算anchor对角线长度
        diagonal = torch.sqrt(dxa ** 2 + dya ** 2)  # (B, N, 1)-->(1, 321408, 1)
        # loss计算中anchor与GT编码的运算:g表示gt,a表示anchor
        # ∆x = (x^gt − xa^da)/diagonal --> x^gt = ∆x * diagonal + x^da
        # 下同
        xg = xt * diagonal + xa
        yg = yt * diagonal + ya
        zg = zt * dza + za
        # ∆l = log(l^gt / l^a)的逆运算 --> l^gt = exp(∆l) * l^a
        # 下同
        dxg = torch.exp(dxt) * dxa
        dyg = torch.exp(dyt) * dya
        dzg = torch.exp(dzt) * dza

        # 如果角度是cos和sin编码,采用新的解码方式 PointPillar为False
        if self.encode_angle_by_sincos:
            rg_cos = cost + torch.cos(ra)
            rg_sin = sint + torch.sin(ra)
            rg = torch.atan2(rg_sin, rg_cos)
        else:
            # rts = [rg - ra] 角度的逆运算
            rg = rt + ra
        # PointPillar无此项
        cgs = [t + a for t, a in zip(cts, cas)]
        return torch.cat([xg, yg, zg, dxg, dyg, dzg, rg, *cgs], dim=-1)

最终得到预测结果:

batch_cls_preds (1, 100)每个ROI Box的置信度得分

 batch_box_preds (1, 100, 7)每个ROI Box的7个参数 (x,y,z,l,w,h,theta)

2.2 后处理

        后处理完成了对最终100个ROI预测结果的NMS操作;同时需要注意的是,每个box的最终分类结果是由第一阶段得出,第二阶段的分类结果得到的是IOU预测;此处实现与FRCNN不同(Frcnn第一阶段完成前背景选取,第二阶段进行分类),需注意。

后处理中的NMS阈值是0.01,不考虑任何overlap在3D世界中。

代码在:pcdet/models/detectors/detector3d_template.py

    def post_processing(self, batch_dict):
        """
        Args:
            batch_dict:
                batch_size:
                batch_cls_preds: (B, num_boxes, num_classes | 1) or (N1+N2+..., num_classes | 1)
                                or [(B, num_boxes, num_class1), (B, num_boxes, num_class2) ...]
                multihead_label_mapping: [(num_class1), (num_class2), ...]
                batch_box_preds: (B, num_boxes, 7+C) or (N1+N2+..., 7+C)
                cls_preds_normalized: indicate whether batch_cls_preds is normalized
                batch_index: optional (N1+N2+...)
                has_class_labels: True/False
                roi_labels: (B, num_rois)  1 .. num_classes
                batch_pred_labels: (B, num_boxes, 1)
        Returns:
        """
        # post_process_cfg后处理参数,包含了nms类型、阈值、使用的设备、nms后最多保留的结果和输出的置信度等设置
        post_process_cfg = self.model_cfg.POST_PROCESSING
        # 推理默认为1
        batch_size = batch_dict['batch_size']
        # 保留计算recall的字典
        recall_dict = 
        # 预测结果存放在此
        pred_dicts = []
        # 逐帧进行处理
        for index in range(batch_size):
            if batch_dict.get('batch_index', None) is not None:
                assert batch_dict['batch_box_preds'].shape.__len__() == 2
                batch_mask = (batch_dict['batch_index'] == index)
            else:
                assert batch_dict['batch_box_preds'].shape.__len__() == 3
                # 得到当前处理的是第几帧
                batch_mask = index
            # box_preds shape (所有anchor的数量, 7)
            box_preds = batch_dict['batch_box_preds'][batch_mask]
            # 复制后,用于recall计算
            src_box_preds = box_preds
 
            if not isinstance(batch_dict['batch_cls_preds'], list):
                # (所有anchor的数量, 3)
                cls_preds = batch_dict['batch_cls_preds'][batch_mask]
                # 同上
                src_cls_preds = cls_preds
                assert cls_preds.shape[1] in [1, self.num_class]
 
                if not batch_dict['cls_preds_normalized']:
                    # 损失函数计算使用的BCE,所以这里使用sigmoid激活函数得到类别概率
                    cls_preds = torch.sigmoid(cls_preds)
            else:
                cls_preds = [x[batch_mask] for x in batch_dict['batch_cls_preds']]
                src_cls_preds = cls_preds
                if not batch_dict['cls_preds_normalized']:
                    cls_preds = [torch.sigmoid(x) for x in cls_preds]
 
            # 是否使用多类别的NMS计算,否,不考虑不同类别的物体会在3D空间中重叠
            if post_process_cfg.NMS_CONFIG.MULTI_CLASSES_NMS:
                if not isinstance(cls_preds, list):
                    cls_preds = [cls_preds]
                    multihead_label_mapping = [torch.arange(1, self.num_class, device=cls_preds[0].device)]
                else:
                    multihead_label_mapping = batch_dict['multihead_label_mapping']
 
                cur_start_idx = 0
                pred_scores, pred_labels, pred_boxes = [], [], []
                for cur_cls_preds, cur_label_mapping in zip(cls_preds, multihead_label_mapping):
                    assert cur_cls_preds.shape[1] == len(cur_label_mapping)
                    cur_box_preds = box_preds[cur_start_idx: cur_start_idx + cur_cls_preds.shape[0]]
                    cur_pred_scores, cur_pred_labels, cur_pred_boxes = model_nms_utils.multi_classes_nms(
                        cls_scores=cur_cls_preds, box_preds=cur_box_preds,
                        nms_config=post_process_cfg.NMS_CONFIG,
                        score_thresh=post_process_cfg.SCORE_THRESH
                    )
                    cur_pred_labels = cur_label_mapping[cur_pred_labels]
                    pred_scores.append(cur_pred_scores)
                    pred_labels.append(cur_pred_labels)
                    pred_boxes.append(cur_pred_boxes)
                    cur_start_idx += cur_cls_preds.shape[0]
 
                final_scores = torch.cat(pred_scores, dim=0)
                final_labels = torch.cat(pred_labels, dim=0)
                final_boxes = torch.cat(pred_boxes, dim=0)
            else:
                # 得到类别预测的最大概率,和对应的索引值
                cls_preds, label_preds = torch.max(cls_preds, dim=-1)
                if batch_dict.get('has_class_labels', False):
                    # 如果有roi_labels在里面字典里面,
                    # 使用第一阶段预测的label为改预测结果的分类类别
                    label_key = 'roi_labels' if 'roi_labels' in batch_dict else 'batch_pred_labels'
                    label_preds = batch_dict[label_key][index]
                else:
                    # 类别预测值加1
                    label_preds = label_preds + 1
 
                # 无类别NMS操作
                # selected : 返回了被留下来的anchor索引
                # selected_scores : 返回了被留下来的anchor的置信度分数
                selected, selected_scores = model_nms_utils.class_agnostic_nms(
                    # 每个anchor的类别预测概率和anchor回归参数
                    box_scores=cls_preds, box_preds=box_preds,
                    nms_config=post_process_cfg.NMS_CONFIG,
                    score_thresh=post_process_cfg.SCORE_THRESH
                )
                # 无此项
                if post_process_cfg.OUTPUT_RAW_SCORE:
                    max_cls_preds, _ = torch.max(src_cls_preds, dim=-1)
                    selected_scores = max_cls_preds[selected]
 
                # 得到最终类别预测的分数
                final_scores = selected_scores
                # 根据selected得到最终类别预测的结果
                final_labels = label_preds[selected]
                # 根据selected得到最终box回归的结果
                final_boxes = box_preds[selected]
 
            # 如果没有GT的标签在batch_dict中,就不会计算recall值
            recall_dict = self.generate_recall_record(
                box_preds=final_boxes if 'rois' not in batch_dict else src_box_preds,
                recall_dict=recall_dict, batch_index=index, data_dict=batch_dict,
                thresh_list=post_process_cfg.RECALL_THRESH_LIST
            )
            # 生成最终预测的结果字典
            record_dict = 
                'pred_boxes': final_boxes,
                'pred_scores': final_scores,
                'pred_labels': final_labels
            
            pred_dicts.append(record_dict)
 
        return pred_dicts, recall_dict

其中 无类别的NMS操作在:pcdet/models/model_utils/model_nms_utils.py

def class_agnostic_nms(box_scores, box_preds, nms_config, score_thresh=None):
    # 1.首先根据置信度阈值过滤掉部过滤掉大部分置信度低的box,加速后面的nms操作
    src_box_scores = box_scores
    if score_thresh is not None:
        # 得到类别预测概率大于score_thresh的mask
        scores_mask = (box_scores >= score_thresh)
        # 根据mask得到哪些anchor的类别预测大于score_thresh-->anchor类别
        box_scores = box_scores[scores_mask]
        # 根据mask得到哪些anchor的类别预测大于score_thresh-->anchor回归的7个参数
        box_preds = box_preds[scores_mask]
 
    # 初始化空列表,用来存放经过nms后保留下来的anchor
    selected = []
    # 如果有anchor的类别预测大于score_thresh的话才进行nms,否则返回空
    if box_scores.shape[0] > 0:
        # 这里只保留最大的K个anchor置信度来进行nms操作,
        # k取min(nms_config.NMS_PRE_MAXSIZE, box_scores.shape[0])的最小值
        box_scores_nms, indices = torch.topk(box_scores, k=min(nms_config.NMS_PRE_MAXSIZE, box_scores.shape[0]))
 
        # box_scores_nms只是得到了类别的更新结果;
        # 此处更新box的预测结果 根据tokK重新选取并从大到小排序的结果 更新boxes的预测
        boxes_for_nms = box_preds[indices]
        # 调用iou3d_nms_utils的nms_gpu函数进行nms,
        # 返回的是被保留下的box的索引,selected_scores = None
        # 根据返回索引找出box索引值
        keep_idx, selected_scores = getattr(iou3d_nms_utils, nms_config.NMS_TYPE)(
            boxes_for_nms[:, 0:7], box_scores_nms, nms_config.NMS_THRESH, **nms_config
        )
        selected = indices[keep_idx[:nms_config.NMS_POST_MAXSIZE]]
 
    if score_thresh is not None:
        # 如果存在置信度阈值,scores_mask是box_scores在src_box_scores中的索引,即原始索引
        original_idxs = scores_mask.nonzero().view(-1)
        # selected表示的box_scores的选择索引,经过这次索引,
        # selected表示的是src_box_scores被选择的box索引
        selected = original_idxs[selected]
 
    return selected, src_box_scores[selected]

最终得到每个预测Box的类别、置信度得分、box的7个参数。

3、PV-RCNN结果

PV-RCNN原论文结果:

PV-RCNN在KITTI数据集测试结果(结果仅显示在kitti验证集moderate精度)

结果推理:

2号彩色相机

 点云检测结果

 

4、消融实验

4.1 voxel-to-keypoint场景编码的效果

        RPN Baseline是直接SECOND的网络形式,

        Pool from Encoder是前面提到过的,直接将多个尺度的特征聚合在一个ROI grid(个人认为,此处ROI应该是指BEV操作前的voxel特征层)中进行调优的结果以KITTI为例,在经过4x下采样的一般场景中,还会存在18K个voxel,如果每个voxel中采用3x3x3的grid point进行聚合的话,那么需要计算2700*18000的成对距离和特征聚合(该方式会占用很多的计算资源和内存)

        PV-RCNN则是本文采用的方式,只将场景编码成一小部分关键点特征,并采用Keypoint-to-grid RoI Feature Abstraction for Proposal Refifinement将的方式来对proposal进行精调

        可以看到在proposal中融入场景特征确实是可以在不同的任务(简单、中等、困难)上均有提升,同时对于困难样本提升更为明显,主要是因为在proposal中融入关键点编码的可以扩大感受野并且使用带有监督的关键点分割可以学习到更好的关键点特征。使用一小部分关键点来作为中间的特征表达相比于Pool from Encoder的方式可以有效的减小资源消耗。

4.2 VSA中使用来自不同层的特征的影响

        从图中可以在处在VSA模块中融入不同尺度的特征对最终结果的影响如何,这里主要说一点,如果只采用来自原始点云中的特征的话,效果会降低很多,说明来自不同3D卷积层的语义信息对于box的定位是有帮助的

4.3 PKW模块的效果

        PKW模块主要用于调整属于前背景关键点特征的权重,可以看1和4行结果,如果没有使用PKW模块对关键点属于前背景权重的权重进行调整,网络的定位精度下降了。说明在proposal中前景点的多尺度特征需要给与更多的关注。

4.4 ROI-grid Pooling的影响

        这里的2、3行主要比对了使用ROI-aware Pooling(来自史帅自己的Part A2 Net)和使用本篇文章提出的ROI-grid Pooling对结果的影响,验证了ROI-grid Pooling相比于ROI-aware Pooling拥有更加丰富的感受野,原因在实现第一章就已经说明了,主要还是基于ROI-grid Pooling因为有SA操作的原因,对proposal外部边沿的点都有聚合的作用。

        3、4行的比对试验表明了基于quality-aware confifidence prediction strategy对最终结果的影响,大家自行看一下。


至此,PV-RCNN的内容就全部解析完了,如果中间有什么错误或是不足,欢迎大家指正、讨论;对于看到这里的小伙伴 ,你们也是牛逼!!!!

下一篇文章,带来Voxel RCNN;了解了本文,Voxel RCNN则只是将VSA模块和keypoint-to-voxel换成了Voxel ROI Pooling层,实现上会简单很多。

参考文章或文献:

1、GitHub - open-mmlab/OpenPCDet: OpenPCDet Toolbox for LiDAR-based 3D Object Detection.

2、https://arxiv.org/pdf/1912.13192.pdf

3、Sensors | Free Full-Text | SECOND: Sparsely Embedded Convolutional Detection

4、【3D计算机视觉】从PointNet到PointNet++理论及pytorch代码_小执着~的博客-CSDN博客_pointnet

5、PointRCNN论文和逐代码详解_NNNNNathan的博客-CSDN博客

以上是关于PV-RCNN的推理实现和LOSS计算的主要内容,如果未能解决你的问题,请参考以下文章

什么是损失函数与平均误差算法分析

Pytorch:损失函数

pytorch loss

pytorch loss

对数损失函数(Logarithmic Loss Function)的原理和 Python 实现

源码解读YOLO v3 训练 - 05 损失函数loss