mean_ap.py源码解读

average_precision

  1. def average_precision(recalls, precisions, mode='area'):
  2. """Calculate average precision (for single or multiple scales).
  3. Args:
  4. recalls (ndarray): shape (num_scales, num_dets) or (num_dets, )
  5. precisions (ndarray): shape (num_scales, num_dets) or (num_dets, )
  6. mode (str): 'area' or '11points', 'area' means calculating the area
  7. under precision-recall curve, '11points' means calculating
  8. the average precision of recalls at [0, 0.1, ..., 1]
  9. Returns:
  10. float or ndarray: calculated average precision
  11. """
  12. no_scale = False
  13. if recalls.ndim == 1:
  14. no_scale = True
  15. recalls = recalls[np.newaxis, :]
  16. precisions = precisions[np.newaxis, :]
  17. assert recalls.shape == precisions.shape and recalls.ndim == 2
  18. num_scales = recalls.shape[0]
  19. ap = np.zeros(num_scales, dtype=np.float32)
  20. if mode == 'area':
  21. zeros = np.zeros((num_scales, 1), dtype=recalls.dtype)
  22. ones = np.ones((num_scales, 1), dtype=recalls.dtype)
  23. mrec = np.hstack((zeros, recalls, ones))
  24. mpre = np.hstack((zeros, precisions, zeros))
  25. for i in range(mpre.shape[1] - 1, 0, -1):
  26. mpre[:, i - 1] = np.maximum(mpre[:, i - 1], mpre[:, i])
  27. for i in range(num_scales):
  28. ind = np.where(mrec[i, 1:] != mrec[i, :-1])[0]
  29. ap[i] = np.sum(
  30. (mrec[i, ind + 1] - mrec[i, ind]) * mpre[i, ind + 1])
  31. elif mode == '11points':
  32. for i in range(num_scales):
  33. for thr in np.arange(0, 1 + 1e-3, 0.1):
  34. precs = precisions[i, recalls[i, :] >= thr]
  35. prec = precs.max() if precs.size > 0 else 0
  36. ap[i] += prec
  37. ap /= 11
  38. else:
  39. raise ValueError(
  40. 'Unrecognized mode, only "area" and "11points" are supported')
  41. if no_scale:
  42. ap = ap[0]
  43. return ap

tpfp_imagenet

  1. def tpfp_imagenet(det_bboxes,
  2. gt_bboxes,
  3. gt_bboxes_ignore=None,
  4. default_iou_thr=0.5,
  5. area_ranges=None,
  6. use_legacy_coordinate=False):
  7. """Check if detected bboxes are true positive or false positive.
  8. Args:
  9. det_bbox (ndarray): Detected bboxes of this image, of shape (m, 5).
  10. gt_bboxes (ndarray): GT bboxes of this image, of shape (n, 4).
  11. gt_bboxes_ignore (ndarray): Ignored gt bboxes of this image,
  12. of shape (k, 4). Default: None
  13. default_iou_thr (float): IoU threshold to be considered as matched for
  14. medium and large bboxes (small ones have special rules).
  15. Default: 0.5.
  16. area_ranges (list[tuple] | None): Range of bbox areas to be evaluated,
  17. in the format [(min1, max1), (min2, max2), ...]. Default: None.
  18. use_legacy_coordinate (bool): Whether to use coordinate system in
  19. mmdet v1.x. which means width, height should be
  20. calculated as 'x2 - x1 + 1` and 'y2 - y1 + 1' respectively.
  21. Default: False.
  22. Returns:
  23. tuple[np.ndarray]: (tp, fp) whose elements are 0 and 1. The shape of
  24. each array is (num_scales, m).
  25. """

tpfp_default

  1. def tpfp_default(det_bboxes,
  2. gt_bboxes,
  3. gt_bboxes_ignore=None,
  4. iou_thr=0.5,
  5. area_ranges=None,
  6. use_legacy_coordinate=False):
  7. """Check if detected bboxes are true positive or false positive.
  8. Args:
  9. det_bbox (ndarray): Detected bboxes of this image, of shape (m, 5).
  10. gt_bboxes (ndarray): GT bboxes of this image, of shape (n, 4).
  11. gt_bboxes_ignore (ndarray): Ignored gt bboxes of this image,
  12. of shape (k, 4). Default: None
  13. iou_thr (float): IoU threshold to be considered as matched.
  14. Default: 0.5.
  15. area_ranges (list[tuple] | None): Range of bbox areas to be
  16. evaluated, in the format [(min1, max1), (min2, max2), ...].
  17. Default: None.
  18. use_legacy_coordinate (bool): Whether to use coordinate system in
  19. mmdet v1.x. which means width, height should be
  20. calculated as 'x2 - x1 + 1` and 'y2 - y1 + 1' respectively.
  21. Default: False.
  22. Returns:
  23. tuple[np.ndarray]: (tp, fp) whose elements are 0 and 1. The shape of
  24. each array is (num_scales, m).
  25. """
  26. if not use_legacy_coordinate:
  27. extra_length = 0.
  28. else:
  29. extra_length = 1.
  30. # an indicator of ignored gts
  31. gt_ignore_inds = np.concatenate(
  32. (np.zeros(gt_bboxes.shape[0], dtype=np.bool),
  33. np.ones(gt_bboxes_ignore.shape[0], dtype=np.bool)))
  34. # stack gt_bboxes and gt_bboxes_ignore for convenience
  35. gt_bboxes = np.vstack((gt_bboxes, gt_bboxes_ignore))
  36. num_dets = det_bboxes.shape[0]
  37. num_gts = gt_bboxes.shape[0]
  38. if area_ranges is None:
  39. area_ranges = [(None, None)]
  40. num_scales = len(area_ranges)
  41. # tp and fp are of shape (num_scales, num_gts), each row is tp or fp of
  42. # a certain scale
  43. tp = np.zeros((num_scales, num_dets), dtype=np.float32)
  44. fp = np.zeros((num_scales, num_dets), dtype=np.float32)
  45. # if there is no gt bboxes in this image, then all det bboxes
  46. # within area range are false positives
  47. if gt_bboxes.shape[0] == 0:
  48. if area_ranges == [(None, None)]:
  49. fp[...] = 1
  50. else:
  51. det_areas = (
  52. det_bboxes[:, 2] - det_bboxes[:, 0] + extra_length) * (
  53. det_bboxes[:, 3] - det_bboxes[:, 1] + extra_length)
  54. for i, (min_area, max_area) in enumerate(area_ranges):
  55. fp[i, (det_areas >= min_area) & (det_areas < max_area)] = 1
  56. return tp, fp
  57. ious = bbox_overlaps(
  58. det_bboxes, gt_bboxes, use_legacy_coordinate=use_legacy_coordinate)
  59. # for each det, the max iou with all gts
  60. ious_max = ious.max(axis=1)
  61. # for each det, which gt overlaps most with it
  62. ious_argmax = ious.argmax(axis=1)
  63. # sort all dets in descending order by scores
  64. sort_inds = np.argsort(-det_bboxes[:, -1])
  65. for k, (min_area, max_area) in enumerate(area_ranges):
  66. gt_covered = np.zeros(num_gts, dtype=bool)
  67. # if no area range is specified, gt_area_ignore is all False
  68. if min_area is None:
  69. gt_area_ignore = np.zeros_like(gt_ignore_inds, dtype=bool)
  70. else:
  71. gt_areas = (gt_bboxes[:, 2] - gt_bboxes[:, 0] + extra_length) * (
  72. gt_bboxes[:, 3] - gt_bboxes[:, 1] + extra_length)
  73. gt_area_ignore = (gt_areas < min_area) | (gt_areas >= max_area)
  74. for i in sort_inds:
  75. if ious_max[i] >= iou_thr:
  76. matched_gt = ious_argmax[i]
  77. if not (gt_ignore_inds[matched_gt]
  78. or gt_area_ignore[matched_gt]):
  79. if not gt_covered[matched_gt]:
  80. gt_covered[matched_gt] = True
  81. tp[k, i] = 1
  82. else:
  83. fp[k, i] = 1
  84. # otherwise ignore this detected bbox, tp = 0, fp = 0
  85. elif min_area is None:
  86. fp[k, i] = 1
  87. else:
  88. bbox = det_bboxes[i, :4]
  89. area = (bbox[2] - bbox[0] + extra_length) * (
  90. bbox[3] - bbox[1] + extra_length)
  91. if area >= min_area and area < max_area:
  92. fp[k, i] = 1
  93. return tp, fp

get_cls_results

  1. def get_cls_results(det_results, annotations, class_id):
  2. """Get det results and gt information of a certain class.
  3. Args:
  4. det_results (list[list]): Same as `eval_map()`.
  5. annotations (list[dict]): Same as `eval_map()`.
  6. class_id (int): ID of a specific class.
  7. Returns:
  8. tuple[list[np.ndarray]]: detected bboxes, gt bboxes, ignored gt bboxes
  9. """
  10. cls_dets = [img_res[class_id] for img_res in det_results]
  11. cls_gts = []
  12. cls_gts_ignore = []
  13. for ann in annotations:
  14. gt_inds = ann['labels'] == class_id
  15. cls_gts.append(ann['bboxes'][gt_inds, :])
  16. if ann.get('labels_ignore', None) is not None:
  17. ignore_inds = ann['labels_ignore'] == class_id
  18. cls_gts_ignore.append(ann['bboxes_ignore'][ignore_inds, :])
  19. else:
  20. cls_gts_ignore.append(np.empty((0, 4), dtype=np.float32))
  21. return cls_dets, cls_gts, cls_gts_ignore

eval_map

  • compute tp and fp for each image with multiple processes
  1. def eval_map(det_results,
  2. annotations,
  3. scale_ranges=None,
  4. iou_thr=0.5,
  5. dataset=None,
  6. logger=None,
  7. tpfp_fn=None,
  8. nproc=4,
  9. use_legacy_coordinate=False):
  10. """Evaluate mAP of a dataset.
  11. Args:
  12. det_results (list[list]): [[cls1_det, cls2_det, ...], ...].
  13. The outer list indicates images, and the inner list indicates
  14. per-class detected bboxes.
  15. annotations (list[dict]): Ground truth annotations where each item of
  16. the list indicates an image. Keys of annotations are:
  17. - `bboxes`: numpy array of shape (n, 4)
  18. - `labels`: numpy array of shape (n, )
  19. - `bboxes_ignore` (optional): numpy array of shape (k, 4)
  20. - `labels_ignore` (optional): numpy array of shape (k, )
  21. scale_ranges (list[tuple] | None): Range of scales to be evaluated,
  22. in the format [(min1, max1), (min2, max2), ...]. A range of
  23. (32, 64) means the area range between (32**2, 64**2).
  24. Default: None.
  25. iou_thr (float): IoU threshold to be considered as matched.
  26. Default: 0.5.
  27. dataset (list[str] | str | None): Dataset name or dataset classes,
  28. there are minor differences in metrics for different datsets, e.g.
  29. "voc07", "imagenet_det", etc. Default: None.
  30. logger (logging.Logger | str | None): The way to print the mAP
  31. summary. See `mmcv.utils.print_log()` for details. Default: None.
  32. tpfp_fn (callable | None): The function used to determine true/
  33. false positives. If None, :func:`tpfp_default` is used as default
  34. unless dataset is 'det' or 'vid' (:func:`tpfp_imagenet` in this
  35. case). If it is given as a function, then this function is used
  36. to evaluate tp & fp. Default None.
  37. nproc (int): Processes used for computing TP and FP.
  38. Default: 4.
  39. use_legacy_coordinate (bool): Whether to use coordinate system in
  40. mmdet v1.x. which means width, height should be
  41. calculated as 'x2 - x1 + 1` and 'y2 - y1 + 1' respectively.
  42. Default: False.
  43. Returns:
  44. tuple: (mAP, [dict, dict, ...])
  45. """
  46. assert len(det_results) == len(annotations)
  47. if not use_legacy_coordinate:
  48. extra_length = 0.
  49. else:
  50. extra_length = 1.
  51. num_imgs = len(det_results)
  52. num_scales = len(scale_ranges) if scale_ranges is not None else 1
  53. num_classes = len(det_results[0]) # positive class num
  54. area_ranges = ([(rg[0]**2, rg[1]**2) for rg in scale_ranges]
  55. if scale_ranges is not None else None)
  56. pool = Pool(nproc)
  57. eval_results = []
  58. for i in range(num_classes):
  59. # get gt and det bboxes of this class
  60. cls_dets, cls_gts, cls_gts_ignore = get_cls_results(
  61. det_results, annotations, i)
  62. # choose proper function according to datasets to compute tp and fp
  63. if tpfp_fn is None:
  64. if dataset in ['det', 'vid']:
  65. tpfp_fn = tpfp_imagenet
  66. else:
  67. tpfp_fn = tpfp_default
  68. if not callable(tpfp_fn):
  69. raise ValueError(
  70. f'tpfp_fn has to be a function or None, but got {tpfp_fn}')
  71. # compute tp and fp for each image with multiple processes
  72. tpfp = pool.starmap(
  73. tpfp_fn,
  74. zip(cls_dets, cls_gts, cls_gts_ignore,
  75. [iou_thr for _ in range(num_imgs)],
  76. [area_ranges for _ in range(num_imgs)],
  77. [use_legacy_coordinate for _ in range(num_imgs)]))
  78. tp, fp = tuple(zip(*tpfp))
  79. # calculate gt number of each scale
  80. # ignored gts or gts beyond the specific scale are not counted
  81. num_gts = np.zeros(num_scales, dtype=int)
  82. for j, bbox in enumerate(cls_gts):
  83. if area_ranges is None:
  84. num_gts[0] += bbox.shape[0]
  85. else:
  86. gt_areas = (bbox[:, 2] - bbox[:, 0] + extra_length) * (
  87. bbox[:, 3] - bbox[:, 1] + extra_length)
  88. for k, (min_area, max_area) in enumerate(area_ranges):
  89. num_gts[k] += np.sum((gt_areas >= min_area)
  90. & (gt_areas < max_area))
  91. # sort all det bboxes by score, also sort tp and fp
  92. cls_dets = np.vstack(cls_dets)
  93. num_dets = cls_dets.shape[0]
  94. sort_inds = np.argsort(-cls_dets[:, -1])
  95. tp = np.hstack(tp)[:, sort_inds]
  96. fp = np.hstack(fp)[:, sort_inds]
  97. # calculate recall and precision with tp and fp
  98. tp = np.cumsum(tp, axis=1)
  99. fp = np.cumsum(fp, axis=1)
  100. eps = np.finfo(np.float32).eps
  101. recalls = tp / np.maximum(num_gts[:, np.newaxis], eps)
  102. precisions = tp / np.maximum((tp + fp), eps)
  103. # calculate AP
  104. if scale_ranges is None:
  105. recalls = recalls[0, :]
  106. precisions = precisions[0, :]
  107. num_gts = num_gts.item()
  108. mode = 'area' if dataset != 'voc07' else '11points'
  109. ap = average_precision(recalls, precisions, mode)
  110. eval_results.append({
  111. 'num_gts': num_gts,
  112. 'num_dets': num_dets,
  113. 'recall': recalls,
  114. 'precision': precisions,
  115. 'ap': ap
  116. })
  117. pool.close()
  118. if scale_ranges is not None:
  119. # shape (num_classes, num_scales)
  120. all_ap = np.vstack([cls_result['ap'] for cls_result in eval_results])
  121. all_num_gts = np.vstack(
  122. [cls_result['num_gts'] for cls_result in eval_results])
  123. mean_ap = []
  124. for i in range(num_scales):
  125. if np.any(all_num_gts[:, i] > 0):
  126. mean_ap.append(all_ap[all_num_gts[:, i] > 0, i].mean())
  127. else:
  128. mean_ap.append(0.0)
  129. else:
  130. aps = []
  131. for cls_result in eval_results:
  132. if cls_result['num_gts'] > 0:
  133. aps.append(cls_result['ap'])
  134. mean_ap = np.array(aps).mean().item() if aps else 0.0
  135. print_map_summary(
  136. mean_ap, eval_results, dataset, area_ranges, logger=logger)
  137. return mean_ap, eval_results

print_map_summary

  1. def print_map_summary(mean_ap,
  2. results,
  3. dataset=None,
  4. scale_ranges=None,
  5. logger=None):
  6. """Print mAP and results of each class.
  7. A table will be printed to show the gts/dets/recall/AP of each class and
  8. the mAP.
  9. Args:
  10. mean_ap (float): Calculated from `eval_map()`.
  11. results (list[dict]): Calculated from `eval_map()`.
  12. dataset (list[str] | str | None): Dataset name or dataset classes.
  13. scale_ranges (list[tuple] | None): Range of scales to be evaluated.
  14. logger (logging.Logger | str | None): The way to print the mAP
  15. summary. See `mmcv.utils.print_log()` for details. Default: None.
  16. """
  17. if logger == 'silent':
  18. return
  19. if isinstance(results[0]['ap'], np.ndarray):
  20. num_scales = len(results[0]['ap'])
  21. else:
  22. num_scales = 1
  23. if scale_ranges is not None:
  24. assert len(scale_ranges) == num_scales
  25. num_classes = len(results)
  26. recalls = np.zeros((num_scales, num_classes), dtype=np.float32)
  27. aps = np.zeros((num_scales, num_classes), dtype=np.float32)
  28. num_gts = np.zeros((num_scales, num_classes), dtype=int)
  29. for i, cls_result in enumerate(results):
  30. if cls_result['recall'].size > 0:
  31. recalls[:, i] = np.array(cls_result['recall'], ndmin=2)[:, -1]
  32. aps[:, i] = cls_result['ap']
  33. num_gts[:, i] = cls_result['num_gts']
  34. if dataset is None:
  35. label_names = [str(i) for i in range(num_classes)]
  36. elif mmcv.is_str(dataset):
  37. label_names = get_classes(dataset)
  38. else:
  39. label_names = dataset
  40. if not isinstance(mean_ap, list):
  41. mean_ap = [mean_ap]
  42. header = ['class', 'gts', 'dets', 'recall', 'ap']
  43. for i in range(num_scales):
  44. if scale_ranges is not None:
  45. print_log(f'Scale range {scale_ranges[i]}', logger=logger)
  46. table_data = [header]
  47. for j in range(num_classes):
  48. row_data = [
  49. label_names[j], num_gts[i, j], results[j]['num_dets'],
  50. f'{recalls[i, j]:.3f}', f'{aps[i, j]:.3f}'
  51. ]
  52. table_data.append(row_data)
  53. table_data.append(['mAP', '', '', '', f'{mean_ap[i]:.3f}'])
  54. table = AsciiTable(table_data)
  55. table.inner_footing_row_border = True
  56. print_log('\n' + table.table, logger=logger)