在我前面的文章中,已經完成了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
