|
|
|
__author__ = 'ZFTurbo: https://kaggle.com/zfturbo' |
|
|
|
import numpy as np |
|
from numba import jit |
|
|
|
|
|
def prepare_boxes(boxes, scores, labels): |
|
result_boxes = boxes.copy() |
|
|
|
cond = (result_boxes < 0) |
|
cond_sum = cond.astype(np.int32).sum() |
|
if cond_sum > 0: |
|
print('Warning. Fixed {} boxes coordinates < 0'.format(cond_sum)) |
|
result_boxes[cond] = 0 |
|
|
|
cond = (result_boxes > 1) |
|
cond_sum = cond.astype(np.int32).sum() |
|
if cond_sum > 0: |
|
print('Warning. Fixed {} boxes coordinates > 1. Check that your boxes was normalized at [0, 1]'.format(cond_sum)) |
|
result_boxes[cond] = 1 |
|
|
|
boxes1 = result_boxes.copy() |
|
result_boxes[:, 0] = np.min(boxes1[:, [0, 2]], axis=1) |
|
result_boxes[:, 2] = np.max(boxes1[:, [0, 2]], axis=1) |
|
result_boxes[:, 1] = np.min(boxes1[:, [1, 3]], axis=1) |
|
result_boxes[:, 3] = np.max(boxes1[:, [1, 3]], axis=1) |
|
|
|
area = (result_boxes[:, 2] - result_boxes[:, 0]) * (result_boxes[:, 3] - result_boxes[:, 1]) |
|
cond = (area == 0) |
|
cond_sum = cond.astype(np.int32).sum() |
|
if cond_sum > 0: |
|
print('Warning. Removed {} boxes with zero area!'.format(cond_sum)) |
|
result_boxes = result_boxes[area > 0] |
|
scores = scores[area > 0] |
|
labels = labels[area > 0] |
|
|
|
return result_boxes, scores, labels |
|
|
|
|
|
def cpu_soft_nms_float(dets, sc, Nt, sigma, thresh, method): |
|
""" |
|
Based on: https://github.com/DocF/Soft-NMS/blob/master/soft_nms.py |
|
It's different from original soft-NMS because we have float coordinates on range [0; 1] |
|
|
|
:param dets: boxes format [x1, y1, x2, y2] |
|
:param sc: scores for boxes |
|
:param Nt: required iou |
|
:param sigma: |
|
:param thresh: |
|
:param method: 1 - linear soft-NMS, 2 - gaussian soft-NMS, 3 - standard NMS |
|
:return: index of boxes to keep |
|
""" |
|
|
|
|
|
N = dets.shape[0] |
|
indexes = np.array([np.arange(N)]) |
|
dets = np.concatenate((dets, indexes.T), axis=1) |
|
|
|
|
|
y1 = dets[:, 1] |
|
x1 = dets[:, 0] |
|
y2 = dets[:, 3] |
|
x2 = dets[:, 2] |
|
scores = sc |
|
areas = (x2 - x1) * (y2 - y1) |
|
|
|
for i in range(N): |
|
|
|
tBD = dets[i, :].copy() |
|
tscore = scores[i].copy() |
|
tarea = areas[i].copy() |
|
pos = i + 1 |
|
|
|
|
|
if i != N - 1: |
|
maxscore = np.max(scores[pos:], axis=0) |
|
maxpos = np.argmax(scores[pos:], axis=0) |
|
else: |
|
maxscore = scores[-1] |
|
maxpos = 0 |
|
if tscore < maxscore: |
|
dets[i, :] = dets[maxpos + i + 1, :] |
|
dets[maxpos + i + 1, :] = tBD |
|
tBD = dets[i, :] |
|
|
|
scores[i] = scores[maxpos + i + 1] |
|
scores[maxpos + i + 1] = tscore |
|
tscore = scores[i] |
|
|
|
areas[i] = areas[maxpos + i + 1] |
|
areas[maxpos + i + 1] = tarea |
|
tarea = areas[i] |
|
|
|
|
|
xx1 = np.maximum(dets[i, 1], dets[pos:, 1]) |
|
yy1 = np.maximum(dets[i, 0], dets[pos:, 0]) |
|
xx2 = np.minimum(dets[i, 3], dets[pos:, 3]) |
|
yy2 = np.minimum(dets[i, 2], dets[pos:, 2]) |
|
|
|
w = np.maximum(0.0, xx2 - xx1) |
|
h = np.maximum(0.0, yy2 - yy1) |
|
inter = w * h |
|
ovr = inter / (areas[i] + areas[pos:] - inter) |
|
|
|
|
|
if method == 1: |
|
weight = np.ones(ovr.shape) |
|
weight[ovr > Nt] = weight[ovr > Nt] - ovr[ovr > Nt] |
|
elif method == 2: |
|
weight = np.exp(-(ovr * ovr) / sigma) |
|
else: |
|
weight = np.ones(ovr.shape) |
|
weight[ovr > Nt] = 0 |
|
|
|
scores[pos:] = weight * scores[pos:] |
|
|
|
|
|
inds = dets[:, 4][scores > thresh] |
|
keep = inds.astype(int) |
|
return keep |
|
|
|
|
|
@jit(nopython=True) |
|
def nms_float_fast(dets, scores, thresh): |
|
""" |
|
# It's different from original nms because we have float coordinates on range [0; 1] |
|
:param dets: numpy array of boxes with shape: (N, 5). Order: x1, y1, x2, y2, score. All variables in range [0; 1] |
|
:param thresh: IoU value for boxes |
|
:return: index of boxes to keep |
|
""" |
|
x1 = dets[:, 0] |
|
y1 = dets[:, 1] |
|
x2 = dets[:, 2] |
|
y2 = dets[:, 3] |
|
|
|
areas = (x2 - x1) * (y2 - y1) |
|
order = scores.argsort()[::-1] |
|
|
|
keep = [] |
|
while order.size > 0: |
|
i = order[0] |
|
keep.append(i) |
|
xx1 = np.maximum(x1[i], x1[order[1:]]) |
|
yy1 = np.maximum(y1[i], y1[order[1:]]) |
|
xx2 = np.minimum(x2[i], x2[order[1:]]) |
|
yy2 = np.minimum(y2[i], y2[order[1:]]) |
|
|
|
w = np.maximum(0.0, xx2 - xx1) |
|
h = np.maximum(0.0, yy2 - yy1) |
|
inter = w * h |
|
ovr = inter / (areas[i] + areas[order[1:]] - inter) |
|
inds = np.where(ovr <= thresh)[0] |
|
order = order[inds + 1] |
|
|
|
return keep |
|
|
|
|
|
def nms_method(boxes, scores, labels, method=3, iou_thr=0.5, sigma=0.5, thresh=0.001, weights=None): |
|
""" |
|
:param boxes: list of boxes predictions from each model, each box is 4 numbers. |
|
It has 3 dimensions (models_number, model_preds, 4) |
|
Order of boxes: x1, y1, x2, y2. We expect float normalized coordinates [0; 1] |
|
:param scores: list of scores for each model |
|
:param labels: list of labels for each model |
|
:param method: 1 - linear soft-NMS, 2 - gaussian soft-NMS, 3 - standard NMS |
|
:param iou_thr: IoU value for boxes to be a match |
|
:param sigma: Sigma value for SoftNMS |
|
:param thresh: threshold for boxes to keep (important for SoftNMS) |
|
:param weights: list of weights for each model. Default: None, which means weight == 1 for each model |
|
|
|
:return: boxes: boxes coordinates (Order of boxes: x1, y1, x2, y2). |
|
:return: scores: confidence scores |
|
:return: labels: boxes labels |
|
""" |
|
|
|
|
|
if weights is not None: |
|
if len(boxes) != len(weights): |
|
print('Incorrect number of weights: {}. Must be: {}. Skip it'.format(len(weights), len(boxes))) |
|
else: |
|
weights = np.array(weights) |
|
for i in range(len(weights)): |
|
scores[i] = (np.array(scores[i]) * weights[i]) / weights.sum() |
|
|
|
|
|
boxes = np.concatenate(boxes) |
|
scores = np.concatenate(scores) |
|
labels = np.concatenate(labels) |
|
|
|
|
|
boxes, scores, labels = prepare_boxes(boxes, scores, labels) |
|
|
|
|
|
unique_labels = np.unique(labels) |
|
final_boxes = [] |
|
final_scores = [] |
|
final_labels = [] |
|
for l in unique_labels: |
|
condition = (labels == l) |
|
boxes_by_label = boxes[condition] |
|
scores_by_label = scores[condition] |
|
labels_by_label = np.array([l] * len(boxes_by_label)) |
|
|
|
if method != 3: |
|
keep = cpu_soft_nms_float(boxes_by_label.copy(), scores_by_label.copy(), Nt=iou_thr, sigma=sigma, thresh=thresh, method=method) |
|
else: |
|
|
|
keep = nms_float_fast(boxes_by_label, scores_by_label, thresh=iou_thr) |
|
|
|
final_boxes.append(boxes_by_label[keep]) |
|
final_scores.append(scores_by_label[keep]) |
|
final_labels.append(labels_by_label[keep]) |
|
final_boxes = np.concatenate(final_boxes) |
|
final_scores = np.concatenate(final_scores) |
|
final_labels = np.concatenate(final_labels) |
|
|
|
return final_boxes, final_scores, final_labels |
|
|
|
|
|
def nms(boxes, scores, labels, iou_thr=0.5, weights=None): |
|
""" |
|
Short call for standard NMS |
|
|
|
:param boxes: |
|
:param scores: |
|
:param labels: |
|
:param iou_thr: |
|
:param weights: |
|
:return: |
|
""" |
|
return nms_method(boxes, scores, labels, method=3, iou_thr=iou_thr, weights=weights) |
|
|
|
|
|
def soft_nms(boxes, scores, labels, method=2, iou_thr=0.5, sigma=0.5, thresh=0.001, weights=None): |
|
""" |
|
Short call for Soft-NMS |
|
|
|
:param boxes: |
|
:param scores: |
|
:param labels: |
|
:param method: |
|
:param iou_thr: |
|
:param sigma: |
|
:param thresh: |
|
:param weights: |
|
:return: |
|
""" |
|
return nms_method(boxes, scores, labels, method=method, iou_thr=iou_thr, sigma=sigma, thresh=thresh, weights=weights) |