Loading [MathJax]/jax/output/CommonHTML/jax.js
본문 바로가기

OpenCV

매칭(Matching) (2)

1. 키 포인트와 특징점 검출기

■ 특징점을 검출하는 알고리즘은 다음 그림과 같이 OpenCV에서 Feature2D 클래스를 상속받아 만들어지며, 다양한 종류가 있다. 

특징점 검출기 상속 구조 [출처] https://docs.opencv.org/3.4/d0/d13/classcv_1_1Feature2D.html

■ 해리스 코너 검출과 시-토마시 검출 함수는 특징점의 좌표만 반환한다. GFTTDetector, FAST 등의 특징점 검출기 함수는 특징점의 좌표뿐만 아니라 특징점 방향, 특징점 반응 강도 등 다양한 특징점 정보를 함께 반환한다.

■ 특징점 정보를 함께 반환하는 방법은 특징점 검출기 함수로 detector 인스턴스를 생성한 다음, detect 메서드로 이미지의 특징점을 검출하면 된다. keypoints = detector.detect(img, mask)

- img는 입력 이미지

- mask는 검출 제외 마스크, 마스크 행렬 원소가 0이 아닌 위치에서만 특징점을 검출

- 반환되는 keypoints는 특징점 검출 결과로, 특징점 정보를 담은 객체 <KeyPoint>가 담겨있다.

- 이 객체에는 다음과 같은 속성들이 저장되어 있다.

- (1) pt: 특징점 좌표(x, y), float 타입으로 반환되기 때문에 특징점 좌표를 사용하려면 정수로 변환해야 한다.

- (2) size: 특징점 이웃의 반지름

- (3) angle: 특징점 방향, angle의 값이 -1이면 아무 의미가 없는 방향임을 의미

- (4) response: 특징점 반응 강도, 강한 특징점을 선별하는데 사용

- (5) octave: 특징점이 발견된 피라미드 레벨

- (6) class_id: 특징점이 속한 객체 ID

■ detect( ) 메서드 결과인 키 포인트 객체에는 특징점의 다양한 정보들이 담겨있다. 특히 pt 속성은 모든 특징점 검출기가 갖고 있지만, 다른 속성은 검출기에 따라 가지고 있지 않을 수도 있다. 

■ OpenCV에서는 detect( ) 메서드 결과인 keypoints를 지정하면 검출한 특징점과 특징점 검출 시 주된 방향 성분, 크기 등 특징점의 정보를 함께 표현할 수 있는 함수 outImg = cv2.drawKeypoints(img, keypoints, outImg, color, flags)를 제공한다.

- img는 입력 이미지

- keypoints는  detect( ) 메서드 결과인 keypoints

- outImg는  특징점이 그려진 결과 이미지

- color는 특징점을 표시할 색상, 디폴트는 랜덤이다.

- flags는 특징점을 그릴 방법으로 cv2.DRAW_MATCHES_FLAGS_DEFAULT를 지정하면 좌표 중심에 동그라미를 표시, cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS를 지정하면 동그라미의 크기를 size와 angle을 반영해서 표시한다.

1.1 GFTTDetector

■ GFTTDetector는 cv2.goodFeaturesToTrack( ) 함수로 구현된 특징점 검출기로 cv2.GFTTDetector_create(img, maxCorners, qualityLevel, minDistance, corners, mask, blockSize, useHarrisDetector, k) 함수를 통해 생성할 수 있으며,  모든 파라미터는 cv2.goodFeaturesToTrack( ) 함수와 동일하다.

img = cv2.imread('road.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

## Good feature to trac 검출기 생성
gftt = cv2.GFTTDetector_create() 

## detect 메서드로 특징점 검출
keypoints = gftt.detect(gray, None)
print(dir(keypoints[0]))  # keypoints 리스트의 첫 번째 KeyPoint 객체 속성 확인
```#결과#```
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__',
'__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__',
'__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', 
'__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 
'angle', 'class_id', 'convert', 'octave', 'overlap', 'pt', 'response', 'size']
````````````

for i, kp in enumerate(keypoints[0:1]):
    print(f'좌표: {kp.pt}')
    print(f'크기: {kp.size}')
    print(f'방향: {kp.angle}')
    print(f'반응 강도: {kp.response}')
    print(f'피라미드 계층(octave): {kp.octave}')
    print(f'클래스 ID: {kp.class_id}')
```#결과#```
좌표: (290.0, 31.0)
크기: 3.0
방향: -1.0
반응 강도: 0.16614824533462524
피라미드 계층(octave): 0
클래스 ID: -1
````````````
## 키 포인트 그리기
img_draw = cv2.drawKeypoints(img, keypoints, None)

cv2.imshow('GFTTDectector', img_draw)
cv2.waitKey(0)
cv2.destroyAllWindows()

 

1.2 FAST

■ 해리스 코너 검출, 시-토마시 검출 방법은 코너를 검출하는 과정에서 소벨 필터로 그래디언트 계산 등의 복잡한 연산을 수행하기 때문에 연산 속도가 느리다는 단점이 있다. 

■ FAST 검출기는 코너 검출 시 미분 연산을 하지 않고 단순한 픽셀 값 비교(=밝기 비교) 방법을 통해 코너를 검출한다. 그러므로 기존 검출기보다 연산 속도가 빠르다.

- FAST 검출기가 해리스 코너 검출기보다 약 20배 이상 빠르게 동작하는 것으로 알려져 있다.

■ 비교 방법은 예를 들어 다음 그림과 같이 특정 픽셀을 둘러싸고 있는 16개의 주변 픽셀 값(=밝기)들을 비교해서 중심 픽셀 p보다 임곗값 이상 밝거나 어두운 픽셀이 9개 이상 연속으로 존재하면 코너로 판단한다.

■ 점 p에서의 밝기를 Ip, 충분히 밝거나 어두운 정도에 대한 임곗값을 t라고 할 때,

- 주변 16개의 픽셀 중 픽셀 값이 Ip+t보다 큰 픽셀이 9개 이상 연속으로 나타나면 점 p는 어두운 영역이 뾰족하게 돌출되어 있는 코너로 판단하고,

- Ipt보다 작은 픽셀이 9개 이상 연속으로 나타나면 점 p는 밝은 영역이 돌출되어 있는 코너로 판단한다.

[출처] https://www.edwardrosten.com/work/fast.html

■ OpenCV에서는 cv2.FastFeatureDetector_create(threshold, nonmaxSuppression, type) 함수로 FAST 검출기를 생성할 수 있다.

- threshold는 코너를 판단하는 임곗값

- nonmaxSuppression는 비최대 억제 파라미터로, 코너 점수가 최대 점수가 아닌 코너를 억제한다. 디폴트는 True이다. 

- FAST 방법은 p 주변 픽셀들도 함께 코너로 검출하는 경우가 많기 때문에 코너 점수가 가장 큰 코너(가장 코너에 적합한 픽셀)만 선택하는 것이 좋다. 

- type은 엣지 검출 패턴으로 디폴트는 cv2.FastFeatureDetector_TYPE_9_16이다. 

- cv2.FastFeatureDetector_TYPE_9_16으로 지정하면 16개 픽셀 중 9개의 연속된 픽셀을 검사한다. cv2.FastFeatureDetector_TYPE_7_12로 지정하면 12개 픽셀 중 7개의 연속된 픽셀을 검사, cv2.FastFeatureDetector_TYPE_5_8로 지정하면 8개 픽셀 중 5개의 연속된 픽셀을 검사하며, 단계가 내려올수록 더 많은 코너가 검출될 수 있기 때문에 일반적으로 기본값인 TYPE_9_16을 사용하는 것이 권장된다. 

img = cv2.imread('road.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

## FASt 특징 검출기 생성
fast = cv2.FastFeatureDetector_create(50) # threshold = 50

## detect 메서드로 키 포인트 검출
keypoints = fast.detect(gray, None)

## 키 포인트 그리기 
img = cv2.drawKeypoints(img, keypoints, None, flags = cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)

cv2.imshow('FAST', img)
cv2.waitKey()
cv2.destroyAllWindows()

 

2. 특징 디스크립터 추출기

■ 특징점(keypoints)은 이미지의 특징이 되는 부분으로 서로 다른 이미지끼리 매칭할 때 사용한다. 

■ 그중 코너는 이미지가 회전되어도 코너 형태가 유지되기 때문에 회전 불변성(rotation invariance)을 가진 특징점이라고 할 수 있다. 즉, 이미지가 회전하더라도 코너의 경사는 항상 급격하게 변하기 때문에 동일한 특징점을 검출할 수 있다.

하지만, 코너의 문제점은 이미지의 크기가 변경된다면 더 이상 동일한 특징점을 검출할 수 없다는 것이다.

그 이유는 다음 그림과 같이 왼쪽 객체는 사각형(검출기) 안에 엣지의 경사가 급변하는, 휘어지는 코너를 검출할 수 있지만, 객체의 크기를 확대한 경우 동일한 사각형 크기로는 왼쪽 객체에서 찾은 동일한 코너를 찾을 수 없다.

[출처] https://m.blog.naver.com/hasukmin12/222110771579

■ 그러므로 크기가 다른 두 이미지에서 코너 점만 이용하여 객체의 동일한 특징을 찾는 것에는 한계가 있다. 

■ 이 크기 문제를 해결하기 위해 사용하는 것이 바로 특징 디스크립터(feature descriptor)이다. 더 구체적으로는, 특징점과 특징 디스크립터를 함께 사용해서 이미지가 회전하거나, 크기가 변하더라도 동일한 특징을 찾아낼 수 있다.

- 특징 디스크립터를 통해 얻고자 하는 것은 특징점 주변 픽셀의 정보(밝기, 색상, 방향, 크기 등)이다.

■ 특징 디스크립터는 특징점 주변 픽셀의 정보를 계산한다. 특징점 주변 픽셀을 일정한 크기의 영역으로 나눈 다음, 각 영역에 속한 픽셀들의 그래디언트 히스토그램을 계산한 것이다. 

- 일반적으로 다음 그림처럼 상하좌우와 네 방향의 대각선, 45도씩 그래디언트를 계산한다.

■ 이렇게 특징점 주변으로부터 특징점의 주 방향 성분을 계산한다.

■ 이를 이용한 대표적인 알고리즘이 바로 SIFT(Scale-Invariant Feature Transform)이다.

2.1 SIFT

■ SIFT 알고리즘은 크기 변화에 무관한 특징점을 추출하기 위해 입력 이미지의 스케일 스페이스(scale spac)를 구성한다. 

- 스케일 스페이스는 다음 그림과 같이 다양한 표준 편차로 가우시안 블러링을 적용한 이미지 집합이다.

[출처] https://do-my-best.tistory.com/entry/SIFT-Scale-Invariant-Feature-TRansform%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%ED%8A%B9%EC%A7%95-%EC%B6%94%EC%B6%9C-%EB%B0%8F-%EB%A7%A4%EC%B9%AD-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98

- 맨 윗줄을 보면 가우시안 블러링이 적용된 이미지 집합을 옥타브(octave)라고 부른다.

■ 이렇게 구성한 스케이스 스페이스를 위의 그림처럼 가로, 세로 1/2씩 다운 샘플링해서 여러 옥타브를 구성해서 이미지 피라미드를 만든다. https://hyeon-jae.tistory.com/125

 

모폴로지(Morphology) 연산, 이미지 피라미드(Image Pyramid)

1. 모폴로지 연산■ 모폴로지 연산은 노이즈 제거, 구멍 채우기, 끊어진 선 이어 붙이기 등에 사용되며 그레이스케일 영상과 바이너리(binary) 영상에 모두 적용 가능하다. ■ 주로 검은색과 흰색

hyeon-jae.tistory.com

- 이러한 방법을 사용하는 이유는 하나의 객체에 대해 다양한 크기 그리고 노이즈가 적용된 상태에서도 크기에 불변하는 특징을 모두 추출하기 위함이다. 

■ 이 상태에서 특징점을 검출하기 위해 인접한 가우시안 블러링 이미지(동일한 크기를 가진 블러링 이미지)끼리 차영상을 계산한다. 이를 차분 가우시안, DoG(Difference of Gaussian)이라고 한다. 

■ SIFT 알고리즘은 DoG를 통해 지역 극값 위치를 특징점으로 사용하며, 엣지 성분이 강하거나 낮은 명암비를 가진 지점은 특징점에서 제외한다.

엣지 성분이 강하거나 낮은 명암비를 가진 지점은 특징점에서 제외하는 이유는

- 명암비가 낮은 지점은 노이즈에 취약하기 때문이며,

- 엣지 성분이 강한 지점은 이미지가 받은 조명에 의해 발생한 것이므로, 조명 변화에 따라 특징점이 다르게 검출되는 것을 방지하기 위해서이다.

■ 이렇게 추출한 특징점과 위에서 언급한 특징점 디스크립터를 사용한 SIFT 알고리즘은 이미지의 크기, 회전 등의 변환뿐만 아니라 이미지에 노이즈가 있거나 이미지가 받는 조명이 달라져도 동일한 특징점을 검출할 수 있다.

■ OpenCV에서는 detector = cv2.xfeatures2d.SIFT_create(nfeatures, nOctaveLayers, contrastThreshold, edgeThreshold, sigma) 함수를 통해 SIFT를 생성할 수 있다.

- nfeatures는 검출할 최대 특징 개수

- nOctaveLayers는 이미지 파라미드의 계층 수(옥타브 레이어의 개수)

- contrastThreshold는 낮은 명암비를 가진 지점을 제외하기 위한 임곗값

- edgeThreshold는 엣지 성분이 강한 지점을 제외하기 위한 임곗값

- sigma는 이미지 피라미드 0 계층(맨 윗줄)에서 사용할 가우시안 필터의 시그마 값이다.

■ 특징 디스크립터는 keypoints, descriptors = detector.compute(image, keypoins, descriptors) 함수를 사용하여 추출할 수 있다. 특징점을 compute 메서드에 전달하면 특징 디스크립터를 계산해서 반환한다.

■ OpenCV는 특징점 검출과 특징 디스크립터 계산을 한꺼번에 수행하는 keypoints, descriptors = detector.detectAndCompute(image, mask, keypoints, decriptors, useProvidedKeypoints) 함수도 제공한다.

- img는 그레이스케일 이미지

- mask는 특징점 검출에 사용할 마스크, 마스크 행렬 원소가 0이 아닌 위치에서만 특징점 검출

- decriptors는 계산된 디스크립터

- useProvidedKeypoints에는 True로 지정할 경우 detectAndCompute( ) 함수에 인자로 전달된 keypoints를 이용하여 특징 디스크립터를 계산한다.

■ 특징 디스크립터를 추출하기 위해 compute( ) 함수를 사용할 경우, 먼저 특징점 검출기로 특징점을 검출해야 하기 때문에 특징점 검출과 특징 디스크립터를 한꺼번에 반환하는 detectAndCompute( ) 함수를 사용하는 것이 더 편리하다.

img = cv2.imread('cheetah2.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

## SIFT 특징점 검출기 생성
sift = cv2.xfeatures2d.SIFT_create()

## 특징점(키 포인트) 검출 및 특징 디스크립터 계산
keypoints, descriptor = sift.detectAndCompute(gray, None)

print('keypoint:', len(keypoints), 'descriptor:', descriptor.shape)
```#결과#```
keypoint: 722 descriptor: (722, 128)
````````````

- SIFT의 nfeatures 파라미터에 어떠한 값도 지정하지 않았을 때 cheetah2.jpg에서 총 722개의 특징점을 추출한 것을 확인할 수 있다.  그리고 반환된 특징 디스크립터는 특징점 1개당 128개의 특징 디스크립터 값을 사용하는 것을 볼 수 있다.

cv2.imshow('SIFT', img_d)
cv2.waitKey()
cv2.destroyAllWindows()

- 원 크기는 특징점 검출 시 고려한 이웃 영역의 크기를 나타낸다. 그리고 원 중심에서 뻗어 나간 직선은 특징점 주변에서 추출된 주 방향(기울기)을 나타낸다.

 

2.2 SURF(Speeded Up Robust Features)

SIFT는 이미지 피라미드를 사용하기 때문에 속도가 느리다는 단점이 있다. SURF는 SIFT를 개선한 알고리즘으로 SIFT에서 사용한 DoG를 단순한 이진 패턴으로 근사화하여 속도를 향상시켰다.

■ OpenCV에서는 detector = cv2.xfeatures2d.SURF_create(hessianThreshold, nOctaves, nOctaveLayers, extended, upright) 함수를 통해 SURF를 생성할 수 있다.

- hessianThreshold는 특징 추출을 위한 해시안 특징 검출기에서 사용하는 임곗값

- nOctaveLayers는 이미지 파라미드의 계층 수(옥타브 레이어의 개수)

- contrastThreshold는 낮은 명암비를 가진 지점을 제외하기 위한 임곗값

- extended는 디스크립터 생성 플러그. True를 지정하면 특징점 1개당 특징 디스크립터 128개, False를 지정하면 특징점 1개당 특징 디스크립터 64개

- upright는 방향성 계산 여부. True를 지정하면 특징점의 방향을 계산하지 않음, Flase를 지정하면 계산

img = cv2.imread('cheetah2.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

## SURF 특징점 검출기 생성 
surf = cv2.xfeatures2d.SURF_create(1000)
## 특징점(키 포인트) 검출 및 특징점 디스크립터 계산
keypoints, descriptor = surf.detectAndCompute(gray, None)

- 특허 문제로 OpenCV 이전 버전을 사용해야 한다.

 SIFT와 SURF는 특징점 1개당 특징 디스크립터 128 또는 64개를 만들기 때문에 메모리 사용량이 많고 특징점 사이의 거리 계산도 오래 걸린다는 단점이 있다. 

 

2.3 ORB(Oriented FAST and Rotated BRIEF)

■ ORB는 FAST 코너 검출 방법을 이용하여 특징점을 추출한다. 단, 기본적인 FAST 검출은 이미지의 크기 변화에 취약하기 때문에 이미지의 크기를 점진적으로 축소한 이미지 피라미드를 구축하여 특징점을 추출한다.

■ 그리고 디스크립터 검출기로 방향을 고려한 BRIEF(Binary Robust Independent Elementary Features) 알고리즘을 사용한다. 이 알고리즘은 특징점 디스크립터만 생성하며 특징점 검출은 지원하지 않는다.

BRIEF는 특징점 주변의 픽셀 쌍(x, y)을 선택한 다음, 픽셀 값의 크기를 비교해서 x < y이면 1, 그 외는 0을 반환한다. τ(x,y)={1if, I(x)<I(y)0otherwise - 예를 들어 특징점 p 주변에 점 a,b,c가 있다고 할 때, 픽셀 값(=밝기)이 c,b,a 순이라고 하자. ( c 점이 가장 밝고, a 점이 가장 어두움)

- 이 3개의 점에 대해 BRIEF 알고리즘을 사용해서 τ(a,b),τ(c,a),τ(b,c)를 구하면 이진수 101을 얻게 된다. 이 이진수는 ba보다 밝고, ac보다 어둡고, cb보다 밝다는 정보를 표현한 것이다.

- BRIEF는 이렇게 특징점 주변 픽셀의 정보를 이진수 형태로 표현하는 바이너리 디스크립터이기 때문에 각 픽셀(점)들의 거리를 계산하기 위해 해밍 거리 방법을 사용한다.

■ 단 기본적인 BRIEF 알고리즘은 픽셀(점)의 방향을 고려한 알고리즘이 아니라서 ORB 알고리즘은 FAST 기반으로 특징점을 추출한 다음, 각 특징점에서 픽셀 밝기 분포를 이용해 코너 방향 성분을 계산한다.

그리고 256개(256개는 디폴트 값)의 비교 픽셀 쌍에 방향 성분의 방향만큼 회전시켜 회전에 불변한 특징 디스크립터를 계산한다. 

■ OpenCV에서는 detector = cv2.ORB_create(nfeatures, scaleFactor, nlevels, edgeThreshold, firstLevel, WTA_K, scoreType, patchSize, fastThreshold) 함수를 통해 SURF를 생성할 수 있다.

- nfeatures는 검출할 최대 특징 개수

- scaleFactor는 이미지 파라미드 생성 비율(이미지 축소 비율)

- nlevels는 이미지 파라미드의 계층 수(옥타브 레이어의 개수)

- edgeThreshold는 특징을 검출하지 않을 이미지 가장자리 픽셀의 크기로 patchSize와 맞춰 사용한다.

- firstLevel은 최초 이미지 피라미드 계층의 레벨로 0을 지정해야 위의 얼룩말 이미지 스케일 공간의 맨 윗줄처럼 원본 이미지의 크기로 스케일 공간을 만들 수 있다.

- WTA_K는 BRIEF로 특징 디스크립터 계산 시 사용할 임의의 점 개수이다. 2, 3, 4 중 하나를 지정해야 한다.

- scoreType은 특징점 검출에 사용할 방식으로 cv2.ORB_HARRIS_SCORE(해리스 코너 검출) 또는 cv2.ORB_FAST_SCORE(FAST 코너 검출) 중 하나를 지정한다.

- patchSize는 BRIEF 계산 시 사용할 디스크립터의 패치 크기

- fastThreshold는 FAST 코너 검출에 사용할 임곗값이다.

img = cv2.imread('cheetah2.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

## ORB 특징점 검출기 생성
orb = cv2.ORB_create()

## 특징점(키 포인트) 검출 및 특징점 디스크립터 계산
keypoints, descriptor = orb.detectAndCompute(gray, None)

print('keypoint:',len(keypoints), 'descriptor:', descriptor.shape)
```#결과#```
keypoint: 500 descriptor: (500, 32)
````````````

- ORB의 nfeatures 파라미터에 어떠한 값도 지정하지 않았을 때 cheetah2.jpg에서 총 500개의 특징점을 추출한 것을 확인할 수 있다. (nfeatures = 500이 디폴트) 

- 그리고 반환된 특징 디스크립터는 특징점 1개당 32개의 특징 디스크립터 값을 사용하는 것을 볼 수 있다.

## 키 포인트 그리기 
img = cv2.drawKeypoints(img, keypoints, None, flags = cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
cv2.imshow('ORB', img)
cv2.waitKey()
cv2.destroyAllWindows()

 

■ SIFT의 결과와 비교했을 때, 더 많은 특징점을 검출하지만, ORB의 경우 SIFT에 비해 치타의 무늬 특징을 일부만 포착한 것을 볼 수 있다. 즉, ORB는 SIFT에 비해 정확성이 약간 떨어진다.

■ 하지만, SIFT는 1개의 특징점에 32개의 특징 디스크립터를 추출하기 때문에 SIFT보다 연산 속도가 빠르다는 장점이 있다.

■ SIFT, SURF, ORB 알고리즘 선택 기준은 정확도가 중요한 경우 SIFT, 연산 속도가 중요한 경우 ORB, 속도와 정확도의 균형이 중요한 경우 SURF를 사용한다.

 

3. 특징점 매칭

■ 특징점 매칭은 서로 다른 두 이미지에서 추출한 특징점과 특징 디스크립터들을 비교하여 서로 비슷한 특징점을 찾아 비슷한 객체끼리 짝짓는 것을 의미한다. 

■ OpenCV에서는 특징점 매칭을 수행하기 위해 matcher = cv2.DescriptorMatcher_create(matcherType)라는 인터페이스 함수를 제공한다.

- matcherType은 생성할 특징점 매칭기의 유형(매칭 알고리즘)을 지정한다.

matcherType 의미
BruteForce NORM_L2를 사용하는 BFMatcher
BruteForce-L1 NORM_L1을 사용하는 BFMatcher
BruteForce-Hamming NORM_HAMMING을 사용하는 BRMatcher
BruteForce-Hamming(2) NORM_HAMMING2를 사용하는 BFMatcher
FlannBased NORM_L2를 사용하는 FlannBasedMatcher

DescriptorMatcher_create( )를 통해 특징점 매칭기(BFMatcher 또는 FlannBasedMatcher)를 생성한 다음, DescriptorMatcher_create( )가 가지고 있는 매칭 함수 match( ), knnMatch( ), radiusMatch( )를 통해 두 개의 디스크립터를 서로 비교해 매칭을 수행한다. 

- BFMatcher와 FlannBasedMatcher 객체를 별도로 생성할 수 있다. 다음 그림과 같이 두 매칭기는 DescriptorMatcher를 상속받아 만들어진다. 

[출처] https://docs.opencv.org/3.4/db/d39/classcv_1_1DescriptorMatcher.html

match( ), knnMatch( ), radiusMatch( ) 세 함수 모두 queryDescriptors 파라미터를 기준으로 trainDescriptors 파라미터에 맞는 매칭을 찾는다.

먼저 match( ) 함수는 matches = matcher.match(queryDescriptors, trainDescriptors, mask)

- queryDescriptors는 특징 디스크립터 배열로 매칭의 기준이 될 디스크립터를 지정한다.

- trainDescriptors도 특징 디스크립터 배열로 매칭의 대상이 될 디스크립터를 지정한다.

- mask는 서로 매칭 가능한 queryDescriptors와 trainDescriptors를 지정할 때 사용한다. 행 개수는 queryDescriptors 개수와 같아야 하고, 열 개수는 trainDescriptors 개수와 같아야 한다.

- mask 파라미터를 지정하지 않는 경우 매칭 결과인 matches에는 queryDescriptors 디스크립터 개수와 같은 수의 DMatch 객체(매칭 결과가 담긴 객체)가 저장된다.

■  match( ) 함수는 queryDescriptors에 지정된 특징 디스크립터에 대해 가장 유사한 특징 디스크립터를 trainDescriptors에 지정한 특징 디스크립터에서 찾는다.

- match( ) 함수는 queryDescriptors 한 개당 가장 비슷한 trainDescriptors 디스크립터 쌍 하나를 찾는 함수이기 때문에 비슷한 trainDescriptors를 찾지 못하는 경우 반환되는 매칭 결과의 개수는 queryDescriptors 개수보다 적을 수 있다.

knnMatch( ) 함수는 matches = matcher.knnMatch(queryDescriptors, trainDescriptors, k, mask, compactResult)로 지정한  queryDescriptors에 대해 k개의 유사한 trainDescriptors를 찾아 반환하는 함수이다.

- k는 최근접 이웃 중 검출할 매칭 개수

- compactResult는 mask가 None이 아닐 때 사용하는 파라미터로, True로 지정할 경우 매칭이 없으면 매칭 결과에 포함시키지 않는다. 디폴트는 False로 매칭을 찾지 못해도 결과에 queryDescriptors의 ID가 담긴 행이 추가된다.

- 나머지 파라미터는 match( ) 함수와 동일하다.

knnMatch( ) 함수는 queryDescriptors 한 개당 k개의 최근접 이웃 개수만큼 queryDescriptors와 비슷한 trainDescriptors 쌍 k개를 찾는다.

radiusMatch( ) 함수는 matches = matcher.radiusMatch(queryDescriptors, trainDescriptors, maxDistance, mask, compactResult)로 지정한 거리(maxDistance) 안에 있는 queryDescriptors, trainDescriptors 쌍을 모두 찾아 반환하는 함수이다.

- maxDistance는 매칭 대상 거리

- 나머지 파라미터는 match( ) 함수와 동일하다.

■ match( ), knnMatch( ), radiusMatch( ) 함수의 반환 결과 matche매칭 결과가 담긴 DMatch 객체 리스트이다.DMatch에는 queryIdx, trainIdx, imgIdx, distance가 담겨 있으며, 

- queryIdx는 queryDescriptors의 인덱스

- trainIdx는 trainDescriptors의 인덱스

- imgIdx는 trainDescriptors의 이미지 인덱스

- distance는 유사도 거리를 의미한다.

- queryIdx와 trainIdx로 두 이미지의 어느 지점이 서로 매칭 되었는지 확인할 수 있으며, distance로 얼마나 가까운 거리인지도 확인할 수 있다. 

■ 두 이미지에서 추출한 특징점의 매칭 결과를 시각적으로 확인할 수 있도록 OpenCV는 cv2.drawMatches(img1, kp1, img2, kp2, matches, flags) 함수를 제공한다. 

- img1은 첫 번째 입력(queryDescriptors의) 이미지, kp1은 첫 번째 이미지(queryDescriptors)에서 검출된 특징점이다. 

- img2은 두 번째 입력(trainDescriptors의) 이미지, kp1은 첫 번째 이미지(trainDescriptors)에서 검출된 특징점이다. 

- matches는 첫 번째 이미지에서 두 번째 이미지로의 매칭 결과

- flags는 매칭 점을 그릴 방법을 지정한다.

flags 의미
cv2.DRAW_MATCHES_FLAGS_DEFAULT 매칭 결과 이미지 새로 생성(디폴트)
cv2.DRAW_MATCHES_FLAGS_DRAW_OVER_OUTIMG 매칭 결과 이미지 새로 생성하지 않음
cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS 특징점 크기와 방향을 표시
cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS 한쪽만 있는 매칭 결과는 표시하지 않음

 

3.1 BFMatcher

■ BFMatcher는 queryDescriptors와 trainDescriptors를 하나하나 확인해 매칭을 수행하는 조사(Brute-Force) 알고리즘이다.

- queryDescriptors에 있는 모든 특징 디스크립터와 trainDescriptors에 있는 모든 특징 디스크립터 사이의 거리를 계산해서, 가장 거리가 작은 특징 디스크립터를 찾아 매칭을 수행한다.

■ 그러므로 특징점 개수가 늘어나면 거리 계산 횟수가 급격히 증가하게 된다. 

- 예를 들어 첫 번째 입력 이미지에 100개의 특징점이 있고, 두 번째 이미지에 200개의 특징점이 있다면, BFMatcher는 모든 특징 디스크립터 사이의 거리를 계산하기 때문에 총 20,000 번 비교 연산을 수행햐야 한다.

이미지 크기가 크고 특징이 많은 경우 몇 천개의 특징점이 검출될 수 있기 때문에, 이런 경우 BFMatcher 매칭기 대신 FlannBasedMatcher를 사용하는 것이 바람직하다.

■ OpenCV에서 cv2.BFMatcher_create(normType, crossCheck) 함수를 통해 BFMatcher를 생성할 수 있다.

- normType은 거리 계산에 사용할 거리 측정 알고리즘으로 세 가지 유클리드 거리 측정법과 두 가지 해밍 거리 측정법이 있다.

normType 의미
cv2.NORM_L1 i|aibi|
cv2.NORM_L2 (디폴트) i(aibi)2
cv2.NORM_L2SQR i(aibi)2
cv2.NORM_HAMMING i(ai==bi)?1:0 
비트값이 다르면 1, 같으면 0
cv2.NORM_HAMMING2 i(ai==bi)(ai+1==bi+1)?1:0
비트값이 다르면 1, 같으면 0

- cv2.NORM_HAMMING와 cv2.NORM_HAMMING2는 해밍 거리를 계산하기 때문에, 예를 들어 두 개의 이진수에서 비트값이 같지 않은 자리의 개수가 3이라면 해밍 거리는 3이 된다.

- crossCheck을 True로 지정하면 i 번째 queryDescriptors와 가장 비슷한 가 j이고, j 번째 trainDescriptors와 가장 비슷한 queryDescriptors가 i인 경우만 매칭 결과로 반환한다.

- 즉, 양쪽 디스크립터 모두 매칭이 되는 것만 반환하므로 속도는 느려지지만, True로 설정하면 불필요한 매칭을 줄일 수 있다. 디폴트는 False이다.

BFMatcher의 거리 측정법(normType) 선택은

- SIFT와 SURF 디스크립터 검출기를 사용할 경우 cv2.NORM_L1, cv.2NORM_L2가 적합하며,

- ORB 디스크립터 검출기를 사용할 경우, ORB는 BRIEF 알고리즘 기반이므로 cv2.NORM_HAMMING가 적합하다.

- cv2.NORM_HAMMING2는 ORB의 WTA_K가 3 또는 4일 때 적합하다.

## BFMatcher - SIFT
img1 = cv2.imread('box.png') # query image
img2 = cv2.imread('box_in_scene.png') # train image

gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)

## SIFT 디스크립터 검출기 생성
detector = cv2.xfeatures2d.SIFT_create()

## 각 이미지의 키 포인트와 특징 디스크립터 추출
kp1, desc1 = detector.detectAndCompute(gray1, None)
kp2, desc2 = detector.detectAndCompute(gray2, None)

## BFMatcher 생성
matcher = cv2.BFMatcher(cv2.NORM_L1, crossCheck=True) # L1 거리, crossCheck=True

## match( ) 함수로 매칭 계산
matches = matcher.match(desc1, desc2)

## 매칭 결과 그리기 - 한쪽만 있는 매칭 결과는 표시 x
result = cv2.drawMatches(img1, kp1, img2, kp2, matches, None, flags=cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS)

cv2.imshow('result', result)
cv2.waitKey()
cv2.destroyAllWindows()

이미지 출처) https://github.com/opencv/opencv/tree/master/samples/data

 

opencv/samples/data at master · opencv/opencv

Open Source Computer Vision Library. Contribute to opencv/opencv development by creating an account on GitHub.

github.com

## BFMatcher - ORB
img1 = cv2.imread('box.png') # query image
img2 = cv2.imread('box_in_scene.png') # train image

gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)

# # ORB 디스크립터 검출기 생성
detector = cv2.ORB_create()

## 각 이미지의 키 포인트와 특징 디스크립터 추출
kp1, desc1 = detector.detectAndCompute(gray1, None)
kp2, desc2 = detector.detectAndCompute(gray2, None)

## BFMatcher 생성
matcher = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True) # 해밍 거리, crossCheck=True

## match( ) 함수로 매칭 계산
matches = matcher.match(desc1, desc2)

## 매칭 결과 그리기 - 한쪽만 있는 매칭 결과는 표시 x
result = cv2.drawMatches(img1, kp1, img2, kp2, matches, None, flags=cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS)

 

3.2 FlannBasedMatcher

■ FlannBasedMatcher는 전수 조사하는 BFMatcher와 다르게 이웃하는 디스크립터끼리 비교하므로 연산 속도가 BFMatcher보다 훨씬 빠르다.

- 단, 근사화된 거리 계산 방법을 사용해서 가장 거리가 작은 특징점은 찾지 못할 수도 있다.

■ OpenCV에서 cv2.FlannBasedMatcher(indexParams, searchParams) 함수를 통해 FlannBasedMatcher를 생성할 수 있다.

- indexParams는 인덱싱 알고리즘 선택 키와  사용할 알고리즘에 따라 종속 키를 지정한 딕셔너리이다.

- SIFT나 SURF를 사용하는 경우 index_params = dict(algorithm=FLANN_INDEX_KDTREE(1), trees)를 사용하는 것이 권장된다. 여기서 trees는 트리 개수를 의미한다.

- ORB를 사용하는 경우 index_params = dict(algorithm=FLANN_INDEX_LSH(6), table_number, key_size, multi_probe_level)를 사용하는 것이 권장된다. 여기서 table_number는 해시 테이블 수, key_size는 키 비트 크기, multi_probe_level은 인접 버킷 검색을 의미한다.

## FLANN - SIFT
img1 = cv2.imread('box.png') # query image
img2 = cv2.imread('box_in_scene.png') # train image

gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)

# # SIFT 디스크립터 검출기 생성
detector = cv2.xfeatures2d.SIFT_create()

## 각 이미지의 키 포인트와 특징 디스크립터 추출
kp1, desc1 = detector.detectAndCompute(gray1, None)
kp2, desc2 = detector.detectAndCompute(gray2, None)

## index_params, search_params 설정
FLANN_INDEX_KDTREE = 1 
index_params = dict(algorithm = FLANN_INDEX_KDTREE, trees = 5)
search_params = dict(checks=50)   

## flann 매칭기 생성
flann = cv2.FlannBasedMatcher(index_params,search_params)

## 매칭 계산
flann = cv2.FlannBasedMatcher(index_params,search_params)

## 매칭 결과 그리기 - 한쪽만 있는 매칭 결과는 표시 x
result = cv2.drawMatches(img1, kp1, img2, kp2, matches, None, flags=cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS)

cv2.imshow('result', result)
cv2.waitKey()
cv2.destroyAllWindows()

## FLANN - ORB
img1 = cv2.imread('chessboard.jpg') # query image
img2 = cv2.imread('frame01.jpg') # train image

gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)

# # ORB 디스크립터 검출기 생성
detector = cv2.ORB_create()

## 각 이미지의 키 포인트와 특징 디스크립터 추출
kp1, desc1 = detector.detectAndCompute(gray1, None)
kp2, desc2 = detector.detectAndCompute(gray2, None)

## index_params, search_params 설정
FLANN_INDEX_LSH = 6  
index_params= dict(algorithm = FLANN_INDEX_LSH, table_number = 6, key_size = 12, multi_probe_level = 1)

## flann 매칭기 생성
flann = cv2.FlannBasedMatcher(index_params,search_params)

## 매칭 계산
matches = flann.match(desc1, desc2)

## 매칭 결과 그리기 - 한쪽만 있는 매칭 결과는 표시 x
result = cv2.drawMatches(img1, kp1, img2, kp2, matches, None, flags=cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS)
plt.imshow(result), plt.axis('off')

 

4. 좋은 매칭 점 찾기

■ 위에서 매칭을 수행한 결과 잘못된 매칭 결과(매칭 점을 이어주는 직선이 너무 길거나 짧은 경우)가 많이 연결되어 있는 것을 확인할 수 있다. 그러므로 매칭 결과에서 올바른 매칭 점만 추출할 필요가 있다.

- 만약, 올바른 매칭 점만 선택했는데, 매칭 점이 몇 개 없다면 두 이미지는 서로 비슷하지 않다고 판단할 수 있다.

여기 부분 강의 다시 듣기

■ 이 후처리 방법은 DMatch가 가지고 있는 특징 디스크립터 사이의 거리를 나타내는 멤버 변수인 distance를 이용하는 것이다.

- distance 값이 너무 큰(너무 긴 직선은 제외) 매칭 결과는 제외하고 distance 값이 작은 매칭 결과만 사용한다.

■ 먼저, match( ) 함수를 통해 좋은 매칭 점을 찾는 방법은 ORB로 특징 디스크립터를 추출하고 해밍 거리로 매칭을 계산한 다음, 매칭 결과의 distance를 기준으로 특정 임곗값보다 distance 값이 작은 매칭 결과만 추출하면 된다.

img1 = cv2.imread('box.png') # query image
img2 = cv2.imread('box_in_scene.png') # train image

gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)

## ORB로 특징 디스크립터 추출
detector = cv2.ORB_create()
kp1, desc1 = detector.detectAndCompute(gray1, None)
kp2, desc2 = detector.detectAndCompute(gray2, None)

## BFMatcher - Hamming으로 매칭
matcher = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
matches = matcher.match(desc1, desc2)
## 매칭 결과를 거리(distance)기준으로 오름차순 정렬
matches = sorted(matches, key=lambda x:x.distance)
matches
```#결과#```
[< cv2.DMatch >,
 < cv2.DMatch >,
 ...,
````````````

## DMatch 객체에 distance가 존재하는지
if 'distance' in dir(matches[0]): print('ture')
else: print('false')
```#결과#```
ture
````````````

print(f'매칭 최소 거리: {matches[0].distance}')
for matche in matches[-3:]:
    print(f'매칭 최대 거리(top 3): {matche.distance}')
```#결과#```
매칭 최소 거리: 23.0
매칭 최대 거리(top 3): 86.0
매칭 최대 거리(top 3): 86.0
매칭 최대 거리(top 3): 89.0
````````````

- 적절한 임곗값을 선정하기 위해 다음과 같이 box plot으로 distance의 분포를 확인할 수 있다.

distances = [m.distance for m in matches]

plt.figure(figsize=(5, 6))
sns.boxplot(y=distances)
sns.swarmplot(y=distances, color = 'black')
plt.title('distances boxplot')
plt.show()

thresh = np.percentile(distances, 10)
print('thresh', thresh)
```#결과#```
thresh 46.6
````````````

- 이 예에서는 최대한 올바른 매칭 점만 추출하기 위해 제1사분위수보다 낮은 10% 지점에 해당하는 값을 임곗값으로 지정하였다. 적절한 임곗값을 찾기 위해 여러 번 임곗값을 지정해 시도해 봐야 한다.

## 임곗값보다 작은 매칭 결과(여기서는 distance)만 좋은 매칭 결과로 분류
good_matches = [m for m in matches if m.distance < thresh]

print('matches:%d/%d' %(len(good_matches),len(matches)))
```#결과#```
matches:15/149 # 149개 중 15개 선택
````````````

## 좋은 매칭 점만 그리기 
result = cv2.drawMatches(img1, kp1, img2, kp2, good_matches, None, flags=cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS)

cv2.imshow('result', result)
cv2.waitKey()
cv2.destroyAllWindows()

■ knnMatch( ) 함수는 queryDescriptors 한 개당 k개의 최근접 이웃 개수만큼 queryDescriptors와 비슷한 trainDescriptors 쌍 k개를 찾아 가까운 순서대로 반환한다.

■ 즉, k개의 최근접 이웃 중 distance가 가까운 것이 좋은 매칭 점이며, distance가 먼 것은 좋지 않은 매칭 점일 가능성이 높다. 그러므로 최근접 이웃 중 거리가 가까운 것 위주로 선택하면 좋은 매칭 점을 추출할 수 있다.

img1 = cv2.imread('box.png') # query image
img2 = cv2.imread('box_in_scene.png') # train image

gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)

## ORB로 특징 디스크립터 추출
detector = cv2.ORB_create()
kp1, desc1 = detector.detectAndCompute(gray1, None)
kp2, desc2 = detector.detectAndCompute(gray2, None)

## BFMatcher
matcher = cv2.BFMatcher(cv2.NORM_HAMMING)
## knnMatch
matches = matcher.knnMatch(desc1, desc2, 2) # k = 2
matches[:3]
```#결과#```
((< cv2.DMatch >, < cv2.DMatch >),
 (< cv2.DMatch >, < cv2.DMatch >),
 (< cv2.DMatch >, < cv2.DMatch >))
````````````

match_pair = matches[0]  # 첫 번째 매치 쌍
first = match_pair[0]  # 첫 번째
second = first_match_pair[1]  # 두 번째

print(first_match.distance)  # 첫 번째 distance 값
print(second_match.distance)  # 두 번째 distance 값
```#결과#```
81.0
83.0
````````````
## 첫번재 이웃의 거리가 두 번째 이웃 거리의 70% 이내인 것만 추출
ratio = 0.7
good_matches = [first for first,second in matches if first.distance < second.distance * ratio]

print('matches:%d/%d' %(len(good_matches),len(matches)))
```#결과#```
matches:22/453 # 453개 중 22개 선택
````````````

result = cv2.drawMatches(img1, kp1, img2, kp2, good_matches, None, flags=cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS)

cv2.imshow('result', result)
cv2.waitKey()
cv2.destroyAllWindows()

■ 이렇게 후처리 작업을 한 결과, 두 번째 이미지에서 첫 번째 이미지에 해당하는 객체의 위치를 비교적 제대로 매칭하는 것을 볼 수 있다.

 

5. 매칭 영역 원근 변환

■ 두 이미지에서 동일한 객체를 찾을 때, 객체가 약간 회전했거나 크기가 달라져도 특징점 매칭 결과로부터 두 이미지의 원근 변환 행렬(호모그래피(homography))을 이용하면 객체를 정확하게 매칭할 수 있다.

■ 호모그래피는 다음 그림처럼 3차원 공간에서 하나의 평면 A를 서로 다른 시점의 평면 B, C에서 바라봤을 때, 평면 B(또는 평면 C)를 평면 C(또는 평면 B)로 투시 변환(perspective transform)하는 것과 같은 관계를 의미한다.

[출처] https://seongyun-dev.tistory.com/7

- 위의 이미지에서 x=Hx는 평면을 C1 시점에서 바라본 왼쪽 이미지(평면)와 동일한 평면을 C2 시점에서 바라본 오른쪽 이미지(평면) 사이의 관계를 원근 변환 행렬 H를 이용해 표현한 것이다.

■ 호모개리프는 투시 변환과 연산 관점에서 투시 변환과 동일하므로, 호모그래피는 3 x 3 실수 행렬로 표현할 수 있다. 즉, 투시 변환과 마찬가지로 4개의 대응되는 점의 좌표 이동 정보가 있으면 호모그래피 행렬을 구할 수 있다. 

■ 그러나 특징점 매칭 결과로부터 호모그래피를 구할 때는 서로 대응되는 점의 개수가 4개보다 훨씬 많아지므로, 호모그래피 행렬을 이용해야 한다.

■ OpenCV에서는 두 이미지에서 추출된 특징점 매칭 정보로부터 호모그래피를 구하는 함수 mtrx, mask = cv2.findHomography(srcPoints, dstPoints, method, ransacReprojThreshold, mask, maxIters, confidence)를 제공한다.

cf) cv2.getPerspectiveTransform( ) 함수는 4개의 꼭짓점으로 호모그래피 행렬을, cv2.findHomography( ) 함수는 여러 개의 점으로 호모그래피 행렬을 반환

- srcPoints와 dstPoints는 원본 평면의 점 좌표와 목표 평면의 점 좌표이다. 둘 다 CV_32FC2 타입의 변수

- method에는 호모그래피 행렬 계산 방법을 지정한다. 디폴트는 0이다.

method 의미
0 모든 점을 사용해 최소 제곱 오차 
cv2.LMEDS 최소  미디언 제곱(Least Median of Squares)
cv2.RANSAC RANSAC(Random Sample Consensus) 알고리즘
cv2.RHO RANSAC을 개선한 PROSAC(Progressive Sample Consensus) 알고리즘

- RANSAC는 모든 점을 사용하지 않고 임의의 점만 선택해서 사용한다.

- ransacReprojThreshold는 method가 RANSAC나 RHO인 경우 지정하는 최대 허용 재투영 에러로, ransacReprojThreshold값 이내로 특징점이 재투영되는 경우에만 정상으로 간주한다. 정상치와 이상치를 구분하는 임곗값

- maxIters는 계산 반복 횟수로 RANSAC인 경우 사용한다.

- confidence는 신뢰도 (0~1 사이 실수), 디폴트는 0.995

- mtrxmask는 cv2.findHomography( ) 함수의 결과로, mtrx는 원근 변환(호모그래피) 행렬이며 mask는호모그래피 계산에 사용된 점들을 알려 주는 행렬이다.(0은 이상치, 1은 정상치)

- mask를 이용하면 올바른 매칭 점(1)과 나쁜 매칭 점(0)을 구분할 수 있기 때문에 원근 변환 행렬에 맞지 않는 나쁜 매칭 점을 한 번 더 제거할 수 있다.

■ 보통 호모그래피를 계산할 때 최소 제곱 오차를 사용하면 오차가 큰 이상치(=잘못 매칭된 점)가 많이 존재할 수 있다. 이런 경우에는 method를 LMEDS, RANSAC, RHO 방법 중 하나로 설정하는 것이 좋다.

- RANSAC나 RHO 방법은 이상치가 50% 이상인 경우 잘 작동하며, LMEDS 방법은 보통 이상치가 50% 이하인 경우에도 잘 작동한다.

RANSAC나 RHO 방법을 사용하는 경우  srcPoints, dstPoints에 있는 점 중 이상치와 정상치를 구분하는 임곗값(ransacReprojThreshold)을 설정해야 한다. 

- 호모그래피 행렬 × srcPoints와 dstPoints 사이의 거리가 ransacReprojThreshold보다 작으면 정상치로 간주한다.

img1 = cv2.imread('box.png') # query image
img2 = cv2.imread('box_in_scene.png') # train image

gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)

detector = cv2.ORB_create() # ORB 
kp1, desc1 = detector.detectAndCompute(gray1, None)
kp2, desc2 = detector.detectAndCompute(gray2, None)
matcher = cv2.BFMatcher(cv2.NORM_HAMMING2) # BF - 해밍 거리로
matches = matcher.knnMatch(desc1, desc2, 2) # knnMatch( )

## 첫번재 이웃의 거리가 두 번째 이웃 거리의 70% 이내인 것만 추출
ratio = 0.7
good_matches = [first for first,second in matches if first.distance < second.distance * ratio]
print('matches:%d/%d' %(len(good_matches),len(matches)))
```#결과#```
matches:14/453
````````````
## 좋은 매칭 점으로부터 원본 평면의 점 좌표와 목표 평면의 점 좌표 구하기
src_pts = np.float32([kp1[m.queryIdx].pt for m in good_matches]) # queryIdx를 이용해 원본 이미지(평면)의 좌표 선택
dst_pts = np.float32([kp2[m.trainIdx].pt for m in good_matches]) # queryIdx를 이용해 목표 이미지(평면)의 좌표 선택

- m.queryIdx는 입력 이미지의 디스크립터의 인덱스이므로 kp1[m.queryIdx].pt는 ORB로 추출한 모든 특징점 중 m.queryIdx에 해당하는 특징점 좌표들만 선택한 것이다.

- 이렇게 구한 src_pts, dst_pts가 올바른 매칭 점이라면 cv2.findHomography( ) 함수의 결과로 mask의 모든 값은 1을 가질 것이다.

## 원근 변환 행렬 계산
mtrx, mask = cv2.findHomography(src_pts, dst_pts)

print(len(mask)); print(mask)
```#결과#```
14
[[1]
 [1]
 [1]
 [1]
 [1]
 [1]
 [1]
 [1]
 [1]
 [1]
 [1]
 [1]
 [1]
 [1]]
````````````
## 원본 이미지 크기로 변환 영역 좌표 생성
h,w, = img1.shape[:2]
pts = np.float32([[[0,0]], [[0,h-1]], [[w-1,h-1]], [[w-1,0]]])

## 원본 이미지(평면) 좌표를 원근 변환
dst = cv2.perspectiveTransform(pts,mtrx)

## 변환 좌표 영역을 표시
img2 = cv2.polylines(img2,[np.int32(dst)], True, (0, 255, 0), 3, cv2.LINE_AA)

## 좋은 매칭 점만 그리기
result = cv2.drawMatches(img1, kp1, img2, kp2, good_matches, None, flags=cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS)

cv2.imshow('result', res)
cv2.waitKey()
cv2.destroyAllWindows()

■ 이번에는 method를 RANSAC으로 지정해서 cv2.findHomography( )의 결과인 mask 중 이상치(0)이 있는 경우, 이를 제거해서 정상치(1)만 매칭해보자.

img1 = cv2.imread('wolf_template.jpg') # query image
img2 = cv2.imread('wolf.jpg') # train image

gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)

detector = cv2.ORB_create() # ORB 
kp1, desc1 = detector.detectAndCompute(gray1, None)
kp2, desc2 = detector.detectAndCompute(gray2, None)
matcher = cv2.BFMatcher(cv2.NORM_HAMMING2, crossCheck=True) # BF - 해밍 거리로
matches = matcher.match(desc1, desc2) # Match( )

matches = sorted(matches, key=lambda x:x.distance)

## 이상치를 제거하지 않고 모든 매칭 점 표시
res1 = cv2.drawMatches(img1, kp1, img2, kp2, matches, None, flags=cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS)

src_pts = np.float32([kp1[m.queryIdx].pt for m in matches]) 
dst_pts = np.float32([kp2[m.trainIdx].pt for m in matches])
## RANSAC로 계산
mtrx, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)

print(len(mask)); print(); print(mask); print(); print(sum(mask == 0)) # 이상치
```#결과#```
39

[[1]
 [1]
 [1]
...,
...,
 [0]
 [0]
 [0]]

[24]
````````````

print(f'accuracy: {sum(mask == 1) / len(mask)}') # 모든 매칭점 중 정상치의 비율
```#결과#```
accuracy: [0.38461538]
````````````

- mask에서 이상치(0)을 제거하지 않을 경우, 정상치는 약 38%

h,w, = img1.shape[:2]
pts = np.float32([[[0,0]], [[0,h-1]], [[w-1,h-1]], [[w-1,0]]])
dst = cv2.perspectiveTransform(pts,mtrx)

## matchesMask를 사용하여 정상치 매칭만 그리기
matchesMask = mask.ravel().tolist() # 평탄화 후 리스트로
res2 = cv2.drawMatches(img1, kp1, img2, kp2, matches, None, 
                       matchesMask = matchesMask, flags=cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS)
                       
# 결과 출력
cv2.imshow('res1', res1)
cv2.imshow('res2 ', res2)

cv2.waitKey()
cv2.destroyAllWindows()

- 왼쪽 이미지는 이상치를 제거하지 않은 경우, 오른쪽 이미지는 이상치를 제외한 경우의 매칭 결과이다.