目录:
- 1.相关概念
- 1)IoU(Intersection over Union)
- 2)Precision和Recall
- 2.mAP(mean Average Precision)
- 1)Pascal VOC
- 2)COCO
- 3.总结mAP的计算流程
- 4.mAP的代码实现
一、相关概念
1.IoU(Intersection over Union)
IoU的定义:预测bbox与实际bbox的交并比。
IoU被用来判断对一个对象的预测是否正确。若IoU > threshold,该预测被认为是TP;否则若IoU <= threshold,该预测被认为是FP。
2.Precision和Recall
为了更好的理解mAP,我们先了解一下Precision和Recall。
Recall(召回率/查全率):是指在所有确实为正的样本中,被预测为正样本的占比。
Precision(精确率/查准率):是指在所有被预测为正的样本中,确实是正样本的占比。
二、mAP(mean Average Precision)
目标检测问题不同于一般的分类问题,不仅要检测出目标,输出目标的类别,还要定位出目标的位置。分类问题中的Accuracy已不能作为目标检测算法的评估指标,而mAP是目标检测算法中最常用的评估指标。
1)AP(Average Precision)是PR曲线围成的面积,用来衡量对一个类检测的好坏。
2)mAP(mean Average Precision)是所有类AP的平均值,衡量多类别目标检测的好坏。
不同的数据集/竞赛可能有不同的评估指标。最常用的是PASCAL VOC和MS COCO中的评价指标。
Pascal VOC
为了计算mAP,首先需要计算每个类的AP。
步骤一:绘制PR曲线
以下面的两张图片为例,包含某一个指定类别的GT框(绿色)和预测框(红色)。
对于每张图片中的每个预测框,计算并选择与预测框的IoU最大那个GT框,统计成表格如下:
VOC中的IoU阈值为0.5,所以IoU > 0.5被视为TP,否则为FP。现在,我们根据置信度confidence从高到低进行排序。值得注意的是,若多个预测框对应同一个GT,则置信度最高的那个视为TP,其他的视为FP。如下表中P3和P4都对应GB,P4(置信度最高)被视为TP,P3被视为FP。
在VOC指标中,第k行(rank)的Recall和Precision的计算,是包含当前行及以上所有行的预测数据。首先,累计计算每一行的TP和FP;然后,根据公式计算每k行的Precision和Recall(Precision等于TP除以当前预测框总数,Recall等于TP除以所有GT框数量)。以表中第二行(rank=2)为例,TP累计为1,当前预测框总数为2,所有GT框数量为3,有Precision=1/2=0.5,Recall=1/3=0.33。
计算完每一行的Precision和Recall之后,以Precision为横坐标,Recall为纵坐标,即可得到PR曲线。
步骤二:平滑PR曲线
每个查全率级别r的Precision,通过取查全率>=r的所有Precision的最大值来进行插值替换。即保证低查全率的精度不小于更高查全率的精度。
步骤三:计算AP
VOC 2007是取11个Recall点[0,0.1,...,1]的Precision的平均值作为AP值。
VOC 2012是取所有Recall点的Precision的平均值作为AP值,即PR曲线下的面积(AUC)。
计算完每个类的AP值之后,求平均即为mAP。
COCO
在VOC中,IoU的阈值固定为0.5,这意味着,IoU分别为0.6、0.9的两个预测被认为是等价的,显然这是有偏差的。COCO中通过指定一个阈值范围[.5:.05:.95]来解决这个问题,它计算这个范围中每个阈值的mAP,然后求平均得到最终的mAP。
另外,COCO中使用101点法(Recall范围[0:.01:1] )来计算AP。
注意:在COCO中mAP也可简写为AP。
COCO中AP的计算步骤(并非唯一):
1)对于每个类,计算该类在不同IoU阈值的AP并取平均。
2)对所有类的AP取平均,得到最终的AP。
可见,COCO中的AP实际是“平均平均平均精度”。
三、总结mAP的计算流程
mAP的计算流程:
- 1.首先,指定一个较低的confidence阈值(通常是0.001、0.01),来筛选网络的预测框。
- 选用低阈值是为了尽可能保留较多的框。由于不同的模型之间合理阈值是不一样的,测试mAP需要屏蔽这个不同,以实现统一标准。
- 2.对筛选后的预测框,进行nms(使用类内nms)非极大值抑制,去除高度重叠的框。
- nms的IoU阈值一般取0.5,你选择0.6、0.7也是可以的,这个影响不大。
- 3.根据经过上面处理后的预测框和真值GT框,来计算mAP:
- 3.1)先计算每个类的、指定IoU阈值的AP(以计算AP75为例,iou_threshold=0.75)。
- 3.1.1)为每个类构建一个matched_table表。
- 行数等于所有的预测框数量,列数为[confidence, matched_iou, matched_groundtruth_index, image_id]。
- image_id为预测框对应的图片ID,confidence为预测框的置信度;
- matched_GT_index为与该预测框的IoU最大的那个GT框索引(同一张图片中的预测框和GT框之间计算IoU);
- max_matched_iou为最大的那个IoU值(用来与iou_threshold做对比,判断是TP还是FP)。
- 按置信度confidence从高到低对matched_table表进行排序。
- 行数等于所有的预测框数量,列数为[confidence, matched_iou, matched_groundtruth_index, image_id]。
- 3.1.2)判断每个预测框是属于TP还是FP。
- 当matched_iou <= iou_threshold时,都视为FP(mAP@[IoU=0.5]的iou_threshold=0.5)。
- 当matched_iou > iou_threshold时,即预测框匹配某个GT时:
- 如果该GT第一次被匹配,则当前预测框(置信度最高)被视为TP,否则被视为FP。
- (注意:一个GT框最多只能对应一个预测框,出现多个预测框匹配同一个GT的情况时,将置信度最高的那个视为TP。)
- 3.1.3)累计每行(rank)总TP数,并计算每行的Precision和Recall。
- 每一行的Recall和Precision的计算公式:
- TP是指当前行及以上所有TP总数,是当前行及以上所有预测框数量,是所有的GT框数量。
- 每一行的Recall和Precision的计算公式:
- 3.1.4)计算完每行的Precision和Recall后,将其绘制成PR曲线,即可计算AP。
- 首先,对PR曲线进行平滑处理。
- 使得低Recall的Precision不低于比它更高的Recall的Precision。
- 然后,计算Recall对应的平均精度(AP)。有几种计算方式:
- VOC 2007:11点法,即取Recall[0:0.1:1]的11个点的平均Precision作为AP。
- VOC 2012:取所有点的平均Precision作为AP,即PR曲线下的面积。
- COCO:101点法,Recall[0:0.01:1]的101点的平均就Precision作为AP。
- 首先,对PR曲线进行平滑处理。
- 3.1.1)为每个类构建一个matched_table表。
- 3.2)对所有类别的AP值求平均,即得到mAP。
- 可通过调整IoU阈值,分别得到AP50、AP75和AP@[IoU=0.5:0.95]。
- 3.1)先计算每个类的、指定IoU阈值的AP(以计算AP75为例,iou_threshold=0.75)。
四、mAP的代码实现
手动实现计算mAP的代码,并与调用pycocotools
库计算mAP做对比,两者结果一致。
1.手动实现计算mAP的代码
实现代码:
# 计算IoU(多对多)
def ious(a, b):
'''
a : 4 x M x 1 left, top, right, bottom
b : 4 x 1 x N left, top, right, bottom
'''
aleft, atop, aright, abottom = [a[i] for i in range(4)]
bleft, btop, bright, bbottom = [b[i] for i in range(4)]
# aleft.shape = M, 1
# bleft.shape = 1, N
cross_left = np.maximum(aleft, bleft) # M x N
cross_top = np.maximum(atop, btop) # M x N
cross_right = np.minimum(aright, bright) # M x N
cross_bottom = np.minimum(abottom, bbottom) # M x N
# cross_area.shape = M x N
cross_area = (cross_right - cross_left + 1).clip(0) * (cross_bottom - cross_top + 1).clip(0)
# union_area.shape = M x N
union_area = (aright - aleft + 1) * (abottom - atop + 1) + (bright - bleft + 1) * (bbottom - btop + 1) - cross_area
# M x N
return cross_area / union_area
# 构建指定类的matched_table
def build_matched_table(classes_index, groundtruths, detections, maxDets=100):
'''
classes_index: 需要构建matched_table的类索引
groundtruths: GT框,形如{"image_id": [[xmin, ymin, xmax, ymax, 0, class_index], ...], ...}
detections: 预测框形如{"image_id": [[xmin, ymin, xmax, ymax, confidence, class_index], ...], ...}
maxDets: 每张图片的最大预测框数量,默认为100
'''
matched_table = [] # 构建的matched_table表
sum_groundtruths = 0 # 统计GT框的数量
# 遍历每张图片
for image_id in groundtruths:
# 选择"当前类"的预测框和GT框,并转换为numpy类型
# [x1,y1,x2,y2,conf,class_index]
select_detections = np.array(list(filter(lambda x: x[5] == classes_index, detections[image_id])))
select_groundtruths = np.array(list(filter(lambda x: x[5] == classes_index, groundtruths[image_id])))
num_detections = len(select_detections)
num_groundtruths = len(select_groundtruths)
# 有用的预测框
num_use_detections = min(num_detections, maxDets)
# 统计GT框数量
sum_groundtruths += num_groundtruths
# 当前图片的预测框数量为0,直接返回
if num_detections == 0:
continue
# 当图片的GT框数量为0时,选择不超过数量上限的预测框(任意选择,不影响,都是FP),matched_iou置为0
if len(select_groundtruths) == 0:
for detection_index in range(num_use_detections):
confidence = select_detections[detection_index, 4]
matched_table.append([confidence, 0, -1, image_id])
continue
# reshape,以便可以广播,同时计算多个iou
sgt = select_groundtruths.T.reshape(6, -1, 1)
sdt = select_detections.T.reshape(6, 1, -1)
# 计算所有GT与所有预测框的IoU
groundtruth_detection_ious = ious(sgt, sdt)
# 构建matched_table表
for detection_index in range(num_use_detections):
confidence = select_detections[detection_index, 4]
matched_groundtruth_index = groundtruth_detection_ious[:, detection_index].argmax()
matched_iou = groundtruth_detection_ious[matched_groundtruth_index, detection_index]
matched_table.append([confidence, matched_iou, matched_groundtruth_index, image_id])
# 按置信度confidence从高到低进行排序
matched_table = sorted(matched_table, key=lambda x: x[0], reverse=True)
return matched_table, sum_groundtruths
# 计算单个类的、指定iou_threshold的AP
def compute_AP(matched_table, iou_threshold, sum_groundtruths):
'''
matched_table: 形如[[confidence, matched_iou, matched_groundtruth_index, image_id], ...]
'''
# 1.判断每个预测框属于TP还是FP。
num_detections = len(matched_table) # 预测框总数量
true_positive = np.zeros((num_detections,)) # 每一个预测框的TP/FP表示(0为FP,1为TP)
# 构建一个groundtruth_seen_map字典,标记某个GT是否已经被预测。
# item[3]是image_id,以image_id为key,value初始为一个空的set()集合。
groundtruth_seen_map = {item[3]:set() for item in matched_table}
# 注意:matched_table是按置信度从大到小进行排序后的。
# 从上到下遍历每个预测框,判断属于TP还是FP:
# 1)当matched_iou <= iou_threshold时,都视为FP。
# 2)当matched_iou > iou_threshold时,即预测框匹配某个GT时:
# 2.1)如果该GT第一次被匹配(即不在image_id对应的set中),则将GT添加到set中,且当前预测框视为TP。
# 2.2)如果该GT已经被预测了(即已经在image_id对应的set中了),则将当前预测视为FP。
for index in range(num_detections):
# [confidence, matched_iou, matched_groundtruth_index, image_id]
confidence, matched_iou, matched_groundtruth_index, image_id = matched_table[index]
# 只有满足matched_iou > iou_threshold且是第一次匹配某个GT时,才认为是TP
image_seen_map = groundtruth_seen_map[image_id] # 获取指定图片的seen_map
if matched_iou > iou_threshold and matched_groundtruth_index not in image_seen_map:
true_positive[index] = 1 # 判断为TP
image_seen_map.add(matched_groundtruth_index) # 添加当前GT到seen_map中
# 2.累加每行的TP,并计算Precision和Recall。
TP_count = np.cumsum(true_positive) # 累计每行的TP
detection_count = np.arange(1, num_detections + 1) # 累计每行的预测框总数
precision = TP_count / detection_count # 计算Precision
recall = TP_count / sum_groundtruths # 计算Recall
# 3.平滑PR曲线
mrec = np.concatenate(([0.], recall, [min(recall[-1] + 1E-3, 1.)])) # 首尾添加两个点
mpre = np.concatenate(([0.], precision, [0.])) # 首尾添加两个点
# 使得低Recall的Precision不低于比它更高的Recall的Precision。
mpre = np.flip(np.maximum.accumulate(np.flip(mpre)))
# 4.计算AP:插值计算101点的平均精度(COCO的计算方法)
AP = np.mean(np.interp(np.linspace(0, 1, 101), mrec, mpre))
return AP
# 计算所有类的mAP
def compute_mAP(groundtruths, detections, classes, maxDets=100):
'''
groundtruths: 形如{"image_id": [[xmin, ymin, xmax, ymax, 0, class_index], [xmin, ymin, xmax, ymax, 0, class_index]], ...}
detections: 形如{"image_id": [[xmin, ymin, xmax, ymax, confidence, class_index], [xmin, ymin, xmax, ymax, confidence, class_index]], ...}
classes: 所有类别,形如["aeroplane", "bicycle", "bird", "boat", "bottle", ...]
maxDets: 每张图片的最大预测框数量,默认为100
'''
APs = []
# 遍历每个类,计算每个类的[AP@[IoU=0.5], AP@[IoU=0.75], AP@[IoU=0.5:0.95]]
for classes_index in range(len(classes)):
# 1.构建指定类的matched_table
matched_table, sum_groundtruths = build_matched_table(classes_index, groundtruths, detections, maxDets)
# 2.根据matched_table计算AP
AP50 = compute_AP(matched_table, 0.5, sum_groundtruths)
AP75 = compute_AP(matched_table, 0.75, sum_groundtruths)
AP = np.mean([compute_AP(matched_table, iou_threshold, sum_groundtruths) for iou_threshold in np.arange(0.5, 1.0, 0.05)])
APs.append([AP, AP50, AP75])
# 计算mAP(所有类的AP的平均值)
return np.mean(APs, axis=0)
预测框detections是经过类内nms处理后的,预测框和GT框的格式形如:
{"image_id": [[xmin, ymin, xmax, ymax, 0, class_index], ...], ...}
计算mAP结果如下:
2.调用pycocotools
库计算mAP
安装pycocotools
命令:pip install pycocotools
from pycocotools.coco import COCO
from pycocotools.cocoeval import COCOeval
def mapCOCO(groundtruth_annotation, detection_annotation, classes):
images = []
annotations = []
categories = []
ann_id = 0
for class_index, class_name in enumerate(classes):
categories.append({"supercategory": class_name, "id": class_index, "name": class_name})
for item in groundtruth_annotation:
filename = item
anns = groundtruth_annotation[item]
image_id = int(filename)
images.append({"id": image_id})
for left, top, right, bottom, score, class_index in anns:
ann_id += 1
width, height = right - left + 1, bottom - top + 1
annotations.append({"image_id": image_id, "id": ann_id, "category_id": class_index, "bbox": [left, top, width, height], "iscrowd": 0, "area": width * height})
gt_coco = {"images": images, "annotations": annotations, "categories": categories}
with open("gt_coco.json", "w") as f:
json.dump(gt_coco, f)
cocoGt = COCO("gt_coco.json")
ann_dets = []
for item in detection_annotation:
anns = detection_annotation[item]
image_id = int(item)
for left, top, right, bottom, score, classes in anns:
# {"image_id":1,"category_id":2,"bbox":[199.84, 190.46, 77.71, 70.88],"score":0.236},
width = right - left + 1
height = bottom - top + 1
object_item = {"image_id": image_id, "category_id": classes, "score": score, "bbox": [left, top, width, height]}
ann_dets.append(object_item)
cocoDt = cocoGt.loadRes(ann_dets)
cocoEval = COCOeval(cocoGt, cocoDt, "bbox")
cocoEval.evaluate()
cocoEval.accumulate()
cocoEval.summarize()
计算mAP结果如下: