Spaces:
Build error
Build error
| # Copyright (c) Meta Platforms, Inc. and affiliates. | |
| # All rights reserved. | |
| # | |
| # This source code is licensed under the license found in the | |
| # LICENSE file in the root directory of this source tree. | |
| import copy | |
| import os | |
| from argparse import ArgumentParser | |
| from multiprocessing import Pool | |
| import matplotlib.pyplot as plt | |
| import numpy as np | |
| from pycocotools.coco import COCO | |
| from pycocotools.cocoeval import COCOeval | |
| def makeplot(rs, ps, outDir, class_name, iou_type): | |
| cs = np.vstack([ | |
| np.ones((2, 3)), | |
| np.array([0.31, 0.51, 0.74]), | |
| np.array([0.75, 0.31, 0.30]), | |
| np.array([0.36, 0.90, 0.38]), | |
| np.array([0.50, 0.39, 0.64]), | |
| np.array([1, 0.6, 0]), | |
| ]) | |
| areaNames = ['allarea', 'small', 'medium', 'large'] | |
| types = ['C75', 'C50', 'Loc', 'Sim', 'Oth', 'BG', 'FN'] | |
| for i in range(len(areaNames)): | |
| area_ps = ps[..., i, 0] | |
| figure_title = iou_type + '-' + class_name + '-' + areaNames[i] | |
| aps = [ps_.mean() for ps_ in area_ps] | |
| ps_curve = [ | |
| ps_.mean(axis=1) if ps_.ndim > 1 else ps_ for ps_ in area_ps | |
| ] | |
| ps_curve.insert(0, np.zeros(ps_curve[0].shape)) | |
| fig = plt.figure() | |
| ax = plt.subplot(111) | |
| for k in range(len(types)): | |
| ax.plot(rs, ps_curve[k + 1], color=[0, 0, 0], linewidth=0.5) | |
| ax.fill_between( | |
| rs, | |
| ps_curve[k], | |
| ps_curve[k + 1], | |
| color=cs[k], | |
| label=str(f'[{aps[k]:.3f}]' + types[k]), | |
| ) | |
| plt.xlabel('recall') | |
| plt.ylabel('precision') | |
| plt.xlim(0, 1.0) | |
| plt.ylim(0, 1.0) | |
| plt.title(figure_title) | |
| plt.legend() | |
| # plt.show() | |
| fig.savefig(outDir + f'/{figure_title}.png') | |
| plt.close(fig) | |
| def autolabel(ax, rects): | |
| """Attach a text label above each bar in *rects*, displaying its height.""" | |
| for rect in rects: | |
| height = rect.get_height() | |
| if height > 0 and height <= 1: # for percent values | |
| text_label = '{:2.0f}'.format(height * 100) | |
| else: | |
| text_label = '{:2.0f}'.format(height) | |
| ax.annotate( | |
| text_label, | |
| xy=(rect.get_x() + rect.get_width() / 2, height), | |
| xytext=(0, 3), # 3 points vertical offset | |
| textcoords='offset points', | |
| ha='center', | |
| va='bottom', | |
| fontsize='x-small', | |
| ) | |
| def makebarplot(rs, ps, outDir, class_name, iou_type): | |
| areaNames = ['allarea', 'small', 'medium', 'large'] | |
| types = ['C75', 'C50', 'Loc', 'Sim', 'Oth', 'BG', 'FN'] | |
| fig, ax = plt.subplots() | |
| x = np.arange(len(areaNames)) # the areaNames locations | |
| width = 0.60 # the width of the bars | |
| rects_list = [] | |
| figure_title = iou_type + '-' + class_name + '-' + 'ap bar plot' | |
| for i in range(len(types) - 1): | |
| type_ps = ps[i, ..., 0] | |
| aps = [ps_.mean() for ps_ in type_ps.T] | |
| rects_list.append( | |
| ax.bar( | |
| x - width / 2 + (i + 1) * width / len(types), | |
| aps, | |
| width / len(types), | |
| label=types[i], | |
| )) | |
| # Add some text for labels, title and custom x-axis tick labels, etc. | |
| ax.set_ylabel('Mean Average Precision (mAP)') | |
| ax.set_title(figure_title) | |
| ax.set_xticks(x) | |
| ax.set_xticklabels(areaNames) | |
| ax.legend() | |
| # Add score texts over bars | |
| for rects in rects_list: | |
| autolabel(ax, rects) | |
| # Save plot | |
| fig.savefig(outDir + f'/{figure_title}.png') | |
| plt.close(fig) | |
| def get_gt_area_group_numbers(cocoEval): | |
| areaRng = cocoEval.params.areaRng | |
| areaRngStr = [str(aRng) for aRng in areaRng] | |
| areaRngLbl = cocoEval.params.areaRngLbl | |
| areaRngStr2areaRngLbl = dict(zip(areaRngStr, areaRngLbl)) | |
| areaRngLbl2Number = dict.fromkeys(areaRngLbl, 0) | |
| for evalImg in cocoEval.evalImgs: | |
| if evalImg: | |
| for gtIgnore in evalImg['gtIgnore']: | |
| if not gtIgnore: | |
| aRngLbl = areaRngStr2areaRngLbl[str(evalImg['aRng'])] | |
| areaRngLbl2Number[aRngLbl] += 1 | |
| return areaRngLbl2Number | |
| def make_gt_area_group_numbers_plot(cocoEval, outDir, verbose=True): | |
| areaRngLbl2Number = get_gt_area_group_numbers(cocoEval) | |
| areaRngLbl = areaRngLbl2Number.keys() | |
| if verbose: | |
| print('number of annotations per area group:', areaRngLbl2Number) | |
| # Init figure | |
| fig, ax = plt.subplots() | |
| x = np.arange(len(areaRngLbl)) # the areaNames locations | |
| width = 0.60 # the width of the bars | |
| figure_title = 'number of annotations per area group' | |
| rects = ax.bar(x, areaRngLbl2Number.values(), width) | |
| # Add some text for labels, title and custom x-axis tick labels, etc. | |
| ax.set_ylabel('Number of annotations') | |
| ax.set_title(figure_title) | |
| ax.set_xticks(x) | |
| ax.set_xticklabels(areaRngLbl) | |
| # Add score texts over bars | |
| autolabel(ax, rects) | |
| # Save plot | |
| fig.tight_layout() | |
| fig.savefig(outDir + f'/{figure_title}.png') | |
| plt.close(fig) | |
| def make_gt_area_histogram_plot(cocoEval, outDir): | |
| n_bins = 100 | |
| areas = [ann['area'] for ann in cocoEval.cocoGt.anns.values()] | |
| # init figure | |
| figure_title = 'gt annotation areas histogram plot' | |
| fig, ax = plt.subplots() | |
| # Set the number of bins | |
| ax.hist(np.sqrt(areas), bins=n_bins) | |
| # Add some text for labels, title and custom x-axis tick labels, etc. | |
| ax.set_xlabel('Squareroot Area') | |
| ax.set_ylabel('Number of annotations') | |
| ax.set_title(figure_title) | |
| # Save plot | |
| fig.tight_layout() | |
| fig.savefig(outDir + f'/{figure_title}.png') | |
| plt.close(fig) | |
| def analyze_individual_category(k, | |
| cocoDt, | |
| cocoGt, | |
| catId, | |
| iou_type, | |
| areas=None): | |
| nm = cocoGt.loadCats(catId)[0] | |
| print(f'--------------analyzing {k + 1}-{nm["name"]}---------------') | |
| ps_ = {} | |
| dt = copy.deepcopy(cocoDt) | |
| nm = cocoGt.loadCats(catId)[0] | |
| imgIds = cocoGt.getImgIds() | |
| dt_anns = dt.dataset['annotations'] | |
| select_dt_anns = [] | |
| for ann in dt_anns: | |
| if ann['category_id'] == catId: | |
| select_dt_anns.append(ann) | |
| dt.dataset['annotations'] = select_dt_anns | |
| dt.createIndex() | |
| # compute precision but ignore superclass confusion | |
| gt = copy.deepcopy(cocoGt) | |
| child_catIds = gt.getCatIds(supNms=[nm['supercategory']]) | |
| for idx, ann in enumerate(gt.dataset['annotations']): | |
| if ann['category_id'] in child_catIds and ann['category_id'] != catId: | |
| gt.dataset['annotations'][idx]['ignore'] = 1 | |
| gt.dataset['annotations'][idx]['iscrowd'] = 1 | |
| gt.dataset['annotations'][idx]['category_id'] = catId | |
| cocoEval = COCOeval(gt, copy.deepcopy(dt), iou_type) | |
| cocoEval.params.imgIds = imgIds | |
| cocoEval.params.maxDets = [100] | |
| cocoEval.params.iouThrs = [0.1] | |
| cocoEval.params.useCats = 1 | |
| if areas: | |
| cocoEval.params.areaRng = [[0**2, areas[2]], [0**2, areas[0]], | |
| [areas[0], areas[1]], [areas[1], areas[2]]] | |
| cocoEval.evaluate() | |
| cocoEval.accumulate() | |
| ps_supercategory = cocoEval.eval['precision'][0, :, k, :, :] | |
| ps_['ps_supercategory'] = ps_supercategory | |
| # compute precision but ignore any class confusion | |
| gt = copy.deepcopy(cocoGt) | |
| for idx, ann in enumerate(gt.dataset['annotations']): | |
| if ann['category_id'] != catId: | |
| gt.dataset['annotations'][idx]['ignore'] = 1 | |
| gt.dataset['annotations'][idx]['iscrowd'] = 1 | |
| gt.dataset['annotations'][idx]['category_id'] = catId | |
| cocoEval = COCOeval(gt, copy.deepcopy(dt), iou_type) | |
| cocoEval.params.imgIds = imgIds | |
| cocoEval.params.maxDets = [100] | |
| cocoEval.params.iouThrs = [0.1] | |
| cocoEval.params.useCats = 1 | |
| if areas: | |
| cocoEval.params.areaRng = [[0**2, areas[2]], [0**2, areas[0]], | |
| [areas[0], areas[1]], [areas[1], areas[2]]] | |
| cocoEval.evaluate() | |
| cocoEval.accumulate() | |
| ps_allcategory = cocoEval.eval['precision'][0, :, k, :, :] | |
| ps_['ps_allcategory'] = ps_allcategory | |
| return k, ps_ | |
| def analyze_results(res_file, | |
| ann_file, | |
| res_types, | |
| out_dir, | |
| extraplots=None, | |
| areas=None): | |
| for res_type in res_types: | |
| assert res_type in ['bbox', 'segm'] | |
| if areas: | |
| assert len(areas) == 3, '3 integers should be specified as areas, \ | |
| representing 3 area regions' | |
| directory = os.path.dirname(out_dir + '/') | |
| if not os.path.exists(directory): | |
| print(f'-------------create {out_dir}-----------------') | |
| os.makedirs(directory) | |
| cocoGt = COCO(ann_file) | |
| cocoDt = cocoGt.loadRes(res_file) | |
| imgIds = cocoGt.getImgIds() | |
| for res_type in res_types: | |
| res_out_dir = out_dir + '/' + res_type + '/' | |
| res_directory = os.path.dirname(res_out_dir) | |
| if not os.path.exists(res_directory): | |
| print(f'-------------create {res_out_dir}-----------------') | |
| os.makedirs(res_directory) | |
| iou_type = res_type | |
| cocoEval = COCOeval( | |
| copy.deepcopy(cocoGt), copy.deepcopy(cocoDt), iou_type) | |
| cocoEval.params.imgIds = imgIds | |
| cocoEval.params.iouThrs = [0.75, 0.5, 0.1] | |
| cocoEval.params.maxDets = [100] | |
| if areas: | |
| cocoEval.params.areaRng = [[0**2, areas[2]], [0**2, areas[0]], | |
| [areas[0], areas[1]], | |
| [areas[1], areas[2]]] | |
| cocoEval.evaluate() | |
| cocoEval.accumulate() | |
| ps = cocoEval.eval['precision'] | |
| ps = np.vstack([ps, np.zeros((4, *ps.shape[1:]))]) | |
| catIds = cocoGt.getCatIds() | |
| recThrs = cocoEval.params.recThrs | |
| with Pool(processes=48) as pool: | |
| args = [(k, cocoDt, cocoGt, catId, iou_type, areas) | |
| for k, catId in enumerate(catIds)] | |
| analyze_results = pool.starmap(analyze_individual_category, args) | |
| for k, catId in enumerate(catIds): | |
| nm = cocoGt.loadCats(catId)[0] | |
| print(f'--------------saving {k + 1}-{nm["name"]}---------------') | |
| analyze_result = analyze_results[k] | |
| assert k == analyze_result[0] | |
| ps_supercategory = analyze_result[1]['ps_supercategory'] | |
| ps_allcategory = analyze_result[1]['ps_allcategory'] | |
| # compute precision but ignore superclass confusion | |
| ps[3, :, k, :, :] = ps_supercategory | |
| # compute precision but ignore any class confusion | |
| ps[4, :, k, :, :] = ps_allcategory | |
| # fill in background and false negative errors and plot | |
| ps[ps == -1] = 0 | |
| ps[5, :, k, :, :] = ps[4, :, k, :, :] > 0 | |
| ps[6, :, k, :, :] = 1.0 | |
| makeplot(recThrs, ps[:, :, k], res_out_dir, nm['name'], iou_type) | |
| if extraplots: | |
| makebarplot(recThrs, ps[:, :, k], res_out_dir, nm['name'], | |
| iou_type) | |
| makeplot(recThrs, ps, res_out_dir, 'allclass', iou_type) | |
| if extraplots: | |
| makebarplot(recThrs, ps, res_out_dir, 'allclass', iou_type) | |
| make_gt_area_group_numbers_plot( | |
| cocoEval=cocoEval, outDir=res_out_dir, verbose=True) | |
| make_gt_area_histogram_plot(cocoEval=cocoEval, outDir=res_out_dir) | |
| def main(): | |
| parser = ArgumentParser(description='COCO Error Analysis Tool') | |
| parser.add_argument('result', help='result file (json format) path') | |
| parser.add_argument('out_dir', help='dir to save analyze result images') | |
| parser.add_argument( | |
| '--ann', | |
| default='data/coco/annotations/instances_val2017.json', | |
| help='annotation file path') | |
| parser.add_argument( | |
| '--types', type=str, nargs='+', default=['bbox'], help='result types') | |
| parser.add_argument( | |
| '--extraplots', | |
| action='store_true', | |
| help='export extra bar/stat plots') | |
| parser.add_argument( | |
| '--areas', | |
| type=int, | |
| nargs='+', | |
| default=[1024, 9216, 10000000000], | |
| help='area regions') | |
| args = parser.parse_args() | |
| analyze_results( | |
| args.result, | |
| args.ann, | |
| args.types, | |
| out_dir=args.out_dir, | |
| extraplots=args.extraplots, | |
| areas=args.areas) | |
| if __name__ == '__main__': | |
| main() | |