A Survey on Performance Metrics for Object-Detection Algorithms
目标检测数据集COCO的MAP评价指标工具pycocotools用于VOC或者自定义类别数据集的MAP计算以及类别的AP结果展现(代码详解)
目录
计算precision和recall以及绘制precision-recall图
讲解缘由
计算机视觉中的每一个子领域都有自己的评价指标,比如图像分类的评价指标主要以一个训练好的模型在给定的测试集上top-1和top-5的准确率为主,特别是基于ImageNet大规模数据集训练的模型;比如语义分割中主要是以平均交并比MIOU来评价分割的效果;比如人群统计领域主要还是以MAE(平均绝对误差)和MSE(均方误差)作为评价指标。上面给出的指标只是在给定的测试集上对模型进行评估,如果要评估模型的泛化性等之类的,可能还需要其他的评价指标,因此这些评价指标并不是绝对的。
同样,最热门的领域目标检测也有自己的评价指标,由于目标检测的目的在于检测出图像或者视频中指定物体的位置,类别以及置信度,也就是要预测给定物体的位置以及高宽,那怎么去衡量模型检测物体位置是否准确呢?其中采用AP(平均精确率)作为模型在每一个类别上的检测效果,如果要计算所有类别的平均检测效果,那么将所有类别检测精度求和比上所有类别数=MAP。
由于目标检测很早就开始研究,因此,关于其评价指标的讲解不管是视频还是博客都有人已经讲解了,所以MAP这个问题并不是什么新鲜或者很难的问题,而是这里除了讲解原理步骤之外,同时也结合代码进行视频的讲解,因为原理上个人感觉有些地方本来就有点绕,如果直接看代码的话更绕,这也是决定在这里重新阐述的原因。
前置知识点
精确率(查准率)和召回率(查全率)
精确率:指被模型正确分类为正类的样本数与模型预测为正类的样本总数之比。它衡量的是模型在预测为正时的准确性。
召回率:指被模型正确分类为正类的样本数与实际正类样本总数之比。它衡量的是模型对正类样本的检测能力。
事实上,精确率和召回率之间一般是不可兼得的,一般精确率越高,召回率越低,同理反过来也是一样的道理。
- 如果调整模型的阈值,使其更严格地预测正类(如提高对正类的预测),那么:
- 精确率 可能会上升,因为只有在模型高度确信时才会预测为正类,从而减少假正例(FP)。
- 召回率 可能会下降,因为更多的实际正类样本可能未被预测正确,导致假负例(FN)增加。
-
反之,若放宽对正类的判断标准(如将阈值降低),则会:
- 召回率 上升,因为更多的实际正类样本被识别出来。
- 精确率 下降,因为新增的假正例增多。
就好比上面我们讲到的top-1和top-5的评价指标一样,top-5相比于top-1放宽了限制条件,也就是阈值降低了。
注:Top-1 精度是指模型预测图像的结果中第一位预测的类别是否与真实类别相符的准确率。
Top-5 精度是指模型预测的前五个候选类别中是否包含实际的真值类别的准确率。
交并比IOU
进入正题
A Survey on Performance Metrics for Object-Detection Algorithms
从 2010 年开始,PASCAL VOC 挑战赛计算 AP 的方法发生了变化。目前PASCAL VOC 挑战赛执行的插值使用所有数据点,而不是像他们的论文中所说的那样只插值 11 个等距点。由于上面给出的github代码默认实现,默认代码(如进一步看到的)遵循他们最新的应用程序(插值所有数据点)。但是也提供了 11 点插值方法。
提出目的
AP 还有六种附加变体,增加了基准测试的可能性,在不同的工作和 AP 实现中缺乏共识是学术和科学界面临的问题。用不同计算语言和平台编写的指标实现通常与共享特定边界框描述的数据集一起分发。这样的项目确实帮助了社区进行评估,但需要额外的工作以适应其他数据集和边界框格式。
本研究探索并比较了用于对象检测算法性能评估的众多指标。平均精度(AP)是一种流行的度量,它通过估计精准度与召回率关系曲线下的面积(AUC)来评估对象检测器的准确性。根据绘图中使用的点插值,可以定义出两种不同的 AP 变体,因此生成不同的结果。本研究回顾了用于对象检测的最常用度量,剖析了它们的差异、应用和主要概念。它还提出了一种标准实现,可以作为不同数据集之间的基准,最小限度地适应注释文件。
11 点插值:
假设如下检测结果
根据IOU阈值得到TP和FP结果
先了解几个定义:
精确度与召回率的曲线可以视为针对不同置信度值生成的边界框之间的权衡。如果检测器的置信度使得其假正例(FP)较低,则精确度较高。然而,在这种情况下,许多正例可能被遗漏,从而导致假负例(FN)较高,进而召回率较低。相反,如果接受更多的正例,召回率将增加,但假正例也会增加,从而降低精确度。然而,一个好的对象检测器应该在识别相关对象时找到所有真实边界框(即 FN = 0,召回率高),同时确保只识别相关对象(即 FP = 0,精确度高)。因此,如果一个目标检测器在召回方面保持高水平的同时,其精确度也保持高水平,那么它可以被认为是好的。这意味着,如果置信度阈值变化,精确度和召回率仍然会保持高水平。因此,高曲线下面积(AUC)往往表示高精确度和高召回率。但是,在实际情况下,精确度与召回率的图通常呈锯齿状曲线,给 AUC 的准确测量带来了挑战。为了去除在 AUC 估计之前的锯齿行为,通常会处理精确度与召回率曲线。
计算precision和recall以及绘制precision-recall图
方法一:11点插值的计算结果
计算所有点执行的插值结果
目标检测挑战及其 AP 变体
评估结果对比
注:表 II 中的指标 AP50 的计算方式与表 III 中的 mAP 指标相同,但由于方法在不同数据集上进行训练和测试,因此在两个评估中得出的结果不同。由于不同数据集之间边界框标注的转换需求,研究人员通常不会使用所有可能的度量来评估所有方法。实际上,如果在一个数据集(例如 PASCAL VOC)上进行训练和测试的方法也可以进行评估,可以使用其他数据集中采用的度量(例如 COCO)进行评估,那将更有意义
附加
AP的计算
改进的AP计算方式
算法代码使用
Argument | Description | Example | Default |
-h,--help | 显示帮助信息 | python pascalvoc.py -h | |
-v,--version | 检查版本 | python pascalvoc.py -v | |
-gt,--gtfolder | 包含gt box的文件夹 | python pascalvoc.py -gt /home/whatever/my_groundtruths/ | /Object-Detection-Metrics/groundtruths |
-det,--detfolder | 包含模型检测的box文件夹 | python pascalvoc.py -det /home/whatever/my_detections/ | /Object-Detection-Metrics/detections/ |
-t,--threshold | 设置的IOU阈值(TP/FP) | python pascalvoc.py -t 0.75 | 0.50 |
-gtformat | Gt box的坐标样式 | python pascalvoc.py -gtformat xyrb | xywh |
-detformat | 模型输出box的坐标样式 | python pascalvoc.py -detformat xyrb | xywh |
-gtcoords | 参考ground的真实边界框坐标。如果注释的坐标是相对于图像大小的(如在YOLO中使用的),则将其设置为rel。如果坐标是绝对值,则不依赖于图像大小,则将其设置为abs | python pascalvoc.py -gtcoords rel | abs |
-detcoords | 同上 | python pascalvoc.py -detcoords rel | abs |
-imgsize | 图像大小 | python pascalvoc.py -imgsize 600,400 | |
-sp,--savepath | 保存计算结果路径 | python pascalvoc.py -sp /home/whatever/my_results/ | Object-Detection-Metrics/results/ |
-np,--noplot | 是否展示绘制的图像结果 | python pascalvoc.py -np | not presented. |
第一步:创建ground truth文件
- 为文件夹
groundtruths/
中的每个图像创建一个单独的真实标签文本文件。- 在这些文件中,每行应遵循以下格式:
<class_name> <left> <top> <right> <bottom>
。- 例如,图像 "2008_000034.jpg" 的真实边界框在文件 "2008_000034.txt" 中表示:
bottle 6 234 45 362
person 1 156 103 336
person 36 111 198 416
person 91 42 338 500
也可以将边界框转换为以下格式:
<class_name> <left> <top> <width> <height>
(。在这种情况下, "2008_000034.txt" 将表示为:
bottle 6 234 39 128
person 1 156 102 180
person 36 111 162 305
person 91 42 247 458
- 为文件夹
detections/
中的每个图像创建一个单独的检测文本文件。- 检测文件的名称必须与其对应的真实标签匹配(例如,"detections/2008_000182.txt" 代表真实标签 "groundtruths/2008_000182.txt")。
- 在这些文件中,每行应遵循以下格式:
<class_name> <confidence> <left> <top> <right> <bottom>
。- 例如:"2008_000034.txt":
bottle 0.14981 80 1 295 500
bus 0.12601 36 13 404 316
horse 0.12526 430 117 500 307
pottedplant 0.14585 212 78 292 118
tvmonitor 0.070565 388 89 500 196
或者表示:<class_name> <confidence> <left> <top> <width> <height>也可以。
计算所有插值点核心算法实现:
def GetPascalVOCMetrics(self,
boundingboxes,
IOUThreshold=0.5,
method=MethodAveragePrecision.EveryPointInterpolation):
"""Get the metrics used by the VOC Pascal 2012 challenge.
Get
Args:todo
boundingboxes:表示真实值和检测到的边界框的 BoundingBoxes 类的对象;
IOUThreshold:IOU 阈值,指示哪些检测将被视为 TP(真正例)或 FP(假正例)(默认值 = 0.5);
method(默认值 = EveryPointInterpolation):可以通过官方 PASCAL VOC 工具包中的实现(EveryPointInterpolation)
计算,或按照论文《PASCAL Visual Object Classes (VOC) Challenge》中描述的 11 点
插值方法(ElevenPointInterpolation)计算。
Returns:todo
一个字典的列表。每个字典包含每个类别的信息和指标。
The keys of each dictionary are:
dict['class']: class representing the current dictionary;
dict['precision']: array with the precision values;
dict['recall']: array with the recall values;
dict['AP']: average precision;
dict['interpolated precision']: interpolated precision values;
dict['interpolated recall']: interpolated recall values;
dict['total positives']: total number of ground truth positives;
dict['total TP']: total number of True Positive detections;
dict['total FP']: total number of False Positive detections;
"""
ret = [] #TODO 包含每个类别的指标(精度、召回率、平均精度)的列表。
#TODO 包含所有真实值的列表(例如:[图像名称,类别,自信度=1,(边界框坐标 XYX2Y2)])。
groundTruths = []
#TODO 包含所有检测结果的列表(例如:[图像名称,类别,自信度,(边界框坐标 XYX2Y2)])。
detections = []
# Get all classes
classes = []
# TODO 遍历所有边界框,将它们分为真实值(GT)和检测结果。
for bb in boundingboxes.getBoundingBoxes():
# TODO 获得当前的box以及判断是否为gt box还是预测box [imageName, class, confidence, (bb coordinates XYX2Y2)]
if bb.getBBType() == BBType.GroundTruth:
groundTruths.append([
bb.getImageName(), #TODO 图像名称
bb.getClassId(), 1, #TODO 类别以及置信度
bb.getAbsoluteBoundingBox(BBFormat.XYX2Y2) #TODO 获得box的绝对坐标
])
else:
detections.append([
bb.getImageName(),
bb.getClassId(),
bb.getConfidence(), #TODO 预测置信度
bb.getAbsoluteBoundingBox(BBFormat.XYX2Y2) #TODO 转换为绝对坐标
])
# TODO 判断当前box对应的类别是否在类别集合中,如果不在就添加进去 get class
if bb.getClassId() not in classes:
classes.append(bb.getClassId())
#TODO 对类别索引进行排序
classes = sorted(classes)
# Precision x Recall is obtained individually by each class
#TODO 遍历每一个类别 Loop through by classes
for c in classes:
# TODO Get only detection of class c
dects = []
#TODO 得到所有预测box中等于当前类别索引c的box
[dects.append(d) for d in detections if d[1] == c]
# TODO Get only ground truths of class c, use filename as key
gts = {}
npos = 0
#TODO 遍历所有gt box,并将等于当前类别的box得到,同时也统计正样本的数量
for g in groundTruths:
if g[1] == c:
npos += 1
#TODO 图像名对应的gt box,gts.get(g[0], []) 尝试获取以 g[0] 为键的值,
# 如果不存在则返回一个空列表 [],然后再将当前的真实值 g 添加到这个列表中。
gts[g[0]] = gts.get(g[0], []) + [g]
# TODO 根据预测的置信度降序排序box sort detections by decreasing confidence
dects = sorted(dects, key=lambda conf: conf[2], reverse=True)
#TODO 保存记录box是TP还是FP
TP = np.zeros(len(dects))
FP = np.zeros(len(dects))
# TODO 根据每一张图像创建对应的字典,create dictionary with amount of gts for each image
det = {key: np.zeros(len(gts[key])) for key in gts}
# print("Evaluating class: %s (%d detections)" % (str(c), len(dects)))
# TODO 遍历当前类别的所有预测box Loop through detections
for d in range(len(dects)):
# print('dect %s => %s' % (dects[d][0], dects[d][3],))
# TODO Find ground truth image 根据预测的图像名获得对应gt box
gt = gts[dects[d][0]] if dects[d][0] in gts else []
iouMax = sys.float_info.min #TODO 可以表示的最小正浮点数
#TODO 遍历当前图像对应的gt box,并计算预测box和gt box中重叠最高的box
for j in range(len(gt)):
# TODO 计算当前图像预测box和gt box之间的IOU print('Ground truth gt => %s' % (gt[j][3],))
iou = Evaluator.iou(dects[d][3], gt[j][3])
#todo 如果IOU大于指定的大小就进行更新,也就是遍历当前的预测box和gt box重叠度最高的
if iou > iouMax:
iouMax = iou
jmax = j
# TODO 判断重叠最高的IOU和指定阈值之间的大小比较 Assign detection as true positive/don't care/false positive
if iouMax >= IOUThreshold:
#TODO 如果当前位置为0,则将当前的指定位置设置为0
if det[dects[d][0]][jmax] == 0:
TP[d] = 1 # TODO count as true positive
det[dects[d][0]][jmax] = 1 # flag as already 'seen'
# print("TP")
else:
FP[d] = 1 # count as false positive
# print("FP")
# - A detected "cat" is overlaped with a GT "cat" with IOU >= IOUThreshold.
else:
FP[d] = 1 # count as false positive
# print("FP")
# TODO 计算累计和 compute precision, recall and average precision
acc_FP = np.cumsum(FP)
acc_TP = np.cumsum(TP)
#TODO 计算recall和precision
rec = acc_TP / npos
prec = np.divide(acc_TP, (acc_FP + acc_TP))
# TODO 判断是11点插值还是所有点 Depending on the method, call the right implementation
if method == MethodAveragePrecision.EveryPointInterpolation:
[ap, mpre, mrec, ii] = Evaluator.CalculateAveragePrecision(rec, prec)
else:
[ap, mpre, mrec, _] = Evaluator.ElevenPointInterpolatedAP(rec, prec)
#TODO add class result in the dictionary to be returned
r = {
'class': c,
'precision': prec,
'recall': rec,
'AP': ap,
'interpolated precision': mpre,
'interpolated recall': mrec,
'total positives': npos,
'total TP': np.sum(TP),
'total FP': np.sum(FP)
}
ret.append(r)
return ret
def CalculateAveragePrecision(rec, prec):
mrec = []
mrec.append(0)
#TODO 得到当前类别计算累计和recall,因为对于recall是累计和越来越大
[mrec.append(e) for e in rec]
mrec.append(1)
#TODO 对于precision累计和是越来越小
mpre = []
mpre.append(0)
[mpre.append(e) for e in prec]
mpre.append(0)
#TODO 去掉锯齿形,当前值和右边进行比较,找到最大值;从 mpre 数组的最后一个元素开始,
# 向前遍历到第一个元素(不包括第一个元素),使用 len(mpre) - 1 作为循环的起始点,0 作为终止点,-1 表示每次递减 1。
for i in range(len(mpre) - 1, 0, -1):
mpre[i - 1] = max(mpre[i - 1], mpre[i])
ii = []
#TODO 判断当前元素(mrec[i])是否不等于下一个元素(mrec[1+i])。这是为了找出 mrec 中连续元素的变化点。
for i in range(len(mrec) - 1):
if mrec[1+i] != mrec[i]:
ii.append(i + 1)
#TODO 计算当前类别的AP,也就是计算矩形的面积
ap = 0
for i in ii:
ap = ap + np.sum((mrec[i] - mrec[i - 1]) * mpre[i])
# return [ap, mpre[1:len(mpre)-1], mrec[1:len(mpre)-1], ii]
return [ap, mpre[0:len(mpre) - 1], mrec[0:len(mpre) - 1], ii]
参考链接: