Object Detection을 할 때 이미지 속 특정 물체가 있는 위치를 네모상자(bbox, bounding box)로 표시한다. 즉, 단순히 classification를 하기 위해서는 이미지와 해당 이미지의 class로 구성된 데이터셋을 이용하지만 Object Detection 문제에서는 이미지와 이미지 속 물체의 class, 그리고 bbox로 구성된 데이터셋을 이용한다. 따라서 Object Detection 문제에서 모델의 성능을 평가할 때는 class가 맞는지도 중요하지만, 위치정보가 들어간 bbox가 잘 예측되었는지도 매우 중요하다. class의 경우 Yes or No로 쉽게 정확도를 측정할 수 있지만, bbox의 경우 어떻게 할까? 코드


IoU(Intersection over Union)


class와 다르게 bbox는 좌표로 표현이 되고, 좌표는 연속형 데이터이기 때문에 ‘Yes or No’문제로 측정할 수 없다. 따라서 정답 bbox와 예측된 bbox의 넓이를 이용해 정확도를 측정하는 도구가 IoU(Intersection over Union)이다.


IoU를 그대로 해석하면 ‘합집합의 교집합’인데, 이름에서도 알 수 있듯이 두 상자가 있을 때(정답bbox, 예측bbox) 두 상자의 합집합에서 두 상자의 교집합이 차지하는 넓이를 말한다.

아래는 IoU를 표현한 사진이다.


사진1. IoU(Intersection over Union)
출처 : https://en.wikipedia.org/wiki/Jaccard_index


Object Detection문제에서는 IoU의 값이 1에 가까울수록 좋은 모델이기 때문에, 예측 bbox와 정답 bbox의 IoU값이 높은 방향으로 학습을 한다. 물론 IoU의 값이 높아도 class를 엉터리로 예측한다면 좋지 않은 모델이지만, 최소한 해당 위치에 ‘객체’가 있다는 것을 예측한 것이기 때문에 반드시 높을수록 좋다.


IoU를 이용해 Object Detection문제에서 객체 위치를 예측한다는 것은 이해했는데, 그렇다면 어느정도의 넓이가 적당할까?


IoU 측정


DeepLearning 모델을 학습할 때는 다양한 하이퍼파라미터를 사용하는 것처럼, IoU의 경우에도 넓이가 곧 하이퍼파미터의 값이 된다.


사진2. Ratio of IoU
출처 : https://en.wikipedia.org/wiki/Jaccard_index


모델을 평가할 때는 적당한 Threshold값을 설정하여 IoU의 수치를 계산한다. 실제로 Object Detection 대회에서는 다양한 IoU값에 대해 평균을 내 모델의 성능을 평가한다. 보통 mAP@0.5:0.05:0.95 이런식으로 표현해서 0.5부터 0.95까지의 Threshold값에 대해 적용하고 평균을 구해 평가지표로 사용한다.

객체의 위치를 구해야하는 대부분의 분야(Localization, Object Tracking 등)에서 IoU방식을 이용해 평가를 한다.


IoU 구하기


지금부터는 IoU의 값을 구하는 방법에 대해 알아보자.


사진3. Example of IoU
출처 : https://www.youtube.com/watch?v=XXYG5ZWtjj0&list=PLhhyoLH6IjfxeoooqP9rhU3HJIAVAJ3Vz&index=44


IoU의 값을 알기 위해서는 먼저 Computer Vision에서 이미지를 다룰 때의 특징을 이해해야한다. 예를 들어 Python으로 이미지를 다루기 위해 이미지를 불러오게 되면, 각 pixel값들이 행렬로 나타난다. 이때 이미지의 사이즈가 100x100이라고 하면 시작점(왼쪽 위)이 (0, 0)이 되고 이미지의 끝점(오른쪽 아래)가 (99, 99)가 된다. 즉, x좌표의 경우 오른쪽으로 갈수록 증가하는 반면, y의 경우 아래로 내려갈수록 증가한다.


사진4. Coordinate System of Image
출처 : http://www.ezgraphics.org/UserGuide/CoordinateSystem


위 내용을 이해했다면 본격적으로 IoU의 값을 구해보자.


우선 Object Detection에서 사용하는 데이터셋을 생각해보면 image와 bbox, 그리고 각 bbox에 대한 class가 주어진다. bbox의 경우 크게 3가지 종류가 있는데, 좌상단 좌표와 가로, 세로 길이가 주어지는 경우($[x1, y1, w, h])$와 좌상단과 우하단의 좌표가 주어지는 경우($[x1, y1, x2, y2]$), 마지막으로 중심 좌표와 가로, 세로 길이가 주어지는 경우($[x, y, w, h]$)가 있다. 3가지 경우 모두 가능하도록 하는 코드는 마지막에 작성을 하고, 지금은 이해를 위해 2번째 경우로만 설명을 하고자 한다.


아래는 각 데이터셋 종류별 bbox의 format이다.


사진5. Format of bbox
출처 : https://www.kaggle.com/mattbast/object-detection-tensorflow-end-to-end


1. 각 bbox의 넓이 구하기

bbox의 넓이를 구하는 방법은 매우 간단하다. 주어진 좌표를 이용해 가로와 세로의 길이를 구해주고 곱해주면 된다. 심지어 coco와 yolo 데이터셋은 가로와 세로 길이가 그대로 제공된다.

# 좌상단, 우하단 꼭지점 좌표
# bbox1 = [x1, y1, x2, y2] (Target, 정답)
# bbox2 = [x1, y1, x2, y2] (Predicted, 예측)

bbox1_area = abs((bbox1[2] - bbox1[0]) * (bbox1[3] - bbox1[1]))
bbox2_area = abs((bbox2[2] - bbox2[0]) * (bbox2[3] - bbox2[1]))


2. Intersection 넓이 구하기

겹치는 구간의 대각의 꼭지점 좌표를 이용하면 겹치는 구간(Intersection)의 넓이를 구할 수 있다. 두 bbox(정답, 예측)의 좌표를 구하기 위해서는 두 bbox의 각 좌상단, 우하단 좌표를 이용하면 쉽게 구할 수 있다.
아래는 두 개의 bbox의 Intersection이 가능 경우를 나타낸 사진이다.


사진6. Shape of Intersection Area
출처 : https://www.kaggle.com/mattbast/object-detection-tensorflow-end-to-end


  • Intersection 구간의 좌상단 좌표(A) : 각 bbox의 좌상단 좌표 중 큰 값(max)
  • Intersection 구간의 우하단 좌표(B) : 각 bbox의 우하단 좌표 중 작은 값(min)


따라서 코드를 작성하면 다음과 같다.

y좌표의 경우 아래로 갈수록 커진다는 것을 명심하자!

# intersection 좌표
X1 = max(bbox1[0], bbox2[0]) # 좌상단 x좌표
Y1 = max(bbox1[1], bbox2[1]) # 좌상단 y좌표
X2 = min(bbox1[2], bbox2[2]) # 우하단 x좌표
Y2 = min(bbox1[3], bbox2[3]) # 우하단 y좌표

# intersection 넓이 
intersection = (X2 - X1) * (Y2 - Y1)


위 방법을 이용하면 겹치는 구간의 좌상단, 우하단 좌표를 구할 수 있다. 하지만 우리는 한 가지 경우에 대해서는 다루지 않았다. 겹치지 않는 경우인데, 만약 저 식을 그대로 사용한다면 (음수 X 음수)와 같은 이상한 결과가 나올 것이다. 과연 두 상자가 겹치지 않았을 경우에는 어떻게 구해야할까? torch 메소드 중 하나인 clamp()를 사용하면 해결할 수 있다.


torch.clamp(min, max)는 최소 또는 최대값을 설정해 해당 범위에 벗어나는 값에 대해서는 설정했던 min, max값으로 대체한다. 따라서 만약 겹치는 부분이 없으면 음수가 발생하기 때문에 min값을 설정해 0이 출력되도록 코드를 수정하면 다음과 같다.

# 겹치지 않는 경우도 추가
# 겹치지 않을 경우 음수값이 발생하고, 두 좌표의 차가 음수가 나오면 0으로 대체
intersection = (X2 - X1).clamp(min=0) * (Y2 - Y1).clamp(min=0)


3. 합집합과 교집합으로 IoU 구하기

최종적으로 두 bbox의 합집합과 교집합으로 IoU를 구하면 다음과 같다.

iou = intersection / (bbox1_area + bbox2_rea - intersection + 1e-6)


  • 합집합을 하는 과정에서 교집합이 한 번 더 계산되었기 때문에 교집합을 분모에서 한 번 뺀다.
  • 분모가 0이 되어 error가 발생하는 것을 방지하기 위해 가장 작은 값 추가


IoU 계산 메소드 생성


앞으로 공부하고자 하는 내용(NMS, mAP 등)에서 IoU가 자주 사용되기 때문에 메소드로 만들어 재활용하고자 한다. 이때 다양한 데이터셋(coco, PASCAL, YOLO)에 모두 사용할 수 있도록 조건을 추가했다. 또한 N개의 객체에 대해서 각각 N개의 bbox가 만들어지기 때문에 이 조건도 추가했다.

def intersection_over_union(boxes_preds, boxes_labels, box_format='corners'):
    # boxes_preds의 shape은 (N, 4), N은 예측한 객체의 개수
    # boxes_labels의 shape은 (N, 4)

    if box_format == 'corners': # YOLO dataset
        preds_x1 = boxes_preds[..., 0:1]
        preds_y1 = boxes_preds[..., 1:2]
        preds_x2 = boxes_preds[..., 2:3]
        preds_y2 = boxes_preds[..., 3:4]
        labels_x1 = boxes_labels[..., 0:1]
        labels_y1 = boxes_labels[..., 1:2]
        labels_x2 = boxes_labels[..., 2:3]
        labels_y2 = boxes_labels[..., 3:4]

    elif box_format == 'midpoint': # VOC-PASCAL dataset
        preds_x1 = bboxes_preds[..., 0:1] - bboxes_preds[..., 2:3] / 2
        preds_y1 = bboxes_preds[..., 1:2] - bboxes_preds[..., 3:4] / 2
        preds_x2 = bboxes_preds[..., 0:1] + bboxes_preds[..., 2:3] / 2
        preds_y2 = bboxes_preds[..., 1:2] + bboxes_preds[..., 3:4] / 2
        labels_x1 = bboxes_labels[..., 0:1] - bboxes_labels[..., 2:3] / 2
        labels_y1 = bboxes_labels[..., 1:2] - bboxes_labels[..., 3:4] / 2
        labels_x2 = bboxes_labels[..., 0:1] + bboxes_labels[..., 2:3] / 2
        labels_y2 = bboxes_labels[..., 1:2] + bboxes_labels[..., 3:4] / 2
        
    else: # COCO dataset
        preds_x1 = boxes_preds[..., 0:1]
        preds_y1 = boxes_preds[..., 1:2]
        preds_x2 = boxes_preds[..., 0:1] + boxes_preds[..., 2:3]
        preds_y2 = boxes_preds[..., 1:2] + boxes_preds[..., 3:4]
        labels_x1 = boxes_labels[..., 0:1]
        labels_y1 = boxes_labels[..., 1:2]
        labels_x2 = boxes_labels[..., 0:1] + boxes_labels[..., 2:3]
        labels_y2 = boxes_labels[..., 1:2] + boxes_labels[..., 3:4]

    # Intersection Area
    x1 = torch.max(preds_x1, labels_x1)
    y1 = torch.max(preds_y1, labels_y1)
    x2 = torch.min(preds_x2, labels_x2)
    y2 = torch.min(preds_y2, labels_y2)
    
    intersection = (x2 - x1).clamp(min=0) * (y2 - y1).clamp(min=0)

    preds_area = abs((preds_x2 - preds_x1) * (preds_y2 - preds_y1))
    labels_area = abs((labels_x2 - labels_x1) * (labels_y2 - labels_y1))
    
    print(f"bbox1 Area : {preds_area.item()} \nbbox2 Area : {labels_area.item()} \nIntersection Area : {intersection.item()}")
    return (intersection / (preds_area + labels_area - intersection + 1e-6)).item()
  • midpoint의 경우 좌상단 좌표는 ‘중심 좌표 - 가로/2’이고 우하단 좌표는 ‘중심 좌표 + 세로/2’
  • 각 상자 넓이를 구할 때 abs를 사용한 이유는 그냥 양수값을 보장하기 위해(사실 필요없음)
  • Slice할 때 : 을 사용하지 않고 을 사용한 이유는 N같이 1일 때도 동작이 가능하도록 하기 위해


IoU(Intersection over Union)에 대해 알아보았다. Object Detection 문제를 다루기 위해서는 반드시 필요한 개념이기 때문에 숙지할 필요가 있다. 또한 앞으로 공부하고자 하는 내용에도 사용되는 개념이기 때문에 메소드로 만들어 재활용하고자 한다. 두루뭉실하게 알고 있을때보다 직접 코드를 작성해서 보니 확실히 이해가 되는 것 같다. 이렇게 객체인식으로 한 걸음 다가갈 수 있게 되었다! 코드


읽어주셔서 감사합니다.(댓글과 수정사항은 언제나 환영입니다!)