< Feature Matching >
이번 장에서는
- 하나의 이미지가 다른 이미지에 대해 특성을 어떻게 매칭하는지
- OpenCV의 FLANN Matcher와 Brute-Force Matcher를 사용해볼 것이다.
Basics of Brute-Force Matcher
Brute-Force matcher는 간단하다. 첫 번째 세트 속 하나의 특성의 디스크립터를 취하고, 그리고 두 번째 세트의 다른 특성들과 거리 계산을 사용하여 매칭이 된다. 그리고 가까운 것이 반환된다.
BF 매처에 대해, 먼저 우리는 cv2.BFMatcher()를 사용해서 BFMatcher 객체를 생성한다. 이는 두 개의 선택적 파라미터를 가지고 있다. 첫 번째로 normType이다. 이는 거리 측정법을 지정한다. 기본값으로는, cv2.NORM_L2로 측정된다. 이는 SIFT, SURF 등에 좋다. ORB, BRIEF,BRISK등과 같이 이진 문자열 기반의 디스크립터들에 대해서는 cv2.NORM_HAMMING(Hamming distance를 사용하여 측정)을 사용해야 한다.
( 출처 - http://www.ktword.co.kr/word/abbr_view.php?m_temp1=3937 )
두 번째 파라미터는 불리언(boolean, 참과 거짓) 변수인 crossCheck의 기본값은 false이다. 값이 True라면, 매쳐는 A 이미지의 i번째 디스크립터와 B 이미지의 j번째 디스크립터를 매칭하여 가장 잘 일치하는 부분을 리턴한다.
이는 A의 i번째 디스크립터 ~ B의 모든 디스크립터를 비교해서, 가장 일치하는 B의 j번째 디스크립터를 뽑고, 반대로 B의 j번째 디스크립터 ~ A의 모든 디스크립터를 비교했을 때, 가장 일치하는 A의 i번째 디스크립터가 되면
생성이 되었다면, 두 가지 중요한 방법은 BFMatcher.match()와 BFMatcher.knnMatch()이다.
BFMatcher.match()
가장 일치하는 것을 반환한다.BFMatcher.knnMatch()
사용자에 의해 정의되는k 에 의해, 가장 일치하는k 개를 반환한다. 추가적인 무언가를 해야할 때 유용하다.
cv2.drawKeypoints()를 사용해서 키포인트들을 그린 것 처럼, cv2.drawMatches()는 매칭들을 그리는데 사용된다. 이는 두 개의 이미지를 수평적으로 쌓아두고 처음 이미지에서 두 번째 이미지로 라인을 그려서 가장 일치하는 매칭을 보여준다. 그리고 또 cv2.drawMatchesKnn은 k개의 잘 일치하는 매칭을 그려준다. 만약에
SURF와 ORB를 사용한 각각 다른 샘플을 보자(서로 다른 거리측정법을 사용한다)
Brute-Force Matching with ORB Descriptors
두 이미지간의 특성을 어떻게 매칭시키는지 볼 것이다. 이 경우, “queryImage”와 “trainImage”를 가지고 있다. 우리는 특성매칭을 사용해서 queryImage를 trainImage 속에서 찾을 것이다.
SIFT 디스크립터를 이용하여 특성을 매치시킬 것이다. 먼저 이미지를 불러오고, 디스크립터를 찾자.
import cv2
import numpy as np
qimg = cv2.imread('./images/queryImage_irene.jpg',0) # queryImage
timg = cv2.imread('./images.trainImage_irene.jpg',0) # trainImage
# timg = cv2.imread('./images/rotated_irene.jpg',0) # trainImage
# SIFT
orb = cv2.ORB_create()
# SIFT로 키포인트와 디스크립터 찾기
kp1,des1 = orb.detectAndCompute(qimg,None)
kp2,des2 = orb.detectAndCompute(timg,None)
그 다음에 cv2.NORM_HAMMING 거리측정법을 사용하고 더 나은 결과를 위해 crossCheck를 켜둔 BFMatcher 객체를 생성하자. 그리고 Matcher.match() 방법을 사용해서 두 이미지에서 최고의 매칭을 얻는다. 이를 거리에 기반하여 오름차순으로 정렬하여 최고의 매칭(거리가 가까움 = low distance)이 앞으로 오게 한다. 그리고 첫 10개만 먼저 그려보자. (눈에 보기 편하게, 더 추가해도 된다.)
# BFMatcher 객체 생성
bf = cv2.BFMatcher(cv2.NORM_HAMMING,crossCheck=True)
# 디스크립터들 매칭시키기
matches = bf.match(des1,des2)
# 거리에 기반하여 순서 정렬하기
matches = sorted(matches, key = lambda x:x.distance)
# 첫 10개 매칭만 그리기
# flags=2는 일치되는 특성 포인트만 화면에 표시!
res = cv2.drawMatches(qimg,kp1,timg,kp2,matches[:10],res,flags=2)
cv2.imshow("Feature Matching",res)
cv2.waitKey(0)
cv2.destroyAllWindows()
10개의 특성 포인트가 매칭이 된 것을 볼 수 있다. 눈에 많은 특성이 매칭되었다.
회전된 사진에도 적용해봤으나 결과가 그리 좋지는 않았다.
What is this Matcher Object?
matches = bf.match(des1,des2) 의 결과는 DMatch 객체들의 리스트이다. DMatch 객체는 다음 특성을 따른다.
- DMatch.distance - 디스크립터 사이의 거리. 값이 낮을 수록 더 좋다.
- DMatch.trainIdx - train 디스크립터 속의 디스크립터의 인덱스이다.
- DMatch.queryIdx - query 디스크립터 속의 디스크립터의 인덱스이다.
- DMatch.imgIdx - train 이미지의 인덱스이다.
Brute-Force Matching with SIFT Descriptors and Ratio Test
이번에는, BFMatcher.knnMatch() 를 사용하여 k개의 베스트 매치를 얻는다. 이 예시에서, k=2로 설정하여 ratio test를 적용해 볼 수 있다.
import cv2
import numpy as np
qimg = cv2.imread('./images/queryImage_irene.jpg',0) # queryImage
timg = cv2.imread('./images/trainImage_irene.jpg',0) # trainImage
res2 = None
sift = cv2.xfeatures2d.SIFT_create()
kp1, des1 = sift.detectAndCompute(qimg,None)
kp2, des2 = sift.detectAndCompute(timg,None)
# 초깃값으로 파라미터 지정
bf = cv2.BFMatcher()
matches = bf.knnMatch(des1,des2,k=2)
# ratio test 적용
good = []
for m,n in matches:
if m.distance < 0.75*n.distance:
good.append([m])
res2 = cv2.drawMatchesKnn(qimg,kp1,timg,kp2,good,res2,flags=2)
cv2.imshow("BF with SIFT",res2)
cv2.waitKey(0)
cv2.destroyAllWindows()
많은 특성 포인트들이 매칭된 모습을 볼 수 있다.
이번의 경우에는 회전되었을 때 거의 정확하게 특성 포인트 매칭을 잘 하고 있는 것을 볼 수 있다.
FLANN based Matcher
FLANN은 Fast Library for Approximate Nearest Neighbors의 줄임말이다. 큰 데이터셋과 높은 차원의 특성에서 가까운 인접점 검색 기능에 대해 최적화된 알고리즘 집합을 포함하고 있다. 대규모 데이터셋의 경우 BFMatcher보다 더 빠르게 작동한다.
FLANN 기반의 매쳐의 경우, 알고리즘에 사용될 관련된 파라미터들을 2개의 딕셔너리 형태로 보내야한다. 하나는 indexParams이다. FLANN의 문서를 요약하자면 SIFT,SURF등과 같은 알고리즘의 경우, 다음의 것을 보낼 수 있다.
index_params = dict(algorithm = FLANN_INDEX_KDTREE,trees=5)
ORB를 사용할 때, 다음의 것을 보낼 수 있다.
index_params = dict(algorithm=FLANN_INDEX_LSH,
table_number = 6, #12
key_size = 12, # 20
multi_probe_level = 1) #2
두 번째 딕셔너리는 SearchParams이다. 이는 인덱스 내에서 트리가 얼마만큼 반복하며 왔다갔다해야하는 지를 정해준다. 높은 값 일수록 정확도가 더 낫지만, 시간이 더 오래 걸린다. 만약에 값을 바꾸고 싶다면, search_params = dict(checks=100)을 보내면 된다.
import cv2
import numpy as np
qimg = cv2.imread('./images/queryImage_irene.jpg',0)
timg = cv2.imread('./images/trainImage_irene.jpg',0)
rimg = cv2.imread('./images/rotated_irene.jpg',0)
res1, res2 = None, None
# SIFT
sift = cv2.xfeatures2d.SIFT_create()
# keypoints & descriptors
kp1, des1 = sift.detectAndCompute(qimg,None)
kp2, des2 = sift.detectAndCompute(timg,None)
kp3, des3 = sift.detectAndCompute(rimg,None)
# FLANN params
FLANN_INDEX_KDTREE = 0
index_params = dict(algorithm=FLANN_INDEX_KDTREE,trees=5)
search_params = dict(checks=50) # 아니면 비워둔 딕셔너리 그대로
flann = cv2.FlannBasedMatcher(index_params,search_params)
matches1 = flann.knnMatch(des1,des2,k=2)
matches2 = flann.knnMatch(des1,des3,k=2)
# 좋은 매칭만 그려야함, 그래서 마스크를 만들어야함
# [1순위 매칭, 2순위 매칭] 을 담는다
matchesMask = [[0,0] for i in range(len(matches))] # len(matches) 만큼 [0,0] 생성
# ratio test
for i,(m,n) in enumerate(matches1):
if m.distance < 0.7*n.distance: # 2순위 매칭 결과의 0.7배보다 더 가까운 값만 취함
matchesMask[i] = [1,0]
draw_params = dict(matchColor = (0,255,0),
singlePointColor = (255,0,0),
matchesMask = matchesMask,
flags = 0)
res1 = cv2.drawMatchesKnn(qimg,kp1,timg,kp2,matches1,res1,**draw_params)
cv2.imshow('FLANN',res1)
cv2.waitKey(0)
cv2.destroyAllWindows()
여기서 팁
‘*' 은 *args로, 함수의 파라미터를 tuple로 저장한다
‘**’은 **kwargs로 keyword argument를 dictonary로 저장한다.
그래서 **draw_params의 형태로 불러온 것!
일단 결과를 보자!
매칭된 것들은 초록색으로 나타나는 것을 볼 수 있다. 대다수의 특성점들이 매칭이 된 것을 볼 수 있다.
for i,(m,n) in enumerate(matches2):
if m.distance < 0.7*n.distance: # 2순위 매칭 결과의 0.7배보다 더 가까운 값만 취함
matchesMask[i] = [1,0]
draw_params = dict(matchColor = (0,255,0),
singlePointColor = (255,0,0),
matchesMask = matchesMask,
flags = 0)
res2 = cv2.drawMatchesKnn(qimg,kp1,rimg,kp3,matches2,res2,**draw_params)
cv2.imshow('FLANN',res2)
cv2.waitKey(0)
cv2.destroyAllWindows()
앞서 구해놨던 matches1을 2로 바꿔주면 된다.
기존의 비교보다는 더 낮은 성능을 보인다!