Bootstrap

目标检测:cocoeval中的evaluateImg,accumulate函数解析

coco数据集的评价指标的计算还是比较复杂的,代码写的也比较凝炼,最近要计算目标检测的混淆矩阵,我看mmdet的计算方式比较奇怪,本着P和R等计算方法要与coco官方对齐的目的,特地写此笔记对coco官方的计算方式进行深入理解。
其他相关优秀笔记:
COCO API-深入解析cocoeval在det中的应用

coco计算不同map有很多变量:iou阈值,目标的面积范围,最大检测框数量等。
coco首先使用cocoEval.evaluate() 函数进行匹配计算,然后使用cocoEval.accumulate()函数进行结果的累加

1. cocoEval.evaluate()

这个函数前面都是一些比较好理解的准备工作,传入参数,计算每张图,每一类中,gt和det两两之间的iou矩阵,存储在self.iou这个字典中,
字典有len(imgId)*len(catId)个key。然后就是调用evaluateImg这个函数了。

        self.evalImgs = [evaluateImg(imgId, catId, areaRng, maxDet)
                 for catId in catIds
                 for areaRng in p.areaRng
                 for imgId in p.imgIds
             ]

根据这个调用方式,以及该函数的return我们可以确定 self.evalImgs是一个长度为len(catId)*len(areaRng)*len(imgId)的列表,其每个元素是一个字典,包含det和gt的匹配信息。
接下来我们再看这个关键的evaluateImg函数

    def evaluateImg(self, imgId, catId, aRng, maxDet):
        '''
        perform evaluation for single category and image
        :return: dict (single image results)
        '''

该函数是为了在特定限制下对单幅图某一类的检测结果进行评估,传入了四个参数:

  • imgId, 表示当前所处理图片的ID
  • catId,表示当前处理的类别ID
  • aRng,表示当前的面积范围,这是为了方便评估s,m,l三种尺度目标的检测效果
  • maxDet,最大检测框数量限制
  1. 函数首先在所有框中挑出imgId, catId的gt框和det框,并根据面积范围的限制设置该gt框是否应该ignore,并把满足面积范围的gt框排在前面,把score更高的det框也排在前面,还设置了该gt框的iscrowd标签,最后从self.ious中挑出imgId, catId的ious矩阵,注意这个最后的ious阵也是根据是否满足面积范围排序了
    self.ious是有n*c个key的字典,n代表图片总数,c代表数据的类别数,每个key所对应value是一个二维矩阵,每一列代表某个gt框与所有det框的交并比。这个字典是之前已经计算好的。该部分代码如下:
        p = self.params
        if p.useCats:
            gt = self._gts[imgId,catId]
            dt = self._dts[imgId,catId]
        else:
            gt = [_ for cId in p.catIds for _ in self._gts[imgId,cId]]
            dt = [_ for cId in p.catIds for _ in self._dts[imgId,cId]]
        if len(gt) == 0 and len(dt) ==0:
            return None

        for g in gt:
            if g['ignore'] or (g['area']<aRng[0] or g['area']>aRng[1]):
                g['_ignore'] = 1
            else:
                g['_ignore'] = 0

        # sort dt highest score first, sort gt ignore last
        gtind = np.argsort([g['_ignore'] for g in gt], kind='mergesort')
        gt = [gt[i] for i in gtind]
        dtind = np.argsort([-d['score'] for d in dt], kind='mergesort')
        dt = [dt[i] for i in dtind[0:maxDet]]
        iscrowd = [int(o['iscrowd']) for o in gt]
        # load computed ious
        ious = self.ious[imgId, catId][:, gtind] if len(self.ious[imgId, catId]) > 0 else self.ious[imgId, catId]
  1. 接下来是一个三重嵌套的循环,首先是0.5:0.95的iou阈值循环,然后是每个det框的循环,最后是每个gt框的循环。
    代码及自己的注释:
		T = len(p.iouThrs)
        G = len(gt)
        D = len(dt)
        gtm  = np.zeros((T,G))
        dtm  = np.zeros((T,D))
        gtIg = np.array([g['_ignore'] for g in gt])
        dtIg = np.zeros((T,D)) # 默认det框都不被ignore
        if not len(ious)==0:
            for tind, t in enumerate(p.iouThrs):
                for dind, d in enumerate(dt): # 优先给score更高的det框匹配gt框
                    # information about best match so far (m=-1 -> unmatched)
                    iou = min([t,1-1e-10])
                    m   = -1
                    for gind, g in enumerate(gt):
                        # if this gt already matched, and not a crowd, continue,
                        '''意思一种iou阈值下一个gt只匹配到一个det框'''
                        if gtm[tind,gind]>0 and not iscrowd[gind]:
                            continue
                        # if dt matched to reg gt, and on ignore gt, stop,
                        '''
                        意思该detbox已有匹配且gt满足面积范围,
                        当前gt不满足面积范围则跳出gt的循环。因为gt是排序过的,
                        这说明在面积范围的gt都被遍历过了,当前匹配的肯定也是
                        iou最高的处于面积范围内的gt框,直接打断循环即可
                        '''
                        if m>-1 and gtIg[m]==0 and gtIg[gind]==1:
                            break
                        # continue to next gt unless better match made,
                        '''意思已有匹配的iou是否小于当前匹配的iou,小于就跳过该gt'''
                        if ious[dind,gind] < iou:
                            continue
                        # if match successful and best so far, store appropriately,
                        iou=ious[dind,gind]
                        m=gind
                    '''
					对该gt框的循环做一总结:
					在当前iou阈值下还没被匹配的gt框中
					1. det框会匹配在面积范围内且iou最高的gt框
					2. 若面积范围内的gt框没有超过当前iou阈值的,那么det框匹配在面积范围外且iou最高的gt框
					'''
                    # if match made store id of match for both dt and gt
                    '''若当前det框与这些gt框的iou都没超过阈值,那就没有匹配,跳过该det框'''
                    if m ==-1: 
                        continue
                    dtIg[tind,dind] = gtIg[m] #当前所匹配的gt是否在面积范围内
                    dtm[tind,dind]  = gt[m]['id'] # 当前det匹配的gt id
                    gtm[tind,m]     = d['id'] # 当前gt匹配的 det 的 id
  1. 最后找出不在面积范围内的且没匹配成功的det框,将其设置为ignore,这个对不同尺度目标检测的评价结果有影响。最终返回该张图该类的匹配结果。代码:
        # set unmatched detections outside of area range to ignore
        a = np.array([d['area']<aRng[0] or d['area']>aRng[1] for d in dt]).reshape((1, len(dt)))
        dtIg = np.logical_or(dtIg, np.logical_and(dtm==0, np.repeat(a,T,0)))
        # store results for given image and category
        return {
                'image_id':     imgId,
                'category_id':  catId,
                'aRng':         aRng,
                'maxDet':       maxDet,
                'dtIds':        [d['id'] for d in dt],
                'gtIds':        [g['id'] for g in gt],
                'dtMatches':    dtm,
                'gtMatches':    gtm,
                'dtScores':     [d['score'] for d in dt],
                'gtIgnore':     gtIg,
                'dtIgnore':     dtIg,
            }

在这里插入图片描述
上图是coco中对两中比较特别的情况的判断方法。coco是将每一类当成二分类进行匹配的,其他类也算作background。即下面三条:

  • TP(True Positive): loU>0.5的检测框数量(同一Ground Truth只计算一次)
  • FP(False Positive): loU<=0.5的检测框(或者是检测到同一个GT的多余检测框的数量)
  • FN(False Negative):没有检测到的GT的数量

而在mmdet的计算混淆矩阵的方法中,并不是把每一类的gt和det拎出来再匹配,而且也没有去除重复的操作,即左边这种情况下,只要iou超过阈值,一个gt会和这两个det都匹配,导致最终的tp值大于了真实的gt数,如果gt和det类别不相同,也就增加了其他类的fp值。在右边的情况中也是没有去除重复,同样会增加两个tp,若类别不相同,那会增加其他类的fp。

2. cocoEval.accumulate()

cocoEval.evaluate() 只是每幅图的det和gt做了匹配,并将结果存在了self.evalImgs中。计算tp等指标需要cocoEval.accumulate()。

  1. 准备工作,传入参数
    def accumulate(self, p = None):
        '''
        Accumulate per image evaluation results and store the result in self.eval
        :param p: input params for evaluation
        :return: None
        '''
        print('Accumulating evaluation results...')
        tic = time.time()
        if not self.evalImgs:
            print('Please run evaluate() first')
        # allows input customized parameters
        if p is None:
            p = self.params
        p.catIds = p.catIds if p.useCats == 1 else [-1]
        T           = len(p.iouThrs) # IoU阈值的数量
        R           = len(p.recThrs) # score阈值的数量
        K           = len(p.catIds) if p.useCats else 1 # 类别数量
        A           = len(p.areaRng) 
        M           = len(p.maxDets)
        precision   = -np.ones((T,R,K,A,M)) # -1 for the precision of absent categories
        recall      = -np.ones((T,K,A,M))
        scores      = -np.ones((T,R,K,A,M))
'''precision,recall,scores这三个张量的形状为什么设置成这样,不太能完全理解'''

        # create dictionary for future indexing
        _pe = self._paramsEval
        catIds = _pe.catIds if _pe.useCats else [-1]
        setK = set(catIds)
        setA = set(map(tuple, _pe.areaRng))
        setM = set(_pe.maxDets)
        setI = set(_pe.imgIds)
		'''全都转换成集合了'''
		
        # get inds to evaluate
        k_list = [n for n, k in enumerate(p.catIds)  if k in setK]
        m_list = [m for n, m in enumerate(p.maxDets) if m in setM]
        a_list = [n for n, a in enumerate(map(lambda x: tuple(x), p.areaRng)) if a in setA]
        i_list = [n for n, i in enumerate(p.imgIds)  if i in setI]
        I0 = len(_pe.imgIds)
        A0 = len(_pe.areaRng)
        # retrieve E at each category, area range, and max number of detections

内容有点多,未完待续。。

;