OpenCV 프로그래밍 - Chapter 05 영상 분할(1)
카테고리: OpenCV
이 포스팅은 ‘python으로 배우는 OpenCV 프로그래밍’ 교재를 참고하여 작성하였습니다.
영상 분할은 입력 영상에서 관심 있는 영역을 분리하는 과정이다.
이러한 영상 분할은 영상 분석, 물체 인식, 추적 등 대부분의 영상처리 응용에서 필수적인 단계이다.
영상 분할은 크게 경계선 또는 영역으로 분할한다.
임계값 사용 방법은 가장 간단한 영상 분할 함수cv2.threshold()
, cv2.adaptiveThreshold()
를 사용하는 것이다.
cv2.threshold()
함수는 주어진 임계값(threshold)에 따라 다양한 임계값 영상을 출력한다.
예를 들어, cv2.THRESH_BINARY
를 사용하면 2개의 값만을 갖는 이진 영상을 생성할 수 있다.
cv2.adaptiveThreshold()
함수는 화소마다 다른 임계값을 적용하는 적응형 임계값 영상을 계산한다.
각 함수의 문법은 다음과 같다.
cv2.threshold(src, thresh, max_val, type[, dst]) -> retval, dst
cv2.adaptiveThreshold(src, max_val, adaptiveMethod, thresholdType, blockSize, C[, dst]) -> dst
각 파라미터 종류에 대해서는 추후에 필요하다면 검색해서 내용을 추가하겠다.
위에서 설명한 바를 바탕으로 이장에서는 Canny Edge detection, Hough transform에 의한 직선, 원 검출, 컬러 범위에 의한 영역 분할, 임계값을 적용한 영역 분할 방법, 윤곽선 검출에 대하여 설명한다.
01 Canny Edge detection
에지는 앞서 설명했듯 영상화소의 그레디언트가 최대가 되는 지점이며 이 지점은 영상의 물체와 물체 사이 또는 물체와 배경 사이의 테두리가 된다.
에지를 검출하는 과정은 (가우시안 블러링을 거친 후) 1차 미분 필터 cv2.Sobel()
와 2차 미분 필터 cv2.Laplacian()
으로 필터링 후 0 - 교차점을 찾아 에지를 검출한다.
0- 교차점에 관한 내용은 프로젝트를 진행하는데에 필요하다면 이곳에 추가하도록 하겠다.
위 과정을 OpenCV에서 cv2.Canny()
함수를 사용해서 에지를 검출할 수 있도록 구현해놓았고, 우리는 그 함수를 사용하는 방법에 대해 알아보도록 하자.
cv2.Canny(image, threshold1, threshold2[, edges[, apertureSize[, L2gradient]]]) -> edges
1 - 채널 8비트 입력 영상 image에서 에지를 검출하여 edges를 반환한다.
apertureSize는 그래디언트를 계산하기 위한 Sobel필터의 크기로 사용하고, 두 임계값을 사용하여 연결된 에지를 얻는다.
먼저 높은 임계값(threshold2)를 사용하여 그래디언트 방향에서 낮은 값의 임계값(threshold1)이 나올 때까지 추적하며 에지를 연결하는 히스테리시스 임계값(hysteresis thresholding)방식을 사용한다.
L2gradient의 True/False에 따라 그래디언트의 크기를 계산하는 방식이 다르다.
입력 영상에 cv2.Canny()함수 호출 전에 cv2.GaussianBlur()함수 같은 블러링 함수로 영상을 부드럽게 하면 noise에 덜 민감하다.
에지 검출 : cv2.Canny()
import cv2
import numpy as np
src = cv2.imread('./data/lena.jpg', cv2.IMREAD_GRAYSCALE)
edges1 = cv2.Canny(src, 50, 100, 3)
edges2 = cv2.Canny(src, 50, 200, 3)
cv2.imshow('edges1', edges1)
cv2.imshow('edges2', edges2)
cv2.waitKey()
cv2.destroyAllWindows()
위 코드를 실행하면 임계값이 다르게 처리된 결과영상이 2개 윈도우에 표시되며 threshold2가 100인 윈도우가 200인 윈도우보다 에지가 더 많이 검출되는 것을 알 수 있다.
따라서 각 상황에 맞는 적절한 임계값을 찾아서 조정해야한다.
02 Hough transform에 의한 직선, 원 검출
우리가 여지껏 검출한 에지는 단순히 화소들의 집합이다.
즉, 에지는 직선, 사각형, 원, 곡선 등의 구조적 정보를 갖지 않는다.
하지만 Hough transform을 사용하면, 에지에서 직선 또는 원의 방정식의 파라미터를 검출할 수 있다.
직선 형태를 띄는 수많은 점들의 집합을 직선으로 표현하기 위한 수학적인 방법이며 극좌표에 의한 직선의 방정식을 사용한다.
직선 검출을 위한 허프 변환 알고리즘은 각 에지 점(x, y)에 대하여, 이산격자 간격에서 점(x, y)을 지나가는 가능한 모든 극좌표에 의한 직선의 방정식의 계수를 계산하여, 대응하는 정수배열을 1씩 증가시킨다.
직선의 형태를 유지한다면 지속적으로 정수배열의 크기가 증가할 것이며, 지정한 threshold보다 큰 수를 가지며 local maxima인 직선이 수많은 점을 나타내는 직선이 될 것이다.
위 과정을 OpenCV에서 cv2.HoughLines()
함수를 사용해서 직선을 구할수 있도록 구현해놓았고, 우리는 그 함수를 사용하는 방법에 대해 알아보도록 하자.
cv2.HoughLines(image, rho, theta, threshold[, lines[, srn[, stn[, min_theta[, max_theta]]]]]) -> lines
1 - 채널 8비트 이진 영상 image에서 lines를 반환한다.
lines는 검출된 직선의 계수가 저장된 배열이다.
rho는 원점으로부터의 거리 간격, theta는 x축과의 각도(라디안), threshold는 직선을 검출하기 위한 임계값이다.
또한, probabilistic Hough변환을 이용하여 양 끝점이 있는 선분을 lines에 검출한다. 이때의 함수는 다음과 같다.
cv2.HoughLinesP(image, rho, theta, threshold[, lines[, minLineLength[, maxLineGap]]]) -> lines
출력되는 lines는 선분의 양 끝점(x1, x2, y1, y2)를 저장하는 배열이다.
minLineLength는 검출할 최소 직선의 길이이고 maxLineGap은 직선 위의 에지들의 최대 허용 간격이다.
직선 검출 : cv2.HoughLines()
import cv2
import numpy as np
src = cv2.imread('./data/rect.jpg')
gray = cv2.cvtColor(src,cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray, 50, 100)
lines = cv2.HoughLines(edges, rho=1, theta=np.pi/180.0, threshold=100)
print('lines.shape=', lines.shape)
for line in lines:
rho, theta = line[0]
c = np.cos(theta)
s = np.sin(theta)
x0 = c*rho
y0 = s*rho
x1 = int(x0 + 1000*(-s))
y1 = int(y0 + 1000*(c))
x2 = int(x0 - 1000*(-s))
y2 = int(y0 - 1000*(c))
cv2.line(src, (x1,y1), (x2,y2), (0,0,255), 2)
cv2.imshow('edges', edges)
cv2.imshow('src', src)
cv2.waitKey()
cv2.destroyAllWindows()
앞서 설명했듯 cv2.HoughLines()
함수는 극좌표에 의한 직선의 방정식의 계수를 반환하며 이들은 각각 원점과의 거리와 x축과의 각도이다.
이를 각 변수에 저장해서 우리가 영상을 표시하는 좌표축인 x - y 좌표축으로 변환시켜준 것이다.
따라서 우리는 cv2.Canny()
함수를 통해 검출된 에지들의 집합을 직선으로 특정지을 수 있게되었고, for문의 코드를 통해 연장된 직선을 화면에 표시한 것이다.
cv2.HoughLines()
함수의 반환값과 영상을 표현하는데에 사용되었던 좌표축간의 관계식은 우리가 너무나도 잘 알고있는 간단한 삼각함수로 표현됨을 알 수 있는 실습코드이다.
한마디로 정리해서 극좌표계와 직교좌표계의 관계식만으로도 cv2.HoughLines()
함수를 통한 직선 표현이 가능하다는 것이다.
와닿지 않을 수도 있지만 이전 장 내용을 상기하며 천천히 복기하면 이해가 될 것이다.
cv2.HoughLinesP()
함수에 대한 실습코드에 대한 설명은 생략하겠다.
필요하다면 이곳에 내용을 추가하겠다.
03 컬러 범위에 의한 영역 분할
cv2.inRange()
함수를 사용하면 lowerb와 upperb 범위의 컬러 영역을 분할할 수 있다.
cv2.inRange(src, lowerb, upperb[. dst]) -> dst
입력 src의 각 화소가 lowerb(i) <= src(i) <= upperb(i) 범위에 있으면 dst(i) = 255(흰색), 그렇지 않으면 dst(i) = 0(검정색)이다.
lowerb와 upperb는 Scalar도 가능하고, dst는 src와 같은 크기의 8비트 부호 없는 정수 자료형이다.
src가 다중 채널인 경우, 모든 채널에 대하여 범위를 만족해야 한다.
3 - 채널 컬러영상에서, HSV 색상으로 변환 후에, 색상 범위를 지정하여 손, 얼굴 등의 피부 검출 등을 분할할 때 유용하다.
컬러 영역 검출 : cv2.inRange()
import cv2
import numpy as np
#1
src1 = cv2.imread('./data/hand.jpg')
hsv1 = cv2.cvtColor(src1, cv2.COLOR_BGR2HSV)
lowerb1 = (0, 40, 0)
upperb1 = (20, 180, 255)
dst1 = cv2.inRange(hsv1, lowerb1, upperb1)
#2
src2 = cv2.imread('./data/flower.jpg')
hsv2 = cv2.cvtColor(src2,cv2.COLOR_BGR2HSV)
lowerb2 = (150, 100, 100)
upperb2 = (180, 255, 255)
dst2 = cv2.inRange(hsv2, lowerb2, upperb2)
#3
cv2.imshow('src1', src1)
cv2.imshow('dst1', dst1)
cv2.imshow('src2', src2)
cv2.imshow('dst2', dst2)
cv2.waitKey()
cv2.destroyAllWindows()
HSV가 의미하는 바와 값에 따른 변화를 알지 못하기에 정확한 동작법은 알지 못하겠다.
따라서 추후에 HSV에 대한 학습이 완료되면 다시 복습하도록 한다.
HSV가 의미하는 바는 모르더라도, inRange()함수는 아래 출력 결과와 같이 범위에 따라서 영역을 분할할 수 있다는 것을 알아가도록 하자.
예상컨데 손이 대략적으로 나타내는 HSV값은 범위에 있고, 배경이 대략적으로 나타내는 HSV값은 범위 밖에 있을 것이다.
마찬가지로 꽃이 대략적으로 나타내는 HSV값은 범위에 있고, 배경이 대략적으로 나타내는 HSV값은 범위 밖에 있을 것이다.
이때 꽃의 가운데 부분은 범위 안에 있지 않은 값으로 추정된다.
댓글남기기