val.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. # YOLOv5 🚀 by Ultralytics, AGPL-3.0 license
  2. """
  3. Validate a trained YOLOv5 detection model on a detection dataset.
  4. Usage:
  5. $ python val.py --weights yolov5s.pt --data coco128.yaml --img 640
  6. Usage - formats:
  7. $ python val.py --weights yolov5s.pt # PyTorch
  8. yolov5s.torchscript # TorchScript
  9. yolov5s.onnx # ONNX Runtime or OpenCV DNN with --dnn
  10. yolov5s_openvino_model # OpenVINO
  11. yolov5s.engine # TensorRT
  12. yolov5s.mlmodel # CoreML (macOS-only)
  13. yolov5s_saved_model # TensorFlow SavedModel
  14. yolov5s.pb # TensorFlow GraphDef
  15. yolov5s.tflite # TensorFlow Lite
  16. yolov5s_edgetpu.tflite # TensorFlow Edge TPU
  17. yolov5s_paddle_model # PaddlePaddle
  18. """
  19. import argparse
  20. import json
  21. import os
  22. import subprocess
  23. import sys
  24. from pathlib import Path
  25. import numpy as np
  26. import torch
  27. from tqdm import tqdm
  28. FILE = Path(__file__).resolve()
  29. ROOT = FILE.parents[0] # YOLOv5 root directory
  30. if str(ROOT) not in sys.path:
  31. sys.path.append(str(ROOT)) # add ROOT to PATH
  32. ROOT = Path(os.path.relpath(ROOT, Path.cwd())) # relative
  33. from models.common import DetectMultiBackend
  34. from utils.callbacks import Callbacks
  35. from utils.dataloaders import create_dataloader, create_dataloader_platform
  36. from utils.general import (
  37. LOGGER,
  38. TQDM_BAR_FORMAT,
  39. Profile,
  40. check_dataset,
  41. check_img_size,
  42. check_yaml,
  43. coco80_to_coco91_class,
  44. colorstr,
  45. increment_path,
  46. non_max_suppression,
  47. print_args,
  48. scale_boxes,
  49. xywh2xyxy,
  50. xyxy2xywh,
  51. )
  52. from utils.metrics import ConfusionMatrix, ap_per_class, box_iou
  53. from utils.plots import output_to_target, plot_images, plot_val_study
  54. from utils.torch_utils import select_device, smart_inference_mode
  55. def save_one_txt(predn, save_conf, shape, file):
  56. """Saves one detection result to a txt file in normalized xywh format, optionally including confidence."""
  57. gn = torch.tensor(shape)[[1, 0, 1, 0]] # normalization gain whwh
  58. for *xyxy, conf, cls in predn.tolist():
  59. xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist() # normalized xywh
  60. line = (cls, *xywh, conf) if save_conf else (cls, *xywh) # label format
  61. with open(file, "a") as f:
  62. f.write(("%g " * len(line)).rstrip() % line + "\n")
  63. def save_one_json(predn, jdict, path, class_map):
  64. """
  65. Saves one JSON detection result with image ID, category ID, bounding box, and score.
  66. Example: {"image_id": 42, "category_id": 18, "bbox": [258.15, 41.29, 348.26, 243.78], "score": 0.236}
  67. """
  68. image_id = int(path.stem) if path.stem.isnumeric() else path.stem
  69. box = xyxy2xywh(predn[:, :4]) # xywh
  70. box[:, :2] -= box[:, 2:] / 2 # xy center to top-left corner
  71. for p, b in zip(predn.tolist(), box.tolist()):
  72. jdict.append(
  73. {
  74. "image_id": image_id,
  75. "category_id": class_map[int(p[5])],
  76. "bbox": [round(x, 3) for x in b],
  77. "score": round(p[4], 5),
  78. }
  79. )
  80. def process_batch(detections, labels, iouv):
  81. """
  82. Return correct prediction matrix.
  83. Arguments:
  84. detections (array[N, 6]), x1, y1, x2, y2, conf, class
  85. labels (array[M, 5]), class, x1, y1, x2, y2
  86. Returns:
  87. correct (array[N, 10]), for 10 IoU levels
  88. """
  89. correct = np.zeros((detections.shape[0], iouv.shape[0])).astype(bool)
  90. iou = box_iou(labels[:, 1:], detections[:, :4])
  91. correct_class = labels[:, 0:1] == detections[:, 5]
  92. for i in range(len(iouv)):
  93. x = torch.where((iou >= iouv[i]) & correct_class) # IoU > threshold and classes match
  94. if x[0].shape[0]:
  95. matches = torch.cat((torch.stack(x, 1), iou[x[0], x[1]][:, None]), 1).cpu().numpy() # [label, detect, iou]
  96. if x[0].shape[0] > 1:
  97. matches = matches[matches[:, 2].argsort()[::-1]]
  98. matches = matches[np.unique(matches[:, 1], return_index=True)[1]]
  99. # matches = matches[matches[:, 2].argsort()[::-1]]
  100. matches = matches[np.unique(matches[:, 0], return_index=True)[1]]
  101. correct[matches[:, 1].astype(int), i] = True
  102. return torch.tensor(correct, dtype=torch.bool, device=iouv.device)
  103. @smart_inference_mode()
  104. def run(
  105. data,
  106. weights=None, # model.pt path(s)
  107. batch_size=32, # batch size
  108. imgsz=640, # inference size (pixels)
  109. conf_thres=0.5, # confidence threshold
  110. iou_thres=0.3, # NMS IoU threshold
  111. max_det=300, # maximum detections per image
  112. task="val", # train, val, test, speed or study
  113. device="", # cuda device, i.e. 0 or 0,1,2,3 or cpu
  114. workers=8, # max dataloader workers (per RANK in DDP mode)
  115. single_cls=False, # treat as single-class dataset
  116. augment=False, # augmented inference
  117. verbose=False, # verbose output
  118. save_txt=False, # save results to *.txt
  119. save_hybrid=False, # save label+prediction hybrid results to *.txt
  120. save_conf=False, # save confidences in --save-txt labels
  121. save_json=False, # save a COCO-JSON results file
  122. project=ROOT / "runs/val", # save to project/name
  123. name="exp", # save to project/name
  124. exist_ok=False, # existing project/name ok, do not increment
  125. half=True, # use FP16 half-precision inference
  126. dnn=False, # use OpenCV DNN for ONNX inference
  127. model=None,
  128. dataloader=None,
  129. save_dir=Path(""),
  130. plots=True,
  131. callbacks=Callbacks(),
  132. compute_loss=None,
  133. use_v7_loss=False,
  134. is_train_on_platform=False,
  135. ):
  136. # Initialize/load model and set device
  137. training = model is not None
  138. if training: # called by train.py
  139. device, pt, jit, engine = next(model.parameters()).device, True, False, False # get model device, PyTorch model
  140. half &= device.type != "cpu" # half precision only supported on CUDA
  141. model.half() if half else model.float()
  142. else: # called directly
  143. device = select_device(device, batch_size=batch_size)
  144. # Directories
  145. save_dir = increment_path(Path(project) / name, exist_ok=exist_ok) # increment run
  146. (save_dir / "labels" if save_txt else save_dir).mkdir(parents=True, exist_ok=True) # make dir
  147. # Load model
  148. model = DetectMultiBackend(weights, device=device, dnn=dnn, data=data, fp16=half)
  149. stride, pt, jit, engine = model.stride, model.pt, model.jit, model.engine
  150. imgsz = check_img_size(imgsz, s=stride) # check image size
  151. half = model.fp16 # FP16 supported on limited backends with CUDA
  152. if engine:
  153. batch_size = model.batch_size
  154. else:
  155. device = model.device
  156. if not (pt or jit):
  157. batch_size = 1 # export.py models default to batch-size 1
  158. LOGGER.info(f"Forcing --batch-size 1 square inference (1,3,{imgsz},{imgsz}) for non-PyTorch models")
  159. # Data
  160. data = check_dataset(data, is_train_on_platform) # check
  161. # Configure
  162. model.eval()
  163. cuda = device.type != "cpu"
  164. is_coco = isinstance(data.get("val"), str) and data["val"].endswith(f"coco{os.sep}val2017.txt") # COCO dataset
  165. nc = 1 if single_cls else int(data["nc"]) # number of classes
  166. iouv = torch.linspace(0.5, 0.95, 10, device=device) # iou vector for mAP@0.5:0.95
  167. niou = iouv.numel()
  168. # Dataloader
  169. if not training:
  170. if pt and not single_cls: # check --weights are trained on --data
  171. ncm = model.model.nc
  172. assert ncm == nc, (
  173. f"{weights} ({ncm} classes) trained on different --data than what you passed ({nc} "
  174. f"classes). Pass correct combination of --weights and --data that are trained together."
  175. )
  176. model.warmup(imgsz=(1 if pt else batch_size, 3, imgsz, imgsz)) # warmup
  177. pad, rect = (0.0, False) if task == "speed" else (0.5, pt) # square inference for benchmarks
  178. task = task if task in ("train", "val", "test") else "val" # path to train/val/test images
  179. if is_train_on_platform:
  180. dataloader = create_dataloader_platform(
  181. imgsz,
  182. batch_size,
  183. stride,
  184. single_cls,
  185. data_dict=data,
  186. train_or_val_data='val',
  187. pad=pad,
  188. rect=rect,
  189. workers=workers,
  190. prefix=colorstr(f"{task}: "),
  191. )[0]
  192. else:
  193. dataloader = create_dataloader(
  194. data[task],
  195. imgsz,
  196. batch_size,
  197. stride,
  198. single_cls,
  199. pad=pad,
  200. rect=rect,
  201. workers=workers,
  202. prefix=colorstr(f"{task}: "),
  203. )[0]
  204. seen = 0
  205. confusion_matrix = ConfusionMatrix(nc=nc)
  206. names = model.names if hasattr(model, "names") else model.module.names # get class names
  207. if isinstance(names, (list, tuple)): # old format
  208. names = dict(enumerate(names))
  209. class_map = coco80_to_coco91_class() if is_coco else list(range(1000))
  210. s = ("%22s" + "%11s" * 6) % ("Class", "Images", "Instances", "P", "R", "mAP50", "mAP50-95")
  211. tp, fp, p, r, f1, mp, mr, map50, ap50, map = 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
  212. dt = Profile(device=device), Profile(device=device), Profile(device=device) # profiling times
  213. loss = torch.zeros(3, device=device)
  214. jdict, stats, ap, ap_class = [], [], [], []
  215. callbacks.run("on_val_start")
  216. pbar = tqdm(dataloader, desc=s, bar_format=TQDM_BAR_FORMAT) # progress bar
  217. for batch_i, (im, targets, paths, shapes) in enumerate(pbar):
  218. callbacks.run("on_val_batch_start")
  219. with dt[0]:
  220. if cuda:
  221. im = im.to(device, non_blocking=True)
  222. targets = targets.to(device)
  223. im = im.half() if half else im.float() # uint8 to fp16/32
  224. im /= 255 # 0 - 255 to 0.0 - 1.0
  225. nb, _, height, width = im.shape # batch size, channels, height, width
  226. # Inference
  227. with dt[1]:
  228. preds, train_out = model(im) if compute_loss else (model(im, augment=augment), None)
  229. # Loss
  230. if compute_loss:
  231. if use_v7_loss:
  232. loss += compute_loss(train_out, targets, im)[1] # box, obj, cls
  233. else:
  234. loss += compute_loss(train_out, targets)[1] # box, obj, cls
  235. # NMS
  236. targets[:, 2:] *= torch.tensor((width, height, width, height), device=device) # to pixels
  237. lb = [targets[targets[:, 0] == i, 1:] for i in range(nb)] if save_hybrid else [] # for autolabelling
  238. with dt[2]:
  239. preds = non_max_suppression(
  240. preds, conf_thres, iou_thres, labels=lb, multi_label=True, agnostic=single_cls, max_det=max_det
  241. )
  242. # Metrics
  243. for si, pred in enumerate(preds):
  244. labels = targets[targets[:, 0] == si, 1:]
  245. nl, npr = labels.shape[0], pred.shape[0] # number of labels, predictions
  246. path, shape = Path(paths[si]), shapes[si][0]
  247. correct = torch.zeros(npr, niou, dtype=torch.bool, device=device) # init
  248. seen += 1
  249. if npr == 0:
  250. if nl:
  251. stats.append((correct, *torch.zeros((2, 0), device=device), labels[:, 0]))
  252. if plots:
  253. confusion_matrix.process_batch(detections=None, labels=labels[:, 0])
  254. continue
  255. # Predictions
  256. if single_cls:
  257. pred[:, 5] = 0
  258. predn = pred.clone()
  259. scale_boxes(im[si].shape[1:], predn[:, :4], shape, shapes[si][1]) # native-space pred
  260. # Evaluate
  261. if nl:
  262. tbox = xywh2xyxy(labels[:, 1:5]) # target boxes
  263. scale_boxes(im[si].shape[1:], tbox, shape, shapes[si][1]) # native-space labels
  264. labelsn = torch.cat((labels[:, 0:1], tbox), 1) # native-space labels
  265. correct = process_batch(predn, labelsn, iouv)
  266. if plots:
  267. confusion_matrix.process_batch(predn, labelsn)
  268. stats.append((correct, pred[:, 4], pred[:, 5], labels[:, 0])) # (correct, conf, pcls, tcls)
  269. # Save/log
  270. if save_txt:
  271. (save_dir / "labels").mkdir(parents=True, exist_ok=True)
  272. save_one_txt(predn, save_conf, shape, file=save_dir / "labels" / f"{path.stem}.txt")
  273. if save_json:
  274. save_one_json(predn, jdict, path, class_map) # append to COCO-JSON dictionary
  275. callbacks.run("on_val_image_end", pred, predn, path, names, im[si])
  276. # Plot images
  277. if plots and batch_i < 3:
  278. plot_images(im, targets, paths, save_dir / f"val_batch{batch_i}_labels.jpg", names) # labels
  279. plot_images(im, output_to_target(preds), paths, save_dir / f"val_batch{batch_i}_pred.jpg", names) # pred
  280. callbacks.run("on_val_batch_end", batch_i, im, targets, paths, shapes, preds)
  281. # Compute metrics
  282. stats = [torch.cat(x, 0).cpu().numpy() for x in zip(*stats)] # to numpy
  283. if len(stats) and stats[0].any():
  284. tp, fp, p, r, f1, ap, ap_class = ap_per_class(*stats, plot=plots, save_dir=save_dir, names=names)
  285. ap50, ap = ap[:, 0], ap.mean(1) # AP@0.5, AP@0.5:0.95
  286. mp, mr, map50, map = p.mean(), r.mean(), ap50.mean(), ap.mean()
  287. nt = np.bincount(stats[3].astype(int), minlength=nc) # number of targets per class
  288. # Print results
  289. pf = "%22s" + "%11i" * 2 + "%11.3g" * 4 # print format
  290. LOGGER.info(pf % ("all", seen, nt.sum(), mp, mr, map50, map))
  291. if nt.sum() == 0:
  292. LOGGER.warning(f"WARNING ⚠️ no labels found in {task} set, can not compute metrics without labels")
  293. # Print results per class
  294. if (verbose or (nc < 50 and not training)) and nc > 1 and len(stats):
  295. for i, c in enumerate(ap_class):
  296. LOGGER.info(pf % (names[c], seen, nt[c], p[i], r[i], ap50[i], ap[i]))
  297. # Print speeds
  298. t = tuple(x.t / seen * 1e3 for x in dt) # speeds per image
  299. if not training:
  300. shape = (batch_size, 3, imgsz, imgsz)
  301. LOGGER.info(f"Speed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {shape}" % t)
  302. # Plots
  303. if plots:
  304. confusion_matrix.plot(save_dir=save_dir, names=list(names.values()))
  305. callbacks.run("on_val_end", nt, tp, fp, p, r, f1, ap, ap50, ap_class, confusion_matrix)
  306. # Save JSON
  307. if save_json and len(jdict):
  308. w = Path(weights[0] if isinstance(weights, list) else weights).stem if weights is not None else "" # weights
  309. anno_json = str(Path("../datasets/coco/annotations/instances_val2017.json")) # annotations
  310. if not os.path.exists(anno_json):
  311. anno_json = os.path.join(data["path"], "annotations", "instances_val2017.json")
  312. pred_json = str(save_dir / f"{w}_predictions.json") # predictions
  313. LOGGER.info(f"\nEvaluating pycocotools mAP... saving {pred_json}...")
  314. with open(pred_json, "w") as f:
  315. json.dump(jdict, f)
  316. try: # https://github.com/cocodataset/cocoapi/blob/master/PythonAPI/pycocoEvalDemo.ipynb
  317. check_requirements("pycocotools>=2.0.6")
  318. from pycocotools.coco import COCO
  319. from pycocotools.cocoeval import COCOeval
  320. anno = COCO(anno_json) # init annotations api
  321. pred = anno.loadRes(pred_json) # init predictions api
  322. eval = COCOeval(anno, pred, "bbox")
  323. if is_coco:
  324. eval.params.imgIds = [int(Path(x).stem) for x in dataloader.dataset.im_files] # image IDs to evaluate
  325. eval.evaluate()
  326. eval.accumulate()
  327. eval.summarize()
  328. map, map50 = eval.stats[:2] # update results (mAP@0.5:0.95, mAP@0.5)
  329. except Exception as e:
  330. LOGGER.info(f"pycocotools unable to run: {e}")
  331. # Return results
  332. model.float() # for training
  333. if not training:
  334. s = f"\n{len(list(save_dir.glob('labels/*.txt')))} labels saved to {save_dir / 'labels'}" if save_txt else ""
  335. LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}{s}")
  336. maps = np.zeros(nc) + map
  337. for i, c in enumerate(ap_class):
  338. maps[c] = ap[i]
  339. return (mp, mr, map50, map, *(loss.cpu() / len(dataloader)).tolist()), maps, t
  340. def parse_opt():
  341. """Parses command-line options for YOLOv5 model inference configuration."""
  342. parser = argparse.ArgumentParser()
  343. parser.add_argument("--data", type=str, default=ROOT / "data/coco128.yaml", help="dataset.yaml path")
  344. parser.add_argument("--weights", nargs="+", type=str, default=ROOT / "yolov5s.pt", help="model path(s)")
  345. parser.add_argument("--batch-size", type=int, default=32, help="batch size")
  346. parser.add_argument("--imgsz", "--img", "--img-size", type=int, default=640, help="inference size (pixels)")
  347. parser.add_argument("--conf-thres", type=float, default=0.001, help="confidence threshold")
  348. parser.add_argument("--iou-thres", type=float, default=0.6, help="NMS IoU threshold")
  349. parser.add_argument("--max-det", type=int, default=300, help="maximum detections per image")
  350. parser.add_argument("--task", default="val", help="train, val, test, speed or study")
  351. parser.add_argument("--device", default="", help="cuda device, i.e. 0 or 0,1,2,3 or cpu")
  352. parser.add_argument("--workers", type=int, default=8, help="max dataloader workers (per RANK in DDP mode)")
  353. parser.add_argument("--single-cls", action="store_true", help="treat as single-class dataset")
  354. parser.add_argument("--augment", action="store_true", help="augmented inference")
  355. parser.add_argument("--verbose", action="store_true", help="report mAP by class")
  356. parser.add_argument("--save-txt", action="store_true", help="save results to *.txt")
  357. parser.add_argument("--save-hybrid", action="store_true", help="save label+prediction hybrid results to *.txt")
  358. parser.add_argument("--save-conf", action="store_true", help="save confidences in --save-txt labels")
  359. parser.add_argument("--save-json", action="store_true", help="save a COCO-JSON results file")
  360. parser.add_argument("--project", default=ROOT / "runs/val", help="save to project/name")
  361. parser.add_argument("--name", default="exp", help="save to project/name")
  362. parser.add_argument("--exist-ok", action="store_true", help="existing project/name ok, do not increment")
  363. parser.add_argument("--half", action="store_true", help="use FP16 half-precision inference")
  364. parser.add_argument("--dnn", action="store_true", help="use OpenCV DNN for ONNX inference")
  365. opt = parser.parse_args()
  366. opt.data = check_yaml(opt.data) # check YAML
  367. opt.save_json |= opt.data.endswith("coco.yaml")
  368. opt.save_txt |= opt.save_hybrid
  369. print_args(vars(opt))
  370. return opt
  371. def main(opt):
  372. """Executes YOLOv5 tasks like training, validation, testing, speed, and study benchmarks based on provided
  373. options.
  374. """
  375. # check_requirements(ROOT / "requirements.txt", exclude=("tensorboard", "thop"))
  376. if opt.task in ("train", "val", "test"): # run normally
  377. if opt.conf_thres > 0.001: # https://github.com/ultralytics/yolov5/issues/1466
  378. LOGGER.info(f"WARNING ⚠️ confidence threshold {opt.conf_thres} > 0.001 produces invalid results")
  379. if opt.save_hybrid:
  380. LOGGER.info("WARNING ⚠️ --save-hybrid will return high mAP from hybrid labels, not from predictions alone")
  381. run(**vars(opt))
  382. else:
  383. weights = opt.weights if isinstance(opt.weights, list) else [opt.weights]
  384. opt.half = torch.cuda.is_available() and opt.device != "cpu" # FP16 for fastest results
  385. if opt.task == "speed": # speed benchmarks
  386. # python val.py --task speed --data coco.yaml --batch 1 --weights yolov5n.pt yolov5s.pt...
  387. opt.conf_thres, opt.iou_thres, opt.save_json = 0.25, 0.45, False
  388. for opt.weights in weights:
  389. run(**vars(opt), plots=False)
  390. elif opt.task == "study": # speed vs mAP benchmarks
  391. # python val.py --task study --data coco.yaml --iou 0.7 --weights yolov5n.pt yolov5s.pt...
  392. for opt.weights in weights:
  393. f = f"study_{Path(opt.data).stem}_{Path(opt.weights).stem}.txt" # filename to save to
  394. x, y = list(range(256, 1536 + 128, 128)), [] # x axis (image sizes), y axis
  395. for opt.imgsz in x: # img-size
  396. LOGGER.info(f"\nRunning {f} --imgsz {opt.imgsz}...")
  397. r, _, t = run(**vars(opt), plots=False)
  398. y.append(r + t) # results and times
  399. np.savetxt(f, y, fmt="%10.4g") # save
  400. subprocess.run(["zip", "-r", "study.zip", "study_*.txt"])
  401. plot_val_study(x=x) # plot
  402. else:
  403. raise NotImplementedError(f'--task {opt.task} not in ("train", "val", "test", "speed", "study")')
  404. if __name__ == "__main__":
  405. opt = parse_opt()
  406. main(opt)