--- 본 포스팅은 데이콘 서포터즈 "데이크루 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/4898?utm_source=dacrew&utm_medium=432727&utm_campaign=dacrew_2
지금부터 5주차 활동 시작하겠습니다 ( •̀ ω •́ ) ✧✧
6. 영상 필터¶
필터는 우리말로 거름망을 의미합니다. 그러나 영상 처리에서는 입력 값에서 원하지 않는 값은 걸러내고 원하는 결과만을 얻는 의미로 사용합니다. 필터는 영상을 흐릿하게 만들거나 또렷하게 만들어 영상의 품질을 높이기도 하지만, 엣지(경계)를 검출하고 엣지의 방향을 알아내는 등 객체 인식과 분리의 기본이 되는 정보를 계산하기도 합니다.
영상 처리는 새로운 영상을 얻기 위해 기존 픽셀 값에 어떠한 연산을 가해서 새로운 픽셀 값을 얻는 작업입니다. 새로운 픽셀 값을 얻는데 있어 기존 픽셀 값 하나가 아닌 주변 픽셀들의 값을 활용하는 방법을 공간 영역 필터라고 하고, 픽셀 값들의 차이를 주파수로 변환해서 활용하는 방법을 주파수 영역 필터라고 합니다.
6.1. 컨볼루션과 블러링¶
공간 영역 필터의 핵심은 컨볼루션(convolution) 연산입니다. 블러링(blurring)을 사례로 컨볼루션의 동작 방법을 알아보겠습니다.
6.1.1. 필터와 컨볼루션¶
공간 영역 필터는 연산 대상 픽셀과 그 주변 픽셀들을 활용하는데, 이 때 주변 픽셀들 중 어디까지를 포함할 것인지와 결과 값을 어떻게 산출할지를 커널을 통해 결정합니다. nxn 크기의 커널(kernel)은 윈도(window), 필터(filter), 마스크(mask)라고도 부르고, 커널의 각 요소와 대응하는 입력 픽셀 값을 곱해서 모두 합한 것을 결과 픽셀 값으로 결정하고 이를 마지막 픽셀까지 반복하는 것을 커볼루션 연산이라고 합니다.
컨볼루션 연산은 하나의 픽셀 값이 결정되면 칸을 옮겨가며 연산을 반복하고 커널에 지정한 값의 비중에 따라 주변 요소들의 값을 반영합니다. 이 때 커널의 크기와 값을 어떻게 하느냐에 따라 결과 영상에 필터를 적용한 효과가 달라집니다. ( ex. 주변 요소 값들의 평균 값을 반영할 때 전체적인 영상은 흐릿해지고 주변 요소들과의 차이를 반영하면 또렷해집니다 )
- dst = cv2.filter2D(src, ddepth, kernel[, dst, anchor, delta, borderType])
- src : 입력 영상
- ddepth : 출력 영상의 dtype
- kernel : 컨볼루션 커널
- dst : 결과 영상
- anchor : 커널의 기준점
- delta : 필터 적용된 결과에 추가할 값
- borderType : 외곽 픽셀 보정 방법 지정
6.1.2. 평균 블러링¶
영상을 초점이 맞지 않은 것 처럼 즉 흐릿하게 만드는 방법을 블러링(blurring) 또는 스무딩(smoothing)이라고 합니다. 블러링을 적용하는 가장 쉬운 방법은 주변 픽셀 값들의 평균을 적용하는 것입니다. 평균 값을 적용한다는 것은 다른 픽셀과 비슷한 값을 갖게 하는 것이기 때문에 전체적인 영상 픽셀의 차이가 적어져서 이미지가 흐릿해집니다.
import cv2
import numpy as np
import matplotlib.pyplot as plt
img = cv2.imread('../img/girl.jpg')
'''
#5x5 평균 필터 커널 생성 ---①
kernel = np.array([[0.04, 0.04, 0.04, 0.04, 0.04],
[0.04, 0.04, 0.04, 0.04, 0.04],
[0.04, 0.04, 0.04, 0.04, 0.04],
[0.04, 0.04, 0.04, 0.04, 0.04],
[0.04, 0.04, 0.04, 0.04, 0.04]])
'''
# 5x5 평균 필터 커널 생성 ---②
kernel = np.ones((5,5))/5**2
# 필터 적용 ---③
blured = cv2.filter2D(img, -1, kernel)
plt.figure(figsize = (20,6))
imgs = {'origin':img, 'avrg blur':blured}
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()
커널의 크기만 지정하면 알아서 평균 커널을 생성하여 블러링을 적용한 영상을 만들어 내는 함수
- dst = cv2.blur(src, ksize[, dst, anchor, borderType])
- src : 입력 영상
- ksize : 커널의 크기
normalize 인자에 True를 지정하면 blur 함수와 같고 만약 False를 지정할 경우 커널 영역의 모든 픽셀의 합을 구합니다. ( 밀도를 이용한 객체 추적 알고리즘에서 사용 )
- dst = cv2.boxFilter(src, ddepth, ksize[, dst, anchor, normalize, borderType])
- src : 입력 영상
- ddepth : 출력 영상의 dtype
- normalize : 커널 크기로 정규화 지정 여부
import cv2
import numpy as np
file_name = '../img/girl.jpg'
img = cv2.imread(file_name)
# blur() 함수로 블러링 ---①
blur1 = cv2.blur(img, (10,10))
# boxFilter() 함수로 블러링 적용 ---②
blur2 = cv2.boxFilter(img, -1, (10,10))
# 결과 출력
plt.figure(figsize = (10,6))
imgs = {'origin':img, 'blur1':blur1, 'blur2':blur2}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,3,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
6.1.3. 가우시안 블러링¶
평균이 아닌 가우시안 분포를 갖는 커널로 블러링을 하는 것을 가우시안 블러링이라고 합니다. 가우시안 커널은 중앙 값이 가장 크고 멀어질수록 그 값이 작아지는 커널을 사용하는 것을 말합니다.
이 방법은 새로운 픽셀 값을 선정할 때 해당 픽셀에 가까울수록 많은 영향을 주고, 멀어질수록 적은 영향을 주기 때문에 원래의 영상과 비슷하면서도 노이즈를 제거하는 효과가 있습니다.
- cv2.GaussianBlur(src, ksize, sigmaX[, sigmaY, borderType])
- src : 입력 영상
- ksize : 커널 크기
- sigmaX : X 방향 표준편차
- sigmaY : Y 방향 표준편차
- borderType : 외곽 테두리 보정 방식
- ret : cv2.getGaussianKernel(ksize, sigma[, ktype])
- ret : 가우시안 커널
GaussianBlur 함수는 ksize에 커널 크기와 sigmaX, sigmaY에 표준편차 값을 전달하면 가우시안 필터를 적용한 블러링을 적용합니다. 이 때 sigmaX에 0을 입력하면 자동으로 표준편차를 선택해서 사용하고 sigmaY 값을 생략하면 sigmaX 값과 동일한 값을 사용합니다.
getGaussiankernel 함수는 커널 크기와 표준편차 값을 전달하면 이에 맞는 가우시안 블러링 커널을 만들어서 반환합니다. 이 때 반환되는 값은 1차원 배열이기 때문에 cv2.filter2D 함수에 사용하려면 ret * ret.T와 같은 꼴로 사용해야합니다.
import cv2
import numpy as np
img = cv2.imread('../img/rat.jpg')
# 가우시안 커널을 직접 생성해서 블러링 ---①
k1 = np.array([[1, 2, 1],
[2, 4, 2],
[1, 2, 1]]) *(1/16)
blur1 = cv2.filter2D(img, -1, k1)
# 가우시안 커널을 API로 얻어서 블러링 ---②
k2 = cv2.getGaussianKernel(3, 0)
blur2 = cv2.filter2D(img, -1, k2*k2.T)
# 가우시안 블러 API로 블러링 ---③
blur3 = cv2.GaussianBlur(img, (3, 3), 0) # 3x3 크기의 커널, 표준편차는 0
# 결과 출력
print('k1:', k1)
print('k2:', k2*k2.T)
plt.figure(figsize = (20,6))
imgs = {'origin':img, 'blur1':blur1, 'blur2':blur2, 'blur3':blur3}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,4,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
k1: [[0.0625 0.125 0.0625] [0.125 0.25 0.125 ] [0.0625 0.125 0.0625]] k2: [[0.0625 0.125 0.0625] [0.125 0.25 0.125 ] [0.0625 0.125 0.0625]]
6.1.4. 미디언 블러링¶
커널 영역 픽셀 값 중 중간 값을 대상 픽셀의 값으로 선택하는 것을 미디언(median) 블러링이라고 합니다. 이전의 블러링 기법에서는 원본 픽셀 값에 대해 새로운 값이 생성되지만 이 필터는 기존 픽셀 값 중에 하나를 선택하기 때문에 기존 값을 재활용한다는 특징이 있습니다. 이 필터는 소금-후추(소금과 후추를 뿌린듯한) 잡음 제거에 효과적입니다.
- dst = cv2.medianBlur(src, ksize)
- src : 입력 영상
- ksize : 커널 크기
import cv2
import numpy as np
img = cv2.imread("../img/dalgona.jpg")
# 미디언 블러 적용 --- ①
blur = cv2.medianBlur(img, 5)
# 결과 출력
plt.figure(figsize = (10,6))
imgs = {'origin':img, 'blur':blur}
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()
소금-후추 잡음이 많이 제거된 것을 확인할 수 있습니다.
6.1.5. 바이레터럴 필터¶
블러링 필터는 대체로 잡음을 제거하는 데 효과가 있지만, 경계를 흐릿하게 만든다는 문제를 가지고 있습니다. 바이레터럴(bilateral) 필터는 이러한 문제를 개선하기 위해 가우시안 필터와 경계 필터 2개를 사용하는데, 그 결과 노이즈는 없고 경계가 비교적 또렷한 영상을 얻을 수 있지만 속도가 느리다는 단점을 가집니다.
- dst = cv2. bilateralFilter(src, d, sigmaColor, sigmaSpace[, dst, borderType])
- src : 입력 영상
- d : 필터의 직경, 5보다 크면 매우 느림
- sigmaColor : 색공간 필터의 시그마 값
- sigmaSpace : 좌표 공간의 시그마 값 ( 단순한 사용을 위해 sigmaColor와 sigmaSpace에 같은 값을 사용할 것을 권장하며, 범위는 10 ~ 150을 권장함)( 150 이상의 값을 지정하면 스케치 효과를 얻을 수 있습니다. )
import cv2
import numpy as np
img = cv2.imread("../img/dalgona.jpg")
# 가우시안 필터 적용 ---①
blur1 = cv2.GaussianBlur(img, (5,5), 0)
# 바이레터럴 필터 적용 ---②
blur2 = cv2.bilateralFilter(img, 5, 75, 75)
blur3 = cv2.bilateralFilter(img, 5, 75, 200) # 스케치 효과
# 결과 출력
plt.figure(figsize = (20,6))
imgs = {'origin':img, 'blur1':blur1, 'blur2':blur2, 'blur3':blur3}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,4,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
6.2. 경계 검출¶
영상에서 경계(edge)를 검출하는 것은 배경과 전경을 분리하는 데 가장 기본적인 작업입니다. 이전까지는 영상을 흐릿하게 만드는 필터들을 알아보았는데 이제는 영상의 경계를 선명하게 만드는 샤프닝(sharping)에 대해 알아보도록 하겠습니다. 샤프닝은 경계를 검출해서 경계에 있는 픽셀만을 골라서 강조하는 것을 의미합니다.
6.2.1. 기본 미분 필터¶
경계를 검출하려면 픽셀 값의 변화가 갑자기 크게 일어나는 지점을 찾아야 합니다. 이는 연속된 픽셀 값에 미분 연산을 적용하여 알 수 있습니다. 영상 속의 픽셀 데이터는 현실과 같은 연속된 공간이 아니므로 이산화하여 근사 값으로 간소화해야 합니다. 간소화한 미분 공식은 다음과 같습니다. ( x축, y축 각각의 방향에서 다음 픽셀의 값에서 현재 픽셀의 값을 빼라는 의미 )
$$Gx = \frac{\partial f(x, y)}{\partial x} \approx f_{x+1, y} - f_{x, y}$$$$Gy = \frac{\partial f(x, y)}{\partial y} \approx f_{x,y+1} - f_{x,y}$$이를 컨볼루션 커널로 만들면 다음과 같습니다. $$Gx = \begin{bmatrix} -1 & 1 \\ \end{bmatrix} $$ $$Gy = \begin{bmatrix} -1 \\ 1 \end{bmatrix}$$
import cv2
import numpy as np
img = cv2.imread("../img/dalgona.jpg")
#미분 커널 생성 ---①
gx_kernel = np.array([[ -1, 1]])
gy_kernel = np.array([[ -1],[ 1]])
# 필터 적용 ---②
edge_gx = cv2.filter2D(img, -1, gx_kernel) # x방향 미분 마스크로 컨볼루션 수행
edge_gy = cv2.filter2D(img, -1, gy_kernel) # y방향 미분 마스크로 컨볼루션 수행
# 결과 출력
plt.figure(figsize = (10,6))
imgs = {'origin':img, 'edge_gx':edge_gx, 'edge_gy':edge_gy}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,3,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
경계를 어느정도 인식하지만 모든 경계를 완벽히 인식하지는 못하는 모습입니다.
위의 결과를 살펴보면 x방향 미분 마스크는 세로 방향 경계를 검출하고 y방향 미분 마스크는 가로 방향 미분 마스크를 검출하는 것을 확인할 수 있습니다. 미분으로 얻은 엣지 정보는 각각 x축과 y축에 대한 값의 변화를 나타내고 이를 기울기(Gradient)라고 합니다. $G_x$, $G_y$ 두 값을 이용하면 엣지의 강도와 방향을 알 수 있는 데 그 방법은 아래의 식과 같습니다.
$$ magnitude = \sqrt{G_x^2+G_y^2} $$$$ direction = arctan(\frac{G_y}{G_x})$$이 때 기울기의 방향과 엣지의 방향은 같은 방향을 가리키는 것이 아니라 서로 수직이라는 점을 주의해야 합니다. 이 값들은 영상의 특징을 묘사하는 중요한 단서가 되고 이를 이용해 영상끼리 얼마나 비슷한지도 알아낼 수 있습니다.
6.2.2. 로버츠 교차 필터¶
1963년 로렌스 로버츠가 제안한 커널을 활용합니다.
$$ Gx = \begin{bmatrix} 1 & 0 \\ 0 & -1 \\ \end{bmatrix} $$$$ Gy = \begin{bmatrix} 1 & 0 \\ 0 & -1 \\ \end{bmatrix} $$위 커널은 대각선 방향으로 1과 -1을 배치하여 사선 경계 검출 효과를 높였지만 노이즈에 민감하고 엣지 강도가 약하다는 단점이 있습니다.
import cv2
import numpy as np
img = cv2.imread("../img/dalgona.jpg")
# 로버츠 커널 생성 ---①
gx_kernel = np.array([[1,0], [0,-1]])
gy_kernel = np.array([[0, 1],[-1,0]])
# 커널 적용 ---②
edge_gx = cv2.filter2D(img, -1, gx_kernel)
edge_gy = cv2.filter2D(img, -1, gy_kernel)
# 결과 출력
plt.figure(figsize = (10,6))
imgs = {'origin':img, 'edge_gx':edge_gx, 'edge_gy':edge_gy, 'edge_gx+edge_gy':edge_gx+edge_gy}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,4,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
기본 미분 필터보다 노이즈에 민감하게 반응하는 결과를 확인할 수 있습니다.
6.2.3. 프리윗 필터¶
주디스 프리윗이 개발한 프리윗 마스크를 활용하는 방법으로 각 방향으로 차분을 3번 계산하도록 배치하여 엣지 강도가 강하고 수직과 수평 엣지를 동등하게 찾는 장점이 있지만 대각선을 검출하는 정도가 약합니다.
$$ Gx = \begin{bmatrix} -1 & 0 & 1 \\ -1 & 0 & 1 \\ -1 & 0 & 1 \\ \end{bmatrix} $$ $$ Gy = \begin{bmatrix} -1 & -1 & -1 \\ 0 & 0 & 0 \\ 1 & 1 & 1 \\ \end{bmatrix} $$import cv2
import numpy as np
file_name = "../img/dalgona.jpg"
img = cv2.imread(file_name)
# 프리윗 커널 생성
gx_k = np.array([[-1,0,1], [-1,0,1],[-1,0,1]])
gy_k = np.array([[-1,-1,-1],[0,0,0], [1,1,1]])
# 프리윗 커널 필터 적용
edge_gx = cv2.filter2D(img, -1, gx_k)
edge_gy = cv2.filter2D(img, -1, gy_k)
# 결과 출력
plt.figure(figsize = (10,6))
imgs = {'origin':img, 'edge_gx':edge_gx, 'edge_gy':edge_gy, 'edge_gx+edge_gy':edge_gx+edge_gy}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,4,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
gx의 경우 오른쪽, gy의 경우 밑을 반대편보다 더 잘 인식하는 모습입니다.
6.2.4. 소벨 필터¶
어윈 소벨은 중심 픽셀의 차분 비중을 2배로 주어 수평, 수직, 대각선 경계 검출에 모든 마스크를 제안했습니다.
$$ Gx = \begin{bmatrix} -1 & 0 & 1 \\ -2 & 0 & 2 \\ -1 & 0 & 1 \\ \end{bmatrix} $$ $$ Gy = \begin{bmatrix} -1 & -2 & -1 \\ 0 & 0 & 0 \\ 1 & 2 & 1 \\ \end{bmatrix} $$소벨 마스크는 가장 대표적인 1차 미분 마스크로 OpenCV는 이를 위한 전용 함수를 제공합니다. (위에서 소개한 필터들은 실무에서 거의 사용하지 않습니다. )
- dst = cv2.Sobel(src, ddepth, dx, dy[, dst, ksize, scale, delta, borderType])
- src : 입력 영상
- ddepth : 출력 영상의 dtype
- dx, dy : 미분 차수 ( 0,1,2 )
- ksize : 커널의 크기 ( 1,3,5,7 )
- scale : 미분에 사용할 계수
- delta : 연산 결과에 가산할 값
import cv2
import numpy as np
img = cv2.imread("../img/dalgona.jpg")
# 소벨 커널을 직접 생성해서 엣지 검출 ---①
## 소벨 커널 생성
gx_k = np.array([[-1,0,1], [-2,0,2],[-1,0,1]])
gy_k = np.array([[-1,-2,-1],[0,0,0], [1,2,1]])
## 소벨 필터 적용
edge_gx = cv2.filter2D(img, -1, gx_k)
edge_gy = cv2.filter2D(img, -1, gy_k)
# 소벨 API를 생성해서 엣지 검출
sobelx = cv2.Sobel(img, -1, 1, 0, ksize=3)
sobely = cv2.Sobel(img, -1, 0, 1, ksize=3)
# 결과 출력
plt.figure(figsize = (10,6))
imgs = {'origin':img, 'edge_gx':edge_gx, 'edge_gy':edge_gy, 'edge_gx+edge_gy':edge_gx+edge_gy}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,4,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
6.2.5. 샤르 필터¶
소벨 필터는 커널의 크기가 작은 경우 또는 커널의 크기가 크더라도 중심에서 멀어질수록 엣지 방향성의 정확성이 떨어진다는 단점이 있습니다. 샤르 필터는 이를 개선한 필터입니다.
$$ Gx = \begin{bmatrix} -3 & 0 & 3 \\ -10 & 0 & 10 \\ -3 & 0 & 3 \\ \end{bmatrix} $$ $$ Gy = \begin{bmatrix} -3 & -10 & -3 \\ 0 & 0 & 0 \\ 3 & 10 & 3 \\ \end{bmatrix} $$- dst = cv2.Scharr(src, ddepth, dx, dy[, dst, scale, delta, borderType])
import cv2
import numpy as np
img = cv2.imread("../img/dalgona.jpg")
# 샤르 커널을 직접 생성해서 엣지 검출 ---①
gx_k = np.array([[-3,0,3], [-10,0,10],[-3,0,3]])
gy_k = np.array([[-3,-10,-3],[0,0,0], [3,10,3]])
edge_gx = cv2.filter2D(img, -1, gx_k)
edge_gy = cv2.filter2D(img, -1, gy_k)
# 샤르 API로 엣지 검출 ---②
scharrx = cv2.Scharr(img, -1, 1, 0)
scharry = cv2.Scharr(img, -1, 0, 1)
# 결과 출력
plt.figure(figsize = (20,6))
imgs = {'origin':img, 'edge_gx':edge_gx, 'edge_gy':edge_gy, 'scharrx':scharrx, 'scharry':scharry}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,5,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
위 예시에서는 경계를 두 줄로 인식하는 경향도 엿보입니다. 또한 gx는 오른쪽, gy는 밑부분만 잘 인식하는 것을 확인할 수 있습니다.
6.2.6. 라플라시안 필터¶
라플라시안 필터는 대표적인 2차 미분 마스크로 다음의 커널을 가집니다. 라플라시안 필터는 노이즈에 민감하기 때문에 사전에 가우시안 필터를 활용해 노이즈를 제거하고 사용하는 것이 좋습니다.
$$ kernel = \begin{bmatrix} 0 & 1 & 0 \\ 1 & -4 & 1 \\ 0 & 1 & 0 \\ \end{bmatrix} $$- dst = cv2.Laplacian(src, ddepth, dx, dy[, dst, ksize, scale, delta, borderType])
import cv2
import numpy as np
img = cv2.imread("../img/dalgona.jpg")
# 라플라시안 필터 적용 ---①
edge = cv2.Laplacian(img, -1)
# 결과 출력
plt.figure(figsize = (10,6))
imgs = {'origin':img, 'edge':edge}
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()
이전까지의 결과보다 뚜렷하게 이미지를 인식하는 것을 확인할 수 있습니다.
6.2.7. 캐니 엣지¶
존 캐니가 1986년 제안한 캐니 엣지 알고리즘은 한 가지 필터를 사용하는 것이 아니라 4단계의 알고리즘을 적용한 잡음에 강한 엣지 검출기 입니다. 캐니 엣지 4단계 알고리즘은 다음과 같습니다.
- 노이즈 제거(Noise Reduction) : 5 x 5 가우시안 블러링 필터로 노이즈를 제거합니다.
- 엣지 그레디언트 방향 계산 : 소벨 마스크로 엣지 및 그레디언트 방향을 검출합니다.
- 비최대치 억제(Non-Maximum Suppression) : 그레디언트 방향에서 검출된 엣지 중에 가장 큰 값만 선택하고 나머지는 제거합니다.
- 이력 스레시홀딩(Hysteresis Thresholding) : 두 개의 경계 값(Max, Min)을 지정해서 경계 영역에 있는 픽셀들 중 큰 경계 값(Max) 밖의 픽셀과 연결성이 없는 픽셀을 제거합니다.
- edges : cv2.Canny(img, threshold1, threshold2, [, edges, apertureSize, L2gardient])
- img : 입력 영상
- threshold1, threshold2 : 이력 스레시홀딩에 사용할 최소, 최대 값
- apertureSize : 소벨 마스크에 사용할 커널 크기
- L2gradient : 그레디언트 강도를 구할 방식 지정 플래그
- True : $ \sqrt{G_x^2+G_y^2} $
- False : $\left|G_x \right| + \left|G_y \right|$
- edges : 엣지 결과 값을 갖는 2차원 배열
이 함수는 경계 검출 효과가 뛰어나고 스레시홀드 값의 지정에 따라 경계 검출 대상을 조정할 수 있어서 가장 많이 사용됩니다.
import cv2, time
import numpy as np
import matplotlib.pyplot as plt
img = cv2.imread("../img/dalgona.jpg")
# 케니 엣지 적용
edges = cv2.Canny(img,100,200)
edges = cv2.cvtColor(edges, cv2.COLOR_GRAY2BGR)
# 결과 출력
plt.figure(figsize = (10,6))
imgs = {'origin':img, 'Canny':edges}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,2,i+1)
plt.title(k)
plt.imshow(v) # rgb를 거꾸로 했을 경우 오류 발생
plt.xticks([]),plt.yticks([])
plt.show()
위에서 사용한 파일 예시로는 캐니엣지 방법이 가장 뚜렷한 결과를 보이는 것을 확인할 수 있습니다.
6.3. 모폴로지¶
모폴로지(morphology)는 형태학이라는 뜻으로 영상 분야에서는 노이즈 제거, 구멍 메꾸기, 연결되지 않은 경계 이어붙이기 등 형태학적인 관점에서의 영상 연산을 의미합니다. 모폴로지는 주로 형태를 다루는 연산이기 때문에 바이너리 이미지를 대상으로 합니다. 대표적인 연산은 침식과 팽창이며 이 둘을 결합한 열림과 닫힌 연산이 존재합니다.
6.3.1. 침식 연산¶
침식(erosion)은 원래 있던 객체의 영역을 깎아내는 연산입니다. 이 연산을 위해서는 구조화 요소(structuring element)라는 0과 1로 채워진 커널이 필요하고, 이 때 1이 채워진 모양에 따라 사각형, 타원형, 십자형 등을 사용할 수 있습니다. 구조화 요소 커널을 입력 영상에 적용할 때 1로 채워진 영역을 온전히 올려 놓을 수 없으면 해당 픽셀을 0으로 변경합니다.
cv2.getStructuringElement(shape, ksize, [, anchor])
- shape : 구조화 요소 커널의 모양 결정
- cv2.MORPH_RECT : 사각형
- cv2.MORPH_ELLIPSE : 타원형
- cv2.MORPH_CROSS : 십자형
- ksize : 커널 크기
- anchor : 구조화 요소의 기준점
- shape : 구조화 요소 커널의 모양 결정
dst = cv2.erode(src, kernel [, anchor, iterations, borderType, borderValue])
- src : 입력 영상
- kernel : 구조화 요소 커널 객체
- iterations : 침식 연산 적용 반복 횟수
- borderType : 외곽 영역 보정 방법 설정 플래그
- borderValue : 외곽 영역 보정 값
침식 연산은 큰 물체의 경우 주변을 깎아서 작게 만들지만 작은 객체는 아예 사라지기 만들 수 있기 때문에 아주 작은 노이즈를 제거하거나, 따로 떨어진 물체가 겹쳐져서 하나의 물체로 보일 때 서로를 떼어내는 데에도 효과적으로 사용할 수 있습니다.
import cv2
import numpy as np
img = cv2.imread('../img/heart.png')
# 구조화 요소 커널, 사각형 (3x3) 생성 ---①
k = cv2.getStructuringElement(cv2.MORPH_RECT, (3,3))
# 침식 연산 적용 ---②
erosion = cv2.erode(img, k)
# 결과 출력
plt.figure(figsize = (10,6))
imgs = {'origin':img, 'Erode':erosion}
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()
선이 전반적으로 얇아졌지만 작은 흰점들의 노이즈가 사라진 것을 확인할 수 있습니다
6.3.2. 팽창 연산¶
팽창(dilatation)은 침식과는 반대로 영상 속 사물의 주변을 덧붙여서 영역을 더 확장하는 연산입니다. 침식 연산과 마찬가지로 구조화 요소 커널을 입력 영상에 적용해서 1로 채워진 영역이 온전히 덮이지 않으면 1로 채워 넣습니다.
- dst = cv2.dilate(src, kernel [, anchor, iterations, borderType, borderValue])
import cv2
import numpy as np
img = cv2.imread('../img/heart.png')
# 구조화 요소 커널, 사각형 (3x3) 생성 ---①
k = cv2.getStructuringElement(cv2.MORPH_RECT, (3,3))
# 팽창 연산 적용 ---②
dst = cv2.dilate(img, k)
# 결과 출력
plt.figure(figsize = (10,6))
imgs = {'origin':img, 'Dilation':dst}
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()
6.3.3. 열림과 닫힘, 그밖의 모폴로지 연산¶
침식과 팽창 연산은 밝은 부분이나 어두운 부분의 점 노이즈를 없애는 데 효과적이지만, 원래 모양이 얇아지거나 두꺼워지는 등의 변형이 일어납니다. 이 때 침식과 팽창 연산을 조합한다면 원래의 모양을 유지하면서 노이즈만 제거할 수 있습니다.
침식 연산을 적용하고나서 팽창 연산을 적용하는 것을 열림(opening) 연산이라고 합니다. 열림 연산은 주변보다 밝은 노이즈 제거에 효과적이며 맞닿아 있는 것으로 보이는 독립된 개체를 분리하거나 돌출된 픽셀을 제거하는 데 좋습니다.
반대로 팽창 연산을 먼저 적용하고 침식 연산을 나중에 적용하는 연산을 닫힘(closing) 연산이라고 하고 주변보다 어두운 노이즈 제거에 효과적이면서 끊어져 보이는 개체를 연결하거나 구멍을 메우는 데 좋습니다.
팽창한 결과 - 침식한 결과를 하게되면 경계만 얻게 되는데 경계 검출과 비슷한 결과를 얻을 수 있어 이를 그레디언트(gradient) 연산이라고 합니다. 또한 원본에서 열림 연산 결과를 빼면 밝기 값이 크게 튀는 영역을 강조할 수 있고 닫힘 연산 결과에서 원본을 빼면 어두운 부분을 강조할 수 있습니다. 이를 각각 탑햇(top hat)과 블랙햇(black hat) 연산이라고 합니다.
- dst = cv2.morphologyEx(src, op, kernel [, dst, anchor, iteration, borderType, borderValue])
- op: 모폴로지 연산 종류 지정
- cv2.MORPH_OPEN : 열림 연산
- cv2.MORPH_CLOSE : 닫힘 연산
- cv2.MORPH_GRADIENT : 그레디언트 연산
- cv2.MORPH_TOPHAT : 탑햇 연산
- cv2.MORPH_BLACKHAT : 블랙햇 연산
- op: 모폴로지 연산 종류 지정
import cv2
import numpy as np
img1 = cv2.imread('../img/heart.png')
img2 = cv2.imread('../img/heart2.png')
# 구조화 요소 커널, 사각형 (5x5) 생성 ---①
k = cv2.getStructuringElement(cv2.MORPH_RECT, (5,5))
# 열림 연산 적용 ---②
opening = cv2.morphologyEx(img1, cv2.MORPH_OPEN, k)
# 닫힘 연산 적용 ---③
closing = cv2.morphologyEx(img2, cv2.MORPH_CLOSE, k)
# 결과 출력
plt.figure(figsize = (10,6))
imgs = {'origin1':img1, 'opening':opening, 'origin2':img2, 'closing':closing}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(2,2,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
# 모폴로지 그레디언트 연산으로 경계 검출
import cv2
import numpy as np
img = cv2.imread('../img/heart.png')
# 구조화 요소 커널, 사각형 (3x3) 생성 ---①
k = cv2.getStructuringElement(cv2.MORPH_RECT, (3,3))
# 열림 연산 적용 ---②
gradient = cv2.morphologyEx(img, cv2.MORPH_GRADIENT, k)
# 결과 출력
plt.figure(figsize = (10,6))
imgs = {'origin':img, 'gradient':gradient}
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()
# Tophat, Blackhat ( 밝은 부분 강조, 어두운 부분 강조)
import cv2
import numpy as np
img = cv2.imread('../img/keyboard.jpg')
# 구조화 요소 커널, 사각형 (5x5) 생성 ---①
k = cv2.getStructuringElement(cv2.MORPH_RECT, (9,9))
# 탑햇 연산 적용 ---②
tophat = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, k)
# 블랫햇 연산 적용 ---③
blackhat = cv2.morphologyEx(img, cv2.MORPH_BLACKHAT, k)
# 결과 출력
plt.figure(figsize = (20,6))
imgs = {'origin':img, 'tophat':tophat, 'blackhat':blackhat}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,3,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
6.4. 이미지 피라미드¶
이미지 피라미드는 영상의 크기를 단계적으로 축소 or 확대하여 피라미드처럼 쌓아 놓는 것을 말합니다. 이미지 피라미드를 사용하는 이유는 영상을 분석할 때 먼저 작은 이미지로 빠르게 훑어보고 다음 단계 크기의 영상으로 분석하는 식으로 정확도를 높이는 것이 효율적이고, 영상의 크기에 따라 분석하는 내용이 다를 수 있기 때문입니다.
6.4.1. 가우시안 피라미드¶
가우시안 필터를 적용한 후에 이미지 피라미드를 구현하는 것을 의미합니다.
- dst = cv2.pyrDown(src [, dst, dstsize, borderType])
- dst = cv2.pyrUp(src [, dst, dstsize, borderType])
- src : 입력 영상
- dst : 결과 영상
- distsize : 결과 영상 크기
- borderType : 외곽 보정 방식
cv2.pyrDown() 함수는 가우시안 필터를 적용하고 나서 모든 짝수 행과 열을 삭제하여 영상의 1/4 크기로 축소합니다.
cv2.pyrUp() 함수는 0으로 채워진 짝수 행과 열을 새롭게 삽입하고 나서 가우시안 필터로 컨볼루션을 수행, 주변 픽셀과 비슷하게 만드는 방법으로 4배 확대합니다.
import cv2
img = cv2.imread('../img/girl.jpg')
# 가우시안 이미지 피라미드 축소
smaller = cv2.pyrDown(img) # img x 1/4
# 가우시안 이미지 피라미드 확대
bigger = cv2.pyrUp(img) # img x 4
# 결과 출력
#plt.figure(figsize = (20,6))
imgs = {'origin':img, 'pyrDown':smaller, 'pyrUp':bigger}
for i, (k, v) in enumerate(imgs.items()):
#plt.subplot(1,3,i+1)
#plt.title(k)
# plt.imshow(v[:,:,(2,1,0)])
#plt.xticks([]),plt.yticks([])
print(k, v.shape)
# plt.show()
origin (426, 640, 3) pyrDown (213, 320, 3) pyrUp (852, 1280, 3)
6.4.2. 라플라시안 피라미드¶
cv2.pyUp() 함수는 4배로 확대할 때 없던 행과 열을 생성해서 가우시안 필터를 적용하기 때문에 원래 영상만큼 완벽하지 못합니다. 따라서 cv2.pyDown() 함수로 한 단계 작아진 영상을 cv2.pyUp() 함수로 확대해도 원래의 영상을 완벽히 복원할 수 없습니다. 이 때 원본 영상에서 cv2.pyUp() 함수로 확대한 영상을 빼고 이 값을 보관해 두었다가 확대 영상에 더하면 원본을 완벽히 복원할 수 있는데 이렇게 차이 값을 단계별로 모아두는 것을 라플라시안 피라미드라고 합니다.
import cv2
import numpy as np
img = cv2.imread('../img/girl.jpg')
# 원본 영상을 가우시안 피라미드로 축소
smaller = cv2.pyrDown(img)
# 축소한 영상을 가우시안 피라미드로 확대
bigger = cv2.pyrUp(smaller)
# 원본에서 확대한 영상 빼기
laplacian = cv2.subtract(img, bigger)
# 확대 한 영상에 라플라시안 영상 더해서 복원
restored = bigger + laplacian
# 결과 출력 (원본 영상, 라플라시안, 확대 영상, 복원 영상)
plt.figure(figsize = (20,6))
imgs = {'origin':img, 'laplacian':laplacian, 'bigger':bigger, 'restored':restored}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,4,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
laplacian : 대부분은 0으로 채워져 있고 경계 부분만 남아있습니다.
bigger : 축소했다가 확대하여 흐릿합니다.
restored : 라플라시안 피라미드를 더해 원본과 같이 또렷하게 복원되었습니다.
- 지금까지 여러가지 방법을 활용하여 다양한 방법으로 영상을 바꿔보았습니다.
- 경계 검출 관련하여 더 많은 방법이 있으니 살펴보면 좋을 것 같습니다!
감사합니다 :)
'대외활동 > DACrew 2기' 카테고리의 다른 글
[🔥팀 포스🔥] 첫번째 프로젝트, Multi-Hand Gesture Recognition (0) | 2022.05.21 |
---|---|
[ 파이썬으로 만드는 OpenCV 프로젝트🔥] 5장. 기하학적 변환 (0) | 2022.05.03 |
[ 파이썬으로 만드는 OpenCV 프로젝트🔥] 4장. 이미지 프로세싱 기초 (1) | 2022.04.28 |
댓글