공간 필터링(Spacial filtering)

Seungpil Choi·2022년 7월 23일
0

공간 필터링(Spacial filtering)

공간 필터링이란?

공간 필터링이란 영상 처리 중에서 가장 기본적인 처리로써 인접화소들의 값을 참조하여 화소의 값을 변경하는 처리이다. "공간영역"이라 칭하는 이유는 "주파수 영역(frequency domain)"과 차이를 두기 위해서이다. 화소들이 존재하는 2차원 공간에서 필터링을 수행한다는 의미이다.

컨볼루션(convolution)

필터링은 컨볼루션이라는 연산을 이용하여 쉽게 구현할 수 있다. 컨볼루션은 중심 화소의 값을 인접 화소값들의 가중 합으로 대체하는 연산이다. 가중치는 작은 2차원 배열로 주어진다. 가중치가 저장된 배열은 흔히 커널(kernel), 필터(filter), 마스크(mask) 라 불린다. 일반적으로 마스크의 크기는 3X3, 5X5, 7X7과 같이 홀수이다. 이 중 3X3, 5X5이 공간필터링에서 많이 사용된다.

컨볼루션은 상당히 시간이 많이 걸리는 처리이다. 따라서 계산을 효율적으로 하는 것이 중요하다. 가능하다면 OpenCV가 제공하는 컨볼루션 전용 함수인 filter2D()를 사용하는 것이 좋다. 마스크안의 가중치의 합은 출력 영상의 전체적인 밝기에 영향을 준다. 우리는 영상의 밝기가 그대로 유지되기를 원하므로 보통 가중치의 합은 1로 한다.

filter2D()함수로 평균값 필터링(mean filtering 또는 blurring) 구현

  • 매개변수
    • src : 입력 영상이다.
    • ddepth : 영상의 깊이(8, 16, 32 등)이다. 음수이면 결과 영상이 원본 영상과 같은 비트수를 가진다는 의미이다.
    • kernel : 입력 영상을 컨볼루션하는 데 사용되는 마스크, 마스크는 다일 채널이고 부동 소수점 행렬이어야 한다.
    • anchor : 커널 행렬에서 중심점의 위치. 이 값을 (-1,-1)로 설정하면 커널 중심이 중심점으로 사용된다.
    • delta : 컨볼루션 후에 모든 화소에 추가되는 값이다.
    • borderType : 영상의 가장자리 화소들을 처리하는 방법으로 사용가능한 값은 다음과 같다.
      • BORDER_REPLICATE
      • BORDER_CONSTANT
      • BORDER_REFLECT_101
      • BORDER_WARP
      • BORDER_TRANSPARENT
      • BORDER_DEFAULT
      • BORDER_ISOLATED
img = cv2.imread('lena.png')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(img)

mask = np.full((3,3), 1/9, dtype=np.float32)
blur = cv2.filter2D(img, -1, mask)
plt.imshow(blur)

가장자리 화소 처리

컨볼루션 함수를 구현할 때 첫 번째 닥치는 어려움은 영상의 가장자리 부분을 어떻게 처리하느냐하는 것이다. 첫 번째 방법(BORDER_CONSTANT)은 컨볼루션 마스크 내의 비어있는 값들을 모두 0이라고 가정하는 것이다. 이 방법은 0-패딩이라고 알려져 있다. 이것은 간단하지만 출력 영상의 에지 부분이 중요하다면 좋은 방법은 아니다. 두번째 방법(BORDER_REPLICATE)은 가장자리 값을 복사하여 사용하는 것이다. 이 두가지 방법이 가장 많이 사용된다. 이것은 filter2D()함수의 borderType매개변수로 설정이 가능하다.

  • BORDER_REPLICATE => iiiiii|abcdefgh|iiiiii 여기서 i는 지정된 값
  • BORDER_CONSTANT => aaaaaa|abcdefgh|hhhhhh
  • BORDER_REFLECT => fedcba|abcdefgh|hgfedcb
  • BORDER_WARP =>cdefgh|abcdefgh|abcdefg

평균값 필터링(mean filtering 또는 blurring)

앞서 평균값 필터링을 filter2D() 함수로 구현 했지만 평균값 필터링은 아주 많이 사용되기 때문에 OpenCV는 전용함수 blur()을 제공한다.

img = cv2.imread('lena.png')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(img)

blur = cv2.blur(img, (3,3))
plt.imshow(blur)

가우시안 스무딩

앞에서는 사각형 형태의 필터를 사용하였다. 하지만 사각형 필터 대신 가우시안 커널(Gaussian kernel) 을 사용하는 경우도 많다. 가우시안 커널이란 다음과 같이 모자처럼 생긴 커널이다. OpenCV에서는 GaussianBlur() 가 지원된다. 커널의 크기는 지정할 수 있고 홀수여야한다. 가우시안 스무딩은 영상에서 가우시안 잡음을 제거하는 데 매우 효과적이다.

  • 가우시안 잡음(Gaussian Noise)
    • 영상 신호의 변동에 의해 발생하는 잡음이다. 자연스러운 잡음 생성 과정을 모델링 하는데 주로 사용한다. 평균값을 중심으로 표준편차 ±σ 범위 내에 모든 잡음 값의 68%, 그리고 ±2σ 범위 내에 95%의 잡음값이 존재한다.
img = cv2.imread('lena.png')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(img)

gaussianblur_img = cv2.GaussianBlur(img, (5,5), 0, 0)
plt.imshow(gaussianblur_img)

샤프닝(Sharpening)

샤프닝은 평균값 필터링과 반대이다. 평균값 필터링이 영상을 부드럽게 만드는 처리이라면 샤프닝은 영상을 날카롭게 만드는 처리이다. 영상을 선명하게 할 때 사용된다. 의료 분야나 군사 분야, 산업용 검사 분야에서 특히 중요하다.

이 마스크를 적용하면 중심 화소값과 인접 화소값의 차이를 더 크게 만든다. 첫 번째로 마스크의 모든 계수를 다 더하면 1이 된다는 것을 알 수 있다. 따라서 영상의 전체 밝기는 변경되지 않는다. 샤프닝 필터링은 영상 안의 고주파 성분을 강화하기 때문에, 출력 영상은 더 날카로운 영상이 된다. 샤프닝 필터링은 잘못된 초점에 의해 흐려진 세부사항을 더욱 잘 보이게 할 수 있다. 하지만 잡음도 증가되는 경향이 있다. 샤프닝은 OpenCV함수인 filter2D()를 사용하여 구현할 수 있다.

img = cv2.imread('lena.png')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(img)

mask = np.asarray([[-1,-1,-1],
                   [-1, 9,-1],
                   [-1,-1,-1]],dtype=np.float32)

sharpening_img = cv2.filter2D(img, -1, mask)
plt.imshow(sharpening_img)

에지(edge) 검출

에지는 영상에서 화소의 밝기가 급격하게 변하는 부분이다. 에지는 영상 안에서 상당한 밝기의 차이가 있는 곳이고 이것은 대개 물체의 윤곽선(경계선)에 해당한다. 따라서 에지를 검출할 수 있으면 물체의 윤곽선을 알 수 있다. 에지 검출은 오래전부터 활발히 연구된 분야이지만 모든 종류의 영상에 대하여 완벽하게 에지를 검출하는 방법은 아직은 없다. 하지만 "캐니 에지 검출" 방법처럼 상당한 수준으로 에지를 신뢰성 있게 검출하는 방법들이 존재한다. 각 에지 검출 방법은 나름대로의 장단점을 가지고 있다.

에지 검출 방법

에지 검출 방법을 크게 나누어 보면 1차 미분값을 이용한 방법, 2차 미분값을 이용한 방법, 그밖의 방법으로 나눌 수 있다. 이들은 대부분 컨볼루션을 통하여 수행될 수 있다.
1차 미분을 이용한 에지 검출 방법에는 차분(differnece) 필터, 소벨(sobel) 필터, 로버츠(Roberts) 필터, 커쉬(Kirsch) 필터, 로빈슨(Robinson) 필터, 프레위트(Prewitt) 필터 등이 있다. 이러한 에지 검출 연산은 모두 화소값의 기울기(slope)를 기초로 한다.

1차 미분을 이용한 에지 검출

에지가 있는 곳은 함수의 순간 변화율 이기 때문에 에지에서는 미분값이 커지게 된다. 따라서 영상의 각 화소 위치에서 미분값을 계산하여서 미분값이 일정 수치 이상인 곳을 찍으면 아마도 그곳이 에지일 것이다. 영상은 1차원이 아니고 2차원이기 때문에 어떤 위치에서의 미분값은 2개가 존재한다. 즉, x축 상의 미분값과 y축 상의 미분값이 존재한다. 수학에서는 이것을 그라디언트(gradient) 라고 한다.

  • 그라디언트는 다음과 같이 벡터로 정의된다.
    • G[f(x,y)] = [Gx, Gy] = [df/dx, df/dy]
    • 여기서 Gx는 x축의 미분값이고 Gy는 y축 상의 미분값이다.
  • 그라디언트는 스칼라가 아니고 벡터이기 때문에 크기와 방향이 존재하고 다음 식으로 계산할 수 있다.
    • G[f(x,y)] = (Gx^2 + Gy^2)^(1/2)
    • α(x,y) = tan(Gy/Gx)^(-1)

이제 문제는 어떻게 Gx와 Gy를 계산할 것이냐이다. Gx와 Gy는 근본적으로 x방향과 y방향으로 화소값이 변화하는 정도(비율)이므로 밝기 변화를 검출할 수 있는 마스크를 사용하여서 컨볼루션하면 된다. 즉 컨볼루션을 이용하여 계산할 수 있다.

많이 사용되는 필터

밑의 필터들 중 Sobel 필터가 가장 많이 사용된다. 이 필터는 마스크의 중심에 위치한 화소에 더 강조를 둔다.

  • Roberts 필터

  • Prewitt 필터

  • Sobel 필터

  • Sobel() 매개변수

    • src : 입력영상.
    • ddepth : 출력 영상의 깊이.
    • dx : x방향으로 몇 차 미분값인지 표시한다.
    • dy : y방향으로 몇 차 미분값인지 표시한다.
    • ksize : 마스크의 크기 (디폴트는 3이다.)
    • scale, delta, BORDER_DEFAULT : 기본값을 사용한다.
img = cv2.imread('lena.png')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(img)

# x방향과 y방향으로 미분값을 계산한다.
grad_x = cv2.Sobel(img, -1, 1, 0, ksize=3)
grad_y = cv2.Sobel(img, -1, 0, 1, ksize=3)

# 처리 결과를 CV_8U형태로 변환한다.
abs_grad_x = cv2.convertScaleAbs(grad_x)
abs_grad_y = cv2.convertScaleAbs(grad_y)

# 2개 행렬의 가중치 합을 계산
grad = cv2.addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0)
plt.imshow(grad)

addWeight()함수는 그라디언트의 수학적인 정의와는 차이가 있는데 근사값만 계산하여도 되기 때문에 큰 문제는 없다.
G[f(x,y)] = (Gx^2 + Gy^2)^(1/2)G[f(x,y)] = 0.5 * Gx + 0.5 * Gy로 근사하여 계산한다.

2차 미분 에지 연산자

앞에서 논의된 에지 검출 방법은 어떤 위치에서 그라디언트를 계산하여서 그 크기가 임계값을 넘으면 에지가 존재하는 것으로 간주하는 방식이다. 이 방법은 너무 많은 에지점을 생성한다는 단점이 있다. 더 개선된 방법은 그라디언트값이 국지적으로 최대인 점만을 에지로 인정하는 것이다. 하지만 2차 미분값은 잡음에 약하기 때문에 2차 미분값 0은 의미 없는 위치에도 나타날 수 있다. 따라서 잡음을 제거하는 필터링이 필요하며 0이 되는 지점보다는 양에서 음으로, 음에서 양으로 변화하는점, 즉 2차 미분값의 영점 통과지점(zero-crossing)을 찾으면 보다 신뢰성 있게 에지를 검출할 수 있다. 대표적인 2차 미분 에지 연산자는 라플라시안(Laplacian)이다.

그라디언트는 벡터이지만 라플라시안은 스칼라이다. 즉 방향이 없이 크기만 존재한다. 라플라시안은 다른 연산자와 달리 모든 방향의 에지를 찾는다. 라플라시안은 OpenCV에서 Laplacian() 함수로 구현된다. 라플라시안은 그라디언트를 사용하기 때문에 Sobel연산자를 내부적으로 호출하여 계산한다.

  • Laplacian() 매개변수
    * src : 입력영상
    • ddepth : 출력 영상의 깊이
    • kernel_size : sobel 연산자의 윈도우 크기
    • scale, delta, borderType : 기본값을 사용함, scale을 키우면 에지가 더 많이 검출됨
img = cv2.imread('lena.png')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(img)

img = cv2.GaussianBlur(img, (5,5), 0, 0)

dst = cv2.Laplacian(img, -1, ksize=3, scale=1, delta=0, borderType=cv2.BORDER_DEFAULT)
plt.imshow(dst)

캐니 에지 연산자

캐니 에지 검출은 널리 사용되고 상당히 신뢰성 있는 에지 검출 알고리즘이다. 캐니 알고리즘은 OpenCV에서 Canny() 함수로 구현되어있으며 다단계 알고리즘이면서 다음과 같은 단계를 거치게 된다.

Step 1 잡음 억제

에지를 검출하기전에 5X5 가우시안 스무딩 필터를 사용하여 영상의 잡음을 제거한다.

Step 2 그라디언트 계산하기

Sobel 마스크로 수평 및 수직 방향의 1차 미분값을 얻는다. 이들 영상으로부터 각 화소에 대한 그라디언트 크기 및 방향을 구할 수 있다. 그라디언트 방향은 에지에 수직이다. 그라디언트 방향은 수직, 수평, 대각선 방향을 나타내는 4개의 각도 중 하나로 반올림된다. 즉 0, 45, 90, 135 중의 하나로 된다.

Step 3 비최대 억제

그라디언트의 크기와 방향을 얻은 후 불필요한 에지를 제거하기 위해 그라디언트의 값이 그라디언트 방향의 인접 화소 중에서 최대값인지를 확인한다. 최대값만 남기고 나머지는 제거한다. 이 단계가 종료되면 "얇은 에지"를 얻을 수 있다.

Step 4 히스테리시스

이 단계에서는 모든 에지 후보들이 실제 에지인지 아닌지를 결정한다. 이를 위해 두 개의 임계값인 minVal과 maxVal이 필요하다. 그라디언트의 크기가 maxVal보다 큰 에지 후보는 확실한 에지라고 간주된다. 그라디언트의 크기가 minVal보다 작은 에지 후보는 에지가 아니므로 제거되어야 한다. 이 두 임계값 사이에 있는 에지 후보들은 연결성에 따라 에지 또는 비 에지로 분류된다. 화소가 "확실한" 에지에 연결되면 에지의 일부로 간주된다. 그렇지 않으면 에지 후보는 삭제된다.

OpenCV에서의 캐니 에지 검출

  • Canny() 매개변수
    • src_image : 입력영상
    • lowTreshold : 하위 임계값
    • highTreshold : 상위 임계값
    • kernel_size : sobel마스크의 크기
img = cv2.imread('lena.png')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(img)

med_val = np.median(img)
#하한 임계값을 0 또는 중앙의 70%로 설정 
lower = int(max(0, 0.7*med_val))
#상향 임계값을 130% 또는 255로 설정
upper = int(min(255, 1.3*med_val))

detected_edges = cv2.GaussianBlur(img, (3,3), 0, 0)
detected_edges = cv2.Canny(detected_edges, lower, upper, 3)
plt.imshow(detected_edges)

중간값 필터링(Median filtering)

화소값들의 가중합으로 계산할 수 없는 또 다른 공간 필터가 있는데, 이들을 비선형 공간 필터라한다. 비선형 공간 필터의 한 예로, 중간값(median) 필터가 있다. 중간값 필터는 영상으로 충격 잡음을 제거하는 데 적당하다. 충격 잡음은 주로 통신 선로의 문제로 발생한다.
영상의 잡음을 제거하는 필터로는 앞서 나온 스무딩 필터가 있다. 여기서 스무딩 필터와 중간값 필터를 비교하면 스무딩 필터에서는 에지가 흐려지는 단점이 나타났지만, 중간값 필터링에서는 이런 단점없이 잡음이 제거된다.

소금-후추 잡음(salt and pepper) 만들기

소금-후추 잡음이란 통신 선로의 문제로 특정 화소가 0 또는 255로 바뀌는 잡음이다.

img = cv2.imread('lena.png', 0)
noise_img = np.zeros(img.shape, dtype=np.uint8)

cv2.randu(noise_img, 0, 255)
black_img = noise_img < 10
white_img = noise_img > 245

img[black_img==True] = 0
img[white_img==True] = 255

plt.imshow(img, cmap="gray")

OpenCV에서의 중간값 필터링

OpenCV에서는 medianBlur() 함수가 중간값 필터링을 지원한다.
아래는 앞서 만든 노이즈 이미지에 대하여 중간값 필터링을 실행한 것이다.

dst = cv2.medianBlur(img, 5)
plt.imshow(dst, cmap='gray')

0개의 댓글