目标检测算法评估
目录
目标检测的评估
引论
precision和recall
PR曲线和AP,mAP
VOC的mAP评价代码
总结
引论
现在关于深度学习的算法层出不穷,那么我们应该用什么指标去评判一个算法的优缺点呢?
以目标检测为例,要想准确地评价目标检测算法,对目标检测的评估工作肯定是少不了的。通常包括对速度的评价和对精度的评价,对速度的评价指标有FPS(Frame Per Second)即每秒处理的图片数,对精度的评价指标有精度(precision),召回率(recall),准确率(Accuracy),平均准确率(MAP)。
下面我来重点介绍几个目标检测的指标。
precision和recall
解释精确率和召回率之前,先来看下混淆矩阵

把正例正确分类为正例,表示为TP(true positive),把正例错误分类为负例,表示为FN(false negative),
把负例正确分类为负例,表示为TN(true negative), 把负例错误分类为正例,表示为FP(false positive)
接着我们应该了解准确率(precision)和召回率(recall)的基本计算方式,参考下图:
由图可知:
- 左边一整个矩形中(false negative和true positive)的数表示ground truth之中为1的(即为正确的)数据
- 右边一整个矩形中的数表示ground truth之中为0的数据
- 精度precision的计算是用检测正确的数据个数/总的检测个数
-
召回率recall的计算是用检测正确的数据个数/ground truth之中所有正数据个数
可得如下公式
对于目标检测,我们通常设置一个
IOU的阈值
来表示是否检测正确,也就是一个检测box和相应目标的ground truth的IOU超过一定的阈值,并且分类正确则认为检测到一个正确的目标。
PR曲线和AP,mAP
由上述定义可知,召回率和准确率受到了阈值设置的影响,而且阈值对于两个指标的影响是相反的:阈值增加则准确率增加,召回率降低,反之亦然。那么我们就可以通过设置一系列的阈值来得到一系列的(准确率,召回率)的指标对,然后利用这些指标对画出坐标图,这就是
PR曲线
,而AP(average precision)就是这个曲线下的面积。
mAP就是多个分类任务的AP的平均值。
VOC的mAP评价代码
def voc_ap(self, rec, prec, use_07_metric=True):
if use_07_metric:
ap = 0.
# 2010年以前按recall等间隔取11个不同点处的精度值做平均(0., 0.1, 0.2, …, 0.9, 1.0)
for t in np.arange(0., 1.1, 0.1):
if np.sum(rec >= t) == 0:
p = 0
else:
# 取最大值等价于2010以后先计算包络线的操作,保证precision非减
p = np.max(prec[rec >= t])
ap = ap + p / 11.
else:
# 2010年以后取所有不同的recall对应的点处的精度值做平均
# first append sentinel values at the end
mrec = np.concatenate(([0.], rec, [1.]))
mpre = np.concatenate(([0.], prec, [0.]))
# 计算包络线,从后往前取最大保证precision非减
for i in range(mpre.size - 1, 0, -1):
mpre[i - 1] = np.maximum(mpre[i - 1], mpre[i])
# 找出所有检测结果中recall不同的点
i = np.where(mrec[1:] != mrec[:-1])[0]
# and sum (\Delta recall) * prec
# 用recall的间隔对精度作加权平均
ap = np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1])
return ap
# 计算每个类别对应的AP,mAP是所有类别AP的平均值
def voc_eval(self, detpath,
classname,
ovthresh=0.5,
use_07_metric=True):
# 提取所有测试图片中当前类别所对应的所有ground_truth
class_recs = {}
npos = 0
# 遍历所有测试图片
for imagename in imagenames:
# 找出所有当前类别对应的object
R = [obj for obj in recs[imagename] if obj['name'] == classname]
# 该图片中该类别对应的所有bbox
bbox = np.array([x['bbox'] for x in R])
difficult = np.array([x['difficult'] for x in R]).astype(np.bool)
# 该图片中该类别对应的所有bbox的是否已被匹配的标志位
det = [False] * len(R)
# 累计所有图片中的该类别目标的总数,不算diffcult
npos = npos + sum(~difficult)
class_recs[imagename] = {'bbox': bbox,
'difficult': difficult,
'det': det}
# 读取相应类别的检测结果文件,每一行对应一个检测目标
if any(lines) == 1:
# 某一行对应的检测目标所属的图像名
image_ids = [x[0] for x in splitlines]
# 读取该目标对应的置信度
confidence = np.array([float(x[1]) for x in splitlines])
# 读取该目标对应的bbox
BB = np.array([[float(z) for z in x[2:]] for x in splitlines])
# 将该类别的检测结果按照置信度大小降序排列
sorted_ind = np.argsort(-confidence)
sorted_scores = np.sort(-confidence)
BB = BB[sorted_ind, :]
image_ids = [image_ids[x] for x in sorted_ind]
# 该类别检测结果的总数(所有检测出的bbox的数目)
nd = len(image_ids)
# 用于标记每个检测结果是tp还是fp
tp = np.zeros(nd)
fp = np.zeros(nd)
# 按置信度遍历每个检测结果
for d in range(nd):
# 取出该条检测结果所属图片中的所有ground truth
R = class_recs[image_ids[d]]
bb = BB[d, :].astype(float)
ovmax = -np.inf
BBGT = R['bbox'].astype(float)
# 计算与该图片中所有ground truth的最大重叠度
if BBGT.size > 0:
......
overlaps = inters / uni
ovmax = np.max(overlaps)
jmax = np.argmax(overlaps)
# 如果最大的重叠度大于一定的阈值
if ovmax > ovthresh:
# 如果最大重叠度对应的ground truth为difficult就忽略
if not R['difficult'][jmax]:
# 如果对应的最大重叠度的ground truth以前没被匹配过则匹配成功,即tp
if not R['det'][jmax]:
tp[d] = 1.
R['det'][jmax] = 1
# 若之前有置信度更高的检测结果匹配过这个ground truth,则此次检测结果为fp
else:
fp[d] = 1.
# 该图片中没有对应类别的目标ground truth或者与所有ground truth重叠度都小于阈值
else:
fp[d] = 1.
# 按置信度取不同数量检测结果时的累计fp和tp
# np.cumsum([1, 2, 3, 4]) -> [1, 3, 6, 10]
fp = np.cumsum(fp)
tp = np.cumsum(tp)
# 召回率为占所有真实目标数量的比例,非减的,注意npos本身就排除了difficult,因此npos=tp+fn
rec = tp / float(npos)
# 精度为取的所有检测结果中tp的比例
prec = tp / np.maximum(tp + fp, np.finfo(np.float64).eps)
# 计算recall-precise曲线下面积(严格来说并不是面积)
ap = self.voc_ap(rec, prec, use_07_metric)
# 如果这个类别对应的检测结果为空,那么都是-1
else:
rec = -1.
prec = -1.
ap = -1.
return rec, prec, ap
总结
mAP计算的总体思想总结如下,得到检测结果dets之后:
- 将所有的det_box按det_score进行排序
- 计算每个det_box与所有gt_box(ground-truth)的IOU
- 取IOU最大(max_IOU)的gt_box作为这个det_box的预测结果是否正确的判断依据,然后根据max_IOU的结果判断预测结果是TP还是FP
针对上述的第3步,每个类别单独处理:
- preTP:max_IOU大于ovp_thresh,同时分类结果与max_IOU对应的gt_box的类别是一致的,则该det_box归为preTP,同时将这个gt_box从候选gt_box中去除。
- preFP:det_box是A类,但是对应max_IOU的gt_box是B类。
- 如果一个det_box的max_IOU与一个已经被前面det_score较大的det_box对应并且从候选的gt_box中去除了,则这个det_box也是一个preFP。
- FN:没有被检测到的gt_box,也就是没有det_box的max_IOU的目标是这个gt_box。
上述的计算过程可以简化,也就是对每个det_box,我们计算与其预测类别一样的gt_box的IOU就行,然后取max_IOU,如果max_IOU大于ovp_thresh,并且这个max_IOU对应的gt_box还没有被别的det_box预测(设置一个found的标志位),则这个det_box就是preTP,并将该gt_box的found设置为true,否则就是preFP。遍历完之后就可以判断,found为false的gt_box为FN。
注意到,上述的过程中,det_score是没有用到的,只是最初做了一个排序,所以求得的是preTP和preFP,还不是最终结果,然后在不同的det_score的阈值下处理上述的结果,就得到了TP和FP,就可以计算不同阈值下的recall和precision,画出PR曲线,计算每个类别的ap,然后得到目标检测算法的mAP。