目标检测的评估指标——mAP

目录:

  • 1.相关概念
    • 1)IoU(Intersection over Union)
    • 2)Precision和Recall
  • 2.mAP(mean Average Precision)
    • 1)Pascal VOC
    • 2)COCO
  • 3.总结mAP的计算流程
  • 4.mAP的代码实现

一、相关概念

1.IoU(Intersection over Union)

IoU的定义:预测bbox与实际bbox的交并比。
IoU被用来判断对一个对象的预测是否正确。若IoU > threshold,该预测被认为是TP;否则若IoU <= threshold,该预测被认为是FP。

2.Precision和Recall

为了更好的理解mAP,我们先了解一下Precision和Recall。

Recall(召回率/查全率):是指在所有确实为正的样本中,被预测为正样本的占比。
Precision(精确率/查准率):是指在所有被预测为正的样本中,确实是正样本的占比。
Recall=\frac{TP}{TP+FN}=\frac{TP}{N_{groundtruths}} \\ Precision=\frac{TP}{TP+FP}=\frac{TP}{N_{predictions}}

二、mAP(mean Average Precision)

目标检测问题不同于一般的分类问题,不仅要检测出目标,输出目标的类别,还要定位出目标的位置。分类问题中的Accuracy已不能作为目标检测算法的评估指标,而mAP是目标检测算法中最常用的评估指标。
1)AP(Average Precision)是PR曲线围成的面积,用来衡量对一个类检测的好坏。
2)mAP(mean Average Precision)是所有类AP的平均值,衡量多类别目标检测的好坏。

不同的数据集/竞赛可能有不同的评估指标。最常用的是PASCAL VOC和MS COCO中的评价指标。

Pascal VOC

为了计算mAP,首先需要计算每个类的AP。

步骤一:绘制PR曲线

以下面的两张图片为例,包含某一个指定类别的GT框(绿色)和预测框(红色)。

对于每张图片中的每个预测框,计算并选择与预测框的IoU最大那个GT框,统计成表格如下:

VOC中的IoU阈值为0.5,所以IoU > 0.5被视为TP,否则为FP。现在,我们根据置信度confidence从高到低进行排序。值得注意的是,若多个预测框对应同一个GT,则置信度最高的那个视为TP,其他的视为FP。如下表中P3和P4都对应GB,P4(置信度最高)被视为TP,P3被视为FP。

在VOC指标中,第k行(rank)的Recall和Precision的计算,是包含当前行及以上所有行的预测数据。首先,累计计算每一行的TP和FP;然后,根据公式计算每k行的Precision和Recall(Precision等于TP除以当前预测框总数,Recall等于TP除以所有GT框数量)。以表中第二行(rank=2)为例,TP累计为1,当前预测框总数为2,所有GT框数量为3,有Precision=1/2=0.5,Recall=1/3=0.33。

计算完每一行的Precision和Recall之后,以Precision为横坐标,Recall为纵坐标,即可得到PR曲线。

步骤二:平滑PR曲线

每个查全率级别r的Precision,通过取查全率>=r的所有Precision的最大值来进行插值替换。即保证低查全率的精度不小于更高查全率的精度。

步骤三:计算AP

VOC 2007是取11个Recall点[0,0.1,...,1]的Precision的平均值作为AP值。
AP=\frac{1}{11}\sum_{r \in (0,0.1,...,1)}p_{interp(r)}

VOC 2012是取所有Recall点的Precision的平均值作为AP值,即PR曲线下的面积(AUC)。

计算完每个类的AP值之后,求平均即为mAP。

COCO

在VOC中,IoU的阈值固定为0.5,这意味着,IoU分别为0.6、0.9的两个预测被认为是等价的,显然这是有偏差的。COCO中通过指定一个阈值范围[.5:.05:.95]来解决这个问题,它计算这个范围中每个阈值的mAP,然后求平均得到最终的mAP。
mAP_{COCO}=\frac{mAP_{0.50} + mAP_{0.55} + ... + mAP_{0.95}}{10}

另外,COCO中使用101点法(Recall范围[0:.01:1] )来计算AP。
注意:在COCO中mAP也可简写为AP。

COCO中AP的计算步骤(并非唯一):
1)对于每个类,计算该类在不同IoU阈值的AP并取平均。
AP[class]=\frac{1}{N_{thresholds}}\sum_{iou \in thresholds}AP[class,iou]

2)对所有类的AP取平均,得到最终的AP。
AP[class]=\frac{1}{N_{thresholds}}\sum_{iou \in thresholds}AP[class,iou]

可见,COCO中的AP实际是“平均平均平均精度”。

三、总结mAP的计算流程

mAP的计算流程:
  • 1.首先,指定一个较低的confidence阈值(通常是0.001、0.01),来筛选网络的预测框。
    • 选用低阈值是为了尽可能保留较多的框。由于不同的模型之间合理阈值是不一样的,测试mAP需要屏蔽这个不同,以实现统一标准。
  • 2.对筛选后的预测框,进行nms(使用类内nms)非极大值抑制,去除高度重叠的框。
    • nms的IoU阈值一般取0.5,你选择0.6、0.7也是可以的,这个影响不大。
  • 3.根据经过上面处理后的预测框和真值GT框,来计算mAP:
    • 3.1)先计算每个类的、指定IoU阈值的AP(以计算AP75为例,iou_threshold=0.75)。
      • 3.1.1)为每个类构建一个matched_table表。
        • 行数等于所有的预测框数量,列数为[confidence, matched_iou, matched_groundtruth_index, image_id]。
          • image_id为预测框对应的图片ID,confidence为预测框的置信度;
          • matched_GT_index为与该预测框的IoU最大的那个GT框索引(同一张图片中的预测框和GT框之间计算IoU);
          • max_matched_iou为最大的那个IoU值(用来与iou_threshold做对比,判断是TP还是FP)。
        • 按置信度confidence从高到低对matched_table表进行排序。
      • 3.1.2)判断每个预测框是属于TP还是FP。
        • 当matched_iou <= iou_threshold时,都视为FP(mAP@[IoU=0.5]的iou_threshold=0.5)。
        • 当matched_iou > iou_threshold时,即预测框匹配某个GT时:
          • 如果该GT第一次被匹配,则当前预测框(置信度最高)被视为TP,否则被视为FP。
          • (注意:一个GT框最多只能对应一个预测框,出现多个预测框匹配同一个GT的情况时,将置信度最高的那个视为TP。)
      • 3.1.3)累计每行(rank)总TP数,并计算每行的Precision和Recall。
        • 每一行的Recall和Precision的计算公式:
          • Precision=\frac{TP}{TP+FP}=\frac{TP}{N_{predictions}},Recall=\frac{TP}{TP+FN}=\frac{TP}{N_{groundtruths}}
          • TP是指当前行及以上所有TP总数,N_{predictions}是当前行及以上所有预测框数量,N_{groundtruths}是所有的GT框数量。
      • 3.1.4)计算完每行的Precision和Recall后,将其绘制成PR曲线,即可计算AP。
        • 首先,对PR曲线进行平滑处理。
          • 使得低Recall的Precision不低于比它更高的Recall的Precision。
        • 然后,计算Recall对应的平均精度(AP)。有几种计算方式:
          • VOC 2007:11点法,即取Recall[0:0.1:1]的11个点的平均Precision作为AP。
          • VOC 2012:取所有点的平均Precision作为AP,即PR曲线下的面积。
          • COCO:101点法,Recall[0:0.01:1]的101点的平均就Precision作为AP。
    • 3.2)对所有类别的AP值求平均,即得到mAP。
      • 可通过调整IoU阈值,分别得到AP50、AP75和AP@[IoU=0.5:0.95]。

四、mAP的代码实现

手动实现计算mAP的代码,并与调用pycocotools库计算mAP做对比,两者结果一致。

1.手动实现计算mAP的代码

实现代码:

# 计算IoU(多对多)
def ious(a, b):
    '''
    a : 4 x M x 1    left, top, right, bottom
    b : 4 x 1 x N    left, top, right, bottom
    '''
    aleft, atop, aright, abottom = [a[i] for i in range(4)]
    bleft, btop, bright, bbottom = [b[i] for i in range(4)]
    
    # aleft.shape = M, 1
    # bleft.shape = 1, N
    cross_left = np.maximum(aleft, bleft)        # M x N
    cross_top = np.maximum(atop, btop)           # M x N
    cross_right = np.minimum(aright, bright)     # M x N
    cross_bottom = np.minimum(abottom, bbottom)  # M x N
    
    # cross_area.shape  =  M x N
    cross_area = (cross_right - cross_left + 1).clip(0) * (cross_bottom - cross_top + 1).clip(0)
    # union_area.shape  =  M x N
    union_area = (aright - aleft + 1) * (abottom - atop + 1) + (bright - bleft + 1) * (bbottom - btop + 1) - cross_area
    # M x N
    return cross_area / union_area

# 构建指定类的matched_table
def build_matched_table(classes_index, groundtruths, detections, maxDets=100):
    '''
    classes_index: 需要构建matched_table的类索引
    groundtruths: GT框,形如{"image_id": [[xmin, ymin, xmax, ymax, 0, class_index], ...], ...}
    detections: 预测框形如{"image_id": [[xmin, ymin, xmax, ymax, confidence, class_index], ...], ...}
    maxDets: 每张图片的最大预测框数量,默认为100
    '''
    matched_table = []  # 构建的matched_table表
    sum_groundtruths = 0  # 统计GT框的数量
    # 遍历每张图片
    for image_id in groundtruths:
        # 选择"当前类"的预测框和GT框,并转换为numpy类型
        # [x1,y1,x2,y2,conf,class_index]
        select_detections = np.array(list(filter(lambda x: x[5] == classes_index, detections[image_id])))    
        select_groundtruths = np.array(list(filter(lambda x: x[5] == classes_index, groundtruths[image_id])))
        num_detections = len(select_detections)
        num_groundtruths = len(select_groundtruths)

        # 有用的预测框
        num_use_detections = min(num_detections, maxDets)
        # 统计GT框数量
        sum_groundtruths += num_groundtruths

        # 当前图片的预测框数量为0,直接返回
        if num_detections == 0:
            continue

        # 当图片的GT框数量为0时,选择不超过数量上限的预测框(任意选择,不影响,都是FP),matched_iou置为0
        if len(select_groundtruths) == 0:
            for detection_index in range(num_use_detections):
                confidence = select_detections[detection_index, 4]
                matched_table.append([confidence, 0, -1, image_id])
            continue

        # reshape,以便可以广播,同时计算多个iou
        sgt = select_groundtruths.T.reshape(6, -1, 1)
        sdt = select_detections.T.reshape(6, 1, -1)

        # 计算所有GT与所有预测框的IoU
        groundtruth_detection_ious = ious(sgt, sdt)
        # 构建matched_table表
        for detection_index in range(num_use_detections):
            confidence = select_detections[detection_index, 4]
            matched_groundtruth_index = groundtruth_detection_ious[:, detection_index].argmax()
            matched_iou = groundtruth_detection_ious[matched_groundtruth_index, detection_index]
            matched_table.append([confidence, matched_iou, matched_groundtruth_index, image_id])

    # 按置信度confidence从高到低进行排序
    matched_table = sorted(matched_table, key=lambda x: x[0], reverse=True)
    return matched_table, sum_groundtruths

# 计算单个类的、指定iou_threshold的AP
def compute_AP(matched_table, iou_threshold, sum_groundtruths):
    '''
    matched_table: 形如[[confidence, matched_iou, matched_groundtruth_index, image_id], ...]
    '''
    # 1.判断每个预测框属于TP还是FP。
    num_detections = len(matched_table)  # 预测框总数量
    true_positive = np.zeros((num_detections,))  # 每一个预测框的TP/FP表示(0为FP,1为TP)
    # 构建一个groundtruth_seen_map字典,标记某个GT是否已经被预测。
    # item[3]是image_id,以image_id为key,value初始为一个空的set()集合。
    groundtruth_seen_map = {item[3]:set() for item in matched_table}
    # 注意:matched_table是按置信度从大到小进行排序后的。
    # 从上到下遍历每个预测框,判断属于TP还是FP:
    # 1)当matched_iou <= iou_threshold时,都视为FP。
    # 2)当matched_iou > iou_threshold时,即预测框匹配某个GT时:
    #   2.1)如果该GT第一次被匹配(即不在image_id对应的set中),则将GT添加到set中,且当前预测框视为TP。
    #   2.2)如果该GT已经被预测了(即已经在image_id对应的set中了),则将当前预测视为FP。
    for index in range(num_detections):
        # [confidence, matched_iou, matched_groundtruth_index, image_id]
        confidence, matched_iou, matched_groundtruth_index, image_id = matched_table[index]

        # 只有满足matched_iou > iou_threshold且是第一次匹配某个GT时,才认为是TP
        image_seen_map = groundtruth_seen_map[image_id]  # 获取指定图片的seen_map
        if matched_iou > iou_threshold and matched_groundtruth_index not in image_seen_map:
            true_positive[index] = 1  # 判断为TP
            image_seen_map.add(matched_groundtruth_index)  # 添加当前GT到seen_map中
                
    # 2.累加每行的TP,并计算Precision和Recall。
    TP_count = np.cumsum(true_positive)  # 累计每行的TP
    detection_count = np.arange(1, num_detections + 1)  # 累计每行的预测框总数
    precision = TP_count / detection_count  # 计算Precision
    recall = TP_count / sum_groundtruths  # 计算Recall
    
    # 3.平滑PR曲线
    mrec = np.concatenate(([0.], recall, [min(recall[-1] + 1E-3, 1.)]))  # 首尾添加两个点
    mpre = np.concatenate(([0.], precision, [0.]))  # 首尾添加两个点
    # 使得低Recall的Precision不低于比它更高的Recall的Precision。
    mpre = np.flip(np.maximum.accumulate(np.flip(mpre)))
    
    # 4.计算AP:插值计算101点的平均精度(COCO的计算方法)
    AP = np.mean(np.interp(np.linspace(0, 1, 101), mrec, mpre))
    return AP

# 计算所有类的mAP
def compute_mAP(groundtruths, detections, classes, maxDets=100):
    '''
    groundtruths: 形如{"image_id": [[xmin, ymin, xmax, ymax, 0, class_index], [xmin, ymin, xmax, ymax, 0, class_index]], ...}
    detections: 形如{"image_id": [[xmin, ymin, xmax, ymax, confidence, class_index], [xmin, ymin, xmax, ymax, confidence, class_index]], ...}
    classes: 所有类别,形如["aeroplane", "bicycle", "bird", "boat", "bottle", ...]
    maxDets: 每张图片的最大预测框数量,默认为100
    '''
    APs = []
    # 遍历每个类,计算每个类的[AP@[IoU=0.5], AP@[IoU=0.75], AP@[IoU=0.5:0.95]]
    for classes_index in range(len(classes)):
        # 1.构建指定类的matched_table
        matched_table, sum_groundtruths = build_matched_table(classes_index, groundtruths, detections, maxDets)
        # 2.根据matched_table计算AP
        AP50 = compute_AP(matched_table, 0.5, sum_groundtruths)
        AP75 = compute_AP(matched_table, 0.75, sum_groundtruths)
        AP = np.mean([compute_AP(matched_table, iou_threshold, sum_groundtruths) for iou_threshold in np.arange(0.5, 1.0, 0.05)])
        APs.append([AP, AP50, AP75])
        
    # 计算mAP(所有类的AP的平均值)
    return np.mean(APs, axis=0)

预测框detections是经过类内nms处理后的,预测框和GT框的格式形如:
{"image_id": [[xmin, ymin, xmax, ymax, 0, class_index], ...], ...}
计算mAP结果如下:

2.调用pycocotools库计算mAP

安装pycocotools命令:pip install pycocotools

from pycocotools.coco import COCO
from pycocotools.cocoeval import COCOeval

def mapCOCO(groundtruth_annotation, detection_annotation, classes):
    images = []
    annotations = []
    categories = []
    ann_id = 0
    for class_index, class_name in enumerate(classes):
        categories.append({"supercategory": class_name, "id": class_index, "name": class_name})

    for item in groundtruth_annotation:
        filename = item
        anns = groundtruth_annotation[item]
        image_id = int(filename)
        images.append({"id": image_id})

        for left, top, right, bottom, score, class_index in anns:
            ann_id += 1
            width, height = right - left + 1, bottom - top + 1
            annotations.append({"image_id": image_id, "id": ann_id, "category_id": class_index, "bbox": [left, top, width, height], "iscrowd": 0, "area": width * height})

    gt_coco = {"images": images, "annotations": annotations, "categories": categories}
    with open("gt_coco.json", "w") as f:
        json.dump(gt_coco, f)

    cocoGt = COCO("gt_coco.json")
    ann_dets = []
    for item in detection_annotation:
        anns = detection_annotation[item]
        image_id = int(item)  
        for left, top, right, bottom, score, classes in anns:
            # {"image_id":1,"category_id":2,"bbox":[199.84, 190.46, 77.71, 70.88],"score":0.236},
            width = right - left + 1
            height = bottom - top + 1
            object_item = {"image_id": image_id, "category_id": classes, "score": score, "bbox": [left, top, width, height]}
            ann_dets.append(object_item)

    cocoDt = cocoGt.loadRes(ann_dets)
    cocoEval = COCOeval(cocoGt, cocoDt, "bbox")
    cocoEval.evaluate()
    cocoEval.accumulate()
    cocoEval.summarize()

计算mAP结果如下:

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容