OpenCV 프로그래밍 - Chapter 04 영상 공간 필터링
카테고리: OpenCV
이 포스팅은 ‘python으로 배우는 OpenCV 프로그래밍’ 교재를 참고하여 작성하였습니다.
이 장에서는 입력 영상 화소의 주변 이웃을 고려하여 처리하는 공간영역 영상처리 필터링에 대하여 설명한다.
일반적인 공간영역 처리방법은 입력 영상 src(x, y)의 화소 (x, y)뿐만 아니라 주위의 이웃 화소 w(x, y)를 고려하여, 변환/연산 함수 T()에 의해 출력 영상 dst(x,y)의 (x, y)위치에서 화소값을 계산한다.
w(x, y)는 필터 또는 윈도우라고 부르며, 주로 홀수(3 * 3, 5 * 5 …) 크기이다.
01 블러 필터
블러 필터는 영상을 부드럽게 하고 영상의 잡음(noise)를 제거한다.
아래 표에서는 영상 밖의 화소를 채우는 borderType을 사용하는 경계선 처리(padding) 방법에 대한 1차원 배열의 예를 설명한다.
주요 경계값을 채우는 메서드들은 아래 표와 같으며 필요하다면 참고하도록 한다.
앞서 블러 필터를 적용할 수 있는 5가지 함수를 표 형식으로 소개했다.
그렇다면 각 함수들의 문법과 작동 원리에 대해 알아보자.
1)cv2.boxFilter(src, ddepth, ksize[, dst[, anchor[, normalize[, borderType]]]]) -> dst
boxFilter() 함수의 매개변수 중 src는 입력 영상, dst는 src와 같은 크기, 같은 자료형의 ddepht 깊이의 필터링된 출력 영상이다.
ddepht = -1 이면 src와 같은 깊이이다.
anchor = (-1, -1)이 디폴트이며 커널 중심을 의미하고, normalize = True는 튜플 커널 크기 ksize = (kw, kh)로 정규화되며, 평균 필터와 같다.
박스 필터의 커널 K는 다음과 같다.
2)
cv2.bilateralFilter(src, d, sigmaColor, sigmaSpace[, dst[, borderType]]) -> dst
bilateralFilter() 함수는 가우시안 함수를 사용하여 에지를 덜 약화하면서 양방향 필터링을 한다.
src는 8비트 또는 32비트 float 자료형의 1 - 채널 또는 3 - 채널 입력 영상이고, dst는 src와 같은 크기, 같은 자료형인 출력 영상이다.
d는 필터링 동안 사용될 각 화소의 이웃을 결정할 지름이다.
실시간처리를 위해서는 d = 5가 적합하다.
만약 d < 0 이면 sigmaSpace에 의해 계산된다.
수학적인 이론은 위 필터 작동원리에 대한 이해가 필요할때 따로 찾아보도록 한다.
boxFilter와 bilateralFilter
import cv2
import numpy as np
src = cv2.imread('./data/lena.jpg', cv2.IMREAD_GRAYSCALE)
dst1= cv2.boxFilter(src, ddepth=-1, ksize=(11, 11))
dst2 = cv2.boxFilter(src, ddepth=-1, ksize=(21, 21))
dst3 = cv2.bilateralFilter(src, d=11, sigmaColor=10, sigmaSpace=10)
dst4 = cv2.bilateralFilter(src, d=-1, sigmaColor=10, sigmaSpace=10)
cv2.imshow('dst1', dst1)
cv2.imshow('dst2', dst2)
cv2.imshow('dst3', dst3)
cv2.imshow('dst4', dst4)
cv2.waitKey()
cv2.destroyAllWindows()
실습 코드에 대한 자세한 설명은 생략하고 각 필터의 파라미터들을 조정하였을때 결과를 통해 위 필터들을 사용하는 방법을 알아보고 넘어가도록 하자.
boxFilter의 출력 결과
bilateralFilter의 출력 결과
각 필터에 대한 설명과 실습 코드의 파라미터의 변화에 따른 각 필터의 출력 결과를 비교하며 추후에 위 두가지 필터를 사용할때 참고하도록 한다.
3)cv2.medianBlur(src, ksize[, dst]) -> dst
medianBlur() 함수는 src에서 정수 커널 크기 ksize에 의해 ksize 8 ksize의 필터를 사용하여 중앙값을 계산하여 dst에 저장한다.
src는 1, 3, 4 채널 영상이고, ksize = 3 또는 5일 때는 src의 깊이가 8비트, 16비트 부호 없는 정수, 32비트 실수가 가능하고, ksize가 크면 8비트만 가능하다.
dst는 src와 같은 크기이며 같은 자료형이다.
4)cv2.blur(src, ksize[, dst[, anchor[, borderType]]]) -> dst
blur() 함수는 튜플 커널 크기 ksize = (kw, kh)내부의 합계를 계산하고, 커널 크기로 정규화된 박스 필터이다.
src는 입력 영상으로 모든 채널이 가능하며, 깊이는 8비트 또는 16비트 부호 없는 정수, 16비트 정수, 32비트 또는 64비트 실수가 가능하다.
dst는 src와 같은 크기이며 같은 자료형이다.
5)cv2.GaussianBlur(src, kszie, sigmaX[, dst[, sigmaY[, borderType]]]) -> dst
튜플 커널 크기 ksize = (kw, kh)의 2차원 Gaussian 커널과 convolution을 수행한다.
sigmaX는 X축 방향으로의 가우시안 커널 표준편차, sigamY는 Y축 방향으로의 가우시안 커널 표준편차이다.
위 내용들은 어떠한 영상처리를 하기 위한 선행적인 절차이다.
필자가 올해 진행하려는 프로젝트도 위 과정이 선행되어야 하며 필자의 프로젝트내에선 GaussianBlur()함수가 위 역할을 수행해줄 예정이다.
따라서 GaussianBlur()함수의 구체적인 작동원리까지는 아니더라도 함수의 각 파라미터가 의미하는 바와 파라미터에 어떠한 값이 입력되었을때 원하는 출력을 얻을 수 있을지에 대한 고찰이 필요하다.
포스팅엔 간단하게 적어두었지만 따로 자료를 찾아볼 필요가 있다.
02 미분 필터
영상을 날카롭게 하는 샤프닝 연산은 미분 연산으로 이루어진다.
미분의 변화율을 측정한 미분 필터를 이용하여 영상을 선명하게 하거나, 에지를 검출 할 수 있다.
필자의 프로젝트는 카메라 센서에서 라인을 검출하는 것이 1차 목적이며 이를 달성하기 위해 해당 내용을 숙지할 필요가 있다.
아래의 표는 OpenCV의 미분 필터 연산 함수이다.
함수 |
---|
cv2.Sobel(src, ddepth, dx, dy[, dst[, ksize[, scale[, delta[, borderType]]]]]) -> dst |
cv2.Scharr(src, ddepth, dx, dy[, dst[, scale[, delta[, borderType]]]]) -> dst |
cv2.Laplacian(src, ddepth, dx, dy[, dst[, ksize[, scale[, delta[, borderType]]]]]) -> dst |
1)차분에 의한 미분 연산
1차 미분은 인접한 값의 차분으로 간단하게 계산할 수 있고, 2차 미분도 차분에 대한 식으로 나타낼 수 있다.
우리가 다루는 영상(gray scale가정)은 f(x, y)인 2차 함수일 것이다.
따라서 이 2차함수의 x축 편미분, y축 편미분을 요쇼로 갖는 2 * 1 그레디언트 벡터를 구할 수 있을 것이다.
이 벡터의 크기와 방향이 존재할텐데 이때 이 벡터의 크기가 가장 큰 값을 갖는 화소를 edge화소라고 하며 이러한 과정을 통해 edge를 검출해내는 메서드가 OpenCV에 구현이 되어 있는것으로 알고있다.
포스팅을 하면서 필자가 얻고자 하는 것은 구현해야하는 목표를 달성하기 위한 수단으로서 OpenCV에 구현되어 있는 다양한 메서드들을 효과적으로 사용할 수 있는 지식 습득이다.
본 포스팅의 목적은 모든 과정의 수학적 이론을 구체적으로 다루는 것이 아니므로, 각 과정의 자세한 수학적 이론 설명은 과감하게 생략하겠다.
2)1차 미분 필터
1차 미분 필터는 cv2.filter2D()
함수로 편미분을 직접 계산하거나 cv2.Sobel()
함수를 사용하여 간단하게 편미분을 계산할 수 있다.
3)2차 미분 필터
2차 미분 필터는 라플라시안 필터링으로 계산할 수 있는데, 라플라시안 필터링은 2차 미분을 사용하므로 noise에 민감하다.
따라서 잡음을 줄이기 위해 입력 영상에 가우시안 필터를 사용하여 영상을 부드럽게 하여 잡음을 제거하여 미분 오차를 줄이고, 라플라시안 필터링을 사용해야 한다.
앞서 설명했듯 블러 필터는 위 과정을 위해 선행되어야 하는 과정이었던 것이다.
2차 미분은 cv2.Laplacian()
함수로 계산되며 src에 대한 2차미분을 이용한 Laplacian을 적용한 후에 scale로 스케일링하고 delta값을 더해 ddepth 깊이의 dst에 저장한다.
###Sobel 필터 1
import cv2
import numpy as np
src = cv2.imread('./data/rect.jpg', cv2.IMREAD_GRAYSCALE)
#1
gx = cv2.Sobel(src, cv2.CV_32F, 1, 0, ksize = 3)
gy = cv2.Sobel(src, cv2.CV_32F, 0, 1, ksize = 3)
#2
dstX = cv2.sqrt(np.abs(gx))
dstX = cv2.normalize(dstX, None, 0, 255, cv2.NORM_MINMAX, dtype=cv2.CV_8U)
#3
dstY = cv2.sqrt(np.abs(gy))
dstY = cv2.normalize(dstY, None, 0, 255, cv2.NORM_MINMAX, dtype=cv2.CV_8U)
#4
mag = cv2.magnitude(gx, gy)
minVal, maxVal, minLoc, maxLoc = cv2.minMaxLoc(mag)
print('mag:', minVal, maxVal, minLoc, maxLoc)
dstM = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX, dtype=cv2.CV_8U)
cv2.imshow('src', src)
cv2.imshow('dstX', dstX)
cv2.imshow('dstY', dstY)
cv2.imshow('dstM', dstM)
cv2.waitKey()
cv2.destroyAllWindows()
#1 : 입력 영상 src의 그래디언트 gx, gy를 cv2.Sobel()함수로 계산한다.
#2 : gx 절대값의 제곱근을 계산하고, cv2.nomalize()로 0 ~ 255 범위로 정규화 한다.
#3 : gy 절대값의 제곱근을 계산하고, cv2.nomalize()로 0 ~ 255 범위로 정규화 한다.
#4 : cv2.magnitude(gx, gy)로 그래디언트의 크기를 계산하여 mag에 저장하고, 이 중에서 가장 큰 값이 에지가 된다.(0 ~ 255로 정규화 하였으므로 255(흰색)가 에지로 표현된다.)
cv2.normalize()
대신 cv2.threshold()
를 사용하면 에지와 배경을 갖는 이진 영상을 얻을 수 있다.
입력 영상의 x, y, 전체 에지를 표시한 출력 결과는 다음과 같다.
Sobel 필터 2 : 에지 그래디언트 방향
import cv2
import numpy as np
from matplotlib import pyplot as plt
src = cv2.imread('./data/rect.jpg', cv2.IMREAD_GRAYSCALE)
##src = cv2.imread('./data/line.png', cv2.IMREAD_GRAYSCALE)
cv2.imshow('src', src)
#1
gx = cv2.Sobel(src, cv2.CV_32F, 1, 0, ksize = 3)
gy = cv2.Sobel(src, cv2.CV_32F, 0, 1, ksize = 3)
mag, angle = cv2.cartToPolar(gx, gy, angleInDegrees=True)
minVal, maxVal, minLoc, maxLoc = cv2.minMaxLoc(angle)
print('angle:', minVal, maxVal, minLoc, maxLoc)
#2
ret, edge = cv2.threshold(mag, 100, 255, cv2.THRESH_BINARY)
edge = edge.astype(np.uint8)
cv2.imshow('edge', edge)
#3
width, height = mag.shape[:2]
angleM = np.full((width,height,3), (255, 255, 255), dtype= np.uint8)
for y in range(height):
for x in range(width):
if edge[y, x] != 0: # if mag[y, x] > 100: # edge
if angle[y, x] == 0:
angleM[y, x] = (0, 0, 255) # red
elif angle[y, x] == 90:
angleM[y, x] = (0, 255, 0) # green
elif angle[y, x] ==180:
angleM[y, x] = (255, 0, 0) # blue
elif angle[y, x] ==270:
angleM[y, x] = (0, 255, 255) # yellow
else:
angleM[y, x] = (128, 128, 128) # gray
cv2.imshow('angleM', angleM)
##cv2.waitKey()
##cv2.destroyAllWindows()
#4
hist = cv2.calcHist(images=[angle], channels=[0], mask=edge,
histSize=[360], ranges=[0, 360])
hist = hist.flatten()
##plt.title('hist: binX = np.arange(360)')
plt.plot(hist, color='r')
binX = np.arange(360)
plt.bar(binX, hist, width=1, color='b')
plt.show()
#1 : cv2.Sobel()
함수를 이용하여 입력 영상 src의 그래디언트 gx, gy를 계산한 후 cv2.cartToPolar()
함수로 그래디언트 크기(mag)와 각도(angle)을 계산한다.
#2 : cv2.threshold()
함수로 mag에서 임계값 100을 사용하여 이진 영상 edge를 계산한다. 이때 화면표시를 위해 화소 자료형을 np.uint8로 변경해줘야 한다.
#3 : angleM을 배경이 흰색(255, 255, 255)인 3 - 채널 컬러 영상으로 생성한 후 그래디언트의 각도에 따라 일정색을 표시되도록 한다.
#4 : 히스토그램을 생성하는 코드인데 설명을 생략한다.
결과적으로 그래디언트의 각도는 0, 90, 180, 270가 대부분이며 이것이 의미하는 바에 대해 생각해볼 필요가 있다.
Laplacian 필터 1
import cv2
import numpy as np
#1
src = cv2.imread('./data/lena.jpg', cv2.IMREAD_GRAYSCALE)
blur= cv2.GaussianBlur(src, ksize=(7, 7), sigmaX=0.0)
cv2.imshow('src', src)
cv2.imshow('blur', blur)
#2
lap = cv2.Laplacian(src, cv2.CV_32F)
minVal, maxVal, minLoc, maxLoc = cv2.minMaxLoc(lap)
print('lap:', minVal, maxVal, minLoc, maxLoc)
dst = cv2.convertScaleAbs(lap)
dst = cv2.normalize(dst, None, 0, 255, cv2.NORM_MINMAX)
cv2.imshow('lap', lap)
cv2.imshow('dst', dst)
#3
lap2 = cv2.Laplacian(blur, cv2.CV_32F)
minVal, maxVal, minLoc, maxLoc = cv2.minMaxLoc(lap2)
print('lap2:', minVal, maxVal, minLoc, maxLoc)
dst2 = cv2.convertScaleAbs(lap2)
dst2 = cv2.normalize(dst2, None, 0, 255, cv2.NORM_MINMAX)
cv2.imshow('lap2', lap2)
cv2.imshow('dst2', dst2)
cv2.waitKey()
cv2.destroyAllWindows()
위 과정에서 핵심적인 것은 라플라시안 필터를 적용하기 이전에 미분 오차를 줄이기 위해 블러처리를 한다는 것이며 이를 숙지하고 넘어가도록 한다.
각 필터가 작동하는 원리도 물론 중요하지만 목적과 문법을 이해하는것이 더 중요하다고 판단하여 구체적인 설명은 생략한다.
04 템플릿 매칭
템플릿 매칭은 참조 영상에서 템플릿 영상과의 매칭 위치를 탐색하는 방법이다.
템플릿 매칭은 이동문제는 해결할 수 있지만, 회전 및 스케일링된 물체에 대한 매칭은 템플릿을 회전 및 스케일링 시켜가며 여러 개의 템플릿을 이용할 수 있긴 하지만 어렵다.
템플릿 매칭에서 영상의 밝기를 그대로 사용할 수도 있고 에지, 코너점, 주파수 변환 등의 특징 공간으로 변환하여 템플릿 매칭을 수행할 수 있으며, 영상의 밝기 등에 덜 민감하도록 정규화 과정이 필요하다.
매칭 방법은 상관관계(correalation), SAD(Sum of absolute differences)등을 사용한다.
위 그림에서처럼 템플릿을 영상(I)에서 이동시키며, 매칭 방법에 따라 계산하여 결과를 result(R)에 저장하고, 매칭 결과를 탐색하여 템플릿의 위치를 찾는다.
상관관계는 최대값, SAD는 최소값에서 템플릿 매칭 위치를 찾는다.
cv2.matchTemplate(), 템플릿 매칭
import cv2
import numpy as np
src = cv2.imread('./data/alphabet.bmp', cv2.IMREAD_GRAYSCALE)
tmp_A = cv2.imread('./data/A.bmp', cv2.IMREAD_GRAYSCALE)
tmp_S = cv2.imread('./data/S.bmp', cv2.IMREAD_GRAYSCALE)
tmp_b = cv2.imread('./data/b.bmp', cv2.IMREAD_GRAYSCALE)
dst = cv2.cvtColor(src, cv2.COLOR_GRAY2BGR) # 출력 표시 영상
#1
R1 = cv2.matchTemplate(src, tmp_A, cv2.TM_SQDIFF_NORMED)
minVal, _, minLoc, _ = cv2.minMaxLoc(R1)
print('TM_SQDIFF_NORMED:', minVal, minLoc)
w, h = tmp_A.shape[:2]
cv2.rectangle(dst, minLoc, (minLoc[0]+h, minLoc[1]+w), (255, 0, 0), 2)
#2
R2 = cv2.matchTemplate(src, tmp_S, cv2.TM_CCORR_NORMED)
_, maxVal, _, maxLoc = cv2.minMaxLoc(R2)
print('TM_CCORR_NORMED:', maxVal, maxLoc)
w, h = tmp_S.shape[:2]
cv2.rectangle(dst, maxLoc, (maxLoc[0]+h, maxLoc[1]+w), (0, 255, 0), 2)
#3
R3 = cv2.matchTemplate(src, tmp_b, cv2.TM_CCOEFF_NORMED)
_, maxVal, _, maxLoc = cv2.minMaxLoc(R3)
print('TM_CCOEFF_NORMED:', maxVal, maxLoc)
w, h = tmp_b.shape[:2]
cv2.rectangle(dst, maxLoc, (maxLoc[0]+h, maxLoc[1]+w), (0, 0, 255), 2)
cv2.imshow('dst', dst)
cv2.waitKey()
cv2.destroyAllWindows()
참조 영상으로 사용한 src는 알파벳이 나열되어 있는 사진이고, 템플릿으로 사용한 세개의 템플릿은 마우스로 각 글자의 영역을 선택하여 미리 저장해둔 것이다.
미리 만들어 놓은 템플릿을 3가지 메서드를 사용해서 각각 매칭시켜보았고, 이를 통해 영상과 템플릿을 매칭시킬 수 있음을 확인할 수 있었다.
하지만 영상에서 글자의 크기가 다르거나 회전이 있으면 잘못된 매칭 결과를 반환한다.
댓글남기기