Source code for cvbase.det.eval

import numpy as np
from six.moves import zip
from terminaltables import AsciiTable

from cvbase.decorators import requires_package
from cvbase.det.bbox_ops import bbox_overlaps
from cvbase.det.labels import read_labels


def _recalls(all_ious, proposal_nums, thrs):

    img_num = all_ious.shape[0]
    total_gt_num = sum([ious.shape[0] for ious in all_ious])

    _ious = np.zeros((proposal_nums.size, total_gt_num), dtype=np.float32)
    for k, proposal_num in enumerate(proposal_nums):
        tmp_ious = np.zeros(0)
        for i in range(img_num):
            ious = all_ious[i][:, :proposal_num].copy()
            gt_ious = np.zeros((ious.shape[0]))
            if ious.size == 0:
                tmp_ious = np.hstack((tmp_ious, gt_ious))
                continue
            for j in range(ious.shape[0]):
                gt_max_overlaps = ious.argmax(axis=1)
                max_ious = ious[np.arange(0, ious.shape[0]), gt_max_overlaps]
                gt_idx = max_ious.argmax()
                gt_ious[j] = max_ious[gt_idx]
                box_idx = gt_max_overlaps[gt_idx]
                ious[gt_idx, :] = -1
                ious[:, box_idx] = -1
            tmp_ious = np.hstack((tmp_ious, gt_ious))
        _ious[k, :] = tmp_ious

    _ious = np.fliplr(np.sort(_ious, axis=1))
    recalls = np.zeros((proposal_nums.size, thrs.size))
    for i, thr in enumerate(thrs):
        recalls[:, i] = (_ious >= thr).sum(axis=1) / float(total_gt_num)

    return recalls


[docs]def set_recall_param(proposal_nums, iou_thrs): """Check proposal_nums and iou_thrs and set correct format """ if isinstance(proposal_nums, list): _proposal_nums = np.array(proposal_nums) elif isinstance(proposal_nums, int): _proposal_nums = np.array([proposal_nums]) else: _proposal_nums = proposal_nums if iou_thrs is None: _iou_thrs = np.array([0.5]) elif isinstance(iou_thrs, list): _iou_thrs = np.array(iou_thrs) elif isinstance(iou_thrs, float): _iou_thrs = np.array([iou_thrs]) else: _iou_thrs = iou_thrs return _proposal_nums, _iou_thrs
[docs]def bbox_recalls(gts, proposals, proposal_nums=None, iou_thrs=None, print_summary=True): """Calculate recalls Args: gts(list or ndarray): a list of arrays of shape (n, 4) proposals(list or ndarray): a list of arrays of shape (k, 4) or (k, 5) proposal_nums(int or list of int or ndarray): top N proposals thrs(float or list or ndarray): iou thresholds Returns: ndarray: recalls of different ious and proposal nums """ img_num = len(gts) assert img_num == len(proposals) proposal_nums, iou_thrs = set_recall_param(proposal_nums, iou_thrs) all_ious = [] for i in range(img_num): if proposals[i].ndim == 2 and proposals[i].shape[1] == 5: scores = proposals[i][:, 4] sort_idx = np.argsort(scores)[::-1] img_proposal = proposals[i][sort_idx, :] else: img_proposal = proposals[i] prop_num = min(img_proposal.shape[0], proposal_nums[-1]) if gts[i] is None or gts[i].shape[0] == 0: ious = np.zeros((0, img_proposal.shape[0]), dtype=np.float32) else: ious = bbox_overlaps(gts[i], img_proposal[:prop_num, :4]) all_ious.append(ious) all_ious = np.array(all_ious) recalls = _recalls(all_ious, proposal_nums, iou_thrs) if print_summary: print_recall_summary(recalls, proposal_nums, iou_thrs) return recalls
[docs]@requires_package('matplotlib') def plot_num_recall(recalls, proposal_nums): """Plot Proposal_num-Recalls curve Args: recalls(ndarray or list): shape (k,) proposal_nums(ndarray or list): same shape as `recalls` """ if isinstance(proposal_nums, np.ndarray): _proposal_nums = proposal_nums.tolist() else: _proposal_nums = proposal_nums if isinstance(recalls, np.ndarray): _recalls = recalls.tolist() else: _recalls = recalls import matplotlib.pyplot as plt f = plt.figure() plt.plot([0] + _proposal_nums, [0] + _recalls) plt.xlabel('Proposal num') plt.ylabel('Recall') plt.axis([0, proposal_nums.max(), 0, 1]) f.show()
[docs]@requires_package('matplotlib') def plot_iou_recall(recalls, iou_thrs): """Plot IoU-Recalls curve Args: recalls(ndarray or list): shape (k,) iou_thrs(ndarray or list): same shape as `recalls` """ if isinstance(iou_thrs, np.ndarray): _iou_thrs = iou_thrs.tolist() else: _iou_thrs = iou_thrs if isinstance(recalls, np.ndarray): _recalls = recalls.tolist() else: _recalls = recalls import matplotlib.pyplot as plt f = plt.figure() plt.plot(_iou_thrs + [1.0], _recalls + [0.]) plt.xlabel('IoU') plt.ylabel('Recall') plt.axis([iou_thrs.min(), 1, 0, 1]) f.show()
[docs]def average_precision(recalls, precisions, mode='area'): """Calculate average precision (for single or multiple scales) Args: recalls(ndarray): shape (num_scales, num_dets) or (num_dets, ) precisions(ndarray): shape (num_scales, num_dets) or (num_dets, ) mode(str): 'area' or '11points', 'area' means calculating the area under precision-recall curve, '11points' means calculating the average precision of recalls at [0, 0.1, ..., 1] Returns: float or ndarray: calculated average precision """ noscale = False if recalls.ndim == 1: noscale = True recalls = recalls[np.newaxis, :] precisions = precisions[np.newaxis, :] assert recalls.shape == precisions.shape and recalls.ndim == 2 num_scales = recalls.shape[0] ap = np.zeros(num_scales, dtype=np.float32) if mode == 'area': zeros = np.zeros((num_scales, 1), dtype=recalls.dtype) ones = np.ones((num_scales, 1), dtype=recalls.dtype) mrec = np.hstack((zeros, recalls, ones)) mpre = np.hstack((zeros, precisions, zeros)) for i in range(mpre.shape[1] - 1, 0, -1): mpre[:, i - 1] = np.maximum(mpre[:, i - 1], mpre[:, i]) for i in range(num_scales): ind = np.where(mrec[i, 1:] != mrec[i, :-1])[0] ap[i] = np.sum( (mrec[i, ind + 1] - mrec[i, ind]) * mpre[i, ind + 1]) elif mode == '11points': for i in range(num_scales): for thr in np.arange(0, 1 + 1e-3, 0.1): precs = precisions[i, recalls[i, :] >= thr] prec = precs.max() if precs.size > 0 else 0 ap[i] += prec ap /= 11 else: raise ValueError( 'Unrecognized mode, only "area" and "11points" are supported') if noscale: ap = ap[0] return ap
[docs]def tpfp_imagenet(det_bboxes, gt_bboxes, gt_ignore, default_iou_thr, area_ranges=None): """Check if detected bboxes are true positive or false positive. Args: det_bbox(ndarray): the detected bbox gt_bboxes(ndarray): ground truth bboxes of this image gt_ignore(ndarray): indicate if gts are ignored for evaluation or not default_iou_thr(float): the iou thresholds for medium and large bboxes area_ranges(list or None): gt bbox area ranges Returns: tuple: two arrays (tp, fp) whose elements are 0 and 1 """ num_dets = det_bboxes.shape[0] num_gts = gt_bboxes.shape[0] if area_ranges is None: area_ranges = [(None, None)] num_scales = len(area_ranges) # tp and fp are of shape (num_scales, num_gts), each row is tp or fp of a certain scale tp = np.zeros((num_scales, num_dets), dtype=np.float32) fp = np.zeros((num_scales, num_dets), dtype=np.float32) if gt_bboxes.shape[0] == 0: if area_ranges == [(None, None)]: fp[...] = 1 else: det_areas = (det_bboxes[:, 2] - det_bboxes[:, 0] + 1) * ( det_bboxes[:, 3] - det_bboxes[:, 1] + 1) for i, (min_area, max_area) in enumerate(area_ranges): fp[i, (det_areas >= min_area) & (det_areas < max_area)] = 1 return tp, fp ious = bbox_overlaps(det_bboxes, gt_bboxes - 1) gt_w = gt_bboxes[:, 2] - gt_bboxes[:, 0] + 1 gt_h = gt_bboxes[:, 3] - gt_bboxes[:, 1] + 1 iou_thrs = np.minimum((gt_w * gt_h) / ((gt_w + 10.0) * (gt_h + 10.0)), default_iou_thr) # sort all detections by scores in descending order sort_inds = np.argsort(-det_bboxes[:, -1]) for k, (min_area, max_area) in enumerate(area_ranges): gt_covered = np.zeros(num_gts, dtype=bool) # if no area range is specified, gt_area_ignore is all False if min_area is None: gt_area_ignore = np.zeros_like(gt_ignore, dtype=bool) else: gt_areas = gt_w * gt_h gt_area_ignore = (gt_areas < min_area) | (gt_areas >= max_area) for i in sort_inds: max_iou = -1 matched_gt = -1 # find best overlapped available gt for j in range(num_gts): # different from PASCAL VOC: allow finding other gts if the # best overlaped ones are already matched by other det bboxes if gt_covered[j]: continue elif ious[i, j] >= iou_thrs[j] and ious[i, j] > max_iou: max_iou = ious[i, j] matched_gt = j # there are 4 cases for a det bbox: # 1. this det bbox matches a gt, tp = 1, fp = 0 # 2. this det bbox matches an ignored gt, tp = 0, fp = 0 # 3. this det bbox matches no gt and within area range, tp = 0, fp = 1 # 4. this det bbox matches no gt but is beyond area range, tp = 0, fp = 0 if matched_gt >= 0: gt_covered[matched_gt] = 1 if not (gt_ignore[matched_gt] or gt_area_ignore[matched_gt]): tp[k, i] = 1 elif min_area is None: fp[k, i] = 1 else: bbox = det_bboxes[i, :4] area = (bbox[2] - bbox[0] + 1) * (bbox[3] - bbox[1] + 1) if area >= min_area and area < max_area: fp[k, i] = 1 return tp, fp
[docs]def tpfp_default(det_bboxes, gt_bboxes, gt_ignore, iou_thr, area_ranges=None): """Check if detected bboxes are true positive or false positive. Args: det_bbox(ndarray): the detected bbox gt_bboxes(ndarray): ground truth bboxes of this image gt_ignore(ndarray): indicate if gts are ignored for evaluation or not iou_thr(float): the iou thresholds Returns: tuple: (tp, fp), two arrays whose elements are 0 and 1 """ num_dets = det_bboxes.shape[0] num_gts = gt_bboxes.shape[0] if area_ranges is None: area_ranges = [(None, None)] num_scales = len(area_ranges) # tp and fp are of shape (num_scales, num_gts), each row is tp or fp of a certain scale tp = np.zeros((num_scales, num_dets), dtype=np.float32) fp = np.zeros((num_scales, num_dets), dtype=np.float32) # if there is no gt bboxes in this image, then all det bboxes # within area range are false positives if gt_bboxes.shape[0] == 0: if area_ranges == [(None, None)]: fp[...] = 1 else: det_areas = (det_bboxes[:, 2] - det_bboxes[:, 0] + 1) * ( det_bboxes[:, 3] - det_bboxes[:, 1] + 1) for i, (min_area, max_area) in enumerate(area_ranges): fp[i, (det_areas >= min_area) & (det_areas < max_area)] = 1 return tp, fp ious = bbox_overlaps(det_bboxes, gt_bboxes) ious_max = ious.max(axis=1) ious_argmax = ious.argmax(axis=1) sort_inds = np.argsort(-det_bboxes[:, -1]) for k, (min_area, max_area) in enumerate(area_ranges): gt_covered = np.zeros(num_gts, dtype=bool) # if no area range is specified, gt_area_ignore is all False if min_area is None: gt_area_ignore = np.zeros_like(gt_ignore, dtype=bool) else: gt_areas = (gt_bboxes[:, 2] - gt_bboxes[:, 0] + 1) * ( gt_bboxes[:, 3] - gt_bboxes[:, 1] + 1) gt_area_ignore = (gt_areas < min_area) | (gt_areas >= max_area) for i in sort_inds: if ious_max[i] >= iou_thr: matched_gt = ious_argmax[i] if not (gt_ignore[matched_gt] or gt_area_ignore[matched_gt]): if not gt_covered[matched_gt]: gt_covered[matched_gt] = True tp[k, i] = 1 else: fp[k, i] = 1 # otherwise ignore this detected bbox, tp = 0, fp = 0 elif min_area is None: fp[k, i] = 1 else: bbox = det_bboxes[i, :4] area = (bbox[2] - bbox[0] + 1) * (bbox[3] - bbox[1] + 1) if area >= min_area and area < max_area: fp[k, i] = 1 return tp, fp
[docs]def get_cls_results(det_results, gt_bboxes, gt_labels, gt_ignore, class_id): """Get det results and gt information of a certain class. """ cls_dets = [det[class_id] for det in det_results] # det bboxes of this class cls_gts = [] # gt bboxes of this class cls_gt_ignore = [] for j in range(len(gt_bboxes)): gt_bbox = gt_bboxes[j] cls_inds = (gt_labels[j] == class_id + 1) cls_gt = gt_bbox[cls_inds, :] if gt_bbox.shape[0] > 0 else gt_bbox cls_gts.append(cls_gt) if gt_ignore is None: cls_gt_ignore.append(np.zeros(cls_gt.shape[0], dtype=np.int32)) else: cls_gt_ignore.append(gt_ignore[j][cls_inds]) return cls_dets, cls_gts, cls_gt_ignore
[docs]def eval_map(det_results, gt_bboxes, gt_labels, gt_ignore=None, scale_ranges=None, iou_thr=0.5, dataset=None, print_summary=True): """Evaluate mAP of a dataset Args: det_results(list): a list of list, [[cls1_det, cls2_det, ...], ...] gt_bboxes(list): ground truth bboxes of each image, a list of K*4 array gt_labels(list): ground truth labels of each image, a list of K array gt_ignore(list): gt ignore indicators of each image, a list of K array scale_ranges(list or None): a list of tuples, [(min1, max1), (min2, max2), ...] iou_thr(float): IoU threshold dataset(None or str): dataset name, there are minor differences in metrics for different datsets, e.g. "voc07", "voc12", "det", "vid" print_summary(bool): whether to print the mAP summary Returns: tuple: (mAP, [dict, dict, ...]) """ assert len(det_results) == len(gt_bboxes) == len(gt_labels) if gt_ignore is not None: assert len(gt_ignore) == len(gt_labels) for i in range(len(gt_ignore)): assert len(gt_labels[i]) == len(gt_ignore[i]) area_ranges = ([(rg[0]**2, rg[1]**2) for rg in scale_ranges] if scale_ranges is not None else None) num_scales = len(scale_ranges) if scale_ranges is not None else 1 eval_results = [] num_classes = len(det_results[0]) # positive class num gt_labels = [ label if label.ndim == 1 else label[:, 0] for label in gt_labels ] for i in range(num_classes): # get gt and det bboxes of this class cls_dets, cls_gts, cls_gt_ignore = get_cls_results( det_results, gt_bboxes, gt_labels, gt_ignore, i) # calculate tp and fp for each image tpfp_func = (tpfp_imagenet if dataset in ['det', 'vid'] else tpfp_default) tpfp = [ tpfp_func(cls_dets[j], cls_gts[j], cls_gt_ignore[j], iou_thr, area_ranges) for j in range(len(cls_dets)) ] tp, fp = tuple(zip(*tpfp)) # calculate gt number of each scale, gts ignored or beyond scale are not counted num_gts = np.zeros(num_scales, dtype=int) for j, bbox in enumerate(cls_gts): if area_ranges is None: num_gts[0] += np.sum(np.logical_not(cls_gt_ignore[j])) else: gt_areas = (bbox[:, 2] - bbox[:, 0] + 1) * ( bbox[:, 3] - bbox[:, 1] + 1) for k, (min_area, max_area) in enumerate(area_ranges): num_gts[k] += np.sum( np.logical_not(cls_gt_ignore[j]) & (gt_areas >= min_area) & (gt_areas < max_area)) # sort all det bboxes by score, also sort tp and fp cls_dets = np.vstack(cls_dets) num_dets = cls_dets.shape[0] sort_inds = np.argsort(-cls_dets[:, -1]) tp = np.hstack(tp)[:, sort_inds] fp = np.hstack(fp)[:, sort_inds] # calculate recall and precision with tp and fp tp = np.cumsum(tp, axis=1) fp = np.cumsum(fp, axis=1) eps = np.finfo(np.float32).eps recalls = tp / np.maximum(num_gts[:, np.newaxis], eps) precisions = tp / np.maximum((tp + fp), eps) # calculate AP if scale_ranges is None: recalls = recalls[0, :] precisions = precisions[0, :] num_gts = num_gts.item() mode = 'area' if dataset != 'voc07' else '11points' ap = average_precision(recalls, precisions, mode) eval_results.append({ 'num_gts': num_gts, 'num_dets': num_dets, 'recall': recalls, 'precision': precisions, 'ap': ap }) if scale_ranges is not None: # shape (num_classes, num_scales) all_ap = np.vstack([cls_result['ap'] for cls_result in eval_results]) all_num_gts = np.vstack( [cls_result['num_gts'] for cls_result in eval_results]) mean_ap = [ all_ap[all_num_gts[:, i] > 0, i].mean() if np.any(all_num_gts[:, i] > 0) else 0.0 for i in range(num_scales) ] else: aps = [] for cls_result in eval_results: if cls_result['num_gts'] > 0: aps.append(cls_result['ap']) mean_ap = np.array(aps).mean().item() if aps else 0.0 if print_summary: print_map_summary(mean_ap, eval_results, dataset) return mean_ap, eval_results