| | import os |
| |
|
| | import numpy as np |
| | import torch |
| |
|
| | from annotator.mmpkg.mmcv.utils import deprecated_api_warning |
| | from ..utils import ext_loader |
| |
|
| | ext_module = ext_loader.load_ext( |
| | '_ext', ['nms', 'softnms', 'nms_match', 'nms_rotated']) |
| |
|
| |
|
| | |
| | class NMSop(torch.autograd.Function): |
| |
|
| | @staticmethod |
| | def forward(ctx, bboxes, scores, iou_threshold, offset, score_threshold, |
| | max_num): |
| | is_filtering_by_score = score_threshold > 0 |
| | if is_filtering_by_score: |
| | valid_mask = scores > score_threshold |
| | bboxes, scores = bboxes[valid_mask], scores[valid_mask] |
| | valid_inds = torch.nonzero( |
| | valid_mask, as_tuple=False).squeeze(dim=1) |
| |
|
| | inds = ext_module.nms( |
| | bboxes, scores, iou_threshold=float(iou_threshold), offset=offset) |
| |
|
| | if max_num > 0: |
| | inds = inds[:max_num] |
| | if is_filtering_by_score: |
| | inds = valid_inds[inds] |
| | return inds |
| |
|
| | @staticmethod |
| | def symbolic(g, bboxes, scores, iou_threshold, offset, score_threshold, |
| | max_num): |
| | from ..onnx import is_custom_op_loaded |
| | has_custom_op = is_custom_op_loaded() |
| | |
| | is_trt_backend = os.environ.get('ONNX_BACKEND') == 'MMCVTensorRT' |
| | if has_custom_op and (not is_trt_backend): |
| | return g.op( |
| | 'mmcv::NonMaxSuppression', |
| | bboxes, |
| | scores, |
| | iou_threshold_f=float(iou_threshold), |
| | offset_i=int(offset)) |
| | else: |
| | from torch.onnx.symbolic_opset9 import select, squeeze, unsqueeze |
| | from ..onnx.onnx_utils.symbolic_helper import _size_helper |
| |
|
| | boxes = unsqueeze(g, bboxes, 0) |
| | scores = unsqueeze(g, unsqueeze(g, scores, 0), 0) |
| |
|
| | if max_num > 0: |
| | max_num = g.op( |
| | 'Constant', |
| | value_t=torch.tensor(max_num, dtype=torch.long)) |
| | else: |
| | dim = g.op('Constant', value_t=torch.tensor(0)) |
| | max_num = _size_helper(g, bboxes, dim) |
| | max_output_per_class = max_num |
| | iou_threshold = g.op( |
| | 'Constant', |
| | value_t=torch.tensor([iou_threshold], dtype=torch.float)) |
| | score_threshold = g.op( |
| | 'Constant', |
| | value_t=torch.tensor([score_threshold], dtype=torch.float)) |
| | nms_out = g.op('NonMaxSuppression', boxes, scores, |
| | max_output_per_class, iou_threshold, |
| | score_threshold) |
| | return squeeze( |
| | g, |
| | select( |
| | g, nms_out, 1, |
| | g.op( |
| | 'Constant', |
| | value_t=torch.tensor([2], dtype=torch.long))), 1) |
| |
|
| |
|
| | class SoftNMSop(torch.autograd.Function): |
| |
|
| | @staticmethod |
| | def forward(ctx, boxes, scores, iou_threshold, sigma, min_score, method, |
| | offset): |
| | dets = boxes.new_empty((boxes.size(0), 5), device='cpu') |
| | inds = ext_module.softnms( |
| | boxes.cpu(), |
| | scores.cpu(), |
| | dets.cpu(), |
| | iou_threshold=float(iou_threshold), |
| | sigma=float(sigma), |
| | min_score=float(min_score), |
| | method=int(method), |
| | offset=int(offset)) |
| | return dets, inds |
| |
|
| | @staticmethod |
| | def symbolic(g, boxes, scores, iou_threshold, sigma, min_score, method, |
| | offset): |
| | from packaging import version |
| | assert version.parse(torch.__version__) >= version.parse('1.7.0') |
| | nms_out = g.op( |
| | 'mmcv::SoftNonMaxSuppression', |
| | boxes, |
| | scores, |
| | iou_threshold_f=float(iou_threshold), |
| | sigma_f=float(sigma), |
| | min_score_f=float(min_score), |
| | method_i=int(method), |
| | offset_i=int(offset), |
| | outputs=2) |
| | return nms_out |
| |
|
| |
|
| | @deprecated_api_warning({'iou_thr': 'iou_threshold'}) |
| | def nms(boxes, scores, iou_threshold, offset=0, score_threshold=0, max_num=-1): |
| | """Dispatch to either CPU or GPU NMS implementations. |
| | |
| | The input can be either torch tensor or numpy array. GPU NMS will be used |
| | if the input is gpu tensor, otherwise CPU NMS |
| | will be used. The returned type will always be the same as inputs. |
| | |
| | Arguments: |
| | boxes (torch.Tensor or np.ndarray): boxes in shape (N, 4). |
| | scores (torch.Tensor or np.ndarray): scores in shape (N, ). |
| | iou_threshold (float): IoU threshold for NMS. |
| | offset (int, 0 or 1): boxes' width or height is (x2 - x1 + offset). |
| | score_threshold (float): score threshold for NMS. |
| | max_num (int): maximum number of boxes after NMS. |
| | |
| | Returns: |
| | tuple: kept dets(boxes and scores) and indice, which is always the \ |
| | same data type as the input. |
| | |
| | Example: |
| | >>> boxes = np.array([[49.1, 32.4, 51.0, 35.9], |
| | >>> [49.3, 32.9, 51.0, 35.3], |
| | >>> [49.2, 31.8, 51.0, 35.4], |
| | >>> [35.1, 11.5, 39.1, 15.7], |
| | >>> [35.6, 11.8, 39.3, 14.2], |
| | >>> [35.3, 11.5, 39.9, 14.5], |
| | >>> [35.2, 11.7, 39.7, 15.7]], dtype=np.float32) |
| | >>> scores = np.array([0.9, 0.9, 0.5, 0.5, 0.5, 0.4, 0.3],\ |
| | dtype=np.float32) |
| | >>> iou_threshold = 0.6 |
| | >>> dets, inds = nms(boxes, scores, iou_threshold) |
| | >>> assert len(inds) == len(dets) == 3 |
| | """ |
| | assert isinstance(boxes, (torch.Tensor, np.ndarray)) |
| | assert isinstance(scores, (torch.Tensor, np.ndarray)) |
| | is_numpy = False |
| | if isinstance(boxes, np.ndarray): |
| | is_numpy = True |
| | boxes = torch.from_numpy(boxes) |
| | if isinstance(scores, np.ndarray): |
| | scores = torch.from_numpy(scores) |
| | assert boxes.size(1) == 4 |
| | assert boxes.size(0) == scores.size(0) |
| | assert offset in (0, 1) |
| |
|
| | if torch.__version__ == 'parrots': |
| | indata_list = [boxes, scores] |
| | indata_dict = { |
| | 'iou_threshold': float(iou_threshold), |
| | 'offset': int(offset) |
| | } |
| | inds = ext_module.nms(*indata_list, **indata_dict) |
| | else: |
| | inds = NMSop.apply(boxes, scores, iou_threshold, offset, |
| | score_threshold, max_num) |
| | dets = torch.cat((boxes[inds], scores[inds].reshape(-1, 1)), dim=1) |
| | if is_numpy: |
| | dets = dets.cpu().numpy() |
| | inds = inds.cpu().numpy() |
| | return dets, inds |
| |
|
| |
|
| | @deprecated_api_warning({'iou_thr': 'iou_threshold'}) |
| | def soft_nms(boxes, |
| | scores, |
| | iou_threshold=0.3, |
| | sigma=0.5, |
| | min_score=1e-3, |
| | method='linear', |
| | offset=0): |
| | """Dispatch to only CPU Soft NMS implementations. |
| | |
| | The input can be either a torch tensor or numpy array. |
| | The returned type will always be the same as inputs. |
| | |
| | Arguments: |
| | boxes (torch.Tensor or np.ndarray): boxes in shape (N, 4). |
| | scores (torch.Tensor or np.ndarray): scores in shape (N, ). |
| | iou_threshold (float): IoU threshold for NMS. |
| | sigma (float): hyperparameter for gaussian method |
| | min_score (float): score filter threshold |
| | method (str): either 'linear' or 'gaussian' |
| | offset (int, 0 or 1): boxes' width or height is (x2 - x1 + offset). |
| | |
| | Returns: |
| | tuple: kept dets(boxes and scores) and indice, which is always the \ |
| | same data type as the input. |
| | |
| | Example: |
| | >>> boxes = np.array([[4., 3., 5., 3.], |
| | >>> [4., 3., 5., 4.], |
| | >>> [3., 1., 3., 1.], |
| | >>> [3., 1., 3., 1.], |
| | >>> [3., 1., 3., 1.], |
| | >>> [3., 1., 3., 1.]], dtype=np.float32) |
| | >>> scores = np.array([0.9, 0.9, 0.5, 0.5, 0.4, 0.0], dtype=np.float32) |
| | >>> iou_threshold = 0.6 |
| | >>> dets, inds = soft_nms(boxes, scores, iou_threshold, sigma=0.5) |
| | >>> assert len(inds) == len(dets) == 5 |
| | """ |
| |
|
| | assert isinstance(boxes, (torch.Tensor, np.ndarray)) |
| | assert isinstance(scores, (torch.Tensor, np.ndarray)) |
| | is_numpy = False |
| | if isinstance(boxes, np.ndarray): |
| | is_numpy = True |
| | boxes = torch.from_numpy(boxes) |
| | if isinstance(scores, np.ndarray): |
| | scores = torch.from_numpy(scores) |
| | assert boxes.size(1) == 4 |
| | assert boxes.size(0) == scores.size(0) |
| | assert offset in (0, 1) |
| | method_dict = {'naive': 0, 'linear': 1, 'gaussian': 2} |
| | assert method in method_dict.keys() |
| |
|
| | if torch.__version__ == 'parrots': |
| | dets = boxes.new_empty((boxes.size(0), 5), device='cpu') |
| | indata_list = [boxes.cpu(), scores.cpu(), dets.cpu()] |
| | indata_dict = { |
| | 'iou_threshold': float(iou_threshold), |
| | 'sigma': float(sigma), |
| | 'min_score': min_score, |
| | 'method': method_dict[method], |
| | 'offset': int(offset) |
| | } |
| | inds = ext_module.softnms(*indata_list, **indata_dict) |
| | else: |
| | dets, inds = SoftNMSop.apply(boxes.cpu(), scores.cpu(), |
| | float(iou_threshold), float(sigma), |
| | float(min_score), method_dict[method], |
| | int(offset)) |
| |
|
| | dets = dets[:inds.size(0)] |
| |
|
| | if is_numpy: |
| | dets = dets.cpu().numpy() |
| | inds = inds.cpu().numpy() |
| | return dets, inds |
| | else: |
| | return dets.to(device=boxes.device), inds.to(device=boxes.device) |
| |
|
| |
|
| | def batched_nms(boxes, scores, idxs, nms_cfg, class_agnostic=False): |
| | """Performs non-maximum suppression in a batched fashion. |
| | |
| | Modified from https://github.com/pytorch/vision/blob |
| | /505cd6957711af790211896d32b40291bea1bc21/torchvision/ops/boxes.py#L39. |
| | In order to perform NMS independently per class, we add an offset to all |
| | the boxes. The offset is dependent only on the class idx, and is large |
| | enough so that boxes from different classes do not overlap. |
| | |
| | Arguments: |
| | boxes (torch.Tensor): boxes in shape (N, 4). |
| | scores (torch.Tensor): scores in shape (N, ). |
| | idxs (torch.Tensor): each index value correspond to a bbox cluster, |
| | and NMS will not be applied between elements of different idxs, |
| | shape (N, ). |
| | nms_cfg (dict): specify nms type and other parameters like iou_thr. |
| | Possible keys includes the following. |
| | |
| | - iou_thr (float): IoU threshold used for NMS. |
| | - split_thr (float): threshold number of boxes. In some cases the |
| | number of boxes is large (e.g., 200k). To avoid OOM during |
| | training, the users could set `split_thr` to a small value. |
| | If the number of boxes is greater than the threshold, it will |
| | perform NMS on each group of boxes separately and sequentially. |
| | Defaults to 10000. |
| | class_agnostic (bool): if true, nms is class agnostic, |
| | i.e. IoU thresholding happens over all boxes, |
| | regardless of the predicted class. |
| | |
| | Returns: |
| | tuple: kept dets and indice. |
| | """ |
| | nms_cfg_ = nms_cfg.copy() |
| | class_agnostic = nms_cfg_.pop('class_agnostic', class_agnostic) |
| | if class_agnostic: |
| | boxes_for_nms = boxes |
| | else: |
| | max_coordinate = boxes.max() |
| | offsets = idxs.to(boxes) * (max_coordinate + torch.tensor(1).to(boxes)) |
| | boxes_for_nms = boxes + offsets[:, None] |
| |
|
| | nms_type = nms_cfg_.pop('type', 'nms') |
| | nms_op = eval(nms_type) |
| |
|
| | split_thr = nms_cfg_.pop('split_thr', 10000) |
| | |
| | if boxes_for_nms.shape[0] < split_thr or torch.onnx.is_in_onnx_export(): |
| | dets, keep = nms_op(boxes_for_nms, scores, **nms_cfg_) |
| | boxes = boxes[keep] |
| | |
| | |
| | |
| | |
| | |
| | scores = dets[:, 4] |
| | else: |
| | max_num = nms_cfg_.pop('max_num', -1) |
| | total_mask = scores.new_zeros(scores.size(), dtype=torch.bool) |
| | |
| | scores_after_nms = scores.new_zeros(scores.size()) |
| | for id in torch.unique(idxs): |
| | mask = (idxs == id).nonzero(as_tuple=False).view(-1) |
| | dets, keep = nms_op(boxes_for_nms[mask], scores[mask], **nms_cfg_) |
| | total_mask[mask[keep]] = True |
| | scores_after_nms[mask[keep]] = dets[:, -1] |
| | keep = total_mask.nonzero(as_tuple=False).view(-1) |
| |
|
| | scores, inds = scores_after_nms[keep].sort(descending=True) |
| | keep = keep[inds] |
| | boxes = boxes[keep] |
| |
|
| | if max_num > 0: |
| | keep = keep[:max_num] |
| | boxes = boxes[:max_num] |
| | scores = scores[:max_num] |
| |
|
| | return torch.cat([boxes, scores[:, None]], -1), keep |
| |
|
| |
|
| | def nms_match(dets, iou_threshold): |
| | """Matched dets into different groups by NMS. |
| | |
| | NMS match is Similar to NMS but when a bbox is suppressed, nms match will |
| | record the indice of suppressed bbox and form a group with the indice of |
| | kept bbox. In each group, indice is sorted as score order. |
| | |
| | Arguments: |
| | dets (torch.Tensor | np.ndarray): Det boxes with scores, shape (N, 5). |
| | iou_thr (float): IoU thresh for NMS. |
| | |
| | Returns: |
| | List[torch.Tensor | np.ndarray]: The outer list corresponds different |
| | matched group, the inner Tensor corresponds the indices for a group |
| | in score order. |
| | """ |
| | if dets.shape[0] == 0: |
| | matched = [] |
| | else: |
| | assert dets.shape[-1] == 5, 'inputs dets.shape should be (N, 5), ' \ |
| | f'but get {dets.shape}' |
| | if isinstance(dets, torch.Tensor): |
| | dets_t = dets.detach().cpu() |
| | else: |
| | dets_t = torch.from_numpy(dets) |
| | indata_list = [dets_t] |
| | indata_dict = {'iou_threshold': float(iou_threshold)} |
| | matched = ext_module.nms_match(*indata_list, **indata_dict) |
| | if torch.__version__ == 'parrots': |
| | matched = matched.tolist() |
| |
|
| | if isinstance(dets, torch.Tensor): |
| | return [dets.new_tensor(m, dtype=torch.long) for m in matched] |
| | else: |
| | return [np.array(m, dtype=np.int) for m in matched] |
| |
|
| |
|
| | def nms_rotated(dets, scores, iou_threshold, labels=None): |
| | """Performs non-maximum suppression (NMS) on the rotated boxes according to |
| | their intersection-over-union (IoU). |
| | |
| | Rotated NMS iteratively removes lower scoring rotated boxes which have an |
| | IoU greater than iou_threshold with another (higher scoring) rotated box. |
| | |
| | Args: |
| | boxes (Tensor): Rotated boxes in shape (N, 5). They are expected to \ |
| | be in (x_ctr, y_ctr, width, height, angle_radian) format. |
| | scores (Tensor): scores in shape (N, ). |
| | iou_threshold (float): IoU thresh for NMS. |
| | labels (Tensor): boxes' label in shape (N,). |
| | |
| | Returns: |
| | tuple: kept dets(boxes and scores) and indice, which is always the \ |
| | same data type as the input. |
| | """ |
| | if dets.shape[0] == 0: |
| | return dets, None |
| | multi_label = labels is not None |
| | if multi_label: |
| | dets_wl = torch.cat((dets, labels.unsqueeze(1)), 1) |
| | else: |
| | dets_wl = dets |
| | _, order = scores.sort(0, descending=True) |
| | dets_sorted = dets_wl.index_select(0, order) |
| |
|
| | if torch.__version__ == 'parrots': |
| | keep_inds = ext_module.nms_rotated( |
| | dets_wl, |
| | scores, |
| | order, |
| | dets_sorted, |
| | iou_threshold=iou_threshold, |
| | multi_label=multi_label) |
| | else: |
| | keep_inds = ext_module.nms_rotated(dets_wl, scores, order, dets_sorted, |
| | iou_threshold, multi_label) |
| | dets = torch.cat((dets[keep_inds], scores[keep_inds].reshape(-1, 1)), |
| | dim=1) |
| | return dets, keep_inds |
| |
|