귀퉁이 서재

컴퓨터 비전 - 5. IoU(Intersection over Union) 본문

딥러닝 컴퓨터 비전

컴퓨터 비전 - 5. IoU(Intersection over Union)

Baek Kyun Shin 2022. 4. 28. 00:41

※ 이 글은 권철민 님의 딥러닝 컴퓨터 비전 완벽 가이드 강의를 바탕으로 작성했습니다.

지난 글에선 슬라이딩 윈도우 방식과 영역 추정 기법을 알아봤습니다. 이번 글에서는 영역 추정의 성능을 측정하는 지표인 IoU에 대해 알아보겠습니다.

이 글에서 사용한 코드: https://github.com/BaekKyunShin/Deep-Learning-Computer-Vision/blob/master/preliminary/IoU.ipynb


IoU란?

객체 탐지의 성능을 평가하는 지표로는 IoU가 있습니다. IoU(Intersection over Union)란 모델이 예측한 경계 박스 영역과 실제 영역이 얼마나 정확히 겹치는지 나타내는 지표입니다. 다음 그림을 보시죠.

출처: https://www.pyimagesearch.com/2016/11/07/intersection-over-union-iou-for-object-detection/

초록색이 실제 경계 박스이고, 빨간색은 예측한 경계 박스입니다.

객체 위치를 표시하는 네모 박스를 경계 박스(Bounding Box)라고 합니다. 자세한 내용이 궁금하신 분은 컴퓨터 비전 - 2. 객체 탐지(Object Detection) 개요를 참고하세요.

실제 경계 박스와 예측한 경계 박스 사이에 약간 차이가 있죠? 예측한 경계 박스가 실제 경계 박스와 정확히 겹칠수록 객체 탐지 성능이 좋습니다. 얼마나 정확히 겹치는지를 나타내는 지표가 IoU이고요. IoU를 계산하는 공식은 다음과 같습니다.

IoU = 경계 박스의 교집합 영역 넓이 / 경계 박스의 합집합 영역 넓이

출처: https://www.pyimagesearch.com/2016/11/07/intersection-over-union-iou-for-object-detection/

IoU 값은 0~1 사이 값을 갖습니다. 1에 가까울수록 성능이 좋은 겁니다. 다음 그림을 보시죠.

출처: https://www.pyimagesearch.com/2016/11/07/intersection-over-union-iou-for-object-detection/

실제 경계 박스와 예측 경계 박스가 정확히 일치할수록 IoU가 1에 가까워집니다. IoU는 간단한 개념이므로 이론은 여기까지 알아보고, 바로 실습으로 들어가 봅시다.


IoU 구하기 실습

실습은 구글 Colab 기반으로 설명합니다.

IoU를 계산하는 실습을 해보겠습니다. 바로 이전 게시글인 컴퓨터 비전 - 4. 슬라이딩 윈도우(Sliding Window)와 영역 추정(Region Proposal)에서 selectivesearch 패키지를 설치하고, 오드리햅번 사진을 다운로드했습니다. 우선 이 두 가지를 똑같이 실행하세요.

1. IoU 계산 함수 정의

먼저, IoU를 계산하는 함수를 생성해보겠습니다. 입력인자로는 예측 경계 박스 좌표와 실제 경계 박스 좌표를 받습니다. 이때 인자로 받는 경계 박스 좌표 순서는 (좌상단 x 좌표, 좌상단 y 좌표, 우하단 x 좌표, 우하단 y 좌표)입니다.

import numpy as np 

def compute_iou(pred_box, gt_box):
    # 교집합 좌표 계산 ---①
    x1 = np.maximum(pred_box[0], gt_box[0])
    y1 = np.maximum(pred_box[1], gt_box[1])
    x2 = np.minimum(pred_box[2], gt_box[2])
    y2 = np.minimum(pred_box[3], gt_box[3])
    # 교집합 영역 넓이 ---②
    intersection = np.maximum(x2 - x1, 0) * np.maximum(y2 - y1, 0)
    # 예측 경계 박스 넓이 ---③
    pred_box_area = (pred_box[2] - pred_box[0]) * (pred_box[3] - pred_box[1])
    # 실제 경계 박스 넓이 ---④
    gt_box_area = (gt_box[2] - gt_box[0]) * (gt_box[3] - gt_box[1])
    # 합집합 영역 넓이 ---⑤
    union = pred_box_area + gt_box_area - intersection
    
    iou = intersection / union # IOU 계산
    return iou

매개변수 pred_box와 gt_box는 각각 예측한 후보 경계 박스, 실제 경계 박스의 좌표를 뜻합니다. 이때 총 네 가지 좌표를 튜플 형태로 전달하면 됩니다. 정확하게는 좌상단 x, y 좌표 그리고 우하단 x, y 좌표입니다.

코드 ①은 교집합 영역의 좌표를 계산합니다. 조금 헷갈려 보이지만 다음 그림을 보면 쉽게 이해가 갈 겁니다.

참고로, 왼쪽에서 오른쪽으로 갈수록 x 좌표값이 커지고, 위에서 아래로 갈수록 y 좌표값이 커집니다.

교집합의 좌상단 x 좌표(x1)는 실제 경계 박스의 좌상단 x 좌표(gt_box[0])와 예측 경계 박스의 좌상단 x 좌표(pred_box[0]) 중 더 큰 값입니다. 여기서는 gt_box[0]와 pred_box[0] 중 pred_box[0]가 더 큽니다(왼쪽에서 오른쪽으로 갈수록 x 좌표값이 커지므로). 따라서 더 큰 값인 pred_box[0]를 취합니다. 마찬가지로 교집합의 좌상단 y 좌표(y1)는 실제 경계 박스의 좌상단 y 좌표(gt_box[1])와 예측 경계 박스의 좌상단 y 좌표(pred_box[1]) 중 더 큰 값입니다. 교집합의 우하단 x 좌표(x2)는 실제 경계 박스의 우하단 x 좌표(gt_box[2])와 예측 경계 박스의 우하단 x 좌표(pred_box[2]) 중 더 작은 값입니다. 이런 방식으로 교집합 영역의 좌상단, 우하단 좌표값을 구했습니다. 위 그림에서는 (x1, y1, x2, y2) = (pred_box[0], pred_box[1], gt_box[2], gt_box[3])입니다.

② 다음으로 교집합 영역의 넓이를 구합니다. 가로 세로를 곱하면 되니까 (x2 - x1) * (y2 - y1)을 해주면 됩니다. 단, 음수가 나올 경우에 대비해 코드 ②와 같이 해주었습니다.

코드 ③과 ④는 각각 예측 경계 박스 넓이와 실제 경계 박스 넓이를 구합니다. 이 두 경계 박스의 넓이를 구한 까닭은 합집합 영역의 넓이를 구하기 위해서입니다. 코드 ⑤에서 합집합 영역 넓이를 구합니다. 두 경계 박스의 넓이 합에서 교집합 영역 넓이를 빼면 합집합 영역 넓이가 되죠.

최종적으로 IoU를 구하는 공식(경계 박스의 교집합 영역 넓이 / 경계 박스의 합집합 영역 넓이)으로 IoU를 계산해준 뒤, 그 값을 반환합니다. 조금 복잡해 보이지만 그림을 그려가며 따라해보면 이해하기 쉽습니다.

2. 실제 경계 박스 그리기

IoU 계산 실습을 하기 전에 임시 경계 박스를 임의로 한번 그려보겠습니다. 

import cv2
import matplotlib.pyplot as plt
%matplotlib inline

# 실제 경계 박스의 좌표가 다음과 같다고 가정 
gt_box = [60, 15, 320, 420]

img = cv2.imread('./data/audrey01.jpg')
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

red = (255, 0, 0) # 실제 경계 박스 색상
img_rgb = cv2.rectangle(img_rgb, (gt_box[0], gt_box[1]), (gt_box[2], gt_box[3]), color=red, thickness=2)

plt.figure(figsize=(8, 8))
plt.imshow(img_rgb);

오드리햅번 사진에 임의로 경계 박스를 표시해봤습니다.

3. IoU 구하기

앞서 경계 박스 좌표를 임의로 그려봤습니다. 이를 실제 경계 박스라고 가정합시다. 이번에는 selective_search() 메서드를 활용해 예측 경계 박스를 구한 뒤, IoU를 계산해보겠습니다. 앞서 정의한 compute_iou() 함수로 IoU를 구해보겠습니다.

import selectivesearch

img = cv2.imread('./data/audrey01.jpg')
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
_, regions = selectivesearch.selective_search(img_rgb, scale=100, min_size=2000)

rects = [region['rect'] for region in regions] # 에측 경계 박스 좌표

for index, pred_box in enumerate(rects):
    pred_box = list(pred_box)
    width, height = pred_box[2], pred_box[3] # 너비, 높이
    # 예측 경계 박스의 우상단 좌표 구하기 ---①
    pred_box[2] = pred_box[0] + width
    pred_box[3] = pred_box[1] + height
    
    iou = compute_iou(pred_box, gt_box)
    print('index:', index, "iou:", iou)

     index: 0 iou: 0.06157293686705451
     index: 1 iou: 0.07156308851224105
     index: 2 iou: 0.2033654637255666
     index: 3 iou: 0.04298195631528965
     index: 4 iou: 0.14541310541310543
     index: 5 iou: 0.10112060778727446
     ...생략...

각 예측 경계 박스별로 IoU를 구했습니다.

selective_search()가 반환한 경계 박스 좌표는 (좌상단 x 좌표, 좌상단 y 좌표, 너비, 높이) 순입니다. 그러나 앞서 만든 compute_iou() 함수의 매개변수에는 (좌상단 x 좌표, 좌상단 y 좌표, 우하단 x 좌표, 우하단 y 좌표) 순으로 전달해야 합니다. 그래서 코드 ①처럼 pred_box[2]와 pred_box[3]를 수정해줘야 합니다. 참고로, 초기 pred_box는 예측 경계 박스의 (좌상단 x 좌표, 좌상단 y 좌표, 너비, 높이)를 담고 있습니다. 여기서 너비(pred_box[2])와 높이(pred_box[3])를 코드 ①을 통해 우하단 x좌표, y좌표로 바꾸어준 것입니다.

이제 실제 오드리햅번 사진에서 예측 경계 박스를 그리고, IoU까지 같이 표시해보겠습니다. 예측 경계 박스가 많아서 크기가 10,000보다 큰 예측 경계 박스만 추출해서 그려보겠습니다. 몇몇 코드만 빼면 앞서 다룬 코드와 비슷합니다.

img = cv2.imread('./data/audrey01.jpg')
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
print('img shape:', img.shape)

green = (125, 255, 51) # 예측 경계 박스 색상
# 크기가 10000보다 큰 경계 박스만 추출
rects = [region['rect'] for region in regions if region['size'] > 10000]
gt_box = [60, 15, 320, 420]

img_rgb = cv2.rectangle(img_rgb, (gt_box[0], gt_box[1]), (gt_box[2], gt_box[3]), color=red, thickness=2)

for index, pred_box in enumerate(rects):
    pred_box = list(pred_box)
    width, height = pred_box[2], pred_box[3] # 너비, 높이
    # 예측 경계 박스의 우상단 좌표 구하기
    pred_box[2] = pred_box[0] + width
    pred_box[3] = pred_box[1] + height
    
    iou = compute_iou(pred_box, gt_box)
    
    if iou > 0.5: # IoU가 0.5보다 큰 경계 박스만 표시 ---①
        print('인덱스:', index, "IoU 값:", iou, '예측 경계 박스 좌표:',(pred_box[0], pred_box[1], pred_box[2], pred_box[3]) )
        cv2.rectangle(img_rgb, (pred_box[0], pred_box[1]), (pred_box[2], pred_box[3]), color=green, thickness=1)
        text = "{}: {:.1f}".format(index, iou)
        cv2.putText(img_rgb, text, (pred_box[0]+ 100, pred_box[1]+10), cv2.FONT_HERSHEY_SIMPLEX, 0.4, color=green, thickness=1)
    
plt.figure(figsize=(12, 12))
plt.imshow(img_rgb);

     img shape: (450, 375, 3)
     인덱스: 3 IoU 값: 0.5184766640298338 예측 경계 박스 좌표: (72, 171, 324, 393)
     인덱스: 7 IoU 값: 0.5409250175192712 예측 경계 박스 좌표: (72, 171, 326, 449)
     인덱스: 17 IoU 값: 0.5490037131949166 예측 경계 박스 좌표: (0, 97, 374, 449)
     인덱스: 20 IoU 값: 0.6341234282410753 예측 경계 박스 좌표: (0, 0, 374, 444)
     인덱스: 21 IoU 값: 0.6270619201314865 예측 경계 박스 좌표: (0, 0, 374, 449)
     인덱스: 22 IoU 값: 0.6270619201314865 예측 경계 박스 좌표: (0, 0, 374, 449)
     인덱스: 23 IoU 값: 0.6270619201314865 예측 경계 박스 좌표: (0, 0, 374, 449)

빨간색 네모는 실제 경계 박스이고, 초록색 네모는 예측 경계 박스입니다.

예측 경계 박스가 겹쳐서 IoU 값도 겹쳐 보이네요. 경계 박스가 많다 보니 코드 ①을 통해 IoU가 0.5보다 큰 경계 박스만 그렸습니다. cv2.rectangle() 메서드로 화면에 경계 박스를 그리고, cv2.putText() 메서드로 해당 경계 박스 위에 IoU 값을 표시했습니다.

지금까지 IoU의 개념을 알아보고, IoU를 계산해보는 실습을 해봤습니다.

0 Comments
댓글쓰기 폼