主頁 >  其他 > PointRCNN的loss計算與推理實作

PointRCNN的loss計算與推理實作

2022-03-06 07:30:00 其他

在我前面的文章中,已經完成了PointRCNN的網路構建,鏈接在這:

PointRCNN論文和逐代碼詳解_NNNNNathan的博客-CSDN博客1、前言當前點云檢測的常見方式分別有1、將點云劃分成voxel來進行檢測,典型的模型有VoxelNet、SECOND等;作然而本文的作者史博士提出這種方法會出現量化造成的資訊損失,2、將點云投影到前視角或者鳥瞰圖來來進行檢測,包括MV3D、PIXOR、AVOD等檢測模型;同時這類模型也會出現量化損失,3、將點云直接生成偽圖片,然后使用2D的方式來進行處理,這主要是PointPillar,本文PointRCNN提出的方法,是一篇比較新穎的點云檢測方法,與此前的檢測模型不同,它直接根據點云分https://blog.csdn.net/qq_41366026/article/details/123214165?spm=1001.2014.3001.5501此處直接來完成網路的Loss計算部分和推理部分的決議,

注:OpenPCDet的損失實作已與原論文和原代碼倉庫不同,網路構建時候已經敘述過此問題,同時原來的實作中,PointRCNN是分階段訓練,先訓練第一階段之后在訓練第二階段網路,但是在OpenPCDet已經變成聯合訓練,

1、loss計算

1、第一階段loss計算

第一階段的損失包含了兩部分:

1. 對該幀中所有的點云計算前背景分類loss

2. 對屬于前景的點云計算box的回歸loss

1.1 前背景分類loss計算

由于在一幀點云中屬于前背景點的數量差異較大,作者在此處使用了Focal Loss:

其中alpha和gamma都與RetinaNet中保持一致,分別為0.25、2,

在計算前背景點的分類loss時,對每個GT enlarge 0.2米后才包括的點,類別置為-1,不計算這些點的分類loss,來提高網路的泛化性,網路構建已經有提到過,

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

每個proposal與之對應的GT,
其中IOU大于0.6為前景,數值為1
0.45-0.6忽略不計算loss,數值為-1
0.45為背景,數值為0

    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

Focal Loss計算代碼在:pcdet/utils/loss_utils.py

    def sigmoid_cross_entropy_with_logits(input: torch.Tensor, target: torch.Tensor):
        """ PyTorch Implementation for tf.nn.sigmoid_cross_entropy_with_logits:
            max(x, 0) - x * z + log(1 + exp(-abs(x))) in
            https://www.tensorflow.org/api_docs/python/tf/nn/sigmoid_cross_entropy_with_logits

        Args:
            input: (B, #anchors, #classes) float tensor.
                Predicted logits for each class
            target: (B, #anchors, #classes) float tensor.
                One-hot encoded classification targets

        Returns:
            loss: (B, #anchors, #classes) float tensor.
                Sigmoid cross entropy loss without reduction
        """
        loss = torch.clamp(input, min=0) - input * target + \
               torch.log1p(torch.exp(-torch.abs(input)))
        return loss

    def forward(self, input: torch.Tensor, target: torch.Tensor, weights: torch.Tensor):
        """
        Args:
            input: (B, #anchors, #classes) float tensor. eg:(4, 321408, 3)
                Predicted logits for each class :一個anchor會預測三種類別
            target: (B, #anchors, #classes) float tensor. eg:(4, 321408, 3)
                One-hot encoded classification targets,:真值
            weights: (B, #anchors) float tensor. eg:(4, 321408)
                Anchor-wise weights.
        Returns:
            weighted_loss: (B, #anchors, #classes) float tensor after weighting.
        """
        pred_sigmoid = torch.sigmoid(input)  # (batch_size, 321408, 3) f(x) = 1 / (1 + e^(-x))
        # 這里的加權主要是解決正負樣本不均衡的問題:正樣本的權重為0.25,負樣本的權重為0.75
        # 交叉熵來自KL散度,衡量兩個分布之間的相似性,針對二分類問題:
        # 合并形式: L = -(y * log(y^) + (1 - y) * log(1 - y^)) <-->
        # 分段形式:y = 1, L = -y * log(y^); y = 0, L = -(1 - y) * log(1 - y^)
        # 這兩種形式等價,只要是0和1的分類問題均可以寫成兩種等價形式,針對focal loss做類似處理
        # 相對熵 = 資訊熵 + 交叉熵, 且交叉熵是凸函式,求導時能夠得到全域最優值-->(sigma(s)- y)x  
        # https://zhuanlan.zhihu.com/p/35709485
        alpha_weight = target * self.alpha + (1 - target) * (1 - self.alpha)  # (4, 321408, 3)
        pt = target * (1.0 - pred_sigmoid) + (1.0 - target) * pred_sigmoid
        focal_weight = alpha_weight * torch.pow(pt, self.gamma)

        # (batch_size, 321408, 3) 交叉熵損失的一種變形,具體推到參考上面的鏈接
        bce_loss = self.sigmoid_cross_entropy_with_logits(input, target)

        loss = focal_weight * bce_loss  # (batch_size, 321408, 3)

        if weights.shape.__len__() == 2 or \
                (weights.shape.__len__() == 1 and target.shape.__len__() == 2):
            weights = weights.unsqueeze(-1)

        assert weights.shape.__len__() == loss.shape.__len__()
        # weights引數使用正anchor數目進行平均,使得每個樣本的損失與樣本中目標的數量無關
        return loss * weights

1.2. 前景點回歸loss計算

此處直接使用了SmoothL1損失計算前景點與GT直接的loss,對角度的編碼使用了residual-cos-based的方法,所以這里的8個回歸引數分別是:(x,y,z,l,w,h,cos(theta),sin(theta))

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

    def get_box_layer_loss(self, tb_dict=None):
        # 使用GT來找出屬于前景的點 (batch * 16384)
        pos_mask = self.forward_ret_dict['point_cls_labels'] > 0
        # 得到前景點的GT box引數 (batch * 16384, 8)
        point_box_labels = self.forward_ret_dict['point_box_labels']
        # 得到網路預測的前景點引數(batch * 16384, 8)
        point_box_preds = self.forward_ret_dict['point_box_preds']
        # 前景點的回歸權重置1;背景點為0,不計算loss
        reg_weights = pos_mask.float()
        # 使用前景點的個數來normalize,使得一批資料中每個前景點貢獻的loss一樣
        pos_normalizer = pos_mask.sum().float()

        reg_weights /= torch.clamp(pos_normalizer, min=1.0)
        # 使用帶權重的SmoothL1Loss來計算第一階段中box的回歸損失
        point_loss_box_src = self.reg_loss_func(
            point_box_preds[None, ...], point_box_labels[None, ...], weights=reg_weights[None, ...]
        )
        # 求和
        point_loss_box = point_loss_box_src.sum()
        # 回歸loss權重
        loss_weights_dict = self.model_cfg.LOSS_CONFIG.LOSS_WEIGHTS
        # 回歸損失乘回歸權重
        point_loss_box = point_loss_box * loss_weights_dict['point_box_weight']
        # 使用.item()將tensor轉換成標量,拋棄Backward屬性,可以優化顯存,
        if tb_dict is None:
            tb_dict = {}
        tb_dict.update({'point_loss_box': point_loss_box.item()})

        return point_loss_box, tb_dict

SmoothL1損失計算在:pcdet/utils/loss_utils.py

    def smooth_l1_loss(diff, beta):
        # 如果beta非常小,則直接用abs計算,否則按照正常的Smooth L1 Loss計算
        if beta < 1e-5:
            loss = torch.abs(diff)
        else:
            n = torch.abs(diff)  # (batch_size, 321408, 7)
            # smoothL1公式,如上面所示 --> (batch_size, 321408, 7)
            loss = torch.where(n < beta, 0.5 * n ** 2 / beta, n - 0.5 * beta)

        return loss

    def forward(self, input: torch.Tensor, target: torch.Tensor, weights: torch.Tensor = None):
        """
        Args:
            input: (B, #anchors, #codes) float tensor.
                Ecoded predicted locations of objects.
            target: (B, #anchors, #codes) float tensor.
                Regression targets.
            weights: (B, #anchors) float tensor if not None.

        Returns:
            loss: (B, #anchors) float tensor.
                Weighted smooth l1 loss without reduction.
        """
        # 如果target為nan,則等于input,否則等于target
        target = torch.where(torch.isnan(target), input, target)  # ignore nan targets# (batch_size, 321408, 7)

        diff = input - target  # (batch_size, 321408, 7)
        # code-wise weighting
        if self.code_weights is not None:
            diff = diff * self.code_weights.view(1, 1, -1)  #(batch_size, 321408, 7) 乘以box每一項的權重

        loss = self.smooth_l1_loss(diff, self.beta)

        # anchor-wise weighting
        if weights is not None:
            assert weights.shape[0] == loss.shape[0] and weights.shape[1] == loss.shape[1]
            # weights引數使用正anchor數目進行平均,使得每個樣本的損失與樣本中目標的數量無關
            loss = loss * weights.unsqueeze(-1)

        return loss

將第一階段的的得到的loss相加,就得到第一階段總的loss,

2、第二階段loss計算

第一階段的損失也包含了兩部分:

1. 對ROI與GT的3D IOU大于0.6的ROI計算分類loss

2. 對ROI與GT的3D IOU大于0.55的ROI計算回歸loss

2.1. 前景ROI置信度loss計算

第二階段的分類損失計算,用于預測前面ROI的類別置信度分數,每個proposal與之對應的GT, 其中IOU大于0.6為前景,數值為1 0.45-0.6忽略不計算loss,數值為-1 0.45為背景,數值為0,

因此,此處直接使用BCE損失計算置信度分數,

    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']
        """
        每個proposal與之對應的GT,
        其中IOU大于0.6為前景,數值為1 
        0.45-0.6忽略不計算loss,數值為-1 
        0.45為背景,數值為0
        rcnn_cls_labels shape (batch *128 ,)
        """
        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

2.2. 前景ROI box 回歸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

至此,PointRCNN的所有loss計算就完成了,下面看看推理的實作,

2、網路推理實作

2.1 預測結果生成

看回第二階段中roi精調的代碼,在預測階段,需要根據前面提出的roi和第二階段的精調結果生成最終的預測結果;分別是

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

注:在推理階段,batch size 默認為1,ROI的個數是100個,

代碼在:pcdet/models/roi_heads/pointrcnn_head.py

    def forward(self, batch_dict):
        """
        Args:
            batch_dict:

        Returns:

        """
        # 生成proposal;在訓練時,NMS保留512個結果,NMS_thresh為0.8;在測驗時,NMS保留100個結果,NMS_thresh為0.85
        targets_dict = self.proposal_layer(
            batch_dict, nms_config=self.model_cfg.NMS_CONFIG['TRAIN' if self.training else 'TEST']
        )
        # 在訓練模式時,需要為每個生成的proposal匹配到與之對應的GT_box
        if self.training:
            targets_dict = self.assign_targets(batch_dict)
        """

        略


        """
       
        # (total_rois, num_features, 1) --> (total_rois, 7)
        rcnn_reg = self.reg_layers(shared_features).transpose(1, 2).contiguous().squeeze(dim=1)  # (B, C)
        
        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

decode_torch完成預測結果和原ROI Box解碼, 這里的對角度的解碼直接將refine的預測結果與ROI的角度相加,因為他們的誤差在正負45度以內,

代碼在:pcdet/utils/box_coder_utils.py

# batch_cls_preds 每個ROI Box的置信度得分
# batch_box_preds 每個ROI Box的7個引數 (x,y,z,l,w,h,theta)

rotate_points_along_z為圍繞偏航角(yaw)的旋轉

代碼在pcdet/utils/common_utils.py

def rotate_points_along_z(points, angle):
    """
    Args:
        points: (B, N, 3 + C)
        angle: (B), angle along z-axis, angle increases x ==> y
    Returns:

    """
    # 首先利用torch.from_numpy().float將numpy轉化為torch
    points, is_numpy = check_numpy_to_torch(points)
    angle, _ = check_numpy_to_torch(angle)

    # 構造旋轉矩陣batch個
    cosa = torch.cos(angle)
    sina = torch.sin(angle)
    zeros = angle.new_zeros(points.shape[0])
    ones = angle.new_ones(points.shape[0])
    rot_matrix = torch.stack((
        cosa,  sina, zeros,
        -sina, cosa, zeros,
        zeros, zeros, ones
    ), dim=1).view(-1, 3, 3).float()
    # 對點云坐標進行旋轉
    points_rot = torch.matmul(points[:, :, 0:3], rot_matrix)
    # 將旋轉后的點云與原始點云拼接
    points_rot = torch.cat((points_rot, points[:, :, 3:]), dim=-1)
    # 將點云轉化為numpy格式,并回傳
    return points_rot.numpy() if is_numpy else points_rot

2.2. 后處理

后處理完成了最終100個ROI的NMS操作;同時需要注意的是,每個box的最終分類結果是由第一階段得出,第二階段的分類結果得到的是該類別屬于前景或背景的置信度得分;此處實作與FRCNN不同,需注意,

代碼在: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、PointRCNN的結果

PointRCNN原論文結果

PointRCNN在KITTI資料集測驗結果(結果僅顯示在kitti驗證集moderate精度)

4、消融實驗

待續

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/438064.html

標籤:AI

上一篇:PyCharm+Anaconda配置OpenCV4.4和PyQt5

下一篇:計算機視覺與深度學習第四章:全連接神經網路

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more