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