--- 본 포스팅은 데이콘 서포터즈 "데이크루 2기" 활동의 일환입니다 ---
- 안녕하세요 데이콘 서포터즈 데이크루 2기 포스(POS)팀의 Rahites입니다 :)
- POS팀은 Python OpenCV Study의 약자로 활동 기간동안 저희 팀은 '파이썬으로 만드는 OpenCV 프로젝트' 책을 가지고 OpenCV를 공부해보고 프로젝트를 진행할 것입니다.
- 자세한 스터디 계획과 운영 방안은 아래의 포스팅에서 확인하실 수 있습니다.
https://dacon.io/codeshare/4759?utm_source=dacrew&utm_medium=432727&utm_campaign=dacrew_2
https://dacon.io/codeshare/5106?utm_source=dacrew&utm_medium=432727&utm_campaign=dacrew_2
지금부터 9주차 활동 시작하겠습니다 (~~ ̄▽ ̄)~~
8. 영상 매칭과 추적¶
영상 매칭이란 서로 다른 두 영상을 비교해서 영상 속 객체가 같은 것인지 알아내거나 여러 영상 중에서 짝이 맞는 영상을 찾아내는 것을 말합니다. 영상에서 의미있다고 판단하는 특징들을 적절한 숫자로 변환하고 그 숫자들을 비교해서 얼마나 비슷한지 판단하게 되고, 이 때 영상의 특징으로 대표할 수 있는 숫자를 특징 벡터 또는 특징 디스크립터라고 합니다. 영상에서 특징 디스크립터를 찾아내는 방법과 두 영상을 매칭해서 영상 속에 찾고자하는 객체가 있는지 알아내는 방법, 동영상에서 움직이는 물체를 지속적으로 추적하는 방법에 대해 알아보도록 하겠습니다.
8.1. 비슷한 그림 찾기¶
새로운 영상이 입력되면 이미 알고있던 영상들 중에서 가장 비슷한 영상을 찾아 그 영상에 있는 객체로 판단하는 방법입니다.
8.1.1. 평균 해시 매칭¶
비슷한 그림을 찾는데 있어 실효성은 조금 떨어지지만 쉽고 간단한 방법으로는 평균 해시 매칭(average hash matching)이 있습니다. 평균 해시는 어떤 영상이든 동일한 크기의 하나의 숫자로 변환되는데, 이때 숫자를 얻기 위해 평균 값을 이용한다는 뜻입니다.
import cv2
import matplotlib.pyplot as plt
#영상 읽어서 그레이 스케일로 변환
img = cv2.imread('../img_1/pistol.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 16x16 크기로 축소 ---①
gray = cv2.resize(gray, (16,16))
# 영상의 평균값 구하기 ---②
avg = gray.mean()
# 평균값을 기준으로 0과 1로 변환 ---③
bin = 1 * (gray > avg)
print(bin)
# 2진수 문자열을 16진수 문자열로 변환 ---④
dhash = []
for row in bin.tolist():
s = ''.join([str(i) for i in row])
dhash.append('%02x'%(int(s,2)))
dhash = ''.join(dhash)
print(dhash)
plt.figure(figsize = (10,6))
imgs = {'pistol':img}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,1,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
[[1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1] [1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] [1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] [1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] [1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] [1 0 0 0 0 0 0 1 0 0 1 1 1 1 1 1] [1 1 0 0 0 0 0 1 1 1 1 1 1 1 1 1] [1 1 0 0 0 0 0 1 1 1 1 1 1 1 1 1] [1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 1] [1 1 0 0 0 0 1 1 1 1 1 1 1 1 1 1] [1 1 0 0 0 1 1 1 1 1 1 1 1 1 1 1] [1 1 0 0 0 1 1 1 1 1 1 1 1 1 1 1] [1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1] [1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1] [1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1] [1 1 0 0 0 1 1 1 1 1 1 1 1 1 1 1]] ffff8000800080008000813fc1ffc1ffc07fc3ffc7ffc7ff87ff87ff87ffc7ff
실행 결과로 출력된 0, 1 숫자의 형태를 보면 0으로 구성된 숫자들의 모양이 그림의 권총 모양과 비슷한 것을 알 수 있습니다. 이렇게 얻는 평균 해시를 다른 영상의 것과 비교해서 얼마나 비슷한지 측정하기 위해서는 유클리드 거리와 해밍 거리를 사용합니다. 유클리드 거리는 두 값의 차이로 거리를 계산하는 방법입니다. 예를 들어 5와 3, 8을 비교한다면 5와 2의 유클리드 거리는 2이고 5와 8의 유클리드 거리는 3으로 5와 더 비슷한 수는 3으로 판단합니다. 해밍 거리는 두 수의 같은 자리의 값이 서로 다른 것이 몇 개인지를 나타내는 것입니다. 예를 들어 12345와 비교할 값으로 12354와 92345가 있을 때 12345와 12354의 마지막 두자리가 다르기 때문에 해밍 거리는 2이고 92345는 맨 처음 자리만 다르기 때문에 해밍 거리는 1입니다. 따라서 더 비슷한 수를 92345라고 판단합니다.(해밍 거리는 두 값의 길이가 같아야만 계산할 수 있습니다)
8.1.2. 템플릿 매칭¶
템플릿 매칭은 어떤 물체가 있는 영상을 준비해 두고 그 물체가 포함되어 있을 것이라고 예상할 수 있는 입력 영상과 비교해서 물체가 매칭되는 위치를 찾는 것입니다. 이때 미리 준비해 둔 영상을 템플릿 영상이라고 하며 이것을 입력 영상에서 찾는 것이므로 템플릿 영상은 입력 영상보다 그 크기가 항상 작아야 합니다.
- result = cv.matchTemplate(img, templ, method[, result, mask])
- img : 입력 영상
- templ : 템플릿 영상
- method : 매칭 메서드
- cv2.TM_SQDIFF : 제곱차이 매칭
- cv2.TM_SQDIFF_NORMED : 제곱 차이 매칭의 정규화
- cv2.TM_CCORR : 상관관계 매칭
- cv2.TM_CCORR_NORMED : 상관관계 매칭의 정규화
- cv2.TM_CCOEFF : 상관계수 매칭
- cv2.TM_CCOEFF_NORMED : 상관계수 매칭의 정규화
- result : 매칭 결과
- mask : TM_SQDIFF, TM_CCORR_NORMED인 경우 사용할 마스크
matchTemplate 함수는 입력 영상 img에서 templ 인자의 영상을 슬라이딩하면서 주어진 메서드에 따라 매칭을 수행합니다.
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 입력이미지와 템플릿 이미지 읽기
img = cv2.imread('../img_1/figures.jpg')
template = cv2.imread('../img_1/taekwonv1.jpg')
th, tw = template.shape[:2]
plt.figure(figsize = (20,6))
# 3가지 매칭 메서드 순회
methods = ['cv2.TM_CCOEFF_NORMED', 'cv2.TM_CCORR_NORMED', 'cv2.TM_SQDIFF_NORMED'] # 매칭 메서드
for i, method_name in enumerate(methods):
img_draw = img.copy()
method = eval(method_name)
# 템플릿 매칭 ---①
res = cv2.matchTemplate(img, template, method)
# 최대, 최소값과 그 좌표 구하기 ---②
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
print(method_name, min_val, max_val, min_loc, max_loc)
# TM_SQDIFF의 경우 최소값이 좋은 매칭, 나머지는 그 반대 ---③
if method in [cv2.TM_SQDIFF, cv2.TM_SQDIFF_NORMED]:
top_left = min_loc
match_val = min_val
else:
top_left = max_loc
match_val = max_val
# 매칭 좌표 구해서 사각형 표시 ---④
bottom_right = (top_left[0] + tw, top_left[1] + th)
cv2.rectangle(img_draw, top_left, bottom_right, (0,0,255),2)
# 매칭 포인트 표시 ---⑤
cv2.putText(img_draw, str(match_val), top_left, \
cv2.FONT_HERSHEY_PLAIN, 2,(0,255,0), 1, cv2.LINE_AA)
plt.subplot(1,3,i+1)
plt.imshow(img_draw[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
cv2.TM_CCOEFF_NORMED -0.1780252307653427 0.5131933093070984 (42, 0) (208, 43) cv2.TM_CCORR_NORMED 0.827332615852356 0.9238022565841675 (85, 6) (208, 43) cv2.TM_SQDIFF_NORMED 0.17028295993804932 0.36860838532447815 (208, 43) (86, 7)
위 방법은 정규화된 세 가지 매칭 메서드만을 이용해서 템플릿 매칭을 각각 수행합니다.
8.2. 영상의 특징과 키 포인트¶
지금까지 다룬 특징 추출과 매칭 방법은 영상 전체를 전역적으로 반영하는 방법입니다. 전역적 매칭은 비교하려는 두 영상의 내용이 거의 대부분 비슷해야 하며, 다른 물체에 가려지거나 회전이나 방향, 크기 변화가 있으면 효과가 없습니다.
8.2.1. 코너 특징 검출¶
영상 속 내용을 판단할 때 주로 픽셀의 변화가 심한 곳에 중점적으로 관심을 둡니다. 주로 엣지와 엣지가 만나는 코너(corner)에 가장 큰 관심을 두게 됩니다. 코너를 검출하기 위한 방법으로는 크리스 해리스(Chris Harris)의 논문에서 처음 소개된 해리스 코너 검출 방법이 있습니다. 이는 소벨 미분으로 엣지를 검출하면서 엣지의 경사도 변화량을 측정하여 변화량이 x축과 y축 모든 방향으로 크게 변화하는 것을 코너로 판단합니다.
- dst = cv.cornerHarris(src, blockSize, ksize, k[, dst, borderType])
- src : 입력 영상, 그레이 스케일
- blockSize : 이웃 픽셀 범위
- ksize : 소벨 미분 커널 크기
- k : 코너 검출 상수
- dst : 코너 검출 결과
- borderType : 외곽 영역 보정 형식
import cv2
import numpy as np
img = cv2.imread('../img_1/house.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 해리스 코너 검출 ---①
corner = cv2.cornerHarris(gray, 2, 3, 0.04)
# 변화량 결과의 최대값 10% 이상의 좌표 구하기 ---②
coord = np.where(corner > 0.1* corner.max())
coord = np.stack((coord[1], coord[0]), axis=-1)
# 코너 좌표에 동그리미 그리기 ---③
for x, y in coord:
cv2.circle(img, (x,y), 5, (0,0,255), 1, cv2.LINE_AA)
# 변화량을 영상으로 표현하기 위해서 0~255로 정규화 ---④
corner_norm = cv2.normalize(corner, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U)
# 화면에 출력
corner_norm = cv2.cvtColor(corner_norm, cv2.COLOR_GRAY2BGR)
plt.figure(figsize = (10,6))
imgs = {'corner_norm':corner_norm, 'Harris Corner':img}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,2,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
위의 코드는 코너 검출을 실행하고 그 결과에서 최대 값의 10% 이상인 좌표에만 빨간색 동그라미로 표시하였습니다. 시(Shi)와 토마시(Tomasi)는 논문을 통해 해리스 코너 검출을 개선한 알고리즘을 발표하였는데, 이 방법으로 검출한 코너는 객체 추적에 좋은 특징이 된다고 합니다.
- corners = cv2.goodFeaturesToTrack(img, maxCorners, qualityLevel, minDistance[, corners, mask, blockSize, useHarrisDetector, k])
- img : 입력 영상
- maxCorners : 얻고 싶은 코너 개수
- qualityLevel : 코너로 판단할 스레시홀드 값
- minDistance : 코너 간 최소 거리
- mask : 검출에 제외할 마스크
- blockSize : 코너 주변 영역의 크기
- useHarrisDetector = False : 코너 검출 방법 선택
- True : 해리스 코너 검출 방법, False = 시와 토마시 검출 방법
- k : 해리스 코너 검출 방법에 사용할 k 계수
- corners : 코너 검출 좌표 결과, 실수 값이므로 정수로 변형 필요
import cv2
import numpy as np
img = cv2.imread('../img_1/house.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 시-토마스의 코너 검출 메서드
corners = cv2.goodFeaturesToTrack(gray, 80, 0.01, 10)
# 실수 좌표를 정수 좌표로 변환
corners = np.int32(corners)
# 좌표에 동그라미 표시
for corner in corners:
x, y = corner[0]
cv2.circle(img, (x, y), 5, (0,0,255), 1, cv2.LINE_AA)
plt.figure(figsize = (10,6))
imgs = {'Corners':img}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,1,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
cv2.goodFeaturesToTrack() 함수의 결과 값은 검출도니 코너의 좌표 개수만큼 1x2 크기로 구성되어 있어서 코너의 좌표를 알기 쉽습니다. 다만 좌표 값이 실수 값으로 되어 있으므로 픽셀 좌표로 쓰려면 정수형으로 변환해야 합니다.
8.2.2. 키 포인트와 특징 검출기¶
영상에서 특징점을 찾아내는 알고리즘은 무척 다양합니다. 각각의 특징점은 픽셀의 좌표 이외에도 표현할 수 있는 정보가 많은데, OpenCV에서는 여러 특징점 검출 알고리즘 중 어떤 것을 사용하든 간에 동일한 코드로 특징점을 검출할 수 있게 하려고 각 알고리즘 구현 클래스가 추상 클래스를 상속받는 방법으로 인터페이스를 통일했습니다. OpenCV는 모든 특징 검출기를 cv2.Feature2D 클래스를 상속받아 구현했으며, 이것으로부터 추출도니 특징점은 cv2.KeyPoint라는 객체에 담아 표현합니다. OpenCV에서 cv2.Feature2D를 상속받아 구현된 특징 검출기는 모두 12가지 이며, 목록은 다음의 URL에서 확인할 수 있습니다. https://docs.opencv.org/3.4.1/d0/d13/classcv_1_1Feature2D.html
- keypoints = detector.detect(img, [, mask]) : 키 포인트 검출 함수
- img : 엽력 영상, 바이너리 스케일
- mask : 검출 제외 마스크
- keypoints : 특징점 검출 결과, KeyPoint의 리스트
- Keypoint : 특징점 정보를 담는 객체
- pt : 키 포인트(x, y) 좌표, float 타입으로 정수로 변환 필요
- size : 의미있는 키 포인트 이웃의 반지름
- angle : 특징점 방향
- response : 특징점 반응 강도
- octave : 발견된 이미지 피라미드 계층
- class_id : 키 포인트가 속한 객체 ID
키 포인트의 속성 中 pt 속성은 항상 값을 갖지만 나머지 속성은 사용하는 검출기에 따라 채워지지 않는 경우도 있습니다. 검출한 키 포인트를 영상에 표시하고 싶을 때는 앞선 예제의 cv2.circle() 함수로 pt의 좌표와 size 값을 표시할 수도 있지만 OpenCV는 키 포인트를 영상에 표시해주는 전용함수를 다음과 같이 제공합니다.
- outImg = cv2.drawKeypoints(img, keypoints, outImg[, color[, flags]])
- img : 입력 이미지
- keypoints : 표시할 키 포인트 리스트
- outImg : 키 포인트가 그려진 결과 이미지
- color : 표시할 색상
- flags : 표시 방법 선택 플래그
- cv2.DRAW_MATCHES_FLAGS_DEFAULT : 좌표 중심에 동그라미만 그림
- cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS : 동그라미의 크기를 size와 angle을 반영해서 그림
8.2.3. GFTTDetector¶
GFTTDetector는 앞서 살펴본 cv2.goodFeaturesToTrack() 함수로 구현된 특징 검출기입니다. 검출기 생성 방법만 아래와 다르고 검출하는 데 사용하는 함수는 cv2.Feature2D의 detect() 함수와 같습니다.
- detector = cv2.GFTTDetector_create([, maxCorners[, qualityLevel, minDistance, blockSize, useHarrisDetector, k])
import cv2
import numpy as np
img = cv2.imread("../img_1/house.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Good feature to trac 검출기 생성 ---①
gftt = cv2.GFTTDetector_create()
# 키 포인트 검출 ---②
keypoints = gftt.detect(gray, None)
# 키 포인트 그리기 ---③
img_draw = cv2.drawKeypoints(img, keypoints, None)
# 결과 출력 ---④
plt.figure(figsize = (10,6))
imgs = {'GFTTDectector':img_draw}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,1,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
GFTTDetector로 검출한 키 포인트를 영상에 그려서 출력합니다. 1번에서는 시-토마시 알고리즘을 사용하는 검출기를 생성하고 2번에서는 키 포인트를 검출해서 코드 3에서 영상에 그림을 그려주는 코드입니다.
8.2.4. FAST¶
FAST(Feature from Accelerated Segment Test)는 속도를 개선한 알고리즘입니다. 2006년 에드워드 로스텐과 톰 드러먼드의 논문에 소개된 알고리즘으로 코너를 검출할 때 미분 연산으로 엣지 검출을 하지 않고 픽셀을 중심으로 특정 개수의 픽셀로 원을 그려서 그 안의 픽셀들이 중심 픽셀 값보다 임계 값 이상 밝거나 어두운 것이 특정 개수 이상 연속되면 코너로 판단합니다.
- detector = cv2.FastFeatureDetector_create([threshold[, nonmaxSuppression, type])
- threshold=10 : 코너 판단 임계 값
- nonmaxSuppression = True : 최대 점수가 아닌 코너 억제
- type : 엣지 검출 패턴
- cv2.FastFeatureDetector_TYPE_9_16 : 16개 중 9개 연속
- cv2.FastFeatureDetector_TYPE_7_12 : 12개 중 7개 연속
- cv2.FastFeatureDetector_TYPE_5_8 : 8개 중 5개 연속
import cv2
import numpy as np
img = cv2.imread('../img_1/house.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# FASt 특징 검출기 생성 ---①
fast = cv2.FastFeatureDetector_create(50)
# 키 포인트 검출 ---②
keypoints = fast.detect(gray, None)
# 키 포인트 그리기 ---③
img = cv2.drawKeypoints(img, keypoints, None)
# 결과 출력 ---④
plt.figure(figsize = (10,6))
imgs = {'FAST':img}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,1,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
8.2.5. SimpleBlobDetector¶
BLOB(Binary Large Object)는 바이너리 스케일 이미지의 연결된 픽셀 그룹을 말하는 것으로, 자잘한 객체는 노이즈로 판단하고 특정 크기 이상의 큰 객체에만 관심을 두는 방법입니다. BLOB는 코너를 이용한 특징 검출과는 방식이 다르지만 영상의 특징을 표현하기 좋은 또 하나의 방법입니다.
- detector = cv2.SimpleBlobDetector_create( [parameters] ) : BLOB 검출기 생성자
- parameters : BLOB 검출 필터 인자 객체
- cv2.SimpleBlobDetector_Params()
- minThreshold, maxThreshold, thresholdStep : BLOB를 생성하기 위한 경계 값
- minRepeatability : BLOB에 참여하기 위한 연속된 경계 값의 개수
- minDistBetweenBlobs : 두 BLOB를 하나의 BLOB로 간주한 거리
- filterByArea : 면적 필터 옵션
- minArea, maxArea : min~max 범위의 면적만 BLOB로 검출
- filterByCircularity : 원형 비율 필터 옵션
- minCircularity, maxCircularity : min~max 범위의 원형 비율만 BLOB로 검출
- filterByColor : 밝기를 이용한 필터 옵션
- blobColor : 0 = 검은색 BLOB 검출, 255 = 흰색 BLOB 검출
- filterByConvexity : 볼록 비율 필터 옵션
- minConvexity, maxConvexity : min~max 범위의 볼록 비율만 BLOB로 검출
- filterByInertia : 관성 비율 필터 옵션
- minInertiaRatio, maxInertiaRatio : min~max 범위의 관성 비율만 BLOB로 검출
import cv2
import numpy as np
img = cv2.imread("../img_1/house.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# SimpleBlobDetector 생성 ---①
detector = cv2.SimpleBlobDetector_create()
# 키 포인트 검출 ---②
keypoints = detector.detect(gray)
# 키 포인트를 빨간색으로 표시 ---③
img = cv2.drawKeypoints(img, keypoints, None, (0,0,255),\
flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
plt.figure(figsize = (10,6))
imgs = {'Blob':img}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,1,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
# 필터 옵션으로 생성한 SimpleBlobDetector
import cv2
import numpy as np
img = cv2.imread("../img_1/house.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# blob 검출 필터 파라미터 생성 ---①
params = cv2.SimpleBlobDetector_Params()
# 경계값 조정 ---②
params.minThreshold = 10
params.maxThreshold = 240
params.thresholdStep = 5
# 면적 필터 켜고 최소 값 지정 ---③
params.filterByArea = True
params.minArea = 200
# 컬러, 볼록 비율, 원형비율 필터 옵션 끄기 ---④
params.filterByColor = False
params.filterByConvexity = False
params.filterByInertia = False
params.filterByCircularity = False
# 필터 파라미터로 blob 검출기 생성 ---⑤
detector = cv2.SimpleBlobDetector_create(params)
# 키 포인트 검출 ---⑥
keypoints = detector.detect(gray)
# 키 포인트 그리기 ---⑦
img_draw = cv2.drawKeypoints(img, keypoints, None, None,\
cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
# 결과 출력 ---⑧
plt.figure(figsize = (10,6))
imgs = {'Blob with Params':img_draw}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,1,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
SimpleBlobDetector 객체를 필터 옵션 객체를 이용해서 생성한 예시로, 파라미터는 경계 값을 넓게 적용하고, 원형, 볼록, 색상 필터를 모두 끄고 검출하여 이전에 기본으로 생성한 검출기보다 많은 BLOB를 검출하는 모습을 확인할 수 있습니다. 필터 값을 조정해서 원하는 모양과 크기의 BLOB를 검출할 수 있습니다.
8.3. 디스크립터 추출기¶
8.3.1. 특징 디스크립터와 추출기¶
키 포인트는 영상의 특징이 있는 픽셀의 좌표와 그 주변 픽셀과의 관계에 대한 정보를 가집니다. 그 중 가장 대표적인 것이 size와 angle 속성으로 코너 특징인 경우 엣지의 경사도 규모와 방향을 나타냅니다. 특징을 나타내는 값을 매칭에 사용하기 위해서는 회전, 크기, 방향 등에 영향이 없어야 하는데, 이를 위해 특징 디스크립터(feature descriptor)가 필요합니다. 이는 키 포인트 주변 픽셀을 일정한 크기의 블록으로 나누어 각 블록에 속한 픽섹의 그레디언트 히스토그램을 계산한 것으로, 키 포인트 주위의 밝기, 색상, 방향, 크기 등의 정보를 표현한 것입니다. 일반적으로는 키 포인트에 적용하는 주변 블록의 크기에 8방향의 경사도를 표현하는 형태인 경우가 많습니다.
- keypoints, descriptors = detector.compute(image, keypoints[, descriptors]): 키 포인트를 전달하면 특징 디스크립터를 계산해서 반환
- keypoints, descriptors = detector.detectAndCompute(image, mask[, descriptors, useProvidedKeypoints]): 키 포인트 검출과 특징디스크립터 계산을 한번에 수행
- image : 입력 영상
- keypoints : 디스크립터 계산을 위해 사용할 키 포인트
- descriptors : 계산된 디스크립터
- mask : 키 포인트 검출에 사용할 마스크
- useProvidedKeypoints : True인 경우 키 포인트 검출을 수행하지 않음
cv2.Feature2D를 상속받은 몇몇 특징 검출기는 detect() 함수만 구현되어 있고 compute()와 detectAndCompute() 함수는 구현되어 있지 않은 경우도 있고, 그 반대의 경우도 있지만 앞으로 나올 디스크립터 추출기는 detectAndCompute() 함수가 모두 구현되어 있으므로 이를 사용하는 것이 편리합니다.
8.3.2. SIFT¶
SIFT(Scale-Invariant Feature Transform)는 이미지 피라미드를 이용해서 크기 변화에 따른 특징 검출의 문제를 해결한 알고리즘입니다. 이 알고리즘은 특허권이 있어 상업적 사용에는 제약이 있으며 OpenCV는 엑스트라 모듈에만 포함되어 있습니다.
- detector = cv2.xfeatures2d.SIFT_create([, nfeatures[, nOctaveLayers [, contrastThreshold [, edgeThreshold[, sigma]]]])
- nfeatures : 검출 최대 특징 수
- nOctaveLayers : 이미지 피라미드에 사용할 계층 수
- contrastThreshold : 필터링할 빈약한 특징 문턱 값
- edgeThreshold : 필터링할 엣지 문턱 값
- sigma : 이미지 피라미드 0계층에서 사용할 가우시안 필터의 시그마 값
import cv2
import numpy as np
img = cv2.imread('../img_1/house.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)
print(descriptor)
# 키 포인트 그리기
img_draw = cv2.drawKeypoints(img, keypoints, None, \
flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
# 결과 출력
plt.figure(figsize = (10,6))
imgs = {'SIFT':img_draw}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,1,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
keypoint: 413 descriptor: (413, 128) [[ 1. 1. 1. ... 0. 0. 1.] [ 8. 24. 0. ... 1. 0. 4.] [ 0. 0. 0. ... 0. 0. 2.] ... [ 1. 8. 71. ... 73. 127. 3.] [ 35. 2. 7. ... 0. 0. 9.] [ 36. 34. 3. ... 0. 0. 1.]]
8.3.3. SURF¶
SIFT는 크기 변화에 따른 특징 검출 문제를 해결하기 위해 이미지 피라미드를 사용하기 때문에 속도가 느리다는 단점이 있습니다. SURF(Speeded Up Robust Features)는 이미지 피라미드 대신 필터의 커널 크기를 바꾸는 방식으로 성능을 개선한 알고리즘입니다. 이 알고리즘 또한 특허권이 있어 상업적 이용에 제한이 있고, 이 기능을 사용하기 위해서는 소스 코드를 빌드하면서 OPENCV_ENABLE_NONFREE=ON 옵션을 지정해야 합니다.
- detector = cv2.xfeatures2d.SURF_create([hessianThreshold, nOctaves, nOctaveLayers, extended, upright])
- hessianThreshold : 특징 추출 경계 값
- nOctaves : 이미지 피라미드 계층 수
- extended : 디스크립터 생성 플래그
- upright : 방향 계산 플래그
아래 코드는 opencv-contrib-python 3.4.2.버전을 설치하면 실행할 수 있습니다.
import cv2
import numpy as np
img = cv2.imread('../img_1/house.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# SURF 추출기 생성 ( 경계:1000, 피라미드:3, 서술자확장:True, 방향적용:True)
surf = cv2.xfeatures2d.SURF_create(1000, 3, True, True)
# 키 포인트 검출 및 서술자 계산
keypoints, desc = surf.detectAndCompute(gray, None)
print(desc.shape, desc)
# 키포인트 이미지에 그리기
img_draw = cv2.drawKeypoints(img, keypoints, None, \
flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
plt.figure(figsize = (10,6))
imgs = {'SURF':img_draw}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,1,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
8.3.4. ORB¶
ORB(Oriented and Rotated BRIEF)는 특징 검출을 지원하지 않는 디스크립터 추출기인 BRIEF(Binary Robust Independent Elementary features)에 방향과 회전을 고려하도록 개선한 알고리즘입니다. 회전과 방향에 영향을 받지 않으면서도 속도가 빨라서 특허 때문에 사용에 제약이 많은 SiFT와 SURF의 좋은 대안으로 사용됩니다.
- detector = cv.ORB_create([nfeatures, scaleFacotr, nlevels, edgeThreshold, firstLevel, WTA_K, scoreType, patchSize, fastThreshold])
- nfeatures = 500 : 검출할 최대 특징 수
- scaleFactor = 1.2 : 이미지 피라미드 비율'
- nlevels = 8 : 이미지 피라미드 계층 수
- edgeThreshold = 31 : 검색에서 제외할 테두리 크기
- firstLevel = 0 : 최초 이미지 피라미드 계층 단계
- WTA_K = 2 : 임의 좌표 생성 수
- scoreType : 키 포인트 검출에 사용할 방식
- cv2.ORB_HARRIS_SCORE : 해리스 코너 검출 (기본 값 but 속도가 느릴 수 있음)
- cv2.ORB_FAST_SCORE : FAST 코너 검출 (속도를 높일 수 있으나 잘못된 특징점이 검출될 수 있음)
- patchSize = 31 : 디스크립터의 패치 크기
- fastThreshold = 20 : FAST에 사용할 임계 값
import cv2
import numpy as np
import matplotlib.pyplot as plt
img = cv2.imread('../img_1/house.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# ORB 추출기 생성
orb = cv2.ORB_create()
# 키 포인트 검출과 서술자 계산
keypoints, descriptor = orb.detectAndCompute(img, None)
# 키 포인트 그리기
img_draw = cv2.drawKeypoints(img, keypoints, None, \
flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
# 결과 출력
plt.figure(figsize = (10,6))
imgs = {'ORB':img_draw}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,1,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
8.4. 특징 매칭¶
특징 매칭(feature matching)이란 서로 다른 두 영상에서 구한 키 포인트와 특징 디스크립터들을 각각 비교해서 그 거리가 비슷한 것끼리 짝짓는 것을 말합니다. 짝지어진 특징점들 중에 거리가 유의미한 것들을 모아서 대칭점으로 표시하면 그 개수에 따라 두 영상이 얼마나 비슷한지 측정할 수 있고 충분히 비슷한 영상이라면 비슷한 모양의 영역을 찾아낼 수도 있습니다. 특징 매칭은 파노라마 사진 생성, 이미지 검색, 등록한 객체 인식 등 다양하게 응용할 수 있습니다.
8.4.1. 특징 매칭 인터페이스¶
- matcher = cv2.DescriptorMatcher_create(matcherType) : 매칭기 생성자
- matcherType : 생성할 구현 클래스의 알고리즘, 문자열
- "BruteForce" : NORM_L2를 사용하는 BFMatcher
- "BruteForce-L1" : NORM_L1을 사용하는 BFMatcher
- "BruteForce-Hamming" : NORM_HAMMING을 사용하는 BFMatcher
- "BruteForce-Hamming(2)" : NORM_HAMMING2를 사용하는 BFMatcher
- "FlannBased" : NORM_L2를 사용하는 FlannBasedMatcher
- matches = matcher.match(queryDescriptors, trainDescriptors[, mask]): 1개의 최적 매칭
- queryDescriptors : 특징 디스크립터 배열, 매칭의 기준이 될 디스크립터
- trainDescriptors : 특징 디스크립터 배열, 매칭의 대상이 될 디스크립터
- mask : 매칭 진행 여부 마스크
- matches : 매칭 결과
- matches = matcher.knnMatch(queryDescriptors, trainDescriptors, k[, mask[, compactResult]]): k개의 가장 근접한 매칭
- k : 매칭할 근접 이웃 개수
- compactResult = False : 매칭이 없는 경우 매칭 결과에 불포함
- matches = matcher.radiusMatch(queryDescriptors, trainDescriptors, maxDistance[, mask, compactResult]) : maxDistance 이내의 거리 매칭
- maxDistance : 매칭 대상 거리
- DMatch : 매칭 결과를 표현하는 객체
- queryIdx : queryDescriptor의 인덱스
- trainIdx : trainDescriptor의 인덱스
- imgIdx : trainDescriptor의 이미지 인덱스
- distance : 유사도 거리
- cv2.drawMatches(img1, kp1, img2, kp2, matches, flags) : 매칭점을 영상에 표시
- img1, kp1 : queryDescriptor의 영상과 키 포인트
- img2, kp2 : trainDescriptor의 영상과 키 포인트
- matches : 매칭 결과
- 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 : 한쪽만 있는 매칭 결과 그리기 제외
- matcherType : 생성할 구현 클래스의 알고리즘, 문자열
OpenCV에서는 BFMatcher와 FLannBasedMatcher 특징 매칭기를 제공합니다. 매칭기 객체를 만들고 나면 두 개의 디스크립터를 이용해서 매칭해주는 3개의 함수 match, knnMatch, radiusMatch가 있습니다. 3개의 함수 모두 첫 번째 인자인 queryDescriptor를 기준으로 두 번째 인자인 trainDescriptor에 맞는 매칭을 찾습니다.
match함수는 queryDescriptor 1개당 최적 매칭을 이루는 trainDescriptor 1개를 찾아 결과에 반영합니다.
knnMatch함수는 queryDescriptor 1개당 k인자에 전달한 최근접 이웃 개수만큼 trainDescriptor에서 찾아 결과에 반영합니다.
radiusMatch() 함수는 queryDescriptor에서 maxDistance 이내에 있는 train Descriptor를 찾아 결과 매칭에 반영합니다.
8.4.2. BFMatcher¶
Brute-Force 매칭기는 일일이 전수조사를 하여 매칭을 하는 알고리즘 입니다.
- matcher = cv.BFMatcher_create([normType[, crossCheck])
- normType : 거리 측정 알고리즘 ( NORM_L1, NORM_L2, NORM_HAMMING 등 )
- crossCheck = False : 상호 매칭이 있는 것만 반영
거리 측정 알고리즘은 3가지 유클리드 거리와 2가지 해밍 거리 중에 선택할 수 있습니다. SIFT, SURF로 추출한 디스크립터에 경우 L1, L2 방법이 적합하고 ORB는 HAMMING, ORB의 WTA_K가 3 or 4인 경우에는 HAMMING2가 적합합니다. crossCheck를 True로 설정하면 양쪽 디스크립터 모두에게서 매칭이 완성된 것만 반영하므로 불필요한 매칭을 줄일 수 있지만 그만큼 속도가 느려집니다.
import cv2
import numpy as np
import matplotlib.pyplot as plt
img1 = cv2.imread('../img/yungyung.png')
img2 = cv2.imread('../img/3yung.png')
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 생성, L1 거리, 상호 체크 ---③
matcher = cv2.BFMatcher(cv2.NORM_L1, crossCheck=True)
# 매칭 계산 ---④
matches = matcher.match(desc1, desc2)
# 매칭 결과 그리기 ---⑤
res = cv2.drawMatches(img1, kp1, img2, kp2, matches, None, \
flags=cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS)
# 결과 출력
plt.figure(figsize = (10,6))
imgs = {'BFMatcher + SIFT':res}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,1,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
- SIFT 추출기 생성
- 두 영상의 키 포인트와 디스크립터를 각각 추출
- BFMatcher 객체를 생성하면서 L1 거리 알고리즘과 상호 체크 옵션 지정
- 두 영상의 디스크립터로 매칭 계산
- 매칭 결과를 영상에 각각 표시
import cv2
import numpy as np
img1 = cv2.imread('../img/yungyung.png')
img2 = cv2.imread('../img/3yung.png')
gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
# SURF 서술자 추출기 생성 ---①
detector = cv2.xfeatures2d.SURF_create() # opencv-contrib-python 3.4.2 버전을 설치해야 가능
kp1, desc1 = detector.detectAndCompute(gray1, None)
kp2, desc2 = detector.detectAndCompute(gray2, None)
# BFMatcher 생성, L2 거리, 상호 체크 ---③
matcher = cv2.BFMatcher(cv2.NORM_L2, crossCheck=True)
# 매칭 계산 ---④
matches = matcher.match(desc1, desc2)
# 매칭 결과 그리기 ---⑤
res = cv2.drawMatches(img1, kp1, img2, kp2, matches, None, \
flags=cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS)
plt.figure(figsize = (10,6))
imgs = {'BF + SURF':res}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,1,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
# ORB
import cv2, numpy as np
img1 = cv2.imread('../img/yungyung.png')
img2 = cv2.imread('../img/3yung.png')
gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
# SIFT 서술자 추출기 생성 ---①
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)
# 매칭 결과 그리기 ---⑤
res = cv2.drawMatches(img1, kp1, img2, kp2, matches, None, \
flags=cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS)
plt.figure(figsize = (10,6))
imgs = {'BFMatcher + ORB':res}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,1,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
8.4.3. FLANN¶
BFMatcher는 특징 디스크립터를 전수 조사하기 때문에 사용하는 영상이 큰 경우에 속도가 느려진다는 단점이 있습니다. FLANN(Fast Library for Approximate Nearest Neighbors Matching)은 모든 특징 디스크립터를 비교하기 보다는 가장 가까운 이웃의 근사 값으로 매칭합니다.
- matcher = cv2.FlannBasedMatcher([indexParams[, searchParams]])
- indexParams : 인덱스 파라미터, 딕셔너리
- algorithm : 알고리즘 선택 키
- searchParams : 검색 파라미터, 딕셔너리 객체
- checks = 32 : 검색할 후보 수
- eps = 0 : 사용안함
- sorted = Ture : 정렬해서 반환
- indexParams : 인덱스 파라미터, 딕셔너리
import cv2, numpy as np
img1 = cv2.imread('../img/yungyung.png')
img2 = cv2.imread('../img/3yung.png')
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)
# 인덱스 파라미터와 검색 파라미터 설정 ---①
FLANN_INDEX_KDTREE = 1
index_params = dict(algorithm=FLANN_INDEX_KDTREE, trees=5)
search_params = dict(checks=50)
# Flann 매처 생성 ---③
matcher = cv2.FlannBasedMatcher(index_params, search_params)
# 매칭 계산 ---④
matches = matcher.match(desc1, desc2)
# 매칭 그리기
res = cv2.drawMatches(img1, kp1, img2, kp2, matches, None, \
flags=cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS)
plt.figure(figsize = (10,6))
imgs = {'Flann + SIFT':res}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,1,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
import cv2, numpy as np
img1 = cv2.imread('../img/yungyung.png')
img2 = cv2.imread('../img/3yung.png')
gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
# SURF 생성
detector = cv2.xfeatures2d.SURF_create() # opencv-contrib-python 3.4.2 버전을 설치해야 가능
# 키 포인트와 서술자 추출
kp1, desc1 = detector.detectAndCompute(gray1, None)
kp2, desc2 = detector.detectAndCompute(gray2, None)
# 인덱스 파라미터와 검색 파라미터 설정 ---①
FLANN_INDEX_KDTREE = 1
index_params = dict(algorithm=FLANN_INDEX_KDTREE, trees=5)
search_params = dict(checks=50)
# Flann 매처 생성 ---③
matcher = cv2.FlannBasedMatcher(index_params, search_params)
# 매칭 계산 ---④
matches = matcher.match(desc1, desc2)
# 매칭 그리기
res = cv2.drawMatches(img1, kp1, img2, kp2, matches, None, \
flags=cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS)
plt.figure(figsize = (10,6))
imgs = {'Flann + SURF':res}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,1,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
# ORB
import cv2, numpy as np
img1 = cv2.imread('../img/yungyung.png')
img2 = cv2.imread('../img/3yung.png')
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)
# 인덱스 파라미터 설정 ---①
FLANN_INDEX_LSH = 6
index_params= dict(algorithm = FLANN_INDEX_LSH,
table_number = 6,
key_size = 12,
multi_probe_level = 1)
# 검색 파라미터 설정 ---②
search_params=dict(checks=32)
# Flann 매처 생성 ---③
matcher = cv2.FlannBasedMatcher(index_params, search_params)
# 매칭 계산 ---④
matches = matcher.match(desc1, desc2)
# 매칭 그리기
res = cv2.drawMatches(img1, kp1, img2, kp2, matches, None, \
flags=cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS)
# 결과 출력
plt.figure(figsize = (10,6))
imgs = {'Flann + ORB':res}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,1,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
8.4.4. 좋은 매칭점 찾기¶
앞서 살펴본 매칭 결과는 잘못된 정보를 너무 많이 포함하는 것을 알 수 있습니다. 그래서 매칭 결과에서 쓸모 없는 매칭점은 버리고 좋은 매칭점만을 골라내는 작업이 필요합니다.
import cv2, numpy as np
img1 = cv2.imread('../img/yungyung.png')
img2 = cv2.imread('../img/3yung.png')
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)
# BF-Hamming으로 매칭 ---②
matcher = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
matches = matcher.match(desc1, desc2)
# 매칭 결과를 거리기준 오름차순으로 정렬 ---③
matches = sorted(matches, key=lambda x:x.distance)
# 최소 거리 값과 최대 거리 값 확보 ---④
min_dist, max_dist = matches[0].distance, matches[-1].distance
# 최소 거리의 15% 지점을 임계점으로 설정 ---⑤
ratio = 0.2
good_thresh = (max_dist - min_dist) * ratio + min_dist
# 임계점 보다 작은 매칭점만 좋은 매칭점으로 분류 ---⑥
good_matches = [m for m in matches if m.distance < good_thresh]
print('matches:%d/%d, min:%.2f, max:%.2f, thresh:%.2f' \
%(len(good_matches),len(matches), min_dist, max_dist, good_thresh))
# 좋은 매칭점만 그리기 ---⑦
res = cv2.drawMatches(img1, kp1, img2, kp2, good_matches, None, \
flags=cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS)
# 결과 출력
plt.figure(figsize = (10,6))
imgs = {'Good Match':res}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,1,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
matches:9/89, min:25.00, max:82.00, thresh:36.40
# knnMatch 함수로 좋은 매칭점 찾기 ( k개의 최근접 이웃 매칭점을 더 가까운 순서대로 반환 )
import cv2, numpy as np
img1 = cv2.imread('../img/yungyung.png')
img2 = cv2.imread('../img/3yung.png')
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)
# BF-Hamming 생성 ---②
matcher = cv2.BFMatcher(cv2.NORM_HAMMING2)
# knnMatch, k=2 ---③
matches = matcher.knnMatch(desc1, desc2, 2)
# 첫번재 이웃의 거리가 두 번째 이웃 거리의 75% 이내인 것만 추출---⑤
ratio = 0.75
good_matches = [first for first,second in matches \
if first.distance < second.distance * ratio]
print('matches:%d/%d' %(len(good_matches),len(matches)))
# 좋은 매칭만 그리기
res = cv2.drawMatches(img1, kp1, img2, kp2, good_matches, None, \
flags=cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS)
# 결과 출력
plt.figure(figsize = (10,6))
imgs = {'Matching':res}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,1,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
matches:9/368
총 368개의 매칭점 중에 9개의 좋은 매칭점을 찾은 것을 확인할 수 있습니다.
8.4.5. 매칭 영역 원근 변환¶
좋은 매칭점으로만 구성된 매칭점 좌표들로 두 영상 간의 원근 변환행렬을 구하면 찾는 물체가 영상 어디에 있는지 표시할 수 있습니다. 이 과정에서 좋은 매칭점 중에 원근 변환행렬에 들어 맞지 않는 매칭점을 구분할 수 있어서 나쁜 매칭점을 또 한번 제거할 수 있습니다.
- mtrx, mask = cv.findHomography(srcPoints, dstPoints[, method[, ransacReprojThreshold[, mask[, maxIters[, confidence]]]])
- srcPoints : 원본 좌표 배열
- dstPoints : 결과 좌표 배열
- method = 0 : 근사 계산 알고리즘 선택 ( 0, cv2.RANSAC, cv2.LMEDS, cv2.RHO )
- ransacReprojThreshold = 3 : 정상치 거리 임계 값
- maxIters = 2000 : 근사 계산 반복 횟수
- confidence : 신뢰도
- mtrx : 결과 변환행렬
- mask : 정상치 판별 결과
- dst = cv.perspectiveTransform(src, m[, dst])
- src : 입력 좌표 배열
- m : 변환 행렬
- dst : 출력 좌표 배열
cv2.findHomography()는 여러 개의 점으로 근사 계산한 원근 변환행렬을 반환합니다.
cv2.RANSAC(Random Sample Consensus) : 모든 입력점을 사용하지 않고 임의의 점들을 선정해서 만족도를 구하는 것을 반복해서 만족도가 가장 크게 선정된 점들만으로 근사 계산합니다.
cv2.LMEDS(Least Median of Squares) : 제곱의 최소 중간값을 사용합니다. 이 방법은 추가 파라미터를 요구하지 않아 사용하기 편리하지만, 정상치가 50\% 이상 있는 경우에만 정상적으로 동작합니다.
cv2.RHD : 이상치가 많은 경우 더 빠른 방법입니다.
import cv2, numpy as np
img1 = cv2.imread('../img/yungyung.png')
img2 = cv2.imread('../img/3yung.png')
gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
# ORB, BF-Hamming 로 knnMatch ---①
detector = cv2.ORB_create()
kp1, desc1 = detector.detectAndCompute(gray1, None)
kp2, desc2 = detector.detectAndCompute(gray2, None)
matcher = cv2.BFMatcher(cv2.NORM_HAMMING2)
matches = matcher.knnMatch(desc1, desc2, 2)
# 이웃 거리의 75%로 좋은 매칭점 추출---②
ratio = 0.75
good_matches = [first for first,second in matches \
if first.distance < second.distance * ratio]
print('good matches:%d/%d' %(len(good_matches),len(matches)))
# 좋은 매칭점의 queryIdx로 원본 영상의 좌표 구하기 ---③
src_pts = np.float32([ kp1[m.queryIdx].pt for m in good_matches ])
# 좋은 매칭점의 trainIdx로 대상 영상의 좌표 구하기 ---④
dst_pts = np.float32([ kp2[m.trainIdx].pt for m in good_matches ])
# 원근 변환 행렬 구하기 ---⑤
mtrx, mask = cv2.findHomography(src_pts, dst_pts)
# 원본 영상 크기로 변환 영역 좌표 생성 ---⑥
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,255,3, cv2.LINE_AA)
# 좋은 매칭 그려서 출력 ---⑨
res = cv2.drawMatches(img1, kp1, img2, kp2, good_matches, None, \
flags=cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS)
plt.figure(figsize = (10,6))
imgs = {'Matching Homography':res}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,1,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
good matches:9/368
good_matches에 있는 매칭점과 같은 자리에 있는 키 포인트 객체에서 각 영상의 매칭점 좌표를 구합니다.
# 매칭기를 통해 얻은 모든 매칭점을 RANSAC 원근 변환 근사 계산으로 잘못된 매칭을 가려냅니다.
import cv2, numpy as np
import matplotlib.pyplot as plt
img1 = cv2.imread('../img/yungyung.png')
img2 = cv2.imread('../img/3yung.png')
gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
# ORB, BF-Hamming 로 knnMatch ---①
detector = cv2.ORB_create()
kp1, desc1 = detector.detectAndCompute(gray1, None)
kp2, desc2 = detector.detectAndCompute(gray2, None)
matcher = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
matches = matcher.match(desc1, desc2)
# 매칭 결과를 거리기준 오름차순으로 정렬 ---③
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)
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,255,3, cv2.LINE_AA)
# 정상치 매칭만 그리기 ---⑦
matchesMask = mask.ravel().tolist()
res2 = cv2.drawMatches(img1, kp1, img2, kp2, matches, None, \
matchesMask = matchesMask,
flags=cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS)
# 모든 매칭점과 정상치 비율 ---⑧
accuracy=float(mask.sum()) / mask.size
print("accuracy: %d/%d(%.2f%%)"% (mask.sum(), mask.size, accuracy))
# 결과 출력
plt.figure(figsize = (8,8))
imgs = {'Matching-All':res1, 'Matching-Inlier ':res2}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(2,1,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
accuracy: 8/89(0.09%)
89개의 전체 매칭점 중에 근사 계산에서 정상치로 판단한 매칭점은 8개입니다. 정상치 매칭점 개수 자체만으로도 그 수가 많을수록 원본 영상과의 정확도가 높다고 볼 수 있고 전체 매칭점 수와의 비율이 높으면 더 확실하다고 볼 수 있습니다.
# 입력 매칭점이 너무 많으면 속도가 느려질 수 있으니 좋은 매칭점을 먼저 선별한 후
# 나쁜 매칭점을 제거하는 것이 속도면에서 유리할 수 있습니다.
import cv2, numpy as np
img1 = None
win_name = 'Camera Matching'
MIN_MATCH = 10
# ORB 검출기 생성 ---①
detector = cv2.ORB_create(1000)
# Flann 추출기 생성 ---②
FLANN_INDEX_LSH = 6
index_params= dict(algorithm = FLANN_INDEX_LSH,
table_number = 6,
key_size = 12,
multi_probe_level = 1)
search_params=dict(checks=32)
matcher = cv2.FlannBasedMatcher(index_params, search_params)
# 카메라 캡쳐 연결 및 프레임 크기 축소 ---③
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
while cap.isOpened():
ret, frame = cap.read()
if img1 is None: # 등록된 이미지 없음, 카메라 바이패스
res = frame
else: # 등록된 이미지 있는 경우, 매칭 시작
img2 = frame
gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
# 키포인트와 디스크립터 추출
kp1, desc1 = detector.detectAndCompute(gray1, None)
kp2, desc2 = detector.detectAndCompute(gray2, None)
# k=2로 knnMatch
matches = matcher.knnMatch(desc1, desc2, 2)
# 이웃 거리의 75%로 좋은 매칭점 추출---②
ratio = 0.75
good_matches = [m[0] for m in matches \
if len(m) == 2 and m[0].distance < m[1].distance * ratio]
print('good matches:%d/%d' %(len(good_matches),len(matches)))
# 모든 매칭점 그리지 못하게 마스크를 0으로 채움
matchesMask = np.zeros(len(good_matches)).tolist()
# 좋은 매칭점 최소 갯수 이상 인 경우
if len(good_matches) > MIN_MATCH:
# 좋은 매칭점으로 원본과 대상 영상의 좌표 구하기 ---③
src_pts = np.float32([ kp1[m.queryIdx].pt for m in good_matches ])
dst_pts = np.float32([ kp2[m.trainIdx].pt for m in good_matches ])
# 원근 변환 행렬 구하기 ---⑤
mtrx, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)
accuracy=float(mask.sum()) / mask.size
print("accuracy: %d/%d(%.2f%%)"% (mask.sum(), mask.size, accuracy))
if mask.sum() > MIN_MATCH: # 정상치 매칭점 최소 갯수 이상 인 경우
# 이상점 매칭점만 그리게 마스크 설정
matchesMask = mask.ravel().tolist()
# 원본 영상 좌표로 원근 변환 후 영역 표시 ---⑦
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,255,3, cv2.LINE_AA)
# 마스크로 매칭점 그리기 ---⑨
res = cv2.drawMatches(img1, kp1, img2, kp2, good_matches, None, \
matchesMask=matchesMask,
flags=cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS)
# 결과 출력
cv2.imshow(win_name, res)
key = cv2.waitKey(1)
if key == 27: # Esc, 종료
break
elif key == ord(' '): # 스페이스바를 누르면 ROI로 img1 설정
x,y,w,h = cv2.selectROI(win_name, frame, False)
if w and h:
img1 = frame[y:y+h, x:x+w]
else:
print("can't open camera.")
cap.release()
cv2.destroyAllWindows()
위의 예제는 연결된 카메라에서 인식할 물체를 들고 space를 누르면 영상이 정지되어 해당 사진에서 마우스를 이용해서 인식할 객체 영역을 선택하고 다시 space를 누르면 객체가 등록되고 다시 영상이 시작됩니다. 그러면 등록한 객체가 실시간으로 매칭되며 영역을 표시해주는 예제입니다. 이 때 글씨나 독특한 문양이 있는 객체는 인식률이 높습니다. 특징 검출기로 ORB를 사용했고, 매칭기 객체로는 FLANN을 사용하였습니다.
8.5. 객체 추적¶
한번 지정한 객체의 위치를 연속된 영상 프레임에서 지속적으로 찾아내는 것을 객체 추적이라고 합니다. 앞서 알아본 매칭이나 앞으로 알아볼 머신러닝 기법으로도 매 장면에서 원하는 객체를 찾아낼 수 있는데 굳이 추적을 사용하는 이유가 무엇일까요? 그 이유는 객체 검출이나 인식은 많은 자원을 필요로 하고 속도가 느리기 때문입니다. 또한 객체 추적은 객체 검출이나 인식이 실패했을 때 좋은 대안으로도 사용할 수 있기 때문입니다. 예를 들어 CCTV에서 범인의 얼굴을 인식했는데 범인이 뒤돌아서 걸어가면 이를 인식하기가 어려운데, 이런 경우에 지속적인 추적을 하기 위해 시간 흐름에 따라 객체를 식별할 수 있는 객체 추적방법을 사용하는 것입니다. 객체 추적 방법은 매 장면을 하나의 영상으로 분석하는 객체 인식 방법과는 달리 장면과 장면의 흐름에 기반하여 시간의 흐름에 따른 객체의 유일성을 알 수 있습니다. 객체 추적을 위한 알고리즘과 OpenCV의 구현은 너무 방대해서 이 책에서 모두 다룰 수는 없고 이 장에서는 기본적인 객체 추적 알고리즘 몇 가지와 그에 따른 OpenCV 구현 함수, 그리고 OpenCV 3.x 버전부터 엑스트라(conbrib) 모듈에 새롭게 추가된 Tracking API를 알아보도록 하겠습니다.
8.5.1. 동영상 배경 제거¶
객체의 움직임을 판단하기 가장 쉬운 방법은 배경만 있는 영상에서 객체가 있는 영상을 빼는 것입니다. 하지만 주변환경이 계속해서 변하는 현실에서 배경만 있는 영상을 찾는 것은 거의 불가능합니다. OpenCV는 동영상에서 배경을 제거하는 다양한 알고리즘을 하나의 인터페이스로 통일하기 위해서 cv2.BackgroundSubtractor 추상 클래스를 만들고 이것을 상속받은 10가지 알고리즘 구현 클래스를 제공합니다. 관련 클래스 목록은 다음 URL에서 살펴볼 수 있습니다. https://docs.opencv.org/3.4.1/d7/df6/classcv_1_1BackgroundSubtractor.html
OpenCV에서 제공하는 두 가지 알고리즘 구현 객체 생성함수는 다음과 같습니다.
- cv2.bgsegm.createBackgroundSubtractorMOG([history, nmixtures, backgroundRatio, noiseSigma)
- history = 200 : 히스토리 길이
- nmixtures = 5 : 가우시안 믹스처의 개수
- backgroundRatio = 0.7 : 배경 비율
- noiseSigma = 0 : 노이즈 강도
import numpy as np, cv2
cap = cv2.VideoCapture('../img_1/walking.avi')
fps = cap.get(cv2.CAP_PROP_FPS) # 프레임 수 구하기
delay = int(1000/fps)
# 배경 제거 객체 생성 --- ①
fgbg = cv2.bgsegm.createBackgroundSubtractorMOG()
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
# 배경 제거 마스크 계산 --- ②
fgmask = fgbg.apply(frame)
cv2.imshow('frame',frame)
cv2.imshow('bgsub',fgmask)
if cv2.waitKey(1) & 0xff == 27:
break
cap.release()
cv2.destroyAllWindows()
위의 방법 말고도 다른 배경 제거 객체 생성 함수도 존재합니다.
- retval = cv2.createBackgroundSubtractorMOG2([, history[, varThreshold[, detectShadows]])
- history = 500 : 히스토리 개수
- varThreshold = 16 : 분산 임계 값
- detectShadows = True : 그림자 표시
# 그림자까지 표시되는 결과를 확인할 수 있다.
import numpy as np, cv2
cap = cv2.VideoCapture('../img_1/walking.avi')
fps = cap.get(cv2.CAP_PROP_FPS) # 프레임 수 구하기
delay = int(1000/fps)
# 배경 제거 객체 생성 --- ①
fgbg = cv2.createBackgroundSubtractorMOG2()
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
# 배경 제거 마스크 계산 --- ②
fgmask = fgbg.apply(frame)
cv2.imshow('frame',frame)
cv2.imshow('bgsub',fgmask)
if cv2.waitKey(delay) & 0xff == 27:
break
cap.release()
cv2.destroyAllWindows()
8.5.2. 옵티컬 플로¶
옵티컬 플로(optical flow)는 이전 장면과 다음 장면 사이의 픽셀이 이동한 방향과 거리에 대한 분포입니다. 이것은 영상 속 물체가 어느 방향으로 얼마만큼 움직였는지를 알 수 있으므로 움직임 자체에 대한 인식은 물론 여기에 추가적인 연산을 가하면 움직임을 예측할 수도 있습니다. 옵티컬 플로는 계산 방식에 따라 2가지로 나뉘는데 일부 픽셀만을 계산하는 희소(sparse) 옵티컬 플로와 영상 전체 픽셀을 모두 계산하는 밀집(dense) 옵티컬 플로로 나뉩니다. 다음은 희소 방식의 루카스-카나데 알고리즘을 구현한 함수입니다.
- nextPts, status, err = cv2.calcOpticalFlowPyrLK(prevImg, nextImg, prevPts, nextPts[, status, err, winSize, maxLevel, criteria, flags, minEigthreshold])
- prevImg : 이전 프레임 영상
- nextImg : 다음 프레임 영상
- prevPts : 이전 프레임의 코너 특징점 ( cv2.goodFeaturesToTrack()으로 검출 )
- nextPts : 다음 프레임에서 이동한 코너 특징점
- status : 결과 상태 벡터, 대응점이 있으면 1, 없으면 0
- err : 결과 에러 벡터, 대응점 간의 오차
- winSize = (21,21) : 각 이미지 피라미드의 검색 윈도 크기
- maxLevel=3 : 이미지 피라미드 계층 수
- criteria = (COUNT+EPS, 30, 0.01) : 반복 탐색 중지 요건
- type
- cv2.TERM_CRITERIA_EPS : 정확도가 epsilon보다 작으면
- cv2.TERM_CRITERIA_MAX_ITER : max_iter 횟수를 채우면
- cv2.TERM_CRITERIA_COUNT : MAX_ITER와 동일
- type
- max_iter : 최대 반복 횟수
- epsilon : 최소 정확도
- flags = 0 : 연산 모드
- minEigThreshold = 1e-4 : 대응점 계산에 사용할 최소 임계 고유값
위 함수는 픽셀 전체를 계산하지 않고 cv2.goodFeaturesToTrack() 함수로 얻은 코너 특징점만 가지고 계산합니다. prevImg와 nextImg에 각각 이전 이후 프레임을 전달하고 prevPts에 이전 프레임에서 검출한 코너 특징점을 전달하면 그 코너점이 이후 프레임의 어디로 이동했는지를 찾아서 nextPts로 반환합니다. 이 때 두 코너점이 서로 대응하는지 여부를 status에 1과 0으로 표시해서 함께 반환합니다. 이 함수는 작은 윈도를 사용하므로 큰 움직임을 계산하기 어렵다는 문제가 있는데 이를 보완하기 위해 이미지 피라미드를 사용합니다. maxLevel에 0을 입력하면 이미지 피라미드를 사용하지 않습니다.
import numpy as np, cv2
cap = cv2.VideoCapture('../img_1/walking.avi')
fps = cap.get(cv2.CAP_PROP_FPS) # 프레임 수 구하기
delay = int(1000/fps)
# 추적 경로를 그리기 위한 랜덤 색상
color = np.random.randint(0,255,(200,3))
lines = None #추적 선을 그릴 이미지 저장 변수
prevImg = None # 이전 프레임 저장 변수
# calcOpticalFlowPyrLK 중지 요건 설정
termcriteria = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03)
while cap.isOpened():
ret,frame = cap.read()
if not ret:
break
img_draw = frame.copy()
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# 최초 프레임 경우
if prevImg is None:
prevImg = gray
# 추적선 그릴 이미지를 프레임 크기에 맞게 생성
lines = np.zeros_like(frame)
# 추적 시작을 위한 코너 검출 ---①
prevPt = cv2.goodFeaturesToTrack(prevImg, 200, 0.01, 10)
else:
nextImg = gray
# 옵티컬 플로우로 다음 프레임의 코너점 찾기 ---②
nextPt, status, err = cv2.calcOpticalFlowPyrLK(prevImg, nextImg, \
prevPt, None, criteria=termcriteria)
# 대응점이 있는 코너, 움직인 코너 선별 ---③
prevMv = prevPt[status==1]
nextMv = nextPt[status==1]
for i,(p, n) in enumerate(zip(prevMv, nextMv)):
px,py = p.ravel()
nx,ny = n.ravel()
# 이전 코너와 새로운 코너에 선그리기 ---④
cv2.line(lines, (px, py), (nx,ny), color[i].tolist(), 2)
# 새로운 코너에 점 그리기
cv2.circle(img_draw, (nx,ny), 2, color[i].tolist(), -1)
# 누적된 추적 선을 출력 이미지에 합성 ---⑤
img_draw = cv2.add(img_draw, lines)
# 다음 프레임을 위한 프레임과 코너점 이월
prevImg = nextImg
prevPt = nextMv.reshape(-1,1,2)
cv2.imshow('OpticalFlow-LK', img_draw)
key = cv2.waitKey(delay)
if key == 27 : # Esc:종료
break
elif key == 8: # Backspace:추적 이력 지우기
prevImg = None
cv2.destroyAllWindows()
cap.release()
- 이전 프레임의 코너를 검출합니다.
- calcOpticalFlowPyrLK() 함수로 옵티컬 플로로 이동한 다음 프레임의 코너 특징점을 찾습니다.
- 이들 중 대응이 잘된것을 선별하여 선과 점으로 표시합니다.
- 이 때 추적선은 매 장면마다 누적해서 보여줘야 하므로 텅빈 이미지에 선을 그린 다음에 원본 영상과 합성하는 방법을 사용했습니다.
- 위 예제를 실행하는 동안 backspace키를 누르면 추적 이력을 지우고 새롭게 추적을 시작합니다.
다음은 밀집 옵티컬 플로 방식의 함수입니다. 밀집 옵티컬 플로는 영상 전체의 픽셀로 계산하므로 추적할 특징점을 따로 전달할 필요가 없지만 속도가 느리다는 단점이 있습니다.
- flow = cv2.calcOpticalFlowFarneback(prev, next, flow, pyr_scale, levels, winsize, iterations, poly_n, poly_sigma, flags)
- prev, next : 이전, 이후 프레임
- flow : 옵티컬 플로 계산 결과, 입력과 동일한 크기의 각 픽셀이 이동한 거리
- pyr_scale : 이미지 피라미드 스케일
- levels : 이미지 피라미드 개수
- winsize : 평균 윈도 크기
- iterations : 각 피라미드에서 반복할 횟수
- poly_n : 다항식 근사를 위한 이웃 크기 ( 5 or 7 )
- poly_sigma : 다항식 근사에서 사용할 가우시안 시그마
- poly_n = 5일때는 1.1 , 7일때는 1.5
- flags : 연산 모드
- cv2.OPTFLOW_USE_INITIAL_FLOW : flow값을 초기 값으로 사용
- cv2.OPTFLOW_FARNEBACK_GAUSSIAN : 박스 필터 대신 가우시안 필터 사용
import cv2, numpy as np
# 플로우 결과 그리기 ---①
def drawFlow(img,flow,step=16):
h,w = img.shape[:2]
# 16픽셀 간격의 그리드 인덱스 구하기 ---②
idx_y,idx_x = np.mgrid[step/2:h:step,step/2:w:step].astype(np.int)
indices = np.stack( (idx_x,idx_y), axis =-1).reshape(-1,2)
for x,y in indices: # 인덱스 순회
# 각 그리드 인덱스 위치에 점 그리기 ---③
cv2.circle(img, (x,y), 1, (0,255,0), -1)
# 각 그리드 인덱스에 해당하는 플로우 결과 값 (이동 거리) ---④
dx,dy = flow[y, x].astype(np.int)
# 각 그리드 인덱스 위치에서 이동한 거리 만큼 선 그리기 ---⑤
cv2.line(img, (x,y), (x+dx, y+dy), (0,255, 0),2, cv2.LINE_AA )
prev = None # 이전 프레임 저장 변수
cap = cv2.VideoCapture('../img_1/walking.avi')
fps = cap.get(cv2.CAP_PROP_FPS) # 프레임 수 구하기
delay = int(1000/fps)
while cap.isOpened():
ret,frame = cap.read()
if not ret:
break
gray = cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY)
# 최초 프레임 경우
if prev is None:
prev = gray # 첫 이전 프레임 --- ⑥
else:
# 이전, 이후 프레임으로 옵티컬 플로우 계산 ---⑦
flow = cv2.calcOpticalFlowFarneback(prev,gray,None,\
0.5,3,15,3,5,1.1,cv2.OPTFLOW_FARNEBACK_GAUSSIAN)
# 계산 결과 그리기, 선언한 함수 호출 ---⑧
drawFlow(frame,flow)
# 다음 프레임을 위해 이월 ---⑨
prev = gray
cv2.imshow('OpticalFlow-Farneback', frame)
if cv2.waitKey(delay) == 27:
break
cap.release()
cv2.destroyAllWindows()
C:\Users\hites\anaconda3\lib\site-packages\ipykernel_launcher.py:7: DeprecationWarning: `np.int` is a deprecated alias for the builtin `int`. To silence this warning, use `int` by itself. Doing this will not modify any behavior and is safe. When replacing `np.int`, you may wish to use e.g. `np.int64` or `np.int32` to specify the precision. If you wish to review your current use, check the release note link for additional information. Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations import sys C:\Users\hites\anaconda3\lib\site-packages\ipykernel_launcher.py:14: DeprecationWarning: `np.int` is a deprecated alias for the builtin `int`. To silence this warning, use `int` by itself. Doing this will not modify any behavior and is safe. When replacing `np.int`, you may wish to use e.g. `np.int64` or `np.int32` to specify the precision. If you wish to review your current use, check the release note link for additional information. Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations
옵티컬 플로를 계산한 결과를 flow는 입력 영상과 같은 크기의 NumPy배열로 저장되고, 각 요소는 해당 위치의 픽셀이 이동한 거리를 (x,y)꼴로 가집니다. 이를 따로 선언해 둔 함수에 전달해서 그리고 미리 선언해둔 drawFlow함수를 이용하여 영상에 16픽셀 간격의 격자 모양으로 점을 그려 각 점에 해당하는 픽셀이 이동한 거리만큼 선을 표시하여 시각화합니다. 특징점만을 이용한 희소 옵티컬 플로로는 특징점의 움직임만을 관찰하는 반면, 밀집 옵티컬 플로는 영상 전체의 픽셀을 관찰하는 것을 알 수 있습니다.
8.5.3. MeanShift 추적¶
MeanShift 추적은 대상 객체의 색상 정보로 추적하는 방법입니다. 평균 이동 알고리즘으로 객체의 색상을 추적하며 아래의 3단계 절차에 따라 계산됩니다.
- 추적 대상을 선정해서 HSV 컬러 스페이스의 H 값 히스토그램 계산
- 전체 영상의 히스토그램(HSV 컬러 스페이스의 H 값) 계산 결과로 역투영
- 역투영 결과에서 이동한 객체를 MeanShift로 추적
우선 영상에서 추적할 대상의 좌표를 구해야 하는데 최초의 추적 대상 좌표는 사람의 손으로 지정하거나 객체 인식을 통해 구해야합니다. 이후 추적 대상 영역의 히스토그램을 계산하는데, 흔히 HSV 컬러 스페이스로 변환한 후 H값만을 사용해서 히스토그램을 계산합니다. 이렇게 구한 히스토그램은 대상 객체의 색상 정보만을 가지게 되고 추적 대상이 지정되었기 때문에 매 장면마다 객체를 추적하기 위해 영상 전체를 HSV 컬러 스페이스로 변환하여 H 값의 히스토그램을 계산하고 대상 객체의 히스토그램과 역투영을 합니다. 역투영은 전체 영상의 색상 정보와 대상 객체의 색상 정보의 비율을 0~255 구간으로 노멀라이즈한 것으로 그 결과는 추적 대상 객체의 색상 값과 비슷한 영역의 픽셀들만 큰 값을 갖습니다. 역투영한 결과에서 최초 객체를 지정한 좌표를 기준으로 평균 이동을 하면 이동한 객체의 중심점을 찾을 수 있습니다.
- retval, window = cv2.meanShift(probImage, window, criteria)
- probImage : 검색할 히스토그램의 역투영 결과
- window : 검색 시작 위치, 검색 결과 위치
- criteria : 검색 중지 요건
- type
- cv2.TERM_CRITERIA_EPS : 정확도가 epsilon보다 작으면
- cv2.TERM_CRITERIA_MAX_ITER : max_iter 횟수를 채우면
- cv2.TERM_CRITERIA_COUNT : MAX_ITER와 동일
- max_iter : 최대 반복 횟수
- epsilon : 최소 정확도
- type
- retval : 수렴한 반복 횟수
위 함수는 probImage 인자에 추적할 객체가 역투영된 히스토그램을 전달하고 window에 초기 추적 위치를 (x,y,w,h) 꼴로 전달하면 반복 시도한 횟수와 함께 새로운 객체의 위치를 (x,y,w,h)꼴로 반환합니다. criteria 인자에는 검색 중지 조건을 전달합니다.
import numpy as np, cv2
roi_hist = None # 추적 객체 히스토그램 저장 변수
win_name = 'MeanShift Tracking'
termination = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 1)
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
while cap.isOpened():
ret, frame = cap.read()
img_draw = frame.copy()
if roi_hist is not None: # 추적 대상 객체 히스토그램 등록 됨
# 전체 영상 hsv 컬로 변환 ---①
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
# 전체 영상 히스토그램과 roi 히스트그램 역투영 ---②
dst = cv2.calcBackProject([hsv], [0], roi_hist, [0,180], 1)
# 역 투영 결과와 초기 추적 위치로 평균 이동 추적 ---③
ret, (x,y,w,h) = cv2.meanShift(dst, (x,y,w,h), termination)
# 새로운 위치에 사각형 표시 ---④
cv2.rectangle(img_draw, (x,y), (x+w, y+h), (0,255,0), 2)
# 컬러 영상과 역투영 영상을 통합해서 출력
result = np.hstack((img_draw, cv2.cvtColor(dst, cv2.COLOR_GRAY2BGR)))
else: # 추적 대상 객체 히스토그램 등록 안됨
cv2.putText(img_draw, "Hit the Space to set target to track", \
(10,30),cv2.FONT_HERSHEY_SIMPLEX, 1, (0,0,255), 1, cv2.LINE_AA)
result = img_draw
cv2.imshow(win_name, result)
key = cv2.waitKey(1) & 0xff
if key == 27: # Esc
break
elif key == ord(' '): # 스페이스-바, ROI 설정
x,y,w,h = cv2.selectROI(win_name, frame, False)
if w and h : # ROI가 제대로 설정됨
# 초기 추적 대상 위치로 roi 설정 --- ⑤
roi = frame[y:y+h, x:x+w]
# roi를 HSV 컬러로 변경 ---⑥
roi = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV)
mask = None
# roi에 대한 히스토그램 계산 ---⑦
roi_hist = cv2.calcHist([roi], [0], mask, [180], [0,180])
cv2.normalize(roi_hist, roi_hist, 0, 255, cv2.NORM_MINMAX)
else: # ROI 설정 안됨
roi_hist = None
else:
print('no camera!')
cap.release()
cv2.destroyAllWindows()
위의 예제는 space를 눌러 영상을 멈추고 마우스로 추적할 객체를 지정하면 선택한 영역을 ROI로 설정하고 HSV 컬러로 변환한 다음 히스토그램을 계산, 이 값을 역투영한 결과를 표시해줍니다. 추적영상의 오른쪽을 보면 선택한 물체 영역만 매우 밝은 값을 갖는 것을 확인할 수 있습니다.
MeanShift 추적은 색상을 기반으로 하므로 추적하려는 객체의 색상이 주변과 비슷하거나 여러 가지 색상으로 이루어진 경우 효과를 보기 어렵고 객체의 크기와 방향과는 상관없이 항상 같은 윈도를 반환한다는 단점을 지닙니다.
8.5.4. CamShift 추적¶
CamShift(Continuously Adaptive MeanShift) 추적은 MeanShift 추적의 문제점인 고정된 윈도 크기와 방향을 개선한 것으로 윈도 크기와 방향을 재설정합니다. 추적을 위한 사전 작업과 절차는 MeanShift 추적과 동일하고 평균 중심점을 찾는 함수만 다릅니다.
- retval, window = cv2.CamShift(probImage, window, criteria)
- 모든 인자와 반환 값은 Meanshift와 동일
import numpy as np, cv2
roi_hist = None # 추적 객체 히스토그램 저장 변수
win_name = 'Camshift Tracking'
termination = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 1)
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
while cap.isOpened():
ret, frame = cap.read()
img_draw = frame.copy()
if roi_hist is not None: # 추적 대상 객체 히스토그램 등록 됨
# 전체 영상 hsv 컬로 변환 ---①
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
# 전체 영상 히스토그램과 roi 히스트그램 역투영 ---②
dst = cv2.calcBackProject([hsv], [0], roi_hist, [0,180], 1)
# 역 투영 결과와 초기 추적 위치로 평균 이동 추적 ---③
ret, (x,y,w,h) = cv2.CamShift(dst, (x,y,w,h), termination)
# 새로운 위치에 사각형 표시 ---④
cv2.rectangle(img_draw, (x,y), (x+w, y+h), (0,255,0), 2)
# 컬러 영상과 역투영 영상을 통합해서 출력
result = np.hstack((img_draw, cv2.cvtColor(dst, cv2.COLOR_GRAY2BGR)))
else: # 추적 대상 객체 히스토그램 등록 안됨
cv2.putText(img_draw, "Hit the Space to set target to track", \
(10,30),cv2.FONT_HERSHEY_SIMPLEX, 1, (0,0,255), 1, cv2.LINE_AA)
result = img_draw
cv2.imshow(win_name, result)
key = cv2.waitKey(1) & 0xff
if key == 27: # Esc
break
elif key == ord(' '): # 스페이스-바, ROI 설정
x,y,w,h = cv2.selectROI(win_name, frame, False)
if w and h : # ROI가 제대로 설정됨
# 초기 추적 대상 위치로 roi 설정 --- ⑤
roi = frame[y:y+h, x:x+w]
# roi를 HSV 컬러로 변경 ---⑥
roi = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV)
mask = None
# roi에 대한 히스토그램 계산 ---⑦
roi_hist = cv2.calcHist([roi], [0], mask, [180], [0,180])
cv2.normalize(roi_hist, roi_hist, 0, 255, cv2.NORM_MINMAX)
else: # ROI 설정 안됨
roi_hist = None
else:
print('no camera!')
cap.release()
cv2.destroyAllWindows()
감사합니다 :)
'대외활동 > DACrew 2기' 카테고리의 다른 글
[ 파이썬으로 만드는 OpenCV 프로젝트🔥] 9장. OpenCV과 머신러닝편(1) (0) | 2022.06.20 |
---|---|
[ 파이썬으로 만드는 OpenCV 프로젝트🔥] 7장. 영상 분할 (0) | 2022.06.03 |
[🔥팀 포스🔥] 첫번째 프로젝트, Multi-Hand Gesture Recognition-2 (0) | 2022.05.27 |
댓글