val.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. # Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
  2. from multiprocessing.pool import ThreadPool
  3. from pathlib import Path
  4. import numpy as np
  5. import torch
  6. import torch.nn.functional as F
  7. from ultralytics.models.yolo.detect import DetectionValidator
  8. from ultralytics.utils import LOGGER, NUM_THREADS, ops
  9. from ultralytics.utils.checks import check_requirements
  10. from ultralytics.utils.metrics import SegmentMetrics, box_iou, mask_iou
  11. from ultralytics.utils.plotting import output_to_target, plot_images
  12. class SegmentationValidator(DetectionValidator):
  13. """
  14. A class extending the DetectionValidator class for validation based on a segmentation model.
  15. Example:
  16. ```python
  17. from ultralytics.models.yolo.segment import SegmentationValidator
  18. args = dict(model="yolov8n-seg.pt", data="coco8-seg.yaml")
  19. validator = SegmentationValidator(args=args)
  20. validator()
  21. ```
  22. """
  23. def __init__(self, dataloader=None, save_dir=None, pbar=None, args=None, _callbacks=None):
  24. """Initialize SegmentationValidator and set task to 'segment', metrics to SegmentMetrics."""
  25. super().__init__(dataloader, save_dir, pbar, args, _callbacks)
  26. self.plot_masks = None
  27. self.process = None
  28. self.args.task = "segment"
  29. self.metrics = SegmentMetrics(save_dir=self.save_dir, on_plot=self.on_plot)
  30. def preprocess(self, batch):
  31. """Preprocesses batch by converting masks to float and sending to device."""
  32. batch = super().preprocess(batch)
  33. batch["masks"] = batch["masks"].to(self.device).float()
  34. return batch
  35. def init_metrics(self, model):
  36. """Initialize metrics and select mask processing function based on save_json flag."""
  37. super().init_metrics(model)
  38. self.plot_masks = []
  39. if self.args.save_json:
  40. check_requirements("pycocotools>=2.0.6")
  41. # more accurate vs faster
  42. self.process = ops.process_mask_native if self.args.save_json or self.args.save_txt else ops.process_mask
  43. self.stats = dict(tp_m=[], tp=[], conf=[], pred_cls=[], target_cls=[], target_img=[])
  44. def get_desc(self):
  45. """Return a formatted description of evaluation metrics."""
  46. return ("%22s" + "%11s" * 10) % (
  47. "Class",
  48. "Images",
  49. "Instances",
  50. "Box(P",
  51. "R",
  52. "mAP50",
  53. "mAP50-95)",
  54. "Mask(P",
  55. "R",
  56. "mAP50",
  57. "mAP50-95)",
  58. )
  59. def postprocess(self, preds):
  60. """Post-processes YOLO predictions and returns output detections with proto."""
  61. p = ops.non_max_suppression(
  62. preds[0],
  63. self.args.conf,
  64. self.args.iou,
  65. labels=self.lb,
  66. multi_label=True,
  67. agnostic=self.args.single_cls or self.args.agnostic_nms,
  68. max_det=self.args.max_det,
  69. nc=self.nc,
  70. )
  71. proto = preds[1][-1] if len(preds[1]) == 3 else preds[1] # second output is len 3 if pt, but only 1 if exported
  72. return p, proto
  73. def _prepare_batch(self, si, batch):
  74. """Prepares a batch for training or inference by processing images and targets."""
  75. prepared_batch = super()._prepare_batch(si, batch)
  76. midx = [si] if self.args.overlap_mask else batch["batch_idx"] == si
  77. prepared_batch["masks"] = batch["masks"][midx]
  78. return prepared_batch
  79. def _prepare_pred(self, pred, pbatch, proto):
  80. """Prepares a batch for training or inference by processing images and targets."""
  81. predn = super()._prepare_pred(pred, pbatch)
  82. pred_masks = self.process(proto, pred[:, 6:], pred[:, :4], shape=pbatch["imgsz"])
  83. return predn, pred_masks
  84. def update_metrics(self, preds, batch):
  85. """Metrics."""
  86. for si, (pred, proto) in enumerate(zip(preds[0], preds[1])):
  87. self.seen += 1
  88. npr = len(pred)
  89. stat = dict(
  90. conf=torch.zeros(0, device=self.device),
  91. pred_cls=torch.zeros(0, device=self.device),
  92. tp=torch.zeros(npr, self.niou, dtype=torch.bool, device=self.device),
  93. tp_m=torch.zeros(npr, self.niou, dtype=torch.bool, device=self.device),
  94. )
  95. pbatch = self._prepare_batch(si, batch)
  96. cls, bbox = pbatch.pop("cls"), pbatch.pop("bbox")
  97. nl = len(cls)
  98. stat["target_cls"] = cls
  99. stat["target_img"] = cls.unique()
  100. if npr == 0:
  101. if nl:
  102. for k in self.stats.keys():
  103. self.stats[k].append(stat[k])
  104. if self.args.plots:
  105. self.confusion_matrix.process_batch(detections=None, gt_bboxes=bbox, gt_cls=cls)
  106. continue
  107. # Masks
  108. gt_masks = pbatch.pop("masks")
  109. # Predictions
  110. if self.args.single_cls:
  111. pred[:, 5] = 0
  112. predn, pred_masks = self._prepare_pred(pred, pbatch, proto)
  113. stat["conf"] = predn[:, 4]
  114. stat["pred_cls"] = predn[:, 5]
  115. # Evaluate
  116. if nl:
  117. stat["tp"] = self._process_batch(predn, bbox, cls)
  118. stat["tp_m"] = self._process_batch(
  119. predn, bbox, cls, pred_masks, gt_masks, self.args.overlap_mask, masks=True
  120. )
  121. if self.args.plots:
  122. self.confusion_matrix.process_batch(predn, bbox, cls)
  123. for k in self.stats.keys():
  124. self.stats[k].append(stat[k])
  125. pred_masks = torch.as_tensor(pred_masks, dtype=torch.uint8)
  126. if self.args.plots and self.batch_i < 3:
  127. self.plot_masks.append(pred_masks[:15].cpu()) # filter top 15 to plot
  128. # Save
  129. if self.args.save_json:
  130. self.pred_to_json(
  131. predn,
  132. batch["im_file"][si],
  133. ops.scale_image(
  134. pred_masks.permute(1, 2, 0).contiguous().cpu().numpy(),
  135. pbatch["ori_shape"],
  136. ratio_pad=batch["ratio_pad"][si],
  137. ),
  138. )
  139. if self.args.save_txt:
  140. self.save_one_txt(
  141. predn,
  142. pred_masks,
  143. self.args.save_conf,
  144. pbatch["ori_shape"],
  145. self.save_dir / "labels" / f"{Path(batch['im_file'][si]).stem}.txt",
  146. )
  147. def finalize_metrics(self, *args, **kwargs):
  148. """Sets speed and confusion matrix for evaluation metrics."""
  149. self.metrics.speed = self.speed
  150. self.metrics.confusion_matrix = self.confusion_matrix
  151. def _process_batch(self, detections, gt_bboxes, gt_cls, pred_masks=None, gt_masks=None, overlap=False, masks=False):
  152. """
  153. Compute correct prediction matrix for a batch based on bounding boxes and optional masks.
  154. Args:
  155. detections (torch.Tensor): Tensor of shape (N, 6) representing detected bounding boxes and
  156. associated confidence scores and class indices. Each row is of the format [x1, y1, x2, y2, conf, class].
  157. gt_bboxes (torch.Tensor): Tensor of shape (M, 4) representing ground truth bounding box coordinates.
  158. Each row is of the format [x1, y1, x2, y2].
  159. gt_cls (torch.Tensor): Tensor of shape (M,) representing ground truth class indices.
  160. pred_masks (torch.Tensor | None): Tensor representing predicted masks, if available. The shape should
  161. match the ground truth masks.
  162. gt_masks (torch.Tensor | None): Tensor of shape (M, H, W) representing ground truth masks, if available.
  163. overlap (bool): Flag indicating if overlapping masks should be considered.
  164. masks (bool): Flag indicating if the batch contains mask data.
  165. Returns:
  166. (torch.Tensor): A correct prediction matrix of shape (N, 10), where 10 represents different IoU levels.
  167. Note:
  168. - If `masks` is True, the function computes IoU between predicted and ground truth masks.
  169. - If `overlap` is True and `masks` is True, overlapping masks are taken into account when computing IoU.
  170. Example:
  171. ```python
  172. detections = torch.tensor([[25, 30, 200, 300, 0.8, 1], [50, 60, 180, 290, 0.75, 0]])
  173. gt_bboxes = torch.tensor([[24, 29, 199, 299], [55, 65, 185, 295]])
  174. gt_cls = torch.tensor([1, 0])
  175. correct_preds = validator._process_batch(detections, gt_bboxes, gt_cls)
  176. ```
  177. """
  178. if masks:
  179. if overlap:
  180. nl = len(gt_cls)
  181. index = torch.arange(nl, device=gt_masks.device).view(nl, 1, 1) + 1
  182. gt_masks = gt_masks.repeat(nl, 1, 1) # shape(1,640,640) -> (n,640,640)
  183. gt_masks = torch.where(gt_masks == index, 1.0, 0.0)
  184. if gt_masks.shape[1:] != pred_masks.shape[1:]:
  185. gt_masks = F.interpolate(gt_masks[None], pred_masks.shape[1:], mode="bilinear", align_corners=False)[0]
  186. gt_masks = gt_masks.gt_(0.5)
  187. iou = mask_iou(gt_masks.view(gt_masks.shape[0], -1), pred_masks.view(pred_masks.shape[0], -1))
  188. else: # boxes
  189. iou = box_iou(gt_bboxes, detections[:, :4])
  190. return self.match_predictions(detections[:, 5], gt_cls, iou)
  191. def plot_val_samples(self, batch, ni):
  192. """Plots validation samples with bounding box labels."""
  193. plot_images(
  194. batch["img"],
  195. batch["batch_idx"],
  196. batch["cls"].squeeze(-1),
  197. batch["bboxes"],
  198. masks=batch["masks"],
  199. paths=batch["im_file"],
  200. fname=self.save_dir / f"val_batch{ni}_labels.jpg",
  201. names=self.names,
  202. on_plot=self.on_plot,
  203. )
  204. def plot_predictions(self, batch, preds, ni):
  205. """Plots batch predictions with masks and bounding boxes."""
  206. plot_images(
  207. batch["img"],
  208. *output_to_target(preds[0], max_det=15), # not set to self.args.max_det due to slow plotting speed
  209. torch.cat(self.plot_masks, dim=0) if len(self.plot_masks) else self.plot_masks,
  210. paths=batch["im_file"],
  211. fname=self.save_dir / f"val_batch{ni}_pred.jpg",
  212. names=self.names,
  213. on_plot=self.on_plot,
  214. ) # pred
  215. self.plot_masks.clear()
  216. def save_one_txt(self, predn, pred_masks, save_conf, shape, file):
  217. """Save YOLO detections to a txt file in normalized coordinates in a specific format."""
  218. from ultralytics.engine.results import Results
  219. Results(
  220. np.zeros((shape[0], shape[1]), dtype=np.uint8),
  221. path=None,
  222. names=self.names,
  223. boxes=predn[:, :6],
  224. masks=pred_masks,
  225. ).save_txt(file, save_conf=save_conf)
  226. def pred_to_json(self, predn, filename, pred_masks):
  227. """
  228. Save one JSON result.
  229. Examples:
  230. >>> result = {"image_id": 42, "category_id": 18, "bbox": [258.15, 41.29, 348.26, 243.78], "score": 0.236}
  231. """
  232. from pycocotools.mask import encode # noqa
  233. def single_encode(x):
  234. """Encode predicted masks as RLE and append results to jdict."""
  235. rle = encode(np.asarray(x[:, :, None], order="F", dtype="uint8"))[0]
  236. rle["counts"] = rle["counts"].decode("utf-8")
  237. return rle
  238. stem = Path(filename).stem
  239. image_id = int(stem) if stem.isnumeric() else stem
  240. box = ops.xyxy2xywh(predn[:, :4]) # xywh
  241. box[:, :2] -= box[:, 2:] / 2 # xy center to top-left corner
  242. pred_masks = np.transpose(pred_masks, (2, 0, 1))
  243. with ThreadPool(NUM_THREADS) as pool:
  244. rles = pool.map(single_encode, pred_masks)
  245. for i, (p, b) in enumerate(zip(predn.tolist(), box.tolist())):
  246. self.jdict.append(
  247. {
  248. "image_id": image_id,
  249. "category_id": self.class_map[int(p[5])],
  250. "bbox": [round(x, 3) for x in b],
  251. "score": round(p[4], 5),
  252. "segmentation": rles[i],
  253. }
  254. )
  255. def eval_json(self, stats):
  256. """Return COCO-style object detection evaluation metrics."""
  257. if self.args.save_json and self.is_coco and len(self.jdict):
  258. anno_json = self.data["path"] / "annotations/instances_val2017.json" # annotations
  259. pred_json = self.save_dir / "predictions.json" # predictions
  260. LOGGER.info(f"\nEvaluating pycocotools mAP using {pred_json} and {anno_json}...")
  261. try: # https://github.com/cocodataset/cocoapi/blob/master/PythonAPI/pycocoEvalDemo.ipynb
  262. check_requirements("pycocotools>=2.0.6")
  263. from pycocotools.coco import COCO # noqa
  264. from pycocotools.cocoeval import COCOeval # noqa
  265. for x in anno_json, pred_json:
  266. assert x.is_file(), f"{x} file not found"
  267. anno = COCO(str(anno_json)) # init annotations api
  268. pred = anno.loadRes(str(pred_json)) # init predictions api (must pass string, not Path)
  269. for i, eval in enumerate([COCOeval(anno, pred, "bbox"), COCOeval(anno, pred, "segm")]):
  270. if self.is_coco:
  271. eval.params.imgIds = [int(Path(x).stem) for x in self.dataloader.dataset.im_files] # im to eval
  272. eval.evaluate()
  273. eval.accumulate()
  274. eval.summarize()
  275. idx = i * 4 + 2
  276. stats[self.metrics.keys[idx + 1]], stats[self.metrics.keys[idx]] = eval.stats[
  277. :2
  278. ] # update mAP50-95 and mAP50
  279. except Exception as e:
  280. LOGGER.warning(f"pycocotools unable to run: {e}")
  281. return stats