logo

이미지 매칭

ORB 특징점과 이진 디스크립터를 매칭하고 RANSAC 호모그래피로 평면 물체의 위치 추정

실습 준비

import cv2 as cv
import numpy as np
from yeom import to_pil
# 기준 이미지는 원본이 612×612이므로 장면 이미지와 비슷한 크기로 축소
img1 = cv.imread("stop_sign1.jpg")
img1 = cv.resize(img1, (250, 250))
img2 = cv.imread("stop_sign2.jpg")

# 특징점 검출은 밝기 영상에서 수행
gray1 = cv.cvtColor(img1, cv.COLOR_BGR2GRAY)
gray2 = cv.cvtColor(img2, cv.COLOR_BGR2GRAY)

img1.shape, img2.shape
실행 결과
((250, 250, 3), (270, 250, 3))

이미지 매칭

이미지 매칭(image matching)은 서로 다른 두 이미지에서 같은 물체의 대응점을 찾는 과정. 픽셀값을 위치별로 비교하는 대신 크기와 시점 변화에도 구별하기 쉬운 지역 특징을 사용.

  • 키포인트(keypoint): 모서리처럼 위치를 다시 찾기 쉬운 특징점
  • 디스크립터(descriptor): 키포인트 주변 모양을 비교 가능한 값으로 표현한 특징 기술자
  • 매치(match): 두 이미지에서 디스크립터가 비슷한 키포인트의 대응 쌍

이 수업에서는 기준 이미지의 정지 표지판을 장면 이미지에서 탐색.

기준 이미지: 흰 배경의 정지 표지판 기준 이미지

장면 이미지: 나무 앞에 설치된 정지 표지판 장면 이미지

ORB 특징 추출

ORB(Oriented FAST and Rotated BRIEF)는 특징점 검출과 디스크립터 생성을 함께 수행.

  • FAST: 후보점 주변 원 위의 픽셀 밝기를 비교하여 코너 검출
  • Oriented FAST: 코너 주변의 밝기 분포로 특징점 방향 계산
  • Rotated BRIEF: 특징점 방향에 맞춰 주변 픽셀 쌍을 비교하고 이진 디스크립터 생성

FAST가 후보점 주변의 원형 픽셀을 검사하는 방식

BRIEF 디스크립터는 주변 픽셀 쌍의 밝기 비교 결과를 0과 1로 저장. ORB의 기본 디스크립터는 키포인트 하나당 32바이트(256비트).

detector = cv.ORB_create()  # nfeatures 기본값 500은 검출할 최대 키포인트 수

kp1, desc1 = detector.detectAndCompute(gray1, None)
kp2, desc2 = detector.detectAndCompute(gray2, None)

len(kp1), len(kp2)  # 이미지별 키포인트 수
실행 결과
(400, 423)
desc1.shape, desc2.shape  # 이미지별 디스크립터 형태
실행 결과
((400, 32), (423, 32))

detectAndCompute()는 키포인트 목록과 디스크립터 배열을 함께 반환. 첫 번째 이미지에서는 400개, 두 번째 이미지에서는 423개를 검출. 기본 상한은 500개지만 조건을 만족하는 코너가 적으면 검출 수도 감소. 열 수 32는 키포인트 하나당 32바이트를 뜻한다.

특징 매칭

ORB 이진 디스크립터는 해밍 거리(Hamming distance)로 비교. 해밍 거리는 두 비트열에서 값이 다른 비트의 개수. 거리가 작을수록 주변 모양이 비슷하다.

전수 조사 매처(brute-force matcher)는 첫 번째 이미지의 각 디스크립터를 두 번째 이미지의 모든 디스크립터와 비교. crossCheck=True는 양쪽에서 서로를 가장 가까운 후보로 선택한 쌍만 유지.

matcher = cv.BFMatcher(
    cv.NORM_HAMMING,   # 이진 디스크립터이므로 해밍 거리
    crossCheck=True,   # 양방향으로 최근접인 쌍만 유지
)
matches = matcher.match(desc1, desc2)
matches = sorted(matches, key=lambda x: x.distance)

match_distances = [match.distance for match in matches]
len(matches)  # 매칭 수
실행 결과
116
(
    min(match_distances),
    float(np.median(match_distances)),
    max(match_distances),
)  # 최소 거리, 중앙값, 최대 거리
실행 결과
(32.0, 62.0, 85.0)

queryIdximg1, trainIdximg2의 키포인트 인덱스. 두 인덱스로 매치가 연결한 실제 좌표 확인.

matched_image = cv.drawMatches(
    img1,
    kp1,
    img2,
    kp2,
    matches,
    None,
    flags=cv.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS,
)
to_pil(matched_image)

ORB 디스크립터의 상호 최근접 매칭 결과

표지판의 글자와 테두리에 많은 대응점이 연결되지만 배경 오매칭도 남는다. crossCheck=True만으로 모든 오매칭 제거는 불가.

RANSAC과 호모그래피

정지 표지판은 거의 평면이라 한 이미지의 표지판을 다른 이미지에 맞추는 호모그래피 계산 가능. 호모그래피는 기하학적 변환에서 다룬 3×3 투시 변환 행렬과 같다. 최소 네 쌍의 대응점이 필요하지만 실제 매칭에는 오매칭이 섞이므로 더 많은 점과 RANSAC을 함께 사용.

  • 인라이어(inlier): 같은 호모그래피로 설명되는 일관된 매칭
  • 아웃라이어(outlier): 잘못 연결되었거나 변환과 맞지 않는 매칭
  • 재투영 임계값 5.0: 변환한 점과 실제 대응점의 거리가 5픽셀 이내이면 인라이어 후보로 판단
# 매치가 가리키는 키포인트 좌표를 (N, 1, 2) 형태로 모음
src_pts = np.float32(
    [kp1[match.queryIdx].pt for match in matches]
).reshape(-1, 1, 2)
dst_pts = np.float32(
    [kp2[match.trainIdx].pt for match in matches]
).reshape(-1, 1, 2)

homography, mask = cv.findHomography(
    src_pts,
    dst_pts,
    cv.RANSAC,
    5.0,  # 재투영 오차 허용치(픽셀)
)

len(matches), int(mask.sum())  # 전체 매칭 수, 인라이어 수
실행 결과
(116, 46)
float(mask.mean())  # 인라이어 비율
실행 결과
0.39655172413793105

116개 매칭 중 46개가 하나의 일관된 변환을 지지. 인라이어 비율은 약 39.7%이며 나머지는 호모그래피 계산에서 제외.

장면에서 표지판 위치 표시

기준 이미지의 네 모서리를 호모그래피로 변환해 장면 이미지에서의 위치를 확인.

h, w = img1.shape[:2]
corners = np.float32(
    [
        [0, 0],
        [0, h - 1],
        [w - 1, h - 1],
        [w - 1, 0],
    ]
).reshape(-1, 1, 2)

# 기준 이미지의 네 모서리를 장면 이미지 좌표로 변환
projected_corners = cv.perspectiveTransform(
    corners,
    homography,
)

localized = img2.copy()
cv.polylines(
    localized,
    [np.int32(projected_corners)],
    True,          # 닫힌 다각형
    (0, 255, 0),
    3,
    cv.LINE_AA,
)
to_pil(localized)

호모그래피로 추정한 기준 이미지의 위치

초록색 사각형은 팔각형 표지판의 경계가 아니라 흰 여백을 포함한 img1 전체의 네 모서리를 투영한 결과. 사각형이 장면의 표지판을 포함해 호모그래피가 기준 이미지의 위치를 찾았음을 확인.

기준 이미지는 표지판을 꽉 채운 클로즈업이라 투영 사각형도 장면 전체와 비슷한 크기. 오른쪽 위 꼭짓점은 y34로 이미지 밖에 놓여 화면에서 잘린다.

겹쳐서 보기

기준 이미지를 장면 이미지의 시점에 맞게 투시 변환한 뒤 반투명 합성으로 정렬 상태 확인.

warped = cv.warpPerspective(
    img1,
    homography,
    (img2.shape[1], img2.shape[0]),  # 출력 크기는 (너비, 높이)
)

alpha = 0.5  # 겹칠 때 기준 이미지의 비율
overlay = cv.addWeighted(
    img2,
    1 - alpha,
    warped,
    alpha,
    0,
)
to_pil(overlay)

장면 이미지와 투시 변환한 기준 이미지를 겹친 결과

표지판의 외곽과 글자는 비슷한 위치에서 겹친다. 호모그래피가 설명하지 못하는 배경과 기준 이미지의 그림자는 불일치.

좋은 매칭점만 보기

cv.findHomography()가 반환한 mask에서 1은 인라이어, 0은 아웃라이어. 인라이어만 다시 그려 실제 호모그래피를 지지한 대응점 확인.

matchesMask = mask.ravel().tolist()
good_matches = [
    match
    for match, keep in zip(matches, matchesMask)
    if keep == 1
]

inlier_image = cv.drawMatches(
    img1,
    kp1,
    img2,
    kp2,
    good_matches,
    None,
    flags=cv.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS,
)
to_pil(inlier_image)

len(good_matches)
실행 결과
46

RANSAC이 선택한 46개의 인라이어 매칭

원래 116개 매칭에서 배경 오매칭이 줄고 표지판 내부의 일관된 대응점이 주로 남는다.

적용 조건과 한계

  • 기준 물체: 글자, 모서리, 무늬처럼 구별 가능한 특징 필요
  • 흐림, 가림, 심한 조명·시점 변화: 특징점과 인라이어 감소
  • 입체 물체 전체: 같은 평면을 가정하는 하나의 호모그래피로 정렬 곤란
  • 비슷한 반복 무늬를 가진 다른 물체: 오매칭 가능
  • 실제 판정: 최소 인라이어 수와 비율을 데이터에 맞게 설정

퀴즈

문제 1 / 8맞음: 0힌트: 0틀림: 0채점중: 0남음: 8

두 장의 정지 표지판 이미지가 서로 다른 각도에서 찍혔을 때, 이미지 매칭이 먼저 찾는 정보에 가장 가까운 것은?

  • 두 이미지에서 서로 대응되는 특징점 쌍
  • 두 이미지의 평균 밝기 차이 하나
  • 두 이미지의 전체 픽셀 개수 차이
  • 두 이미지에서 각각 검출된 키포인트의 총 개수

퀴즈를 풀려면 대화형 기능을 불러와야 합니다.

Previous
허프 변환