Edge Detection는 이름에서도 볼 수 있듯 영상에서 밝기나 색상이 급격히 변하는 경계 선을 찾아내는 기술, 즉 Edge를 검출하는 기술이다.
Edge를 픽셀의 밝기가 급격히 변화하는 부분으로 가정하면, 이를 위해 미분(derivative)과 기울기(gradient) 연산을 수행해 픽셀의 밝기 변화율이 높은 부분을 에지로 볼 수 있다.
오늘은 이렇게 에지를 찾아내는 Edge Detection의 종류에 대해 알아볼 것이다.
이를 그림을 통해 알아보면 더 이해하기 쉽다. f(x)에 대해 1차 미분을 진행하면 상승 에지와 하강 에지가 발생하는 부분에서만 값의 변화가 발생하는 것을 확인할 수 있다. 이처럼 1차 미분에서의 극값이 Edge를 나타낸다.
일반적으로, 영상에서는 1픽셀을 기준으로 했을 때, Δx = 1로 두고 우측/상단 픽셀 – 현재 픽셀과 같이 변화량을 구해 미분한다.
이를 마스크를 정의하면 아래의 그림과 같다. (숫자가 표시되지 않은 부분은 모두 0으로 간주한다) 이 마스크를 통해 컨볼루션하게 되면, 1차 미분의 결과가 나온다.
이 두 개의 필터는 각각 Roberts와 Prewitt이라는 사람이 에지 검출을 위한 1차 미분 마스크 형태를 정의해 둔 것을 의미한다.
Roberts는 2*2 형태로 마스크를 만들었고, 기준점이 되는 위치의 왼쪽 위에서 기준점의 위치를 빼거나, 오른쪽 위에서 기준점의 위치를 빼도록 설계했다.
이는 다른 마스크보다 크기는 작지만 효과적으로 사용할 수 있다는 장점이 있으나 노이즈에 민감하다는 단점이 존재한다.
Prewitt은 중앙값이 되는 위치를 기준으로 좌우 또는 상하 픽셀의 값 차이를 계산하도록 설계했다. Prewitt이 설계한 필터는 Roberts가 설계한 필터보다 고려하는 픽셀들이 더 많은데, 그렇기 때문에 노이즈에 더 강하다는 장점이 있다.
이를 코드로 한 번 살펴보자. 이때, Roberts Filter과 Prewitt Filter는 OpenCV에서 기본적으로 제공하는 필터가 아니기 때문에 마스크를 정의해야 한다.
import cv2
from skimage import data
import numpy as np
from google.colab.patches import cv2_imshow
image = data.camera()
rob_x = np.array([[0, 0, -1], [0, 1, 0], [0, 0, 0]])
rob_y = np.array([[-1, 0, 0], [0, 1, 0], [0, 0, 0]])
pre_x = np.array([[1, 0, -1], [1, 0, -1], [1, 0, -1]])
pre_y = np.array([[-1, -1, -1], [0, 1, 0], [1, 1, 1]])
# cv2.filter2D(input, ddepth, mask) - input 이미지에 대해 특정 필터 적용 (이때, depth는 이미지 깊이를 나타내며 -1이면 입력 이미지와 동일하게 설정하겠다는 의미!)
# cv2.convertScaleAbs(input) - 필터링 과정에서 발생하는 음수값 처리를 위해 절대값을 취함 (0~255 범위로 조정)
rob_x_image = cv2.convertScaleAbs(cv2.filter2D(image, -1, rob_x))
rob_y_image = cv2.convertScaleAbs(cv2.filter2D(image, -1, rob_y))
pre_x_image = cv2.convertScaleAbs(cv2.filter2D(image, -1, pre_x))
pre_y_image = cv2.convertScaleAbs(cv2.filter2D(image, -1, pre_y))
cv2_imshow(image)
cv2_imshow(rob_x_image)
cv2_imshow(rob_y_image)
cv2_imshow(pre_x_image)
cv2_imshow(pre_y_image)
코드의 결과를 살펴보면 Roberts가 설계한 필터는 작은 크기의 마스크를 활용해 에지의 세밀하고 얇은 부분을 잘 나타나는 경향이 있음을 확인할 수 있다.
* 참고로... 귀찮아서 x 방향 에지 강조 결과만 가져왔다.
Sobel Filter는 Prewitt Filter와 유사하지만, 중심 픽셀에 가까운 점에서 영향력을 더 높게 고려하도록 설계된 필터이다. 쉽게 말해, 중앙점으로부터 가까울수록 가중치를 더 높게 준다. 가우시안+미분이라고 이해하면 된다.
이를 코드로 살펴보면 다음과 같다.
import cv2
from skimage import data
import numpy as np
from google.colab.patches import cv2_imshow
image = data.camera()
# cv2.Sobel(src, ddepth, dx, dy, ksize)
# dx, dy는 각 방향에서의 미분 차수로 1로 두면 그 방향을 고려해 미분을 수행하겠다는 의미임.
sobel_x = cv2.Sobel(image, -1, 1, 0, ksize=3)
sobel_y = cv2.Sobel(image, -1, 0, 1, ksize=3)
sobel_xy = cv2.Sobel(image, -1, 1, 1, ksize=3)
cv2_imshow(sobel_x)
cv2_imshow(sobel_y)
cv2_imshow(sobel_xy)
세 결과의 차이는 각각 x 방향을 강조했는지, y 방향을 강조했는지, xy 방향을 모두 고려하여 강조했는지의 차이다. 두 방향을 모두 고려하면 한 방향만 고려했을 때보다 노이즈의 영향을 덜 받을 수 있음을 알 수 있다. 이렇게 Sobel Filter를 활용해 에지를 쉽게 검출할 수 있다.
Scharr Filter는 Sobel Filter와 동일하지만 Mask Size가 3*3으로 고정되어 있으며, Sobel보다 좀 더 정확하게 적용하고자 할 때 사용한다. (왜냐하면 Sobel에 비해 미스크 안의 값들이 커서 밝기 값 차이가 있는 부분에서의 차이가 Sobel보다 더 두드러진다)
이를 코드로 살펴보면 다음과 같다.
import cv2
from skimage import data
import numpy as np
from google.colab.patches import cv2_imshow
image = data.camera()
# Scharr 필터는 기본적으로 x와 y 방향을 따로 적용하므로 두 방향에서의 엣지를 결합하여 결과를 얻고 싶으면 두 결과를 합쳐야 한다.
# sch_imgXY = cv2.Scharr(image, -1, 1, 1) 과 같이 코드를 작성하면 오류가 발생하니 주의하자!
sch_imgX = cv2.Scharr(image, -1, 1, 0)
sch_imgY = cv2.Scharr(image, -1, 0, 1)
cv2_imshow(sch_imgX)
cv2_imshow(sch_imgY)
Sobel Filter보다 밝기 차이가 더 두드러지는 결과를 확인할 수 있다.
1차 미분을 한 뒤, 1차 미분의 결과를 다시 미분한 것을 2차 미분이라고 이야기한다. 이러한 2차 미분을 활용한 필터 중 대표적인 것이 바로 Laplacian Filter이다.
Laplacian을 마스크로 정의했을 때, 다음과 같은 그림이 된다. 이는 좌우상하가 같은 구조를 지니기 때문에 동형(isotropic) 필터에 해당한다고 이야기하기도 한다.
이러한 Laplacian 필터는 주변 픽셀과 중심 픽셀의 차이를 계산하는 것이기 때문에 의도치 않게 노이즈가 강조될 수 있다. 그래서 필터를 적용하기 전에 가우시안 블러링과 같은 처리를 통해 노이즈에 대한 영향을 최소화하는데, 이를 Laplacian of Gaussian(LoG)라고 한다.
Laplacian을 사용하면 Sobel이나 Prewitt을 적용했을 때보다 에지가 더 잘 검출되는 것을 확인할 수 있다.
이를 코드로 구현하면 다음과 같다. Laplacian은 이미 이전 글에서 Sharpening에 대해 공부할 때 한 번 써본 적이 있으므로 이번엔 조금 새롭게 LoG로 구현했다.
import cv2
import numpy as np
from skimage import data
from google.colab.patches import cv2_imshow
image = data.camera()
blurred = cv2.GaussianBlur(image, (3, 3), 1)
laplacian = image = cv2.Laplacian(blurred, cv2.CV_64F)
laplacian = cv2.convertScaleAbs(laplacian)
cv2_imshow(laplacian)
코드와 결과를 살펴보면, Sobel이나 Prewitt과 같은 1차 미분 필터와 같이 어느 방향을 고려해야 할지 지정하지 않아도 되고, 노이즈에 대해서도 저항성을 지녀 Sobel이나 Prewitt보다 더 나은 결과를 확인할 수 있다는 것을 알 수 있다.
Canny Edge Detection는 가장 대표적인 에지 검출 방법으로, 단순 컨볼루션으로 이뤄지는 연산이 아니라 내부적으로 컨볼루션이 일부 활용된 알고리즘이라고 생각하면 된다. 더 자세히 이야기하면, 최소 오류율과 검출된 에지의 높은 위치 정확도, 실제 에지에 해당하는 곳의 얇은 에지 두께라는 기준을 충족하도록 설계한 알고리즘라고 정의할 수 있다.
Canny Edge Detection은 크게 네 가지 단계를 거친다.
Edge Gradient Detection
Non-maximum Suppression
이를 코드로 한 번 살펴보자. OpenCV에서는 이렇게 복잡한 Canny Edge Detection의 과정을 하나의 함수에 정의해 두고 있다.
import cv2
import numpy as np
from skimage import data
from google.colab.patches import cv2_imshow
image = data.camera()
# cv2.Canny(input, 최소임계값, 최대임계값)
canny1 = cv2.Canny(image, 10, 50)
canny2 = cv2.Canny(image, 150, 300)
cv2_imshow(canny1)
cv2_imshow(canny2)
결과에서 볼 수 있듯, 최소 임계값이 작으면 작을수록, 최소 임계값과 최대 임계값 사이의 차이가 작을수록 결과에서 잘못된 Edge가 보일 가능성이 높아짐을 알 수 있다.
반면에, 최소 임계값의 크기가 크고, 최소 임계값과 최대 임계값 사이의 차이가 크면 좀 더 정확도 있게 Edge를 판별할 수 있음을 확인할 수 있다.