본문 바로가기

Deep Learning(강의 및 책)/OpenCV

[OpenCV] 영상 필터

728x90

이 글은 '파이썬으로 만드는 OpenCV 프로젝트'를 보고 작성했습니다.

 

필터(filter)는 우리말로는 거름망이나 여과기 정도인데, 영상 처리에서는 입력 값에서 원하지 않는 값은 걸러내고 원하는 결과만을 얻는다는 의미로 쓰입니다. 영상을 흐릿하게 만들거나 또렷하게 만들기도 해서 영상의 품질을 높이기도 하지만, 엣지(edge, 경계)를 검출하고 엣지의 방향을 알아내는 등 객체 인식과 분리의 기본이 되는 정보를 계산하기도 합니다.

 

6.1 컨볼루션(Convolution)과 블러링(Blurring)

■ 필터와 컨볼루션

공간 영역 필터는 연산 대상 픽셀과 그 주변 픽셀 값을 활용하는데, 주변 픽셀들 중에 어디까지를 포함할 것인지 그리고 결과 값을 어떻게 산출할 것인지를 결정하는 것을 커널이라 합니다. n x n 크기의 커널(kernel)은 윈도우(window), 필터(filter), 마스크(mask)라고도 부르는데, 커널의 각 요소와 대응하는 입력 픽셀 값을 곱해서 모두 합한 것을 결과 픽셀 값으로 결정하고, 이것을 마지막 픽셀까지 반복하는 것을 컨볼루션 연산이라 합니다.

하나의 픽셀 값이 결정되면 한 칸 옮겨서 같은 연산을 반복하는 식으로 마지막 픽셀까지 적용함녀 커널에 지정한 값의 비중에 따라 주변 요소들의 값이 반영된 새로운 영상을 얻을 수 있습니다. 커널 크기와 값에 따라 결과 영상에 필터를 적용한 효과가 달라집니다. 예를 들어 주변 요소 값들의 평균값을 반영하면 전체적인 영상은 흐릿해지고 주변 요소 값들과의 차이를 반영하면 또렷해집니다.

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

■ 평균 블러링

영상을 초점이 맞지 않는 것처럼 흐릿하게 만드는 것을 블러링(blurring) 또는 스무딩(smoothing)이라고 합니다. 블러링을 적용하는 가장 손쉬운 방법은 주변 픽셀 값들의 평균을 적용하는 것입니다. 평균 값을 적용한다는 것은 다른 픽셀과 비슷한 값을 갖게 하는 것이므로 전체적인 영상의 픽셀 값의 차이가 적어져서 이미지는 흐릿해집니다.

 

import cv2
import numpy as np

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)

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

 

이와 같이 이미지가 흐릿해진 것을 볼 수 있습니다. cv2.filter2D() 함수로 필터를 적용합니다.

  • dst = cv2.blur(src, ksize [, dst, anchor, borderType])
    • src: 입력 영상, Numpy 배열
    • ksize: 커널의 크기
    • 나머지 인자는 cv2.filter2D와 동일
  • dst = cv2.boxFilter(src, ddepth, ksize [, dst, anchor, normalize, borderType])
    • src: 입력 영상, Numpy 배열
    • ddepth: 출력 영상의 dtype, -1: 입력 영상과 동일
    • normalize: 커널 크기로 정규화(1/ksize^2)지정 여부, 불(boolean)
    • 나머지 인자는 cv2.filter2D와 동일

cv2.blur() 함수는 커널의 크기만 지정하면 알아서 평균 커널을 생성해서 블러링 적용한 영상을 만들어 냅니다. 이때 커널의 크기는 홀수를 사용하는 것이 일반적입니다. cv2.boxFilter() 함수는 normalize 인자에 True를 지정하면 cv2.blur() 함수와 같습니다. 만약 False를 입력하면 커널 영역의 모든 픽셀의 합을 구하게 되는데, 이것은 밀도를 이용한 객체 추적 알고리즘에서 사용합니다.

 

■ 가우시안 블러링

평균이 아닌 가우시안 분포를 갖는 커널로 블러링 하는 것을 가우시안 블러링이라 합니다. 중앙에서 가장 큰 값을 갖고 중앙에서 멀어질수록 작은 값을 갖는 커널입니다. 이렇게 하면 새로운 픽셀 값을 선정할 때 대상 픽셀에 가까울수록 많은 영향을 주고, 멀어질수록 적은 영향을 주기 때문에 원래의 영상과 비슷하면서도 노이즈를 제거하는 효과가 있습니다.

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

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

cv2.getGaussiankernel() 함수는 커널 크기와 표준편차 값을 전달하면 그에 맞는 가우시안 블러링 커널을 만들어서 반환합니다. 다만 반환 값은 2차원이 아닌 1차원 배열이므로 cv2.filter2D() 함수에 사용하려면 ret * ret.T와 같은 꼴로 사용해야 합니다.

 

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()

 

세 가지 경우 모두 noise 제거 효과가 있는 것을 볼 수 있습니다.

 

■ 미디언 블러링

커널 영역 픽셀 값 중에 중간 값을 대상 픽셀의 값으로 선택ㄷ하는 것을 미디언(median) 블러링이라고 합니다. 이 전까지의 블러링은 원본 픽셀 값에 대해 새로운 값이 생성되는 것에 비해서 이 필터는 기존 픽셀 값 중에 하나를 선택하므로 기존 값을 재활용한다는 특징이 있습니다. 이 필터는 소금-후추(salt-and-pepper, 소금과 후추를 뿌린 듯한) 잡음 제거에 효과적입니다.

  • dst = cv2.medianBlur(src, ksize)
    • src: 입력 영상, Numpy 배열
    • ksize: 커널 크기
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()

 

노이즈가 제거된 모습을 볼 수 있습니다.

 

■ 바이레터럴 필터

블러링 필터는 대체로 잡음을 제거하는 데 효과가 있지만, 경계(edge)도 흐릿하게 만드는 문제가 있습니다. 바이레터럴(bilateral) 필터는 이 문제를 개선하기 위해 가우시안 필터와 경계 필터 2개를 사용하는데, 그 결과 노이즈는 없고 경계가 비교적 또렷한 영상을 얻을 수 있지만 속도가 느리다는 단점이 있습니다.

  • dst = cv2.bilateralFilter(src, d, sigmaColor. sigmaSpace [, dst, borderType])
    • src: 입력 영상, Numpy 배열
    • d: 픽터의 직경(diameter), 5보다 크면 매우 느림
    • sigmaColor: 색공간 필터의 시그마 값
    • sigmaSpace: 좌표 공간의 시그마 값(단순한 사용을 위해 sigmaColor와 sigmaSpace에 같은 값을 사용할 것을 권장하며, 범위는 10 ~ 150을 권장함)
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()

 

가우시안 필터와 바이레터럴 필터를 각각 적용해서 출력한 모습입니다. 바이레터럴 필터를 적용한 결과를 보면 노이즈는 줄고 엣지는 유지되는 것을 확인할 수 있습니다. cv2.bilateralFilter() 함수의 시그마 값을 150 이상으로 지정하면 스케치 효과를 얻을 수 있습니다.

 

6.2 경계 검출

영상에서 경계(edge)를 검출하는 것은 배경과 전경을 분리하는 데 가장 기본적인 작업입니다. 우리가 물체를 알아보는 것도 배경과 전경을 분리하는 경계 검출 없이는 불가능한 일입니다. 앞서 본 필터들은 영상을 흐릿하게 만들었는데, 그 반대로 영상의 경계를 선명하게 만드는 것을 샤프닝(sharping)이라고 합니다. 샤프닝은 경계를 검출해서 경계에 있는 픽셀만을 골라서 강조하는 것입니다.

 

■ 기본 미분 필터

경계를 검출하려면 픽셀 값의 변화가 갑자기 크게 일어나는 지점을 찾아내야 하는데, 이것은 연속된 픽셀 값에 미분 연산ㅇ르 하면 알 수 있습니다. 영상 속의 픽셀 데이터는 현실과 같은 연속된 공간이 아니므로 이산화시켜서 근사 값으로 간소화해야 합니다.

Gx = f_(x+1, y) - f_(x, y)로 근사가능하며 Gy = f_(x, y + 1) - f_(x, y)로 근사 가능합니다. 즉 x축과 y축 각각의 방향에서 다음 픽셀의 값에서 현재 픽셀의 값을 빼라는 것입니다. 영상에 대한 미분 연산을 컨볼루션 커널로 만들면 Gx = [[-1 1]], Gy = [[-1], [1]]이 됩니다.

 

import cv2
import numpy as np

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

#미분 커널 생성 ---①
gx_kernel = np.array([[ -1, 1]])
gy_kernel = np.array([[ -1],[ 1]])

# 필터 적용 ---②
edge_gx = cv2.filter2D(img, -1, gx_kernel)
edge_gy = cv2.filter2D(img, -1, gy_kernel)
# 결과 출력
merged = np.hstack((img, edge_gx, edge_gy))
cv2.imshow('edge', merged)
cv2.waitKey(0)
cv2.destroyAllWindows()

 

x축 방향과 y축 방향 미분 마스크를 생성해서 cv2.filter2D() 함수에 적용한 결과입니다. x 방향 미분 마스크는 세로 방향 경계를 검출하고, 반대로 y축 방향 미분 마스크는 가로 방향 미분 마스크를 검출하는 모습입니다.

근사 값이긴 하지만 미분으로 얻은 엣지 정보는 각각 x축과 y축에 대한 값의 변화를 나타내는 것이고 이것을 기울기, 즉 그레디언트(gradient)라고 합니다. Gx, Gy 두 값을 이용하면 엣지의 강도(magnitude)와 방향(direction)이라는 두 가지 중요한 정보를 추가로 얻을 수 있습니다. 강도의 경우 (Gx^2 + Gy^2)^(1/2)로 표현 가능하며 방향은 tan^-1(Gy/Gx)로 표현 가능합니다. 그레디언트의 방향과 엣지의 방향은 같은 방향을 가리키는 것이 아니라 서로 수직입니다.

 

■ 로버츠 교차 필터

1963년 로렌스 로버츠(Lawrence Roberts)는 기본 미분 커널을 개선한 커널을 제안했습니다. Gx = [[1 0], [0 -1]], Gy = [[1 0], [0 -1]]입니다. 대각선 방향으로 1과 -1을 배치해서 사선 경계 검출 효과를 높였지만 노이즈에 민감하고 엣지 강도가 약한 단점이 있습니다.

 

import cv2
import numpy as np

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

# 결과 출력
merged = np.hstack((img, edge_gx, edge_gy, edge_gx+edge_gy))
cv2.imshow('roberts cross', merged)
cv2.waitKey(0)
cv2.destroyAllWindows()

 

 

■ 프리윗 필터

주디스 프리윗(Judith M. S. Prewitt)이 개발한 프리윗 마스크는 각 방향으로 차분을 세 번 계산하도록 배치해서 엣지 강도가 강하고 수직과 수평 엣지를 동등하게 찾는 장점이 있지만 대각선 검출이 약합니다.

Gx = [[-1 0 1], [-1 0 1], [-1 0 1]] Gy = [[-1 -1 -1], [0 0 0], [1 1 1]]입니다.

 

import cv2
import numpy as np

file_name = "../img/sudoku.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)

# 결과 출력
merged = np.hstack((img, edge_gx, edge_gy, edge_gx+edge_gy))
cv2.imshow('prewitt', merged)
cv2.waitKey(0)
cv2.destroyAllWindows()

 

■ 소벨 필터

1968년 어윈 소벨(Irwin Sobel)은 중심 픽셀의 차분 비중을 두 배로 주어 수평, 수직 대각선 경계 검출에 모두 강한 마스크를 제안합니다.

Gx = [[-1 0 1], [-2 0 2], [-1 0 1]] Gy = [[-1 -2 -1], [0 0 0], [1 2 1]]입니다.

소벨 마스크는 가장 대표적인 1차 미분 마스크로 OpenCV는 전용 함수를 제공합니다.

  • dst = cv2.Sobel(src, ddepth, dx, dy [, dst, ksize, scale, delta, borderType])
    • src: 입력 영상, Numpy 배열
    • ddepth: 출력 영상의 dtype(-1: 입력 영상과 동일)
    • dx, dy: 미분 차수(0, 1, 2 중 선택, 둘 다 0일 순 없음)
    • ksize: 커널의 크기(1, 3, 5, 7 중 선택)
    • scale: 미분에 사용할 계수
    • delta: 연산 결과에 가산할 값
import cv2
import numpy as np

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

# 결과 출력
merged1 = np.hstack((img, edge_gx, edge_gy, edge_gx+edge_gy))
merged2 = np.hstack((img, sobelx, sobely, sobelx+sobely))
merged = np.vstack((merged1, merged2))
cv2.imshow('sobel', merged)
cv2.waitKey(0)
cv2.destroyAllWindows()

 

마스크를 생성해서 적용하거나 cv2.Sobel() 함수로 필터를 적용한 모습입니다.

 

■ 샤르 필터

소벨 필터는 커널의 크기가 작은 경우, 또는 커널의 크기가 크더라도 그 중심에서 멀어질수록 엣지 방향성의 정확도가 떨어지는 단점이 있는데, 이를 개선한 필터가 샤르(Scharr) 필터입니다.

Gx = [[-3 0 3], [-10 0 10], [-3 0 3]], Gy = [[-3 -10 -3], [0 0 0], [3 10 3]]입니다.

  • dst = cv2.Scharr(src, ddepth, dx, dy [, dst, scale, delta, borderType]): 함수의 인자는 ksize가 없다는 것을 제외하면 cv2.Sobel()과 동일
import cv2
import numpy as np

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

# 결과 출력
merged1 = np.hstack((img, edge_gx, edge_gy))
merged2 = np.hstack((img, scharrx, scharry))
merged = np.vstack((merged1, merged2))
cv2.imshow('Scharr', merged)
cv2.waitKey(0)
cv2.destroyAllWindows()

 

■ 라플라시안 필터

1차 미분 결과를 다시 미분하는 2차 미분을 적용하면 경계를 더 확실히 검출할 수 있습니다. 라플라시안(Laplacian) 필터는 대표적인 2차 미분 마스크입니다. 2차 미분을 수식으로 나타내면 f(x, y + 1) - f(x, y) + f(x, y -1)이고 커널로 표현하면 [[0 1 0], [1 -4 1], [0 1 0]]입니다.

  • dst = cv2.Laplacian(src, ddepth [, dst, ksize, scale, delta, borderType]): 함수의 인자는 cv2.Sobel()과 동일
import cv2
import numpy as np

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

# 라플라시안 필터 적용 ---①
edge = cv2.Laplacian(img, -1)

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

 

라플라시안 필터는 노이즈에 민감하므로 사전에 가우시안 필터로 노이즈를 제거하고 사용하는 것이 좋습니다.

 

■ 캐니 엣지

1986년 존 캐니(John F. Canny)가 제안한 캐니 엣지 알고리즘은 한 가지 필터를 사용하는 것이 아니라 4단계의 알고리즘을 적용한 잡음에 강한 뛰어난 엣지 검출기입니다.

  1. 노이즈 제거(Noise Reduction): 5 x 5 가우시안 블러링 필터로 노이즈를 제거
  2. 엣지 그레디언트 방향 계산: 소벨 마스크로 엣지 및 그레디언트 방향을 검출
  3. 비최대치 억제(Non-Maximum Suppression): 그레디언트 방향에서 검출된 엣지 중에 가장 큰 값만 선택하고 나머지는 제거
  4. 이력 스레시홀딩(Hysteresis Thresholding): 두 개의 경계 값(Max, Min)을 지정해서 경계 영역에 있는 픽셀들 중 큰 경계 값(Max) 밖의 픽셀과 연결성이 없는 픽셀을 제거

OpenCV는 알고리즘을 구현한 cv2.Canny() 함수를 제공합니다.

  • edges = cv2.Canny(img, threshold1, threshold2 [, edges, apertureSize, L2gradient])
    • img: 입력 영상, Numpy 배열
    • threshold1, threshold2: 이력 스레시홀딩에 사용할 최소, 최대 값
    • apertureSize: 소벨 마스크에 사용할 커널 크기
    • L2gradient: 그레디언트 강도를 구할 방식 지정 플래그
      • True: (Gx^2 + Gy^2)^(1/2)
      • False: |Gx| + |Gy|
    • edges: 엣지 결과 값을 갖는 2차원 배열
import cv2, time
import numpy as np

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

# 케니 엣지 적용 
edges = cv2.Canny(img,100,200)

# 결과 출력
cv2.imshow('Original', img)
cv2.imshow('Canny', edges)
cv2.waitKey(0)
cv2.destroyAllWindows()

 

캐니 엣지는 경계 검출 결과가 뛰어나고 스레시홀드 값의 지정에 따라 경계 검출 대상을 조정할 수 있어서 가장 많이 사용되는 함수입니다.

 

6.3 모폴로지

모폴로지(morpholohy)는 형태학이란 뜻으로 영상 분야에서는 노이즈 제거, 구어 메꾸기, 연결되지 않은 경계 이어 붙이기 등 형태학적 관점에서의 영상 연산을 말합니다. 모폴로지는 주로 형태를 다루는 연산이므로 바이너리 이미지를 대상으로 합니다. 대표적인 연산은 침식과 팽창이며, 이 둘을 결합한 열림과 닫힘 연산도 있습니다.

 

■ 침식 연산

침식(erosion)은 원래 있던 객체의 영역을 깎아 내는 연산입니다. 이 연산을 위해서는 구조화 요소(structuring element)라는 0과 1로 채워진 커널이 필요한데, 1이 채워진 모양에 따라 사각형, 타원형, 십자형 등을 사용할 수 있습니다. 침식 연산은 구조화 요소 커널을 입력 영상에 적용해서 1로 채워진 영역을 온전히 올려놓을 수 없으면 해당 픽셀을 0으로 변경합니다.

OpenCV는 구조화 요소 커널 생성을 위한 함수로 cv2.getStructuringElement() 함수를, 침식 연산을 위한 함수로는 cv2.erode()를 제공합니다.

  • cv2.getStructuringElement(shape, ksize [, anchor])
    • shape: 구조화 요소 커널의 모양 결정
      • cv2.MORPH_RECT: 사각형
      • cv2.MORPH_ELLIPSE: 타원형
      • cv2.MORPH_CROSS: 십자형
    • ksize: 커널 크기
    • anchor: 구조화 요소의 기준점, cv2.MORPH_CROSS에만 의미 있고 기본 값은 중심점(-1, -1)
  • dst = cv2.erode(src, kernel [, anchor, iterations, borderType, borderValue])
    • src: 입력 영상, Numpy 객체, 바이너리 영상(검은색: 배경, 흰색: 전경)
    • kernel: 구조화 요소 커널 객체
    • anchor: cv2.getStructuringElemenet()와 동일
    • iterations: 침식 연산 적용 반복 횟수
    • borderType: 외곽 영역 보정 방법 설정 플래그
    • borderValue: 외곽 영역 보정 값

침식 연산은 큰 물체는 주변을 깎아서 작게 만들지만 작은 객체는 아예 사라지게 만들 수 있으므로 아주 작은 노이즈를 제거하거나 원래는 따로 떨어진 물체인데 겹쳐져서 하나의 물체로 보일 때 서로를 떼어내는 데 효과적입니다.

 

import cv2
import numpy as np

img = cv2.imread('../img/morph_dot.png')

# 구조화 요소 커널, 사각형 (3x3) 생성 ---①
k = cv2.getStructuringElement(cv2.MORPH_RECT, (3,3))
# 침식 연산 적용 ---②
erosion = cv2.erode(img, k)

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

 

3 x 3 크기 구조화 커널을 생성하고 침식 연산을 적용합니다. 결과 화면을 보면 글씨가 전반적으로 홀쭉해지긴 했지만 작은 흰 점들로 구성된 노이즈가 사라진 것을 알 수 있습니다.

 

■ 팽창 연산

팽창(dilatation)은 침식과 반대로 영상 속 사물의 주변을 덧붙여서 영역을 더 확장하는 연산입니다. 침식 연산과 마찬가지로 구조화 요소 커널을 입력 영상에 적용해서 1로 채워진 영역이 온전히 덮이지 않으면 1로 채워 넣습니다.

  • dst = cv2.dilate(src, kernel [, dst, anchor, iterations, borderType, borderValue]): 모든 인자는 cv2.erode() 함수와 동일
import cv2
import numpy as np

img = cv2.imread('../img/morph_hole.png')

# 구조화 요소 커널, 사각형 (3x3) 생성 ---①
k = cv2.getStructuringElement(cv2.MORPH_RECT, (3,3))
# 팽창 연산 적용 ---②
dst = cv2.dilate(img, k)

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

 

글씨가 좀 더 뚱뚱해지긴 했지만, 글씨 안의 점 노이즈가 사라진 것을 알 수 있습니다.

 

■ 열림과 닫힘, 그 밖의 모폴로지 연산

침식과 팽창 연산은 밝은 부분이나 어두운 부분의 점 노이즈를 없애는 데 효과적이지만, 원래의 모양이 홀쭉해지거나 뚱뚱해지는 변형이 일어납니다. 침식과 팽창 연산을 조합하면 원래의 모양을 유지하면서 노이즈만 제거할 수 있습니다. 침식 연산을 적용하고 나서 팽창 연산을 적용하는 것을 열림(opening) 연산이라 합니다. 열림 연산은 주변보다 밝은 노이즈 제거에 효과적이면서 맞닿아 있는 것으로 보이는 독립된 개체를 분리하거나 돌출된 픽셀을 제거하는 데 좋습니다.

반대로 팽창 연산을 먼저 하고 침식 연산을 나중에 적용하는 연산을 닫힘(closing) 연산이라고 하고 주변보다 어두운 노이즈 제거에 효과적이면서 끊어져 보이는 개체를 연결하거나 구멍을 메우는 데 좋습니다.

팽창한 결과에서 침식한 결과를 빼면 경계만 얻게 되는데, 경계 검출과 비슷한 결과를 얻을 수 있어 이를 그레디언트(gradient) 연산이라고 합니다. 또, 원본에서 열림 연산 결과를 빼면 밝기 값이 크게 튀는 영역을 강조할 수 있고 닫힘 연산 결과에서 원본을 빼면 어두운 부분을 강조할 수 있습니다. 이를 각각 탑햇(top hat)과 블랙 햇(black hat) 연산이라고 합니다.

  • dst = cv2.morphologyEx(src, op, kernel [, dst, anchor, iteration, borderType, borderValue])
    • src: 입력 영상, Numpy 배열
    • op: 모폴로지 연산 종류 지정
      • cv2.MORPH_OPEN: 열림 연산
      • cv2.MORPH_CLOSE: 닫힘 연산
      • cv2.MORPH_GRADIENT: 그레디언트 연산
      • cv2.MORPH_TOPHAT: 탑햇 연산
      • cv2.MORPH_BLACKHAT: 블랙햇 연산
    • kernel: 구조화 요소 커널
    • dst: 결과 영상
    • anchor: 커널의 기준점
    • iteration: 연산 반복 횟수
    • borderType: 외곽 보정 방식
    • borderValue: 외곽 보정 값
import cv2
import numpy as np

img1 = cv2.imread('../img/morph_dot.png', cv2.IMREAD_GRAYSCALE)
img2 = cv2.imread('../img/morph_hole.png', cv2.IMREAD_GRAYSCALE)    

# 구조화 요소 커널, 사각형 (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)

# 결과 출력
merged1 = np.hstack((img1, opening))
merged2 = np.hstack((img2, closing))
merged3 = np.vstack((merged1, merged2))
cv2.imshow('opening, closing', merged3)
cv2.waitKey(0)
cv2.destroyAllWindows()

 

팽창과 침식과 달리 뚱뚱해지거나 홀쭉해지지 않고 원래의 크기를 그대로 유지한 채 노이즈를 제거하는 모습입니다.

 

6.4 이미지 피라미드

이미지 피라미드(image pyramids)는 영상의 크기를 단계적으로 축소 또는 확대해서 피라미드처럼 쌓아 놓는 것을 말합니다. 이미지를 작은 크기로 빠르게 훑어보고 다음 단계 크기의 영상으로 분석하는 식으로 정확도를 높이는 것이 효율적이기 때문에 이미지 피라미드가 사용됩니다.

 

■ 가우시안 피라미드

가우시안 필터를 적용한 후에 이미지 피라미드를 구현하는 것을 가우시안 피라미드라 합니다.

  • dst = cv2.pyrDown(src [, dst, dstsize, borderType])
  • dst = cv2.pyrUp(src, [, dst, dstsize, borderType])
    • src: 입력 영상, Numpy 배열
    • dst: 결과 영상
    • dstsize: 결과 영상 크기
    • 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

# 결과 출력
cv2.imshow('img', img)
cv2.imshow('pyrDown', smaller)
cv2.imshow('pyrUp', bigger)
cv2.waitKey(0)
cv2.destroyAllWindows()

 

■ 라플라시안 피라미드

 cv2.pryUp() 함수는 4배로 확대할 때 없던 행과 열을 생성해서 필터를 적용하므로 원본 영상만큼 완벽하지 못합니다. 그래서 cv2.pryDown() 함수로 한 단계 작아진 영상을 cv2.pryUp() 함수로 확대해도 원본 영상을 완벽히 복원할 수 없습니다.

원본 영상에서 cv2.pryUp()으로 확대한 영상을 빼면 그만큼이 바로 원본과 확대 본의 차이가 되는데, 이를 보관해 두었다가 확대 영상에 더하면 원본을 완벽히 복원할 수 있습니다.

원본과 cv2.pryUp() 함수를 적용한 영상의 차이를 단계별로 모아두는 것을 라플라시안 피라미드라고 합니다.

 

import cv2
import numpy as np

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

# 원본 영상을 가우시안 피라미드로 축소
smaller = cv2.pyrDown(img)
# 축소한 영상을 가우시안 피라미드로 확대
bigger = cv2.pyrUp(smaller)

# 원본에서 확대한 영상 빼기
laplacian = cv2.subtract(img, bigger)
# 확대 한 영상에 라플라시안 영상 더해서 복원
restored = bigger + laplacian

# 결과 출력 (원본 영상, 라플라시안, 확대 영상, 복원 영상)
merged = np.hstack((img, laplacian, bigger, restored))
cv2.imshow('Laplacian Pyramid', merged)
cv2.waitKey(0)
cv2.destroyAllWindows()

 

더 또렷한 영상이 복원된 것을 볼 수 있습니다. 라플라시안 피라미드는 영상의 복원의 목적뿐만 아니라 경계 검출에도 활용할 수 있습니다.

'Deep Learning(강의 및 책) > OpenCV' 카테고리의 다른 글

[OpenCV] 영상 매칭과 추적  (0) 2022.08.20
[OpenCV] 영상 분할  (0) 2022.08.04
[OpenCV] 기하학적 변환  (0) 2022.07.30
[OpenCV] 이미지 프로세싱 기초  (0) 2022.07.23
[OpenCV] 기본 입출력  (0) 2022.07.11