目标检测中TP、FP、TN、FN
首先,我们需要知道的一个概念便是目标检测里的TP、FP、TN、FN。目标检测中的TP意味着预测框里有目标(并不严谨,但可以直观的理解),FP意味着预测框里没有目标,TN意味着没有目标的位置没有预测框,FN意味着有目标的位置没有预测框。绿色实线或虚线框:人脸的真实标注
红色实线或虚线框:输出的检测结果
红色数字:检测结果为人脸的概率
参考上图与我们定义的概念,我们可以得知四个红色实线框将贡献TP值,红色虚线框贡献FP值,绿色虚线框则贡献FN值。当然你会发现TN值难以度量,但其实我们并不需要它。
Precision和Recall的定义
上面我们已经定义好了TP、FP、TN、FN,下面我们就可以过渡到直接参与计算mAP值的两个指标Precision和Recall。相信大家对这两个指标应该很熟悉,为了完整性,这里再重申一遍:
Precision是查准率,意味着预测框中有多少真正包含了目标,计算公式如下:
Recall是召回率,意味着真正的目标中有多少被预测框包含,计算公式如下:
依旧以上图为例,我们可以轻松的得出TP=4,FP=1,FN=1。从而可以计算出Precision和Recall值都为0.8。很简单对吧,那mAP值又和这两个指标有什么关系呢?我们继续往下看
Precision和Recall其实是冤家
为何说Precision和Recall是冤家的关系呢,因为它俩是此消彼长的关系。可以发现,我们在上文中,我们还没有用到框框上面的置信度值,这个值意味着当前这个框的可信程度,而我们完全可以划定一个置信度阈值,让低于该值的预测框不可信,即我们可以理解为并没有这个预测框。
这样,当我们设定不同的置信度阈值之后,Precision和Recall这两个指标也会发生变化,具体见下面的示例:
置信度阈值为0.85
置信度阈值为0.90
置信度阈值为0.91
置信度阈值为0.95
置信度阈值为:1.00
于是乎我们得到了下面的PR曲线:
看到这个图你很难理解Precision和Recall是冤家的概念,因为样本数目太少,细化程度不够,一般的PR曲线长下面这样:
看了上面这个图,现在你应该明白Precision和Recall其实是鱼和熊掌很难兼得的关系(之所以说很难,是因为万一你的P-R曲线是面积为1的正方形呢,狗头),那么衡量一个检测网络就要综合考量这两个指标。
mAP和AP值的计算:
mAP是mean AP,即不同类别样本AP的平均值,所以我们先只关心AP值即可。AP值怎么算呢,根据我们上面的论述,PR曲线所代表的的面积值其实就是一个综合考量的指标啊,故公式如下: AP=S(RP曲线)
现在假设我们有要检测的n个类,比如人、车、猫、狗等,那么我们计算出每一类的AP值,求和然后取平均值,就可得到mAP,它能够综合考量对所有类别的检测效果,公式如下:
mAP=∑AP / n
知道了上面的概念,当我们求取一个检测网络的mAP时,我们要做的一件事情就是对每一个类别,根据不同的置信度阈值来得到不同置信度阈值下的Precision和Recall值,从而得到P-R曲线,进而求的mAP。因此,关键在于求的每个类别的AP,当然我们也要回归到代码,那么让我们参考一下VOC数据集标准的AP计算代码,看一看它如何做到的。
def voc_eval(self, detpath, classname, cachedir, ovthresh=0.5, use_07_metric=True):
if not os.path.isdir(cachedir):
os.mkdir(cachedir)
cachefile = os.path.join(cachedir, 'annots.pkl')
# read list of images
with open(self.imgsetpath, 'r') as f:
lines = f.readlines()
imagenames = [x.strip() for x in lines]
if not os.path.isfile(cachefile):
# load annots
recs = {}
for i, imagename in enumerate(imagenames):
recs[imagename] = self.parse_rec(self.annopath % (imagename))
if i % 100 == 0 and self.display:
print('Reading annotation for {:d}/{:d}'.format(
i + 1, len(imagenames)))
# save
if self.display:
print('Saving cached annotations to {:s}'.format(cachefile))
with open(cachefile, 'wb') as f:
pickle.dump(recs, f)
else:
# load
with open(cachefile, 'rb') as f:
recs = pickle.load(f)
# extract gt objects for this class
class_recs = {}
npos = 0
for imagename in imagenames:
R = [obj for obj in recs[imagename] if obj['name'] == classname]
bbox = np.array([x['bbox'] for x in R])
difficult = np.array([x['difficult'] for x in R]).astype(np.bool)
det = [False] * len(R)
npos = npos + sum(~difficult)
class_recs[imagename] = {'bbox': bbox,
'difficult': difficult,
'det': det}
# read dets
detfile = detpath.format(classname)
with open(detfile, 'r') as f:
lines = f.readlines()
if any(lines) == 1:
splitlines = [x.strip().split(' ') for x in lines]
image_ids = [x[0] for x in splitlines]
confidence = np.array([float(x[1]) for x in splitlines])
BB = np.array([[float(z) for z in x[2:]] for x in splitlines])
# sort by confidence
sorted_ind = np.argsort(-confidence)
sorted_scores = np.sort(-confidence)
BB = BB[sorted_ind, :]
image_ids = [image_ids[x] for x in sorted_ind]
# go down dets and mark TPs and FPs
# nd的长度是检测出来的每个框
nd = len(image_ids)
tp = np.zeros(nd)
fp = np.zeros(nd)
# 把输出的所有框的置信度作为阈值卡一遍
for d in range(nd):
R = class_recs[image_ids[d]]
bb = BB[d, :].astype(float)
ovmax = -np.inf
BBGT = R['bbox'].astype(float)
if BBGT.size > 0:
# compute overlaps
# intersection
ixmin = np.maximum(BBGT[:, 0], bb[0])
iymin = np.maximum(BBGT[:, 1], bb[1])
ixmax = np.minimum(BBGT[:, 2], bb[2])
iymax = np.minimum(BBGT[:, 3], bb[3])
iw = np.maximum(ixmax - ixmin, 0.)
ih = np.maximum(iymax - iymin, 0.)
inters = iw * ih
uni = ((bb[2] - bb[0]) * (bb[3] - bb[1]) +
(BBGT[:, 2] - BBGT[:, 0]) *
(BBGT[:, 3] - BBGT[:, 1]) - inters)
overlaps = inters / uni
ovmax = np.max(overlaps)
jmax = np.argmax(overlaps)
if ovmax > ovthresh:
if not R['difficult'][jmax]:
if not R['det'][jmax]:
tp[d] = 1.
R['det'][jmax] = 1
else:
fp[d] = 1.
else:
fp[d] = 1.
# compute precision recall
fp = np.cumsum(fp)
tp = np.cumsum(tp)
rec = tp / float(npos)
# avoid divide by zero in case the first detection matches a difficult
# ground truth
prec = tp / np.maximum(tp + fp, np.finfo(np.float64).eps)
ap = self.voc_ap(rec, prec, use_07_metric)
else:
rec = -1.
prec = -1.
ap = -1.
return rec, prec, ap
上述是代码里最核心的函数voc_eval(),如果你仔细阅读这段代码,你会发现这段代码会把关于某一类比如人预测出的所有框的置信度都作为一个阈值,从而计算当前置信度阈值下的Precision和Recall值。比如你在7000多张图片里预测了30000个包含人的框,那么你将这30000个框的置信度都分别作为阈值去计算。这种好处就是得到的P-R曲线很精细,而不是你随便筛选几个阈值去计算。
总之,关于AP和mAP的计算就大致如上喽!
一些细节补充:
关于目标检测中TP、FP、TN、FN这几个概念,有必要详实一下。比如我们上文说,目标检测中的TP意味着预测框里有目标,这个并不严谨。下面我们补充几点,来确保某框就是TP框。
图片源于知乎OpenMMLab
作为一个TP框:
1、首先要保证预测框与gt框(真实框)的IOU要大于一定的阈值,IOU概念如果不了解,请网上查阅。
2、没有比本框更好的包含相同目标的框了,下图左下角的0.89框可不是TP框。
3、置信度达标
个人总结:
计算某个类别的混淆矩阵时(以猫类别为例):
首先找到所有预测类别为猫的预测框:
让这些预测框与所有类别为猫的gtbox中未匹配的gtbox进行匹配,如果最大的iou大于阈值,那么就是TP,需要注意的是一个gtbox只能匹配一次。(当多个预测框与某个gtbox的iou都大于阈值时,则选择置信度较大作为TP。剩下的一个如果能与别的gtbox匹配上,则其仍为TP,否则就是FP)。
如果预测框与gtbox的iou小于一定阈值,那么就是FP,另一层含义就是他本应该是背景,但被预测为了猫。
如果gtbox没有被预测框匹配上,那么就是FN,漏检了。
计算某个类别的AP(其实就是计算某个类别在不同置信度阈值下的Precison和Recall,进而得到AP):
根据预测为某个类别的所有预测框的目标置信度进行降序排列;
计算当前目标与gt中未匹配目标的IoU,当最大IoU大于给定阈值时,认为当前预测框为TP,否则为FP。
根据置信度排序的先后依次计算符合当前置信度下对所有样本的Precision和Recall,形成PR曲线(当recall相同时,选择precision最大的点),AP即为PR曲线的面积。
(换言之,计算AP就是将所有预测框按照置信度降序排好,然后计算不同置信度的情况下现存的预测框所对应的混淆矩阵,得出Precision和Recall,有多少个置信度就计算出多少组P和R,进行画出PR曲线得到AP)
注:存在两种特殊情况:第一种情况,两个检测框A和B和同一个gt的IOU都大于阈值。检测框是按置信度由大到小排列的,当A和B置信度大的那个与当前gt匹配以后,这个gt就不再合后续的检测框匹配了,如果置信度小的那个框没有和其它gt相匹配的话,就会定义为FP。第二种情况,一个检测框同时和两个gt的IOU大于阈值。对于当前检测框来说,计算与其IOU最大的那个gt,超过阈值后会作为一个TP,则当前检测框的判断就结束了,不会再判断与其他gt的IOU是否超过阈值了。
另一个例子:https://www.zhihu.com/question/53405779/answer/993913699