Python과 OpenCV – 31 : Harris Corner Detection

이 글의 원문은 https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_feature2d/py_features_harris/py_features_harris.html 입니다.

이미지의 특징점인 귀퉁이(Corner) 지점을 추출하는 알고리즘 중 하나인 Harris Corner Detection에 대한 예제를 살펴 봅니다.

import cv2
import numpy as np

filename = './data/blox.jpg'
img = cv2.imread(filename)
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)

gray = np.float32(gray)
dst = cv2.cornerHarris(gray,2,3,0.04)

img[dst>0.01*dst.max()]=[0,0,255]

cv2.imshow('dst',img)
cv2.waitKey(0)
cv2.destroyAllWindows()

결과는 아래와 같습니다. 입력 이미지에 대해 Corner로 인식되는 부분에 빨간색 점 표시가 되어 있습니다.

cv2.cornerHarris() 함수의 인자를 살펴보면, 첫번째는 입력 이미지로써 단일 채널 이미지(Grayscale)여야 하며 데이터 타입은 float32입니다. 두번째는 Corner 검출을 위한 알고리즘 수행 중 검사할 이웃 픽셀의 크기입니다. 세번째는 내부적으로 적용할 Sobel 필터링에 대한 인자(Apeture Parameter)입니다. 끝으로 네번째는 방적식의 Harris Detector의 자유변수입니다. cv2.corenrHarris() 함수의 결과로 입력 이미지와 동일한 크기의 2차원 배열이 생성되는데, Corner 지점은 이 배열의 값들 중 최대값의 0.01 배 이상인 지점만을 표시하기 위해 11번 코드가 사용됩니다.

Corner를 cv2.corenrHarris 함수를 통해 추출한 뒤, 이 추출 결과를 이용해 다시 좀더 정밀한 귀퉁이를 추출할 수 있습니다. 이를 SubPixel Accuraacy를 가지는 Corner이라고 합니다. 예제는 다음과 같습니다.

import cv2
import numpy as np

filename = './data/blox.jpg'
img = cv2.imread(filename)
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)

# find Harris corners
gray = np.float32(gray)
dst = cv2.cornerHarris(gray,2,3,0.04)
dst = cv2.dilate(dst,None)
ret, dst = cv2.threshold(dst,0.01*dst.max(),255,0)
dst = np.uint8(dst)

# find centroids
ret, labels, stats, centroids = cv2.connectedComponentsWithStats(dst)

# define the criteria to stop and refine the corners
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 0.001)
corners = cv2.cornerSubPix(gray,np.float32(centroids),(5,5),(-1,-1),criteria)

# Now draw them
res = np.hstack((centroids,corners))
res = np.int0(res)
img[res[:,1],res[:,0]] = [0,0,255]
img[res[:,3],res[:,2]] = [0,255,0]

cv2.imshow('dst',img)
cv2.waitKey(0)
cv2.destroyAllWindows()

결과는 다음과 같습니다.

픽셀이 작아 눈에 보기 힘들 수 있지만, 빨간색의 픽셀이 앞서 언급한 cv2.cornerHarris 함수를 통해 추출한 Corner이고 초록색의 픽셀이 cv2.cornerHarris 함수를 통해 얻은 결과를 활용해 cv2.cornerSubPix 함수를 적용해 좀더 정확한 Corenr의 위치를 추출한 결과입니다.

Python과 OpenCV – 29 : GrabCut 알고리즘을 이용한 전경(Foreground) 추출

이 글의 원문은 https://github.com/abidrahmank/OpenCV2-Python-Tutorials/blob/master/source/py_tutorials/py_imgproc/py_grabcut/py_grabcut.rst 입니다.

GrabCut 알고리즘은 이미지에서 배경이 아닌 전경에 해당하는 이미지를 추출해 내는 방법입니다. 이미지에서 한번에 전경을 추출해 내는 것이 아닌 사용자와의 상호 작용을 통해 단계적으로 전경을 추출합니다. 이 상호작용은 크게 2가지 단계로 진행되는데, 첫번째는 이미지에서 전경이 포함되는 영역을 사각형으로 대략적으로 지정합니다. 단, 이때 지정한 사각형 영역 안에는 전경이 모두 포함되어 있어야 합니다. 그리고 두번째는 첫번째에서 얻어진 전경 이미지의 내용중 포함되어진 배경 부분은 어디인지, 누락된 전경 부분은 어디인지를 마킹하면 이를 이용해 다시 전경 이미지가 새롭게 추출됩니다.

위의 GrabCut 알고리즘에 대한 OpenCV의 구현은 cv2.grabCut 입니다. 예제를 통해 이 함수를 살펴보겠습니다. 첫번째 예제는 앞서 언급한 GrabCut의 상호작용 중 전경이 포함된 사각 영역을 지정하는 것만을 이용해 전경을 추출하는 경우입니다.

import numpy as np
import cv2
from matplotlib import pyplot as plt

img = cv2.imread('./data/messi5.jpg')
mask = np.zeros(img.shape[:2],np.uint8)

bgdModel = np.zeros((1,65),np.float64)
fgdModel = np.zeros((1,65),np.float64)

rect = (50,50,450,290)
cv2.grabCut(img,mask,rect,bgdModel,fgdModel,5,cv2.GC_INIT_WITH_RECT)

mask2 = np.where((mask==2)|(mask==0),0,1).astype('uint8')
img = img*mask2[:,:,np.newaxis]

plt.imshow(img),plt.colorbar(),plt.show()

결과는 아래와 같습니다.

코드 중 전경이 포함된 사각영역은 11번 코드에서 rect 변수에 지정하고 있습니다. 이 rect 변수를 이용해 12번 코드에서 전경을 추출하는 cv2.grabCut 함수가 실행됩니다. 이 함수의 인자를 살펴보면 첫번째는 입력 이미지, 두번째는 6번 코드에서 생성한 모든 요소가 0인 마스크 이미지, 세번째는 사각형역, 네번째와 다섯번째는 이 알고리즘의 수행 과정중 활용할 메모리, 여섯번째는 해당 알고리즘의 반복 수행 횟수, 일곱번재는 사각형 영역 지정을 통한 GrabCut일 경우 cv2.GC_INIT_WITH_RECT를 지정합니다. 이 인자 중 6번째인 mask에는 전경에 해당하는 화소 위치에 값이 설정되는데, 0 또는 2 값은 배경이고 1 또는 3인 전경이라는 의미입니다. 이를 이용해 원본 이미지에 마스킹 처리를 해서 전경만을 표시하는 것이 14-17번 코드입니다.

다음은 사각형 영역의 지정 이후에 추출된 전경에 대해 다시 누락된 전경과 포함된 배경 부분을 마킹하여 보다 완전한 전경 이미지를 추출하는 전체적인 GrabCut 알고리즘에 대한 예제입니다.

import numpy as np
import cv2
from matplotlib import pyplot as plt

img = cv2.imread('./data/messi5.jpg')
mask = np.zeros(img.shape[:2],np.uint8)

bgdModel = np.zeros((1,65),np.float64)
fgdModel = np.zeros((1,65),np.float64)

# Step 1
rect = (50,50,450,290)
cv2.grabCut(img,mask,rect,bgdModel,fgdModel,1,cv2.GC_INIT_WITH_RECT)

# Step 2
newmask = cv2.imread('./data/newmask2.png',0)
mask[newmask == 0] = 0
mask[newmask == 255] = 1
cv2.grabCut(img,mask,None,bgdModel,fgdModel,1,cv2.GC_INIT_WITH_MASK)

mask2 = np.where((mask==2)|(mask==0),0,1).astype('uint8')
img = img*mask2[:,:,np.newaxis]
plt.imshow(img),plt.colorbar(),plt.show()

결과는 다음과 같습니다.

코드 중 Step1으로 처리된 부분은 앞서 설명한 사각영역 지정을 통한 전경의 추출이고 Step2가 마킹을 통한 전경 이미지 추출입니다. 마킹 방식은 newmask2.png에 마킹 정보를 나타내고 있는데 이 이미지는 아래와 같습니다.

즉, 하얀색 픽셀값(255)은 전경을, 검정색 픽셀값(0)은 배경임을 나타내는 마킹 정보를 포함하고 있는 이미지입니다.

Python과 OpenCV – 28 : Watershed 알고리즘을 이용한 이미지 분할

이 글의 원문은 https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_watershed/py_watershed.html 입니다.

회색조 이미지는 지형처럼 해석할 수 있는데, 값이 높은 픽셀 위치는 산꼭대기이고 값이 낮은 픽셀 위치는 계곡이라고 해석할 수 있습니다. 지형이므로 고립되어 분리된 계곡이 있을 것이고 이 계곡들을 서로 다른 색의 물로 채우기 시작하면 물이 점점 차오르다가 이웃한 계곡의 언저리에서 물이 합쳐지게 됩니다. 물이 합쳐지는 것을 피하기 위해서 합쳐지는 순간에서의 위치에 경계를 생성하는거죠. 그럼 이 경계선이 이미지 분할의 결과가 됩니다. 이것이 바로 Watershed 알고리즘의 기본철학입니다. 아래의 동영상 이미지를 보면 좀더 직관적으로 이해할 수 있습니다.

이 방식을 통해 이미지를 분할하게 되면 분할에 오류가 발생할 수 있는데, 이는 이미지의 잡음이나 어떤 불규칙한 것들로 인한 요소 등이 이유입니다. 그래서 OpenCV는 마커 기반의 Watershed 알고리즘을 구현해 제공하는데.. 각 계곡을 구성하는 화소들을 병합시켜 번호를 매기고, 병합 수 없는 애매한 화소는 0값을 매깁니다. 이를 인터렉티브한 이미지 분할 기법이라고 합니다. 우리가 알고 있는 객체에 각각에 대해 0 이상의 번호를 매기는 것인데요. 전경이 되거나 객체인 것에, 또 배경에도 0 이상의 값을 매깁니다. 그러나 그외 불명확하다라고 판단되는 것은 0을 매깁니다. 이 불명확한 것이 어떤 요소, 즉 배경인지 전경인지 또는 어떤 객체의 소유인지는 Watershed 알고리즘을 통해 결정됩니다. Watershed 알고리즘을 통해 분할 경계선이 생길 것이고 이 경계선에 대해서는 -1 값을 매깁니다.

자, 이제 이론에 대한 설명은 끝났으므로 예제 코드를 살펴보겠습니다.

import cv2
import numpy as np
from matplotlib import pyplot as plt

img = cv2.imread('./data/water_coins.jpg')

# 이진 이미지로 변환
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)

# 잡음 제거
kernel = np.ones((3,3), np.uint8)
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)

# 이미지 확장을 통해 확실한 배경 요소 확보
sure_bg = cv2.dilate(opening, kernel, iterations=3)

# distance transform을 적용하면 중심으로 부터 Skeleton Image를 얻을 수 있음.
# 이 결과에 Threshold를 적용하여 확실한 객체 또는 전경 요소를 확보
dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5)
ret, sure_fg = cv2.threshold(dist_transform, 0.5*dist_transform.max(), 255, 0)
sure_fg = np.uint8(sure_fg)

# 배경과 전경을 제외한 영역 곳을 확보
unknown = cv2.subtract(sure_bg, sure_fg)

# 마커 생성 작성
ret, markers = cv2.connectedComponents(sure_fg)
markers = markers + 1
markers[unknown == 255] = 0

# 앞서 생성한 마커를 이용해 Watershed 알고리즘을 적용
markers = cv2.watershed(img, markers)
img[markers == -1] = [255,0,0]

images = [gray,thresh,opening, sure_bg, dist_transform, sure_fg, unknown, markers, img]
titles = ['Gray', 'Binary', 'Opening', 'Sure BG', 'Distance', 'Sure FG', 'Unknow', 'Markers', 'Result']

for i in range(len(images)):
    plt.subplot(3,3,i+1)
    plt.imshow(images[i])
    plt.title(titles[i])
    plt.xticks([])
    plt.yticks([])

plt.show()

실행 결과는 다음과 같습니다.

코드와 실행 결과를 비교해 가며, 설명을 해 보면.. 먼저 5번 코드에서 입력 이미지를 파일로부터 읽고 이 이미지를 2진 이미지로 생성하는 것이 5-9번 코드이고 결과 이미지의 Binary입니다. 잡음을 제거 하기 위해 12-13번 코드가 실행되고 결과 이미지의 Opening입니다. 잡음을 제거한 이미지에 dilate 함수를 통해 이미지의 객체를 확장시킨 것이 16번 코드이고 결과 이미지의 Sure_BG입니다. 이제 확실한 전경 또는 객체에 대한 화소를 얻기 위해 18-22번 코드가 실행되고 그 결과 이미지는 Sure_FG입니다. Sure_FG는 결과 이미지의 Distance 이미지로부터 threshold 처리를 통해 얻어진 것입니다. 이제 배경인 Sure_BG에서 전경인 Sure_FG를 빼면 애매모호한 영역을 얻을 수 있게 되는데, 25번 코드가 이에 해당되고 그 결과 이미지는 Unknonw입니다. 즉 어떤 문제를 해결하기 위해 문제의 범위를 좁혀 나가고 있다는 것을 직감할 수 있습니다. 이제 마커 이미지를 sure_fg를 이용해 생성하는데 28-30번 코드입니다. 머커는 0값부터 지정되므로 결과 마커에 1씩 증감시키고, 애매모호한 부분에 대해서는 0 값을 지정합니다. 앞서 이론에 언급했던 것처럼요. 마커가 준비되었으므로, 이제 Watershed 알고리즘을 적용하고 분할 경계선에 해당되는 화소에 지정된 값인 -1을 가지는 부분을 [255,0,0] 색상으로 지정하는 것이 33-34번 코드이며, 최종 결과 이미지인 Result입니다.

Python과 OpenCV – 27 : 이미지에서 원형 도형 검출(Hough Circle Transform)

이 글의 원문은 https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_houghcircles/py_houghcircles.html#hough-circles 입니다.

앞서 이미지에서 선형 도형을 검출하는데, Hough Transform 알고리즘을 사용했습니다. 이 알고리즘은 수학적 모델링이 가능한 모든 도형을 이미지에서 검출할 수 있는 방법입니다. 그렇다면 원형에 대한 수학적 모델식을 이용해 Hought Transform을 적용할 수 있는데, 문제는 원에 대한 수학식이 중심점 (x, y)와 반지름(r)이라는 3개의 매개변수로 구성되어 있고, 결국 3차원 배열이라는 저장소를 요구한다는 점. 그럼으로 인해 연산이 매우 비효율적이라는 점입니다. 이에 대한 개선으로 Gradient(가장자리에서의 기울기값)을 이용하여 Hought Transform을 적용할 수 있고, 이에 대한 구현으로 OpenCV에서는 cv2.HoughCircles 함수를 제공합니다. 이 함수의 예는 다음과 같습니다.

import cv2
import numpy as np

img = cv2.imread('./data/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,:]:
    cv2.circle(cimg,(i[0],i[1]),i[2],(0,255,0),2)

cv2.imshow('detected circles',cimg)
cv2.waitKey(0)
cv2.destroyAllWindows()

cv2.HoughCircles는 제법 많은 인자를 받는데요. 위의 예제를 통해 보면, 첫번째는 입력 이미지로써 8비트 단일 채널인 Grayscale 이미지, 두번째는 방식으로써 현재는 cv2.HOUGH_GRADIENT만을 지원합니다. 세번째는 대부분 1을 지정하는데, 이 경우 입력 이미지와 동일한 해상도가 사용됩니다. 네번째는 검출한 원의 중심과의 최소거리값으로 이 최소값보다 작으면 원으로 판별되지 않습니다. 그리고 param1은 Canny Edge에 전달되는 인자값, param2는 검출 결과를 보면서 적당이 조정해야 하는 값으로 작으면 오류가 높고 크면 검출률이 낮아진다고 합니다. minRadius와 masRadius는 각각 원의 최소, 최대 반지름이고 0으로 지정하면 사용되지 않습니다. 결과는 다음과 같습니다.