You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1293 lines
53KB

  1. import glob
  2. import logging
  3. import math
  4. import os
  5. import platform
  6. import random
  7. import shutil
  8. import subprocess
  9. import time
  10. from contextlib import contextmanager
  11. from copy import copy
  12. from pathlib import Path
  13. import cv2
  14. import matplotlib
  15. import matplotlib.pyplot as plt
  16. import numpy as np
  17. import torch
  18. import torch.nn as nn
  19. import yaml
  20. from scipy.cluster.vq import kmeans
  21. from scipy.signal import butter, filtfilt
  22. from tqdm import tqdm
  23. from utils.google_utils import gsutil_getsize
  24. from utils.torch_utils import is_parallel, init_torch_seeds
  25. # Set printoptions
  26. torch.set_printoptions(linewidth=320, precision=5, profile='long')
  27. np.set_printoptions(linewidth=320, formatter={'float_kind': '{:11.5g}'.format}) # format short g, %precision=5
  28. matplotlib.rc('font', **{'size': 11})
  29. # Prevent OpenCV from multithreading (to use PyTorch DataLoader)
  30. cv2.setNumThreads(0)
  31. @contextmanager
  32. def torch_distributed_zero_first(local_rank: int):
  33. """
  34. Decorator to make all processes in distributed training wait for each local_master to do something.
  35. """
  36. if local_rank not in [-1, 0]:
  37. torch.distributed.barrier()
  38. yield
  39. if local_rank == 0:
  40. torch.distributed.barrier()
  41. def set_logging(rank=-1):
  42. logging.basicConfig(
  43. format="%(message)s",
  44. level=logging.INFO if rank in [-1, 0] else logging.WARN)
  45. def init_seeds(seed=0):
  46. random.seed(seed)
  47. np.random.seed(seed)
  48. init_torch_seeds(seed)
  49. def get_latest_run(search_dir='./runs'):
  50. # Return path to most recent 'last.pt' in /runs (i.e. to --resume from)
  51. last_list = glob.glob(f'{search_dir}/**/last*.pt', recursive=True)
  52. return max(last_list, key=os.path.getctime) if last_list else ''
  53. def check_git_status():
  54. # Suggest 'git pull' if repo is out of date
  55. if platform.system() in ['Linux', 'Darwin'] and not os.path.isfile('/.dockerenv'):
  56. s = subprocess.check_output('if [ -d .git ]; then git fetch && git status -uno; fi', shell=True).decode('utf-8')
  57. if 'Your branch is behind' in s:
  58. print(s[s.find('Your branch is behind'):s.find('\n\n')] + '\n')
  59. def check_img_size(img_size, s=32):
  60. # Verify img_size is a multiple of stride s
  61. new_size = make_divisible(img_size, int(s)) # ceil gs-multiple
  62. if new_size != img_size:
  63. print('WARNING: --img-size %g must be multiple of max stride %g, updating to %g' % (img_size, s, new_size))
  64. return new_size
  65. def check_anchors(dataset, model, thr=4.0, imgsz=640):
  66. # Check anchor fit to data, recompute if necessary
  67. print('\nAnalyzing anchors... ', end='')
  68. m = model.module.model[-1] if hasattr(model, 'module') else model.model[-1] # Detect()
  69. shapes = imgsz * dataset.shapes / dataset.shapes.max(1, keepdims=True)
  70. scale = np.random.uniform(0.9, 1.1, size=(shapes.shape[0], 1)) # augment scale
  71. wh = torch.tensor(np.concatenate([l[:, 3:5] * s for s, l in zip(shapes * scale, dataset.labels)])).float() # wh
  72. def metric(k): # compute metric
  73. r = wh[:, None] / k[None]
  74. x = torch.min(r, 1. / r).min(2)[0] # ratio metric
  75. best = x.max(1)[0] # best_x
  76. aat = (x > 1. / thr).float().sum(1).mean() # anchors above threshold
  77. bpr = (best > 1. / thr).float().mean() # best possible recall
  78. return bpr, aat
  79. bpr, aat = metric(m.anchor_grid.clone().cpu().view(-1, 2))
  80. print('anchors/target = %.2f, Best Possible Recall (BPR) = %.4f' % (aat, bpr), end='')
  81. if bpr < 0.98: # threshold to recompute
  82. print('. Attempting to generate improved anchors, please wait...' % bpr)
  83. na = m.anchor_grid.numel() // 2 # number of anchors
  84. new_anchors = kmean_anchors(dataset, n=na, img_size=imgsz, thr=thr, gen=1000, verbose=False)
  85. new_bpr = metric(new_anchors.reshape(-1, 2))[0]
  86. if new_bpr > bpr: # replace anchors
  87. new_anchors = torch.tensor(new_anchors, device=m.anchors.device).type_as(m.anchors)
  88. m.anchor_grid[:] = new_anchors.clone().view_as(m.anchor_grid) # for inference
  89. m.anchors[:] = new_anchors.clone().view_as(m.anchors) / m.stride.to(m.anchors.device).view(-1, 1, 1) # loss
  90. check_anchor_order(m)
  91. print('New anchors saved to model. Update model *.yaml to use these anchors in the future.')
  92. else:
  93. print('Original anchors better than new anchors. Proceeding with original anchors.')
  94. print('') # newline
  95. def check_anchor_order(m):
  96. # Check anchor order against stride order for YOLOv5 Detect() module m, and correct if necessary
  97. a = m.anchor_grid.prod(-1).view(-1) # anchor area
  98. da = a[-1] - a[0] # delta a
  99. ds = m.stride[-1] - m.stride[0] # delta s
  100. if da.sign() != ds.sign(): # same order
  101. print('Reversing anchor order')
  102. m.anchors[:] = m.anchors.flip(0)
  103. m.anchor_grid[:] = m.anchor_grid.flip(0)
  104. def check_file(file):
  105. # Search for file if not found
  106. if os.path.isfile(file) or file == '':
  107. return file
  108. else:
  109. files = glob.glob('./**/' + file, recursive=True) # find file
  110. assert len(files), 'File Not Found: %s' % file # assert file was found
  111. assert len(files) == 1, "Multiple files match '%s', specify exact path: %s" % (file, files) # assert unique
  112. return files[0] # return file
  113. def check_dataset(dict):
  114. # Download dataset if not found
  115. val, s = dict.get('val'), dict.get('download')
  116. if val and len(val):
  117. val = [os.path.abspath(x) for x in (val if isinstance(val, list) else [val])] # val path
  118. if not all(os.path.exists(x) for x in val):
  119. print('\nWARNING: Dataset not found, nonexistant paths: %s' % [*val])
  120. if s and len(s): # download script
  121. print('Downloading %s ...' % s)
  122. if s.startswith('http') and s.endswith('.zip'): # URL
  123. f = Path(s).name # filename
  124. torch.hub.download_url_to_file(s, f)
  125. r = os.system('unzip -q %s -d ../ && rm %s' % (f, f)) # unzip
  126. else: # bash script
  127. r = os.system(s)
  128. print('Dataset autodownload %s\n' % ('success' if r == 0 else 'failure')) # analyze return value
  129. else:
  130. raise Exception('Dataset not found.')
  131. def make_divisible(x, divisor):
  132. # Returns x evenly divisble by divisor
  133. return math.ceil(x / divisor) * divisor
  134. def labels_to_class_weights(labels, nc=80):
  135. # Get class weights (inverse frequency) from training labels
  136. if labels[0] is None: # no labels loaded
  137. return torch.Tensor()
  138. labels = np.concatenate(labels, 0) # labels.shape = (866643, 5) for COCO
  139. classes = labels[:, 0].astype(np.int) # labels = [class xywh]
  140. weights = np.bincount(classes, minlength=nc) # occurences per class
  141. # Prepend gridpoint count (for uCE trianing)
  142. # gpi = ((320 / 32 * np.array([1, 2, 4])) ** 2 * 3).sum() # gridpoints per image
  143. # weights = np.hstack([gpi * len(labels) - weights.sum() * 9, weights * 9]) ** 0.5 # prepend gridpoints to start
  144. weights[weights == 0] = 1 # replace empty bins with 1
  145. weights = 1 / weights # number of targets per class
  146. weights /= weights.sum() # normalize
  147. return torch.from_numpy(weights)
  148. def labels_to_image_weights(labels, nc=80, class_weights=np.ones(80)):
  149. # Produces image weights based on class mAPs
  150. n = len(labels)
  151. class_counts = np.array([np.bincount(labels[i][:, 0].astype(np.int), minlength=nc) for i in range(n)])
  152. image_weights = (class_weights.reshape(1, nc) * class_counts).sum(1)
  153. # index = random.choices(range(n), weights=image_weights, k=1) # weight image sample
  154. return image_weights
  155. def coco80_to_coco91_class(): # converts 80-index (val2014) to 91-index (paper)
  156. # https://tech.amikelive.com/node-718/what-object-categories-labels-are-in-coco-dataset/
  157. # a = np.loadtxt('data/coco.names', dtype='str', delimiter='\n')
  158. # b = np.loadtxt('data/coco_paper.names', dtype='str', delimiter='\n')
  159. # x1 = [list(a[i] == b).index(True) + 1 for i in range(80)] # darknet to coco
  160. # x2 = [list(b[i] == a).index(True) if any(b[i] == a) else None for i in range(91)] # coco to darknet
  161. 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,
  162. 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,
  163. 64, 65, 67, 70, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 84, 85, 86, 87, 88, 89, 90]
  164. return x
  165. def xyxy2xywh(x):
  166. # Convert nx4 boxes from [x1, y1, x2, y2] to [x, y, w, h] where xy1=top-left, xy2=bottom-right
  167. y = torch.zeros_like(x) if isinstance(x, torch.Tensor) else np.zeros_like(x)
  168. y[:, 0] = (x[:, 0] + x[:, 2]) / 2 # x center
  169. y[:, 1] = (x[:, 1] + x[:, 3]) / 2 # y center
  170. y[:, 2] = x[:, 2] - x[:, 0] # width
  171. y[:, 3] = x[:, 3] - x[:, 1] # height
  172. return y
  173. def xywh2xyxy(x):
  174. # Convert nx4 boxes from [x, y, w, h] to [x1, y1, x2, y2] where xy1=top-left, xy2=bottom-right
  175. y = torch.zeros_like(x) if isinstance(x, torch.Tensor) else np.zeros_like(x)
  176. y[:, 0] = x[:, 0] - x[:, 2] / 2 # top left x
  177. y[:, 1] = x[:, 1] - x[:, 3] / 2 # top left y
  178. y[:, 2] = x[:, 0] + x[:, 2] / 2 # bottom right x
  179. y[:, 3] = x[:, 1] + x[:, 3] / 2 # bottom right y
  180. return y
  181. def scale_coords(img1_shape, coords, img0_shape, ratio_pad=None):
  182. # Rescale coords (xyxy) from img1_shape to img0_shape
  183. if ratio_pad is None: # calculate from img0_shape
  184. gain = min(img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1]) # gain = old / new
  185. pad = (img1_shape[1] - img0_shape[1] * gain) / 2, (img1_shape[0] - img0_shape[0] * gain) / 2 # wh padding
  186. else:
  187. gain = ratio_pad[0][0]
  188. pad = ratio_pad[1]
  189. coords[:, [0, 2]] -= pad[0] # x padding
  190. coords[:, [1, 3]] -= pad[1] # y padding
  191. coords[:, :4] /= gain
  192. clip_coords(coords, img0_shape)
  193. return coords
  194. def clip_coords(boxes, img_shape):
  195. # Clip bounding xyxy bounding boxes to image shape (height, width)
  196. boxes[:, 0].clamp_(0, img_shape[1]) # x1
  197. boxes[:, 1].clamp_(0, img_shape[0]) # y1
  198. boxes[:, 2].clamp_(0, img_shape[1]) # x2
  199. boxes[:, 3].clamp_(0, img_shape[0]) # y2
  200. def ap_per_class(tp, conf, pred_cls, target_cls, plot=False, fname='precision-recall_curve.png'):
  201. """ Compute the average precision, given the recall and precision curves.
  202. Source: https://github.com/rafaelpadilla/Object-Detection-Metrics.
  203. # Arguments
  204. tp: True positives (nparray, nx1 or nx10).
  205. conf: Objectness value from 0-1 (nparray).
  206. pred_cls: Predicted object classes (nparray).
  207. target_cls: True object classes (nparray).
  208. plot: Plot precision-recall curve at mAP@0.5
  209. fname: Plot filename
  210. # Returns
  211. The average precision as computed in py-faster-rcnn.
  212. """
  213. # Sort by objectness
  214. i = np.argsort(-conf)
  215. tp, conf, pred_cls = tp[i], conf[i], pred_cls[i]
  216. # Find unique classes
  217. unique_classes = np.unique(target_cls)
  218. # Create Precision-Recall curve and compute AP for each class
  219. px, py = np.linspace(0, 1, 1000), [] # for plotting
  220. pr_score = 0.1 # score to evaluate P and R https://github.com/ultralytics/yolov3/issues/898
  221. s = [unique_classes.shape[0], tp.shape[1]] # number class, number iou thresholds (i.e. 10 for mAP0.5...0.95)
  222. ap, p, r = np.zeros(s), np.zeros(s), np.zeros(s)
  223. for ci, c in enumerate(unique_classes):
  224. i = pred_cls == c
  225. n_gt = (target_cls == c).sum() # Number of ground truth objects
  226. n_p = i.sum() # Number of predicted objects
  227. if n_p == 0 or n_gt == 0:
  228. continue
  229. else:
  230. # Accumulate FPs and TPs
  231. fpc = (1 - tp[i]).cumsum(0)
  232. tpc = tp[i].cumsum(0)
  233. # Recall
  234. recall = tpc / (n_gt + 1e-16) # recall curve
  235. r[ci] = np.interp(-pr_score, -conf[i], recall[:, 0]) # r at pr_score, negative x, xp because xp decreases
  236. # Precision
  237. precision = tpc / (tpc + fpc) # precision curve
  238. p[ci] = np.interp(-pr_score, -conf[i], precision[:, 0]) # p at pr_score
  239. # AP from recall-precision curve
  240. py.append(np.interp(px, recall[:, 0], precision[:, 0])) # precision at mAP@0.5
  241. for j in range(tp.shape[1]):
  242. ap[ci, j] = compute_ap(recall[:, j], precision[:, j])
  243. # Compute F1 score (harmonic mean of precision and recall)
  244. f1 = 2 * p * r / (p + r + 1e-16)
  245. if plot:
  246. py = np.stack(py, axis=1)
  247. fig, ax = plt.subplots(1, 1, figsize=(5, 5))
  248. ax.plot(px, py, linewidth=0.5, color='grey') # plot(recall, precision)
  249. ax.plot(px, py.mean(1), linewidth=2, color='blue', label='all classes')
  250. ax.set_xlabel('Recall')
  251. ax.set_ylabel('Precision')
  252. ax.set_xlim(0, 1)
  253. ax.set_ylim(0, 1)
  254. plt.legend()
  255. fig.tight_layout()
  256. fig.savefig(fname, dpi=200)
  257. return p, r, ap, f1, unique_classes.astype('int32')
  258. def compute_ap(recall, precision):
  259. """ Compute the average precision, given the recall and precision curves.
  260. Source: https://github.com/rbgirshick/py-faster-rcnn.
  261. # Arguments
  262. recall: The recall curve (list).
  263. precision: The precision curve (list).
  264. # Returns
  265. The average precision as computed in py-faster-rcnn.
  266. """
  267. # Append sentinel values to beginning and end
  268. mrec = np.concatenate(([0.], recall, [min(recall[-1] + 1E-3, 1.)]))
  269. mpre = np.concatenate(([0.], precision, [0.]))
  270. # Compute the precision envelope
  271. mpre = np.flip(np.maximum.accumulate(np.flip(mpre)))
  272. # Integrate area under curve
  273. method = 'interp' # methods: 'continuous', 'interp'
  274. if method == 'interp':
  275. x = np.linspace(0, 1, 101) # 101-point interp (COCO)
  276. ap = np.trapz(np.interp(x, mrec, mpre), x) # integrate
  277. else: # 'continuous'
  278. i = np.where(mrec[1:] != mrec[:-1])[0] # points where x axis (recall) changes
  279. ap = np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1]) # area under curve
  280. return ap
  281. def bbox_iou(box1, box2, x1y1x2y2=True, GIoU=False, DIoU=False, CIoU=False, eps=1e-9):
  282. # Returns the IoU of box1 to box2. box1 is 4, box2 is nx4
  283. box2 = box2.T
  284. # Get the coordinates of bounding boxes
  285. if x1y1x2y2: # x1, y1, x2, y2 = box1
  286. b1_x1, b1_y1, b1_x2, b1_y2 = box1[0], box1[1], box1[2], box1[3]
  287. b2_x1, b2_y1, b2_x2, b2_y2 = box2[0], box2[1], box2[2], box2[3]
  288. else: # transform from xywh to xyxy
  289. b1_x1, b1_x2 = box1[0] - box1[2] / 2, box1[0] + box1[2] / 2
  290. b1_y1, b1_y2 = box1[1] - box1[3] / 2, box1[1] + box1[3] / 2
  291. b2_x1, b2_x2 = box2[0] - box2[2] / 2, box2[0] + box2[2] / 2
  292. b2_y1, b2_y2 = box2[1] - box2[3] / 2, box2[1] + box2[3] / 2
  293. # Intersection area
  294. inter = (torch.min(b1_x2, b2_x2) - torch.max(b1_x1, b2_x1)).clamp(0) * \
  295. (torch.min(b1_y2, b2_y2) - torch.max(b1_y1, b2_y1)).clamp(0)
  296. # Union Area
  297. w1, h1 = b1_x2 - b1_x1, b1_y2 - b1_y1 + eps
  298. w2, h2 = b2_x2 - b2_x1, b2_y2 - b2_y1 + eps
  299. union = w1 * h1 + w2 * h2 - inter + eps
  300. iou = inter / union
  301. if GIoU or DIoU or CIoU:
  302. cw = torch.max(b1_x2, b2_x2) - torch.min(b1_x1, b2_x1) # convex (smallest enclosing box) width
  303. ch = torch.max(b1_y2, b2_y2) - torch.min(b1_y1, b2_y1) # convex height
  304. if CIoU or DIoU: # Distance or Complete IoU https://arxiv.org/abs/1911.08287v1
  305. c2 = cw ** 2 + ch ** 2 + eps # convex diagonal squared
  306. rho2 = ((b2_x1 + b2_x2 - b1_x1 - b1_x2) ** 2 +
  307. (b2_y1 + b2_y2 - b1_y1 - b1_y2) ** 2) / 4 # center distance squared
  308. if DIoU:
  309. return iou - rho2 / c2 # DIoU
  310. elif CIoU: # https://github.com/Zzh-tju/DIoU-SSD-pytorch/blob/master/utils/box/box_utils.py#L47
  311. v = (4 / math.pi ** 2) * torch.pow(torch.atan(w2 / h2) - torch.atan(w1 / h1), 2)
  312. with torch.no_grad():
  313. alpha = v / ((1 + eps) - iou + v)
  314. return iou - (rho2 / c2 + v * alpha) # CIoU
  315. else: # GIoU https://arxiv.org/pdf/1902.09630.pdf
  316. c_area = cw * ch + eps # convex area
  317. return iou - (c_area - union) / c_area # GIoU
  318. else:
  319. return iou # IoU
  320. def box_iou(box1, box2):
  321. # https://github.com/pytorch/vision/blob/master/torchvision/ops/boxes.py
  322. """
  323. Return intersection-over-union (Jaccard index) of boxes.
  324. Both sets of boxes are expected to be in (x1, y1, x2, y2) format.
  325. Arguments:
  326. box1 (Tensor[N, 4])
  327. box2 (Tensor[M, 4])
  328. Returns:
  329. iou (Tensor[N, M]): the NxM matrix containing the pairwise
  330. IoU values for every element in boxes1 and boxes2
  331. """
  332. def box_area(box):
  333. # box = 4xn
  334. return (box[2] - box[0]) * (box[3] - box[1])
  335. area1 = box_area(box1.T)
  336. area2 = box_area(box2.T)
  337. # inter(N,M) = (rb(N,M,2) - lt(N,M,2)).clamp(0).prod(2)
  338. inter = (torch.min(box1[:, None, 2:], box2[:, 2:]) - torch.max(box1[:, None, :2], box2[:, :2])).clamp(0).prod(2)
  339. return inter / (area1[:, None] + area2 - inter) # iou = inter / (area1 + area2 - inter)
  340. def wh_iou(wh1, wh2):
  341. # Returns the nxm IoU matrix. wh1 is nx2, wh2 is mx2
  342. wh1 = wh1[:, None] # [N,1,2]
  343. wh2 = wh2[None] # [1,M,2]
  344. inter = torch.min(wh1, wh2).prod(2) # [N,M]
  345. return inter / (wh1.prod(2) + wh2.prod(2) - inter) # iou = inter / (area1 + area2 - inter)
  346. class FocalLoss(nn.Module):
  347. # Wraps focal loss around existing loss_fcn(), i.e. criteria = FocalLoss(nn.BCEWithLogitsLoss(), gamma=1.5)
  348. def __init__(self, loss_fcn, gamma=1.5, alpha=0.25):
  349. super(FocalLoss, self).__init__()
  350. self.loss_fcn = loss_fcn # must be nn.BCEWithLogitsLoss()
  351. self.gamma = gamma
  352. self.alpha = alpha
  353. self.reduction = loss_fcn.reduction
  354. self.loss_fcn.reduction = 'none' # required to apply FL to each element
  355. def forward(self, pred, true):
  356. loss = self.loss_fcn(pred, true)
  357. # p_t = torch.exp(-loss)
  358. # loss *= self.alpha * (1.000001 - p_t) ** self.gamma # non-zero power for gradient stability
  359. # TF implementation https://github.com/tensorflow/addons/blob/v0.7.1/tensorflow_addons/losses/focal_loss.py
  360. pred_prob = torch.sigmoid(pred) # prob from logits
  361. p_t = true * pred_prob + (1 - true) * (1 - pred_prob)
  362. alpha_factor = true * self.alpha + (1 - true) * (1 - self.alpha)
  363. modulating_factor = (1.0 - p_t) ** self.gamma
  364. loss *= alpha_factor * modulating_factor
  365. if self.reduction == 'mean':
  366. return loss.mean()
  367. elif self.reduction == 'sum':
  368. return loss.sum()
  369. else: # 'none'
  370. return loss
  371. def smooth_BCE(eps=0.1): # https://github.com/ultralytics/yolov3/issues/238#issuecomment-598028441
  372. # return positive, negative label smoothing BCE targets
  373. return 1.0 - 0.5 * eps, 0.5 * eps
  374. class BCEBlurWithLogitsLoss(nn.Module):
  375. # BCEwithLogitLoss() with reduced missing label effects.
  376. def __init__(self, alpha=0.05):
  377. super(BCEBlurWithLogitsLoss, self).__init__()
  378. self.loss_fcn = nn.BCEWithLogitsLoss(reduction='none') # must be nn.BCEWithLogitsLoss()
  379. self.alpha = alpha
  380. def forward(self, pred, true):
  381. loss = self.loss_fcn(pred, true)
  382. pred = torch.sigmoid(pred) # prob from logits
  383. dx = pred - true # reduce only missing label effects
  384. # dx = (pred - true).abs() # reduce missing label and false label effects
  385. alpha_factor = 1 - torch.exp((dx - 1) / (self.alpha + 1e-4))
  386. loss *= alpha_factor
  387. return loss.mean()
  388. def compute_loss(p, targets, model): # predictions, targets, model
  389. device = targets.device
  390. lcls, lbox, lobj = torch.zeros(1, device=device), torch.zeros(1, device=device), torch.zeros(1, device=device)
  391. tcls, tbox, indices, anchors = build_targets(p, targets, model) # targets
  392. h = model.hyp # hyperparameters
  393. # Define criteria
  394. BCEcls = nn.BCEWithLogitsLoss(pos_weight=torch.Tensor([h['cls_pw']])).to(device)
  395. BCEobj = nn.BCEWithLogitsLoss(pos_weight=torch.Tensor([h['obj_pw']])).to(device)
  396. # Class label smoothing https://arxiv.org/pdf/1902.04103.pdf eqn 3
  397. cp, cn = smooth_BCE(eps=0.0)
  398. # Focal loss
  399. g = h['fl_gamma'] # focal loss gamma
  400. if g > 0:
  401. BCEcls, BCEobj = FocalLoss(BCEcls, g), FocalLoss(BCEobj, g)
  402. # Losses
  403. nt = 0 # number of targets
  404. np = len(p) # number of outputs
  405. balance = [4.0, 1.0, 0.4] if np == 3 else [4.0, 1.0, 0.4, 0.1] # P3-5 or P3-6
  406. for i, pi in enumerate(p): # layer index, layer predictions
  407. b, a, gj, gi = indices[i] # image, anchor, gridy, gridx
  408. tobj = torch.zeros_like(pi[..., 0], device=device) # target obj
  409. n = b.shape[0] # number of targets
  410. if n:
  411. nt += n # cumulative targets
  412. ps = pi[b, a, gj, gi] # prediction subset corresponding to targets
  413. # Regression
  414. pxy = ps[:, :2].sigmoid() * 2. - 0.5
  415. pwh = (ps[:, 2:4].sigmoid() * 2) ** 2 * anchors[i]
  416. pbox = torch.cat((pxy, pwh), 1).to(device) # predicted box
  417. giou = bbox_iou(pbox.T, tbox[i], x1y1x2y2=False, CIoU=True) # giou(prediction, target)
  418. lbox += (1.0 - giou).mean() # giou loss
  419. # Objectness
  420. tobj[b, a, gj, gi] = (1.0 - model.gr) + model.gr * giou.detach().clamp(0).type(tobj.dtype) # giou ratio
  421. # Classification
  422. if model.nc > 1: # cls loss (only if multiple classes)
  423. t = torch.full_like(ps[:, 5:], cn, device=device) # targets
  424. t[range(n), tcls[i]] = cp
  425. lcls += BCEcls(ps[:, 5:], t) # BCE
  426. # Append targets to text file
  427. # with open('targets.txt', 'a') as file:
  428. # [file.write('%11.5g ' * 4 % tuple(x) + '\n') for x in torch.cat((txy[i], twh[i]), 1)]
  429. lobj += BCEobj(pi[..., 4], tobj) * balance[i] # obj loss
  430. s = 3 / np # output count scaling
  431. lbox *= h['giou'] * s
  432. lobj *= h['obj'] * s * (1.4 if np == 4 else 1.)
  433. lcls *= h['cls'] * s
  434. bs = tobj.shape[0] # batch size
  435. loss = lbox + lobj + lcls
  436. return loss * bs, torch.cat((lbox, lobj, lcls, loss)).detach()
  437. def build_targets(p, targets, model):
  438. # Build targets for compute_loss(), input targets(image,class,x,y,w,h)
  439. det = model.module.model[-1] if is_parallel(model) else model.model[-1] # Detect() module
  440. na, nt = det.na, targets.shape[0] # number of anchors, targets
  441. tcls, tbox, indices, anch = [], [], [], []
  442. gain = torch.ones(7, device=targets.device) # normalized to gridspace gain
  443. ai = torch.arange(na, device=targets.device).float().view(na, 1).repeat(1, nt) # same as .repeat_interleave(nt)
  444. targets = torch.cat((targets.repeat(na, 1, 1), ai[:, :, None]), 2) # append anchor indices
  445. g = 0.5 # bias
  446. off = torch.tensor([[0, 0],
  447. [1, 0], [0, 1], [-1, 0], [0, -1], # j,k,l,m
  448. # [1, 1], [1, -1], [-1, 1], [-1, -1], # jk,jm,lk,lm
  449. ], device=targets.device).float() * g # offsets
  450. for i in range(det.nl):
  451. anchors = det.anchors[i]
  452. gain[2:6] = torch.tensor(p[i].shape)[[3, 2, 3, 2]] # xyxy gain
  453. # Match targets to anchors
  454. t = targets * gain
  455. if nt:
  456. # Matches
  457. r = t[:, :, 4:6] / anchors[:, None] # wh ratio
  458. j = torch.max(r, 1. / r).max(2)[0] < model.hyp['anchor_t'] # compare
  459. # j = wh_iou(anchors, t[:, 4:6]) > model.hyp['iou_t'] # iou(3,n)=wh_iou(anchors(3,2), gwh(n,2))
  460. t = t[j] # filter
  461. # Offsets
  462. gxy = t[:, 2:4] # grid xy
  463. gxi = gain[[2, 3]] - gxy # inverse
  464. j, k = ((gxy % 1. < g) & (gxy > 1.)).T
  465. l, m = ((gxi % 1. < g) & (gxi > 1.)).T
  466. j = torch.stack((torch.ones_like(j), j, k, l, m))
  467. t = t.repeat((5, 1, 1))[j]
  468. offsets = (torch.zeros_like(gxy)[None] + off[:, None])[j]
  469. else:
  470. t = targets[0]
  471. offsets = 0
  472. # Define
  473. b, c = t[:, :2].long().T # image, class
  474. gxy = t[:, 2:4] # grid xy
  475. gwh = t[:, 4:6] # grid wh
  476. gij = (gxy - offsets).long()
  477. gi, gj = gij.T # grid xy indices
  478. # Append
  479. a = t[:, 6].long() # anchor indices
  480. indices.append((b, a, gj, gi)) # image, anchor, grid indices
  481. tbox.append(torch.cat((gxy - gij, gwh), 1)) # box
  482. anch.append(anchors[a]) # anchors
  483. tcls.append(c) # class
  484. return tcls, tbox, indices, anch
  485. def non_max_suppression(prediction, conf_thres=0.1, iou_thres=0.6, merge=False, classes=None, agnostic=False):
  486. """Performs Non-Maximum Suppression (NMS) on inference results
  487. Returns:
  488. detections with shape: nx6 (x1, y1, x2, y2, conf, cls)
  489. """
  490. nc = prediction[0].shape[1] - 5 # number of classes
  491. xc = prediction[..., 4] > conf_thres # candidates
  492. # Settings
  493. min_wh, max_wh = 2, 4096 # (pixels) minimum and maximum box width and height
  494. max_det = 300 # maximum number of detections per image
  495. time_limit = 10.0 # seconds to quit after
  496. redundant = True # require redundant detections
  497. multi_label = nc > 1 # multiple labels per box (adds 0.5ms/img)
  498. t = time.time()
  499. output = [None] * prediction.shape[0]
  500. for xi, x in enumerate(prediction): # image index, image inference
  501. # Apply constraints
  502. # x[((x[..., 2:4] < min_wh) | (x[..., 2:4] > max_wh)).any(1), 4] = 0 # width-height
  503. x = x[xc[xi]] # confidence
  504. # If none remain process next image
  505. if not x.shape[0]:
  506. continue
  507. # Compute conf
  508. x[:, 5:] *= x[:, 4:5] # conf = obj_conf * cls_conf
  509. # Box (center x, center y, width, height) to (x1, y1, x2, y2)
  510. box = xywh2xyxy(x[:, :4])
  511. # Detections matrix nx6 (xyxy, conf, cls)
  512. if multi_label:
  513. i, j = (x[:, 5:] > conf_thres).nonzero(as_tuple=False).T
  514. x = torch.cat((box[i], x[i, j + 5, None], j[:, None].float()), 1)
  515. else: # best class only
  516. conf, j = x[:, 5:].max(1, keepdim=True)
  517. x = torch.cat((box, conf, j.float()), 1)[conf.view(-1) > conf_thres]
  518. # Filter by class
  519. if classes:
  520. x = x[(x[:, 5:6] == torch.tensor(classes, device=x.device)).any(1)]
  521. # Apply finite constraint
  522. # if not torch.isfinite(x).all():
  523. # x = x[torch.isfinite(x).all(1)]
  524. # If none remain process next image
  525. n = x.shape[0] # number of boxes
  526. if not n:
  527. continue
  528. # Sort by confidence
  529. # x = x[x[:, 4].argsort(descending=True)]
  530. # Batched NMS
  531. c = x[:, 5:6] * (0 if agnostic else max_wh) # classes
  532. boxes, scores = x[:, :4] + c, x[:, 4] # boxes (offset by class), scores
  533. i = torch.ops.torchvision.nms(boxes, scores, iou_thres)
  534. if i.shape[0] > max_det: # limit detections
  535. i = i[:max_det]
  536. if merge and (1 < n < 3E3): # Merge NMS (boxes merged using weighted mean)
  537. try: # update boxes as boxes(i,4) = weights(i,n) * boxes(n,4)
  538. iou = box_iou(boxes[i], boxes) > iou_thres # iou matrix
  539. weights = iou * scores[None] # box weights
  540. x[i, :4] = torch.mm(weights, x[:, :4]).float() / weights.sum(1, keepdim=True) # merged boxes
  541. if redundant:
  542. i = i[iou.sum(1) > 1] # require redundancy
  543. except: # possible CUDA error https://github.com/ultralytics/yolov3/issues/1139
  544. print(x, i, x.shape, i.shape)
  545. pass
  546. output[xi] = x[i]
  547. if (time.time() - t) > time_limit:
  548. break # time limit exceeded
  549. return output
  550. def strip_optimizer(f='weights/best.pt', s=''): # from utils.general import *; strip_optimizer()
  551. # Strip optimizer from 'f' to finalize training, optionally save as 's'
  552. x = torch.load(f, map_location=torch.device('cpu'))
  553. x['optimizer'] = None
  554. x['training_results'] = None
  555. x['epoch'] = -1
  556. x['model'].half() # to FP16
  557. for p in x['model'].parameters():
  558. p.requires_grad = False
  559. torch.save(x, s or f)
  560. mb = os.path.getsize(s or f) / 1E6 # filesize
  561. print('Optimizer stripped from %s,%s %.1fMB' % (f, (' saved as %s,' % s) if s else '', mb))
  562. def coco_class_count(path='../coco/labels/train2014/'):
  563. # Histogram of occurrences per class
  564. nc = 80 # number classes
  565. x = np.zeros(nc, dtype='int32')
  566. files = sorted(glob.glob('%s/*.*' % path))
  567. for i, file in enumerate(files):
  568. labels = np.loadtxt(file, dtype=np.float32).reshape(-1, 5)
  569. x += np.bincount(labels[:, 0].astype('int32'), minlength=nc)
  570. print(i, len(files))
  571. def coco_only_people(path='../coco/labels/train2017/'): # from utils.general import *; coco_only_people()
  572. # Find images with only people
  573. files = sorted(glob.glob('%s/*.*' % path))
  574. for i, file in enumerate(files):
  575. labels = np.loadtxt(file, dtype=np.float32).reshape(-1, 5)
  576. if all(labels[:, 0] == 0):
  577. print(labels.shape[0], file)
  578. def crop_images_random(path='../images/', scale=0.50): # from utils.general import *; crop_images_random()
  579. # crops images into random squares up to scale fraction
  580. # WARNING: overwrites images!
  581. for file in tqdm(sorted(glob.glob('%s/*.*' % path))):
  582. img = cv2.imread(file) # BGR
  583. if img is not None:
  584. h, w = img.shape[:2]
  585. # create random mask
  586. a = 30 # minimum size (pixels)
  587. mask_h = random.randint(a, int(max(a, h * scale))) # mask height
  588. mask_w = mask_h # mask width
  589. # box
  590. xmin = max(0, random.randint(0, w) - mask_w // 2)
  591. ymin = max(0, random.randint(0, h) - mask_h // 2)
  592. xmax = min(w, xmin + mask_w)
  593. ymax = min(h, ymin + mask_h)
  594. # apply random color mask
  595. cv2.imwrite(file, img[ymin:ymax, xmin:xmax])
  596. def coco_single_class_labels(path='../coco/labels/train2014/', label_class=43):
  597. # Makes single-class coco datasets. from utils.general import *; coco_single_class_labels()
  598. if os.path.exists('new/'):
  599. shutil.rmtree('new/') # delete output folder
  600. os.makedirs('new/') # make new output folder
  601. os.makedirs('new/labels/')
  602. os.makedirs('new/images/')
  603. for file in tqdm(sorted(glob.glob('%s/*.*' % path))):
  604. with open(file, 'r') as f:
  605. labels = np.array([x.split() for x in f.read().splitlines()], dtype=np.float32)
  606. i = labels[:, 0] == label_class
  607. if any(i):
  608. img_file = file.replace('labels', 'images').replace('txt', 'jpg')
  609. labels[:, 0] = 0 # reset class to 0
  610. with open('new/images.txt', 'a') as f: # add image to dataset list
  611. f.write(img_file + '\n')
  612. with open('new/labels/' + Path(file).name, 'a') as f: # write label
  613. for l in labels[i]:
  614. f.write('%g %.6f %.6f %.6f %.6f\n' % tuple(l))
  615. shutil.copyfile(src=img_file, dst='new/images/' + Path(file).name.replace('txt', 'jpg')) # copy images
  616. def kmean_anchors(path='./data/coco128.yaml', n=9, img_size=640, thr=4.0, gen=1000, verbose=True):
  617. """ Creates kmeans-evolved anchors from training dataset
  618. Arguments:
  619. path: path to dataset *.yaml, or a loaded dataset
  620. n: number of anchors
  621. img_size: image size used for training
  622. thr: anchor-label wh ratio threshold hyperparameter hyp['anchor_t'] used for training, default=4.0
  623. gen: generations to evolve anchors using genetic algorithm
  624. Return:
  625. k: kmeans evolved anchors
  626. Usage:
  627. from utils.general import *; _ = kmean_anchors()
  628. """
  629. thr = 1. / thr
  630. def metric(k, wh): # compute metrics
  631. r = wh[:, None] / k[None]
  632. x = torch.min(r, 1. / r).min(2)[0] # ratio metric
  633. # x = wh_iou(wh, torch.tensor(k)) # iou metric
  634. return x, x.max(1)[0] # x, best_x
  635. def fitness(k): # mutation fitness
  636. _, best = metric(torch.tensor(k, dtype=torch.float32), wh)
  637. return (best * (best > thr).float()).mean() # fitness
  638. def print_results(k):
  639. k = k[np.argsort(k.prod(1))] # sort small to large
  640. x, best = metric(k, wh0)
  641. bpr, aat = (best > thr).float().mean(), (x > thr).float().mean() * n # best possible recall, anch > thr
  642. print('thr=%.2f: %.4f best possible recall, %.2f anchors past thr' % (thr, bpr, aat))
  643. print('n=%g, img_size=%s, metric_all=%.3f/%.3f-mean/best, past_thr=%.3f-mean: ' %
  644. (n, img_size, x.mean(), best.mean(), x[x > thr].mean()), end='')
  645. for i, x in enumerate(k):
  646. print('%i,%i' % (round(x[0]), round(x[1])), end=', ' if i < len(k) - 1 else '\n') # use in *.cfg
  647. return k
  648. if isinstance(path, str): # *.yaml file
  649. with open(path) as f:
  650. data_dict = yaml.load(f, Loader=yaml.FullLoader) # model dict
  651. from utils.datasets import LoadImagesAndLabels
  652. dataset = LoadImagesAndLabels(data_dict['train'], augment=True, rect=True)
  653. else:
  654. dataset = path # dataset
  655. # Get label wh
  656. shapes = img_size * dataset.shapes / dataset.shapes.max(1, keepdims=True)
  657. wh0 = np.concatenate([l[:, 3:5] * s for s, l in zip(shapes, dataset.labels)]) # wh
  658. # Filter
  659. i = (wh0 < 3.0).any(1).sum()
  660. if i:
  661. print('WARNING: Extremely small objects found. '
  662. '%g of %g labels are < 3 pixels in width or height.' % (i, len(wh0)))
  663. wh = wh0[(wh0 >= 2.0).any(1)] # filter > 2 pixels
  664. # Kmeans calculation
  665. print('Running kmeans for %g anchors on %g points...' % (n, len(wh)))
  666. s = wh.std(0) # sigmas for whitening
  667. k, dist = kmeans(wh / s, n, iter=30) # points, mean distance
  668. k *= s
  669. wh = torch.tensor(wh, dtype=torch.float32) # filtered
  670. wh0 = torch.tensor(wh0, dtype=torch.float32) # unflitered
  671. k = print_results(k)
  672. # Plot
  673. # k, d = [None] * 20, [None] * 20
  674. # for i in tqdm(range(1, 21)):
  675. # k[i-1], d[i-1] = kmeans(wh / s, i) # points, mean distance
  676. # fig, ax = plt.subplots(1, 2, figsize=(14, 7))
  677. # ax = ax.ravel()
  678. # ax[0].plot(np.arange(1, 21), np.array(d) ** 2, marker='.')
  679. # fig, ax = plt.subplots(1, 2, figsize=(14, 7)) # plot wh
  680. # ax[0].hist(wh[wh[:, 0]<100, 0],400)
  681. # ax[1].hist(wh[wh[:, 1]<100, 1],400)
  682. # fig.tight_layout()
  683. # fig.savefig('wh.png', dpi=200)
  684. # Evolve
  685. npr = np.random
  686. f, sh, mp, s = fitness(k), k.shape, 0.9, 0.1 # fitness, generations, mutation prob, sigma
  687. pbar = tqdm(range(gen), desc='Evolving anchors with Genetic Algorithm') # progress bar
  688. for _ in pbar:
  689. v = np.ones(sh)
  690. while (v == 1).all(): # mutate until a change occurs (prevent duplicates)
  691. v = ((npr.random(sh) < mp) * npr.random() * npr.randn(*sh) * s + 1).clip(0.3, 3.0)
  692. kg = (k.copy() * v).clip(min=2.0)
  693. fg = fitness(kg)
  694. if fg > f:
  695. f, k = fg, kg.copy()
  696. pbar.desc = 'Evolving anchors with Genetic Algorithm: fitness = %.4f' % f
  697. if verbose:
  698. print_results(k)
  699. return print_results(k)
  700. def print_mutation(hyp, results, yaml_file='hyp_evolved.yaml', bucket=''):
  701. # Print mutation results to evolve.txt (for use with train.py --evolve)
  702. a = '%10s' * len(hyp) % tuple(hyp.keys()) # hyperparam keys
  703. b = '%10.3g' * len(hyp) % tuple(hyp.values()) # hyperparam values
  704. c = '%10.4g' * len(results) % results # results (P, R, mAP@0.5, mAP@0.5:0.95, val_losses x 3)
  705. print('\n%s\n%s\nEvolved fitness: %s\n' % (a, b, c))
  706. if bucket:
  707. url = 'gs://%s/evolve.txt' % bucket
  708. if gsutil_getsize(url) > (os.path.getsize('evolve.txt') if os.path.exists('evolve.txt') else 0):
  709. os.system('gsutil cp %s .' % url) # download evolve.txt if larger than local
  710. with open('evolve.txt', 'a') as f: # append result
  711. f.write(c + b + '\n')
  712. x = np.unique(np.loadtxt('evolve.txt', ndmin=2), axis=0) # load unique rows
  713. x = x[np.argsort(-fitness(x))] # sort
  714. np.savetxt('evolve.txt', x, '%10.3g') # save sort by fitness
  715. # Save yaml
  716. for i, k in enumerate(hyp.keys()):
  717. hyp[k] = float(x[0, i + 7])
  718. with open(yaml_file, 'w') as f:
  719. results = tuple(x[0, :7])
  720. c = '%10.4g' * len(results) % results # results (P, R, mAP@0.5, mAP@0.5:0.95, val_losses x 3)
  721. f.write('# Hyperparameter Evolution Results\n# Generations: %g\n# Metrics: ' % len(x) + c + '\n\n')
  722. yaml.dump(hyp, f, sort_keys=False)
  723. if bucket:
  724. os.system('gsutil cp evolve.txt %s gs://%s' % (yaml_file, bucket)) # upload
  725. def apply_classifier(x, model, img, im0):
  726. # applies a second stage classifier to yolo outputs
  727. im0 = [im0] if isinstance(im0, np.ndarray) else im0
  728. for i, d in enumerate(x): # per image
  729. if d is not None and len(d):
  730. d = d.clone()
  731. # Reshape and pad cutouts
  732. b = xyxy2xywh(d[:, :4]) # boxes
  733. b[:, 2:] = b[:, 2:].max(1)[0].unsqueeze(1) # rectangle to square
  734. b[:, 2:] = b[:, 2:] * 1.3 + 30 # pad
  735. d[:, :4] = xywh2xyxy(b).long()
  736. # Rescale boxes from img_size to im0 size
  737. scale_coords(img.shape[2:], d[:, :4], im0[i].shape)
  738. # Classes
  739. pred_cls1 = d[:, 5].long()
  740. ims = []
  741. for j, a in enumerate(d): # per item
  742. cutout = im0[i][int(a[1]):int(a[3]), int(a[0]):int(a[2])]
  743. im = cv2.resize(cutout, (224, 224)) # BGR
  744. # cv2.imwrite('test%i.jpg' % j, cutout)
  745. im = im[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, to 3x416x416
  746. im = np.ascontiguousarray(im, dtype=np.float32) # uint8 to float32
  747. im /= 255.0 # 0 - 255 to 0.0 - 1.0
  748. ims.append(im)
  749. pred_cls2 = model(torch.Tensor(ims).to(d.device)).argmax(1) # classifier prediction
  750. x[i] = x[i][pred_cls1 == pred_cls2] # retain matching class detections
  751. return x
  752. def fitness(x):
  753. # Returns fitness (for use with results.txt or evolve.txt)
  754. w = [0.0, 0.0, 0.1, 0.9] # weights for [P, R, mAP@0.5, mAP@0.5:0.95]
  755. return (x[:, :4] * w).sum(1)
  756. def output_to_target(output, width, height):
  757. # Convert model output to target format [batch_id, class_id, x, y, w, h, conf]
  758. if isinstance(output, torch.Tensor):
  759. output = output.cpu().numpy()
  760. targets = []
  761. for i, o in enumerate(output):
  762. if o is not None:
  763. for pred in o:
  764. box = pred[:4]
  765. w = (box[2] - box[0]) / width
  766. h = (box[3] - box[1]) / height
  767. x = box[0] / width + w / 2
  768. y = box[1] / height + h / 2
  769. conf = pred[4]
  770. cls = int(pred[5])
  771. targets.append([i, cls, x, y, w, h, conf])
  772. return np.array(targets)
  773. def increment_dir(dir, comment=''):
  774. # Increments a directory runs/exp1 --> runs/exp2_comment
  775. n = 0 # number
  776. dir = str(Path(dir)) # os-agnostic
  777. d = sorted(glob.glob(dir + '*')) # directories
  778. if len(d):
  779. n = max([int(x[len(dir):x.rfind('_') if '_' in Path(x).name else None]) for x in d]) + 1 # increment
  780. return dir + str(n) + ('_' + comment if comment else '')
  781. # Plotting functions ---------------------------------------------------------------------------------------------------
  782. def hist2d(x, y, n=100):
  783. # 2d histogram used in labels.png and evolve.png
  784. xedges, yedges = np.linspace(x.min(), x.max(), n), np.linspace(y.min(), y.max(), n)
  785. hist, xedges, yedges = np.histogram2d(x, y, (xedges, yedges))
  786. xidx = np.clip(np.digitize(x, xedges) - 1, 0, hist.shape[0] - 1)
  787. yidx = np.clip(np.digitize(y, yedges) - 1, 0, hist.shape[1] - 1)
  788. return np.log(hist[xidx, yidx])
  789. def butter_lowpass_filtfilt(data, cutoff=1500, fs=50000, order=5):
  790. # https://stackoverflow.com/questions/28536191/how-to-filter-smooth-with-scipy-numpy
  791. def butter_lowpass(cutoff, fs, order):
  792. nyq = 0.5 * fs
  793. normal_cutoff = cutoff / nyq
  794. b, a = butter(order, normal_cutoff, btype='low', analog=False)
  795. return b, a
  796. b, a = butter_lowpass(cutoff, fs, order=order)
  797. return filtfilt(b, a, data) # forward-backward filter
  798. def plot_one_box(x, img, color=None, label=None, line_thickness=None):
  799. # Plots one bounding box on image img
  800. tl = line_thickness or round(0.002 * (img.shape[0] + img.shape[1]) / 2) + 1 # line/font thickness
  801. color = color or [random.randint(0, 255) for _ in range(3)]
  802. c1, c2 = (int(x[0]), int(x[1])), (int(x[2]), int(x[3]))
  803. cv2.rectangle(img, c1, c2, color, thickness=tl, lineType=cv2.LINE_AA)
  804. if label:
  805. tf = max(tl - 1, 1) # font thickness
  806. t_size = cv2.getTextSize(label, 0, fontScale=tl / 3, thickness=tf)[0]
  807. c2 = c1[0] + t_size[0], c1[1] - t_size[1] - 3
  808. cv2.rectangle(img, c1, c2, color, -1, cv2.LINE_AA) # filled
  809. cv2.putText(img, label, (c1[0], c1[1] - 2), 0, tl / 3, [225, 255, 255], thickness=tf, lineType=cv2.LINE_AA)
  810. def plot_wh_methods(): # from utils.general import *; plot_wh_methods()
  811. # Compares the two methods for width-height anchor multiplication
  812. # https://github.com/ultralytics/yolov3/issues/168
  813. x = np.arange(-4.0, 4.0, .1)
  814. ya = np.exp(x)
  815. yb = torch.sigmoid(torch.from_numpy(x)).numpy() * 2
  816. fig = plt.figure(figsize=(6, 3), dpi=150)
  817. plt.plot(x, ya, '.-', label='YOLOv3')
  818. plt.plot(x, yb ** 2, '.-', label='YOLOv5 ^2')
  819. plt.plot(x, yb ** 1.6, '.-', label='YOLOv5 ^1.6')
  820. plt.xlim(left=-4, right=4)
  821. plt.ylim(bottom=0, top=6)
  822. plt.xlabel('input')
  823. plt.ylabel('output')
  824. plt.grid()
  825. plt.legend()
  826. fig.tight_layout()
  827. fig.savefig('comparison.png', dpi=200)
  828. def plot_images(images, targets, paths=None, fname='images.jpg', names=None, max_size=640, max_subplots=16):
  829. tl = 3 # line thickness
  830. tf = max(tl - 1, 1) # font thickness
  831. if isinstance(images, torch.Tensor):
  832. images = images.cpu().float().numpy()
  833. if isinstance(targets, torch.Tensor):
  834. targets = targets.cpu().numpy()
  835. # un-normalise
  836. if np.max(images[0]) <= 1:
  837. images *= 255
  838. bs, _, h, w = images.shape # batch size, _, height, width
  839. bs = min(bs, max_subplots) # limit plot images
  840. ns = np.ceil(bs ** 0.5) # number of subplots (square)
  841. # Check if we should resize
  842. scale_factor = max_size / max(h, w)
  843. if scale_factor < 1:
  844. h = math.ceil(scale_factor * h)
  845. w = math.ceil(scale_factor * w)
  846. # Empty array for output
  847. mosaic = np.full((int(ns * h), int(ns * w), 3), 255, dtype=np.uint8)
  848. # Fix class - colour map
  849. prop_cycle = plt.rcParams['axes.prop_cycle']
  850. # https://stackoverflow.com/questions/51350872/python-from-color-name-to-rgb
  851. hex2rgb = lambda h: tuple(int(h[1 + i:1 + i + 2], 16) for i in (0, 2, 4))
  852. color_lut = [hex2rgb(h) for h in prop_cycle.by_key()['color']]
  853. for i, img in enumerate(images):
  854. if i == max_subplots: # if last batch has fewer images than we expect
  855. break
  856. block_x = int(w * (i // ns))
  857. block_y = int(h * (i % ns))
  858. img = img.transpose(1, 2, 0)
  859. if scale_factor < 1:
  860. img = cv2.resize(img, (w, h))
  861. mosaic[block_y:block_y + h, block_x:block_x + w, :] = img
  862. if len(targets) > 0:
  863. image_targets = targets[targets[:, 0] == i]
  864. boxes = xywh2xyxy(image_targets[:, 2:6]).T
  865. classes = image_targets[:, 1].astype('int')
  866. gt = image_targets.shape[1] == 6 # ground truth if no conf column
  867. conf = None if gt else image_targets[:, 6] # check for confidence presence (gt vs pred)
  868. boxes[[0, 2]] *= w
  869. boxes[[0, 2]] += block_x
  870. boxes[[1, 3]] *= h
  871. boxes[[1, 3]] += block_y
  872. for j, box in enumerate(boxes.T):
  873. cls = int(classes[j])
  874. color = color_lut[cls % len(color_lut)]
  875. cls = names[cls] if names else cls
  876. if gt or conf[j] > 0.3: # 0.3 conf thresh
  877. label = '%s' % cls if gt else '%s %.1f' % (cls, conf[j])
  878. plot_one_box(box, mosaic, label=label, color=color, line_thickness=tl)
  879. # Draw image filename labels
  880. if paths is not None:
  881. label = os.path.basename(paths[i])[:40] # trim to 40 char
  882. t_size = cv2.getTextSize(label, 0, fontScale=tl / 3, thickness=tf)[0]
  883. cv2.putText(mosaic, label, (block_x + 5, block_y + t_size[1] + 5), 0, tl / 3, [220, 220, 220], thickness=tf,
  884. lineType=cv2.LINE_AA)
  885. # Image border
  886. cv2.rectangle(mosaic, (block_x, block_y), (block_x + w, block_y + h), (255, 255, 255), thickness=3)
  887. if fname is not None:
  888. mosaic = cv2.resize(mosaic, (int(ns * w * 0.5), int(ns * h * 0.5)), interpolation=cv2.INTER_AREA)
  889. cv2.imwrite(fname, cv2.cvtColor(mosaic, cv2.COLOR_BGR2RGB))
  890. return mosaic
  891. def plot_lr_scheduler(optimizer, scheduler, epochs=300, save_dir=''):
  892. # Plot LR simulating training for full epochs
  893. optimizer, scheduler = copy(optimizer), copy(scheduler) # do not modify originals
  894. y = []
  895. for _ in range(epochs):
  896. scheduler.step()
  897. y.append(optimizer.param_groups[0]['lr'])
  898. plt.plot(y, '.-', label='LR')
  899. plt.xlabel('epoch')
  900. plt.ylabel('LR')
  901. plt.grid()
  902. plt.xlim(0, epochs)
  903. plt.ylim(0)
  904. plt.tight_layout()
  905. plt.savefig(Path(save_dir) / 'LR.png', dpi=200)
  906. def plot_test_txt(): # from utils.general import *; plot_test()
  907. # Plot test.txt histograms
  908. x = np.loadtxt('test.txt', dtype=np.float32)
  909. box = xyxy2xywh(x[:, :4])
  910. cx, cy = box[:, 0], box[:, 1]
  911. fig, ax = plt.subplots(1, 1, figsize=(6, 6), tight_layout=True)
  912. ax.hist2d(cx, cy, bins=600, cmax=10, cmin=0)
  913. ax.set_aspect('equal')
  914. plt.savefig('hist2d.png', dpi=300)
  915. fig, ax = plt.subplots(1, 2, figsize=(12, 6), tight_layout=True)
  916. ax[0].hist(cx, bins=600)
  917. ax[1].hist(cy, bins=600)
  918. plt.savefig('hist1d.png', dpi=200)
  919. def plot_targets_txt(): # from utils.general import *; plot_targets_txt()
  920. # Plot targets.txt histograms
  921. x = np.loadtxt('targets.txt', dtype=np.float32).T
  922. s = ['x targets', 'y targets', 'width targets', 'height targets']
  923. fig, ax = plt.subplots(2, 2, figsize=(8, 8), tight_layout=True)
  924. ax = ax.ravel()
  925. for i in range(4):
  926. ax[i].hist(x[i], bins=100, label='%.3g +/- %.3g' % (x[i].mean(), x[i].std()))
  927. ax[i].legend()
  928. ax[i].set_title(s[i])
  929. plt.savefig('targets.jpg', dpi=200)
  930. def plot_study_txt(f='study.txt', x=None): # from utils.general import *; plot_study_txt()
  931. # Plot study.txt generated by test.py
  932. fig, ax = plt.subplots(2, 4, figsize=(10, 6), tight_layout=True)
  933. ax = ax.ravel()
  934. fig2, ax2 = plt.subplots(1, 1, figsize=(8, 4), tight_layout=True)
  935. for f in ['study/study_coco_yolov5%s.txt' % x for x in ['s', 'm', 'l', 'x']]:
  936. y = np.loadtxt(f, dtype=np.float32, usecols=[0, 1, 2, 3, 7, 8, 9], ndmin=2).T
  937. x = np.arange(y.shape[1]) if x is None else np.array(x)
  938. s = ['P', 'R', 'mAP@.5', 'mAP@.5:.95', 't_inference (ms/img)', 't_NMS (ms/img)', 't_total (ms/img)']
  939. for i in range(7):
  940. ax[i].plot(x, y[i], '.-', linewidth=2, markersize=8)
  941. ax[i].set_title(s[i])
  942. j = y[3].argmax() + 1
  943. ax2.plot(y[6, :j], y[3, :j] * 1E2, '.-', linewidth=2, markersize=8,
  944. label=Path(f).stem.replace('study_coco_', '').replace('yolo', 'YOLO'))
  945. ax2.plot(1E3 / np.array([209, 140, 97, 58, 35, 18]), [34.6, 40.5, 43.0, 47.5, 49.7, 51.5],
  946. 'k.-', linewidth=2, markersize=8, alpha=.25, label='EfficientDet')
  947. ax2.grid()
  948. ax2.set_xlim(0, 30)
  949. ax2.set_ylim(28, 50)
  950. ax2.set_yticks(np.arange(30, 55, 5))
  951. ax2.set_xlabel('GPU Speed (ms/img)')
  952. ax2.set_ylabel('COCO AP val')
  953. ax2.legend(loc='lower right')
  954. plt.savefig('study_mAP_latency.png', dpi=300)
  955. plt.savefig(f.replace('.txt', '.png'), dpi=300)
  956. def plot_labels(labels, save_dir=''):
  957. # plot dataset labels
  958. c, b = labels[:, 0], labels[:, 1:].transpose() # classes, boxes
  959. nc = int(c.max() + 1) # number of classes
  960. fig, ax = plt.subplots(2, 2, figsize=(8, 8), tight_layout=True)
  961. ax = ax.ravel()
  962. ax[0].hist(c, bins=np.linspace(0, nc, nc + 1) - 0.5, rwidth=0.8)
  963. ax[0].set_xlabel('classes')
  964. ax[1].scatter(b[0], b[1], c=hist2d(b[0], b[1], 90), cmap='jet')
  965. ax[1].set_xlabel('x')
  966. ax[1].set_ylabel('y')
  967. ax[2].scatter(b[2], b[3], c=hist2d(b[2], b[3], 90), cmap='jet')
  968. ax[2].set_xlabel('width')
  969. ax[2].set_ylabel('height')
  970. plt.savefig(Path(save_dir) / 'labels.png', dpi=200)
  971. plt.close()
  972. # seaborn correlogram
  973. try:
  974. import seaborn as sns
  975. import pandas as pd
  976. x = pd.DataFrame(b.transpose(), columns=['x', 'y', 'width', 'height'])
  977. sns.pairplot(x, corner=True, diag_kind='hist', kind='scatter', markers='o',
  978. plot_kws=dict(s=3, edgecolor=None, linewidth=1, alpha=0.02),
  979. diag_kws=dict(bins=50))
  980. plt.savefig(Path(save_dir) / 'labels_correlogram.png', dpi=200)
  981. plt.close()
  982. except Exception as e:
  983. pass
  984. def plot_evolution(yaml_file='data/hyp.finetune.yaml'): # from utils.general import *; plot_evolution()
  985. # Plot hyperparameter evolution results in evolve.txt
  986. with open(yaml_file) as f:
  987. hyp = yaml.load(f, Loader=yaml.FullLoader)
  988. x = np.loadtxt('evolve.txt', ndmin=2)
  989. f = fitness(x)
  990. # weights = (f - f.min()) ** 2 # for weighted results
  991. plt.figure(figsize=(10, 12), tight_layout=True)
  992. matplotlib.rc('font', **{'size': 8})
  993. for i, (k, v) in enumerate(hyp.items()):
  994. y = x[:, i + 7]
  995. # mu = (y * weights).sum() / weights.sum() # best weighted result
  996. mu = y[f.argmax()] # best single result
  997. plt.subplot(6, 5, i + 1)
  998. plt.scatter(y, f, c=hist2d(y, f, 20), cmap='viridis', alpha=.8, edgecolors='none')
  999. plt.plot(mu, f.max(), 'k+', markersize=15)
  1000. plt.title('%s = %.3g' % (k, mu), fontdict={'size': 9}) # limit to 40 characters
  1001. if i % 5 != 0:
  1002. plt.yticks([])
  1003. print('%15s: %.3g' % (k, mu))
  1004. plt.savefig('evolve.png', dpi=200)
  1005. print('\nPlot saved as evolve.png')
  1006. def plot_results_overlay(start=0, stop=0): # from utils.general import *; plot_results_overlay()
  1007. # Plot training 'results*.txt', overlaying train and val losses
  1008. s = ['train', 'train', 'train', 'Precision', 'mAP@0.5', 'val', 'val', 'val', 'Recall', 'mAP@0.5:0.95'] # legends
  1009. t = ['GIoU', 'Objectness', 'Classification', 'P-R', 'mAP-F1'] # titles
  1010. for f in sorted(glob.glob('results*.txt') + glob.glob('../../Downloads/results*.txt')):
  1011. results = np.loadtxt(f, usecols=[2, 3, 4, 8, 9, 12, 13, 14, 10, 11], ndmin=2).T
  1012. n = results.shape[1] # number of rows
  1013. x = range(start, min(stop, n) if stop else n)
  1014. fig, ax = plt.subplots(1, 5, figsize=(14, 3.5), tight_layout=True)
  1015. ax = ax.ravel()
  1016. for i in range(5):
  1017. for j in [i, i + 5]:
  1018. y = results[j, x]
  1019. ax[i].plot(x, y, marker='.', label=s[j])
  1020. # y_smooth = butter_lowpass_filtfilt(y)
  1021. # ax[i].plot(x, np.gradient(y_smooth), marker='.', label=s[j])
  1022. ax[i].set_title(t[i])
  1023. ax[i].legend()
  1024. ax[i].set_ylabel(f) if i == 0 else None # add filename
  1025. fig.savefig(f.replace('.txt', '.png'), dpi=200)
  1026. def plot_results(start=0, stop=0, bucket='', id=(), labels=(),
  1027. save_dir=''): # from utils.general import *; plot_results()
  1028. # Plot training 'results*.txt' as seen in https://github.com/ultralytics/yolov5#reproduce-our-training
  1029. fig, ax = plt.subplots(2, 5, figsize=(12, 6))
  1030. ax = ax.ravel()
  1031. s = ['GIoU', 'Objectness', 'Classification', 'Precision', 'Recall',
  1032. 'val GIoU', 'val Objectness', 'val Classification', 'mAP@0.5', 'mAP@0.5:0.95']
  1033. if bucket:
  1034. # os.system('rm -rf storage.googleapis.com')
  1035. # files = ['https://storage.googleapis.com/%s/results%g.txt' % (bucket, x) for x in id]
  1036. files = ['results%g.txt' % x for x in id]
  1037. c = ('gsutil cp ' + '%s ' * len(files) + '.') % tuple('gs://%s/results%g.txt' % (bucket, x) for x in id)
  1038. os.system(c)
  1039. else:
  1040. files = glob.glob(str(Path(save_dir) / 'results*.txt')) + glob.glob('../../Downloads/results*.txt')
  1041. for fi, f in enumerate(files):
  1042. try:
  1043. results = np.loadtxt(f, usecols=[2, 3, 4, 8, 9, 12, 13, 14, 10, 11], ndmin=2).T
  1044. n = results.shape[1] # number of rows
  1045. x = range(start, min(stop, n) if stop else n)
  1046. for i in range(10):
  1047. y = results[i, x]
  1048. if i in [0, 1, 2, 5, 6, 7]:
  1049. y[y == 0] = np.nan # dont show zero loss values
  1050. # y /= y[0] # normalize
  1051. label = labels[fi] if len(labels) else Path(f).stem
  1052. ax[i].plot(x, y, marker='.', label=label, linewidth=1, markersize=6)
  1053. ax[i].set_title(s[i])
  1054. # if i in [5, 6, 7]: # share train and val loss y axes
  1055. # ax[i].get_shared_y_axes().join(ax[i], ax[i - 5])
  1056. except Exception as e:
  1057. print('Warning: Plotting error for %s; %s' % (f, e))
  1058. fig.tight_layout()
  1059. ax[1].legend()
  1060. fig.savefig(Path(save_dir) / 'results.png', dpi=200)