box3d_nms.py 10.6 KB
Newer Older
dingchang's avatar
dingchang committed
1
# Copyright (c) OpenMMLab. All rights reserved.
2
3
from typing import Optional, Tuple

4
5
import numba
import numpy as np
zhangwenwei's avatar
zhangwenwei committed
6
import torch
7
from mmcv.ops import nms, nms_rotated
8
9
10
11
12
13
14
15
16
17
18
19
20
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]:
21
22
    """Multi-class NMS for 3D boxes. The IoU used for NMS is defined as the 2D
    IoU between BEV boxes.
zhangwenwei's avatar
zhangwenwei committed
23
24

    Args:
25
        mlvl_bboxes (Tensor): Multi-level boxes with shape (N, M).
zhangwenwei's avatar
zhangwenwei committed
26
            M is the dimensions of boxes.
27
28
        mlvl_bboxes_for_nms (Tensor): Multi-level boxes with shape (N, 5)
            ([x1, y1, x2, y2, ry]). N is the number of boxes.
29
            The coordinate system of the BEV boxes is counterclockwise.
30
31
32
        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.
zhangwenwei's avatar
zhangwenwei committed
33
        max_num (int): Maximum number of boxes will be kept.
34
        cfg (dict): Configuration dict of NMS.
35
36
37
38
39
40
        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.
zhangwenwei's avatar
zhangwenwei committed
41
42

    Returns:
43
44
45
        Tuple[Tensor]: Return results after nms, including 3D bounding boxes,
        scores, labels, direction scores, attribute scores (optional) and
        2D bounding boxes (optional).
zhangwenwei's avatar
zhangwenwei committed
46
    """
zhangwenwei's avatar
zhangwenwei committed
47
48
49
50
51
52
53
    # do multi class nms
    # the fg class id range: [0, num_classes-1]
    num_classes = mlvl_scores.shape[1] - 1
    bboxes = []
    scores = []
    labels = []
    dir_scores = []
54
55
    attr_scores = []
    bboxes2d = []
zhangwenwei's avatar
zhangwenwei committed
56
57
58
59
60
61
62
63
64
65
    for i in range(0, num_classes):
        # get bboxes and scores of this class
        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:
66
            nms_func = nms_bev
zhangwenwei's avatar
zhangwenwei committed
67
        else:
68
            nms_func = nms_normal_bev
zhangwenwei's avatar
zhangwenwei committed
69
70
71
72
73
74
75
76
77
78
79
80
81

        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])
82
83
84
85
86
87
        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])
zhangwenwei's avatar
zhangwenwei committed
88
89
90
91
92
93
94

    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)
95
96
97
98
        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)
zhangwenwei's avatar
zhangwenwei committed
99
100
101
102
103
104
105
106
        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]
107
108
109
110
            if mlvl_attr_scores is not None:
                attr_scores = attr_scores[inds]
            if mlvl_bboxes2d is not None:
                bboxes2d = bboxes2d[inds]
zhangwenwei's avatar
zhangwenwei committed
111
112
113
    else:
        bboxes = mlvl_scores.new_zeros((0, mlvl_bboxes.size(-1)))
        scores = mlvl_scores.new_zeros((0, ))
zhangwenwei's avatar
zhangwenwei committed
114
        labels = mlvl_scores.new_zeros((0, ), dtype=torch.long)
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
        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
wuyuefeng's avatar
Votenet  
wuyuefeng committed
132
133


134
135
def aligned_3d_nms(boxes: Tensor, scores: Tensor, classes: Tensor,
                   thresh: float) -> Tensor:
136
    """3D NMS for aligned boxes.
wuyuefeng's avatar
Votenet  
wuyuefeng committed
137
138

    Args:
139
140
141
        boxes (Tensor): Aligned box with shape [N, 6].
        scores (Tensor): Scores of each box.
        classes (Tensor): Class of each box.
142
        thresh (float): IoU threshold for nms.
wuyuefeng's avatar
Votenet  
wuyuefeng committed
143
144

    Returns:
145
        Tensor: Indices of selected boxes.
wuyuefeng's avatar
Votenet  
wuyuefeng committed
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
    """
    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()
178
179
        score_sorted = score_sorted[torch.nonzero(
            iou <= thresh, as_tuple=False).flatten()]
wuyuefeng's avatar
Votenet  
wuyuefeng committed
180
181
182

    indices = boxes.new_tensor(pick, dtype=torch.long)
    return indices
183
184
185


@numba.jit(nopython=True)
186
def circle_nms(dets: Tensor, thresh: float, post_max_size: int = 83) -> Tensor:
187
188
    """Circular NMS.

189
190
    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.
191
192

    Args:
193
        dets (Tensor): Detection results with the shape of [N, 3].
194
        thresh (float): Value of threshold.
195
        post_max_size (int): Max number of prediction to be kept.
196
            Defaults to 83.
197
198

    Returns:
199
        Tensor: Indexes of the detections to be kept.
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
    """
    x1 = dets[:, 0]
    y1 = dets[:, 1]
    scores = dets[:, 2]
    order = scores.argsort()[::-1].astype(np.int32)  # highest->lowest
    ndets = dets.shape[0]
    suppressed = np.zeros((ndets), dtype=np.int32)
    keep = []
    for _i in range(ndets):
        i = order[_i]  # start with highest score box
        if suppressed[
                i] == 1:  # if any box have enough iou with this, remove it
            continue
        keep.append(i)
        for _j in range(_i + 1, ndets):
            j = order[_j]
            if suppressed[j] == 1:
                continue
            # calculate center distance between i and j box
            dist = (x1[i] - x1[j])**2 + (y1[i] - y1[j])**2

            # ovr = inter / areas[j]
            if dist <= thresh:
                suppressed[j] = 1
224
225
226
227
228

    if post_max_size < len(keep):
        return keep[:post_max_size]

    return keep
229
230
231
232
233


# This function duplicates functionality of mmcv.ops.iou_3d.nms_bev
# from mmcv<=1.5, but using cuda ops from mmcv.ops.nms.nms_rotated.
# Nms api will be unified in mmdetection3d one day.
234
235
236
237
238
def nms_bev(boxes: Tensor,
            scores: Tensor,
            thresh: float,
            pre_max_size: Optional[int] = None,
            post_max_size: Optional[int] = None) -> Tensor:
239
240
241
242
243
244
    """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:
245
        boxes (Tensor): Input boxes with the shape of [N, 5]
246
            ([x1, y1, x2, y2, ry]).
247
        scores (Tensor): Scores of boxes with the shape of [N].
248
249
        thresh (float): Overlap threshold of NMS.
        pre_max_size (int, optional): Max size of boxes before NMS.
250
            Defaults to None.
251
        post_max_size (int, optional): Max size of boxes after NMS.
252
            Defaults to None.
253
254

    Returns:
255
        Tensor: Indexes after NMS.
256
257
258
259
260
261
    """
    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()
262
263
    scores = scores[order]

264
265
266
267
268
269
270
271
    # xyxyr -> back to xywhr
    # note: better skip this step before nms_bev call in the future
    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]
272
    keep = order[keep]
273
274
275
276
277
278
279
280
    if post_max_size is not None:
        keep = keep[:post_max_size]
    return keep


# This function duplicates functionality of mmcv.ops.iou_3d.nms_normal_bev
# from mmcv<=1.5, but using cuda ops from mmcv.ops.nms.nms.
# Nms api will be unified in mmdetection3d one day.
281
def nms_normal_bev(boxes: Tensor, scores: Tensor, thresh: float) -> Tensor:
282
283
284
285
286
    """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:
287
288
        boxes (Tensor): Input boxes with shape (N, 5).
        scores (Tensor): Scores of predicted boxes with shape (N).
289
290
291
        thresh (float): Overlap threshold of NMS.

    Returns:
292
        Tensor: Remaining indices with scores in descending order.
293
294
    """
    assert boxes.shape[1] == 5, 'Input boxes shape should be [N, 5]'
295
    return nms(boxes[:, :-1], scores, thresh)[1]