[딥러닝] 딥러닝 객체 탐지 모델 R-CNN 시리즈 총정리 (R-CNN, Fast R-CNN, Faster R-CNN)
- 이번 포스팅에서는 딥러닝을 이용한 객체 탐지 모델인 R-CNN 시리즈에 대해 알아보겠습니다.
오래된 기술이지만 컴퓨터 비전 분야의 발전 역사상 의미가 깊은 모델이며, 딥러닝 기반 객체 탐지 모델의 시초이므로 제대로 정리하고 넘어가려고 합니다.
- 객체 탐지란,
이미지 내에서 어떠한 객체의 위치를 파악하는 무척이나 활용도가 높은 기능입니다.
사람의 경우 시각 정보에서 가장 중요하고 빈번하게 사용하는 기능으로,
여러 분야의 자동화 및 자율 동작 로봇을 구현하는 데에 있어 첫번째의 과제이기도 하죠.
일반적으로 의미하는 객체 탐지는, 어떠한 모델 f 에 시각 이미지 x 를 넣으면, 해당 이미지 내의 객체의 위치가 4개의 직사각형 좌표로 나오는 것을 의미하며, 그것을 넘어 객체의 윤곽을 전부 탐지하는 기술은 객체탐지라기보다는 Image Segmentation 이라고 부릅니다.
객체 탐지가 이처럼 좌표를 4개 반환하는 모델로서 발전해온 이유는,
이미지 내에서 객체의 위치를 사각형으로만 나타낼 수 있더라도 충분히 유용하게 사용할 수가 있으며,
이미지 분석이라는 작업은 알고리즘에 따라 차이는 있지만 대부분 굉장히 연산량이 많고 무거운 작업이므로 예측값을 최소화 하기 위해서이기도 합니다.
이런 이유로 최근, 하드웨어의 발전 및 Segment 기술의 발전으로 충분히 빠르게 Segment 를 할 수 있더라도 여전히 객체 탐지 모델은 유용하게 사용되고 주요하게 발전해나가고 있는 추세입니다.
딥러닝이 발전하기 전, 컴퓨터 비전 분야에서 CNN 모델이 생겨나 이미지 분석 영역을 장악하기 전에도 객체 탐지 기술은 수학적, 알고리즘적, 그외 머신러닝적 접근 방법으로 연구되고 있었는데, 이미지 분류 문제에서 CNN 기반 모델들이 기존 기술들을 성능으로 압도하기 시작하며 객체 탐지 영역에서도 이에 대한 연구가 진행되었고,
2013년 Ross Girshick 가 합성곱 모델과, 당시 유망했던 Support Vector Machine(SVM) 을 활용해 객체탐지를 수행하는 모델인 R-CNN 을 발표하여 CNN 기반 객체 탐지 모델의 성능을 증명하였으며,
이후 실행 속도 및 정확도를 개선한 Fast R-CNN, Faster R-CNN 으로 발전하였습니다.
- 현 시점 객체 탐지 모델의 최고 성능은,
정확도로는 Transformer 계열의 객체 탐지 모델인 RF_DETR(COCO mAP 정확도 60+, FPS 108 이하) 이고,
속도로는 YOLO 시리즈의 YOLOv6‑N(COCO mAP 정확도 40 이하, FPS 1200 ~ 1234)입니다.
물론 상기한 모델들은 전부 이해하고 정리할 것이며, 그것을 위한 발판으로 이렇게 딥러닝을 활용한 객체 탐지 모델의 시초라고 할 수 있는 R-CNN 계열 모델을 자세히 이해하고 넘어가도록 하겠습니다.
[R-CNN(Region-based Convolutional Neural Network)]
- R-CNN 은 이미지 내 객체 탐지 문제를 CNN 모델을 기반으로 풀기 시작한 최초의 중요한 연구 결과물입니다.
2013 년의 Rich feature hierarchies for accurate object detection and semantic segmentation 논문을 기반으로 발표되었습니다.
- R-CNN 이 나오기 이전의 객체 탐지 기법은 hand-craft feature 방식을 사용하였습니다.
수동으로 이미지의 특성을 추출해내는 함수를 작성하여 적용하는 방식입니다.
이미지를 분석한다는 것을 수학적으로 표현한다는 것은 매우 어려운 일입니다.
자세히 설명은 하지 않겠지만, x 축, y 축 방향으로 편미분을 적용하여 픽셀 값의 변화 정도를 파악하거나, 이러한 원리를 이용한 Sobel Filter Mask 와 같은 기법으로 이미지 내의 외곽 선(Edge)을 탐지하거나, 허프 변환으로 직선 혹은 원을 탐지하는 등 굉장히 복잡하고 전문적인 지식들을 총 동원하여 최적의 알고리즘을 찾아내는 방식을 사용했죠.
하지만 이러한 수동 탐지 기법은 외곽선 검출이나 간단한 도형 검출에는 어느정도 성과가 있었지만, 복잡하거나 노이즈가 심한 이미지에서는 뛰어난 성능을 보여주지 못했습니다.
이후 컴퓨터 비전 기술이 발전하며 CNN이라는 딥러닝 기반의 이미지 특징 추출 모델이 개발되고,
CNN 기반의 이미지 분류 모델들이 이미지 분류 문제에 있어 기존의 알고리즘보다 나은 성능을 증명하자 이렇게 자동으로 이미지 특성을 추출해주는 CNN 모델을 객체 탐지 모델에도 사용할 수 있으리라는 발상이 나오는 것은 자연스러운 흐름일 것입니다.
바로 이때 생겨난 것이 특징 추출 모델인 CNN 을 활용하는 R-CNN 입니다.
기존의 hand-craft feature 추출 + Support Vector Machine(SVM) 기술에서,
CNN + SVM 으로, 특징 추출 모듈을 CNN 으로 바꾸어 성과를 낸 최초의 모델이라는 의미가 있습니다.
- 본격적으로 R-CNN 이 객체 탐지를 할 수 있는 이유에 대해 알아보겠습니다.
기존의 CNN 을 이해하신다면 CNN 을 이용한 이미지 분류 문제 해결에 대해서는 쉽게 이해하실 수 있을 것입니다.
CNN 레이어를 거치고 나온 최종 Feature Map 을 Flatten 하여 얻어지는 Latent Vector 의 데이터 고유성을 가지고 이미지를 분류하는 것이죠.
즉, CNN 레이어는 이미지가 가진 공간적 정보를 이해할 수 있고, 이곳에서 나온 정보에는 이미지의 공간적 특성이 존재한다는 것을 증명합니다.
그러면 이러한 이미지 특징 벡터를 어떻게 처리해서 좌표값을 도출하게 하는 것일까요?
R-CNN 에서의 방법론이나 그 다음 모델에서의 방법론 등의 기술적 발전 흐름 순서에 따라 설명하겠습니다.
- 선택적 탐색(Selective Search)
선택적 탐색은 객체 후보를 추출하는 핵심 기법입니다.
이 단계의 핵심 역할은 "이미지 내에서 뭔가 있을 법한 영역을 찾는 것"으로, 아직 딥러닝이 컴퓨터 비전 분야에서 완전히 자리매김하기 전에 객체 탐지 모델의 핵심 기술로 사용되던 기술입니다.
R-CNN 이 탄생한 시점에서도, CNN 은 단지 특징 추출의 역할을 하고, 머신러닝 예측기인 SVM 은 클래스 분류와 객체 정확도 검증, 그리고 검색된 박스의 정밀 보정 역할만 담당하는 것이지, 실질적인 객체 탐지는 CNN 이 특징을 추출하기 전인 선택적 탐색 과정에서 거의 완성된다고 봐도 됩니다.
CNN 은 GPU 활용이 없던 당시 시점에는 굉장히 무거운 기술이었기 때문에,
비교적 가벼운 선택적 탐지 알고리즘을 사용하여 이미지 내에서 CNN 을 수행할만한 후보군을 찾는 개념으로,
원리를 간단히 설명하자면,
하얀 대리석 바닥에 검은 강아지가 누워 있으면, 강아지라는 객체의 영역은 검고 어두운 색을 지니는 픽셀 위치라고 생각해도 무방하죠?
이처럼 입력 이미지에 대해 인접한 픽셀과 비슷한 색상, 질감, 밝기, 크기 등 유사한 특징을 가지는 영역을 군집화하는 알고리즘이 선택적 탐지 알고리즘입니다.
본 게시글에서는 완전 딥러닝 기반 R-CNN 에 집중하기 위하여 선택적 탐색에 대한 자세한 알고리즘은 설명하지 않겠습니다.
- R-CNN 의 전체 프로세스에서 위와 같이 선택적 탐색으로 굉장히 많은 후보 영역이 생성되게 되는데(2000 개 정도),
그럼에도 이미지 전체를 가능한 모든 영역을 가정하여 CNN 을 돌리는 것보다는 후보 영역을 선출하는 것이 훨씬 연산량을 줄일 수 있습니다.
이제 이렇게 추출된 각 후보 영역의 CNN Feature Map 들을 사용하여 클래스 분류 및 좌표 미세조정을 하면 됩니다.
- R-CNN 의 전체 프로세스를 정리하겠습니다.
이미지 입력 -> 선택적 탐색을 통한 2000 개 후보 구역 선정 -> 각 후보 구역을 CNN 레이어에 넣어서 Feature Map 추출 -> 각 후보의 Feature Map 을 SVM 과 Linear NN 에 넣음.
위와 같은 식으로 진행됩니다.
마지막 과정에서 SVM 의 역할은 해당 후보 영역이 어떤 클래스에 속하는지에 대한 클래스 분류 역할을 하며,
Linear NN 은 선택적 탐색에서 검출된 Bounding Box(BBox) 의 좌표 값을 수정할 때의 회귀 모델의 역할을 합니다.
BBox 회귀 결과는 그대로 좌표값 미세조정으로 사용하고, SVM 의 클래스 분류 결과는 그 정확도를 기준으로 후보군 중에서 잘 탐지된 영역을 추려내는 데에도 사용 합니다.(클래스 분류 결과가 높을수록 구역이 잘 선정되었다는 뜻)
참고로 Classification 은 이해가 잘 되더라도 BBox 회귀 부분이 정확히 어떻게 되는지에 대해서는 깊게 생각할 것 없습니다.
저 역시 예전에 공부할 때에는 어떻게 CNN 피쳐맵으로 좌표값이 반환되는지에 대하여 깊이 고민한 적이 있는데,
그냥 딥러닝 특성상 피쳐맵이 반환하는 정보의 유형에 따라서 올바른 실수값이 반환되도록 어떠한 법칙과 수식을 스스로 학습했다고 생각하는 것이 딥러닝 모델을 공부하는데에 있어 나쁘지 않은 접근 방법입니다.
- 추가적으로, NMS (Non-Maximum Suppression) 라는 기법을 사용하여 후처리를 합니다.
생각해보자면 클래스 분류 결과가 높더라도 겹치는 구간이 존재할 수 있습니다.
후보군 선정시 유사하게 겹치는 영역을 탐지했다면, 거의 동일한 값이 나오는 것이죠.
고로 좌표 구간이 겹치는 것 중에서 높은 탐지 결과만 남기고 낮은 결과는 제거하는 것이 NMS 입니다.
NMS 의 프로세스는,
1. 모든 박스를 confidence score 기준으로 정렬
2. 가장 높은 점수를 가진 박스 선정
3. 선정된 박스와 IoU(Intersection over Union, 겹치는 구간) 이 미리 설정한 기준값 이상인 박스를 제거
4. 남은 박스 중 최고 점수 박스를 선택하고 위 프로세스 반복
위와 같습니다.
IoU 에 대해 설명하자면,
두 박스의 '겹치는 영역 / 전체 영역' 의 값입니다.
값은 0~1 사이로, 1이라면 완전히 겹침, 0이라면 완전히 겹치지 않음 으로 해석하죠.
보시다시피 기계적인 프로세스이므로 객체 탐지 모델이 겹치는 이미지의 뒤쪽 객체를 무시하거나 하는 현상은 이 부분에서 일어날 가능성이 크고, 그렇기에 IoU 설정값을 적절히 선택해야만 합니다.
IoU 설정값이 너무 작으면 필요 이상으로 많은 박스를 제거하게 되고, 너무 크게 잡으면 후보로 선택된 박스가 모두 노출되게 됩니다.
NMS 를 코드상으로 보면 아래와 같습니다.
import numpy as np
def compute_iou(box1, box2):
"""
box = [x1, y1, x2, y2] 형식 (좌상단, 우하단 좌표)
"""
x1 = max(box1[0], box2[0])
y1 = max(box1[1], box2[1])
x2 = min(box1[2], box2[2])
y2 = min(box1[3], box2[3])
inter_w = max(0, x2 - x1)
inter_h = max(0, y2 - y1)
inter_area = inter_w * inter_h
area1 = (box1[2] - box1[0]) * (box1[3] - box1[1])
area2 = (box2[2] - box2[0]) * (box2[3] - box2[1])
union_area = area1 + area2 - inter_area
return inter_area / union_area if union_area > 0 else 0
def nms(boxes, scores, iou_threshold):
"""
boxes: numpy array of shape (N, 4), [x1, y1, x2, y2]
scores: numpy array of shape (N,)
iou_threshold: float, IoU 임계값
return: 선택된 박스들의 인덱스 리스트
"""
idxs = scores.argsort()[::-1] # 점수 내림차순 정렬
selected_idxs = []
while len(idxs) > 0:
current = idxs[0]
selected_idxs.append(current)
rest = idxs[1:]
filtered_idxs = []
for i in rest:
iou = compute_iou(boxes[current], boxes[i])
if iou < iou_threshold:
filtered_idxs.append(i)
idxs = np.array(filtered_idxs)
return selected_idxs
# 사용 예시
boxes = np.array([
[100, 100, 210, 210],
[105, 105, 215, 215],
[150, 150, 250, 250],
])
scores = np.array([0.9, 0.8, 0.7])
iou_threshold = 0.5
selected_indices = nms(boxes, scores, iou_threshold)
print("Selected boxes indices:", selected_indices)
- 이번 글에서 SVM 을 다룰 생각은 없으므로, R-CNN 은 이만하고 바로 다음의 Fast R-CNN 으로 넘어가겠습니다.
[Fast R-CNN]
- 앞서 정리한 R-CNN 은 기존의 객체 탐지 모델에서 특징 추출 부분을 CNN 으로 대체하여 성능을 높인 모델입니다.
다만, 당시 CPU 기반 처리 환경에서 후보로 선정된 2000 개의 영역을 전부 각각 CNN 으로 입력하는 방식은 굉장히 느리고 비효율적인 성능을 보여주었습니다.
- Fast R-CNN 은 이름 그대로 R-CNN 의 속도를 개선한 모델입니다.
잠시 생각해보겠습니다.
이미지가 있고, CNN 은 이미지의 특징을 추출합니다.
R-CNN 보다 미래의 이야기이긴 하지만 앞서 정리한 U-Net 모델에서는 CNN 의 공간적 특징 추출 능력을 보존하기 위해 Deconv 를 사용하는 Fully Convolutional Networks 까지 사용할 정도이며, Class Activation Map 에서는 이미지의 특징을 그대로 이미지로 투영할 수도 있습니다.
그렇다는 것은 CNN 이 반환하는 특징맵은 이미지의 공간적 정보가 포함되어 있다는 의미가 되죠.
이게 무슨 의미냐면, Feature Map 은 입력된 이미지의 축소버전이라는 것으로,
만약 선택적 탐색으로 선정된 후보 영역이 서로 겹치는 영역일 경우, 해당 부분에 대해 CNN 을 따로 실행하는 것은 낭비이며, 겹치는 부분의 특징맵은 재활용 할 수 있다는 의미입니다.
- 간단히 전체 프로세스를 보고 R-CNN 에서 뭐가 바뀌었는지 확인해보겠습니다.
1. 이미지 전체에 CNN 을 한번만 적용하여 전체 이미지에 대한 특징맵을 추출
2. 선택적 탐색으로 후보군 선정
3. 선정된 후보군 영역을 기반으로 특징맵 내의 해당 영역의 정보를 가져오기(Region of Interest Pooling, ROI Pooling, 관심영역 풀링 - 후보 영역 크기가 달라도 고정 크기를 반환)
4. 가져온 특징맵을 SVM 이 아닌 Deep Learning Neural Network 에 속하는 클래스 분류 모델과 BBox 회귀 모델에 각각 넣어서 결과 추출
5. 각 후보군에 대한 결과값을 가지고 NMS 수행
위와 같습니다.
R-CNN 과 바뀐 점으로는, 앞서 이야기 한대로 CNN 을 전체 이미지에 적용하여 피쳐맵을 한번에 뽑아내고, 후보군 영역에 따라서 특징맵을 오려서 재활용 하는 방식을 사용하는 것입니다.
이러한 방식으로 인하여 연산 효율이 엄청나게 올라가서 R-CNN 에 비해 무척이나 빠른 성능을 보이게 되었죠.
추가적으로는 클래스 분류기에서도 SVM 이 아니라 DNN 방식을 사용하여,
CNN 부터 클래스 분류, BBox 회귀까지 끝에서 끝까지 학습이 가능한 종단 간 학습(End-to-End Learning)으로 인해 정확도 성능이 향상되었다는 특징이 있습니다.
- ROI Pooling 에 대해 조금 더 자세히 알아보겠습니다.
앞서 생성된 후보군은 그 사이즈와 종횡비가 각각 서로 다를 것입니다.
이를 동일한 구조를 지닌 BBoX 회귀 모델이나 클래스 분류 모델에 넣으면 텐서간 차원 문제가 발생합니다.
ROI Pooling 은 서로 다른 크기와 종횡비를 가진 ROI 영역을 입력하더라도, 항상 고정된 크기로 반환하게 만드는 Pooling 기술입니다.
프로세스는,
1. 입력된 ROI 영역을 n x m 영역으로 균등 분할
2. 각 분할된 영역별 최대 값(Max Pooling)을 추출
3. 추출된 값들은 n x m 형태를 띄므로 어떤 값이 입력되어도 고정값이 반환됨
위와 같이 쉽게 이해가 가능합니다.
[Faster R-CNN]
- Fast R-CNN 에서 CNN 전체 피쳐맵에서 ROI Pooling 을 사용하는 방식으로 연산량을 줄였지만, 전체 실행 시간 중 선택적 탐색 알고리즘은 매우 느린 탐색 속도를 가지고 있고, 딥러닝 외부 알고리즘이라 구조적 통일성 및 End-to-End 학습에 따른 성능 향상에도 악영향을 끼칩니다.
이를 해결하기 위한 후속 모델인 Faster R-CNN 은,
선택적 탐색 알고리즘을 제거하고, Region Proposal 기능을 담당하는 딥러닝 아키텍쳐인 RPN(Region Proposal Network)를 만들어 적용한 것으로 실행 속도를 대폭 높이고(R-CNN : ~0.05FPS, Fast R-CNN : ~0.5FPS, Faster R-CNN : 57FPS), 정확도도 향상시켰으며, 최초로 전 부분에 딥러닝을 적용한 객체 탐지 모델로써 후속 모델들의 발판이 되었습니다.
- Faster R-CNN 전체 구조
Faster R-CNN 의 전체 구조는 위와 같습니다.
1. 이미지를 CNN 백본에 입력
2. 출력된 이미지 특징맵을 통째로 RPN 에 넣어서 박스 분류, 박스 회귀의 후보군 선정
3. RPN 에서 출력된 후보 영역과 특징맵을 가지고 Fast R-CNN 에서 한 것과 같이 ROI Pooling 을 하여 클래스 분류 및 좌표 회귀
이처럼 Fast R-CNN 에 비교했을 때 달라진 점은 후보 영역 선정을 RPN 으로 했다는 것이 거의 전부라고 할 수 있는데, 세부 사항은 어떨까요?
- RPN 을 자세히 살펴보겠습니다.
RPN 의 입력물은 CNN 에서 추출된 Feature Map 입니다.
이 Feature Map 을 돌며, Fast R-CNN 에서 한 것처럼 클래스와 B-Box 를 구하면 되는데, Fast R-CNN 에서는 선택 탐색 알고리즘이 범위를 지정해주었죠?
RPN 의 계산 방식은 Fast R-CNN 에서 선택 탐색 알고리즘의 뒷 부분 뉴럴 네트워크와 동일한 방식으로 계산되는데,
그러면 후보가 되는 구역이 필요합니다.
그 역할을 해주는 것으로 앵커 박스라는 것이 존재합니다.
Anchor Box 란,
이미지를 일정 구역으로 나누고, 각 구역별 미리 지정된 다양한 사이즈, 다양한 비율로 후보 구역을 만드는 것과 같은 의미입니다.
위와 같이 앵커 박스가 3 사이즈, 3 비율로 하여 총 9개가 존재하며, 각 구간마다 다양한 사이즈, 다양한 비율을 가진 앵커박스의 범위를 탐지하여 최종 후보 구역을 구하는 것이 RPN 입니다.
앵커 박스의 표현 방식은, [cx, cy, w, h] 로, 앵커 박스의 중심 좌표와 width, height 로 이루어집니다.
앵커 박스의 적용 규칙은,
위 이미지에서 시각적으로 나타나므로 설명하겠습니다.
이미지 원본 크기가 800x600 인 상황에서 피쳐맵을 추출하면 800/16 = 50, 600/16 = 37.5 크기가 되는데, 픽셀값이므로 5를 반올림하여 50x38 이 되고, 이를 곱하면 피쳐맵의 그리드 총 개수는 1900 개 입니다.
즉, 앵커 박스 k 개가 1900 개의 위치에서 적용되는 것이죠.
앵커 박스의 ratio 가 0.5, 1, 2 로 설정하고, 크기는 8, 16, 32 로 적용했다면, 각 그리드를 중심으로 해당 앵커 사이즈만큼의 범위들을 탐지 후보로 하는 것으로, 그 결과는 위 이미지의 붉은 영역과 같이 1900 개의 점을 중심으로 각각 9 개의 앵커 범위가 지정되었으므로 17100 개의 구역이 선정되는 것으로 해석할 수 있습니다.
- FeatureMap 추출 -> 앵커 박스 선정이 끝났다면 바로 RPN 을 수행합니다.
class RPN(nn.Module):
def __init__(self, in_channels=512, mid_channels=512, num_anchors=9):
super(RPN, self).__init__()
# 3x3 conv layer (shared conv)
self.conv = nn.Conv2d(in_channels, mid_channels, kernel_size=3, padding=1)
# objectness classification (2 classes: object / background)
self.cls_logits = nn.Conv2d(mid_channels, num_anchors * 2, kernel_size=1)
# bounding box regression (4 coordinates per anchor)
self.bbox_pred = nn.Conv2d(mid_channels, num_anchors * 4, kernel_size=1)
# weight initialization
for layer in [self.conv, self.cls_logits, self.bbox_pred]:
nn.init.normal_(layer.weight, std=0.01)
nn.init.constant_(layer.bias, 0)
def forward(self, x):
t = F.relu(self.conv(x))
objectness = self.cls_logits(t)
bbox_regression = self.bbox_pred(t)
return objectness, bbox_regression
코드로 보자면 위와 같습니다.
(batch_size, channels, height, width) 크기의 피쳐맵 x 를 입력하고,
cls_logits 로 conv 연산을 하여 (batch_size, 2 * num_anchors, height, width) 크기의 클래스 분류(2개 클래스를 가정) 값과,
bbox_pred 로 conv 연산을 하여 (batch_size, 4 * num_anchors, height, width) 크기의 좌표 회귀(4개 좌표) 값을 출력하는 것입니다.
이러한 과정을 거치고 나면 공간 정보가 아직 살아있는 h x w 크기의 피쳐맵 크기 안에 채널별로 클래스 분류를 위한 정보들과 좌표 회귀를 위한 정보들이 저장되어 있는 상태입니다.
이제 이 정보를 가지고 최종적으로 후보 영역을 가져오는 다음 처리과정을 보겠습니다.
- 먼저, bbox_regression 값을,
# bbox_regression: (B, 4*num_anchors, H, W) -> (B, num_anchors, 4, H, W) -> (B, H*W*num_anchors, 4)
bbox_reg = bbox_regression.view(B, A, 4, H, W)
bbox_reg = bbox_reg.permute(0, 3, 4, 1, 2).reshape(B, -1, 4)
이렇게, (B, H*W*num_anchor, 4) 형태로 변경합니다.
이 값(여기서 batch 는 무시합시다.) 에,
def apply_deltas_to_anchors(anchors, deltas):
cx = anchors[:, 0]
cy = anchors[:, 1]
w = anchors[:, 2]
h = anchors[:, 3]
tx = deltas[:, 0]
ty = deltas[:, 1]
tw = deltas[:, 2]
th = deltas[:, 3]
pred_cx = tx * w + cx
pred_cy = ty * h + cy
pred_w = torch.exp(tw) * w
pred_h = torch.exp(th) * h
x1 = pred_cx - pred_w / 2
y1 = pred_cy - pred_h / 2
x2 = pred_cx + pred_w / 2
y2 = pred_cy + pred_h / 2
return torch.stack([x1, y1, x2, y2], dim=1)
위와 같이 [N, 4] 형태의 anchor([center_x, center_y, width, height]) 와 동일 형태의 RPN 추론 bbox 좌표값([tx, ty, tw, th])을 가지고 와서, 변형을 거쳐 [N, 4] 형태의 [x1, y1, x2, y2] 값, 즉 좌측 상단 좌표와 우측 하단 좌표로 변환하는 것입니다.
이로써 보았을 때, bbox 좌표값은 미리 정해진 앵커 박스에 대한 상대적 위치와 크기 변형량을 나타내는 것이며,
RPN 의 bbox 예측기도 그런 목적으로 만들어지고 그런 목적을 달성시키는 방향으로 학습이 이루어진다는 것을 알 수 있습니다.
어쨌건 여기까지 해서 앵커를 기반으로 한 후보 범위의 좌표값을 알게 되었습니다.
다음으로는,
def clip_boxes(boxes, image_shape):
height, width = image_shape
boxes[:, 0] = boxes[:, 0].clamp(min=0, max=width - 1)
boxes[:, 1] = boxes[:, 1].clamp(min=0, max=height - 1)
boxes[:, 2] = boxes[:, 2].clamp(min=0, max=width - 1)
boxes[:, 3] = boxes[:, 3].clamp(min=0, max=height - 1)
return boxes
위와 같이
boxes: [N, 4], [x1, y1, x2, y2] 박스 좌표,
image_shape: (height, width) 원본 이미지 크기
를 가지고, 앞서 출력된 좌표값에서 이미지 경계 밖으로 나간 좌표를 경계 내로 제한합니다.
음수라면 0으로, 이미지 최대 크기 바깥이라면 최대 크기로 변경하는 함수를 사용하고,
def filter_boxes(boxes, min_size=16):
ws = boxes[:, 2] - boxes[:, 0]
hs = boxes[:, 3] - boxes[:, 1]
keep = (ws >= min_size) & (hs >= min_size)
return keep
이렇게 일정 크기 이하의 의미 없는 좌표 박스는 제거하는 방식으로 삭제할 위치를 선정하고,
keep = filter_boxes(proposals)
proposals = proposals[keep]
이렇게 제거하면 됩니다.
- 다음으로 objectness 값을 처리하는 프로세스를 보겠습니다.
이 값 역시 계산할 수 있도록,
# objectness: (B, 2*num_anchors, H, W) -> (B, num_anchors, 2, H, W) -> (B, H*W*num_anchors, 2)
B, C, H, W = objectness_logits.shape
A = C // 2 # num_anchors
objectness = objectness_logits.view(B, A, 2, H, W)
objectness = objectness.permute(0, 3, 4, 1, 2).reshape(B, -1, 2)
이렇게 차원을 변경하고,
objectness_prob = F.softmax(objectness, dim=2)[:, :, 1] # foreground prob
이렇게, 각 클래스에 대한 softmax 분류 예측을 수행합니다.
probs = objectness_prob[b]
위와 같이 batch 를 순회하며 클래스 분류 값을 가져오고,
앞서 bbox 에서 filter_boxes 를 할 때에 구한 제거 인덱스를 가져와서
scores = probs[keep]
이렇게 제거합니다.(bbox 리스트와 probs 리스트는 쌍이 맞아야 하기 때문에)
다음으로는,
scores, order = scores.sort(descending=True)
order = order[:pre_nms_top_n]
proposals = proposals[order]
scores = scores[:pre_nms_top_n]
keep = nms(proposals, scores, nms_thresh)
keep = keep[:post_nms_top_n]
proposals = proposals[keep]
scores = scores[keep]
이처럼 클래스 점수가 일정 이하인 것, NMS 에 따라 삭제될 대상인 것들을 전부 삭제해버리고 남은 것을 반환하면 RPN 은 끝입니다.
RPN 설명 마지막으로 위 프로세스 전체 코드는 아래와 같습니다.
import torch
import torch.nn as nn
import torch.nn.functional as F
from torchvision.ops import nms
# 1. CNN 백본 (간단하게 Conv + ReLU 몇 개로 대체)
class SimpleBackbone(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(3, 64, 3, padding=1) # 예: input RGB 3채널
self.conv2 = nn.Conv2d(64, 128, 3, padding=1)
self.conv3 = nn.Conv2d(128, 512, 3, padding=1) # RPN 입력 채널 512로 맞춤
def forward(self, x):
x = F.relu(self.conv1(x))
x = F.relu(self.conv2(x))
x = F.relu(self.conv3(x))
return x # [B, 512, H, W]
# 2. 앵커 생성 함수 (간단히 피쳐맵 각 위치별 3개 크기만 생성)
def generate_anchors(feature_size, stride=16, sizes=[32, 64, 128]):
"""
feature_size: (height, width) of feature map
stride: 한 피쳐 위치가 원본 이미지에서 차지하는 픽셀 크기
sizes: 앵커 박스 정사각형 크기 (가로=세로)
반환: anchors (N, 4) [cx, cy, w, h] 좌표, N = H*W*len(sizes)
"""
anchors = []
H, W = feature_size
for y in range(H):
for x in range(W):
cx = (x + 0.5) * stride
cy = (y + 0.5) * stride
for size in sizes:
anchors.append([cx, cy, size, size])
return torch.tensor(anchors, dtype=torch.float32)
# 3. RPN 네트워크 (질문에 나온 버전)
class RPN(nn.Module):
def __init__(self, in_channels=512, mid_channels=512, num_anchors=3):
super().__init__()
self.conv = nn.Conv2d(in_channels, mid_channels, kernel_size=3, padding=1)
self.cls_logits = nn.Conv2d(mid_channels, num_anchors * 2, kernel_size=1)
self.bbox_pred = nn.Conv2d(mid_channels, num_anchors * 4, kernel_size=1)
for layer in [self.conv, self.cls_logits, self.bbox_pred]:
nn.init.normal_(layer.weight, std=0.01)
nn.init.constant_(layer.bias, 0)
def forward(self, x):
t = F.relu(self.conv(x))
objectness = self.cls_logits(t)
bbox_regression = self.bbox_pred(t)
return objectness, bbox_regression
# 4. bbox 회귀값 적용 함수
def apply_deltas_to_anchors(anchors, deltas):
cx = anchors[:, 0]
cy = anchors[:, 1]
w = anchors[:, 2]
h = anchors[:, 3]
tx = deltas[:, 0]
ty = deltas[:, 1]
tw = deltas[:, 2]
th = deltas[:, 3]
pred_cx = tx * w + cx
pred_cy = ty * h + cy
pred_w = torch.exp(tw) * w
pred_h = torch.exp(th) * h
x1 = pred_cx - pred_w / 2
y1 = pred_cy - pred_h / 2
x2 = pred_cx + pred_w / 2
y2 = pred_cy + pred_h / 2
return torch.stack([x1, y1, x2, y2], dim=1)
# 5. 박스 클리핑
def clip_boxes(boxes, image_shape):
height, width = image_shape
boxes[:, 0] = boxes[:, 0].clamp(min=0, max=width - 1)
boxes[:, 1] = boxes[:, 1].clamp(min=0, max=height - 1)
boxes[:, 2] = boxes[:, 2].clamp(min=0, max=width - 1)
boxes[:, 3] = boxes[:, 3].clamp(min=0, max=height - 1)
return boxes
# 6. 작은 박스 제거
def filter_boxes(boxes, min_size=16):
ws = boxes[:, 2] - boxes[:, 0]
hs = boxes[:, 3] - boxes[:, 1]
keep = (ws >= min_size) & (hs >= min_size)
return keep
# 7. RPN 후처리
def rpn_postprocess(objectness_logits, bbox_regression, anchors, image_shape,
nms_thresh=0.7, pre_nms_top_n=6000, post_nms_top_n=300):
# objectness_logits: [1, 2*num_anchors, H, W]
# bbox_regression: [1, 4*num_anchors, H, W]
batch_size = objectness_logits.shape[0]
num_anchors = anchors.shape[0]
# objectness: (B, 2*num_anchors, H, W) -> (B, num_anchors, 2, H, W) -> (B, H*W*num_anchors, 2)
B, C, H, W = objectness_logits.shape
A = C // 2 # num_anchors
objectness = objectness_logits.view(B, A, 2, H, W)
objectness = objectness.permute(0, 3, 4, 1, 2).reshape(B, -1, 2)
objectness_prob = F.softmax(objectness, dim=2)[:, :, 1] # foreground prob
# bbox_regression: (B, 4*num_anchors, H, W) -> (B, num_anchors, 4, H, W) -> (B, H*W*num_anchors, 4)
bbox_reg = bbox_regression.view(B, A, 4, H, W)
bbox_reg = bbox_reg.permute(0, 3, 4, 1, 2).reshape(B, -1, 4)
proposals_list = []
scores_list = []
for b in range(batch_size):
probs = objectness_prob[b]
deltas = bbox_reg[b]
proposals = apply_deltas_to_anchors(anchors, deltas)
proposals = clip_boxes(proposals, image_shape)
keep = filter_boxes(proposals)
proposals = proposals[keep]
scores = probs[keep]
scores, order = scores.sort(descending=True)
order = order[:pre_nms_top_n]
proposals = proposals[order]
scores = scores[:pre_nms_top_n]
keep = nms(proposals, scores, nms_thresh)
keep = keep[:post_nms_top_n]
proposals = proposals[keep]
scores = scores[keep]
proposals_list.append(proposals)
scores_list.append(scores)
return proposals_list, scores_list
- 이 이후로는 위에서 얻은 Proposals 를 기반으로 ROI Polling 및 회귀 분류를 진행하는 Fast R-CNN 과 동일하기에 생략하겠습니다.
(추가 설명)
- 객체 탐지 모델을 Faster R-CNN 이 나오고 두갈래로 나뉘게 된다고 보셔도 됩니다.
느린편이지만 정확도가 높은 편인 Faster R-CNN 이 백본이나 여러 부분이 튜닝되며 계속 사용되었고,
정확도가 낮은 편이지만 속도만은 훨씬 빠른 YOLO 모델이 나오게 되면서 실시간 시스템과 오프라인 분석 영역의 적용 모델이 갈리게 됩니다.
하지만 YOLO 모델이 계속 발전해가며 성능은 물론 정확성까지 챙기기 시작한 이래 YOLO v4(속도 30~60FPS, 정확도 43~45%) 까지만 가더라도 Faster R-CNN(속도 5~10 FPS, 정확도 42~45%) 을 압도적인 속도는 물론 정확도마저도 근소하게 능가하였고, 최신 YOLO v12-M(속도 206 FPS, 정확도 52.5%) 모델은 비교도 안 될 만큼 속도와 정확도가 Faster R-CNN 을 능가하는 상황입니다.
고로 현 시점 대다수의 서비스나 제품에서의 객체 탐지 모델은 YOLO 시리즈가 사용되고 있는 상태이므로,
본 게시글의 다음으로 객체 탐지 모델을 정리할 때에는, YOLO 모델 첫번째 모델을 시작으로 굵직한 변화가 있는 중간 버전들과 최신 버전까지를 선정하여 객체 탐지 분야의 발전 흐름을 자세히 정리하겠습니다.
- 이상입니다.