1"""
214. 특징점 매칭
3- BFMatcher (Brute Force)
4- FLANN 매처
5- 좋은 매칭 선별 (ratio test)
6- Homography 계산
7"""
8
9import cv2
10import numpy as np
11
12
13def create_test_images():
14 """매칭용 테스트 이미지 쌍 생성"""
15 # 원본 이미지
16 img1 = np.zeros((300, 400, 3), dtype=np.uint8)
17 img1[:] = [200, 200, 200]
18
19 # 특징이 있는 패턴
20 cv2.rectangle(img1, (50, 50), (150, 150), (50, 50, 50), -1)
21 cv2.circle(img1, (250, 100), 40, (100, 100, 100), -1)
22 cv2.rectangle(img1, (300, 150), (380, 250), (80, 80, 80), -1)
23
24 # 체커보드 패턴 추가
25 for i in range(3):
26 for j in range(3):
27 x, y = 100 + i * 30, 180 + j * 30
28 if (i + j) % 2 == 0:
29 cv2.rectangle(img1, (x, y), (x + 30, y + 30), (0, 0, 0), -1)
30
31 cv2.putText(img1, 'MATCH', (150, 280), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 0), 2)
32
33 # 변환된 이미지 (회전 + 스케일)
34 h, w = img1.shape[:2]
35 center = (w // 2, h // 2)
36 M = cv2.getRotationMatrix2D(center, 15, 0.9) # 15도 회전, 0.9배 스케일
37 img2 = cv2.warpAffine(img1, M, (w, h), borderValue=(200, 200, 200))
38
39 return img1, img2
40
41
42def bf_matcher_demo():
43 """Brute Force 매처 데모"""
44 print("=" * 50)
45 print("BFMatcher (Brute Force Matcher)")
46 print("=" * 50)
47
48 img1, img2 = create_test_images()
49 gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
50 gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
51
52 # ORB 특징점 검출
53 orb = cv2.ORB_create()
54 kp1, des1 = orb.detectAndCompute(gray1, None)
55 kp2, des2 = orb.detectAndCompute(gray2, None)
56
57 # BFMatcher 생성
58 # NORM_HAMMING: 이진 디스크립터용 (ORB, BRIEF)
59 # NORM_L2: 실수 디스크립터용 (SIFT, SURF)
60 bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
61
62 # 매칭
63 matches = bf.match(des1, des2)
64
65 # 거리순 정렬
66 matches = sorted(matches, key=lambda x: x.distance)
67
68 print(f"키포인트: img1={len(kp1)}, img2={len(kp2)}")
69 print(f"매칭 수: {len(matches)}")
70
71 # 상위 매칭 정보
72 print("\n상위 5개 매칭:")
73 for i, m in enumerate(matches[:5]):
74 print(f" {i}: queryIdx={m.queryIdx}, trainIdx={m.trainIdx}, distance={m.distance:.1f}")
75
76 # 결과 그리기
77 result = cv2.drawMatches(
78 img1, kp1, img2, kp2,
79 matches[:20], None,
80 flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS
81 )
82
83 print("\nBFMatcher 특성:")
84 print(" - 모든 쌍 비교 (O(n*m))")
85 print(" - crossCheck=True: 상호 최근접만 선택")
86 print(" - 정확하지만 느림")
87
88 cv2.imwrite('bf_match_img1.jpg', img1)
89 cv2.imwrite('bf_match_img2.jpg', img2)
90 cv2.imwrite('bf_match_result.jpg', result)
91
92
93def knn_match_demo():
94 """KNN 매칭과 Ratio Test 데모"""
95 print("\n" + "=" * 50)
96 print("KNN 매칭 + Ratio Test")
97 print("=" * 50)
98
99 img1, img2 = create_test_images()
100 gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
101 gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
102
103 orb = cv2.ORB_create()
104 kp1, des1 = orb.detectAndCompute(gray1, None)
105 kp2, des2 = orb.detectAndCompute(gray2, None)
106
107 # BFMatcher (crossCheck=False for knnMatch)
108 bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=False)
109
110 # KNN 매칭 (k=2)
111 matches = bf.knnMatch(des1, des2, k=2)
112
113 print(f"KNN 매칭 수: {len(matches)}")
114
115 # Lowe's Ratio Test
116 # 최근접 거리 / 차선 거리 < threshold
117 good_matches = []
118 for m, n in matches:
119 if m.distance < 0.75 * n.distance:
120 good_matches.append(m)
121
122 print(f"Ratio Test 후: {len(good_matches)}")
123
124 # 결과 그리기
125 result = cv2.drawMatches(
126 img1, kp1, img2, kp2,
127 good_matches, None,
128 flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS
129 )
130
131 print("\nRatio Test (Lowe's ratio):")
132 print(" - 최근접 / 차선 < 0.75 (보통 0.7~0.8)")
133 print(" - 모호한 매칭 제거")
134 print(" - 오매칭 감소")
135
136 cv2.imwrite('knn_match_result.jpg', result)
137
138 return kp1, kp2, good_matches
139
140
141def flann_matcher_demo():
142 """FLANN 매처 데모"""
143 print("\n" + "=" * 50)
144 print("FLANN Matcher")
145 print("=" * 50)
146
147 img1, img2 = create_test_images()
148 gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
149 gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
150
151 try:
152 # SIFT 사용 (실수 디스크립터)
153 sift = cv2.SIFT_create()
154 kp1, des1 = sift.detectAndCompute(gray1, None)
155 kp2, des2 = sift.detectAndCompute(gray2, None)
156
157 # FLANN 파라미터 (SIFT/SURF용)
158 FLANN_INDEX_KDTREE = 1
159 index_params = dict(algorithm=FLANN_INDEX_KDTREE, trees=5)
160 search_params = dict(checks=50)
161
162 except AttributeError:
163 # SIFT 없으면 ORB 사용
164 print("SIFT 없음, ORB 사용")
165 orb = cv2.ORB_create()
166 kp1, des1 = orb.detectAndCompute(gray1, None)
167 kp2, des2 = orb.detectAndCompute(gray2, None)
168
169 # FLANN 파라미터 (ORB용)
170 FLANN_INDEX_LSH = 6
171 index_params = dict(
172 algorithm=FLANN_INDEX_LSH,
173 table_number=6,
174 key_size=12,
175 multi_probe_level=1
176 )
177 search_params = dict(checks=50)
178
179 # FLANN 매처 생성
180 flann = cv2.FlannBasedMatcher(index_params, search_params)
181
182 # KNN 매칭
183 matches = flann.knnMatch(des1, des2, k=2)
184
185 # Ratio Test
186 good_matches = []
187 for pair in matches:
188 if len(pair) == 2:
189 m, n = pair
190 if m.distance < 0.75 * n.distance:
191 good_matches.append(m)
192
193 print(f"FLANN 매칭: {len(matches)} → 좋은 매칭: {len(good_matches)}")
194
195 # 결과 그리기
196 result = cv2.drawMatches(
197 img1, kp1, img2, kp2,
198 good_matches, None,
199 flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS
200 )
201
202 print("\nFLANN 특성:")
203 print(" - 근사 최근접 이웃 탐색")
204 print(" - 대규모 데이터에 효율적")
205 print(" - KD-Tree (SIFT) 또는 LSH (ORB)")
206
207 cv2.imwrite('flann_match_result.jpg', result)
208
209
210def homography_demo():
211 """호모그래피 계산 데모"""
212 print("\n" + "=" * 50)
213 print("호모그래피 (Homography)")
214 print("=" * 50)
215
216 img1, img2 = create_test_images()
217 gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
218 gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
219
220 orb = cv2.ORB_create()
221 kp1, des1 = orb.detectAndCompute(gray1, None)
222 kp2, des2 = orb.detectAndCompute(gray2, None)
223
224 bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=False)
225 matches = bf.knnMatch(des1, des2, k=2)
226
227 # Ratio Test
228 good_matches = []
229 for m, n in matches:
230 if m.distance < 0.75 * n.distance:
231 good_matches.append(m)
232
233 print(f"좋은 매칭 수: {len(good_matches)}")
234
235 if len(good_matches) >= 4:
236 # 매칭점 좌표 추출
237 src_pts = np.float32([kp1[m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2)
238 dst_pts = np.float32([kp2[m.trainIdx].pt for m in good_matches]).reshape(-1, 1, 2)
239
240 # 호모그래피 계산 (RANSAC)
241 H, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)
242 matches_mask = mask.ravel().tolist()
243
244 inliers = sum(matches_mask)
245 print(f"인라이어: {inliers}/{len(good_matches)}")
246
247 if H is not None:
248 print(f"\n호모그래피 행렬:\n{H}")
249
250 # img1의 경계를 img2에 투영
251 h, w = img1.shape[:2]
252 pts = np.float32([[0, 0], [w, 0], [w, h], [0, h]]).reshape(-1, 1, 2)
253 dst = cv2.perspectiveTransform(pts, H)
254
255 # img2에 경계 그리기
256 result = img2.copy()
257 dst = np.int32(dst)
258 cv2.polylines(result, [dst], True, (0, 255, 0), 3)
259
260 cv2.imwrite('homography_result.jpg', result)
261
262 # 매칭 시각화 (인라이어만)
263 draw_params = dict(
264 matchColor=(0, 255, 0),
265 singlePointColor=None,
266 matchesMask=matches_mask,
267 flags=2
268 )
269 match_result = cv2.drawMatches(img1, kp1, img2, kp2, good_matches, None, **draw_params)
270 cv2.imwrite('homography_matches.jpg', match_result)
271
272 print("\n호모그래피 용도:")
273 print(" - 이미지 정합 (Image Registration)")
274 print(" - 파노라마 스티칭")
275 print(" - 객체 인식 (위치 추정)")
276 print(" - 증강 현실")
277
278
279def match_object_demo():
280 """객체 매칭 데모"""
281 print("\n" + "=" * 50)
282 print("객체 매칭 실습")
283 print("=" * 50)
284
285 # 템플릿 이미지 (찾을 객체)
286 template = np.zeros((100, 100, 3), dtype=np.uint8)
287 template[:] = [200, 200, 200]
288 cv2.rectangle(template, (10, 10), (90, 90), (50, 50, 50), -1)
289 cv2.circle(template, (50, 50), 20, (100, 100, 100), -1)
290
291 # 장면 이미지 (객체가 포함된)
292 scene = np.zeros((300, 400, 3), dtype=np.uint8)
293 scene[:] = [180, 180, 180]
294
295 # 템플릿을 장면에 배치 (회전 및 스케일 적용)
296 h, w = template.shape[:2]
297 center = (w // 2, h // 2)
298 M = cv2.getRotationMatrix2D(center, 30, 0.8)
299 rotated_template = cv2.warpAffine(template, M, (w, h), borderValue=(180, 180, 180))
300
301 # 장면에 붙이기
302 scene[100:200, 150:250] = rotated_template
303
304 # 다른 객체 추가 (방해물)
305 cv2.circle(scene, (80, 80), 30, (120, 120, 120), -1)
306 cv2.rectangle(scene, (300, 200), (380, 280), (90, 90, 90), -1)
307
308 # 특징점 매칭
309 gray_template = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY)
310 gray_scene = cv2.cvtColor(scene, cv2.COLOR_BGR2GRAY)
311
312 orb = cv2.ORB_create()
313 kp1, des1 = orb.detectAndCompute(gray_template, None)
314 kp2, des2 = orb.detectAndCompute(gray_scene, None)
315
316 bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=False)
317 matches = bf.knnMatch(des1, des2, k=2)
318
319 good = []
320 for m, n in matches:
321 if m.distance < 0.75 * n.distance:
322 good.append(m)
323
324 print(f"템플릿 키포인트: {len(kp1)}")
325 print(f"장면 키포인트: {len(kp2)}")
326 print(f"좋은 매칭: {len(good)}")
327
328 # 결과 시각화
329 result = cv2.drawMatches(template, kp1, scene, kp2, good, None)
330 cv2.imwrite('object_template.jpg', template)
331 cv2.imwrite('object_scene.jpg', scene)
332 cv2.imwrite('object_match.jpg', result)
333
334
335def main():
336 """메인 함수"""
337 # BF 매처
338 bf_matcher_demo()
339
340 # KNN 매칭
341 knn_match_demo()
342
343 # FLANN 매처
344 flann_matcher_demo()
345
346 # 호모그래피
347 homography_demo()
348
349 # 객체 매칭
350 match_object_demo()
351
352 print("\n특징점 매칭 데모 완료!")
353
354
355if __name__ == '__main__':
356 main()