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.

435 lines
17KB

  1. # General utils
  2. import glob
  3. import logging
  4. import math
  5. import os
  6. import platform
  7. import random
  8. import re
  9. import subprocess
  10. import time
  11. from pathlib import Path
  12. import cv2
  13. import matplotlib
  14. import numpy as np
  15. import torch
  16. import yaml
  17. from utils.google_utils import gsutil_getsize
  18. from utils.metrics import fitness
  19. from utils.torch_utils import init_torch_seeds
  20. # Set printoptions
  21. torch.set_printoptions(linewidth=320, precision=5, profile='long')
  22. np.set_printoptions(linewidth=320, formatter={'float_kind': '{:11.5g}'.format}) # format short g, %precision=5
  23. matplotlib.rc('font', **{'size': 11})
  24. # Prevent OpenCV from multithreading (to use PyTorch DataLoader)
  25. cv2.setNumThreads(0)
  26. def set_logging(rank=-1):
  27. logging.basicConfig(
  28. format="%(message)s",
  29. level=logging.INFO if rank in [-1, 0] else logging.WARN)
  30. def init_seeds(seed=0):
  31. random.seed(seed)
  32. np.random.seed(seed)
  33. init_torch_seeds(seed)
  34. def get_latest_run(search_dir='.'):
  35. # Return path to most recent 'last.pt' in /runs (i.e. to --resume from)
  36. last_list = glob.glob(f'{search_dir}/**/last*.pt', recursive=True)
  37. return max(last_list, key=os.path.getctime) if last_list else ''
  38. def check_git_status():
  39. # Suggest 'git pull' if repo is out of date
  40. if platform.system() in ['Linux', 'Darwin'] and not os.path.isfile('/.dockerenv'):
  41. s = subprocess.check_output('if [ -d .git ]; then git fetch && git status -uno; fi', shell=True).decode('utf-8')
  42. if 'Your branch is behind' in s:
  43. print(s[s.find('Your branch is behind'):s.find('\n\n')] + '\n')
  44. def check_img_size(img_size, s=32):
  45. # Verify img_size is a multiple of stride s
  46. new_size = make_divisible(img_size, int(s)) # ceil gs-multiple
  47. if new_size != img_size:
  48. print('WARNING: --img-size %g must be multiple of max stride %g, updating to %g' % (img_size, s, new_size))
  49. return new_size
  50. def check_file(file):
  51. # Search for file if not found
  52. if os.path.isfile(file) or file == '':
  53. return file
  54. else:
  55. files = glob.glob('./**/' + file, recursive=True) # find file
  56. assert len(files), 'File Not Found: %s' % file # assert file was found
  57. assert len(files) == 1, "Multiple files match '%s', specify exact path: %s" % (file, files) # assert unique
  58. return files[0] # return file
  59. def check_dataset(dict):
  60. # Download dataset if not found locally
  61. val, s = dict.get('val'), dict.get('download')
  62. if val and len(val):
  63. val = [Path(x).resolve() for x in (val if isinstance(val, list) else [val])] # val path
  64. if not all(x.exists() for x in val):
  65. print('\nWARNING: Dataset not found, nonexistent paths: %s' % [str(x) for x in val if not x.exists()])
  66. if s and len(s): # download script
  67. print('Downloading %s ...' % s)
  68. if s.startswith('http') and s.endswith('.zip'): # URL
  69. f = Path(s).name # filename
  70. torch.hub.download_url_to_file(s, f)
  71. r = os.system('unzip -q %s -d ../ && rm %s' % (f, f)) # unzip
  72. else: # bash script
  73. r = os.system(s)
  74. print('Dataset autodownload %s\n' % ('success' if r == 0 else 'failure')) # analyze return value
  75. else:
  76. raise Exception('Dataset not found.')
  77. def make_divisible(x, divisor):
  78. # Returns x evenly divisible by divisor
  79. return math.ceil(x / divisor) * divisor
  80. def labels_to_class_weights(labels, nc=80):
  81. # Get class weights (inverse frequency) from training labels
  82. if labels[0] is None: # no labels loaded
  83. return torch.Tensor()
  84. labels = np.concatenate(labels, 0) # labels.shape = (866643, 5) for COCO
  85. classes = labels[:, 0].astype(np.int) # labels = [class xywh]
  86. weights = np.bincount(classes, minlength=nc) # occurrences per class
  87. # Prepend gridpoint count (for uCE training)
  88. # gpi = ((320 / 32 * np.array([1, 2, 4])) ** 2 * 3).sum() # gridpoints per image
  89. # weights = np.hstack([gpi * len(labels) - weights.sum() * 9, weights * 9]) ** 0.5 # prepend gridpoints to start
  90. weights[weights == 0] = 1 # replace empty bins with 1
  91. weights = 1 / weights # number of targets per class
  92. weights /= weights.sum() # normalize
  93. return torch.from_numpy(weights)
  94. def labels_to_image_weights(labels, nc=80, class_weights=np.ones(80)):
  95. # Produces image weights based on class mAPs
  96. n = len(labels)
  97. class_counts = np.array([np.bincount(labels[i][:, 0].astype(np.int), minlength=nc) for i in range(n)])
  98. image_weights = (class_weights.reshape(1, nc) * class_counts).sum(1)
  99. # index = random.choices(range(n), weights=image_weights, k=1) # weight image sample
  100. return image_weights
  101. def coco80_to_coco91_class(): # converts 80-index (val2014) to 91-index (paper)
  102. # https://tech.amikelive.com/node-718/what-object-categories-labels-are-in-coco-dataset/
  103. # a = np.loadtxt('data/coco.names', dtype='str', delimiter='\n')
  104. # b = np.loadtxt('data/coco_paper.names', dtype='str', delimiter='\n')
  105. # x1 = [list(a[i] == b).index(True) + 1 for i in range(80)] # darknet to coco
  106. # x2 = [list(b[i] == a).index(True) if any(b[i] == a) else None for i in range(91)] # coco to darknet
  107. 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,
  108. 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,
  109. 64, 65, 67, 70, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 84, 85, 86, 87, 88, 89, 90]
  110. return x
  111. def xyxy2xywh(x):
  112. # Convert nx4 boxes from [x1, y1, x2, y2] to [x, y, w, h] where xy1=top-left, xy2=bottom-right
  113. y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x)
  114. y[:, 0] = (x[:, 0] + x[:, 2]) / 2 # x center
  115. y[:, 1] = (x[:, 1] + x[:, 3]) / 2 # y center
  116. y[:, 2] = x[:, 2] - x[:, 0] # width
  117. y[:, 3] = x[:, 3] - x[:, 1] # height
  118. return y
  119. def xywh2xyxy(x):
  120. # Convert nx4 boxes from [x, y, w, h] to [x1, y1, x2, y2] where xy1=top-left, xy2=bottom-right
  121. y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x)
  122. y[:, 0] = x[:, 0] - x[:, 2] / 2 # top left x
  123. y[:, 1] = x[:, 1] - x[:, 3] / 2 # top left y
  124. y[:, 2] = x[:, 0] + x[:, 2] / 2 # bottom right x
  125. y[:, 3] = x[:, 1] + x[:, 3] / 2 # bottom right y
  126. return y
  127. def scale_coords(img1_shape, coords, img0_shape, ratio_pad=None):
  128. # Rescale coords (xyxy) from img1_shape to img0_shape
  129. if ratio_pad is None: # calculate from img0_shape
  130. gain = min(img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1]) # gain = old / new
  131. pad = (img1_shape[1] - img0_shape[1] * gain) / 2, (img1_shape[0] - img0_shape[0] * gain) / 2 # wh padding
  132. else:
  133. gain = ratio_pad[0][0]
  134. pad = ratio_pad[1]
  135. coords[:, [0, 2]] -= pad[0] # x padding
  136. coords[:, [1, 3]] -= pad[1] # y padding
  137. coords[:, :4] /= gain
  138. clip_coords(coords, img0_shape)
  139. return coords
  140. def clip_coords(boxes, img_shape):
  141. # Clip bounding xyxy bounding boxes to image shape (height, width)
  142. boxes[:, 0].clamp_(0, img_shape[1]) # x1
  143. boxes[:, 1].clamp_(0, img_shape[0]) # y1
  144. boxes[:, 2].clamp_(0, img_shape[1]) # x2
  145. boxes[:, 3].clamp_(0, img_shape[0]) # y2
  146. def bbox_iou(box1, box2, x1y1x2y2=True, GIoU=False, DIoU=False, CIoU=False, eps=1e-9):
  147. # Returns the IoU of box1 to box2. box1 is 4, box2 is nx4
  148. box2 = box2.T
  149. # Get the coordinates of bounding boxes
  150. if x1y1x2y2: # x1, y1, x2, y2 = box1
  151. b1_x1, b1_y1, b1_x2, b1_y2 = box1[0], box1[1], box1[2], box1[3]
  152. b2_x1, b2_y1, b2_x2, b2_y2 = box2[0], box2[1], box2[2], box2[3]
  153. else: # transform from xywh to xyxy
  154. b1_x1, b1_x2 = box1[0] - box1[2] / 2, box1[0] + box1[2] / 2
  155. b1_y1, b1_y2 = box1[1] - box1[3] / 2, box1[1] + box1[3] / 2
  156. b2_x1, b2_x2 = box2[0] - box2[2] / 2, box2[0] + box2[2] / 2
  157. b2_y1, b2_y2 = box2[1] - box2[3] / 2, box2[1] + box2[3] / 2
  158. # Intersection area
  159. inter = (torch.min(b1_x2, b2_x2) - torch.max(b1_x1, b2_x1)).clamp(0) * \
  160. (torch.min(b1_y2, b2_y2) - torch.max(b1_y1, b2_y1)).clamp(0)
  161. # Union Area
  162. w1, h1 = b1_x2 - b1_x1, b1_y2 - b1_y1 + eps
  163. w2, h2 = b2_x2 - b2_x1, b2_y2 - b2_y1 + eps
  164. union = w1 * h1 + w2 * h2 - inter + eps
  165. iou = inter / union
  166. if GIoU or DIoU or CIoU:
  167. cw = torch.max(b1_x2, b2_x2) - torch.min(b1_x1, b2_x1) # convex (smallest enclosing box) width
  168. ch = torch.max(b1_y2, b2_y2) - torch.min(b1_y1, b2_y1) # convex height
  169. if CIoU or DIoU: # Distance or Complete IoU https://arxiv.org/abs/1911.08287v1
  170. c2 = cw ** 2 + ch ** 2 + eps # convex diagonal squared
  171. rho2 = ((b2_x1 + b2_x2 - b1_x1 - b1_x2) ** 2 +
  172. (b2_y1 + b2_y2 - b1_y1 - b1_y2) ** 2) / 4 # center distance squared
  173. if DIoU:
  174. return iou - rho2 / c2 # DIoU
  175. elif CIoU: # https://github.com/Zzh-tju/DIoU-SSD-pytorch/blob/master/utils/box/box_utils.py#L47
  176. v = (4 / math.pi ** 2) * torch.pow(torch.atan(w2 / h2) - torch.atan(w1 / h1), 2)
  177. with torch.no_grad():
  178. alpha = v / ((1 + eps) - iou + v)
  179. return iou - (rho2 / c2 + v * alpha) # CIoU
  180. else: # GIoU https://arxiv.org/pdf/1902.09630.pdf
  181. c_area = cw * ch + eps # convex area
  182. return iou - (c_area - union) / c_area # GIoU
  183. else:
  184. return iou # IoU
  185. def box_iou(box1, box2):
  186. # https://github.com/pytorch/vision/blob/master/torchvision/ops/boxes.py
  187. """
  188. Return intersection-over-union (Jaccard index) of boxes.
  189. Both sets of boxes are expected to be in (x1, y1, x2, y2) format.
  190. Arguments:
  191. box1 (Tensor[N, 4])
  192. box2 (Tensor[M, 4])
  193. Returns:
  194. iou (Tensor[N, M]): the NxM matrix containing the pairwise
  195. IoU values for every element in boxes1 and boxes2
  196. """
  197. def box_area(box):
  198. # box = 4xn
  199. return (box[2] - box[0]) * (box[3] - box[1])
  200. area1 = box_area(box1.T)
  201. area2 = box_area(box2.T)
  202. # inter(N,M) = (rb(N,M,2) - lt(N,M,2)).clamp(0).prod(2)
  203. inter = (torch.min(box1[:, None, 2:], box2[:, 2:]) - torch.max(box1[:, None, :2], box2[:, :2])).clamp(0).prod(2)
  204. return inter / (area1[:, None] + area2 - inter) # iou = inter / (area1 + area2 - inter)
  205. def wh_iou(wh1, wh2):
  206. # Returns the nxm IoU matrix. wh1 is nx2, wh2 is mx2
  207. wh1 = wh1[:, None] # [N,1,2]
  208. wh2 = wh2[None] # [1,M,2]
  209. inter = torch.min(wh1, wh2).prod(2) # [N,M]
  210. return inter / (wh1.prod(2) + wh2.prod(2) - inter) # iou = inter / (area1 + area2 - inter)
  211. def non_max_suppression(prediction, conf_thres=0.1, iou_thres=0.6, merge=False, classes=None, agnostic=False):
  212. """Performs Non-Maximum Suppression (NMS) on inference results
  213. Returns:
  214. detections with shape: nx6 (x1, y1, x2, y2, conf, cls)
  215. """
  216. nc = prediction[0].shape[1] - 5 # number of classes
  217. xc = prediction[..., 4] > conf_thres # candidates
  218. # Settings
  219. min_wh, max_wh = 2, 4096 # (pixels) minimum and maximum box width and height
  220. max_det = 300 # maximum number of detections per image
  221. time_limit = 10.0 # seconds to quit after
  222. redundant = True # require redundant detections
  223. multi_label = nc > 1 # multiple labels per box (adds 0.5ms/img)
  224. t = time.time()
  225. output = [torch.zeros(0, 6)] * prediction.shape[0]
  226. for xi, x in enumerate(prediction): # image index, image inference
  227. # Apply constraints
  228. # x[((x[..., 2:4] < min_wh) | (x[..., 2:4] > max_wh)).any(1), 4] = 0 # width-height
  229. x = x[xc[xi]] # confidence
  230. # If none remain process next image
  231. if not x.shape[0]:
  232. continue
  233. # Compute conf
  234. x[:, 5:] *= x[:, 4:5] # conf = obj_conf * cls_conf
  235. # Box (center x, center y, width, height) to (x1, y1, x2, y2)
  236. box = xywh2xyxy(x[:, :4])
  237. # Detections matrix nx6 (xyxy, conf, cls)
  238. if multi_label:
  239. i, j = (x[:, 5:] > conf_thres).nonzero(as_tuple=False).T
  240. x = torch.cat((box[i], x[i, j + 5, None], j[:, None].float()), 1)
  241. else: # best class only
  242. conf, j = x[:, 5:].max(1, keepdim=True)
  243. x = torch.cat((box, conf, j.float()), 1)[conf.view(-1) > conf_thres]
  244. # Filter by class
  245. if classes:
  246. x = x[(x[:, 5:6] == torch.tensor(classes, device=x.device)).any(1)]
  247. # Apply finite constraint
  248. # if not torch.isfinite(x).all():
  249. # x = x[torch.isfinite(x).all(1)]
  250. # If none remain process next image
  251. n = x.shape[0] # number of boxes
  252. if not n:
  253. continue
  254. # Sort by confidence
  255. # x = x[x[:, 4].argsort(descending=True)]
  256. # Batched NMS
  257. c = x[:, 5:6] * (0 if agnostic else max_wh) # classes
  258. boxes, scores = x[:, :4] + c, x[:, 4] # boxes (offset by class), scores
  259. i = torch.ops.torchvision.nms(boxes, scores, iou_thres)
  260. if i.shape[0] > max_det: # limit detections
  261. i = i[:max_det]
  262. if merge and (1 < n < 3E3): # Merge NMS (boxes merged using weighted mean)
  263. # update boxes as boxes(i,4) = weights(i,n) * boxes(n,4)
  264. iou = box_iou(boxes[i], boxes) > iou_thres # iou matrix
  265. weights = iou * scores[None] # box weights
  266. x[i, :4] = torch.mm(weights, x[:, :4]).float() / weights.sum(1, keepdim=True) # merged boxes
  267. if redundant:
  268. i = i[iou.sum(1) > 1] # require redundancy
  269. output[xi] = x[i]
  270. if (time.time() - t) > time_limit:
  271. break # time limit exceeded
  272. return output
  273. def strip_optimizer(f='weights/best.pt', s=''): # from utils.general import *; strip_optimizer()
  274. # Strip optimizer from 'f' to finalize training, optionally save as 's'
  275. x = torch.load(f, map_location=torch.device('cpu'))
  276. x['optimizer'] = None
  277. x['training_results'] = None
  278. x['epoch'] = -1
  279. x['model'].half() # to FP16
  280. for p in x['model'].parameters():
  281. p.requires_grad = False
  282. torch.save(x, s or f)
  283. mb = os.path.getsize(s or f) / 1E6 # filesize
  284. print('Optimizer stripped from %s,%s %.1fMB' % (f, (' saved as %s,' % s) if s else '', mb))
  285. def print_mutation(hyp, results, yaml_file='hyp_evolved.yaml', bucket=''):
  286. # Print mutation results to evolve.txt (for use with train.py --evolve)
  287. a = '%10s' * len(hyp) % tuple(hyp.keys()) # hyperparam keys
  288. b = '%10.3g' * len(hyp) % tuple(hyp.values()) # hyperparam values
  289. c = '%10.4g' * len(results) % results # results (P, R, mAP@0.5, mAP@0.5:0.95, val_losses x 3)
  290. print('\n%s\n%s\nEvolved fitness: %s\n' % (a, b, c))
  291. if bucket:
  292. url = 'gs://%s/evolve.txt' % bucket
  293. if gsutil_getsize(url) > (os.path.getsize('evolve.txt') if os.path.exists('evolve.txt') else 0):
  294. os.system('gsutil cp %s .' % url) # download evolve.txt if larger than local
  295. with open('evolve.txt', 'a') as f: # append result
  296. f.write(c + b + '\n')
  297. x = np.unique(np.loadtxt('evolve.txt', ndmin=2), axis=0) # load unique rows
  298. x = x[np.argsort(-fitness(x))] # sort
  299. np.savetxt('evolve.txt', x, '%10.3g') # save sort by fitness
  300. # Save yaml
  301. for i, k in enumerate(hyp.keys()):
  302. hyp[k] = float(x[0, i + 7])
  303. with open(yaml_file, 'w') as f:
  304. results = tuple(x[0, :7])
  305. c = '%10.4g' * len(results) % results # results (P, R, mAP@0.5, mAP@0.5:0.95, val_losses x 3)
  306. f.write('# Hyperparameter Evolution Results\n# Generations: %g\n# Metrics: ' % len(x) + c + '\n\n')
  307. yaml.dump(hyp, f, sort_keys=False)
  308. if bucket:
  309. os.system('gsutil cp evolve.txt %s gs://%s' % (yaml_file, bucket)) # upload
  310. def apply_classifier(x, model, img, im0):
  311. # applies a second stage classifier to yolo outputs
  312. im0 = [im0] if isinstance(im0, np.ndarray) else im0
  313. for i, d in enumerate(x): # per image
  314. if d is not None and len(d):
  315. d = d.clone()
  316. # Reshape and pad cutouts
  317. b = xyxy2xywh(d[:, :4]) # boxes
  318. b[:, 2:] = b[:, 2:].max(1)[0].unsqueeze(1) # rectangle to square
  319. b[:, 2:] = b[:, 2:] * 1.3 + 30 # pad
  320. d[:, :4] = xywh2xyxy(b).long()
  321. # Rescale boxes from img_size to im0 size
  322. scale_coords(img.shape[2:], d[:, :4], im0[i].shape)
  323. # Classes
  324. pred_cls1 = d[:, 5].long()
  325. ims = []
  326. for j, a in enumerate(d): # per item
  327. cutout = im0[i][int(a[1]):int(a[3]), int(a[0]):int(a[2])]
  328. im = cv2.resize(cutout, (224, 224)) # BGR
  329. # cv2.imwrite('test%i.jpg' % j, cutout)
  330. im = im[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, to 3x416x416
  331. im = np.ascontiguousarray(im, dtype=np.float32) # uint8 to float32
  332. im /= 255.0 # 0 - 255 to 0.0 - 1.0
  333. ims.append(im)
  334. pred_cls2 = model(torch.Tensor(ims).to(d.device)).argmax(1) # classifier prediction
  335. x[i] = x[i][pred_cls1 == pred_cls2] # retain matching class detections
  336. return x
  337. def increment_path(path, exist_ok=True, sep=''):
  338. # Increment path, i.e. runs/exp --> runs/exp{sep}0, runs/exp{sep}1 etc.
  339. path = Path(path) # os-agnostic
  340. if (path.exists() and exist_ok) or (not path.exists()):
  341. return str(path)
  342. else:
  343. dirs = glob.glob(f"{path}{sep}*") # similar paths
  344. matches = [re.search(rf"%s{sep}(\d+)" % path.stem, d) for d in dirs]
  345. i = [int(m.groups()[0]) for m in matches if m] # indices
  346. n = max(i) + 1 if i else 2 # increment number
  347. return f"{path}{sep}{n}" # update path