귀퉁이 서재

OpenCV - 18. 경계 검출 (미분 필터, 로버츠 교차 필터, 프리윗 필터, 소벨 필터, 샤르 필터, 라플라시안 필터, 캐니 엣지) 본문

OpenCV

OpenCV - 18. 경계 검출 (미분 필터, 로버츠 교차 필터, 프리윗 필터, 소벨 필터, 샤르 필터, 라플라시안 필터, 캐니 엣지)

Baek Kyun Shin 2020. 10. 8. 00:01

이번 포스팅부터는 경계를 검출하는 방법에 대해 알아보겠습니다. 이번 포스팅 역시 '파이썬으로 만드는 OpenCV 프로젝트(이세우 저)'를 정리한 것임을 밝힙니다.

코드: github.com/BaekKyunShin/OpenCV_Project_Python/tree/master/06.filter

지금까지는 영상을 흐릿하게 하는 블러링에 대해 알아봤습니다. 이번 포스팅에서는 반대로 영상의 경계를 뚜렷하게 만드는 방법에 대해 알아보겠습니다. 영상의 경계를 선명하고 뚜렷하게 만드는 작업을 샤프닝(sharpening)이라고 합니다. 샤프닝은 영상에서 경계를 검출하여 경계에 있는 픽셀을 강조합니다.

기본 미분 필터

경계(엣지)를 검출하기 위해서는 픽셀 값이 급격하게 변하는 지점을 찾아야 합니다. 경계 부분에서는 당연히 픽셀 값이 급격히 변하겠죠? 이는 연속된 픽셀 값에 미분을 하여 찾아낼 수 있습니다. 하지만 픽셀은 연속 공간 안에 있지 않으므로 미분 근사값을 구해야 합니다. 미분 근사값은 간단합니다. 서로 붙어 있는 픽셀 값을 빼면 됩니다. x방향, y방향으로 각각 픽셀 값을 빼면 미분 근사값이 됩니다. 

영상 내 픽셀 값의 미분 근사값 연산을 위한 컨볼루션 커널은 아래와 같습니다. 요소가 -1과 1인 이유는 단지 접해있는 픽셀 값을 빼기 때문입니다.

이 미분 컨볼루션 커널을 이용하여 경계를 검출해보겠습니다.

# 미분 커널로 경계 검출 (edge_differential.py)

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방향의 미분 커널을 생성하여 필터링을 적용했습니다. x방향 미분 필터는 세로 방향의 경계를 검출했고, y방향 미분 필터는 가로 방향의 경계를 검출했습니다. x방향 미분 필터는 좌우 픽셀 값의 차를 기반으로 필터링했기 때문에 세로 방향의 경계를 검출한 것이고, 반대로 y방향 미분 필터는 상하 픽셀 값의 차를 기반으로 필터링했기 때문에 가로 방향의 경계를 검출한 것입니다.

로버츠 교차 필터 (Roberts Cross Filter)

로렌스 로버츠라는 미국 엔지니어는 기본 미분 필터를 개선한 로버츠 교차 필터를 제안했습니다. 로버츠 교차 필터를 위한 컨볼루션 커널은 아래와 같습니다.

이 커널은 대각선 방향으로 +1과 -1을 배치시켜 사선 경계 검출 효과를 높였습니다. 하지만 노이즈에 민감하다는 단점이 있습니다.

# 로버츠 교차 필터를 적용한 경계 검출 (edge_roberts.py)

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

프리윗 필터 (Prewitt Filter)

프리윗 필터는 x축과 y축의 각 방향으로 차분을 세 번 계산하여 경계를 검출하는 필터입니다. 프리윗 필터는 상하/좌우 경계는 뚜렷하게 잘 검출하지만 대각선 검출이 약합니다.

# 프리윗 마스크를 적용한 경계 검출 (edge_prewitt.py)

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

확실히 기본 미분 필터나 로버츠 교차 필터에 비해 상하/좌우 경계 검출 강도가 강합니다. 마지막은 상하/좌우 경계 검출 필터로 필터링한 결과를 합친 것입니다.

소벨 필터 (Sobel Filter)

소벨 필터는 중심 픽셀의 차분 비중을 두 배로 준 필터입니다. 따라서 소벨 필터는 x축, y축, 대각선 방향의 경계 검출에 모두 강합니다.

앞서 설명한 로버츠 필터와 프리윗 필터는 현재는 거의 쓰이지 않습니다. 반면 소벨 필터는 실무적으로도 쓰이므로 OpenCV에서 별도의 함수를 제공합니다.

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

아래는 소벨 필터를 적용한 예제 코드입니다.

# 소벨 마스크를 적용한 경계 검출 (edge_sobel.py)

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

소벨 필터는 커널의 중심에서 멀어질수록 엣지 방향성의 정확도가 떨어집니다. 이를 개선한 필터가 샤르 필터입니다. 샤를 필터를 위한 컨볼루션 커널은 아래와 같습니다.

  • dst = cv2.Scharr(src, ddepth, dx, dy, dst, scale, delta, borderType)
    ksize가 없다는 것을 제외하면 모든 파라미터는 cv2.Sobel()과 동일합니다.
# 샤르 마스크를 적용한 경계 검출 (edge_scharr.py)

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

라플라시안 필터 (Laplacian Filter)

라플라시안 필터는 2차 미분을 적용한 필터입니다. 경계를 더 제대로 검출할 수 있습니다.

  • dst = cv2.Laplacian(src, ddepth, dst, ksize, scale, delta, borderType)
    파라미터는 cv2.Sobel()과 동일합니다.
# 라플라시안 마스크를 적용한 경계 검출 (edge_laplacian.py)

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

확실히 경계가 뚜렷하네요.

캐니 엣지 (Canny Edge)

캐니 엣지는 지금까지 살펴본 것처럼 한 가지 필터만 사용하는 것이 아니라 다음의 4단계 알고리즘에 따라 경계를 검출합니다. 

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

OpenCV에서 제공하는 캐니 엣지는 함수는 아래와 같습니다.

  • edges = cv2.Canny(img, threshold1, threshold2, edges, apertureSize, L2gardient)
    img: 입력 영상
    threshold1, threshold2: 이력 스레시홀딩에 사용할 Min, Max 값
    apertureSize: 소벨 마스크에 사용할 커널 크기
    L2gradient: 그레디언트 강도를 구할 방식 (True: 제곱 합의 루트 False: 절댓값의 합)
    edges: 엣지 결과 값을 갖는 2차원 배열
# 캐니 엣지 검출 (edge_canny.py)

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

확실히 하나의 필터를 적용한 것보다 경계 검출이 잘 됩니다. 그만큼 많이 쓰이는 필터입니다.

 

지금까지 여러 필터를 활용하여 경계를 검출하는 방법에 대해 알아봤습니다.

Comments