[CV] Gaussian Filter

31044 단어 CVGaussianFilterCV

요즘 CV에 대해서 공부하고 있는데, 정리해두지 않으면 잊어버릴 것 같아서 벨로그에 정리해두려고 한다.. 한달에 두개 정도 이상 하는게 목표인데 잘 되면 좋겠다

1. 필터

필터는 CV에서 굉장히 중요한 요소다. 주로 convolution을 이용하는데, 이미지에 다양한 처리를 할 수 있게 된다. 크기를 조정한다든지, 이미지를 블러 처리한다든지, 등등 다양한 옵션을 가지고 있어서 유용하게 사용할 수 있다. 필터라고도 부르지만, 커널이라고도 한다.

2. 가우시안 필터 (Gaussian Filter)

이번에는 가우시안 필터에 대해서 배웠는데, 주로 이미지를 smoothing 하는데 사용되는 것 같다. lena 이미지에 대해서 여러가지 값들을 변경해가면서 결과를 도출하는 걸 시도해봤다.

opencv 라이브러리에 물론 가우시안 필터를 만들 수 있는 간단한 방법이 존재하지만, 직접 해보는게 목표라! 식을 이해하고 코딩으로 표현해봤다.

2-1. 1D Gaussian Filter

가장 기본적인 형태의 가우시안 필터이다 (그래프는 μ=0,σ=1\mu = 0, \sigma =1

G(x)=12πσex22σ2G(x) = \dfrac 1 {\sqrt{2\pi}\sigma} e^{-\frac {x^2} {2\sigma^2}}

모든 필터에는 사이즈와 시그마 값이 존재한다. 여기서 xx는 커널의 중심에서 떨어진 거리이고, σ\sigma 는 유저가 직접 변경할 수 있는 값이다. 이 값에 따라서 노이즈 처리의 정도가 달라진다.

사용한 라이브러리 목록

import scipy
import cv2
import numpy as np
import matplotlib.pyplot as plt
import math

1D Gaussian Filter 생성하기

def genGausKernel1D(width, sigma):
    arr = np.arange((width//2)*(-1), (width//2)+1)
    #중심에서부터의 거리를 계산함,
    arr = np.array([float(x) for x in arr])
    #float 처리를 해주지 않으면 값이 달라질 수 있어서 처리해주었다
    kernel_1d= np.exp((-arr*arr)/(2*sigma**2)) 
    #위에서 봤던 수식을 그대로 적용했다. 달라진 점은, 앞에 상수로 나눠주는 부분을 생략한건데 
    #어차피 나중에 커널 전체 값의 합을 1로 만들기 위해서 커널 전체의 합으로 나눠주기 때문에 
    #상수부분이 필요 없다고 생각해서 나누지 않았다
    kernel_1d /= kernel_1d.sum()
    #kernel은 모든 element의 합이 1이어야하기 때문에 전체 수의 합으로 나눠준다. 
    #비율이 중요하다는 뜻으로 해석했다.
    return np.array([kernel_1d])

opencv 라이브러리를 이용해 한번에 생성!

kernel_1d = cv2.getGaussianKernel(5, 3)
# getGaussiankernel(width, sigma)의 형태임

물론 간단하게 하자면 라이브러리의 함수를 쓰는게 더 좋지만, 이해한 걸 확인하고 싶어서 직접 구현해봤다.

결과적으로 내가 생성한 두개의 필터가 동일한 값을 가지는 것을 확인할 수 있었다! :)

2-2. 2D Gaussian Filter

1D Gaussian filter와 크게 다르지 않다.

point-spread 형식의 그래프라고 하는데, 맨 윗 점에서 퍼지는 모양으로 존재한다는 뜻이다. convolution으로 얻을 수 있고, array에 값으로 저장되기 때문에 추가적인 처리가 필요하다.
수식으로 나타내면 다음과 같다!

G(x,y)=12πσ2ex2+y22σ2G(x,y) = \dfrac 1 {{2\pi}\sigma^2} e^{-\frac {x^2+y^2} {2\sigma^2}}

앞에서 1D Gaussian Filter 얘기를 했었는데, 그걸 이용해서 2D Gaussian Filter를 구할 수도 있다. 간단하게 설명하자면 벡터에서 매트릭스를 얻는 방식을 이용한다.

TMI : 벡터와 매트릭스
선형대수에서 벡터는 보통 세로로 긴 형태를 띤다. 이 점을 이용하면, 벡터 계산을 할때 두 가지 옵션이 생긴다!
- 가로형 벡터 * 세로형 벡터 (스칼라 형태)
: 이걸 벡터의 내적이라고 한다
- 세로형 벡터 * 가로형 벡터 (매트릭스 형태)
: 이걸 벡터의 외적이라고 함

1D Gaussian Filter 이용하기

width = 11
sigma = 3 
kernel_x = genGausKernel1D(width, sigma) 
kernel_y = kernel_x.T
# .T는 transpose인데, 벡터를 뒤집어서 가로형은 세로로, 세로형은 가로로 표현한다는 뜻
kernel_2d = np.outer(kernel_x, kernel_y)
#outer는 외적이라는 뜻! inner는 내적

2D gaussian filter 생성하는 새로운 함수 작성

def genGaussianKernel(width, sigma):
    array = np.arange((width//2)*(-1), (width//2)+1)
    #중심에서부터의 거리 계산
    arr = np.zeros((width, width))
    #x^2+y^2 부분을 미리 계산해둘 매트릭스 initialize
    for x in range(width):
        for y in range(width):
            arr[x,y] = array[x]**2+array[y]**2
            #중심에서부터의 거리를 제곱합으로 계산
    kernel_2d = np.zeros((width, width))
    #커널의 값을 저장할 매트릭스 생성
    for x in range(width):
        for y in range(width):
             kernel_2d[x,y] = np.exp(-arr[x,y]/(2*sigma**2))
             #수식에 맞게 값 저장(역시나 상수 부분은 생략)
    kernel_2d /= kernel_2d.sum()
    #전체 값의 합으로 나누어 필터 전체의 합이 1이 되도록 함
    return kernel_2d

생성된 필터 확인

이미지 로딩

lena       = cv2.imread('SourceImages/lena.bmp', 0) 
# 0 represents gray-scale 
#(1 for unchanged, -1 for color - default)
lena_noise = cv2.imread('SourceImages/lena_noise.bmp', 0)

위의 함수를 이용하여 커널(필터) 생성

kernel_1 = genGaussianKernel(5,1) # 5 by 5 kernel with sigma of 1
kernel_2 = genGaussianKernel(11,3)   # 11 by 11 kernel with sigma of 3

이미지 변환

res_lena_kernel1 = cv2.filter2D(lena, -1, kernel_1) 
# cv2.filter2D(src, ddepth, kernel)
# src : input
# ddepth = -1 : return type same as the source
# kernel (= mask)
res_lena_kernel2 = cv2.filter2D(lena, -1, kernel_2) 
res_lena_noise_kernel1 = cv2.filter2D(lena_noise, -1, kernel_1) 
res_lena_noise_kernel2 = cv2.filter2D(lena_noise, -1, kernel_2) 

커널에서 시그마 값을 변경하고, 커널 사이즈를 변경하면서 이미지가 어떻게 변하는지 볼 수 있었다!

생성된 두 필터를 비교

재밌는걸 또 시도해봤는데, filter를 두개 이어서도 적용할 수 있다는 것이다.

width = 11
sigma = 3 
kernel_x = genGausKernel1D(width, sigma) 
kernel_y = kernel_x.T
kernel_2d = np.outer(kernel_x, kernel_y)
res_lena_noise_kernel1d_x  = cv2.filter2D(lena_noise, -1, kernel_x) 
res_lena_noise_kernel1d_xy = cv2.filter2D(res_lena_noise_kernel1d_x, -1, kernel_y)
#두번 연속해서 커널을 적용하고 싶을때 쓰는 방법!
res_lena_noise_kernel2d    = cv2.filter2D(lena_noise, -1, kernel_2d)

위의 사진은 순서대로 원래 이미지, kernel_x 만 적용한 이미지, 거기에 kernel_y를 추가로 적용한 이미지, 그리고 2D kernel을 적용한 이미지 이렇게 네개를 나타낸 것이다!
마지막 두개의 결과값이 꽤 비슷했고, 차이를 그래프로 그려봤을때 크지 않아서 만족스러웠다.
코드 자체의 시간복잡도나 좀 Redundant한 부분들이 신경쓰이지만, 다음에 리팩토링 시도해보는 걸로 하고, 오늘은 여기서 끝!

아 그리고 1D와 2D 필터에 대해서 모두 구현해봤는데, 내가 직접 실험해본건 아니지만, 구글링하다보니 1D필터를 사용했을때가 미세하게(물론 처리량이 많아지면 차이가 심해지겠지만) 빠르다. 이건 개인의 코딩 스타일이나 시간복잡도에 따라 달라질수 있는 부분인것 같다. 블로그 참고

REF
https://homepages.inf.ed.ac.uk/rbf/HIPR2/gsmooth.htm
https://docs.opencv.org/4.x/
https://toitoitoi79.tistory.com/90 (1D와 2D 실행시간 비교)

좋은 웹페이지 즐겨찾기