马春杰杰 Exit Reader Mode

修改pycocotools以输出每一类的AP值

pycocotools的使用非常广泛,但是貌似并不支持输出每一类的AP值,尤其是AP50,但是在很多数据集上每一类的指标是必须的,所以修改了一些现有的pycocotools,方便使用。

用法:

在所有的安装流程结束,并且你的算法已经测试可以用之后,卸载自动安装的pycocotools,重新安装:

pip uninstall pycocotools
git clone https://github.com/ma3252788/mcjpycocotools.git
# Gitee备份
git clone https://gitee.com/mcj686/mcjpycocotools.git
cd mcjpycocotools
pip install .

可以完全替代原有pycocotools,其他功能都没动,只是增加了类别的显示。

结果大概长这个样:

IoU metric: bbox
 Average Precision  (AP) @[ IoU=0.50:0.95 | area=   all | maxDets=100 ] = 0.136
 Average Precision  (AP) @[ IoU=0.50      | area=   all | maxDets=100 ] = 0.355
 Average Precision  (AP) @[ IoU=0.75      | area=   all | maxDets=100 ] = 0.065
 Average Precision  (AP) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = -1.000
 Average Precision  (AP) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.125
 Average Precision  (AP) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.214
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=   all | maxDets=  1 ] = 0.263
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=   all | maxDets= 10 ] = 0.357
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=   all | maxDets=100 ] = 0.443
 Average Recall     (AR) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = -1.000
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.386
 Average Recall     (AR) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.439
Category 0's AP50 - C1     : 0.07680768076807681
Category 1's AP50 - C2     : 0.48648723419809553
Category 2's AP50 - C3     : 0.3595831820151127
Category 3's AP50 - C4     : 0.47395630972138675
Category 4's AP50 - C5     : 0.37917377944691016
(all categories) mAP : 0.3552016372299164

目前还没有做美化的事,欢迎提PR

一些有用的COCOAPI:

首先需要引入这个包,然后创建一个 coco 对象,创建时要将 annotation 的路径传进去

from pycocotools.coco import COCO
coco = COCO('/NAS_REMOTE/PUBLIC/data/coco2017/annotations/instances_train2017.json')

如果直接运行的话就会输出以下 log

loading annotations into memory...
Done (t=15.79s)
creating index...
index created!

创建好了 coco 对象后就可以使用他的一些 API 了,下面列举一些常用的,后续有用到其他的话也会继续更新

getImgIds

coco.getImgIds(imgIds=[], catIds=[]) 可以获取到 coco 所有图片对应的 id 号,以便后续处理,另外,传入 catIds 参数的话也可以只返回特定的类别的图片对应的 id 号,但是 catIds 需要通过其他方式获得,假设我们知道狗的 catId18,我们就可以通过下面代码得到所有有狗的图片的 id

coco.getImgIds(catIds=18)

[98304, 204800, 524291, 311301, 491525, 147471, 131087, 278550, 581654, 253981, 450590, 106525, 368676, 253988, …]

getCatIds

getCatIds(catNms=[], supNms=[], catIds=[]) 可以获取到 coco 类别对应的 label 号,因为 coco80 类,所以如果不传入其他参数的话返回的就是一个拥有 80 个元素的列表(不是连续的数字,中间会跳过几个数字

coco.getCatIds()

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, …]

当然也可以传入类别名字,筛选出特定类别代表的 label ,如下说明 dog标签18

coco.getCatIds(catNms='dog')

[18]

getAnnIds

getAnnIds(imgIds=[], catIds=[], areaRng=[], iscrowd=None) 获取某图像对应的 annotationid 号,即 groundtruth,以下说明了该图有 4 个 标注

coco.getAnnIds(imgIds=coco.getImgIds()[0])

[151091, 202758, 1260346, 1766676]

loadImgs

loadImgs(ids=[]) 用的时候会在里面加上图片对应的 id 号返回某张图片的信息

coco.loadImgs(ids=coco.getImgIds()[0])

[{‘coco_url’: ‘http://images.cocoda…391895.jpg’, ‘date_captured’: ‘2013-11-14 11:18:45’, ‘file_name’: ‘000000391895.jpg’, ‘flickr_url’: ‘http://farm9.staticf…8349_z.jpg’, ‘height’: 360, ‘id’: 391895, ‘license’: 3, ‘width’: 640}]

loadCats

loadCats(ids=[]) 用法和上面差不多,是通过类别的 label 来返回className

coco.loadCats(ids=18)

[{‘id’: 18, ‘name’: ‘dog’, ‘supercategory’: ‘animal’}]

loadAnns

loadAnns(ids=[]) 用来加载特定 annotation id 号的标注

coco.loadAnns(ids=202758)

[{‘area’: 14107.271300000002, ‘bbox’: […], ‘category_id’: 1, ‘id’: 202758, ‘image_id’: 391895, ‘iscrowd’: 0, ‘segmentation’: […]}]

showAnns

showAnns(anns, draw_bbox=False) 同上,如果获取到了 Anno 信息之后,可以用这个函数直接将 Anno 信息给可视化出来,挺有意思的

dataset_dir = '/NAS_REMOTE/PUBLIC/data/coco2017/train2017/'
coco = COCO(os.path.join('/NAS_REMOTE/PUBLIC/data/coco2017', 'annotations', 'instances_' + 'train2017' + '.json'))
image_ids = coco.getImgIds()
img = coco.loadImgs(image_ids[0])[0]
I = io.imread(dataset_dir + img['file_name'])
plt.axis('off')
plt.imshow(I)
annIds = coco.getAnnIds(imgIds=image_ids[0])
annos = coco.loadAnns(ids=annIds)
coco.showAnns(annos)
plt.show()

还可以在 showAnns 中将 draw_bbox 参数变成 True 来画出 bbox

其他

类别统计,检测coco类型的数据集

import pycocotools.coco as COCO

def check_coco_json(annot_path):
    coco = COCO.COCO(annot_path)
    cats = coco.loadCats(coco.getCatIds())
    cat_nms=[cat['name'] for cat in cats]
    print('-' * 40)
    print('COCO categories number: {}'.format(len(cats)))
    print('COCO categories: {}'.format(' |'.join(cat_nms)))

    # 统计各类的图片数量和标注框数量
    catID = coco.getCatIds() # 顺序很重要
    imgID = coco.getImgIds()
    annID = coco.getAnnIds()
    for i,cid in enumerate(catID):
        if i+1 != cid:
            catID_e = True
            print('\nWaring: catgory id is not right cid:{} != {}'.format(cid,i))
    if len(set(imgID)) != len(imgID):
        print('Error: pic num is not equal to pic id numbers')
    else:
        print('\nAll img number:',len(imgID))
    if len(set(annID)) != len(annID):
        annID_e = True
        print('Error: annID repeat!')
    else:
        print('All ann label number:',len(annID))
    print('-'*40)
    print("{:<15} {:<6}  {:<10}  {:<8}".format('Catetories', 'image_num', 'target_num','class_id'))
    for cat_name in cat_nms:
        catId = coco.getCatIds(catNms=cat_name)
        if len(catId)>1:
            catId = [catId[-1]]
        imgId = coco.getImgIds(catIds=catId)
        annId = coco.getAnnIds(imgIds=imgId, catIds=catId, iscrowd=None)
        if len(imgId)==0:
            aa = 1
        print("{:<15} {:<6d}     {:<10d} {}".format(cat_name, len(imgId), len(annId),catId))

if __name__ == "__main__":
    annot_path = '/home/kcadmin/user/20200106/aa.json'
    check_coco_json(annot_path)

cocoAPI进行maprecall计算

#-*-coding:utf-8-*-

'''
cocoAIP
1. 获取类别名称对应的id
2. 获取类别(单类)对应的图片id
4. 获取多类对应的图片id
3. 获取图片id对应的图片名称
4. 通过gt的json文件和pred的json文件计算map
5. 通过gt和pred计算每个类别的ap,recall
'''

import pickle, json
import numpy as np
from pycocotools.coco import COCO
from pycocotools.cocoeval import COCOeval
import itertools
from terminaltables import AsciiTable

def read_pickle(pkl):
    with open(pkl,'rb') as f:
        data = pickle.load(f)
    return data

def read_json(json_pth):
    with open(json_pth,'r') as f:
        data = json.load(f)
    return data

det_json = 'data/det.json'
gt_json = 'data/gt.json'
CLASSES = ('A','B','C')
class_num = len(CLASSES)

cocoGt = COCO(gt_json)

# 获取所有图片的id
all_id = cocoGt.getImgIds()

# 获取类别(单类)对应的所有图片id
catIds = cocoGt.getCatIds(catNms=list(CLASSES)) #,'long','meihua'

# 获取多个类别对应的所有图片的id
imgid_list = []
for id_c in catIds:
    imgIds = cocoGt.getImgIds(catIds=id_c)
    imgid_list.extend(imgIds)
imgid_list = list(set(imgid_list))

# 获取图片id对应的图片路径
img_info = cocoGt.load_imgs([imgid_list[0]])[0]
fname = img_info['file_name']


# 通过gt的json文件和pred的json文件计算map
det_json = 'data/det.json'
gt_json = 'data/gt.json'
CLASSES = ('A','B','C')
class_num = len(CLASSES)
cocoGt = COCO(gt_json)
cocoDt = cocoGt.loadRes(det_json)
cocoEval = COCOeval(cocoGt, cocoDt, "bbox")
cocoEval.params.iouThrs = np.linspace(0.5, 0.95, int(np.round((0.95 - .5) / .05)) + 1, endpoint=True)
cocoEval.params.maxDets = list((100, 300, 1000))
cocoEval.evaluate()
cocoEval.accumulate()
cocoEval.summarize()

# 过gt和pred计算每个类别的recall
precisions = cocoEval.eval['precision'] # TP/(TP+FP) right/detection
recalls = cocoEval.eval['recall'] # iou*class_num*Areas*Max_det TP/(TP+FN) right/gt
print('\nIOU:{} MAP:{:.3f} Recall:{:.3f}'.format(cocoEval.params.iouThrs[0],np.mean(precisions[0, :, :, 0, -1]),np.mean(recalls[0, :, 0, -1])))
# Compute per-category AP
# from https://github.com/facebookresearch/detectron2/
# precision: (iou, recall, cls, area range, max dets)
results_per_category = []
results_per_category_iou50 = []
res_item = []
for idx, catId in enumerate(range(class_num)):
    name = CLASSES[idx]
    precision = precisions[:, :, idx, 0, -1]
    precision_50 = precisions[0, :, idx, 0, -1]
    precision = precision[precision > -1]

    recall = recalls[ :, idx, 0, -1]
    recall_50 = recalls[0, idx, 0, -1]
    recall = recall[recall > -1]

    if precision.size:
        ap = np.mean(precision)
        ap_50 = np.mean(precision_50)
        rec = np.mean(recall)
        rec_50 = np.mean(recall_50)
    else:
        ap = float('nan')
        ap_50 = float('nan')
        rec = float('nan')
        rec_50 = float('nan')
    res_item = [f'{name}', f'{float(ap):0.3f}',f'{float(rec):0.3f}']
    results_per_category.append(res_item)
    res_item_50 = [f'{name}', f'{float(ap_50):0.3f}', f'{float(rec_50):0.3f}']
    results_per_category_iou50.append(res_item_50)

item_num = len(res_item)
num_columns = min(6, len(results_per_category) * item_num)
results_flatten = list(
    itertools.chain(*results_per_category))
headers = ['category', 'AP', 'Recall'] * (num_columns // item_num)
results_2d = itertools.zip_longest(*[
    results_flatten[i::num_columns]
    for i in range(num_columns)
])
table_data = [headers]
table_data += [result for result in results_2d]
table = AsciiTable(table_data)
print('\n' + table.table)

num_columns_50 = min(6, len(results_per_category_iou50) * item_num)
results_flatten_50 = list(
    itertools.chain(*results_per_category_iou50))
iou_ = cocoEval.params.iouThrs[0]
headers_50 = ['category', 'AP{}'.format(iou_),'Recall{}'.format(iou_)] * (num_columns_50 // item_num)
results_2d_50 = itertools.zip_longest(*[
    results_flatten_50[i::num_columns_50]
    for i in range(num_columns_50)
])

table_data_50 = [headers_50]
table_data_50 += [result for result in results_2d_50]
table_50 = AsciiTable(table_data_50)
print('\n' + table_50.table)

 

COCO内部实现的方法
1.getAnnIds(self, imgIds=[], catIds=[], areaRng=[], iscrowd=None)
    参数:
        imgIds->指定image_id范围     [2,60,0]
        catIds->指定category_id范围  [1,2,56]
        areaRng-> 指定area范围  [min,max]
        iscrowd-> 指定iscrowd值 
    返回:
        满足以上条件的id,如果4个参数都没有指定,则返回所有id
    
2.getCatIds(self, catNms=[], supNms=[], catIds=[]):
    参数:
        catNms->指定 子类别 范围     ['tv','car','people']
        supNms->指定 父类别 范围     ['vehicle','animal','person']
        catIds->指定category_id范围  [1,2,56]
    返回:
        满足以上条件的category_id,如果3个参数都没有指定,则返回所有category_id
    
3.getImgIds(self, imgIds=[], catIds=[]):
    参数:
        imgIds->指定image_id范围     [0,85,33]
        catIds->指定category_id范围  [89,2,11]
    返回:
        满足以上条件的image_id,如果2个参数都没有指定,则返回所有image_id
        
4.loadAnns(self, ids=[]):
    参数:
        ids->指定id范围     [1221111,85,8742312] 或 2
    返回:
        满足以上条件的ann_info -> [{},{},{}] 或[{}]  注: {}仅代表一个dict型信息,并非为空dict,下同
        
5.loadCats(self, ids=[]):
    参数:
        ids->指定category_id范围     [2,89,19] 或 2
    返回:
        满足以上条件的category_info -> [{},{},{}] 或[{}]

6.loadImgs(self, ids=[]):
    参数:
        ids->指定image_id范围     [333,89,19] 或 2
    返回:
        满足以上条件的image_info -> [{},{},{}] 或[{}]

7.showAnns(self, anns, draw_bbox=False):
    参数:
        anns->指定anns内容     [{},{},{}] 或[{}]
        draw_bbox->是否绘制矩形框
    返回:
        无
    使用方法: 1.plt.imshow(img) 2.anns = loadAnns(annIds)  3.coco.showAnns(anns)

8.loadRes(self, resFile):
    参数:
        resFile->指定json文件 假设现有数据集为A,resFile
    返回:
        COCO初始化了的resFile
    执行流程
        0.res=COCO(),且将A.dataset中的images字段复制到res.dataset中
        1.如果resFile是路径的话则json加载 B = json.load(f)
        2.如果是numpy型数据的话则调用loadNumpyAnnotations加载 B = self.loadNumpyAnnotations(resFile)
        3.如果都不是以上两种情况,则直接赋值 B = resFile
        4.但B必须是list对象 -> [{ann_1},{ann_2},...]
        5.A,B中的数据必须满足这样的关系,A∪B=A或者 A∩B=B,否则报错.其实如果不出意外A=B(imgIds维度)
        6.如果ann_1中有caption字段(看图说话)
            6-0.for id, ann in enumerate(B) 循环B
            6-1.获取A∩B的imgIds,由5可知,为B的imgIds
            6-2.更新res.dataset中的images字段,过滤掉不存在于B中的imgIds
            6-3.重置或添加ann中的id值,∈[1,len(B)]
        7.如果ann_1中有bbox字段(检测),将A.dataset中的categories字段复制(深)给res.dataset
            7-0.for id, ann in enumerate(B) 循环B
            7-2.如果'segmentation'不在ann中,那么ann['segmentation'] = [[x1, y1, x1, y2, x2, y2, x2, y1]]
            7-3.添加ann中的几个字段 area, id, iscrowd, 分别为 w*h id+1 0,以使其满足COCO数据的格式要求
        8.如果ann_1中有segmentation字段(分割),将A.dataset中的categories字段复制(深)给res.dataset
            8-0.for id, ann in enumerate(B) 循环B
            8-1.根据像素计算出mask区域的面积
            8-2.如果'bbox'不在ann中,那么计算出mask区域的最小外接矩形,并将其赋予'bbox'
            8-3.添加ann中的几个字段 id, iscrowd, 分别为 id+1 0,以使其满足COCO数据的格式要求
        9.如果ann_1中有keypoints字段(关键点),将A.dataset中的categories字段复制(深)给res.dataset
            9-0.for id, ann in enumerate(B) 循环B
            9-1.s = ann['keypoints']
            9-2.x,y = s[0::3],s[1::3]
            9-3.获取其最小外接矩形,area, id, bbox字段同上.
        10.将更新后了的B赋予res.dataset中的'annotations'字段.此时就集齐了images,categories,annotations三个字段
        11.对res初始化.

9.download(self, tarDir = None, imgIds = [] ):
    参数:
        tarDir:保存文件夹
        imgIds:指定所要下载的image_id [22112,12341] 或 21231344
    返回:
        无

10.loadNumpyAnnotations(self, data):
    参数:
        data->[N,7] numpy array  7->[imageID,x1,y1,w,h,score,class]
    返回:
        ann (python嵌套列表) [{'image_id' : imageID,
                              'bbox'  : [x1,y1,w,h],
                              'score' : score,
                              'category_id': int(class)},*N]

11.annToRLE(self, ann):
    参数:
        ann:标注信息{}
    返回:
        binary mask (numpy 2D array)
"""

 

【COCO数据集】COCO API入门 超详细注解

pycocotools一些api的用法记录

COCO数据集深入理解

本文最后更新于2023年1月13日,已超过 1 年没有更新,如果文章内容或图片资源失效,请留言反馈,我们会及时处理,谢谢!