🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

객체 탐지 기초 R-CNN 계열 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 9. · 61 Views

객체 탐지 기초 R-CNN 계열 완벽 가이드

이미지에서 물체를 찾아내는 객체 탐지 기술의 핵심인 R-CNN 계열 알고리즘을 처음 접하는 개발자도 쉽게 이해할 수 있도록 실무 스토리와 비유로 풀어냅니다. R-CNN부터 Faster R-CNN까지의 발전 과정을 따라가며 RoI Pooling과 Anchor Box 같은 핵심 개념을 마스터할 수 있습니다.


목차

  1. 객체_탐지_문제_정의
  2. R-CNN_구조와_한계
  3. Fast_R-CNN_개선점
  4. Faster_R-CNN과_RPN
  5. RoI_Pooling_이해
  6. Anchor_Box_개념

1. 객체 탐지 문제 정의

어느 날 김개발 씨는 자율주행 프로젝트 팀에 합류했습니다. 팀장님이 첫 미팅에서 이렇게 말씀하셨습니다.

"카메라로 촬영한 도로 이미지에서 차량, 보행자, 신호등을 찾아내야 합니다." 김개발 씨는 문득 궁금해졌습니다. 이미지에서 물체를 찾는다는 게 정확히 무슨 의미일까요?

객체 탐지란 이미지 안에서 관심 있는 물체가 어디에 있는지 위치를 찾고, 그것이 무엇인지 분류하는 작업입니다. 마치 사진 속에서 친구 얼굴을 찾아 네모 박스로 표시하고 이름을 붙이는 것과 같습니다.

단순히 "이 사진에 고양이가 있다"를 넘어서 "왼쪽 상단에 고양이가, 오른쪽 하단에 강아지가 있다"는 정보까지 알아내야 합니다.

다음 코드를 살펴봅시다.

# 객체 탐지 결과 데이터 구조 예시
detection_result = {
    'image': 'street_scene.jpg',
    'objects': [
        {'class': 'car', 'bbox': [100, 150, 300, 400], 'confidence': 0.95},
        {'class': 'person', 'bbox': [50, 80, 120, 250], 'confidence': 0.88},
        {'class': 'traffic_light', 'bbox': [400, 50, 450, 150], 'confidence': 0.92}
    ]
}

# bbox는 [x1, y1, x2, y2] 형식 - 물체를 감싸는 사각형의 좌상단/우하단 좌표
# confidence는 모델이 얼마나 확신하는지를 나타내는 점수 (0~1)

김개발 씨는 입사 일주일 차 주니어 개발자입니다. 컴퓨터 비전 프로젝트에 처음 투입되어 선배 개발자 박시니어 씨와 함께 코드를 살펴보고 있었습니다.

화면에는 도로 사진이 보였고, 그 위에 차량과 사람을 감싸는 색색의 사각형들이 그려져 있었습니다. "선배님, 이게 바로 객체 탐지인가요?" 김개발 씨가 조심스럽게 물었습니다.

박시니어 씨가 고개를 끄덕이며 설명을 시작했습니다. 객체 탐지란 정확히 무엇일까요?

쉽게 비유하자면, 객체 탐지는 마치 사진첩을 보며 친구들을 찾는 것과 같습니다. 여러분이 단체 사진을 볼 때 "아, 여기 철수가 있네, 저기 영희도 있고"라고 하나씩 찾아내는 것처럼, 컴퓨터도 이미지를 보며 관심 있는 물체들을 하나씩 찾아냅니다.

다만 사람과 다른 점은 각 물체 주변에 정확한 사각형을 그리고, 그것이 무엇인지 확률까지 계산해낸다는 것입니다. 객체 탐지가 왜 필요할까요?

초창기 딥러닝 시대에는 이미지 분류만 가능했습니다. 사진 한 장을 보고 "이건 고양이 사진이야" 또는 "이건 자동차 사진이야"라고 전체 이미지에 하나의 라벨만 붙일 수 있었죠.

하지만 현실 세계의 사진은 훨씬 복잡합니다. 한 장면에 여러 물체가 동시에 등장하니까요.

자율주행 자동차를 예로 들어봅시다. 카메라로 찍은 도로 사진에는 여러 대의 차량, 보행자, 신호등, 표지판이 함께 나타납니다.

단순히 "이 사진에 차가 있다"를 아는 것만으로는 부족합니다. "앞 차가 3미터 전방에 있고, 왼쪽에서 보행자가 다가오고 있다"는 구체적인 위치 정보가 필요합니다.

이런 정보 없이는 안전한 주행이 불가능하겠죠. 객체 탐지가 해결하는 핵심 문제는 무엇일까요?

객체 탐지는 크게 두 가지 문제를 동시에 해결합니다. 첫 번째는 위치 찾기입니다.

이미지의 어느 영역에 물체가 있는지를 픽셀 좌표로 정확히 찾아냅니다. 보통 바운딩 박스라고 부르는 사각형으로 표현하죠.

두 번째는 분류하기입니다. 찾아낸 영역이 차량인지, 사람인지, 신호등인지 구분합니다.

위의 코드를 살펴보면 이 개념이 명확해집니다. detection_result라는 딕셔너리를 보면, objects 리스트 안에 여러 개의 물체 정보가 담겨 있습니다.

각 물체마다 class(종류), bbox(바운딩 박스 좌표), confidence(확신도)를 가지고 있죠. bbox의 네 개 숫자는 사각형의 왼쪽 위 모서리와 오른쪽 아래 모서리의 x, y 좌표입니다.

이 네 점만 알면 사각형을 그릴 수 있습니다. confidence는 모델이 자신의 예측을 얼마나 확신하는지 나타냅니다.

0.95라면 95퍼센트 확신한다는 의미입니다. 실무에서는 보통 0.5나 0.7 같은 임계값을 정해서 그보다 낮은 확신도를 가진 검출 결과는 무시합니다.

실제 현업에서는 어떻게 활용될까요? 쇼핑몰 앱을 개발한다고 가정해봅시다.

사용자가 옷을 입은 사진을 업로드하면, 객체 탐지 모델이 자동으로 상의, 하의, 신발, 가방 등을 찾아냅니다. 각 아이템의 위치를 알면 해당 영역만 잘라내서 유사한 상품을 추천할 수 있습니다.

또 다른 예로 공장 자동화가 있습니다. 컨베이어 벨트 위의 제품들을 카메라로 촬영하면서 불량품을 실시간으로 검출하고 분류하는 시스템도 객체 탐지 기술을 활용합니다.

박시니어 씨가 화면을 가리키며 말했습니다. "이 모든 게 가능해진 건 R-CNN이라는 획기적인 알고리즘 덕분이에요.

앞으로 우리가 배울 내용이죠." 김개발 씨는 이제 객체 탐지가 무엇인지 이해했습니다. 단순히 물체를 인식하는 것을 넘어, 정확한 위치와 종류를 모두 파악하는 기술이었습니다.

이제 본격적으로 R-CNN 계열 알고리즘을 배울 준비가 되었습니다.

실전 팁

💡 - 객체 탐지의 핵심은 위치 찾기(Localization) + 분류(Classification) 두 가지를 동시에 수행하는 것

  • 바운딩 박스 좌표는 보통 [x1, y1, x2, y2] 또는 [x, y, width, height] 형식으로 표현됨
  • 실무에서는 confidence threshold를 조정해서 정밀도와 재현율의 균형을 맞춤

2. R-CNN 구조와 한계

다음 날 김개발 씨는 박시니어 씨와 함께 코드 저장소를 살펴보고 있었습니다. 파일 이름에 "rcnn_legacy"라고 적힌 오래된 코드가 보였습니다.

"이게 바로 객체 탐지의 시조 격인 R-CNN입니다. 지금은 안 쓰지만, 이걸 이해해야 최신 기술도 이해할 수 있어요." 선배의 말에 김개발 씨는 호기심이 생겼습니다.

R-CNN은 Regions with CNN features의 약자로, 2014년에 등장한 최초의 딥러닝 기반 객체 탐지 알고리즘입니다. 이미지에서 물체가 있을 법한 영역(region proposal)을 약 2000개 뽑아낸 뒤, 각 영역마다 CNN을 돌려서 분류하는 방식입니다.

마치 의심스러운 곳을 일일이 돋보기로 들여다보는 것처럼 느리지만, 당시로서는 혁신적인 정확도를 보여줬습니다.

다음 코드를 살펴봅시다.

# R-CNN의 처리 흐름 (의사코드)
import selective_search  # 영역 제안 알고리즘
import cnn_model  # 사전학습된 CNN

def rcnn_detect(image):
    # 1단계: Selective Search로 약 2000개 영역 제안
    region_proposals = selective_search.get_proposals(image)  # ~2000개

    detections = []
    for region in region_proposals:
        # 2단계: 각 영역을 고정 크기로 조정 (227x227)
        warped_region = resize(region, (227, 227))

        # 3단계: CNN으로 특징 추출 (AlexNet 등)
        features = cnn_model.extract_features(warped_region)

        # 4단계: SVM으로 분류
        class_scores = svm_classifier.predict(features)

        # 5단계: Bounding Box Regression으로 박스 위치 조정
        refined_bbox = bbox_regressor.refine(region, features)

        detections.append({'bbox': refined_bbox, 'scores': class_scores})

    return detections

김개발 씨는 화면 속 코드를 보며 고개를 갸우뚱했습니다. "선배님, 이미지 한 장 처리하는 데 왜 이렇게 여러 단계를 거쳐요?" 박시니어 씨가 의자를 돌려 앉으며 설명을 시작했습니다.

R-CNN이 나오기 전에는 어땠을까요? 2012년 AlexNet이 이미지 분류 대회에서 우승하면서 CNN의 성능이 입증되었습니다.

하지만 이미지 분류는 전체 이미지에 하나의 라벨만 붙이는 작업이었죠. 물체의 위치를 찾는 객체 탐지에는 어떻게 CNN을 적용할 수 있을까요?

이미지 전체를 작은 창문으로 슬라이딩하면서 일일이 CNN을 돌릴 수도 있지만, 너무 비효율적입니다. R-CNN의 핵심 아이디어는 무엇일까요?

연구자들은 영리한 방법을 생각해냈습니다. "물체가 있을 법한 곳만 집중적으로 보자!" 이것이 바로 Region Proposal 개념입니다.

전통적인 컴퓨터 비전 기법인 Selective Search를 사용해서 색상, 질감, 경계선 등을 분석해 물체가 있을 만한 영역을 약 2000개 제안받습니다. 쉽게 비유하자면, 큰 창고에서 물건을 찾을 때 구석구석 다 뒤지는 대신 "여기 뭔가 있을 것 같은데?"하는 곳만 집중적으로 살펴보는 것과 같습니다.

2000개면 여전히 많아 보이지만, 이미지의 모든 픽셀 위치를 다 확인하는 것보다는 훨씬 효율적입니다. R-CNN의 작동 과정을 단계별로 살펴봅시다.

첫 번째 단계에서 Selective Search가 2000개 정도의 영역을 뽑아냅니다. 이 영역들은 크기와 비율이 제각각입니다.

두 번째 단계에서는 이 모든 영역을 227x227 픽셀로 강제로 조정합니다. CNN은 고정된 크기의 입력만 받을 수 있기 때문이죠.

이 과정에서 이미지가 찌그러지는 문제가 생기지만 어쩔 수 없습니다. 세 번째 단계에서 각 영역을 미리 학습된 CNN(주로 AlexNet)에 통과시켜 4096차원의 특징 벡터를 추출합니다.

네 번째 단계에서는 이 특징 벡터를 여러 개의 SVM 분류기에 입력해서 각 클래스별 점수를 얻습니다. 마지막 다섯 번째 단계에서는 Bounding Box Regressor라는 모델이 박스의 위치를 미세 조정합니다.

위의 코드를 다시 보면 이 흐름이 명확합니다. selective_search.get_proposals()가 약 2000개의 후보 영역을 반환하면, for 루프가 각 영역마다 반복됩니다.

이게 바로 R-CNN의 가장 큰 문제입니다. 2000번 CNN을 돌려야 하니 엄청나게 느립니다.

한 장의 이미지를 처리하는 데 GPU로도 13초가 걸렸다고 합니다. 박시니어 씨가 화면을 가리키며 말했습니다.

"여기 resize 함수 보이죠? 이미지를 강제로 찌그러뜨리는 부분이에요.

가로로 긴 차량 영역도, 세로로 긴 사람 영역도 모두 정사각형에 가깝게 변형됩니다. 정보 손실이 생기는 거죠." R-CNN의 학습 과정도 복잡합니다.

R-CNN은 총 세 가지 모델을 따로따로 학습해야 합니다. CNN 특징 추출기, SVM 분류기, 바운딩 박스 회귀 모델.

각각 학습하고 저장하고 불러오는 과정이 번거롭습니다. 게다가 추출한 특징을 디스크에 저장했다가 불러와야 해서 수백 GB의 저장 공간이 필요했습니다.

그럼에도 불구하고 R-CNN은 왜 혁신적이었을까요? 2014년 당시 객체 탐지 벤치마크인 PASCAL VOC에서 기존 최고 성능을 30% 이상 뛰어넘는 정확도를 보여줬습니다.

CNN의 강력한 특징 추출 능력을 객체 탐지에 처음 성공적으로 적용한 것입니다. 느리고 복잡하지만, 가능성을 증명했습니다.

김개발 씨가 고개를 끄덕였습니다. "아, 그래서 legacy 코드로 남겨둔 거군요.

느려서 실제로는 못 쓰지만, 역사적으로 중요한 알고리즘이네요." 박시니어 씨가 웃으며 답했습니다. "맞아요.

이제 이 문제들을 어떻게 개선했는지 Fast R-CNN을 보러 가봅시다."

실전 팁

💡 - R-CNN의 세 가지 주요 문제: 느린 속도(2000번 CNN), 복잡한 학습 파이프라인(3단계 분리), 막대한 저장공간 필요

  • Region Proposal에는 딥러닝이 아닌 전통적인 Selective Search 알고리즘 사용
  • 이미지를 고정 크기로 warping하면서 종횡비가 왜곡되는 문제 발생

3. Fast R-CNN 개선점

일주일 후 김개발 씨는 프로젝트 미팅에서 발표 자료를 준비하고 있었습니다. "R-CNN은 너무 느려서 실시간 처리가 불가능하다"는 내용을 정리하던 중, 박시니어 씨가 다가왔습니다.

"그래서 2015년에 같은 연구자가 Fast R-CNN을 발표했어요. 이름 그대로 빠른 버전이죠.

어떻게 빠르게 만들었는지 볼까요?"

Fast R-CNN은 R-CNN의 속도 문제를 획기적으로 개선한 알고리즘입니다. 핵심 아이디어는 이미지 전체를 한 번만 CNN에 통과시켜 특징 맵을 만들고, 그 위에서 각 region proposal의 특징을 추출하는 것입니다.

또한 RoI Pooling이라는 기법으로 크기가 다른 영역들을 고정 크기 특징으로 변환합니다. 학습도 end-to-end로 한 번에 진행할 수 있게 되었습니다.

다음 코드를 살펴봅시다.

# Fast R-CNN의 처리 흐름 (의사코드)
def fast_rcnn_detect(image, region_proposals):
    # 핵심 개선 1: 이미지 전체를 한 번만 CNN에 통과
    feature_map = cnn_backbone.forward(image)  # 한 번만 실행!

    detections = []
    for region in region_proposals:
        # 핵심 개선 2: feature map에서 해당 영역만 추출
        # 원본 이미지 좌표를 feature map 좌표로 변환
        roi_feature = map_region_to_feature_map(feature_map, region)

        # 핵심 개선 3: RoI Pooling으로 고정 크기로 변환
        pooled_feature = roi_pooling(roi_feature, output_size=(7, 7))

        # 핵심 개선 4: FC layer로 분류와 bbox regression 동시 수행
        class_scores = fc_classifier(pooled_feature)
        bbox_deltas = fc_regressor(pooled_feature)

        detections.append({
            'bbox': refine_bbox(region, bbox_deltas),
            'scores': class_scores
        })

    return detections

# 학습도 한 번에 (multi-task loss)
def train_fast_rcnn(image, gt_boxes, gt_classes):
    # 분류 손실 + bbox 회귀 손실을 동시에 최적화
    loss = classification_loss + lambda * bbox_regression_loss
    return loss

김개발 씨는 Fast R-CNN 논문을 읽으며 눈이 반짝였습니다. "와, 속도가 9배나 빨라졌다고요?" 박시니어 씨가 고개를 끄덕이며 화이트보드로 다가갔습니다.

"네, R-CNN의 가장 큰 병목이 뭐였는지 기억나요?" R-CNN의 핵심 병목 지점은 무엇이었을까요? 2000개의 region proposal을 각각 CNN에 통과시켜야 했던 점입니다.

CNN은 계산량이 많은 작업이니까 2000번 반복하면 당연히 느릴 수밖에 없습니다. 게다가 인접한 영역들은 겹치는 부분이 많아서 중복 계산이 엄청나게 많았습니다.

Fast R-CNN의 첫 번째 혁신은 무엇일까요? "이미지 전체를 딱 한 번만 CNN에 통과시키자!" 이게 핵심입니다.

원본 이미지를 CNN에 넣으면 마지막 convolutional layer에서 feature map이 나옵니다. 예를 들어 800x600 이미지가 CNN을 거치면 50x37 크기의 feature map이 됩니다.

이 feature map은 원본 이미지의 압축된 표현이며, 각 위치가 원본 이미지의 어느 영역에 대응하는지 계산할 수 있습니다. 쉽게 비유하자면, 거대한 지도를 축소 복사해서 작은 요약 지도를 만드는 것과 같습니다.

원본 지도의 서울 지역이 요약 지도의 어느 부분에 해당하는지 알 수 있듯이, 원본 이미지의 특정 영역이 feature map의 어느 위치에 대응하는지 매핑할 수 있습니다. 코드를 살펴보며 이해해봅시다.

첫 줄에서 cnn_backbone.forward(image)가 딱 한 번 실행됩니다. R-CNN에서는 이 연산이 2000번 반복되었지만, Fast R-CNN에서는 단 한 번입니다.

이것만으로도 속도가 엄청나게 빨라집니다. 그 다음 for 루프에서는 이미 계산된 feature_map에서 각 region proposal에 해당하는 부분만 잘라냅니다.

map_region_to_feature_map 함수가 원본 이미지 좌표를 feature map 좌표로 변환해줍니다. 예를 들어 원본 이미지에서 [100, 150, 300, 400] 영역이 feature map에서는 [6, 9, 18, 25] 정도 영역에 대응할 수 있습니다.

RoI Pooling이라는 새로운 기법도 등장합니다. 문제는 각 region proposal의 크기가 제각각이라는 점입니다.

Feature map에서 추출한 영역도 크기가 다릅니다. 어떤 건 10x15, 어떤 건 5x8일 수 있죠.

하지만 뒤따라 나오는 Fully Connected layer는 고정된 크기의 입력을 요구합니다. RoI Pooling은 이 문제를 해결합니다.

크기가 다른 영역을 7x7 같은 고정 크기로 변환하는 특별한 풀링 기법입니다. 영역을 7x7 그리드로 나누고, 각 칸에서 최댓값을 뽑아내는 식입니다.

자세한 원리는 다음 카드에서 배우겠습니다. 학습 파이프라인도 간소화되었습니다.

R-CNN에서는 CNN, SVM, Bbox Regressor를 따로따로 학습했습니다. Fast R-CNN에서는 모든 것을 한 번에 학습합니다.

분류 손실과 bbox 회귀 손실을 합친 multi-task loss를 사용합니다. train_fast_rcnn 함수를 보면 두 손실을 더해서 하나의 loss로 만듭니다.

이렇게 하면 역전파도 한 번에 진행되어 학습이 훨씬 효율적입니다. 실제로 얼마나 빨라졌을까요?

논문에 따르면 학습 시간이 R-CNN 대비 9배 빨라졌고, 테스트 시간은 213배나 빨라졌습니다. 이미지 한 장을 처리하는 데 13초 걸리던 게 0.3초로 줄어든 것입니다.

여전히 실시간은 아니지만, 실용성이 훨씬 높아졌습니다. 하지만 여전히 한 가지 병목이 남아 있었습니다.

박시니어 씨가 화이트보드에 동그라미를 그렸습니다. "Selective Search가 여전히 CPU에서 돌아가면서 시간을 잡아먹어요.

Region proposal 생성하는 데만 2초 정도 걸립니다." 김개발 씨가 물었습니다. "그럼 이것도 GPU로 돌릴 수 없나요?" 박시니어 씨가 웃으며 답했습니다.

"바로 그걸 해결한 게 Faster R-CNN입니다. 다음에 볼 거예요." Fast R-CNN은 R-CNN의 주요 문제들을 대부분 해결했지만, region proposal 단계는 여전히 딥러닝이 아니었습니다.

다음 단계는 이마저도 학습 가능한 네트워크로 만드는 것이었습니다.

실전 팁

💡 - Fast R-CNN의 핵심은 feature map 공유 - 이미지를 한 번만 CNN에 통과시킴

  • RoI Pooling으로 가변 크기 영역을 고정 크기로 변환하여 FC layer 입력으로 사용
  • Multi-task loss로 분류와 bbox regression을 동시에 학습하여 end-to-end 학습 가능

4. Faster R-CNN과 RPN

김개발 씨는 이제 Fast R-CNN 코드를 능숙하게 읽을 수 있게 되었습니다. 하지만 프로파일링을 돌려보니 여전히 Selective Search가 전체 처리 시간의 대부분을 차지했습니다.

"선배님, 이 부분만 해결하면 진짜 실시간 탐지가 가능할 것 같은데요?" 박시니어 씨가 고개를 끄덕였습니다. "맞아요.

그래서 나온 게 Faster R-CNN이에요. Region Proposal Network라는 게 핵심입니다."

Faster R-CNN은 region proposal 생성마저 딥러닝으로 해결한 완전한 end-to-end 객체 탐지 시스템입니다. **RPN(Region Proposal Network)**이라는 작은 네트워크가 feature map을 보고 물체가 있을 법한 영역을 제안합니다.

Selective Search를 제거하고 모든 과정을 GPU에서 처리하여 실시간에 가까운 속도를 달성했습니다. 2015년에 발표되어 이후 객체 탐지 연구의 표준이 되었습니다.

다음 코드를 살펴봅시다.

# Faster R-CNN의 전체 구조 (의사코드)
def faster_rcnn_detect(image):
    # 1단계: 공유 feature map 추출
    feature_map = backbone_cnn(image)  # VGG, ResNet 등

    # 2단계: RPN으로 region proposal 생성 (GPU에서!)
    rpn_proposals, rpn_scores = region_proposal_network(feature_map)
    # rpn_proposals: ~2000개의 후보 영역
    # rpn_scores: 각 영역이 물체일 확률 (objectness score)

    # 3단계: Fast R-CNN과 동일 - RoI Pooling
    detections = []
    for proposal in rpn_proposals:
        roi_feature = extract_roi(feature_map, proposal)
        pooled = roi_pooling(roi_feature, (7, 7))

        # 4단계: 분류 및 bbox 정제
        class_scores = classifier(pooled)
        bbox_refined = regressor(pooled)

        detections.append({'bbox': bbox_refined, 'scores': class_scores})

    return detections

# RPN의 핵심 구조
def region_proposal_network(feature_map):
    # 각 위치마다 여러 개의 anchor box 검사
    # 3x3 conv로 각 위치의 특징 추출
    rpn_features = conv3x3(feature_map)

    # 각 anchor가 물체인지 아닌지 판별 (objectness)
    objectness_scores = conv1x1_cls(rpn_features)  # 2개 출력 (물체/배경)

    # 각 anchor의 위치를 조정할 offset 예측
    bbox_deltas = conv1x1_reg(rpn_features)  # 4개 출력 (dx, dy, dw, dh)

    return generate_proposals(objectness_scores, bbox_deltas)

김개발 씨는 코드를 보며 감탄했습니다. "와, 이제 Selective Search가 완전히 사라졌네요!" 박시니어 씨가 웃으며 답했습니다.

"네, 이게 Faster R-CNN의 가장 큰 혁신이에요. Region proposal을 생성하는 작은 신경망을 추가한 거죠." Faster R-CNN이 해결하려는 문제는 무엇이었을까요?

Fast R-CNN까지는 region proposal을 위해 Selective Search라는 전통적인 알고리즘을 사용했습니다. 이건 CPU에서 돌아가고, 학습도 안 되고, 느렸습니다.

연구자들은 생각했습니다. "어차피 feature map에 물체 정보가 다 들어있는데, 이걸 보고 region proposal을 예측하면 안 될까?" RPN의 핵심 아이디어는 무엇일까요?

Feature map의 각 위치를 돌면서 "여기 물체가 있을까?"를 판단하는 것입니다. 각 위치마다 미리 정의된 여러 크기와 비율의 anchor box들을 놓고, 각 anchor가 물체를 포함하는지 아닌지를 예측합니다.

동시에 anchor의 위치를 조금씩 조정하는 offset도 예측합니다. 쉽게 비유하자면, 바둑판의 각 교차점에 여러 크기의 투명한 틀을 겹쳐놓고, "이 틀 안에 뭔가 있나?"를 하나씩 확인하는 것과 같습니다.

빈 곳은 빠르게 넘기고, 뭔가 있어 보이는 곳만 "여기 물체 있음!" 표시를 하는 거죠. RPN의 구조를 자세히 살펴봅시다.

region_proposal_network 함수를 보면, 먼저 3x3 convolution을 적용해 각 위치의 특징을 추출합니다. 그 다음 두 개의 1x1 convolution으로 분기됩니다.

하나는 objectness_scores를 예측합니다. 이건 "물체다/배경이다"를 판별하는 이진 분류입니다.

다른 하나는 bbox_deltas를 예측합니다. 이건 anchor box의 위치를 조정하는 4개의 숫자입니다.

Feature map의 크기가 50x37이고 각 위치마다 9개의 anchor를 사용한다면, 총 50x37x9 = 16,650개의 anchor box를 검사하게 됩니다. 이 중에서 objectness score가 높은 상위 2000개 정도를 region proposal로 선택합니다.

Anchor box는 무엇이고 왜 필요할까요? 하나의 feature map 위치는 원본 이미지의 어느 정도 크기 영역에 대응합니다.

하지만 실제 물체는 다양한 크기와 종횡비를 가집니다. 작은 사람도 있고 큰 차량도 있죠.

가로로 긴 버스도 있고 세로로 긴 신호등도 있습니다. Anchor box는 이런 다양성을 커버하기 위해 미리 정의된 박스 템플릿입니다.

보통 3가지 크기(128², 256², 512²)와 3가지 비율(1:1, 1:2, 2:1)을 조합해서 9개의 anchor를 사용합니다. 각 위치마다 이 9개를 다 검사하는 거죠.

자세한 내용은 다음 카드에서 다룹니다. 학습은 어떻게 진행될까요?

Faster R-CNN은 RPN과 Fast R-CNN 부분을 함께 학습합니다. 처음에는 RPN만 학습시켜 region proposal을 생성하는 법을 가르치고, 다음에는 그 proposal을 사용해 Fast R-CNN 부분을 학습시킵니다.

이후 이 두 네트워크를 번갈아가며 fine-tuning합니다. 최신 구현에서는 두 부분을 동시에 학습하는 방법도 사용됩니다.

실제 성능은 어떨까요? Faster R-CNN은 GPU에서 이미지 한 장을 약 0.2초에 처리합니다.

Fast R-CNN의 0.3초보다도 빠르면서 정확도는 비슷하거나 더 좋습니다. 2015년 COCO 객체 탐지 대회에서 1위를 차지했습니다.

이후 ResNet 같은 강력한 backbone을 사용하면서 정확도는 더욱 높아졌습니다. 박시니어 씨가 화면을 가리키며 설명했습니다.

"보세요, 이제 모든 연산이 CNN 안에서 일어납니다. Selective Search 같은 외부 알고리즘이 없어요.

완전히 end-to-end로 학습 가능한 시스템이죠." 김개발 씨는 코드를 다시 보며 정리했습니다. "그러니까 backbone CNN이 feature map을 만들고, RPN이 그걸 보고 proposal을 생성하고, 그 proposal을 RoI Pooling으로 처리해서 분류하는 거네요!" 박시니어 씨가 고개를 끄덕였습니다.

"정확합니다. 이제 핵심 기법들을 하나씩 깊이 파보죠." Faster R-CNN은 R-CNN 계열의 완성판이라 할 수 있습니다.

이후 YOLO, SSD 같은 1-stage detector들이 등장하지만, 2-stage detector의 대표주자로 여전히 많이 사용됩니다.

실전 팁

💡 - RPN은 작지만 강력한 네트워크로, feature map에서 직접 region proposal 생성

  • Objectness score는 "물체인지 배경인지"만 판별, 구체적인 클래스는 나중에 분류
  • Faster R-CNN 이후 모든 과정이 GPU에서 실행되어 진정한 end-to-end 학습 가능

5. RoI Pooling 이해

김개발 씨는 코드를 리뷰하다가 roi_pooling 함수 앞에서 멈췄습니다. "선배님, 이 부분이 정확히 뭘 하는 건지 잘 모르겠어요.

Max pooling하고는 다른 건가요?" 박시니어 씨가 화이트보드를 가져오며 답했습니다. "좋은 질문이에요.

RoI Pooling은 Fast R-CNN의 핵심 기술 중 하나입니다. 천천히 설명해드릴게요."

RoI Pooling은 Region of Interest Pooling의 약자로, 크기가 다른 feature map 영역을 고정된 크기의 특징으로 변환하는 기법입니다. Feature map에서 추출한 영역을 격자로 나눈 뒤, 각 칸에서 최댓값을 뽑아내는 방식입니다.

이렇게 하면 어떤 크기의 영역이든 항상 동일한 크기(예: 7x7)의 출력을 얻을 수 있어서 Fully Connected layer에 입력할 수 있습니다.

다음 코드를 살펴봅시다.

# RoI Pooling 구현 (간소화된 버전)
import numpy as np

def roi_pooling(feature_map, roi_bbox, output_size=(7, 7)):
    """
    feature_map: (H, W, C) 크기의 feature map
    roi_bbox: [x1, y1, x2, y2] - feature map 좌표계
    output_size: 출력 크기 (height, width)
    """
    x1, y1, x2, y2 = roi_bbox
    roi_height = y2 - y1
    roi_width = x2 - x1

    # 출력 격자의 각 칸 크기 계산 (실수일 수 있음)
    bin_height = roi_height / output_size[0]  # 예: 14 / 7 = 2.0
    bin_width = roi_width / output_size[1]    # 예: 21 / 7 = 3.0

    pooled = np.zeros((output_size[0], output_size[1], feature_map.shape[2]))

    # 출력 격자의 각 칸마다
    for i in range(output_size[0]):
        for j in range(output_size[1]):
            # 해당 칸이 커버하는 영역 계산
            start_h = int(y1 + i * bin_height)
            end_h = int(y1 + (i + 1) * bin_height)
            start_w = int(x1 + j * bin_width)
            end_w = int(x1 + (j + 1) * bin_width)

            # 해당 영역에서 max pooling
            roi_region = feature_map[start_h:end_h, start_w:end_w, :]
            pooled[i, j, :] = np.max(roi_region, axis=(0, 1))

    return pooled  # 항상 (7, 7, C) 크기

박시니어 씨가 화이트보드에 사각형을 그렸습니다. "자, 이게 feature map이라고 생각해보세요.

크기는 50x37입니다. 여기서 RPN이나 Selective Search가 제안한 영역을 추출했는데, 크기가 14x21이라고 가정하죠." 왜 RoI Pooling이 필요할까요?

김개발 씨는 이미 Fast R-CNN에서 이 문제를 본 적이 있습니다. Feature map에서 추출한 각 region proposal의 크기가 제각각이라는 점이 문제였습니다.

어떤 건 10x15, 어떤 건 5x30일 수 있습니다. 하지만 뒤따라 나오는 Fully Connected layer는 고정된 크기의 입력을 요구합니다.

예를 들어 FC layer가 7x7x512 = 25,088차원의 입력을 기대한다면, 모든 RoI를 이 크기로 맞춰야 합니다. R-CNN에서는 어떻게 했을까요?

R-CNN은 각 region을 227x227로 강제로 resize했습니다. 이 과정에서 종횡비가 왜곡되어 가로로 긴 차량이 찌그러지거나 세로로 긴 사람이 뚱뚱해지는 문제가 있었습니다.

정보 손실이 크죠. RoI Pooling의 영리한 해결책은 무엇일까요?

RoI Pooling은 resize 대신 적응형 pooling을 사용합니다. 쉽게 비유하자면, 다양한 크기의 피자를 항상 7x7 = 49조각으로 자르는 것과 같습니다.

큰 피자든 작은 피자든, 49조각으로 자르면 각 조각의 크기는 다르지만 조각 개수는 동일합니다. 각 조각에서 가장 중요한 정보(최댓값)만 뽑아내면 됩니다.

코드로 단계별로 살펴봅시다. 먼저 roi_bbox로 받은 영역의 높이와 너비를 계산합니다.

예를 들어 [10, 5, 24, 26]이라면 높이는 21, 너비는 14입니다. 출력 크기를 7x7로 만들어야 하므로, bin_height = 21/7 = 3.0, bin_width = 14/7 = 2.0이 됩니다.

즉, 각 출력 칸이 커버하는 입력 영역의 크기입니다. 다음으로 이중 for 루프가 출력 격자의 각 칸(총 49개)을 순회합니다.

각 칸마다 해당하는 입력 영역을 계산합니다. 예를 들어 출력의 (0, 0) 칸은 입력의 [5:8, 10:12] 영역에 해당할 수 있습니다.

이 영역에서 np.max로 최댓값을 뽑아냅니다. 채널마다 독립적으로 최댓값을 뽑으므로 결과는 벡터가 됩니다.

실수 좌표는 어떻게 처리할까요? 위 코드에서 bin_height나 bin_width가 정수로 떨어지지 않을 수 있습니다.

예를 들어 높이 22를 7로 나누면 3.14...가 됩니다. 이 경우 int()로 내림하면서 약간의 오차가 생깁니다.

실제 구현에서는 이런 양자화 오차가 학습에 영향을 줄 수 있어서, 나중에 RoI Align이라는 개선된 기법이 나왔습니다. RoI Align은 bilinear interpolation을 사용해 부드럽게 샘플링합니다.

RoI Pooling의 장점은 무엇일까요? 첫째, 종횡비를 보존합니다.

Resize처럼 찌그러뜨리지 않고 원본 비율을 유지한 채로 샘플링합니다. 둘째, 미분 가능합니다.

Max pooling의 일종이므로 역전파가 가능해서 end-to-end 학습이 됩니다. 셋째, 빠릅니다.

단순한 연산이라 GPU에서 효율적으로 실행됩니다. 박시니어 씨가 화이트보드에 그림을 더 그렸습니다.

"보세요, 14x21 영역을 7x7 격자로 나누면 각 칸의 크기가 2x3 정도가 됩니다. 각 칸에서 최댓값만 뽑으면 7x7이 되죠.

크기가 28x42인 다른 영역도 마찬가지로 7x7로 변환됩니다. 칸의 크기만 달라질 뿐이에요." 실제 사용 예시를 보겠습니다.

자율주행 차량이 도로를 촬영한 이미지를 처리한다고 가정합시다. RPN이 멀리 있는 작은 차량과 가까이 있는 큰 차량을 proposal로 제안했습니다.

작은 차는 feature map에서 5x8 영역, 큰 차는 20x30 영역일 수 있습니다. RoI Pooling을 거치면 둘 다 7x7x512 특징이 됩니다.

이제 동일한 FC layer로 둘 다 처리할 수 있습니다. 김개발 씨가 코드를 다시 보며 말했습니다.

"아, 그래서 항상 같은 크기의 출력이 나오는 거군요. 입력이 뭐든 상관없이요!" 박시니어 씨가 미소 지었습니다.

"정확합니다. 이게 Fast R-CNN이 작동하는 핵심 메커니즘이에요." RoI Pooling은 간단하지만 강력한 아이디어입니다.

이후 Mask R-CNN 같은 발전된 모델에서는 RoI Align으로 개선되었지만, 기본 개념은 동일합니다.

실전 팁

💡 - RoI Pooling의 핵심은 적응형 격자 분할 - 크기에 관계없이 동일한 출력 생성

  • 각 격자 칸의 크기가 실수일 수 있어 양자화 오차 발생 가능 (RoI Align으로 개선)
  • Max pooling 방식이므로 역전파 가능하여 end-to-end 학습 지원

6. Anchor Box 개념

프로젝트가 마무리 단계에 접어들자 김개발 씨는 RPN 코드를 최적화하는 작업을 맡았습니다. 코드에 anchor_scales = [128, 256, 512]와 anchor_ratios = [0.5, 1, 2] 같은 설정값이 보였습니다.

"선배님, 이 숫자들은 어떻게 정한 건가요?" 박시니어 씨가 설명을 시작했습니다. "이게 바로 anchor box의 핵심이에요.

RPN이 작동하는 원리와 직접 연결되어 있습니다."

Anchor box는 feature map의 각 위치에 미리 정의된 여러 크기와 비율의 참조 박스입니다. RPN은 이 anchor들이 물체를 포함하는지 판단하고, 위치를 조정하는 offset을 예측합니다.

보통 3가지 크기(scale)와 3가지 비율(aspect ratio)을 조합해 각 위치마다 9개의 anchor를 사용합니다. 이를 통해 다양한 크기와 형태의 물체를 효과적으로 탐지할 수 있습니다.

다음 코드를 살펴봅시다.

# Anchor box 생성 코드 (간소화된 버전)
import numpy as np

def generate_anchors(scales=[128, 256, 512], ratios=[0.5, 1, 2]):
    """
    한 위치에 대한 anchor box들을 생성
    scales: 박스의 기본 크기 (픽셀 단위)
    ratios: 박스의 종횡비 (높이/너비)
    """
    anchors = []
    for scale in scales:
        for ratio in ratios:
            # 비율을 고려한 높이와 너비 계산
            # 면적 = scale^2을 유지하면서 비율 적용
            h = scale * np.sqrt(ratio)
            w = scale / np.sqrt(ratio)

            # 중심이 (0, 0)인 anchor box
            anchor = [-w/2, -h/2, w/2, h/2]
            anchors.append(anchor)

    return np.array(anchors)  # (9, 4) 크기

# Feature map 전체에 대한 anchor 생성
def generate_all_anchors(feature_map_shape, stride=16):
    """
    feature_map_shape: (H, W) - 예: (50, 37)
    stride: feature map과 원본 이미지의 비율 - 보통 16
    """
    base_anchors = generate_anchors()  # (9, 4)
    h, w = feature_map_shape

    all_anchors = []
    for i in range(h):
        for j in range(w):
            # Feature map 좌표를 원본 이미지 좌표로 변환
            center_x = j * stride + stride / 2  # 예: 0*16 + 8 = 8
            center_y = i * stride + stride / 2

            # 각 위치의 9개 anchor를 생성
            for base_anchor in base_anchors:
                # 중심점을 이동
                anchor = base_anchor.copy()
                anchor[0] += center_x  # x1
                anchor[1] += center_y  # y1
                anchor[2] += center_x  # x2
                anchor[3] += center_y  # y2
                all_anchors.append(anchor)

    return np.array(all_anchors)  # (H*W*9, 4) - 예: (16650, 4)

김개발 씨는 화이트보드 앞에 섰습니다. 박시니어 씨가 feature map을 나타내는 격자를 그렸습니다.

"자, 이게 50x37 크기의 feature map이라고 합시다. 각 격자 하나가 원본 이미지의 16x16 영역에 해당합니다." Anchor box는 왜 필요할까요?

객체 탐지의 근본적인 문제는 물체의 크기와 형태가 다양하다는 점입니다. 작은 신호등, 중간 크기의 사람, 큰 버스가 같은 이미지에 나타날 수 있습니다.

또 가로로 긴 차량, 정사각형에 가까운 표지판, 세로로 긴 전신주도 있습니다. 만약 각 feature map 위치에서 하나의 고정된 크기 박스만 검사한다면 어떻게 될까요?

작은 물체는 놓치거나, 큰 물체는 일부만 감지될 것입니다. 다양한 크기를 커버하려면 여러 크기의 박스를 동시에 검사해야 합니다.

Anchor box의 핵심 아이디어는 무엇일까요? 쉽게 비유하자면, anchor box는 마치 옷 사이즈와 같습니다.

S, M, L 세 가지 사이즈에 정상, 톨, 와이드 같은 핏을 조합하면 9가지 옵션이 생깁니다. 모든 사람의 체형을 정확히 맞출 수는 없지만, 이 9가지면 대부분을 커버할 수 있습니다.

필요하면 약간씩 수선(offset 조정)하면 되죠. Anchor box도 마찬가지입니다.

세 가지 크기(128², 256², 512²)와 세 가지 비율(1:2, 1:1, 2:1)을 조합하면 9가지 템플릿이 만들어집니다. 실제 물체가 이 중 하나와 정확히 일치하지 않아도, 가장 가까운 anchor를 선택하고 위치를 조금 조정하면 됩니다.

코드를 단계별로 살펴봅시다. generate_anchors 함수는 한 위치에 대한 9개의 기본 anchor를 만듭니다.

각 scale과 ratio 조합마다 높이와 너비를 계산합니다. 예를 들어 scale=256, ratio=2라면 h = 256 * √2 ≈ 362, w = 256 / √2 ≈ 181입니다.

면적은 대략 256²을 유지하면서 높이가 너비의 2배인 박스가 만들어집니다. 결과는 중심이 (0, 0)인 박스 좌표 [-w/2, -h/2, w/2, h/2]로 표현됩니다.

이게 기준 anchor이고, 나중에 각 feature map 위치로 이동시킵니다. Feature map 전체에 anchor를 배치하는 과정을 보겠습니다.

generate_all_anchors 함수는 feature map의 각 위치마다 9개의 anchor를 생성합니다. stride=16은 feature map의 한 칸이 원본 이미지의 16 픽셀에 해당한다는 의미입니다.

Feature map 좌표 (i, j)는 원본 이미지의 (i16+8, j16+8) 위치에 대응합니다. +8은 칸의 중심을 가리키기 위함입니다.

각 위치의 중심점을 계산한 뒤, 9개의 기본 anchor를 이 중심으로 이동시킵니다. 50x37 feature map이라면 총 50 * 37 * 9 = 16,650개의 anchor가 생성됩니다.

엄청나게 많아 보이지만, 이 중 대부분은 배경이고 실제로 물체와 매칭되는 건 극소수입니다. RPN은 anchor를 어떻게 사용할까요?

RPN의 출력을 다시 생각해봅시다. 각 anchor마다 두 가지를 예측합니다.

첫째, objectness score - 이 anchor가 물체를 포함하는가? 둘째, bbox deltas - 이 anchor를 얼마나 조정해야 실제 물체를 정확히 감싸는가?

예를 들어 어떤 anchor의 objectness score가 0.95로 높게 나왔다면, "여기 물체 있다!"고 판단합니다. 동시에 bbox deltas가 [+10, -5, +15, +8] 같은 값으로 나왔다면, anchor의 위치를 이만큼 조정해서 최종 region proposal을 만듭니다.

학습할 때는 어떻게 될까요? 각 anchor를 ground truth 박스와 비교합니다.

IoU(Intersection over Union)를 계산해서 0.7 이상이면 positive anchor(물체), 0.3 이하면 negative anchor(배경)로 레이블링합니다. Positive anchor는 objectness가 1에 가깝게, bbox deltas는 ground truth에 맞게 학습됩니다.

Negative anchor는 objectness가 0에 가깝게 학습됩니다. 박시니어 씨가 화이트보드에 그림을 그렸습니다.

"보세요, 이 자동차 주변에 여러 anchor들이 겹쳐져 있습니다. 이 중에서 파란색 anchor가 IoU가 가장 높아서 positive로 선택됩니다.

다른 anchor들은 무시되거나 negative로 학습되죠." 실무에서 anchor 설정은 어떻게 할까요? Anchor의 크기와 비율은 데이터셋의 특성에 맞춰 조정합니다.

COCO 데이터셋처럼 작은 물체가 많으면 작은 scale(64, 128)을 추가합니다. 자율주행처럼 가로로 긴 차량이 많으면 1:3 비율을 추가할 수 있습니다.

최근에는 anchor를 자동으로 학습하거나, anchor-free 방식도 연구되고 있습니다. 김개발 씨가 코드를 보며 정리했습니다.

"그러니까 anchor는 일종의 초기 추측값이고, RPN이 이걸 정교하게 조정하는 거네요!" 박시니어 씨가 박수를 쳤습니다. "완벽해요!

이제 R-CNN 계열의 핵심을 다 이해했습니다." Anchor box는 Faster R-CNN 이후 SSD, RetinaNet 같은 다른 객체 탐지 모델에서도 널리 사용되는 중요한 개념입니다. 최근에는 YOLO v3 이후 버전이나 FCOS 같은 anchor-free 방식도 나왔지만, 여전히 많은 모델이 anchor 기반입니다.

실전 팁

💡 - Anchor box는 다양한 크기/비율을 커버하는 템플릿 - 보통 9개(3 scales × 3 ratios) 사용

  • Feature map의 stride(보통 16)에 따라 anchor의 실제 위치가 원본 이미지에 매핑됨
  • 학습 시 IoU 기준(positive: >0.7, negative: <0.3)으로 anchor를 레이블링하여 RPN 학습

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Python#ObjectDetection#RCNN#DeepLearning#ComputerVision#Object Detection,R-CNN

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스