귀퉁이 서재

OpenCV - 17. 필터(Filter)와 컨볼루션(Convolution) 연산, 평균 블러링, 가우시안 블러링, 미디언 블러링, 바이레터럴 필터 본문

OpenCV

OpenCV - 17. 필터(Filter)와 컨볼루션(Convolution) 연산, 평균 블러링, 가우시안 블러링, 미디언 블러링, 바이레터럴 필터

Baek Kyun Shin 2020. 10. 5. 21:23

이번 포스팅부터는 영상 필터에 대해 알아보겠습니다. 이번 포스팅 역시 '파이썬으로 만드는 OpenCV 프로젝트(이세우 저)'를 정리한 것임을 밝힙니다.

코드: github.com/BaekKyunShin/OpenCV_Project_Python/tree/master/06.filter

시작하기 앞서 용어에 대해 먼저 정의하고 가겠습니다. 영상 처리는 새로운 영상을 얻기 위해 기존 픽셀 값에 어떤 연산을 가해서 새로운 픽셀 값을 얻는 작업입니다. 새로운 픽셀 값을 얻을 때 하나의 픽셀 값이 아닌 그 주변 픽셀들의 값을 활용하는 방법을 공간 영역 필터링(spacial domain filtering)라고 합니다. 또한, 블러링(Blurring)이란 기존의 영상을 흐릿하게 만드는 작업을 뜻합니다.

필터(Filter)와 컨볼루션(Convolution)

컨볼루션 연산은 공간 영역 필터링을 위한 핵심 연산 방법입니다. 블러링 작업을 예로 들어 컨볼루션 연산이 어떻게 진행되는지 살펴보겠습니다.

공간 영역 필터링은 연산 대상 픽셀과 그 주변 픽셀들을 활용하여 새로운 픽셀 값을 얻는 방법이라고 했습니다. 이때 주변 픽셀을 어느 범위까지 활용할지 그리고 연산은 어떻게 할지를 결정해야 합니다. 이런 역할을 하는 것이 바로 커널(kernel)입니다. 커널은 윈도(window), 필터(filter), 마스크(mask)라고도 부릅니다. 아래 그림에서 가운데 있는 3 x 3 짜리 행렬이 바로 커널입니다. 

출처: https://www.slipp.net/wiki/pages/viewpage.action?pageId=26641520

위 그림은 3 x 3 커널로 컨볼루션 연산을 하는 예시입니다. 기존 영상에서 픽셀 값 6을 기준으로 주변에 있는 픽셀 값인 3, 0, 1, 2, 1, 4, 2, 2(시계 방향)까지 활용했습니다. 일대일로 대응하는 위치에 있는 커널의 요소와 대응하는 입력 픽셀 값을 곱해서 모두 합한 것을 결과 픽셀 값으로 결정했습니다. 이런 연산을 마지막 픽셀까지 반복하는 것을 컨볼루션 연산이라고 합니다.

출처: https://www.slipp.net/wiki/pages/viewpage.action?pageId=26641520
출처: https://www.slipp.net/wiki/pages/viewpage.action?pageId=26641520

OpenCV에서는 아래 함수로 컨볼루션 연산을 지원합니다.

  • dst = cv2.filter2D(src, ddepth, kernel, dst, anchor, delta, borderType)
    src: 입력 영상, Numpy 배열
    ddepth: 출력 영상의 dtype (-1: 입력 영상과 동일)
    kernel: 컨볼루션 커널, float32의 n x n 크기 배열
    dst(optional): 결과 영상
    anchor(optional): 커널의 기준점, default: 중심점 (-1, -1)
    delta(optional): 필터가 적용된 결과에 추가할 값
    borderType(optional): 외곽 픽셀 보정 방법 지정

평균 블러링(Average Blurring)

앞서 설명했듯이 블러링은 초점이 맞지 않듯이 영상을 흐릿하게 하는 작업을 뜻합니다. 가장 간단한 블러링 방법으로는 평균 블러링이 있습니다. 평균 블러링은 주변 픽셀 값들의 평균을 적용합니다. 주변 픽셀들의 평균값을 적용하면 픽셀 간 차이가 적어져 선명도가 떨어져 전체적으로 흐릿해집니다.

아래는 5 x 5 평균 블러링 필터를 활용하여 컨볼루션 연산을 적용한 예시입니다.

# 평균 필터를 생상하여 블러 적용 (blur_avg_kernel.py)

import cv2
import numpy as np

img = cv2.imread('../img/yeosu_small.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)

# 결과 출력
cv2.imshow('origin', img)
cv2.imshow('avrg blur', blured) 
cv2.waitKey()
cv2.destroyAllWindows()

원본인 여수 야경에 평균 블러링을 적용하니 흐릿해진 것을 볼 수 있습니다. 위 코드에서 np.ones((5, 5))/5**2는 평균 블러링 필터 역할을 합니다. 위에서 맨 처음 컨볼루션 연산을 배울 때는 원본 이미지의 픽셀 값과 그에 대응하는 필터의 픽셀 값을 요소 별로 곱한 뒤 모두 합해줬습니다. 하지만 여기서는 평균 블러링을 적용해야 하므로 5 x 5 필터의 요소 개수인 5**2(=25)로 나누어 준 것입니다.

출처: https://hsg2510.tistory.com/112

필터의 크기가 클수록 평균 블러링을 적용했을 때 선명도가 더 떨어집니다.

출처: https://hsg2510.tistory.com/112

위와 같이 개발자가 직접 커널을 생성하지 않고도 평균 블러링을 적용할 수 있습니다. OpenCV에서는 아래와 같은 평균 블러링 함수를 제공합니다.

  • dst = cv2.blur(src, ksize, dst, anchor, borderType)
    src: 입력 영상, numpy 배열
    ksize: 커널의 크기
    나머지 파라미터는 cv2.filter2D()와 동일
  • dst = cv2.boxFilter(src, ddepth, ksize, dst, anchor, normalize, borderType)
    ddepth: 출력 영상의 dtype (-1: 입력 영상과 동일)
    normalize(optional): 커널 크기로 정규화(1/ksize²) 지정 여부 (Boolean), default=True
    나머지 파라미터는 cv2.filter2D()와 동일

cv2.blur() 함수는 커널의 크기만 정해주면 알아서 평균 커널을 생성해서 평균 블러링을 적용한 영상을 출력합니다. 커널 크기는 일반적으로 홀수로 정합니다. cv2.boxFilter() 함수는 normalize에 True를 전달하면 cv2.blur() 함수와 동일한 기능을 합니다.

아래는 cv2.blur() 함수와 cv2.boxFilter() 함수를 이용하여 평균 블러링을 적용하는 예제 코드입니다.

# 블러 전용 함수로 블러링 적용 (blur_avg_api.py)

import cv2
import numpy as np

file_name = '../img/taekwonv1.jpg'
img = cv2.imread(file_name)

# blur() 함수로 블러링  ---①
blur1 = cv2.blur(img, (10,10))
# boxFilter() 함수로 블러링 적용 ---②
blur2 = cv2.boxFilter(img, -1, (10,10))

# 결과 출력
merged = np.hstack( (img, blur1, blur2))
cv2.imshow('blur', merged)
cv2.waitKey(0)
cv2.destroyAllWindows()

 cv2.blur() 함수와 cv2.boxFilter() 함수가 동일한 결과를 냈음을 알 수 있습니다.

가우시안 블러링(Gaussian Blurring)

가우시안 분포를 갖는 커널로 블러링 하는 것을 가우시안 블러링이라고 합니다. 가우시안 분포(gaussian distribution)란 정규 분포(normal distribution)이라고도 하는데, 평균 근처에 몰려 있는 값들의 개수가 많고 평균에서 멀어질수록 그 개수가 적어지는 분포를 말합니다.

가우시안 블러링 커널은 아래와 같이 중앙값이 가장 크고 중앙에서 멀어질수록 그 값이 작아집니다. 

출처: https://hsg2510.tistory.com/112

첫 번째 커널에서 16으로 나눈 이유는 커널의 모든 요소의 합이 16이기 때문입니다. (1+2+1+2+4+2+1+2+1 = 16) 두 번째 커널도 모든 요소의 합이 256이므로 256으로 나누어준 것입니다. 이런 가우시안 블러링 커널을 적용하면 대상 픽셀에 가까울수록 많은 영향을 주고, 멀어질수록 적은 영향을 주기 때문에 원래의 영상과 비슷하면서도 노이즈를 제거하는 효과가 있습니다.

OpenCV에서는 아래와 같이 가우시안 블러링을 적용하는 함수를 제공합니다.

  • cv2.GaussianBlur(src, ksize, sigmaX, sigmaY, borderType)
    src: 입력 영상
    ksize: 커널 크기 (주로 홀수)
    sigmaX: X 방향 표준편차 (0: auto)
    sigmaY(optional): Y 방향 표준편차 (default: sigmaX)
    borderType(optional): 외곽 테두리 보정 방식
  • ret = cv2.getGaussianKernel(ksize, sigma, ktype)
    ret: 가우시안 커널 (1차원이므로 ret * ret.T 형태로 사용해야 함)

cv2.GaussianBlur() 함수는 커널 크기와 표준 편차를 전달하면 가우시안 블러링을 적용해줍니다. sigmaX에 0을 전달하면 자동으로 표준편차를 선택해서 사용하고, sigmaY를 생략하면 sigmaX 값과 동일하게 적용합니다.

cv2.getGaussianKernel() 함수는 커널 크기와 표준 편차를 전달하면 가우시안 필터를 반환합니다. 반환된 필터는 1차원이므로 cv2.filter2D() 함수에 사용하려면 ret * ret.T와 같은 형식으로 전달해야 합니다.

# 가우시안 블러링 (blur_gaussian.py)

import cv2
import numpy as np

img = cv2.imread('../img/gaussian_noise.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)

# 결과 출력
print('k1:', k1)
print('k2:', k2*k2.T)
merged = np.hstack((img, blur1, blur2, blur3))
cv2.imshow('gaussian blur', merged)
cv2.waitKey(0)
cv2.destroyAllWindows()

첫 번째로 가우시안 필터를 직접 생성해서 cv2.filter2D() 함수에 전달하여 블러링을 적용했습니다. 두 번째로 cv2.getGaussianKernel() 함수를 이용해 가우시안 커널을 얻었습니다. 얻은 커널을 역시 cv2.filter2D() 함수에 전달하여 블러링을 적용했습니다. 이때 주의할 것은 k2*k2.T와 같은 형태로 전달해야 한다는 점입니다. 마지막으로 cv2.GaussianBlur() 함수를 활용하여 필터를 별도로 구하지 않고 직접 가우시안 블러링을 적용했습니다. 결과 이미지가 작아서 잘 안 보이겠지만 노이즈가 제거된 것을 알 수 있습니다. 이렇듯 가우시안 블러링은 노이즈를 제거하는 효과가 있습니다.

미디언 블러링(Median Blurring)

커널의 픽셀 값 중 중앙값을 선택하는 것을 미디언 블러링이라고 합니다. 미디언 블러링은 소금-후추 잡음을 제거하는 효과가 있습니다. 소금-후추 잡음이란 이미지에 소금과 후추를 뿌린 것과 같이 생긴 잡음을 뜻합니다. OpenCV는 미디언 블러링을 위해 아래 함수를 제공합니다.

  • dst = cv2.medianBlur(src, ksize)
    src: 입력 영상
    ksize: 커널 크기
# 미디언 블러링 (blur_median.py)

import cv2
import numpy as np

img = cv2.imread("../img/salt_pepper_noise.jpg")

# 미디언 블러 적용 --- ①
blur = cv2.medianBlur(img, 5)

# 결과 출력 
merged = np.hstack((img,blur))
cv2.imshow('media', merged)
cv2.waitKey(0)
cv2.destroyAllWindows()

보시는 바와 같이 미디언 블러링을 적용하니 소금-후추 잡음이 제거되었습니다.

바이레터럴 필터(Bilateral Filter)

지금까지 적용한 블러링은 잡음을 제거하는 효과는 뛰어났지만 그만큼 경계도 흐릿하게 만드는 문제가 있었습니다. 바이레터럴 필터는 이를 개선하기 위해 가우시안 필터와 경계 필터를 결합합니다. 경계도 뚜렷하고 노이즈도 제거되는 효과가 있지만 속도가 느리다는 단점이 있습니다.

  • dst = cv2.bilateralFilter(src, d, sigmaColor, sigmaSpace, dst, borderType)
    src: 입력 영상
    d: 필터의 직경(diameter), 5보다 크면 매우 느림
    sigmaColor: 색공간의 시그마 값
    sigmaSpace: 좌표 공간의 시그마 값

일반적으로 sigmaColor와 sigmaSpace는 같은 값을 사용하며, 값의 범위는 10~150을 권장합니다.

# 바이레터럴 필터와 가우시안 필터 비교 (blur_bilateral.py)

import cv2
import numpy as np

img = cv2.imread("../img/gaussian_noise.jpg")

# 가우시안 필터 적용 ---①
blur1 = cv2.GaussianBlur(img, (5,5), 0)

# 바이레터럴 필터 적용 ---②
blur2 = cv2.bilateralFilter(img, 5, 75, 75)

# 결과 출력
merged = np.hstack((img, blur1, blur2))
cv2.imshow('bilateral', merged)
cv2.waitKey(0)
cv2.destroyAllWindows()

가우시안 필터를 적용했을 때는 경곗값이 흐릿하지만, 바이레터럴 필터를 적용했을 때는 노이즈는 줄면서 경곗값을 유지되는 것을 볼 수 있습니다. 

Comments