image_instance_segmentation_metric.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. # Part of the implementation is borrowed and modified from MMDetection, publicly available at
  2. # https://github.com/open-mmlab/mmdetection/blob/master/mmdet/datasets/coco.py
  3. import os.path as osp
  4. import tempfile
  5. from collections import OrderedDict
  6. from typing import Any, Dict
  7. import numpy as np
  8. import pycocotools.mask as mask_util
  9. from pycocotools.coco import COCO
  10. from pycocotools.cocoeval import COCOeval
  11. from modelscope.fileio import dump, load
  12. from modelscope.metainfo import Metrics
  13. from modelscope.metrics import METRICS, Metric
  14. from modelscope.utils.registry import default_group
  15. @METRICS.register_module(
  16. group_key=default_group, module_name=Metrics.image_ins_seg_coco_metric)
  17. class ImageInstanceSegmentationCOCOMetric(Metric):
  18. """The metric computation class for COCO-style image instance segmentation.
  19. """
  20. def __init__(self):
  21. self.ann_file = None
  22. self.classes = None
  23. self.metrics = ['bbox', 'segm']
  24. self.proposal_nums = (100, 300, 1000)
  25. self.iou_thrs = np.linspace(
  26. .5, 0.95, int(np.round((0.95 - .5) / .05)) + 1, endpoint=True)
  27. self.results = []
  28. def add(self, outputs: Dict[str, Any], inputs: Dict[str, Any]):
  29. result = outputs['eval_result']
  30. # encode mask results
  31. if isinstance(result[0], tuple):
  32. result = [(bbox_results, encode_mask_results(mask_results))
  33. for bbox_results, mask_results in result]
  34. self.results.extend(result)
  35. if self.ann_file is None:
  36. self.ann_file = outputs['img_metas'][0]['ann_file']
  37. self.classes = outputs['img_metas'][0]['classes']
  38. def evaluate(self):
  39. cocoGt = COCO(self.ann_file)
  40. self.cat_ids = cocoGt.getCatIds(catNms=self.classes)
  41. self.img_ids = cocoGt.getImgIds()
  42. result_files, tmp_dir = self.format_results(self.results, self.img_ids)
  43. eval_results = OrderedDict()
  44. for metric in self.metrics:
  45. iou_type = metric
  46. if metric not in result_files:
  47. raise KeyError(f'{metric} is not in results')
  48. try:
  49. predictions = load(result_files[metric])
  50. if iou_type == 'segm':
  51. # Refer to https://github.com/cocodataset/cocoapi/blob/master/PythonAPI/pycocotools/coco.py#L331 # noqa
  52. # When evaluating mask AP, if the results contain bbox,
  53. # cocoapi will use the box area instead of the mask area
  54. # for calculating the instance area. Though the overall AP
  55. # is not affected, this leads to different
  56. # small/medium/large mask AP results.
  57. for x in predictions:
  58. x.pop('bbox')
  59. cocoDt = cocoGt.loadRes(predictions)
  60. except IndexError:
  61. print('The testing results of the whole dataset is empty.')
  62. break
  63. cocoEval = COCOeval(cocoGt, cocoDt, iou_type)
  64. cocoEval.params.catIds = self.cat_ids
  65. cocoEval.params.imgIds = self.img_ids
  66. cocoEval.params.maxDets = list(self.proposal_nums)
  67. cocoEval.params.iouThrs = self.iou_thrs
  68. # mapping of cocoEval.stats
  69. coco_metric_names = {
  70. 'mAP': 0,
  71. 'mAP_50': 1,
  72. 'mAP_75': 2,
  73. 'mAP_s': 3,
  74. 'mAP_m': 4,
  75. 'mAP_l': 5,
  76. 'AR@100': 6,
  77. 'AR@300': 7,
  78. 'AR@1000': 8,
  79. 'AR_s@1000': 9,
  80. 'AR_m@1000': 10,
  81. 'AR_l@1000': 11
  82. }
  83. cocoEval.evaluate()
  84. cocoEval.accumulate()
  85. cocoEval.summarize()
  86. metric_items = [
  87. 'mAP', 'mAP_50', 'mAP_75', 'mAP_s', 'mAP_m', 'mAP_l'
  88. ]
  89. for metric_item in metric_items:
  90. key = f'{metric}_{metric_item}'
  91. val = float(
  92. f'{cocoEval.stats[coco_metric_names[metric_item]]:.3f}')
  93. eval_results[key] = val
  94. ap = cocoEval.stats[:6]
  95. eval_results[f'{metric}_mAP_copypaste'] = (
  96. f'{ap[0]:.3f} {ap[1]:.3f} {ap[2]:.3f} {ap[3]:.3f} '
  97. f'{ap[4]:.3f} {ap[5]:.3f}')
  98. if tmp_dir is not None:
  99. tmp_dir.cleanup()
  100. return eval_results
  101. def merge(self, other: 'ImageInstanceSegmentationCOCOMetric'):
  102. self.results.extend(other.results)
  103. def __getstate__(self):
  104. return self.results
  105. def __setstate__(self, state):
  106. self.__init__()
  107. self.results = state
  108. def format_results(self, results, img_ids, jsonfile_prefix=None, **kwargs):
  109. """Format the results to json (standard format for COCO evaluation).
  110. Args:
  111. results (list[tuple | numpy.ndarray]): Testing results of the
  112. dataset.
  113. data_infos(list[tuple | numpy.ndarray]): data information
  114. jsonfile_prefix (str | None): The prefix of json files. It includes
  115. the file path and the prefix of filename, e.g., "a/b/prefix".
  116. If not specified, a temp file will be created. Default: None.
  117. Returns:
  118. tuple: (result_files, tmp_dir), result_files is a dict containing \
  119. the json filepaths, tmp_dir is the temporal directory created \
  120. for saving json files when jsonfile_prefix is not specified.
  121. """
  122. assert isinstance(results, list), 'results must be a list'
  123. assert len(results) == len(img_ids), (
  124. 'The length of results is not equal to the dataset len: {} != {}'.
  125. format(len(results), len(img_ids)))
  126. if jsonfile_prefix is None:
  127. tmp_dir = tempfile.TemporaryDirectory()
  128. jsonfile_prefix = osp.join(tmp_dir.name, 'results')
  129. else:
  130. tmp_dir = None
  131. result_files = self.results2json(results, jsonfile_prefix)
  132. return result_files, tmp_dir
  133. def xyxy2xywh(self, bbox):
  134. """Convert ``xyxy`` style bounding boxes to ``xywh`` style for COCO
  135. evaluation.
  136. Args:
  137. bbox (numpy.ndarray): The bounding boxes, shape (4, ), in
  138. ``xyxy`` order.
  139. Returns:
  140. list[float]: The converted bounding boxes, in ``xywh`` order.
  141. """
  142. _bbox = bbox.tolist()
  143. return [
  144. _bbox[0],
  145. _bbox[1],
  146. _bbox[2] - _bbox[0],
  147. _bbox[3] - _bbox[1],
  148. ]
  149. def _proposal2json(self, results):
  150. """Convert proposal results to COCO json style."""
  151. json_results = []
  152. for idx in range(len(self.img_ids)):
  153. img_id = self.img_ids[idx]
  154. bboxes = results[idx]
  155. for i in range(bboxes.shape[0]):
  156. data = dict()
  157. data['image_id'] = img_id
  158. data['bbox'] = self.xyxy2xywh(bboxes[i])
  159. data['score'] = float(bboxes[i][4])
  160. data['category_id'] = 1
  161. json_results.append(data)
  162. return json_results
  163. def _det2json(self, results):
  164. """Convert detection results to COCO json style."""
  165. json_results = []
  166. for idx in range(len(self.img_ids)):
  167. img_id = self.img_ids[idx]
  168. result = results[idx]
  169. for label in range(len(result)):
  170. # Here we skip invalid predicted labels, as we use the fixed num_classes of 80 (COCO)
  171. # (assuming the num class of input dataset is no more than 80).
  172. # Recommended manually set `num_classes=${your test dataset class num}` in the
  173. # configuration.json in practice.
  174. if label >= len(self.classes):
  175. break
  176. bboxes = result[label]
  177. for i in range(bboxes.shape[0]):
  178. data = dict()
  179. data['image_id'] = img_id
  180. data['bbox'] = self.xyxy2xywh(bboxes[i])
  181. data['score'] = float(bboxes[i][4])
  182. data['category_id'] = self.cat_ids[label]
  183. json_results.append(data)
  184. return json_results
  185. def _segm2json(self, results):
  186. """Convert instance segmentation results to COCO json style."""
  187. bbox_json_results = []
  188. segm_json_results = []
  189. for idx in range(len(self.img_ids)):
  190. img_id = self.img_ids[idx]
  191. det, seg = results[idx]
  192. for label in range(len(det)):
  193. # Here we skip invalid predicted labels, as we use the fixed num_classes of 80 (COCO)
  194. # (assuming the num class of input dataset is no more than 80).
  195. # Recommended manually set `num_classes=${your test dataset class num}` in the
  196. # configuration.json in practice.
  197. if label >= len(self.classes):
  198. break
  199. # bbox results
  200. bboxes = det[label]
  201. for i in range(bboxes.shape[0]):
  202. data = dict()
  203. data['image_id'] = img_id
  204. data['bbox'] = self.xyxy2xywh(bboxes[i])
  205. data['score'] = float(bboxes[i][4])
  206. data['category_id'] = self.cat_ids[label]
  207. bbox_json_results.append(data)
  208. # segm results
  209. # some detectors use different scores for bbox and mask
  210. if isinstance(seg, tuple):
  211. segms = seg[0][label]
  212. mask_score = seg[1][label]
  213. else:
  214. segms = seg[label]
  215. mask_score = [bbox[4] for bbox in bboxes]
  216. for i in range(bboxes.shape[0]):
  217. data = dict()
  218. data['image_id'] = img_id
  219. data['bbox'] = self.xyxy2xywh(bboxes[i])
  220. data['score'] = float(mask_score[i])
  221. data['category_id'] = self.cat_ids[label]
  222. if isinstance(segms[i]['counts'], bytes):
  223. segms[i]['counts'] = segms[i]['counts'].decode()
  224. data['segmentation'] = segms[i]
  225. segm_json_results.append(data)
  226. return bbox_json_results, segm_json_results
  227. def results2json(self, results, outfile_prefix):
  228. """Dump the detection results to a COCO style json file.
  229. There are 3 types of results: proposals, bbox predictions, mask
  230. predictions, and they have different data types. This method will
  231. automatically recognize the type, and dump them to json files.
  232. Args:
  233. results (list[list | tuple | ndarray]): Testing results of the
  234. dataset.
  235. outfile_prefix (str): The filename prefix of the json files. If the
  236. prefix is "somepath/xxx", the json files will be named
  237. "somepath/xxx.bbox.json", "somepath/xxx.segm.json",
  238. "somepath/xxx.proposal.json".
  239. Returns:
  240. dict[str: str]: Possible keys are "bbox", "segm", "proposal", and \
  241. values are corresponding filenames.
  242. """
  243. result_files = dict()
  244. if isinstance(results[0], list):
  245. json_results = self._det2json(results)
  246. result_files['bbox'] = f'{outfile_prefix}.bbox.json'
  247. result_files['proposal'] = f'{outfile_prefix}.bbox.json'
  248. dump(json_results, result_files['bbox'])
  249. elif isinstance(results[0], tuple):
  250. json_results = self._segm2json(results)
  251. result_files['bbox'] = f'{outfile_prefix}.bbox.json'
  252. result_files['proposal'] = f'{outfile_prefix}.bbox.json'
  253. result_files['segm'] = f'{outfile_prefix}.segm.json'
  254. dump(json_results[0], result_files['bbox'])
  255. dump(json_results[1], result_files['segm'])
  256. elif isinstance(results[0], np.ndarray):
  257. json_results = self._proposal2json(results)
  258. result_files['proposal'] = f'{outfile_prefix}.proposal.json'
  259. dump(json_results, result_files['proposal'])
  260. else:
  261. raise TypeError('invalid type of results')
  262. return result_files
  263. def encode_mask_results(mask_results):
  264. """Encode bitmap mask to RLE code.
  265. Args:
  266. mask_results (list | tuple[list]): bitmap mask results.
  267. In mask scoring rcnn, mask_results is a tuple of (segm_results,
  268. segm_cls_score).
  269. Returns:
  270. list | tuple: RLE encoded mask.
  271. """
  272. if isinstance(mask_results, tuple): # mask scoring
  273. cls_segms, cls_mask_scores = mask_results
  274. else:
  275. cls_segms = mask_results
  276. num_classes = len(cls_segms)
  277. encoded_mask_results = [[] for _ in range(num_classes)]
  278. for i in range(len(cls_segms)):
  279. for cls_segm in cls_segms[i]:
  280. encoded_mask_results[i].append(
  281. mask_util.encode(
  282. np.array(
  283. cls_segm[:, :, np.newaxis], order='F',
  284. dtype='uint8'))[0]) # encoded with RLE
  285. if isinstance(mask_results, tuple):
  286. return encoded_mask_results, cls_mask_scores
  287. else:
  288. return encoded_mask_results