|
|
|
|
|
from typing import Optional, Tuple |
|
|
|
|
|
import numba |
|
|
import numpy as np |
|
|
import torch |
|
|
from mmcv.ops import nms, nms_rotated |
|
|
from torch import Tensor |
|
|
|
|
|
|
|
|
def box3d_multiclass_nms( |
|
|
mlvl_bboxes: Tensor, |
|
|
mlvl_bboxes_for_nms: Tensor, |
|
|
mlvl_scores: Tensor, |
|
|
score_thr: float, |
|
|
max_num: int, |
|
|
cfg: dict, |
|
|
mlvl_dir_scores: Optional[Tensor] = None, |
|
|
mlvl_attr_scores: Optional[Tensor] = None, |
|
|
mlvl_bboxes2d: Optional[Tensor] = None) -> Tuple[Tensor]: |
|
|
"""Multi-class NMS for 3D boxes. The IoU used for NMS is defined as the 2D |
|
|
IoU between BEV boxes. |
|
|
|
|
|
Args: |
|
|
mlvl_bboxes (Tensor): Multi-level boxes with shape (N, M). |
|
|
M is the dimensions of boxes. |
|
|
mlvl_bboxes_for_nms (Tensor): Multi-level boxes with shape (N, 5) |
|
|
([x1, y1, x2, y2, ry]). N is the number of boxes. |
|
|
The coordinate system of the BEV boxes is counterclockwise. |
|
|
mlvl_scores (Tensor): Multi-level boxes with shape (N, C + 1). |
|
|
N is the number of boxes. C is the number of classes. |
|
|
score_thr (float): Score threshold to filter boxes with low confidence. |
|
|
max_num (int): Maximum number of boxes will be kept. |
|
|
cfg (dict): Configuration dict of NMS. |
|
|
mlvl_dir_scores (Tensor, optional): Multi-level scores of direction |
|
|
classifier. Defaults to None. |
|
|
mlvl_attr_scores (Tensor, optional): Multi-level scores of attribute |
|
|
classifier. Defaults to None. |
|
|
mlvl_bboxes2d (Tensor, optional): Multi-level 2D bounding boxes. |
|
|
Defaults to None. |
|
|
|
|
|
Returns: |
|
|
Tuple[Tensor]: Return results after nms, including 3D bounding boxes, |
|
|
scores, labels, direction scores, attribute scores (optional) and |
|
|
2D bounding boxes (optional). |
|
|
""" |
|
|
|
|
|
|
|
|
num_classes = mlvl_scores.shape[1] - 1 |
|
|
bboxes = [] |
|
|
scores = [] |
|
|
labels = [] |
|
|
dir_scores = [] |
|
|
attr_scores = [] |
|
|
bboxes2d = [] |
|
|
for i in range(0, num_classes): |
|
|
|
|
|
cls_inds = mlvl_scores[:, i] > score_thr |
|
|
if not cls_inds.any(): |
|
|
continue |
|
|
|
|
|
_scores = mlvl_scores[cls_inds, i] |
|
|
_bboxes_for_nms = mlvl_bboxes_for_nms[cls_inds, :] |
|
|
|
|
|
if cfg.use_rotate_nms: |
|
|
nms_func = nms_bev |
|
|
else: |
|
|
nms_func = nms_normal_bev |
|
|
|
|
|
selected = nms_func(_bboxes_for_nms, _scores, cfg.nms_thr) |
|
|
_mlvl_bboxes = mlvl_bboxes[cls_inds, :] |
|
|
bboxes.append(_mlvl_bboxes[selected]) |
|
|
scores.append(_scores[selected]) |
|
|
cls_label = mlvl_bboxes.new_full((len(selected), ), |
|
|
i, |
|
|
dtype=torch.long) |
|
|
labels.append(cls_label) |
|
|
|
|
|
if mlvl_dir_scores is not None: |
|
|
_mlvl_dir_scores = mlvl_dir_scores[cls_inds] |
|
|
dir_scores.append(_mlvl_dir_scores[selected]) |
|
|
if mlvl_attr_scores is not None: |
|
|
_mlvl_attr_scores = mlvl_attr_scores[cls_inds] |
|
|
attr_scores.append(_mlvl_attr_scores[selected]) |
|
|
if mlvl_bboxes2d is not None: |
|
|
_mlvl_bboxes2d = mlvl_bboxes2d[cls_inds] |
|
|
bboxes2d.append(_mlvl_bboxes2d[selected]) |
|
|
|
|
|
if bboxes: |
|
|
bboxes = torch.cat(bboxes, dim=0) |
|
|
scores = torch.cat(scores, dim=0) |
|
|
labels = torch.cat(labels, dim=0) |
|
|
if mlvl_dir_scores is not None: |
|
|
dir_scores = torch.cat(dir_scores, dim=0) |
|
|
if mlvl_attr_scores is not None: |
|
|
attr_scores = torch.cat(attr_scores, dim=0) |
|
|
if mlvl_bboxes2d is not None: |
|
|
bboxes2d = torch.cat(bboxes2d, dim=0) |
|
|
if bboxes.shape[0] > max_num: |
|
|
_, inds = scores.sort(descending=True) |
|
|
inds = inds[:max_num] |
|
|
bboxes = bboxes[inds, :] |
|
|
labels = labels[inds] |
|
|
scores = scores[inds] |
|
|
if mlvl_dir_scores is not None: |
|
|
dir_scores = dir_scores[inds] |
|
|
if mlvl_attr_scores is not None: |
|
|
attr_scores = attr_scores[inds] |
|
|
if mlvl_bboxes2d is not None: |
|
|
bboxes2d = bboxes2d[inds] |
|
|
else: |
|
|
bboxes = mlvl_scores.new_zeros((0, mlvl_bboxes.size(-1))) |
|
|
scores = mlvl_scores.new_zeros((0, )) |
|
|
labels = mlvl_scores.new_zeros((0, ), dtype=torch.long) |
|
|
if mlvl_dir_scores is not None: |
|
|
dir_scores = mlvl_scores.new_zeros((0, )) |
|
|
if mlvl_attr_scores is not None: |
|
|
attr_scores = mlvl_scores.new_zeros((0, )) |
|
|
if mlvl_bboxes2d is not None: |
|
|
bboxes2d = mlvl_scores.new_zeros((0, 4)) |
|
|
|
|
|
results = (bboxes, scores, labels) |
|
|
|
|
|
if mlvl_dir_scores is not None: |
|
|
results = results + (dir_scores, ) |
|
|
if mlvl_attr_scores is not None: |
|
|
results = results + (attr_scores, ) |
|
|
if mlvl_bboxes2d is not None: |
|
|
results = results + (bboxes2d, ) |
|
|
|
|
|
return results |
|
|
|
|
|
|
|
|
def aligned_3d_nms(boxes: Tensor, scores: Tensor, classes: Tensor, |
|
|
thresh: float) -> Tensor: |
|
|
"""3D NMS for aligned boxes. |
|
|
|
|
|
Args: |
|
|
boxes (Tensor): Aligned box with shape [N, 6]. |
|
|
scores (Tensor): Scores of each box. |
|
|
classes (Tensor): Class of each box. |
|
|
thresh (float): IoU threshold for nms. |
|
|
|
|
|
Returns: |
|
|
Tensor: Indices of selected boxes. |
|
|
""" |
|
|
x1 = boxes[:, 0] |
|
|
y1 = boxes[:, 1] |
|
|
z1 = boxes[:, 2] |
|
|
x2 = boxes[:, 3] |
|
|
y2 = boxes[:, 4] |
|
|
z2 = boxes[:, 5] |
|
|
area = (x2 - x1) * (y2 - y1) * (z2 - z1) |
|
|
zero = boxes.new_zeros(1, ) |
|
|
|
|
|
score_sorted = torch.argsort(scores) |
|
|
pick = [] |
|
|
while (score_sorted.shape[0] != 0): |
|
|
last = score_sorted.shape[0] |
|
|
i = score_sorted[-1] |
|
|
pick.append(i) |
|
|
|
|
|
xx1 = torch.max(x1[i], x1[score_sorted[:last - 1]]) |
|
|
yy1 = torch.max(y1[i], y1[score_sorted[:last - 1]]) |
|
|
zz1 = torch.max(z1[i], z1[score_sorted[:last - 1]]) |
|
|
xx2 = torch.min(x2[i], x2[score_sorted[:last - 1]]) |
|
|
yy2 = torch.min(y2[i], y2[score_sorted[:last - 1]]) |
|
|
zz2 = torch.min(z2[i], z2[score_sorted[:last - 1]]) |
|
|
classes1 = classes[i] |
|
|
classes2 = classes[score_sorted[:last - 1]] |
|
|
inter_l = torch.max(zero, xx2 - xx1) |
|
|
inter_w = torch.max(zero, yy2 - yy1) |
|
|
inter_h = torch.max(zero, zz2 - zz1) |
|
|
|
|
|
inter = inter_l * inter_w * inter_h |
|
|
iou = inter / (area[i] + area[score_sorted[:last - 1]] - inter) |
|
|
iou = iou * (classes1 == classes2).float() |
|
|
score_sorted = score_sorted[torch.nonzero( |
|
|
iou <= thresh, as_tuple=False).flatten()] |
|
|
|
|
|
indices = boxes.new_tensor(pick, dtype=torch.long) |
|
|
return indices |
|
|
|
|
|
|
|
|
@numba.jit(nopython=True) |
|
|
def circle_nms(dets: Tensor, thresh: float, post_max_size: int = 83) -> Tensor: |
|
|
"""Circular NMS. |
|
|
|
|
|
An object is only counted as positive if no other center with a higher |
|
|
confidence exists within a radius r using a bird-eye view distance metric. |
|
|
|
|
|
Args: |
|
|
dets (Tensor): Detection results with the shape of [N, 3]. |
|
|
thresh (float): Value of threshold. |
|
|
post_max_size (int): Max number of prediction to be kept. |
|
|
Defaults to 83. |
|
|
|
|
|
Returns: |
|
|
Tensor: Indexes of the detections to be kept. |
|
|
""" |
|
|
x1 = dets[:, 0] |
|
|
y1 = dets[:, 1] |
|
|
scores = dets[:, 2] |
|
|
order = scores.argsort()[::-1].astype(np.int32) |
|
|
ndets = dets.shape[0] |
|
|
suppressed = np.zeros((ndets), dtype=np.int32) |
|
|
keep = [] |
|
|
for _i in range(ndets): |
|
|
i = order[_i] |
|
|
if suppressed[ |
|
|
i] == 1: |
|
|
continue |
|
|
keep.append(i) |
|
|
for _j in range(_i + 1, ndets): |
|
|
j = order[_j] |
|
|
if suppressed[j] == 1: |
|
|
continue |
|
|
|
|
|
dist = (x1[i] - x1[j])**2 + (y1[i] - y1[j])**2 |
|
|
|
|
|
|
|
|
if dist <= thresh: |
|
|
suppressed[j] = 1 |
|
|
|
|
|
if post_max_size < len(keep): |
|
|
return keep[:post_max_size] |
|
|
|
|
|
return keep |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def nms_bev(boxes: Tensor, |
|
|
scores: Tensor, |
|
|
thresh: float, |
|
|
pre_max_size: Optional[int] = None, |
|
|
post_max_size: Optional[int] = None) -> Tensor: |
|
|
"""NMS function GPU implementation (for BEV boxes). The overlap of two |
|
|
boxes for IoU calculation is defined as the exact overlapping area of the |
|
|
two boxes. In this function, one can also set ``pre_max_size`` and |
|
|
``post_max_size``. |
|
|
|
|
|
Args: |
|
|
boxes (Tensor): Input boxes with the shape of [N, 5] |
|
|
([x1, y1, x2, y2, ry]). |
|
|
scores (Tensor): Scores of boxes with the shape of [N]. |
|
|
thresh (float): Overlap threshold of NMS. |
|
|
pre_max_size (int, optional): Max size of boxes before NMS. |
|
|
Defaults to None. |
|
|
post_max_size (int, optional): Max size of boxes after NMS. |
|
|
Defaults to None. |
|
|
|
|
|
Returns: |
|
|
Tensor: Indexes after NMS. |
|
|
""" |
|
|
assert boxes.size(1) == 5, 'Input boxes shape should be [N, 5]' |
|
|
order = scores.sort(0, descending=True)[1] |
|
|
if pre_max_size is not None: |
|
|
order = order[:pre_max_size] |
|
|
boxes = boxes[order].contiguous() |
|
|
scores = scores[order] |
|
|
|
|
|
|
|
|
|
|
|
boxes = torch.stack( |
|
|
((boxes[:, 0] + boxes[:, 2]) / 2, (boxes[:, 1] + boxes[:, 3]) / 2, |
|
|
boxes[:, 2] - boxes[:, 0], boxes[:, 3] - boxes[:, 1], boxes[:, 4]), |
|
|
dim=-1) |
|
|
|
|
|
keep = nms_rotated(boxes, scores, thresh)[1] |
|
|
keep = order[keep] |
|
|
if post_max_size is not None: |
|
|
keep = keep[:post_max_size] |
|
|
return keep |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def nms_normal_bev(boxes: Tensor, scores: Tensor, thresh: float) -> Tensor: |
|
|
"""Normal NMS function GPU implementation (for BEV boxes). The overlap of |
|
|
two boxes for IoU calculation is defined as the exact overlapping area of |
|
|
the two boxes WITH their yaw angle set to 0. |
|
|
|
|
|
Args: |
|
|
boxes (Tensor): Input boxes with shape (N, 5). |
|
|
scores (Tensor): Scores of predicted boxes with shape (N). |
|
|
thresh (float): Overlap threshold of NMS. |
|
|
|
|
|
Returns: |
|
|
Tensor: Remaining indices with scores in descending order. |
|
|
""" |
|
|
assert boxes.shape[1] == 5, 'Input boxes shape should be [N, 5]' |
|
|
return nms(boxes[:, :-1], scores, thresh)[1] |
|
|
|