블러링(Blurring)
이미지 블러링(Blurring)이란 이미지의 고주파 부분을 조금 더 자연스럽게 바꾸어줄 수 있는 방법이다. 이를 이해하기 위해 주파수에 대한 설명이 필요하다.
영상에서 고주파는 이미지의 색의 차이가 큰 부분이고, 저주파는 색의 차이가 적은 부분을 의미한다. 또한 상대적 고주파만 통과시키기 위한 LPF(Low Pass Filter)와 상대적 저주파를 제거해주기 위한 HPF(High Pass Filter)가 있는데, 블러링은 LPF를 통해 고주파에 해당하는 부분을 매끈하게(smoothing) 하게 보이도록 만드는 효과를 낸다.
사실 이미지 상에서 픽셀의 값은 공간적으로 느리게 변한다. 따라서 픽셀 간의 상관관계(correlation)가 크다. 이를 Slow Spatial Variation이라고 하는데, 이는 이미지에서 시그널(signal)에 해당한다. 반면, 노이즈는 픽셀간의 상관관계가 없으며 이미지에서 어떤 패턴도 나타내지 않는다.
따라서 Slow Spatial Variation에 의해 노이즈가 아닌 부분의 데이터는 상대적으로 서로 상관관계가 크며 저주파에 해당한다. 하지만 노이즈는 서로 상관관계가 아주 약하고 고주파 부분에 해당한다. 따라서 블러링을 통해 고주파 부분을 완화시켜주면 edge가 부분적으로 줄어들게 되어 원본 데이터가 조금 흐릿해지긴 하지만 노이즈는 어느 정도 제거해줄 수 있다. 블러링은 새로운 픽셀 값을 얻을 때 하나의 픽셀 값이 아닌 그 주변 픽셀들의 값을 활용하는 방법을 사용하므로 공간 영역 필터링(spacial domain filtering)라고도 한다.
대부분의 이미지 블러링 알고리즘은 특정 크기의 필터 안의 픽셀값들의 평균을 내는데 이때 필터의 합성(2D Convolution)을 통해 이뤄진다. 따라서 블러링은 평균을 내는 방식이 단순한 산술 평균이냐 아니면 가중치를 고려한 평균이냐에 따라 방법이 다양하게 나뉜다. 여기서 Convolution은 CNN의 그 합성곱을 뜻하는 Convolution이다.
openCV에서 아래 함수를 통해 filter2D를 구현할 수 있다.
cv2.filter2D(src, ddepth, kernel, dst=None, anchor=None, delta=None, borderType=None)
- src: input image
- ddepth: 출력 영상의 dtype (-1: 입력 영상과 동일)
- kernel: 컨볼루션 커널, float32의 n x n 크기 배열
- dst(optional): 결과 영상
- anchor(optional): 커널의 기준점, default: 중심점 (-1, -1)
- delta(optional): 필터가 적용된 결과에 추가할 값
- borderType(optional): 외곽 픽셀 보정 방법 지정
import cv2
import numpy as np
img = cv2.imread('brokenEgg.jpg')
n=5
kernel = np.ones((n, n), np.float32) / n**2
result = cv2.filter2D(img, -1, kernel)
cv2.imshow('original', img)
cv2.imshow('result', result)
cv2.waitKey(0)
처음 컨볼루션 연산을 할 때 원본 이미지의 픽셀 값과 그에 대응하는 필터의 픽셀 값을 요소 별로 곱한 뒤 모두 합해줬지만 여기서는 평균 블러링을 적용해야 하므로 5 x 5 필터의 요소 개수인 25로 나누어 정규화를 수행하였다. 이를 위해 mask의 모든 배열의 값을 더한 다음 역수(np.ones((5, 5))/5**2)를 통해 평균 블러링 필터를 만들어준다. 참고로 필터의 크기가 클수록 평균 블러링을 적용했을 때 선명도가 더 떨어진다.
이렇게 우리가 직접 커널을 생성하지 않고 평균 블러링을 사용할 수 있다. openCV는 아래의 함수들을 제공한다.
- Averaging
- Gaussian Blur(Gaussian smooth)
- Median Blurring
- Bilateral filter
1. Averaging
cv2.blur(src, ksize, dst, anchor, borderType)
- src: input image
- ksize: 커널 크기
- dst(optional): 결과 영상
- anchor(optional): 커널의 기준점, default: 중심점 (-1, -1)
- borderType(optional): 외곽 픽셀 보정 방법 지정
cv2.blur() 함수는 커널의 크기만 정해주면 알아서 평균 커널을 생성해서 평균 블러링을 적용한 영상을 반환한다. 커널 크기는 일반적으로 홀수로 정한다. 이와 유사한 cv2.boxFilter() 함수는 normalize에 True를 전달하면 cv2.blur() 함수와 동일한 기능을 한다. blur와 boxFilter는 에지 포함해서 전체적으로 블러링을 한다.
import cv2
import numpy as np
img = cv2.imread('brokenEgg.jpg')
n=15
result = cv2.blur(img, (n, n))
# result = cv2.boxFilter(img, -1, (n, n))
merged = np.hstack((img, result))
cv2.imshow('original-result', merged)
cv2.waitKey(0)
2. Gaussian Blur(Gaussian smooth)
cv2.GaussianBlur(src, ksize, sigmaX, sigmaY, borderType)
- src: input image
- ksize: 커널 크기
- sigmaX: X 방향 표준편차 (0: auto)
- sigmaY(optional): Y 방향 표준편차 (default: sigmaX)
- borderType(optional): 외곽 테두리 보정 방식
가우시안 블러링은 가운데 픽셀에 가장 큰 가중치를 두어서 계산한다. 이는 가우시안 분포를 갖는 커널로 블러링을 수행하는데 이 때 가우시안 분포(gaussian distribution)란 정규 분포(normal distribution)와 같은 의미로 받아들이면 된다. 정규 분포는 아래와 같이 평균 근처에 몰려 있는 값들의 개수가 많고 평균에서 멀어질수록 그 개수가 적어지는 분포를 말한다. 캐니(Canny)로 에지를 검출하기 전에 노이즈를 제거하기 위해 사용하기도 한다.
import cv2
import numpy as np
img = cv2.imread('brokenEgg.jpg')
n=15
result = cv2.GaussianBlur(img, (n, n), 0)
merged = np.hstack((img, result))
cv2.imshow('original-result', merged)
cv2.waitKey(0)
3. Median Blurring
cv2.medianBlur(src, ksize)
- src: input image
- ksize: 커널 크기
Median Blurring은 크기순으로 픽셀을 정렬한 후 중간값을 뽑아서 픽셀 값으로 사용합니다. 또한 Salt Pepper Noise를 제거하는 효과가 있다. 실행되는 구조는 박스 내 픽셀 값들을 크기순으로 나열하여 중간값을 고르고 이것을 현재 픽셀 값으로 정한다.
import cv2
import numpy as np
img = cv2.imread('brokenEgg.jpg')
sp = SaltPepper(img)
n=9
result = cv2.medianBlur(img, (n))
merged = np.hstack((img, sp, result))
cv2.imshow('original-result', merged)
cv2.waitKey(0)
Salt-Pepper 노이즈가 잘 지워졌다. 다른 블러로 테스트해보면 Median Blurring만큼 잘 지워지지 않는다.
4. Bilateral filter
cv2.bilateralFilter(src, d, sigmaColor, sigmaSpace, dst, borderType)
- src: input image
- d: 필터의 직경(diameter), 5보다 크면 매우 느림
- sigmaColor: 색공간의 시그마 값
- sigmaSpace: 좌표 공간의 시그마 값
- dst(optional): 결과 영상
- borderType(optioal): 외곽 픽셀 보정 방법 지정
Bilateral Filter는 다른 블러링과 다르게 edge를 보존하면서 노이즈를 감소시킬 수 있다. 다만 성능이 좋다면 속도가 느린 당연한 trade-off가 있다. 이 블러링 방식은 Similarity Function(유사성 함수)를 이용해 현재 픽셀의 값이 주변 픽셀과 비슷한 값을 가지면 1에 가까운 값을, 차이가 큰 값의 픽셀에 대해서는 0에 가까운 값을 주어 필터에 가중치를 부여한다.
import cv2
import numpy as np
img = cv2.imread('brokenEgg.jpg')
n=15
gaussian = cv2.GaussianBlur(img, (n, n), 0)
bilateral = cv2.bilateralFilter(img, n, 75, 75)
merged = np.hstack((img, gaussian, bilateral))
cv2.imshow('original-result', merged)
cv2.waitKey(0)
확실히 다른 블러링보다 원본 이미지의 형태가 잘 살아있다는 것이 느껴진다.
이 블러링은 노이즈를 지우는 데에도 사용할 수 있지만 이외에도 이미지 분류 딥러닝 모델 생성을 위한 학습 데이터 증대를 위해 사용해보았는데 나름 긍정적인 결과가 있었다.
아래는 전체 python 소스 코드이다.
import cv2
import numpy as np
def BlurImage(img, option=0, n=3):
'''
:param img: original image
:param option: 0: Convolution, 1: Averaging Blurring, 2: Gaussian Blurring, 3: Median Blurring, 4: Bilateral Filtering
:param n: size
'''
if option == 0:
# 컨볼루션 계산은 커널과 이미지 상에 대응되는 값끼리 곱한 후, 모두 더하여 구함
# 이 결과값을 결과 영상의 현재 위치에 기록
# default 3
kernel = np.ones((n, n), np.float32) / n**2
result = cv2.filter2D(img, -1, kernel)
elif option == 1:
# 이웃 픽셀의 평균을 결과 이미지의 픽셀값으로하는 평균 블러링
# 에지 포함해서 전체적으로 블러링
# default 15
result = cv2.blur(img, (n, n))
elif option == 2:
# 모든 픽셀에 똑같은 가중치를 부여했던 평균 블러링과 달리 가우시안 블러링은 중심에 있는 픽셀에 높은 가중치를 부여
# 캐니(Canny)로 에지를 검출하기전에 노이즈를 제거하기 위해 사용
# default 15
result = cv2.GaussianBlur(img, (n, n), 0)
elif option == 3:
# 관심화소 주변으로 지정한 커널 크기(5 x 5) 내의 픽셀을 크기순으로 정렬한 후 중간값을 뽑아서 픽셀값으로 사용
# default 15
result = cv2.medianBlur(img, n)
elif option == 4:
# 에지를 보존하면서 노이즈를 감소시킬수 있는 방법
# default 15
result = cv2.bilateralFilter(img, n, 75, 75)
return result
img = cv2.imread('brokenEgg.jpg')
result = BlurImage(img, option=0, n=15)
merged = np.hstack((img, result))
cv2.imshow('original-result', merged)
cv2.waitKey(0)
참고 자료
https://hoony-gunputer.tistory.com/entry/opencv-python-blurring
소스 코드
https://github.com/sehoon787/Personal_myBlog/blob/main/Data%20Science/CV/SaltPepper.py
https://github.com/sehoon787/Personal_myBlog/blob/main/Data%20Science/CV/Blurring.py