| import glob |
| import logging |
| import os |
| import platform |
| import random |
| import re |
| import shutil |
| import subprocess |
| import time |
| import torchvision |
| from contextlib import contextmanager |
| from copy import copy |
| from pathlib import Path |
|
|
| import cv2 |
| import math |
| import matplotlib |
| import matplotlib.pyplot as plt |
| import numpy as np |
| import torch |
| import torch.nn as nn |
| import yaml |
| from PIL import Image |
| from scipy.cluster.vq import kmeans |
| from scipy.signal import butter, filtfilt |
| from tqdm import tqdm |
|
|
|
|
| def bbox_iou(box1, box2, x1y1x2y2=True, GIoU=False, DIoU=False, CIoU=False, eps=1e-9): |
| |
| box2 = box2.T |
|
|
| |
| if x1y1x2y2: |
| b1_x1, b1_y1, b1_x2, b1_y2 = box1[0], box1[1], box1[2], box1[3] |
| b2_x1, b2_y1, b2_x2, b2_y2 = box2[0], box2[1], box2[2], box2[3] |
| else: |
| b1_x1, b1_x2 = box1[0] - box1[2] / 2, box1[0] + box1[2] / 2 |
| b1_y1, b1_y2 = box1[1] - box1[3] / 2, box1[1] + box1[3] / 2 |
| b2_x1, b2_x2 = box2[0] - box2[2] / 2, box2[0] + box2[2] / 2 |
| b2_y1, b2_y2 = box2[1] - box2[3] / 2, box2[1] + box2[3] / 2 |
|
|
| |
| inter = (torch.min(b1_x2, b2_x2) - torch.max(b1_x1, b2_x1)).clamp(0) * \ |
| (torch.min(b1_y2, b2_y2) - torch.max(b1_y1, b2_y1)).clamp(0) |
|
|
| |
| w1, h1 = b1_x2 - b1_x1, b1_y2 - b1_y1 + eps |
| w2, h2 = b2_x2 - b2_x1, b2_y2 - b2_y1 + eps |
| union = w1 * h1 + w2 * h2 - inter + eps |
|
|
| iou = inter / union |
| if GIoU or DIoU or CIoU: |
| cw = torch.max(b1_x2, b2_x2) - torch.min(b1_x1, b2_x1) |
| ch = torch.max(b1_y2, b2_y2) - torch.min(b1_y1, b2_y1) |
| if CIoU or DIoU: |
| c2 = cw ** 2 + ch ** 2 + eps |
| rho2 = ((b2_x1 + b2_x2 - b1_x1 - b1_x2) ** 2 + |
| (b2_y1 + b2_y2 - b1_y1 - b1_y2) ** 2) / 4 |
| if DIoU: |
| return iou - rho2 / c2 |
| elif CIoU: |
| v = (4 / math.pi ** 2) * torch.pow(torch.atan(w2 / h2) - torch.atan(w1 / h1), 2) |
| with torch.no_grad(): |
| alpha = v / ((1 + eps) - iou + v) |
| return iou - (rho2 / c2 + v * alpha) |
| else: |
| c_area = cw * ch + eps |
| return iou - (c_area - union) / c_area |
| else: |
| return iou |
|
|
|
|
| def box_iou(box1, box2): |
| |
| """ |
| Return intersection-over-union (Jaccard index) of boxes. |
| Both sets of boxes are expected to be in (x1, y1, x2, y2) format. |
| Arguments: |
| box1 (Tensor[N, 4]) |
| box2 (Tensor[M, 4]) |
| Returns: |
| iou (Tensor[N, M]): the NxM matrix containing the pairwise |
| IoU values for every element in boxes1 and boxes2 |
| """ |
|
|
| def box_area(box): |
| |
| return (box[2] - box[0]) * (box[3] - box[1]) |
|
|
| area1 = box_area(box1.T) |
| area2 = box_area(box2.T) |
|
|
| |
| inter = (torch.min(box1[:, None, 2:], box2[:, 2:]) - torch.max(box1[:, None, :2], box2[:, :2])).clamp(0).prod(2) |
| return inter / (area1[:, None] + area2 - inter) |
|
|
| def non_max_suppression(prediction, conf_thres=0.25, iou_thres=0.45, classes=None, agnostic=False, labels=()): |
| """Performs Non-Maximum Suppression (NMS) on inference results |
| |
| Returns: |
| detections with shape: nx6 (x1, y1, x2, y2, conf, cls) |
| """ |
|
|
| nc = prediction.shape[2] - 5 |
| xc = prediction[..., 4] > conf_thres |
|
|
| |
| min_wh, max_wh = 2, 4096 |
| max_det = 300 |
| max_nms = 30000 |
| time_limit = 10.0 |
| redundant = True |
| multi_label = nc > 1 |
| merge = False |
|
|
| t = time.time() |
| output = [torch.zeros((0, 6), device=prediction.device)] * prediction.shape[0] |
| for xi, x in enumerate(prediction): |
| |
| |
| x = x[xc[xi]] |
|
|
| |
| if labels and len(labels[xi]): |
| l = labels[xi] |
| v = torch.zeros((len(l), nc + 5), device=x.device) |
| v[:, :4] = l[:, 1:5] |
| v[:, 4] = 1.0 |
| v[range(len(l)), l[:, 0].long() + 5] = 1.0 |
| x = torch.cat((x, v), 0) |
|
|
| |
| if not x.shape[0]: |
| continue |
|
|
| |
| x[:, 5:] *= x[:, 4:5] |
|
|
| |
| box = xywh2xyxy(x[:, :4]) |
|
|
| |
| if multi_label: |
| i, j = (x[:, 5:] > conf_thres).nonzero(as_tuple=False).T |
| x = torch.cat((box[i], x[i, j + 5, None], j[:, None].float()), 1) |
| else: |
| conf, j = x[:, 5:].max(1, keepdim=True) |
| x = torch.cat((box, conf, j.float()), 1)[conf.view(-1) > conf_thres] |
|
|
| |
| if classes is not None: |
| x = x[(x[:, 5:6] == torch.tensor(classes, device=x.device)).any(1)] |
|
|
| |
| |
| |
|
|
| |
| n = x.shape[0] |
| if not n: |
| continue |
| elif n > max_nms: |
| x = x[x[:, 4].argsort(descending=True)[:max_nms]] |
|
|
| |
| c = x[:, 5:6] * (0 if agnostic else max_wh) |
| boxes, scores = x[:, :4] + c, x[:, 4] |
| i = torchvision.ops.nms(boxes, scores, iou_thres) |
| if i.shape[0] > max_det: |
| i = i[:max_det] |
| if merge and (1 < n < 3E3): |
| |
| iou = box_iou(boxes[i], boxes) > iou_thres |
| weights = iou * scores[None] |
| x[i, :4] = torch.mm(weights, x[:, :4]).float() / weights.sum(1, keepdim=True) |
| if redundant: |
| i = i[iou.sum(1) > 1] |
|
|
| output[xi] = x[i] |
| if (time.time() - t) > time_limit: |
| print(f'WARNING: NMS time limit {time_limit}s exceeded') |
| break |
|
|
| return output |
|
|
|
|
| def xywh2xyxy(x): |
| |
| y = torch.zeros_like(x) if isinstance(x, torch.Tensor) else np.zeros_like(x) |
| y[:, 0] = x[:, 0] - x[:, 2] / 2 |
| y[:, 1] = x[:, 1] - x[:, 3] / 2 |
| y[:, 2] = x[:, 0] + x[:, 2] / 2 |
| y[:, 3] = x[:, 1] + x[:, 3] / 2 |
| return y |
| |
| def fitness(x): |
| |
| w = [0.0, 0.0, 0.1, 0.9] |
| return (x[:, :4] * w).sum(1) |
|
|
| def check_img_size(img_size, s=32): |
| |
| new_size = make_divisible(img_size, int(s)) |
| if new_size != img_size: |
| print('WARNING: --img-size %g must be multiple of max stride %g, updating to %g' % (img_size, s, new_size)) |
| return new_size |
|
|
| def scale_coords(img1_shape, coords, img0_shape, ratio_pad=None): |
| |
| if ratio_pad is None: |
| gain = min(img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1]) |
| pad = (img1_shape[1] - img0_shape[1] * gain) / 2, (img1_shape[0] - img0_shape[0] * gain) / 2 |
| else: |
| gain = ratio_pad[0][0] |
| pad = ratio_pad[1] |
|
|
| coords[:, [0, 2]] -= pad[0] |
| coords[:, [1, 3]] -= pad[1] |
| coords[:, :4] /= gain |
| clip_coords(coords, img0_shape) |
| return coords |
|
|
| def clip_coords(boxes, img_shape): |
| |
| boxes[:, 0].clamp_(0, img_shape[1]) |
| boxes[:, 1].clamp_(0, img_shape[0]) |
| boxes[:, 2].clamp_(0, img_shape[1]) |
| boxes[:, 3].clamp_(0, img_shape[0]) |
|
|
| def make_divisible(x, divisor): |
| |
| return math.ceil(x / divisor) * divisor |
|
|
| def xyxy2xywh(x): |
| |
| y = torch.zeros_like(x) if isinstance(x, torch.Tensor) else np.zeros_like(x) |
| y[:, 0] = (x[:, 0] + x[:, 2]) / 2 |
| y[:, 1] = (x[:, 1] + x[:, 3]) / 2 |
| y[:, 2] = x[:, 2] - x[:, 0] |
| y[:, 3] = x[:, 3] - x[:, 1] |
| return y |
|
|
| def plot_images(images, targets, paths=None, fname='images.jpg', names=None, max_size=640, max_subplots=16): |
| |
|
|
| if isinstance(images, torch.Tensor): |
| images = images.cpu().float().numpy() |
| if isinstance(targets, torch.Tensor): |
| targets = targets.cpu().numpy() |
|
|
| |
| if np.max(images[0]) <= 1: |
| images *= 255 |
|
|
| tl = 3 |
| tf = max(tl - 1, 1) |
| bs, _, h, w = images.shape |
| bs = min(bs, max_subplots) |
| ns = np.ceil(bs ** 0.5) |
|
|
| |
| scale_factor = max_size / max(h, w) |
| if scale_factor < 1: |
| h = math.ceil(scale_factor * h) |
| w = math.ceil(scale_factor * w) |
|
|
| colors = color_list() |
| mosaic = np.full((int(ns * h), int(ns * w), 3), 255, dtype=np.uint8) |
| for i, img in enumerate(images): |
| if i == max_subplots: |
| break |
|
|
| block_x = int(w * (i // ns)) |
| block_y = int(h * (i % ns)) |
|
|
| img = img.transpose(1, 2, 0) |
| if scale_factor < 1: |
| img = cv2.resize(img, (w, h)) |
|
|
| mosaic[block_y:block_y + h, block_x:block_x + w, :] = img |
| if len(targets) > 0: |
| image_targets = targets[targets[:, 0] == i] |
| boxes = xywh2xyxy(image_targets[:, 2:6]).T |
| classes = image_targets[:, 1].astype('int') |
| labels = image_targets.shape[1] == 6 |
| conf = None if labels else image_targets[:, 6] |
|
|
| if boxes.shape[1]: |
| if boxes.max() <= 1.01: |
| boxes[[0, 2]] *= w |
| boxes[[1, 3]] *= h |
| elif scale_factor < 1: |
| boxes *= scale_factor |
| boxes[[0, 2]] += block_x |
| boxes[[1, 3]] += block_y |
| for j, box in enumerate(boxes.T): |
| cls = int(classes[j]) |
| color = colors[cls % len(colors)] |
| cls = names[cls] if names else cls |
| if labels or conf[j] > 0.25: |
| label = '%s' % cls if labels else '%s %.1f' % (cls, conf[j]) |
| plot_one_box(box, mosaic, label=label, color=color, line_thickness=tl) |
|
|
| |
| if paths: |
| label = Path(paths[i]).name[:40] |
| t_size = cv2.getTextSize(label, 0, fontScale=tl / 3, thickness=tf)[0] |
| cv2.putText(mosaic, label, (block_x + 5, block_y + t_size[1] + 5), 0, tl / 3, [220, 220, 220], thickness=tf, |
| lineType=cv2.LINE_AA) |
|
|
| |
| cv2.rectangle(mosaic, (block_x, block_y), (block_x + w, block_y + h), (255, 255, 255), thickness=3) |
|
|
| if fname: |
| r = min(1280. / max(h, w) / ns, 1.0) |
| mosaic = cv2.resize(mosaic, (int(ns * w * r), int(ns * h * r)), interpolation=cv2.INTER_AREA) |
| |
| Image.fromarray(mosaic).save(fname) |
| return mosaic |
|
|
| def plot_one_box(x, img, color=None, label=None, line_thickness=None): |
| |
| tl = line_thickness or round(0.002 * (img.shape[0] + img.shape[1]) / 2) + 1 |
| color = color or [random.randint(0, 255) for _ in range(3)] |
| c1, c2 = (int(x[0]), int(x[1])), (int(x[2]), int(x[3])) |
| cv2.rectangle(img, c1, c2, color, thickness=tl, lineType=cv2.LINE_AA) |
| if label: |
| tf = max(tl - 1, 1) |
| t_size = cv2.getTextSize(label, 0, fontScale=tl / 3, thickness=tf)[0] |
| c2 = c1[0] + t_size[0], c1[1] - t_size[1] - 3 |
| cv2.rectangle(img, c1, c2, color, -1, cv2.LINE_AA) |
| cv2.putText(img, label, (c1[0], c1[1] - 2), 0, tl / 3, [225, 255, 255], thickness=tf, lineType=cv2.LINE_AA) |
|
|
| def color_list(): |
| |
| def hex2rgb(h): |
| return tuple(int(str(h[1 + i:1 + i + 2]), 16) for i in (0, 2, 4)) |
|
|
| return [hex2rgb(h) for h in plt.rcParams['axes.prop_cycle'].by_key()['color']] |
|
|
| def ap_per_class(tp, conf, pred_cls, target_cls, plot=False, save_dir='precision-recall_curve.png', names=[]): |
| """ Compute the average precision, given the recall and precision curves. |
| Source: https://github.com/rafaelpadilla/Object-Detection-Metrics. |
| # Arguments |
| tp: True positives (nparray, nx1 or nx10). |
| conf: Objectness value from 0-1 (nparray). |
| pred_cls: Predicted object classes (nparray). |
| target_cls: True object classes (nparray). |
| plot: Plot precision-recall curve at mAP@0.5 |
| save_dir: Plot save directory |
| # Returns |
| The average precision as computed in py-faster-rcnn. |
| """ |
|
|
| |
| i = np.argsort(-conf) |
| tp, conf, pred_cls = tp[i], conf[i], pred_cls[i] |
|
|
| |
| unique_classes = np.unique(target_cls) |
|
|
| |
| px, py = np.linspace(0, 1, 1000), [] |
| pr_score = 0.1 |
| s = [unique_classes.shape[0], tp.shape[1]] |
| ap, p, r = np.zeros(s), np.zeros((unique_classes.shape[0], 1000)), np.zeros((unique_classes.shape[0], 1000)) |
| for ci, c in enumerate(unique_classes): |
| i = pred_cls == c |
| n_l = (target_cls == c).sum() |
| n_p = i.sum() |
|
|
| if n_p == 0 or n_l == 0: |
| continue |
| else: |
| |
| fpc = (1 - tp[i]).cumsum(0) |
| tpc = tp[i].cumsum(0) |
|
|
| |
| recall = tpc / (n_l + 1e-16) |
| r[ci] = np.interp(-px, -conf[i], recall[:, 0], left=0) |
|
|
| |
| precision = tpc / (tpc + fpc) |
| p[ci] = np.interp(-px, -conf[i], precision[:, 0], left=1) |
| |
| for j in range(tp.shape[1]): |
| ap[ci, j], mpre, mrec = compute_ap(recall[:, j], precision[:, j]) |
| if plot and (j == 0): |
| py.append(np.interp(px, mrec, mpre)) |
|
|
| |
| f1 = 2 * p * r / (p + r + 1e-16) |
| i=r.mean(0).argmax() |
|
|
| if plot: |
| plot_pr_curve(px, py, ap, save_dir, names) |
|
|
| return p[:, i], r[:, i], ap, f1[:, i], unique_classes.astype('int32') |
|
|
| def compute_ap(recall, precision): |
| """ Compute the average precision, given the recall and precision curves. |
| Source: https://github.com/rbgirshick/py-faster-rcnn. |
| # Arguments |
| recall: The recall curve (list). |
| precision: The precision curve (list). |
| # Returns |
| The average precision as computed in py-faster-rcnn. |
| """ |
|
|
| |
| mrec = np.concatenate(([0.], recall, [recall[-1] + 1E-3])) |
| mpre = np.concatenate(([1.], precision, [0.])) |
|
|
| |
| mpre = np.flip(np.maximum.accumulate(np.flip(mpre))) |
|
|
| |
| method = 'interp' |
| if method == 'interp': |
| x = np.linspace(0, 1, 101) |
| ap = np.trapz(np.interp(x, mrec, mpre), x) |
|
|
| else: |
| i = np.where(mrec[1:] != mrec[:-1])[0] |
| ap = np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1]) |
|
|
| return ap, mpre, mrec |
|
|
| def coco80_to_coco91_class(): |
| |
| |
| |
| |
| |
| x = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 27, 28, 31, 32, 33, 34, |
| 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, |
| 64, 65, 67, 70, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 84, 85, 86, 87, 88, 89, 90] |
| return x |
|
|
| def output_to_target(output): |
| |
| targets = [] |
| for i, o in enumerate(output): |
| for *box, conf, cls in o.cpu().numpy(): |
| targets.append([i, cls, *list(*xyxy2xywh(np.array(box)[None])), conf]) |
| return np.array(targets) |
|
|
| def plot_pr_curve(px, py, ap, save_dir='.', names=()): |
| fig, ax = plt.subplots(1, 1, figsize=(9, 6), tight_layout=True) |
| py = np.stack(py, axis=1) |
|
|
| if 0 < len(names) < 21: |
| for i, y in enumerate(py.T): |
| ax.plot(px, y, linewidth=1, label=f'{names[i]} %.3f' % ap[i, 0]) |
| else: |
| ax.plot(px, py, linewidth=1, color='grey') |
|
|
| ax.plot(px, py.mean(1), linewidth=3, color='blue', label='all classes %.3f mAP@0.5' % ap[:, 0].mean()) |
| ax.set_xlabel('Recall') |
| ax.set_ylabel('Precision') |
| ax.set_xlim(0, 1) |
| ax.set_ylim(0, 1) |
| plt.legend(bbox_to_anchor=(1.04, 1), loc="upper left") |
| fig.savefig(Path(save_dir) / 'precision_recall_curve.png', dpi=250) |