AI/Object Detection

[Python] mAP(mean Average Precision) 예시 및 코드

슈퍼짱짱 2022. 6. 8. 18:10
반응형

지난 시간에 Object Detection에서 사용하는 여러 용어들을 정리해 보았다.

그 중 mAP도 있었는데, 

오늘은 그 mAP를 계산하는 코드에 대해 설명해보려 한다.

실제 yolo v5 저자가 올려놓은 코드로 설명한다.

 

2022.03.31 - [AI/Object Detection] - Object Detection이란? Object Detection 용어정리

 

Object Detection이란? Object Detection 용어정리

Object Detection이란? Object Detection은 말 그대로 물체를 검출하는 문제이다. 딥러닝으로 이미지 관련 무언가를 한다면 대체로 다음과 같다. 1. Classification 가장 기본이 되는 문제이다. 이미지가 주어

leedakyeong.tistory.com

 

yolo v5 저자가 직접 올려놓은 코드는 여기 있고, 

이 중 utils 폴더에 metrics.py 파일에 ap_per_class() 라는 이름으로 구현되어 있다.

실제 사용은 val.py에서 한다.


전체 코드는 다음과 같다.

(plot은 주석처리했다.)

 

여기서 구현된 코드는 iou가 0.5기준으로 TP라 했을때, 0.55를 기준으로, 0.6을 기준으로, ... 0.95를 기준으로 TP라 했을 때 AP의 결과를 계산해준다. 즉 최종적으로 10개의 AP가 계산된다. 

 

import numpy as np
import os
import torch

def compute_ap(recall, precision):
    """ Compute the average precision, given the recall and precision curves
    # Arguments
        recall:    The recall curve (list)
        precision: The precision curve (list)
    # Returns
        Average precision, precision curve, recall curve
    """

    # Append sentinel values to beginning and end
    mrec = np.concatenate(([0.0], recall, [1.0]))
    mpre = np.concatenate(([1.0], precision, [0.0]))

    # Compute the precision envelope
    mpre = np.flip(np.maximum.accumulate(np.flip(mpre)))

    # Integrate area under curve
    method = 'interp'  # methods: 'continuous', 'interp'
    if method == 'interp':
        x = np.linspace(0, 1, 101)  # 101-point interp (COCO)
        ap = np.trapz(np.interp(x, mrec, mpre), x)  # integrate
    else:  # 'continuous'
        i = np.where(mrec[1:] != mrec[:-1])[0]  # points where x axis (recall) changes
        ap = np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1])  # area under curve

    return ap, mpre, mrec

def smooth(y, f=0.05):
    # Box filter of fraction f
    nf = round(len(y) * f * 2) // 2 + 1  # number of filter elements (must be odd)
    p = np.ones(nf // 2)  # ones padding
    yp = np.concatenate((p * y[0], y, p * y[-1]), 0)  # y padded
    return np.convolve(yp, np.ones(nf) / nf, mode='valid')  # y-smoothed

def ap_per_class(tp, conf, pred_cls, target_cls, plot=False, save_dir='.', names=(), eps=1e-16):
    """ Compute the average precision, given the recall and precision curves.
    Source: https://github.com/rafaelpadilla/Object-Detection-Metrics.
    # Arguments
        tp:  True positives (nparray, nx1 or nx10).
        conf:  Objectness value from 0-1 (nparray).
        pred_cls:  Predicted object classes (nparray).
        target_cls:  True object classes (nparray).
        plot:  Plot precision-recall curve at mAP@0.5
        save_dir:  Plot save directory
    # Returns
        The average precision as computed in py-faster-rcnn.
    """

    # Sort by objectness
    i = np.argsort(-conf)
    tp, conf, pred_cls = tp[i], conf[i], pred_cls[i]

    # Find unique classes
    unique_classes, nt = np.unique(target_cls, return_counts=True)
    nc = unique_classes.shape[0]  # number of classes, number of detections

    # Create Precision-Recall curve and compute AP for each class
    px, py = np.linspace(0, 1, 1000), []  # for plotting
    ap, p, r = np.zeros((nc, tp.shape[1])), np.zeros((nc, 1000)), np.zeros((nc, 1000))
    for ci, c in enumerate(unique_classes):
        i = pred_cls == c
        n_l = nt[ci]  # number of labels
        n_p = i.sum()  # number of predictions
        if n_p == 0 or n_l == 0:
            continue

        # Accumulate FPs and TPs
        fpc = (1 - tp[i]).cumsum(0)
        tpc = tp[i].cumsum(0)

        # Recall
        recall = tpc / (n_l + eps)  # recall curve
        r[ci] = np.interp(-px, -conf[i], recall[:, 0], left=0)  # negative x, xp because xp decreases

        # Precision
        precision = tpc / (tpc + fpc)  # precision curve
        p[ci] = np.interp(-px, -conf[i], precision[:, 0], left=1)  # p at pr_score

        # AP from recall-precision curve
        for j in range(tp.shape[1]):
            ap[ci, j], mpre, mrec = compute_ap(recall[:, j], precision[:, j])
            if plot and j == 0:
                py.append(np.interp(px, mrec, mpre))  # precision at mAP@0.5

    # Compute F1 (harmonic mean of precision and recall)
    f1 = 2 * p * r / (p + r + eps)
    names = [v for k, v in names.items() if k in unique_classes]  # list: only classes that have data
    names = dict(enumerate(names))  # to dict
    # if plot:
    #     plot_pr_curve(px, py, ap, Path(save_dir) / 'PR_curve.png', names)
    #     plot_mc_curve(px, f1, Path(save_dir) / 'F1_curve.png', names, ylabel='F1')
    #     plot_mc_curve(px, p, Path(save_dir) / 'P_curve.png', names, ylabel='Precision')
    #     plot_mc_curve(px, r, Path(save_dir) / 'R_curve.png', names, ylabel='Recall')

    i = smooth(f1.mean(0), 0.1).argmax()  # max F1 index
    p, r, f1 = p[:, i], r[:, i], f1[:, i]
    tp = (r * nt).round()  # true positives
    fp = (tp / (p + eps) - tp).round()  # false positives
    return tp, fp, p, r, f1, ap, unique_classes.astype('int32')

 


예를 들어, 다음과 같은 예측 결과가 있다고 하자.

 

https://www.youtube.com/watch?v=FppOzcDvaDI

 

총 3장의 이미지가 있고, Class는 Dog 하나만 있다고 하자.

Ground Truth는 3개이고(초록색 박스), 모델이 예측한 결과(빨간색 박스)는 7개가 있다고 하자.

 

이를 표로 나타내면 다음과 같다.

confidence score가 높은 순서대로 sorting했다. 

 

 

ap_per_class() 에 parameter로 넣어 줄 변수들을 정의한다.

 

tp = np.array([[False, False, False, False, False, False, False, False, False, False], # 0
              [False, False, False, False, False, False, False, False, False, False], # 0.4
              [True, True, True, True, True, True, True, True, True, True], # 0.95
              [True, True, True, True, True, True, False, False, False, False], # 0.79
              [False, False, False, False, False, False, False, False, False, False], # 0
              [False, False, False, False, False, False, False, False, False, False], # 0
              [True, True, True, False, False, False, False, False, False, False]]) # 0.61
conf = np.array([0.3, 0.6, 0.7, 0.5, 0.2, 0.8, 0.9])
pred_cls = np.array([1,1,1,1,1,1,1])
target_cls = np.array([1,1,1])

names = {0:'cat',
        1:"dog"}

 

tp는 각 iou([0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95])에 대한 TP table이다.

예를 들어, 첫 번째 conf 0.3 박스는 GT와 iou가 0이다. 따라서 tp table 첫 번째 줄은 모두 False이다.

두 번째로 conf 0.6 박스도 GT와 iou가 0.5보다 작으므로 모든 값이 False이다.

세 번째로 conf 0.7 박스는 GT와 iou가 0.95이므로 모든 값이 True이다.

네 번째로 conf 0.5 박스는 GT와 iou가 0.79이므로 iou 0.5부터 0.8전까지 값 즉, 첫 6개 값이 True이다. 

...

 

이를 그림으로 표현하면 다음과 같다.

 

 

실제 예측값과 GT로 tp table 만드는 방법은 아래 소개하겠다.

 

conf는 confidence score를 의미하고, pred_cls는 prediction한 class, target_cls는 target class를 의미한다.

모두 1로 통일해주었다.

names도 임의로 0 : 'cat'을 추가했다. 

 

이 변수들을 위에서 선언한 ap_per_class() 에 넣어주면 다음과 같은 결과를 얻을 수 있다.

 

tp, fp, p, r, f1, ap, ap_class = ap_per_class(tp, conf, pred_cls, target_cls, names = names)

 

 

tp는 3개가 맞는데, fp는 4개인데 2개로 결과가 나왔다. 

이는 tp가 3개이고 fp가 2개일 때 즉, confidence score threshold를 0.5로 하면 최적의 성능이 나온다는 걸 의미한다.

 

 

p, r, f1은 최고 성능 precision, recall, f1 score를 의미하고

ap는 위에서 설명한대로 iou 0.5, 0.55, ..., 0.95에 따른 average precision을 의미한다.

 


이제 이 코드를 하나하나 뜯어보겠다.

 

먼저 confidence score에 따라 내림차순 정렬한다.

 

본 예시에서는 class가 dog 한 개지만, 보통은 여러개 일 것이므로 unique한 class에 대해서도 정의해준다.

 

 

다음으로, 본격적으로 precision과 recall을 계산하기 위한 준비를 해준다.

ap가 최종적으로 iou에 따른 10개 ap를 담을 변수이고,

p와 r은 iou 0.5를 기준으로 recall, precision curve를 그리기 위해 계산된 recall, precision을 1000개로 interpolation한 결과를 담을 변수이다.

 

 

recall과 precision을 계산하기 위해 각 iou에 대해 tp와 fp 개수를 cumsum한다. 

 

 

recall은 TP / (TP + FN) 이고, precision은 TP / (TP + FP)이다.

confidence score를 기준으로 내림차순 정렬하여

conf가 가장 높은 것 부터 TP와 FP, FN의 개수를 계산해 recall과 precision을 계산해주므로 Cumsum으로 TP와 FP를 계산했다.

 

그리고 recall을 계산하면 다음과 같다.

미리 iou에 따른 TP를 tpc 변수에 계산해 놓았으므로 recall을 한 번에 계산할 수 있다.

n_l은 TP+FN이며, eps는 혹시 n_l이 0일 것을 대비하여 아주 작은 값을 더해준다.

 

n_l은 위에서 계산한 값으로, 3이다. 즉, TP(잘 맞춘 것)가 3개이고 FN(검출되어야 하는데 검출되지 않은 물체)는 0개이다.

 

r은 iou 0.5에 대해 recall을 1000개로 interpolation한 값이다. 

이는 필요하다면 plot 그릴 때도 사용된다.

 

 

다음으로 precision이다.

p 역시 iou 0.5에 대한 precision을 1000개로 interpolation한 결과이다.

 

 

이를가지고 iou 10개에 대한 ap를 계산한다.

 

 

iou 0.5에 대한 f1도 계산한다.

위에서 미리 계산한 precision과 recall을 1000개로 interpolation한 결과를 가지고 

모든 point에 대한 f1 score를 계산한다.

 

 

위에서 계산한 f1이 max인 point의 precision과 recall, f1 score도 계산한다.

즉, 이 수치들이 최적의 성능인 것이다.

 

 

그 때의 TP 갯수와 FP 개수도 계산한다.

 

 

위에서도 설명했지만

TP가 3개이고 FP가 2개 일 때, 즉 confidence score가 0.5일 때 최적의 성능을 낼 수 있다.

이는 confidence score threshold를 몇으로 할 지에 대한 힌트도 될 수 있다. 

 


이제 위에서는 수기로 입력했던 tp table을 실제 예측값과 Ground Truth로 계산하는 방법을 소개한다.

val.py에 process_batch() 에 구현되어있다.

 

def box_area(box):
    # box = xyxy(4,n)
    return (box[2] - box[0]) * (box[3] - box[1])

def box_iou(box1, box2):
    # https://github.com/pytorch/vision/blob/master/torchvision/ops/boxes.py
    """
    Return intersection-over-union (Jaccard index) of boxes.
    Both sets of boxes are expected to be in (x1, y1, x2, y2) format.
    Arguments:
        box1 (Tensor[N, 4])
        box2 (Tensor[M, 4])
    Returns:
        iou (Tensor[N, M]): the NxM matrix containing the pairwise
            IoU values for every element in boxes1 and boxes2
    """

    # inter(N,M) = (rb(N,M,2) - lt(N,M,2)).clamp(0).prod(2)
    (a1, a2), (b1, b2) = box1[:, None].chunk(2, 2), box2.chunk(2, 1)
    inter = (torch.min(a2, b2) - torch.max(a1, b1)).clamp(0).prod(2)

    # IoU = inter / (area1 + area2 - inter)
    return inter / (box_area(box1.T)[:, None] + box_area(box2.T) - inter)


def process_batch(detections, labels, iouv):
    """
    Return correct predictions matrix. Both sets of boxes are in (x1, y1, x2, y2) format.
    Arguments:
        detections (Array[N, 6]), x1, y1, x2, y2, conf, class
        labels (Array[M, 5]), class, x1, y1, x2, y2
    Returns:
        correct (Array[N, 10]), for 10 IoU levels
    """
    correct = torch.zeros(detections.shape[0], iouv.shape[0], dtype=torch.bool, device=iouv.device) # same dim detections # [220,6]
    iou = box_iou(labels[:, 1:], detections[:, :4]) # [1,220]
    x = torch.where((iou >= iouv[0]) & (labels[:, 0:1] == detections[:, 5]))  # IoU above threshold and classes match
    if x[0].shape[0]:
        matches = torch.cat((torch.stack(x, 1), iou[x[0], x[1]][:, None]), 1).cpu().numpy()  # [label, detection, iou]
        if x[0].shape[0] > 1:
            matches = matches[matches[:, 2].argsort()[::-1]]
            matches = matches[np.unique(matches[:, 1], return_index=True)[1]]
            # matches = matches[matches[:, 2].argsort()[::-1]]
            matches = matches[np.unique(matches[:, 0], return_index=True)[1]]
        matches = torch.from_numpy(matches).to(iouv.device)
        correct[matches[:, 1].long()] = matches[:, 2:3] >= iouv
    return correct, iou

 

예를 들어, 1개의 Ground Truth에 대해 3개의 예측 BBox가 있다고 하자.

 

# BBox
detections = torch.tensor([[300,100,400,600,0.9,1], # x, y, x, y, conf, cls
                           [200,300,500,500,0.9,1], # x, y, x, y, conf, cls
                          [200,300,500,500,0.9,2]])
# GT
labels = torch.tensor([[1,200,200,500,500]]) # cls, x, y, x, y
iouv = torch.tensor(np.array([0.50000, 0.55000, 0.60000, 0.65000, 0.70000, 0.75000, 0.80000, 0.85000, 0.90000, 0.95000]))

 

xywh형태가 아닌 xyxy의 형태로 입력한다.

예측한 box에 대해서는 [x, y, x, y, conf, class] 형태로 입력하고, Ground Truth는 [class, x, y, x, y]의 형태로 입력한다.

iouv는 [0.5, 0.55, ..., 095]를 입력한다. 

 

 

1, 2번 박스는 class를 맞췄고, 마지막 box는 class를 맞추지 못했으므로 마지막 box는 iou와 상관없이 모든 값이 False이다.

첫 번째 박스는 class는 맞췄지만 GT와의 iou가 0.2727로 0.5보다 작아 모든 값이 False이다.

두 번째 박스는 class도 맞췄고 iou도 0.6667로 0.5~0.7사이인 첫 4개의 값이 True이다.

 

반응형