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
需要通过其他方式获得,假设我们知道狗的 catId
为 18
,我们就可以通过下面代码得到所有有狗的图片的 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
号,因为 coco
有 80
类,所以如果不传入其他参数的话返回的就是一个拥有 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)
获取某图像对应的 annotation
的 id
号,即 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
进行map
,recall
计算
#-*-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) """