Object Detection 모델의 결과(bbox)를 바탕으로 IoU 계산을 통한 성능(mAP) 도출 방법

성능 측정 방법 개요

  • 예측데이터를 정답데이터와 비교해 IoU 값을 바탕으로 옳바르게 객체를 탐지했는지 판단함
  • 각 클래스별 탐지결과를 종합하여 mAP (mean Average Precision) 성능을 도출함

데이터셋

  • 정답데이터 (Ground Truth)
  • 예측데이터 (Prediction)
  • 객체 탐지 모델에서 데이터는 각 이미지의 바운딩박스가 다음과 같은 형태로 저장됨
    • [ class_id, center_x, center_y, width, height, confidence ]

IoU 계산

  • 예측데이터의 바운딩박스와 정답데이터의 바운딩 박스의 겹치는 부분의 비율(IoU:Intersection over Union)
  • IoU 값을 이용해 예측된 바운딩박스가 정답인지 아닌지 판단함

성능지표

  • Accuracy, Precision, Recall, AP, F1-score 등 다양한 성능지표가 존재하며, 대부분 객체 탐지모델에서 성능지표로 AP를 이용한 mAP를 사용함

성능 측정

1. 데이터셋 준비

성능측정에 사용될 예측데이터와 정답데이터, IoU 임계값, 클래스 등을 정의함

# 클래스 정의
classes = {"0" : "person", "1" : "vehicle", "2" : "rocks", "3" : "vail", "4" : "tractor", "5" : "pole"}
# GT 및 Prediction 데이터셋 경로
dataset_path = "./data/dataset_v2/testset/"
pred_path = "./data/dataset_v2/predictions/yolov8n_mobilus_v2_1280.pt_pred0/"
result_path = "./results/"
# iou 임계값 및 이미지해상도 정의
iou_threshold = 0.5
w, h = 3840, 2160

2. 바운딩 박스 생성 함수

GT 및 Prediction 데이터셋의 각 클래스별 상대좌표를 이용해 바운딩박스 생성

# box : (centerX, centerY, width, height)
# 상대좌표의 절대좌표 변환
def convertToAbsoluteValues(size, box):
    xIn = round(((2 * float(box[0]) - float(box[2])) * size[0] / 2))
    yIn = round(((2 * float(box[1]) - float(box[3])) * size[1] / 2))
    xEnd = xIn + round(float(box[2]) * size[0])
    yEnd = yIn + round(float(box[3]) * size[1])
    # 바운딩박스 사이즈가 이미지 사이즈를 초과하는 경우
    if xIn < 0:
        xIn = 0
    if yIn < 0:
        yIn = 0
    if xEnd >= size[0]:
        xEnd = size[0] - 1
    if yEnd >= size[1]:
        yEnd = size[1] - 1
    return (xIn, yIn, xEnd, yEnd)
def makeLabel(label_path):
    labels = []
    for file in os.listdir(label_path):
        filename = os.path.splitext(file)[0]
        if file.endswith('.txt'):
            with open(os.path.join(label_path, file)) as f:
                labelinfos = f.readlines()
            for labelinfo in labelinfos:
                label_values = labelinfo.strip().split()
                if len(label_values) == 6:
                    label, xn, yn, wn, hn, conf = map(float, label_values)
                elif len(label_values) == 5:
                    label, xn, yn, wn, hn = map(float, label_values)
                    conf = 1.0
                else:
                    print("### Label Format Error!")
                    return
                x1, y1, x2, y2 = convertToAbsoluteValues((w, h), (xn, yn, wn, hn))
                boxinfo = [filename, int(label), conf, (x1, y1, x2, y2)]
                # if label not in classes:
                #     classes.append(label)
                labels.append(boxinfo)
    return labels
def boundingBoxes(predPath, gtPath):
    # prediction boxes
    detections = makeLabel(predPath)
    # groundtruth boxes
    groundtruths = makeLabel(gtPath)
    return detections, groundtruths
detections, groundtruths = boundingBoxes(pred_path, dataset_path)
print(detections[0])
print(groundtruths[0])

3. 바운딩박스 비교 및 IoU 계산 함수

def getArea(box):
    return (box[2] - box[0] + 1) * (box[3] - box[1] + 1)
def getUnionAreas(boxA, boxB, interArea=None):
    area_A = getArea(boxA)
    area_B = getArea(boxB)
    if interArea is None:
        interArea = getIntersectionArea(boxA, boxB)
    return float(area_A + area_B - interArea)
def getIntersectionArea(boxA, boxB):
    xA = max(boxA[0], boxB[0])
    yA = max(boxA[1], boxB[1])
    xB = min(boxA[2], boxB[2])
    yB = min(boxA[3], boxB[3])
    # intersection area
    return (xB - xA + 1) * (yB - yA + 1)
# boxA = (Ax1,Ay1,Ax2,Ay2)
# boxB = (Bx1,By1,Bx2,By2)
def boxesIntersect(boxA, boxB):
    if boxA[0] > boxB[2]:
        return False  # boxA is right of boxB
    if boxB[0] > boxA[2]:
        return False  # boxA is left of boxB
    if boxA[3] < boxB[1]:
        return False  # boxA is above boxB
    if boxA[1] > boxB[3]:
        return False  # boxA is below boxB
    return True
def iou(boxA, boxB):
    # if boxes dont intersect
    if boxesIntersect(boxA, boxB) is False:
        return 0
    interArea = getIntersectionArea(boxA, boxB)
    union = getUnionAreas(boxA, boxB, interArea=interArea)
    # intersection over union
    result = interArea / union
    assert result >= 0
    return result

4. AP(Average Precision) 계산

클래스별 Recall-Precision 그래프를 이용해 AP를 구하는 함수 YOLO에서 AP는 11점 보간법을 이용해 구함

def calculateAveragePrecision(rec, prec):
    mrec = [0] + [e for e in rec] + [1]
    mpre = [0] + [e for e in prec] + [0]
    for i in range(len(mpre)-1, 0, -1):
        mpre[i-1] = max(mpre[i-1], mpre[i])
    ii = []
    for i in range(len(mrec)-1):
        if mrec[1:][i] != mrec[0:-1][i]:
            ii.append(i+1)
    ap = 0
    for i in ii:
        ap = ap + np.sum((mrec[i] - mrec[i-1]) * mpre[i])
    return [ap, mpre[0:len(mpre)-1], mrec[0:len(mpre)-1], ii]
def ElevenPointInterpolatedAP(rec, prec):
    mrec = [e for e in rec]
    mpre = [e for e in prec]
    recallValues = np.linspace(0, 1, 11)
    recallValues = list(recallValues[::-1])
    rhoInterp, recallValid = [], []
    for r in recallValues:
        argGreaterRecalls = np.argwhere(mrec[:] >= r)
        pmax = 0
        if argGreaterRecalls.size != 0:
            pmax = max(mpre[argGreaterRecalls.min():])
        recallValid.append(r)
        rhoInterp.append(pmax)
    ap = sum(rhoInterp) / 11
    return [ap, rhoInterp, recallValues, None]
def AP(detections, groundtruths, classes, IOUThreshold, method = 'AP'):
    result = []
    for c in classes:
        dects = [d for d in detections if d[1] == c]
        gts = [g for g in groundtruths if g[1] == c]
        npos = len(gts)
        dects = sorted(dects, key = lambda conf : conf[2], reverse=True)
        TP = np.zeros(len(dects))
        FP = np.zeros(len(dects))
        det = Counter(cc[0] for cc in gts)
        # 각 이미지별 ground truth box의 수
        # {99 : 2, 380 : 4, ....}
        # {99 : [0, 0], 380 : [0, 0, 0, 0], ...}
        for key, val in det.items():
            det[key] = np.zeros(val)
        for d in range(len(dects)):
            gt = [gt for gt in gts if gt[0] == dects[d][0]]
            iouMax = 0
            for j in range(len(gt)):
                iou1 = iou(dects[d][3], gt[j][3])
                if iou1 > iouMax:
                    iouMax = iou1
                    jmax = j
            if iouMax >= IOUThreshold:
                if det[dects[d][0]][jmax] == 0:
                    TP[d] = 1
                    det[dects[d][0]][jmax] = 1
                else:
                    FP[d] = 1
            else:
                FP[d] = 1
        acc_FP = np.cumsum(FP)
        acc_TP = np.cumsum(TP)
        # Recall
        rec = acc_TP / npos 
        # Precision
        prec = np.divide(acc_TP, (acc_FP + acc_TP))
        if method == "AP":
            [ap, mpre, mrec, ii] = calculateAveragePrecision(rec, prec)
        else:
            [ap, mpre, mrec, _] = ElevenPointInterpolatedAP(rec, prec)
        r = {
            'class' : c,
            'precision' : prec,
            'recall' : rec,
            'AP' : ap,
            'interpolated precision' : mpre,
            'interpolated recall' : mrec,
            'total positives' : npos,
            'total TP' : np.sum(TP),
            'total FP' : np.sum(FP)
        }
        result.append(r)
    return result
class_ids = list(map(int, classes.keys()))
result_AP = AP(detections, groundtruths, class_ids, IOUThreshold=iou_threshold)
print(result_AP[0]['AP'])

5. 전체 클래스에 대한 mAP 도출

전체 클래스에 대한 각 클래스 AP의 평균

# calc mAP
def mAP(AP):
    ap = 0
    for r in AP:
        ap += r['AP']
    mAP = ap / len(AP)
    return mAP
for r in result_AP:
    print("{:^8} AP : {}".format(classes[str(r['class'])], round(float(r['AP']), 4)))
print("---------------------------")
print(f"mAP{int(iou_threshold * 100)} : {round(mAP(result_AP), 4)}")
>>
 person  AP : 0.9405
vehicle  AP : 0.9883
 rocks   AP : 1.0
  vail   AP : 0.9938
tractor  AP : 1.0
  pole   AP : 0.979
---------------------------
mAP50 : 0.9836

참고