특징점 검출 및 추적 기초
1. 특징점의 개요
- 특징점(Feature Point)이란?
- 이미지에서 주변 픽셀과 구별되는 특징적인 점
- 코너(corner), 블롭(blob), 엣지(edge) 등
- 특징 (이상적인 특징점은 다음 조건을 만족해야 함)
- 반복성(Repeatability)
- 동일한 객체가 다른 이미지에 나타나도 같은 특징점이 검출되어야 함
- 구별성(Distinctiveness)
- 주변 특징점과 구별될 수 있는 고유한 특성을 가져야 함
- 지역성(Locality)
- 작은 영역에서 정의되어 폐색(occlusion)과 기하학적 변형에 강인해야 함
- 정확성(Accuracy)
- 이미지 내에서 정확한 위치를 가져야 함
- 효율성(Efficiency)
- 실시간 응용을 위해 빠르게 계산될 수 있어야 함
- 반복성(Repeatability)
- 자율주행에서의 특징점 활용
- 위치 추정(Localization)
- 특징점을 이용해 차량의 현재 위치를 추정하는 Visual SLAM 구현
- SLAM: Simultaneous Localization And Mapping (동시 위치추정 및 지도작성)
- 특징점을 이용해 차량의 현재 위치를 추정하는 Visual SLAM 구현
- 객체 인식 및 추적
- 도로 위 다른 차량, 보행자, 표지판 등을 인식하고 추적
- 장면 인식
- 이전에 방문한 장소를 인식하여 루프 클로저(Loop Closure) 탐지
- 움직임 추정
- 연속된 프레임 간의 특징점 대응을 통해 카메라/차량의 움직임 추정
- 위치 추정(Localization)
2. 주요 특징점 검출 알고리즘
2.1. 해리스 코너 검출기
Harris Corner Detector
- 원리
- 이미지의 작은 윈도우를 모든 방향으로 이동시켰을 때 픽셀 값의 변화가 큰 지점을 코너로 판단
- 특징
- 회전에 불변
- 크기 변화에는 민감함
- 장점
- 계산이 간단하고 빠르며
- 코너 검출에 효과적
- 실습 코드 (해리스 코너 검출)
#// file: "feature_detection.py"
import cv2
import numpy as np
# 이미지 불러오기
img_color = cv2.imread('./images/road_image.jpg')
img_gray = cv2.cvtColor(img_color, cv2.COLOR_BGR2GRAY)
# 해리스 코너 검출
img_gray = np.float32(img_gray)
img_gray_dst = cv2.cornerHarris(img_gray, blockSize=2, ksize=3, k=0.04)
# 코너 강화 (dilate)
dst = cv2.dilate(img_gray_dst, None)
# 임계값 이상인 부분을 빨간색으로 표시 (코너)
threshold = 0.01 * dst.max()
img_color[dst > threshold] = [0, 0, 255] # 빨간색으로 표시
cv2.imshow('Harris Corner Detection', img_color)
cv2.waitKey(0)
cv2.destroyAllWindows()
# 코너 개수 출력
corner_count = np.sum(dst > threshold)
print(f"검출된 코너 개수: {corner_count}")
2.2. SIFT
Scale-Invariant Feature Transform (스케일 불변 특징 변환)
- 원리
- 다양한 스케일에서 DoG(Difference of Gaussian) 피라미드를 구성하고, 극값을 찾아 특징점으로 선택
- 각 특징점에 대해 방향과 크기 정보를 포함한 디스크립터(descriptor) 생성
- 특징
- 크기, 회전, 조명 변화에 강인
- 부분적 가림에도 견고함
- 단점
- 계산 비용이 높고
- 특허 문제가 있었음(현재는 해결되어 OpenCV에 포함됨)
실습 코드 (SIFT 특징점 검출)
#// file: "feature_detection.py"
img_color = cv2.imread('./images/road_image.jpg')
img_gray = cv2.cvtColor(img_color, cv2.COLOR_BGR2GRAY)
# SIFT 객체 생성
sift = cv2.SIFT_create()
# 특징점 검출 및 디스크립터 계산
keypoints, descriptors = sift.detectAndCompute(img_gray, None)
# 특징점 그리기
img_sift = cv2.drawKeypoints(img_color, keypoints, None, flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
cv2.imshow(f'SIFT Features (Features count: {len(keypoints)}', img_sift)
cv2.waitKey(0)
cv2.destroyAllWindows()
# 디스크립터 정보 출력
if descriptors is not None:
print(f"특징점 개수: {len(keypoints)}")
print(f"디스크립터 형태: {descriptors.shape}")
print(f"각 디스크립터의 차원: {descriptors.shape[1]}")
2.3. SURF
Speeded-Up Robust Features (더욱 빨라진 강력한 기능)
- 원리
- SIFT와 유사하지만,
- 가우시안 2차 미분 근사를 위해 Box 필터를 사용하여 속도 개선
- 특징
- SIFT보다 빠르면서도
- 비슷한 수준의 강인함을 제공
- 단점
- 여전히 계산 비용이 높고,
- 특허 문제가 있음(현재는 해결되었다고 하지만 몇 가지 조건이 있으며, OpenCV 설정을 수정하여 직접 빌드하여야 함)
- OpenCV에서 SURF를 사용하려면
xfeatures2d모듈을 설치해야 함
- 실습 코드 (SURF 특징점 검출)
(참고: SURF는 opencv-contrib-python 패키지에 포함되어 있으므로, 설치가 필요할 수 있음: pip install opencv-contrib-python)
#// file: "surf_feature_detection.py"
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 이미지 불러오기
img_color = cv2.imread('./images/road_image.jpg')
img_gray = cv2.cvtColor(img_color, cv2.COLOR_BGR2GRAY)
try:
# SURF 객체 생성 (xfeatures2d 모듈 필요)
surf = cv2.xfeatures2d.SURF_create()
# 특징점 검출 및 디스크립터 계산
keypoints_surf, descriptors_surf = surf.detectAndCompute(img_gray, None)
# 특징점 그리기
img_surf = cv2.drawKeypoints(img_color, keypoints_surf, None, flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
# 결과 시각화
cv2.imshow(f'SURF Features (Features count: {len(keypoints_surf)}', img_surf)
cv2.waitKey(0)
cv2.destroyAllWindows()
# 디스크립터 정보 출력
if descriptors_surf is not None:
print(f"SURF 특징점 개수: {len(keypoints_surf)}")
print(f"SURF 디스크립터 형태: {descriptors_surf.shape}")
except AttributeError:
print("SURF를 사용하려면 `opencv-contrib-python`을 설치하고, `cv2.xfeatures2d` 모듈을 불러와야 합니다.")
print("pip install opencv-contrib-python")
except Exception as e:
print(f"SURF 실행 중 오류 발생: {e}")
2.4. ORB
Oriented FAST and Rotated BRIEF (FAST 지향 및 BRIEF 회전)
- 원리
- FAST 특징점 검출기와 BRIEF 디스크립터의 아이디어를 결합하여,
- 회전 불변성과 스케일 불변성을 추가하고
- 속도를 극대화함
- 특징
- SIFT/SURF보다 훨씬 빠르면서도 괜찮은 성능을 보이며
- 실시간 응용에 적합
- 특허 문제가 없음
- 장점
- 실시간 처리 속도
- 오픈소스 라이선스
- 임베디드 시스템이나 모바일 환경에 적합함
- 단점
- SIFT/SURF보다 크기 및 시점 변화에 대한 강인함이 다소 떨어질 수 있음
- 실습 코드 (ORB 특징점 검출)
#// file: "orb_feature_detection.py"
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 이미지 불러오기
img_color = cv2.imread('./images/road_image.jpg')
img_gray = cv2.cvtColor(img_color, cv2.COLOR_BGR2GRAY)
# ORB 객체 생성 (최대 특징점 개수, 스케일 팩터 등 설정 가능)
orb = cv2.ORB_create(nfeatures=500, scaleFactor=1.2, nlevels=8)
# 특징점 검출 및 디스크립터 계산
keypoints_orb, descriptors_orb = orb.detectAndCompute(img_gray, None)
# 특징점 그리기
img_orb = cv2.drawKeypoints(img_color, keypoints_orb, None, flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
# 결과 시각화
cv2.imshow(f'ORB Features (Features count: {len(keypoints_orb)}', img_orb)
cv2.waitKey(0)
cv2.destroyAllWindows()
if descriptors_orb is not None:
print(f"ORB 특징점 개수: {len(keypoints_orb)}")
print(f"ORB 디스크립터 형태: {descriptors_orb.shape}")
3. 특징점 매칭 및 추적
3.1. 특징점 매칭
Feature Matching
- 목표
- 두 이미지(혹은 연속된 프레임)에서 동일한 객체에 해당하는 특징점을 서로 찾아 연결하는 과정
- 방법
- 한 이미지의 특징점 디스크립터와 다른 이미지의 특징점 디스크립터 간의 유사도 측정
- 유클리드 거리(Euclidean Distance)나 해밍 거리(Hamming Distance) 등이 사용됨
- 매칭 종류
- Brute-Force Matcher (BFMatcher)
- 한 특징점의 디스크립터를 다른 이미지의 모든 특징점 디스크립터와 비교하여 가장 유사한 것을 채택
- FLANN Matcher
- 대규모 데이터셋에 효율적인 근접 이웃 검색 알고리즘 사용
- Brute-Force Matcher (BFMatcher)
- 실습 코드 (두 이미지 간 ORB 특징점 매칭)
#// file: "feature_matching.py"
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 매칭을 위한 두 번째 이미지 로드 (첫 번째 이미지와 유사하지만 약간 다른 시점/객체)
img1 = cv2.imread('./images/road_image2.jpg', cv2.IMREAD_GRAYSCALE) # 원본 이미지 (gray1)
img2 = cv2.imread('./images/road_image2_shifted.jpg', cv2.IMREAD_GRAYSCALE) # 약간 이동된 이미지 (gray2)
if img1 is None or img2 is None:
print("두 번째 이미지를 찾을 수 없습니다. 'road_image_shifted.jpg' 파일을 확인하거나 생성하세요.")
exit()
# ORB 특징점 검출기 생성
orb = cv2.ORB_create(nfeatures=500)
# 두 이미지에서 특징점 및 디스크립터 검출
kp1, des1 = orb.detectAndCompute(img1, None)
kp2, des2 = orb.detectAndCompute(img2, None)
# BFMatcher 생성 (ORB는 바이너리 디스크립터이므로 NORM_HAMMING 사용)
bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True) # crossCheck=True는 양방향 매칭으로 오매칭 줄임
# 특징점 매칭
matches = bf.match(des1, des2)
# 매칭 결과 거리에 따라 정렬 (거리가 짧을수록 유사함)
matches = sorted(matches, key=lambda x: x.distance)
# 상위 N개의 매칭 결과만 시각화
num_matches_to_draw = min(50, len(matches))
img_matches = cv2.drawMatches(img1, kp1, img2, kp2, matches[:num_matches_to_draw], None, flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)
# 결과 시각화
cv2.imshow(f'ORB Feature Matching ({num_matches_to_draw} matched)', img_matches)
cv2.waitKey(0)
cv2.destroyAllWindows()
print(f"총 매치된 특징점 개수: {len(matches)}")
print(f"상위 {num_matches_to_draw}개 매치 그리기.")
참고: road_image_shifted.jpg 파일은 road_image.jpg를 약간 이동시키거나 회전시킨 이미지여야 매칭 결과를 볼 수 있습니다.
3.2. 특징점 추적
Feature Tracking
- 목표
- 비디오 시퀀스(연속된 이미지 프레임)에서 동일한 특징점이 프레임 간 어떻게 이동하는지 추적
- 방법:
- 옵티컬 플로우(Optical Flow)
- 이미지 시퀀스에서 객체나 패턴의 움직임을 추정하는 방법
- Lucas-Kanade 옵티컬 플로우가 널리 사용됨
- 디스크립터 매칭 기반 추적
- 각 프레임에서 특징점을 검출하고 디스크립터를 매칭하는 과정을 반복
- 옵티컬 플로우(Optical Flow)
- 실습 코드 (Lucas-Kanade 옵티컬 플로우를 이용한 특징점 추적)
(동영상 파일이 필요합니다. 예를 들어 video.mp4 파일을 사용하거나 실시간 카메라를 사용할 수 있습니다.)
#// file: "feature_matching.py"
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 매칭을 위한 두 번째 이미지 로드 (첫 번째 이미지와 유사하지만 약간 다른 시점/객체)
# 추적을 위한 이전 프레임과 현재 프레임에서 Good Features to Track 사용
feature_params = dict( maxCorners = 100,
qualityLevel = 0.3,
minDistance = 7,
blockSize = 7 )
# 옵티컬 플로우를 위한 파라미터
lk_params = dict( winSize = (15,15),
maxLevel = 2,
criteria = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03))
# 임의의 색상 (추적 선 그리기 용도)
color = np.random.randint(0,255,(100,3))
# 비디오 캡처 객체 생성 (0번 카메라 또는 비디오 파일)
cap = cv2.VideoCapture('video.mp4') # 'video.mp4' 파일 경로를 사용하거나 0으로 실시간 카메라 사용
if not cap.isOpened():
print("오류: 비디오 파일 또는 카메라를 열 수 없습니다.")
exit()
# 첫 번째 프레임 읽기
ret, old_frame = cap.read()
if not ret:
print("첫 프레임을 읽을 수 없습니다.")
exit()
old_gray = cv2.cvtColor(old_frame, cv2.COLOR_BGR2GRAY)
p0 = cv2.goodFeaturesToTrack(old_gray, mask = None, **feature_params) # 추적할 초기 특징점 찾기
# 추적된 점들을 그릴 마스크 이미지 생성
mask = np.zeros_like(old_frame)
while True:
ret, frame = cap.read()
if not ret:
print("프레임을 모두 읽었습니다. 또는 더 이상 프레임이 없습니다.")
break
frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# 옵티컬 플로우 계산 (이전 프레임의 점 p0를 현재 프레임에서 추적하여 p1 얻기)
p1, st, err = cv2.calcOpticalFlowPyrLK(old_gray, frame_gray, p0, None, **lk_params)
# 제대로 추적된 점들만 선택
if p1 is not None:
good_new = p1[st==1]
good_old = p0[st==1]
# 추적 선과 점 그리기
for i, (new, old) in enumerate(zip(good_new, good_old)):
a, b = new.ravel()
c, d = old.ravel()
mask = cv2.line(mask, (int(a),int(b)),(int(c),int(d)), color[i].tolist(), 2)
frame = cv2.circle(frame, (int(a),int(b)),5, color[i].tolist(),-1)
img_track = cv2.add(frame,mask)
cv2.imshow('Feature Tracking', img_track)
# 'q' 키를 누르면 종료
if cv2.waitKey(1) & 0xFF == ord('q'):
break
# 다음 프레임을 위해 현재 프레임과 점들을 업데이트
old_gray = frame_gray.copy()
p0 = good_new.reshape(-1,1,2)
else:
cv2.imshow('Feature Tracking', frame) # 추적된 점이 없어도 원본 프레임 표시
if cv2.waitKey(1) & 0xFF == ord('q'):
break
# 자원 해제
cap.release()
cv2.destroyAllWindows()
print("특징점 추적 종료.")
4. 자율주행에서의 적용 사례
- Visual SLAM (Simultaneous Localization And Mapping)
- 차량의 카메라 영상에서 특징점을 추출하고 추적하여
- 차량의 정확한 위치를 판정
- 동시에 주변 환경의 3D 지도를 구축
- 이는 GPS 신호가 불안정한 터널이나 실내 환경에서 특히 중요함
- 객체 추적
- 도로 위의 다른 차량이나 보행자의 특징점을 추적
- 이들의 움직임을 예측하고 충돌 위험을 회피하는 데 사용
- 움직임 보상
- 차량의 움직임으로 인해 발생하는 영상의 흔들림을 특징점 추적을 통해 보상
- 안정적인 영상 데이터를 얻고
- 다음 처리 단계의 정확도를 높임
- 모노-SLAM (Mono-SLAM)
- 단안 카메라(Mono Camera)만을 사용하여
- 특징점 검출 및 추적을 통해 3D 정보 획득
- 차량의 위치를 추정
- 루프 클로저 (Loop Closure)
- 차량이 이전에 방문했던 장소를 다시 지날 때,
- 과거와 현재의 이미지에서 공통된 특징점을 매칭하여
- 이를 감지하고 SLAM 지도의 오차를 수정함