지난번에는 키포인트를 검출하고, 기술하는 SIFT 알고리즘을 살펴보았습니다. 문제는 이 알고리즘이 매우 느리다는 점인데 2006년에 이를 개선한 SURF: Speed Up Robust Feature라는 논문이 나왔는데, 이름 그대로 SIFT의 가속화된 버전이라 할수 있겠습니다.
SIFT에서는 다양한 스케일 공간에대해서 LoG를 근사한 가우시안 차 영상을 사용합니다. SUFT는 여기서 더 나아가 LOG를 박스 필터로 근사해서 사용합니다. 아래의 그림이 근사한 예시를 보여주고 있는데요. 이 근사의 가장 큰 이점은 박스 필터를 컨볼루션하면 적분 영상 계산이 매우 쉬워집니다. 그리고 다른 스케일에서 적용할수 있겠습니다.
방향을 배정하기 위해서 SURF는 수평과 수직 방향에 대해 크기가 6s인 웨이블릿 응답을 사용하는데요.
* 웨이블릿이란?
- 0을 중심으로 증가 감소를 반복하는 진동. 아래의 그림과 같이 짧은 형태로 기존의 신호에 컨볼루션하여 모르는 정보를 추출하는데 사용할 수 있음.
이들을 아래의 이미지에 플로팅시키면 주 방향 dorminant orientation은 60도의 방향의 윈도우를 슬라이딩하여 모든 응답의 합으로 구할수 있는데요, 여기서 주의깊게 볼점은 적분 영상으로 웨이블릿 응답을 얻을수 있다는 점입니다.
많은 어플리케이션에서 회전 불변이 요구되지는 않고 있으니 SURF에서는 방향을 계산하지 않고 속도를 더 높일수 있도록 하는 기능도 제공하고 있습니다.
특징 기술에 있어서 SURF는 수직, 수평 방향에 대해 웨이블릿 응답을 사용하고 있습니다. 키포인트 주위로 20 x 20형태의 이웃을 가지며, 이들을 4 x 4 형태의 하부 영역으로 나눕니다. 각 하부영역에 대해서 수평, 수직 방향 웨이블릿 응답을 얻고, 그 결과를 아래의 형태를 가진 백터로 만들어 줍시다.
이 벡터는 SURF 특징 기술자로 총 64개으 ㅣ차원이 되겠습니다. 차원도 작고, 고속 계산과 매칭이 가능하나 구분 하기 좋은 특징들을 만들어 줍니다.
이전까지는 해리스 같은 코너 검출기들을 살펴보았습니다. 이들은 회전 불변으로, 이미지가 회전이 된다해도 같은 코너점을 찾을수가 있겠습니다. 하지만 크기변화에선 어떨까요?
이미지가 크기가 조정되면 코너가 더이상 코너가 안될수도 있습니다. 예를들자면 아래의 그림을 한번 살펴보세요. 왼쪽 그림에서 코너는 작은 윈도우에 들어갔지만, 줌해서 커진경우에는 여러 부분들을 찾을수가 있겠습니다.
그래서 해리스 코너는 크기 변화에 불변하지 않다고 얘기 합니다.
2004년 D.Lowe는 새로운 알고리즘으로 크기에 불변한 특징 변환 이라고 하는 SIFT 알고리즘을 제안하였습니다. 여기서 크기 불변 키포인트로 이미지 특징이라는 논문에서 키포인트를 추출하고, 기술자를 계산하는 방법에 대해서 소개하고 있습니다.
SIFT 알고리즘에대해서 4단계로 나누어 설명해보겠습니다.
1. 크기 공간 극점 검출 Scale-space Etrema Detection
위 그림을 보시면 다른 스케일의 키포인트를 검출할때 같은 크기의 윈도우를 사용할수가 없는걸 봤었습니다. 작은 코너라면 괜찬겠죠. 하지만ㄴ 큰 코너를 구한다면 큰 윈도우가 필요할겁니다.
이것떄문에 크기 공간 필터가 사용됩니다. 이를 위해서 다양한 분산 값 omega을 가진 이미지의 LoG Laplacian of Gaussian을 구해보겠습니다. LoG는 방울 검출기 처럼 동작을할건데 omega의 값 변화에 따라 다양한 크기의 blob을 검출하게 될겁니다.
여기서 omega는 스케일 파라미터로 사용되어, 위 영상에서 omega가 작은 가우시안 커널을 사용한다면 작은 코너들을 찾아낼것이고, 큰 omega값을 가진 가우시안 커널을 사용시 큰 코너들을 찾아낼 겁니다.
이렇게 해서 다양한 스케일에 대해 지역 극대점을 찾아내고, 이들을 (x, y, omega)값의 목록으로 정리하겠습니다. 이 값의 의미는 omega 스케일의 공간에서 (x, y)에서 키포인트를 의미합니다.
하지만 LoG는 계산하기에는 비용이커 SIFT 알고리즘은 DoG Differeence of Gaussian을 사용하게되는데 이는 LoG를 근사시킨거라고 할수 있겠습니다. DoG 필터는 두개의 다른 omega로 블러링한 가우시안 영상의 차이로 얻을수가 있겠습니다. 이 방법은 가우시안 피라미드에서 다른 옥타브를 가진것끼리 차 연산을해서 얻을수가 있겠습니다.
그래서 DoG를 구하면, 크기와 공간에 대해서 지역 특징들을 찾을겁니다. 어느 이미지에서 한 픽셀들은 주위의 8개 이웃과 비교할거고, 또 이전 스캐일의 9픽셀과 비교하겠습니다. 이 픽셀이 지역적으로 극점이라면, (잠재적인 키포인트라 판단하고) 해당 스케일에서 최적인 키포인트가 되겠습니다. 이에 대한 그림은 아래와 같습니다.
이 논문에서는 4개의 옥타브와 5개의 스케일 레벨이 주어지고, 초기값으로 omega = 1.6, k = root(2)를 줄때 최적의 결과를 얻을수 있다고 합니다.
2. 키포인트 위치 추정 keypoint localization
잠재적 키포인트의 위치를 찾아내었다면, 실제 결과를 얻기 위해 재정의 하겠습니다. 극점의 정확한 위치를 얻기 위해 스케일 공간에 대해 테일러 전개를 사용할건데, 이 극점에서의 강도가 임계값 0.03(논문에서)보다 작다면 극점이 아닌것으로 판단합니다. 여기서 사용하는 임계치를 contrastTrheshold라 부릅니다.
DoG는 에지에서 강한 응답을 보이는데, 그래서 에지를 제거하여야 합니다. 해리스 코너에서 본것과 비슷한 컨샙을 사용할건데, 2 x 2 해시안 행렬을 사용해서 곡률을 계산하겠습니다
. 해리스 코너 검출기에서 에지의 경우 고유값은 다른것보다 크다는 사실을 알고 있으므로, 이를 간단한 함수로 사용할수 있는데, 이 비율이 임계치보다 크면 그 잠재적 키포인트는 제거하겠습니다. 이렇게 하여 차이가 적은 키포인트와 에지들을 제거하여 강인한 점들만 남게 됩니다.
3. 방향 할당 orientation assingment
각각의 키포인트에는 이미지 회전에 강인할수 있도록 방향이 배정됩니다. 키포인트 주위에 있는 이웃점들은 스케일에 의존하고, 그라디언트 크기와 방향은 이 공간에서 계산할수 있겠습니다. 360도 방향을 다루기위해 36개의 이진 값을 가진 방향 히스토그램을 만들겠습니다.
이 히스토그램은 가우시안 크기와 가우시안 가중치 원형 윈도우(oemga는 1.5인)으로 가중화 될건데, 히스토그램의 가장 높은 지점이나 80%이상인 지점을 방향으로 계산하겠습니다. 이를 통해 같은 크기위 위치를 가졌으나 다른 방향인 키포인트가 만들어지겠습니다. 이 개념이 추후 매치하는데 사용되겠습니다.
4. 키포인트 기술자 keypoint descriptor
이제 키포인트 기술자를 만들어보겠습니다. 키포인트 주위에 16 x 16크기의 이웃점들을 사용할건데 이들을 4 x4 크기의 16개 작은 블록으로 나누겠습니다. 각각의 작은 블록들을 8 이진 방향 히스토그램으로 만들겠습니다. 총 128개의 이진 값들을 얻겠습니다. 이렇게 얻은 벡터를 기포인트 기술자라고 부르겠습니다. 키포인트 기술자를 사용해서 조명 변화나 회전에 강인해질수 있겠습니다.
5. 키포인트 매칭
두 영상 사이의 키포인트를 주위의 최근접 이웃들을 확인해서 매치시킬수가 있겠습니다. 하지만 어떤 경우에는 가장 가까운것보다 두번째것들이 가장 가까울수가 있는데요. 이 경우에는 노이즈나 다른 이유로 발생할수도 있겠습니다. 이 경우에는 두번째 최근접 거리에 대한 최근접 거리의 비율이 필요하겠습니다. 0.8보다 크면 이들은 제거되어 90% 오탐을 제거하는 반면에 5%의 정탐만을 제거한다고 합니다.
이번 장의 목표는 피처에 대해서 이해하고 이게 왜 중요한지, 코너가 왜 중요한지 배워나가겠습니다.
많은 분들이 퍼즐을 해보신적이 있을겁니다. 이미지의 여러 조각들이 주어지고 이것들을 모아 큰 임지ㅣ를 만들어야 합니다. 어떻게 이런것들을 할수 있을까요? 이와 같은 논리를 컴퓨터 프로그램으로 할수 있지 않을까요?
이걸 해내기 위해서 고유하고, 쉽게 추적할수 있고, 비교하기 좋은 특정한 패턴/특징들을 찾고자 합니다. 특징은 이미지의 모든 방향으로 변화가 큰 부분을 말하며, 이런 특징들을 찾는것을 특징 검출이라 합니다. 특징들을 찾았다면 다른 영상에서 같은 부분을 찾고싶을 겁니다. 이건 어떻게 할까요?
이 그림에서 한번봅시다. 윗 부분은 파란 하늘이고 아래는 건물들로 되어있는데 여기서, 특징들이 이 이미지의 어디에 있는지 찾아봅시다. 여러분들은 특징에 대해 조사해보고 컴퓨터도 특징에 대해서 살펴볼겁니다. 이에 대한 설명(기술)을 특징 기술자라고 부릅니다.
특징과 기술자를 찾는다면 모든 이미지에서 찾을수 있고, 나열하며 이을수도 있겠습니다. 앞으로 살펴볼 것들은 어떻게 opencv에서 특징들을 찾고, 기술하고, 매치시키는지 배워보겠습니다.
- 흑백 이미지를 토폴로지로 볼수 있는데, 고 강도 부분은 언덕, 저 강도 부분을 계곡이라 볼수 있습니다. 이제 고립된 계곡 부분(지역 최소점)에 다른색의 물(라벨)이 차있다고 해봅시다. 물이 차면서 언덕이 어떻게 되있느냐에 따라 물이 섞이게 됩니다. 이런 현상을 막으려면 물이 석기는 지점에 장벽을 만들어야 되는데요. 물이 언덕에 찰때까지 물을 더붇고 장벽을 더 지어봅시다. 그렇게 만든 장벽이 분할한 결과라 할수 있겠습니다.
=> 이것이 워터셰드 방법의 기본 개념이 되겠습니다.
- 하지만 노이즈 때문에 너무 과하게 분할될수도있어요. 그래서 OpenCV에선 마커기반 워터세드 알고리즘이 있는데, 지정한 계곡점부분은 합쳐지거나 합쳐지지 않게도 할수 있어요
일단 위 동전 이미지에서 오츠 알고리즘으로 이진화 부터 해봅시다.
import numpy as np
import cv2
from matplotlib import pyplot as plt
img = cv2.imread('coins.png')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gray,0,255,cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)
cv2.imshow("ret", ret)
cv2.waitKey(0)
cv2.destroyAllWindows()
- 이제 이진화 결과중에 아직 흰색 작은 노이즈가 약간식 존재하게 되는데 이를 모폴로지 열림 연산으로 없앨수 있습니다. 작은 점들을 제거하고 나선 모폴로지 닫힘 연산을 진행하겠습니다. 그렇게 해서 물체의 중심 위치와 배경 위치를 파악할수 있게 되지만 아직은 동전들사이 경계선은 알수 없습니다. #noise removal
- 이제 확실히 동전인 영역들을 추출해내야되는데요. 여기서 침식 연산으로 경계 픽셀들을 제거하겠습니다. 그러면 동전이 서로 닫지 않는경우에 확실히 동전인걸 알수 있겠지만 여기 있는 동전들을 서로 붙어있기 때문에, 거리 변환을 수행한후 적절한 임계치로 찾는게 좋은 방법이 되겠습니다. # find sure foreground area
- 다음으로 확실히 동전이 아닌 공간을 찾아보겠습니다. 이 경우 팽창을 하여 얻을수 있는데, 팽창은 물체의 경계를 증가시켜 실제 배경 부분을 더 잘 나타낼수 있도록 도와줍니다. #sure background area
- 확실히 배경인 부분과 확실히 물체인 부분들 이외의 공간을 구하려면 sure_fg - sure_bg 로 얻을 수 있습ㄴ디ㅏ.
- 아래의 이진화 결과 이미지를 보면 붙어있지만 동전들의 위치를 알수 있게 되었습니다. 여기까지 내용이 붙어있는 물체들간의 분할이었고, 물체 분할을 하고자 한다면, 거리 분할을 할 필요없이 침식 연산이면 충분하여 앞에 존재하는 물체들끼리 나눌수 있게 됩니다.
- 이제 코인들의 위치를 알수 있게되었고 어디까지가 배경인지도 알게 되었습니다. 이제 마커를 생성하고 그 마커 공간에 라벨을 붙이겠습니다. 이 영역은 확실히 배경인지 물체인지 알수 있는 것들로 서로 다른 양의 정수값들을 주겠습니다. 배경에는 0을주고,다른 물체는 1에서 부터 라벨을 붙이기 시작하겠습니다.
- 하지만 배경이 0으로 준다면, 워터셰드는 모르는 공간으로 판단해서 동작하게 될겁니다. 그래서 배경도 마킹하려면 다른 정수를 써야됩니다.
=> 배경은 원래 0이나 1로 하고, 다른 물체들은 1 이하의 정수, 모르는 부분은 0으로 마킹
# Marker labelling
ret, markers = cv2.connectedComponents(sure_fg)
# Add one to all labels so that sure background is not 0, but 1
markers = markers+1
# Now, mark the region of unknown with zero
markers[unknown==255] = 0
- 마커는 준비되었으니 워터 셰드를 하겠습니다. 경계 영역은 -1로 마커되니 빨간색으로 칠하겠습니다.
- 위 식에서 3개의 파라미터 x_center, y_center, r을 구하려면 3D 누적기가 필요한데, 대신 opencv에서는 에지의 경사 정보를 이용한 허프 그라디언트 방법이 있습니다.
- cv2.HoughCircles()함수로 원 검출하는 예제는 아래와 같습니다.
import numpy as np
import cv2
img = cv2.imread('./res/opencv_logo.png',0)
img = cv2.medianBlur(img,5)
cimg = cv2.cvtColor(img,cv2.COLOR_GRAY2BGR)
circles = cv2.HoughCircles(img,cv2.HOUGH_GRADIENT,1,20,
param1=50,param2=30,minRadius=0,maxRadius=0)
circles = np.uint16(np.around(circles))
for i in circles[0,:]:
# draw the outer circle
cv2.circle(cimg,(i[0],i[1]),i[2],(0,255,0),2)
# draw the center of the circle
cv2.circle(cimg,(i[0],i[1]),2,(0,0,255),3)
cv2.imshow('detected circles',cimg)
cv2.waitKey(0)
cv2.destroyAllWindows()
- opencv에선 cv2.matchTemplate()이란 함수를 제공하고있는데요. 단순히 탬플릿 이미지를 2차원 컨볼루션처럼 슬라이드하고, 탬플릿과 이미지 일부(이미지와 탬플릿이 겹친 부분)를 비교 한다고 할수 있겠습니다.
- 여기서는 탬플릿과 이웃한 픽셀들로 이루어진 흑백 영상을 반환하게 됩니다.
- W x N 크기의 입력 영상과 w x h 크기의 탬플릿 영상이 주어지면, W-w+1, H-h+1 크기의 출력 영상이 나오게 되는데 여기서 cv2.minMaxLoc()함수로 왼쪽위 코너점의 좌표와 영역의 폭과 높이를 반환해 줍니다. 이 사각 영역이 탬플릿의 공간이라 할수 있겠습니다.
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('C:/Users/do/Documents/github/opencv_python/res/messi.jpg',0)
img2 = img.copy()
template = cv2.imread('C:/Users/do/Documents/github/opencv_python/res/template.jpg',0)
w, h = template.shape[::-1]
# All the 6 methods for comparison in a list
methods = ['cv2.TM_CCOEFF', 'cv2.TM_CCOEFF_NORMED', 'cv2.TM_CCORR',
'cv2.TM_CCORR_NORMED', 'cv2.TM_SQDIFF', 'cv2.TM_SQDIFF_NORMED']
for meth in methods:
img = img2.copy()
method = eval(meth)
# Apply template Matching
res = cv2.matchTemplate(img,template,method)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
# If the method is TM_SQDIFF or TM_SQDIFF_NORMED, take minimum
if method in [cv2.TM_SQDIFF, cv2.TM_SQDIFF_NORMED]:
top_left = min_loc
else:
top_left = max_loc
bottom_right = (top_left[0] + w, top_left[1] + h)
cv2.rectangle(img,top_left, bottom_right, 255, 2)
plt.subplot(121),plt.imshow(res,cmap = 'gray')
plt.title('Matching Result'), plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(img,cmap = 'gray')
plt.title('Detected Point'), plt.xticks([]), plt.yticks([])
plt.suptitle(meth)
plt.show()
탬플릿 매치로 다중 물체 검출하기
- 이전 장에서는 메시 얼굴을 찾아냈다면 이번에는 다중 물체를 검출하겠습니다.
- 이번과 같이 다중 물체 검출의 경우 cv2.minMaxLoC()는 모든 물체의 위치를 알려주지 못하므로 이번에는 임계화를 통해 마리오 게임에서 코인들을 찾아내겠습니다.
import cv2
import numpy as np
from matplotlib import pyplot as plt
img_rgb = cv2.imread('C:/Users/do/Documents/github/opencv_python/res/mario.png')
img_gray = cv2.cvtColor(img_rgb, cv2.COLOR_BGR2GRAY)
img_rgb = cv2.cvtColor(img_rgb, cv2.COLOR_BGR2RGB)
template = cv2.imread('C:/Users/do/Documents/github/opencv_python/res/mario_coin.png',0)
w, h = template.shape[::-1]
res = cv2.matchTemplate(img_gray,template,cv2.TM_CCOEFF_NORMED)
threshold = 0.8
loc = np.where( res >= threshold)
for pt in zip(*loc[::-1]):
cv2.rectangle(img_rgb, pt, (pt[0] + w, pt[1] + h), (0,0,255), 2)
plt.imshow(img_rgb)
plt.show()
#cv2.imwrite('res.png',img_rgb)
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('C:/Users/do/Documents/github/opencv_python/res/home.jpg',0)
plt.subplot(121),plt.imshow(img, cmap="gray")
plt.subplot(122),plt.hist(img.ravel(),256,[0,256]);
plt.show()
2. 컬러 이미지 히스토그램 출력
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('C:/Users/do/Documents/github/opencv_python/res/home.jpg')
color = ('b','g','r')
for i,col in enumerate(color):
histr = cv2.calcHist([img],[i],None,[256],[0,256])
plt.plot(histr,color = col)
plt.xlim([0,256])
plt.show()
3. 비트와이즈 연산으로 마스킹
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('C:/Users/do/Documents/github/opencv_python/res/home.jpg',0)
# create a mask
mask = np.zeros(img.shape[:2], np.uint8)
mask[100:300, 100:400] = 255
masked_img = cv2.bitwise_and(img,img,mask = mask)
# Calculate histogram with mask and without mask
# Check third argument for mask
hist_full = cv2.calcHist([img],[0],None,[256],[0,256])
hist_mask = cv2.calcHist([img],[0],mask,[256],[0,256])
plt.subplot(221), plt.imshow(img, 'gray')
plt.subplot(222), plt.imshow(mask,'gray')
plt.subplot(223), plt.imshow(masked_img, 'gray')
plt.subplot(224), plt.plot(hist_full), plt.plot(hist_mask)
plt.xlim([0,256])
plt.show()