# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import absolute_import from __future__ import division from __future__ import print_function from __future__ import unicode_literals import os import sys import numpy as np from ..data.source.voc import pascalvoc_label from .map_utils import DetectionMAP from .coco_eval import bbox2out from .logger import setup_logger logger = setup_logger(__name__) __all__ = ['bbox_eval', 'bbox2out', 'get_category_info'] def bbox_eval(results, class_num, overlap_thresh=0.5, map_type='11point', is_bbox_normalized=False, evaluate_difficult=False): """ Bounding box evaluation for VOC dataset Args: results (list): prediction bounding box results. class_num (int): evaluation class number. overlap_thresh (float): the postive threshold of bbox overlap map_type (string): method for mAP calcualtion, can only be '11point' or 'integral' is_bbox_normalized (bool): whether bbox is normalized to range [0, 1]. evaluate_difficult (bool): whether to evaluate difficult gt bbox. """ assert 'bbox' in results[0] logger.info("Start evaluate...") detection_map = DetectionMAP( class_num=class_num, overlap_thresh=overlap_thresh, map_type=map_type, is_bbox_normalized=is_bbox_normalized, evaluate_difficult=evaluate_difficult) for t in results: bboxes = t['bbox'] bbox_lengths = t['bbox_num'] if bboxes.shape == (1, 1) or bboxes is None: continue gt_boxes = t['gt_bbox'] gt_labels = t['gt_class'] difficults = t['difficult'] if not evaluate_difficult \ else None scale_factor = t['scale_factor'] if 'scale_factor' in t else np.ones( (gt_boxes.shape[0], 2)).astype('float32') bbox_idx = 0 for i in range(gt_boxes.shape[0]): gt_box = gt_boxes[i] h, w = scale_factor[i] gt_box = gt_box / np.array([w, h, w, h]) gt_label = gt_labels[i] difficult = None if difficults is None \ else difficults[i] bbox_num = bbox_lengths[i] bbox = bboxes[bbox_idx:bbox_idx + bbox_num] gt_box, gt_label, difficult = prune_zero_padding(gt_box, gt_label, difficult) detection_map.update(bbox, gt_box, gt_label, difficult) bbox_idx += bbox_num logger.info("Accumulating evaluatation results...") detection_map.accumulate() map_stat = 100. * detection_map.get_map() logger.info("mAP({:.2f}, {}) = {:.2f}".format(overlap_thresh, map_type, map_stat)) return map_stat def prune_zero_padding(gt_box, gt_label, difficult=None): valid_cnt = 0 for i in range(len(gt_box)): if gt_box[i, 0] == 0 and gt_box[i, 1] == 0 and \ gt_box[i, 2] == 0 and gt_box[i, 3] == 0: break valid_cnt += 1 return (gt_box[:valid_cnt], gt_label[:valid_cnt], difficult[:valid_cnt] if difficult is not None else None) def get_category_info(anno_file=None, with_background=True, use_default_label=False): if use_default_label or anno_file is None \ or not os.path.exists(anno_file): logger.info("Not found annotation file {}, load " "voc2012 categories.".format(anno_file)) return vocall_category_info(with_background) else: logger.info("Load categories from {}".format(anno_file)) return get_category_info_from_anno(anno_file, with_background) def get_category_info_from_anno(anno_file, with_background=True): """ Get class id to category id map and category id to category name map from annotation file. Args: anno_file (str): annotation file path with_background (bool, default True): whether load background as class 0. """ cats = [] with open(anno_file) as f: for line in f.readlines(): cats.append(line.strip()) if cats[0] != 'background' and with_background: cats.insert(0, 'background') if cats[0] == 'background' and not with_background: cats = cats[1:] clsid2catid = {i: i for i in range(len(cats))} catid2name = {i: name for i, name in enumerate(cats)} return clsid2catid, catid2name def vocall_category_info(with_background=True): """ Get class id to category id map and category id to category name map of mixup voc dataset Args: with_background (bool, default True): whether load background as class 0. """ label_map = pascalvoc_label(with_background) label_map = sorted(label_map.items(), key=lambda x: x[1]) cats = [l[0] for l in label_map] if with_background: cats.insert(0, 'background') clsid2catid = {i: i for i in range(len(cats))} catid2name = {i: name for i, name in enumerate(cats)} return clsid2catid, catid2name