coco_eval.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914
  1. # Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
  2. # pyre-unsafe
  3. """
  4. COCO evaluator that works in distributed mode.
  5. Mostly copy-paste from https://github.com/pytorch/vision/blob/edfd5a7/references/detection/coco_eval.py
  6. The difference is that there is less copy-pasting from pycocotools
  7. in the end of the file, as python3 can suppress prints with contextlib
  8. """
  9. import contextlib
  10. import copy
  11. import json
  12. import logging
  13. import os
  14. import pickle
  15. from collections import defaultdict
  16. from pathlib import Path
  17. from typing import Any, List, Optional
  18. import numpy as np
  19. import pycocotools.mask as mask_utils
  20. import torch
  21. from iopath.common.file_io import g_pathmgr
  22. from pycocotools.coco import COCO
  23. from pycocotools.cocoeval import COCOeval
  24. from sam3.train.masks_ops import rle_encode
  25. from sam3.train.utils.distributed import (
  26. all_gather,
  27. gather_to_rank_0_via_filesys,
  28. get_rank,
  29. is_main_process,
  30. )
  31. RARITY_BUCKETS = {0: "frequent", 1: "common", 2: "medium", 3: "rare"}
  32. class CocoEvaluator:
  33. def __init__(
  34. self,
  35. coco_gt,
  36. iou_types: List[str],
  37. useCats: bool,
  38. dump_dir: Optional[str],
  39. postprocessor,
  40. average_by_rarity=False,
  41. metrics_dump_dir: Optional[str] = None,
  42. gather_pred_via_filesys=False,
  43. use_normalized_areas=True,
  44. maxdets=[1, 10, 100],
  45. exhaustive_only=False,
  46. all_exhaustive_only=True,
  47. ):
  48. """Online coco evaluator. It will evaluate images as they are generated by the model, then accumulate/summarize at the end
  49. Args:
  50. - coco_gt: COCO api object containing the gt
  51. - iou_types: can be either "bbox" or "segm"
  52. - useCats: If true, categories will be used for evaluation
  53. - dump_dir: if non null, then the predictions will be dumped in that directory
  54. - postprocessor: Module to convert the model's output into the coco format
  55. - average_by_rarity: if true then we expect the images information in the gt dataset
  56. to have a "rarity" field. Then the AP will be computed on all rarity buckets
  57. individually, then averaged
  58. - gather_pred_via_filesys: if true, we use the filesystem for collective gathers
  59. - use_normalized_areas: if true, the areas of the objects in the GT are assumed to be
  60. normalized by the area of the image. In that case, the size buckets are adjusted
  61. - maxdets: maximal number of detections to be evaluated on each image.
  62. - exhaustive_only: If true, we restrict eval only to exhaustive annotations
  63. - all_exhaustive_only: If true, datapoints are restricted only to those with all exhaustive annotations
  64. """
  65. # coco_gt = copy.deepcopy(coco_gt)
  66. self.coco_gts = [coco_gt] if not isinstance(coco_gt, list) else coco_gt
  67. assert len(maxdets) == 3, f"expecting 3 detection threshold, got {len(maxdets)}"
  68. self.use_normalized_areas = use_normalized_areas
  69. self.iou_types = iou_types
  70. self.useCats = useCats
  71. self.maxdets = maxdets
  72. self.dump = None
  73. self.dump_dir = dump_dir
  74. if self.dump_dir is not None:
  75. self.dump = []
  76. if is_main_process():
  77. if not os.path.exists(self.dump_dir):
  78. os.makedirs(self.dump_dir, exist_ok=True)
  79. logging.info(f"Create the folder: {dump_dir}")
  80. self.initialized = False
  81. # Whether to gather predictions through filesystem (instead of torch
  82. # collective ops; requiring a shared filesystem across all ranks)
  83. self.gather_pred_via_filesys = gather_pred_via_filesys
  84. self.use_self_evaluate = True # CPP version is disabled
  85. self.postprocessor = postprocessor
  86. self.average_by_rarity = average_by_rarity
  87. self.exhaustive_only = exhaustive_only
  88. self.all_exhaustive_only = all_exhaustive_only
  89. self.metrics_dump_dir = metrics_dump_dir
  90. if self.metrics_dump_dir is not None:
  91. if is_main_process():
  92. if not os.path.exists(self.metrics_dump_dir):
  93. os.makedirs(self.metrics_dump_dir, exist_ok=True)
  94. logging.info(f"Create the folder: {metrics_dump_dir}")
  95. def _lazy_init(self, coco_cls=COCO):
  96. if self.initialized:
  97. return
  98. self.initialized = True
  99. self.coco_gts = [
  100. coco_cls(g_pathmgr.get_local_path(gt)) if isinstance(gt, str) else gt
  101. for gt in self.coco_gts
  102. ]
  103. self.reset()
  104. self.eval_img_ids = None
  105. if self.exhaustive_only:
  106. exclude_img_ids = set()
  107. # exclude_img_ids are the ids that are not exhaustively annotated in any of the other gts
  108. if self.all_exhaustive_only:
  109. for coco_gt in self.coco_gts[1:]:
  110. exclude_img_ids = exclude_img_ids.union(
  111. {
  112. img["id"]
  113. for img in coco_gt.dataset["images"]
  114. if not img["is_instance_exhaustive"]
  115. }
  116. )
  117. # we only eval on instance exhaustive queries
  118. self.eval_img_ids = [
  119. img["id"]
  120. for img in self.coco_gts[0].dataset["images"]
  121. if (img["is_instance_exhaustive"] and img["id"] not in exclude_img_ids)
  122. ]
  123. self.rarity_buckets = None
  124. if self.average_by_rarity:
  125. self.rarity_buckets = defaultdict(list)
  126. eval_img_ids_set = (
  127. set(self.eval_img_ids) if self.eval_img_ids is not None else None
  128. )
  129. for img in self.coco_gts[0].dataset["images"]:
  130. if self.eval_img_ids is not None and img["id"] not in eval_img_ids_set:
  131. continue
  132. self.rarity_buckets[img["rarity"]].append(img["id"])
  133. print("Rarity buckets sizes:")
  134. for k, v in self.rarity_buckets.items():
  135. print(f"{k}: {len(v)}")
  136. def set_sync_device(self, device: torch.device) -> Any:
  137. self._sync_device = device
  138. def _evaluate(self, *args, **kwargs):
  139. return evaluate(*args, **kwargs)
  140. def _loadRes(self, *args, **kwargs):
  141. return loadRes(*args, **kwargs)
  142. def update(self, *args, **kwargs):
  143. self._lazy_init()
  144. predictions = self.postprocessor.process_results(*args, **kwargs)
  145. img_ids = list(np.unique(list(predictions.keys())))
  146. self.img_ids.extend(img_ids)
  147. for iou_type in self.iou_types:
  148. results = self.prepare(predictions, iou_type)
  149. self._dump(results)
  150. assert len(self.coco_gts) == len(self.coco_evals)
  151. all_scorings = []
  152. for cur_coco_gt, cur_coco_eval in zip(self.coco_gts, self.coco_evals):
  153. # suppress pycocotools prints
  154. with open(os.devnull, "w") as devnull:
  155. with contextlib.redirect_stdout(devnull):
  156. coco_dt = (
  157. self._loadRes(cur_coco_gt, results) if results else COCO()
  158. )
  159. coco_eval = cur_coco_eval[iou_type]
  160. coco_eval.cocoDt = coco_dt
  161. coco_eval.params.imgIds = list(img_ids)
  162. coco_eval.params.useCats = self.useCats
  163. coco_eval.params.maxDets = self.maxdets
  164. img_ids, eval_imgs = self._evaluate(coco_eval, self.use_self_evaluate)
  165. all_scorings.append(eval_imgs)
  166. selected = self.select_best_scoring(all_scorings)
  167. self.eval_imgs[iou_type].append(selected)
  168. def select_best_scoring(self, scorings):
  169. # This function is used for "oracle" type evaluation.
  170. # It accepts the evaluation results with respect to several ground truths, and picks the best
  171. if len(scorings) == 1:
  172. return scorings[0]
  173. # Currently we don't support Oracle Phrase AP.
  174. # To implement it, we likely need to modify the cpp code since the eval_image type is opaque
  175. raise RuntimeError("Not implemented")
  176. def _dump(self, results):
  177. if self.dump is not None:
  178. dumped_results = copy.deepcopy(results)
  179. for r in dumped_results:
  180. if "bbox" not in self.iou_types and "bbox" in r:
  181. del r["bbox"]
  182. elif "bbox" in r:
  183. r["bbox"] = [round(coord, 5) for coord in r["bbox"]]
  184. r["score"] = round(r["score"], 5)
  185. self.dump.extend(dumped_results)
  186. def synchronize_between_processes(self):
  187. self._lazy_init()
  188. logging.info("Coco evaluator: Synchronizing between processes")
  189. for iou_type in self.iou_types:
  190. if len(self.eval_imgs[iou_type]) > 0:
  191. self.eval_imgs[iou_type] = np.concatenate(self.eval_imgs[iou_type], 2)
  192. else:
  193. num_areas = len(self.coco_evals[0][iou_type].params.areaRng)
  194. # assuming 1 class
  195. assert not self.useCats
  196. self.eval_imgs[iou_type] = np.empty((1, num_areas, 0))
  197. create_common_coco_eval(
  198. self.coco_evals[0][iou_type],
  199. self.img_ids,
  200. self.eval_imgs[iou_type],
  201. use_self_evaluate=self.use_self_evaluate,
  202. gather_pred_via_filesys=self.gather_pred_via_filesys,
  203. metrics_dump_dir=self.metrics_dump_dir,
  204. )
  205. if self.dump is not None:
  206. dumped_file = Path(self.dump_dir) / f"coco_predictions_{get_rank()}.json"
  207. logging.info(f"COCO evaluator: Dumping local predictions to {dumped_file}")
  208. with g_pathmgr.open(str(dumped_file), "w") as f:
  209. json.dump(self.dump, f)
  210. # if self.gather_pred_via_filesys:
  211. # dump = gather_to_rank_0_via_filesys(self.dump)
  212. # else:
  213. # dump = all_gather(self.dump, force_cpu=True)
  214. # self.dump = sum(dump, [])
  215. def accumulate(self, imgIds=None):
  216. self._lazy_init()
  217. logging.info(
  218. f"Coco evaluator: Accumulating on {len(imgIds) if imgIds is not None else 'all'} images"
  219. )
  220. if not is_main_process():
  221. return
  222. if imgIds is None:
  223. for coco_eval in self.coco_evals[0].values():
  224. accumulate(coco_eval, use_self_eval=self.use_self_evaluate)
  225. if imgIds is not None:
  226. imgIds = set(imgIds)
  227. for coco_eval in self.coco_evals[0].values():
  228. p = coco_eval.params
  229. id_mask = np.array([(i in imgIds) for i in p.imgIds], dtype=bool)
  230. old_img_ids = p.imgIds
  231. coco_eval.params.imgIds = np.asarray(p.imgIds)[id_mask]
  232. old_img_evals = coco_eval.evalImgs
  233. catIds = p.catIds if p.useCats else [-1]
  234. coco_eval.evalImgs = list(
  235. np.asarray(coco_eval.evalImgs)
  236. .reshape(len(catIds), len(p.areaRng), len(old_img_ids))[
  237. ..., id_mask
  238. ]
  239. .flatten()
  240. )
  241. accumulate(coco_eval, use_self_eval=self.use_self_evaluate)
  242. coco_eval.evalImgs = old_img_evals
  243. coco_eval.params.imgIds = old_img_ids
  244. def summarize(self):
  245. self._lazy_init()
  246. logging.info("Coco evaluator: Summarizing")
  247. if not is_main_process():
  248. return {}
  249. outs = {}
  250. if self.rarity_buckets is None:
  251. self.accumulate(self.eval_img_ids)
  252. for iou_type, coco_eval in self.coco_evals[0].items():
  253. print("IoU metric: {}".format(iou_type))
  254. summarize(coco_eval)
  255. if "bbox" in self.coco_evals[0]:
  256. for key, value in zip(*self.coco_evals[0]["bbox"].stats):
  257. outs[f"coco_eval_bbox_{key}"] = value
  258. if "segm" in self.coco_evals[0]:
  259. for key, value in zip(*self.coco_evals[0]["segm"].stats):
  260. outs[f"coco_eval_masks_{key}"] = value
  261. else:
  262. total_stats = {}
  263. all_keys = {}
  264. for bucket, img_list in self.rarity_buckets.items():
  265. self.accumulate(imgIds=img_list)
  266. bucket_name = RARITY_BUCKETS[bucket]
  267. for iou_type, coco_eval in self.coco_evals[0].items():
  268. print(f"IoU metric: {iou_type}. Rarity bucket: {bucket_name}")
  269. summarize(coco_eval)
  270. if "bbox" in self.coco_evals[0]:
  271. if "bbox" not in total_stats:
  272. total_stats["bbox"] = np.zeros_like(
  273. self.coco_evals[0]["bbox"].stats[1]
  274. )
  275. all_keys["bbox"] = self.coco_evals[0]["bbox"].stats[0]
  276. total_stats["bbox"] += self.coco_evals[0]["bbox"].stats[1]
  277. for key, value in zip(*self.coco_evals[0]["bbox"].stats):
  278. outs[f"coco_eval_bbox_{bucket_name}_{key}"] = value
  279. if "segm" in self.coco_evals[0]:
  280. if "segm" not in total_stats:
  281. total_stats["segm"] = np.zeros_like(
  282. self.coco_evals[0]["segm"].stats[1]
  283. )
  284. all_keys["segm"] = self.coco_evals[0]["segm"].stats[0]
  285. total_stats["segm"] += self.coco_evals[0]["segm"].stats[1]
  286. for key, value in zip(*self.coco_evals[0]["segm"].stats):
  287. outs[f"coco_eval_masks_{bucket_name}_{key}"] = value
  288. if "bbox" in total_stats:
  289. total_stats["bbox"] /= len(self.rarity_buckets)
  290. for key, value in zip(all_keys["bbox"], total_stats["bbox"]):
  291. outs[f"coco_eval_bbox_{key}"] = value
  292. if "segm" in total_stats:
  293. total_stats["segm"] /= len(self.rarity_buckets)
  294. for key, value in zip(all_keys["segm"], total_stats["segm"]):
  295. outs[f"coco_eval_masks_{key}"] = value
  296. # if self.dump is not None:
  297. # assert self.dump_dir is not None
  298. # logging.info("Coco evaluator: Dumping the global result file to disk")
  299. # with g_pathmgr.open(str(Path(self.dump_dir) / "coco_eval.json"), "w") as f:
  300. # json.dump(self.dump, f)
  301. return outs
  302. def compute_synced(self):
  303. self._lazy_init()
  304. self.synchronize_between_processes()
  305. return self.summarize()
  306. def compute(self):
  307. self._lazy_init()
  308. return {"": 0.0}
  309. def reset(self, cocoeval_cls=COCOeval):
  310. self.coco_evals = [{} for _ in range(len(self.coco_gts))]
  311. for i, coco_gt in enumerate(self.coco_gts):
  312. for iou_type in self.iou_types:
  313. self.coco_evals[i][iou_type] = cocoeval_cls(coco_gt, iouType=iou_type)
  314. self.coco_evals[i][iou_type].params.useCats = self.useCats
  315. self.coco_evals[i][iou_type].params.maxDets = self.maxdets
  316. if self.use_normalized_areas:
  317. self.coco_evals[i][iou_type].params.areaRng = [
  318. [0, 1e5],
  319. [0, 0.001],
  320. [0.001, 0.01],
  321. [0.01, 0.1],
  322. [0.1, 0.5],
  323. [0.5, 0.95],
  324. [0.95, 1e5],
  325. ]
  326. self.coco_evals[i][iou_type].params.areaRngLbl = [
  327. "all",
  328. "tiny",
  329. "small",
  330. "medium",
  331. "large",
  332. "huge",
  333. "whole_image",
  334. ]
  335. self.img_ids = []
  336. self.eval_imgs = {k: [] for k in self.iou_types}
  337. if self.dump is not None:
  338. self.dump = []
  339. def write(self, stats):
  340. self._lazy_init()
  341. """Write the results in the stats dict"""
  342. if "bbox" in self.coco_evals[0]:
  343. stats["coco_eval_bbox"] = self.coco_evals[0]["bbox"].stats.tolist()
  344. if "segm" in self.coco_evals[0]:
  345. stats["coco_eval_masks"] = self.coco_evals[0]["segm"].stats.tolist()
  346. return stats
  347. def prepare(self, predictions, iou_type):
  348. self._lazy_init()
  349. if iou_type == "bbox":
  350. return self.prepare_for_coco_detection(predictions)
  351. elif iou_type == "segm":
  352. return self.prepare_for_coco_segmentation(predictions)
  353. elif iou_type == "keypoints":
  354. return self.prepare_for_coco_keypoint(predictions)
  355. else:
  356. raise ValueError("Unknown iou type {}".format(iou_type))
  357. def prepare_for_coco_detection(self, predictions):
  358. self._lazy_init()
  359. coco_results = []
  360. for original_id, prediction in predictions.items():
  361. if len(prediction) == 0:
  362. continue
  363. boxes = prediction["boxes"]
  364. boxes = convert_to_xywh(boxes).tolist()
  365. scores = prediction["scores"].tolist()
  366. labels = prediction["labels"].tolist()
  367. coco_results.extend(
  368. [
  369. {
  370. "image_id": original_id,
  371. "category_id": labels[k],
  372. "bbox": box,
  373. "score": scores[k],
  374. }
  375. for k, box in enumerate(boxes)
  376. ]
  377. )
  378. return coco_results
  379. @torch.no_grad()
  380. def prepare_for_coco_segmentation(self, predictions):
  381. self._lazy_init()
  382. coco_results = []
  383. for original_id, prediction in predictions.items():
  384. if len(prediction) == 0:
  385. continue
  386. scores = prediction["scores"].tolist()
  387. labels = prediction["labels"].tolist()
  388. boundaries, dilated_boundaries = None, None
  389. if "boundaries" in prediction:
  390. boundaries = prediction["boundaries"]
  391. dilated_boundaries = prediction["dilated_boundaries"]
  392. assert dilated_boundaries is not None
  393. assert len(scores) == len(boundaries)
  394. if "masks_rle" in prediction:
  395. rles = prediction["masks_rle"]
  396. areas = []
  397. for rle in rles:
  398. cur_area = mask_utils.area(rle)
  399. h, w = rle["size"]
  400. areas.append(cur_area / (h * w))
  401. else:
  402. masks = prediction["masks"]
  403. masks = masks > 0.5
  404. h, w = masks.shape[-2:]
  405. areas = masks.flatten(1).sum(1) / (h * w)
  406. areas = areas.tolist()
  407. rles = rle_encode(masks.squeeze(1))
  408. # memory clean
  409. del masks
  410. del prediction["masks"]
  411. assert len(areas) == len(rles) == len(scores)
  412. for k, rle in enumerate(rles):
  413. payload = {
  414. "image_id": original_id,
  415. "category_id": labels[k],
  416. "segmentation": rle,
  417. "score": scores[k],
  418. "area": areas[k],
  419. }
  420. if boundaries is not None:
  421. payload["boundary"] = boundaries[k]
  422. payload["dilated_boundary"] = dilated_boundaries[k]
  423. coco_results.append(payload)
  424. return coco_results
  425. def prepare_for_coco_keypoint(self, predictions):
  426. self._lazy_init()
  427. coco_results = []
  428. for original_id, prediction in predictions.items():
  429. if len(prediction) == 0:
  430. continue
  431. boxes = prediction["boxes"]
  432. boxes = convert_to_xywh(boxes).tolist()
  433. scores = prediction["scores"].tolist()
  434. labels = prediction["labels"].tolist()
  435. keypoints = prediction["keypoints"]
  436. keypoints = keypoints.flatten(start_dim=1).tolist()
  437. coco_results.extend(
  438. [
  439. {
  440. "image_id": original_id,
  441. "category_id": labels[k],
  442. "keypoints": keypoint,
  443. "score": scores[k],
  444. }
  445. for k, keypoint in enumerate(keypoints)
  446. ]
  447. )
  448. return coco_results
  449. def convert_to_xywh(boxes):
  450. xmin, ymin, xmax, ymax = boxes.unbind(-1)
  451. return torch.stack((xmin, ymin, xmax - xmin, ymax - ymin), dim=-1)
  452. def merge(img_ids, eval_imgs, gather_pred_via_filesys=False):
  453. if gather_pred_via_filesys:
  454. # only gather the predictions to rank 0 (other ranks will receive empty
  455. # lists for `all_img_ids` and `all_eval_imgs`, which should be OK as
  456. # merging and evaluation are only done on rank 0)
  457. all_img_ids = gather_to_rank_0_via_filesys(img_ids)
  458. all_eval_imgs = gather_to_rank_0_via_filesys(eval_imgs)
  459. else:
  460. all_img_ids = all_gather(img_ids, force_cpu=True)
  461. all_eval_imgs = all_gather(eval_imgs, force_cpu=True)
  462. if not is_main_process():
  463. return None, None
  464. merged_img_ids = []
  465. for p in all_img_ids:
  466. merged_img_ids.extend(p)
  467. merged_eval_imgs = []
  468. for p in all_eval_imgs:
  469. merged_eval_imgs.append(p)
  470. merged_img_ids = np.array(merged_img_ids)
  471. merged_eval_imgs = np.concatenate(merged_eval_imgs, 2)
  472. # keep only unique (and in sorted order) images
  473. merged_img_ids, idx = np.unique(merged_img_ids, return_index=True)
  474. merged_eval_imgs = merged_eval_imgs[..., idx]
  475. return merged_img_ids, merged_eval_imgs
  476. def create_common_coco_eval(
  477. coco_eval,
  478. img_ids,
  479. eval_imgs,
  480. use_self_evaluate,
  481. gather_pred_via_filesys=False,
  482. metrics_dump_dir=None,
  483. ):
  484. img_ids, eval_imgs = merge(img_ids, eval_imgs, gather_pred_via_filesys)
  485. if not is_main_process():
  486. return
  487. if metrics_dump_dir is not None:
  488. dumped_file = (
  489. Path(metrics_dump_dir) / f"coco_eval_img_metrics_{get_rank()}.json"
  490. )
  491. logging.info(f"COCO evaluator: Dumping local predictions to {dumped_file}")
  492. with g_pathmgr.open(str(dumped_file), "w") as f:
  493. json.dump(eval_imgs.squeeze(), f, default=lambda x: x.tolist())
  494. img_ids = list(img_ids)
  495. # If some images were not predicted, we need to create dummy detections for them
  496. missing_img_ids = set(coco_eval.cocoGt.getImgIds()) - set(img_ids)
  497. if len(missing_img_ids) > 0:
  498. print(f"WARNING: {len(missing_img_ids)} images were not predicted!")
  499. coco_eval.cocoDt = COCO()
  500. coco_eval.params.imgIds = list(missing_img_ids)
  501. new_img_ids, new_eval_imgs = evaluate(coco_eval, use_self_evaluate)
  502. img_ids.extend(new_img_ids)
  503. eval_imgs = np.concatenate((eval_imgs, new_eval_imgs), axis=2)
  504. eval_imgs = list(eval_imgs.flatten())
  505. assert len(img_ids) == len(coco_eval.cocoGt.getImgIds())
  506. coco_eval.evalImgs = eval_imgs
  507. coco_eval.params.imgIds = img_ids
  508. coco_eval._paramsEval = copy.deepcopy(coco_eval.params)
  509. #################################################################
  510. # From pycocotools, just removed the prints and fixed
  511. # a Python3 bug about unicode not defined
  512. #################################################################
  513. # Copy of COCO prepare, but doesn't convert anntoRLE
  514. def segmentation_prepare(self):
  515. """
  516. Prepare ._gts and ._dts for evaluation based on params
  517. :return: None
  518. """
  519. p = self.params
  520. if p.useCats:
  521. gts = self.cocoGt.loadAnns(
  522. self.cocoGt.getAnnIds(imgIds=p.imgIds, catIds=p.catIds)
  523. )
  524. dts = self.cocoDt.loadAnns(
  525. self.cocoDt.getAnnIds(imgIds=p.imgIds, catIds=p.catIds)
  526. )
  527. else:
  528. gts = self.cocoGt.loadAnns(self.cocoGt.getAnnIds(imgIds=p.imgIds))
  529. dts = self.cocoDt.loadAnns(self.cocoDt.getAnnIds(imgIds=p.imgIds))
  530. for gt in gts:
  531. gt["ignore"] = gt["ignore"] if "ignore" in gt else 0
  532. gt["ignore"] = "iscrowd" in gt and gt["iscrowd"]
  533. if p.iouType == "keypoints":
  534. gt["ignore"] = (gt["num_keypoints"] == 0) or gt["ignore"]
  535. self._gts = defaultdict(list) # gt for evaluation
  536. self._dts = defaultdict(list) # dt for evaluation
  537. for gt in gts:
  538. self._gts[gt["image_id"], gt["category_id"]].append(gt)
  539. for dt in dts:
  540. self._dts[dt["image_id"], dt["category_id"]].append(dt)
  541. self.evalImgs = defaultdict(list) # per-image per-category evaluation results
  542. self.eval = {} # accumulated evaluation results
  543. def evaluate(self, use_self_evaluate):
  544. """
  545. Run per image evaluation on given images and store results (a list of dict) in self.evalImgs
  546. :return: None
  547. """
  548. # tic = time.time()
  549. # print('Running per image evaluation...', use_self_evaluate)
  550. p = self.params
  551. # add backward compatibility if useSegm is specified in params
  552. if p.useSegm is not None:
  553. p.iouType = "segm" if p.useSegm == 1 else "bbox"
  554. print(
  555. "useSegm (deprecated) is not None. Running {} evaluation".format(p.iouType)
  556. )
  557. # print('Evaluate annotation type *{}*'.format(p.iouType))
  558. p.imgIds = list(np.unique(p.imgIds))
  559. if p.useCats:
  560. p.catIds = list(np.unique(p.catIds))
  561. p.maxDets = sorted(p.maxDets)
  562. self.params = p
  563. self._prepare()
  564. # loop through images, area range, max detection number
  565. catIds = p.catIds if p.useCats else [-1]
  566. if p.iouType == "segm" or p.iouType == "bbox":
  567. computeIoU = self.computeIoU
  568. elif p.iouType == "keypoints":
  569. computeIoU = self.computeOks
  570. self.ious = {
  571. (imgId, catId): computeIoU(imgId, catId)
  572. for imgId in p.imgIds
  573. for catId in catIds
  574. }
  575. maxDet = p.maxDets[-1]
  576. if use_self_evaluate:
  577. evalImgs = [
  578. self.evaluateImg(imgId, catId, areaRng, maxDet)
  579. for catId in catIds
  580. for areaRng in p.areaRng
  581. for imgId in p.imgIds
  582. ]
  583. # this is NOT in the pycocotools code, but could be done outside
  584. evalImgs = np.asarray(evalImgs).reshape(
  585. len(catIds), len(p.areaRng), len(p.imgIds)
  586. )
  587. return p.imgIds, evalImgs
  588. # <<<< Beginning of code differences with original COCO API
  589. # def convert_instances_to_cpp(instances, is_det=False):
  590. # # Convert annotations for a list of instances in an image to a format that's fast
  591. # # to access in C++
  592. # instances_cpp = []
  593. # for instance in instances:
  594. # instance_cpp = _CPP.InstanceAnnotation(
  595. # int(instance["id"]),
  596. # instance["score"] if is_det else instance.get("score", 0.0),
  597. # instance["area"],
  598. # bool(instance.get("iscrowd", 0)),
  599. # bool(instance.get("ignore", 0)),
  600. # )
  601. # instances_cpp.append(instance_cpp)
  602. # return instances_cpp
  603. # # Convert GT annotations, detections, and IOUs to a format that's fast to access in C++
  604. # ground_truth_instances = [
  605. # [convert_instances_to_cpp(self._gts[imgId, catId]) for catId in p.catIds]
  606. # for imgId in p.imgIds
  607. # ]
  608. # detected_instances = [
  609. # [
  610. # convert_instances_to_cpp(self._dts[imgId, catId], is_det=True)
  611. # for catId in p.catIds
  612. # ]
  613. # for imgId in p.imgIds
  614. # ]
  615. # ious = [[self.ious[imgId, catId] for catId in catIds] for imgId in p.imgIds]
  616. # if not p.useCats:
  617. # # For each image, flatten per-category lists into a single list
  618. # ground_truth_instances = [
  619. # [[o for c in i for o in c]] for i in ground_truth_instances
  620. # ]
  621. # detected_instances = [[[o for c in i for o in c]] for i in detected_instances]
  622. # # Call C++ implementation of self.evaluateImgs()
  623. # _evalImgs_cpp = _CPP.COCOevalEvaluateImages(
  624. # p.areaRng, maxDet, p.iouThrs, ious, ground_truth_instances, detected_instances
  625. # )
  626. # self._paramsEval = copy.deepcopy(self.params)
  627. # evalImgs = np.asarray(_evalImgs_cpp).reshape(
  628. # len(catIds), len(p.areaRng), len(p.imgIds)
  629. # )
  630. # return p.imgIds, evalImgs
  631. #################################################################
  632. # end of straight copy from pycocotools, just removing the prints
  633. #################################################################
  634. #################################################################
  635. # From pycocotools, but disabled mask->box conversion which is
  636. # pointless
  637. #################################################################
  638. def loadRes(self, resFile):
  639. """
  640. Load result file and return a result api object.
  641. :param resFile (str) : file name of result file
  642. :return: res (obj) : result api object
  643. """
  644. res = COCO()
  645. res.dataset["images"] = [img for img in self.dataset["images"]]
  646. if type(resFile) == str:
  647. anns = json.load(open(resFile))
  648. elif type(resFile) == np.ndarray:
  649. anns = self.loadNumpyAnnotations(resFile)
  650. else:
  651. anns = resFile
  652. assert type(anns) == list, "results in not an array of objects"
  653. annsImgIds = [ann["image_id"] for ann in anns]
  654. assert set(annsImgIds) == (set(annsImgIds) & set(self.getImgIds())), (
  655. "Results do not correspond to current coco set"
  656. )
  657. if "caption" in anns[0]:
  658. imgIds = set([img["id"] for img in res.dataset["images"]]) & set(
  659. [ann["image_id"] for ann in anns]
  660. )
  661. res.dataset["images"] = [
  662. img for img in res.dataset["images"] if img["id"] in imgIds
  663. ]
  664. for id, ann in enumerate(anns):
  665. ann["id"] = id + 1
  666. elif "bbox" in anns[0] and not anns[0]["bbox"] == []:
  667. res.dataset["categories"] = copy.deepcopy(self.dataset["categories"])
  668. for id, ann in enumerate(anns):
  669. bb = ann["bbox"]
  670. x1, x2, y1, y2 = [bb[0], bb[0] + bb[2], bb[1], bb[1] + bb[3]]
  671. if "segmentation" not in ann:
  672. ann["segmentation"] = [[x1, y1, x1, y2, x2, y2, x2, y1]]
  673. ann["area"] = bb[2] * bb[3]
  674. ann["id"] = id + 1
  675. ann["iscrowd"] = 0
  676. elif "segmentation" in anns[0]:
  677. res.dataset["categories"] = copy.deepcopy(self.dataset["categories"])
  678. for id, ann in enumerate(anns):
  679. # now only support compressed RLE format as segmentation results
  680. # ann["area"] = mask_util.area(ann["segmentation"])
  681. # The following lines are disabled because they are pointless
  682. # if not 'bbox' in ann:
  683. # ann['bbox'] = maskUtils.toBbox(ann['segmentation'])
  684. ann["id"] = id + 1
  685. ann["iscrowd"] = 0
  686. elif "keypoints" in anns[0]:
  687. res.dataset["categories"] = copy.deepcopy(self.dataset["categories"])
  688. for id, ann in enumerate(anns):
  689. s = ann["keypoints"]
  690. x = s[0::3]
  691. y = s[1::3]
  692. x0, x1, y0, y1 = np.min(x), np.max(x), np.min(y), np.max(y)
  693. ann["area"] = (x1 - x0) * (y1 - y0)
  694. ann["id"] = id + 1
  695. ann["bbox"] = [x0, y0, x1 - x0, y1 - y0]
  696. res.dataset["annotations"] = anns
  697. res.createIndex()
  698. return res
  699. #################################################################
  700. # end of straight copy from pycocotools
  701. #################################################################
  702. #################################################################
  703. # From pycocotools, but added handling of custom area rngs, and returns stat keys
  704. #################################################################
  705. def summarize(self):
  706. """
  707. Compute and display summary metrics for evaluation results.
  708. Note this functin can *only* be applied on the default parameter setting
  709. """
  710. def _summarize(ap=1, iouThr=None, areaRng="all", maxDets=100):
  711. p = self.params
  712. iStr = " {:<18} {} @[ IoU={:<9} | area={:>6s} | maxDets={:>3d} ] = {:0.3f}"
  713. titleStr = "Average Precision" if ap == 1 else "Average Recall"
  714. typeStr = "(AP)" if ap == 1 else "(AR)"
  715. iouStr = (
  716. "{:0.2f}:{:0.2f}".format(p.iouThrs[0], p.iouThrs[-1])
  717. if iouThr is None
  718. else "{:0.2f}".format(iouThr)
  719. )
  720. aind = [i for i, aRng in enumerate(p.areaRngLbl) if aRng == areaRng]
  721. mind = [i for i, mDet in enumerate(p.maxDets) if mDet == maxDets]
  722. if ap == 1:
  723. # dimension of precision: [TxRxKxAxM]
  724. s = self.eval["precision"]
  725. # IoU
  726. if iouThr is not None:
  727. t = np.where(iouThr == p.iouThrs)[0]
  728. s = s[t]
  729. s = s[:, :, :, aind, mind]
  730. else:
  731. # dimension of recall: [TxKxAxM]
  732. s = self.eval["recall"]
  733. if iouThr is not None:
  734. t = np.where(iouThr == p.iouThrs)[0]
  735. s = s[t]
  736. s = s[:, :, aind, mind]
  737. if len(s[s > -1]) == 0:
  738. mean_s = -1
  739. else:
  740. mean_s = np.mean(s[s > -1])
  741. print(iStr.format(titleStr, typeStr, iouStr, areaRng, maxDets, mean_s))
  742. return mean_s
  743. def _summarizeDets():
  744. nb_results = 6 + (len(self.params.areaRng) - 1) * 2
  745. assert len(self.params.areaRng) == len(self.params.areaRngLbl)
  746. stats = np.zeros((nb_results,))
  747. keys = ["AP", "AP_50", "AP_75"]
  748. stats[0] = _summarize(1, maxDets=self.params.maxDets[2])
  749. stats[1] = _summarize(1, iouThr=0.5, maxDets=self.params.maxDets[2])
  750. stats[2] = _summarize(1, iouThr=0.75, maxDets=self.params.maxDets[2])
  751. cur_id = 3
  752. for area in self.params.areaRngLbl[1:]:
  753. stats[cur_id] = _summarize(1, areaRng=area, maxDets=self.params.maxDets[2])
  754. cur_id += 1
  755. keys.append(f"AP_{area}")
  756. stats[cur_id] = _summarize(0, maxDets=self.params.maxDets[0])
  757. cur_id += 1
  758. stats[cur_id] = _summarize(0, maxDets=self.params.maxDets[1])
  759. cur_id += 1
  760. stats[cur_id] = _summarize(0, maxDets=self.params.maxDets[2])
  761. cur_id += 1
  762. keys += ["AR", "AR_50", "AR_75"]
  763. for area in self.params.areaRngLbl[1:]:
  764. stats[cur_id] = _summarize(0, areaRng=area, maxDets=self.params.maxDets[2])
  765. cur_id += 1
  766. keys.append(f"AR_{area}")
  767. assert len(stats) == len(keys)
  768. return keys, stats
  769. if not self.eval:
  770. raise Exception("Please run accumulate() first")
  771. self.stats = _summarizeDets()
  772. #################################################################
  773. # end of straight copy from pycocotools
  774. #################################################################
  775. #################################################################
  776. # From https://github.com/facebookresearch/detectron2/blob/main/detectron2/evaluation/fast_eval_api.py
  777. # with slight adjustments
  778. #################################################################
  779. def accumulate(self, use_self_eval=False):
  780. """
  781. Accumulate per image evaluation results and store the result in self.eval. Does not
  782. support changing parameter settings from those used by self.evaluate()
  783. """
  784. if use_self_eval:
  785. self.accumulate()
  786. return
  787. # CPP code is disabled
  788. # self.eval = _CPP.COCOevalAccumulate(self.params, self.evalImgs)
  789. # # recall is num_iou_thresholds X num_categories X num_area_ranges X num_max_detections
  790. # self.eval["recall"] = np.array(self.eval["recall"]).reshape(
  791. # self.eval["counts"][:1] + self.eval["counts"][2:]
  792. # )
  793. # # precision and scores are num_iou_thresholds X num_recall_thresholds X num_categories X
  794. # # num_area_ranges X num_max_detections
  795. # self.eval["precision"] = np.array(self.eval["precision"]).reshape(
  796. # self.eval["counts"]
  797. # )
  798. # self.eval["scores"] = np.array(self.eval["scores"]).reshape(self.eval["counts"])