안녕하세요. 지난 포스팅의 Opencv 제대로 쓰기[3].외부 카메라 동영상 스크린샷 저장하기에서는 웹캠을 이용해서 핸드폰으로 입력받은 동영상에서 원하는 시점마다 스크린샷을 저장하는 프로그램을 구현해보았습니다. 오늘부터는 본격적으로 영상 처리와 관련된 이야기를 해보려고 합니다. 오늘은 가장 간단한 흐림 처리와 관련된 함수들을 알아보도록 하겠습니다. 기본적인 내용은 아래의 링크에 정리를 해두었으니 코드를 보시기 전에 미리 보고 오시면 더 쉽게 이해할 수 있습니다.
제가 생각했을 때 영상 처리에서 가장 중요한 연산은 컨볼루션 연산이라고 생각합니다. 기본적으로 오늘 알아볼 평균 필터(또는 박스 필터), 가우시안 필터, 중앙값 필터 모두 컨볼루션 공간에서 수행되는 연산이기 때문이죠. opencv에서는 이러한 컨볼루션 연산을 지원하는 함수로 cv2.filter2D(src, dst, ddepth, kernel, anchor, delta, borderType)를 제공하고 있습니다. 각 파라미터에 대한 설명은 아래와 같습니다. 수식과 함께 보시면 더욱 빠르게 이해하실 수 있습니다.
$$\text{dst}(x, y) = \sum_{0 \le \text{kernel.cols}} \sum_{0 \le \text{kernel.rows}} \text{kernel}(x', y') * \text{src}(x + x' - \text{anchor}.x, y + y' - \text{anchor}.y) + \text{delta}$$
- src : 입력 영상
- dst : 입력 영상과 동일한 사이즈를 가지는 출력 영상
- ddepth : 출력 영상의 깊이
- kernel : 입력 영상에 적용할 커널
- anchor : 컨볼루션 연산을 통해 할당될 픽셀 위치
- delta : 컨볼루션 연산을 적용하고 출력 영상에 저장하기 전에 더할 값
- borderType : 적용할 테두리 외삽법(Border Extrapolation)
여기서 ddepth는 다시 여러 개의 종류로 나뉘게 됩니다.
위의 표와 같이 입력 영상의 깊이에 따라서 짝지어진 출력 영상의 깊이가 다른 것을 볼 수 있습니다. 이 부분은 나중에 더 자세히 설명하도록 하겠습니다. 여기서는 borderType 역시 여러 개의 종류가 존재합니다.
- cv2.BORDER_CONSTANT : iiiiii|abcdefgh|iiiiii
- cv2.BORDER_REPLICATE : aaaaaa|abcdefgh|hhhhhh
- cv2.BORDER_REFLECT : fedcba|abcdefgh|hgfedcb
- cv2.BORDER_WRAP : cdefgh|abcdefgh|abcdefg
- cv2.BORDER_REFLECT_101 : gfedcb|abcdefgh|gfedcba
- cv2.BORDER_REFLECT101 : gfedcb|abcdefgh|gfedcba
- cv2.BORDER_DEFAULT : gfedcb|abcdefgh|gfedcba
- cv2.BORDER_TRANSPARENT : uvwxyz|abcdefgh|ijklmno
- cv2.BORDER_ISOLATED : 관심 영역 밖은 고려하지 않음
이렇게 많은 테두리 외삽법 중 보통 그냥 cv2.BORDER_DEFAULT를 사용하는 편입니다. 이 함수를 이용하여 직접 다양한 사이즈의 박스 커널을 정의하면 아래와 같은 결과를 얻을 수 있습니다.
import os
import sys
import cv2
import numpy as np
import matplotlib.pyplot as plt
save_path = './opencv_smoothing/result'
if not os.path.exists(save_path) : os.makedirs(save_path)
image_root_path = 'example/Ch3'
image_path = os.path.join(image_root_path, 'Fig0333(a)(test_pattern_blurring_orig).tif')
print(image_path)
image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
if image is None :
print("Image is not loaded.")
sys.exit(-1)
fig, ax = plt.subplots(3, 3, figsize=(6, 6))
ax[0, 0].imshow(image, cmap='gray')
ax[0, 0].axis('off')
i, j = 0, 1
for kernelSize in [3, 5, 7, 9, 11, 13, 15, 17] :
kernel = np.ones((kernelSize, kernelSize), dtype=np.float32)/(kernelSize**2)
result = cv2.filter2D(image, -1, kernel)
ax[i, j].imshow(result, cmap='gray')
ax[i, j].axis('off')
j += 1
if j == 3 :
i += 1; j = 0
plt.tight_layout()
plt.subplots_adjust(left=0, bottom=0, right=1, top=1, hspace=0, wspace=0)
plt.savefig(os.path.join(save_path, 'convolution example.png'))
영상의 좌측 최상단은 입력 영상이고 오른쪽부터 각각 박스 커널의 크기를 3, 5, 7, 9, 11, 13, 15, 17로 했을 때 결과입니다. 디지털 영상 처리에서 본 것과 마찬가지로 커널의 크기가 커질수록 흐림 효과가 강해지는 것을 볼 수 있습니다.
이와 비슷한 역할을 하는 함수가 바로 cv2.blur(src, dst, ksize, anchor, borderType)입니다. cv2.blur와 cv2.filter2D는 파라미터 자체는 거의 유사하지만 다른 점은 cv2.filter2D는 그냥 저희가 어떠한 종류의 이상한 커널이라고 집어넣어서 컨볼루션 연산을 취할 수 있지만 cv2.blur는 항상 정규화된 박스 커널만 사용하기 때문에 커널의 사이즈인 ksize만 넘겨주면 됩니다.
import os
import sys
import cv2
import numpy as np
import matplotlib.pyplot as plt
save_path = './opencv_smoothing/result'
if not os.path.exists(save_path) : os.makedirs(save_path)
image_root_path = 'example/Ch3'
image_path = os.path.join(image_root_path, 'Fig0333(a)(test_pattern_blurring_orig).tif')
print(image_path)
image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
if image is None :
print("Image is not loaded.")
sys.exit(-1)
fig, ax = plt.subplots(3, 3, figsize=(6, 6))
ax[0, 0].imshow(image, cmap='gray')
ax[0, 0].axis('off')
i, j = 0, 1
for kernelSize in [3, 5, 7, 9, 11, 13, 15, 17] :
result = cv2.blur(image, (kernelSize, kernelSize))
ax[i, j].imshow(result, cmap='gray')
ax[i, j].axis('off')
j += 1
if j == 3 :
i += 1; j = 0
plt.tight_layout()
plt.subplots_adjust(left=0, bottom=0, right=1, top=1, hspace=0, wspace=0)
plt.savefig(os.path.join(save_path, 'blur example.png'))
참고로 두 함수의 결과는 동일하지만 구현만 살짝 다르기 때문에 쉽게 이해하실 수 있을 것입니다. 스무딩은 그 종류에 따라서 아주 다양한 이름이 존재합니다. 위의 2가지 처럼 평균 필터를 적용할 수도 있지만 커널 내의 가중치가 가우시안 분포를 따르는 것으로 만들어서 가우시안 블러를 수행할 수도 있고 정렬을 통해 최댓값, 최솟값, 중앙값을 사용하는 비선형 필터 역시 할 수 있습니다. 이번에는 가우시안 블러링을 수행하는 함수인 cv2.GaussianBlur(src, ksize, sigmaX, sigmaY, borderType)에 대해서 알아보도록 하겠습니다. 이미 저희가 알고 있는 파라미터인 src, ksize, borderType을 제외하면 sigmaX와 sigmaY 파라미터가 추가되었습니다. 기본적으로 가우시안 분포를 정의하기 위해서는 분산부터 정의해야하고 영상은 2차원이기 때문에 $x$ 축 방향의 분산과 $y$ 축 방향의 분산을 각각 정의해주어야합니다.
import os
import sys
import cv2
import numpy as np
import matplotlib.pyplot as plt
save_path = './opencv_smoothing/result'
if not os.path.exists(save_path) : os.makedirs(save_path)
image_root_path = 'example/Ch3'
image_path = os.path.join(image_root_path, 'Fig0333(a)(test_pattern_blurring_orig).tif')
print(image_path)
image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
if image is None :
print("Image is not loaded.")
sys.exit(-1)
fig, ax = plt.subplots(3, 3, figsize=(6, 6))
ax[0, 0].imshow(image, cmap='gray')
ax[0, 0].axis('off')
i, j = 0, 1
sigmaX, sigmaY = 10, 10
for kernelSize in [3, 5, 7, 9, 11, 13, 15, 17] :
result = cv2.GaussianBlur(image, (kernelSize, kernelSize), sigmaX, sigmaY)
ax[i, j].imshow(result, cmap='gray')
ax[i, j].axis('off')
j += 1
if j == 3 :
i += 1; j = 0
plt.tight_layout()
plt.subplots_adjust(left=0, bottom=0, right=1, top=1, hspace=0, wspace=0)
plt.savefig(os.path.join(save_path, 'gaussian blur example(sigmaX = {}, sigmaY = {}).png'.format(sigmaX, sigmaY)))
왼쪽부터 분산을 1, 5, 10으로 했을 때 변화를 보여주고 있습니다. 가우시안 분포는 분산이 커질수록 넓게 분포하는 특성이 존재합니다. 따라서 분산이 커지면 커질수록 대부분의 값이 비슷해질것이고 이는 평균 필터와 유사한 결과를 보여줄 것입니다. 가우시안 필터링과 박스 필터링의 가장 큰 차이점은 위의 경우에는 보이지 않았지만 박스 필터는 커질수록 영상 내에 인위적인 박스 구조물이 생성되는 블록 현상(Blocky Effect)가 발생할 가능성이 크지만 가우시안 필터는 그렇지 않기 때문에 자주 쓰이는 필터링 방법입니다.
중앙값 필터링(Median Filtering)을 적용해보도록 하겠습니다. 중앙값 필터링은 커널 내의 픽셀값을 기준으로 정렬한 뒤 중앙값을 선택하는 비선형 필터링의 대표주자입니다. 또한 중앙값 필터링은 소금-후추 노이즈 제거에 아주 좋은 성능을 보여주는 필터링입니다. opencv에서 중앙값 필터링을 위한 함수로 cv2.medianBlur(src, ksize)를 지원하고 있습니다. 이제 다들 파라미터가 어떤 의미인지 이해하셨을 테니 설명은 하지 않도록 하겠습니다. 다른점은 중앙값 필터에서 ksize는 항상 정수값을 받는 다는 점입니다.
import os
import sys
import cv2
import numpy as np
import matplotlib.pyplot as plt
save_path = './opencv_smoothing/result'
if not os.path.exists(save_path) : os.makedirs(save_path)
image_root_path = 'example/Ch3'
image_path = os.path.join(image_root_path, 'Fig0335(a)(ckt_board_saltpep_prob_pt05).tif')
print(image_path)
image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
if image is None :
print("Image is not loaded.")
sys.exit(-1)
fig, ax = plt.subplots(3, 3, figsize=(6, 6))
ax[0, 0].imshow(image, cmap='gray')
ax[0, 0].axis('off')
i, j = 0, 1
for kernelSize in [3, 5, 7, 9, 11, 13, 15, 17] :
result = cv2.medianBlur(image, kernelSize)
ax[i, j].imshow(result, cmap='gray')
ax[i, j].axis('off')
j += 1
if j == 3 :
i += 1; j = 0
plt.tight_layout()
plt.subplots_adjust(left=0, bottom=0, right=1, top=1, hspace=0, wspace=0)
plt.savefig(os.path.join(save_path, 'median filter example.png'))
좌측 최상단 그림은 입력 영상입니다. 아주 심한 소금-후추 노이즈가 발생한 것을 관찰할 수 있습니다. 이 영상에 서로 다른 크기의 중앙값 필터를 적용하면서 어떤 변화가 생기는 지 관찰할 수 있습니다. 작은 크기의 커널로도 소금-후추 노이즈가 거의 완벽하게 제거되는 것을 볼 수 있습니다. 하지만 커널의 크기가 커질수록 심한 왜곡이 발생하게 됩니다.
'Programming > Python' 카테고리의 다른 글
Opencv 제대로 쓰기[6].영상 히스토그램 (0) | 2021.06.12 |
---|---|
Opencv 제대로 쓰기[5].영상 샤프닝 (0) | 2021.05.31 |
Opencv 제대로 쓰기[3].외부 카메라 동영상 스크린샷 저장하기 (0) | 2021.05.01 |
Opencv 제대로 쓰기[2]- 외부 카메라로부터 동영상 입력받기 (0) | 2021.04.25 |
Opencv 제대로 쓰기[1] - 컴퓨터 내 영상/동영상 입출력 (0) | 2021.04.17 |