|
import os
|
|
|
|
import numpy as np
|
|
import torch
|
|
|
|
from annotator.uniformer.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
|
|
|