• 目标检测: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
                 ]
    
    • 1
    • 2
    • 3
    • 4
    • 5

    根据这个调用方式,以及该函数的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)
            '''
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

    • 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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    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,
                }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    在这里插入图片描述
    上图是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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

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

  • 相关阅读:
    机器人非线性系统反馈线性化与解耦
    566页19万字区级一网通办政务服务应用平台建设项目方案书
    Python的requests库使用总结
    4603. 最大价值
    Golang协程WaitGroup
    python读取amazon s3上的文件到内存
    如何优雅构建自定义 Spring Boot 验证器,让你的代码更加丝滑!
    代码随想录算法训练营第四十四天|km46. 携带研究材料、 416. 分割等和子集
    element 当prop动态时失效问题 添加key值即可
    【场景化解决方案】瓴羊“呼叫中心”,提升企业CRM管理效率
  • 原文地址:https://blog.csdn.net/kill2013110/article/details/126055937