엣지(Edge) 검출
개요
- Edge란 가장자리 또는 윤곽선을 의미하며, 영상에서 밝기값이 급격하게 변하는 지점을 의미함.
- 영상에서 보통 물체의 윤곽선, 패턴의 경계, 질감 변화 등에서 나타나며, 엣지 검출은 컴퓨터 비전에서 객체 인식, 분할, 특징 추출의 기본 전처리 단계로 사용할 수 있음.
엣지 검출의 기본 원리
- 영상의 밝기 함수 에서 기울기(Gradient) 크기가 큰 영역이 엣지.
- 미분 연산(픽셀 값 차이) → 큰 변화값 검출.
- 기울기 벡터 :
- 기울기 크기 :
- 방향 :
기울기
- 위와 같이 픽셀값의 변화율은 미분을 통해 구할 수 있으나, 픽셀 값은 행렬로 나타나 있기 때문에 연속함수가 아닌 이산함수.
- 이산함수는 미분이 불가능하므로 아래와 같은 미분 근사화 방법을 사용해야 한다.
- 전진 차분(Forward difference) :
- 후진 차분(Backward difference) :
- 중앙 차분(Centered difference) :
- 영상에서 는 픽셀의 간격을 의미하며, 보통 최소 단위인 1을 사용함.
- 일반적으로 오류가 가장 적은 중앙 차분 방법이 엣지 검출에 주로 사용된다.
- 1차원 이산함수인 픽셀값을 중앙 차분 방식으로 미분 근사화를 한다면 아래와 같은 미분 마스크를 사용할 수 있다.
1. Prewitt 필터
개요
- Prewitt 필터는 1차 미분 기반의 엣지 검출 필터로, 영상에서 수평·수직 방향의 기울기를 계산하여 엣지를 추출한다.
- 구현이 단순하고 계산량이 적지만, 노이즈에 상대적으로 취약하다.
방법
- 영상의 밝기 변화(기울기)를 근사하기 위해 수평 방향()과 수직 방향()의 차분 연산을 수행한다.
- 각 방향의 기울기를 계산한 뒤, 이를 결합하여 엣지의 크기와 방향을 구한다.
- 수평 방향 마스크 :
- 수직 방향 마스크 :
- 기울기 크기 계산 :
- 기울기 방향 계산 :
- 검출된 엣지를 이미지로 만들 때, 기울기 방향은 사용되지 않는다.
- 검출된 엣지(기울기 크기)를 0~255 범위로 정규화(Normalization)한 것을 이미지로 출력할 수 있다.
왜 3x3 마스크인가?
- 위와 같은 3x3 마스크를 사용하여 컨볼루션하면 현재 픽셀 기준 x방향, y방향 각각 전후 픽셀값의 차이를 구하는 것.
- 그렇다면 단순히 와 같이 현재 픽셀와 바로 직전 픽셀의 차이를 구하기 위해 아래와 같이 마스크를 만들 수 있지 않을까.
- 3x3 마스크를 쓰는 이유는 노이즈(작은 픽셀 값 변화)에 대응하여 안정적으로 엣지를 검출하기 위함.
- 엣지는 단일 픽셀이 아니라 연속된 영역에 걸쳐 나타나고, 3x3 마스크는 이 공간적 연속성을 감안해 주변 영역 전체의 변화량을 반영하여 더 자연스러운 검출 결과를 얻을 수 있다.
- 연산량과 효과의 균형이 좋기 때문에 오래전부터 표준적으로 사용되어 왔다.
OpenCV
import cv2
import numpy as np
img = cv2.imread('image.jpg', cv2.IMREAD_GRAYSCALE)
# Prewitt 커널 정의
kernelx = np.array([[-1, 0, 1],
[-1, 0, 1],
[-1, 0, 1]], dtype=np.float32)
kernely = np.array([[-1, -1, -1],
[ 0, 0, 0],
[ 1, 1, 1]], dtype=np.float32)
# 수평/수직 방향 경계 검출
prewitt_x = cv2.filter2D(img, cv2.CV_64F, kernelx)
prewitt_y = cv2.filter2D(img, cv2.CV_64F, kernely)
# 기울기 크기 계산
prewitt = cv2.magnitude(prewitt_x, prewitt_y)
cv2.imshow('Prewitt', prewitt / prewitt.max())
Original | Prewitt |
---|---|
![]() | ![]() |
2. Sobel 필터
개요
- Sobel 필터는 1차 미분 기반의 엣지 검출 필터로, Prewitt 필터와 유사하지만 중앙 픽셀에 더 큰 가중치(2)를 부여하여 노이즈 영향을 줄이고 경계 검출을 더 부드럽게 만든다.
방법
- 수평 방향 마스크 :
- 수직 방향 마스크 :
- 기울기 크기 계산 :
OpenCV
import cv2
import numpy as np
img = cv2.imread('image.jpg', cv2.IMREAD_GRAYSCALE)
# Sobel 연산 (x방향, y방향)
Gx = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3) # dx=1, dy=0 → 수평 경계
Gy = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3) # dx=0, dy=1 → 수직 경계
# 기울기 크기 계산
magnitude = cv2.magnitude(Gx, Gy)
# 0~255 정규화
magnitude = cv2.normalize(magnitude, None, 0, 255, cv2.NORM_MINMAX)
magnitude = magnitude.astype(np.uint8)
cv2.imshow('Sobel Edge', magnitude)
dst = cv2.Sobel(src, ddepth, dx, dy, dst, ksize, scale, delta, borderType)
- src: 입력 영상
- ddepth: 출력 영상의 채널 (-1: 입력 영상과 동일)
- dx: x 방향 미분 차수 (0, 1, 2)
- dy: y 방향 미분 차수 (0, 1, 2)
- ksize: 커널의 크기 (1, 3, 5, 7)
- scale: 결과에 곱할 스케일
- delta: 결과에 더할 값(밝기 보정 등)
- borderType: 가장자리 픽셀 확장 방식
Original | Sobel |
---|---|
![]() | ![]() |
3. Laplacian 필터
개요
- Laplacian 필터는 영상에서 2차 미분을 이용해 엣지를 검출하는 필터이다.
- 영상 밝기 함수 의 2차 미분 값을 계산해, 급격한 밝기 변화(엣지)가 있는 부분을 강조한다.
- 1차 미분 필터(Prewitt, Sobel)가 변화율의 크기를 구하는 데 반해, Laplacian은 변화율의 변화(즉, 곡률)를 포착한다.
- 2차 미분한 그래프를 보면 엣지부분이 0으로 부호가 바뀌게 되는 것을 확인할 수 있다. 이를 변곡점 또는 영교차(zero crossing)라고 하며, 이 기준을 사용하여 이미지의 엣지를 검출할 수 있다.
방법
수식
- Laplacian 연산자는 아래와 같이, 두 방향에서 2차 미분을 합산하여 계산한다.
- x축 방향 2차 미분 :
- y축 방향 2차 미분 :
- 두 방향 합치기 :
2차 미분으로 전개
- 1차 미분 (x축, 중앙 차분 근사)
- 2차 미분 (1차 미분을 한 번 더 미분)
- 위 식은 픽셀 간격이 2이며, 픽셀 간격을 1로 두어 아래와 같이 표현할 수 있다.
커널 형태로 표현
- 4방향 라플라시안 :
- 8방향 라플라시안 (대각선 성분의 2차 미분까지 고려) :
- OpenCV의
cv2.Laplacian()
은 기본적으로 8방향 커널을 사용.
- OpenCV의
OpenCV
import cv2
import numpy as np
img = cv2.imread('image.jpg', cv2.IMREAD_GRAYSCALE)
# Laplacian 필터 적용
laplacian = cv2.Laplacian(img, cv2.CV_64F)
# 결과를 0~255 범위로 정규화
laplacian_norm = cv2.normalize(laplacian, None, 0, 255, cv2.NORM_MINMAX)
laplacian_norm = laplacian_norm.astype(np.uint8)
cv2.imshow('Laplacian Edge', laplacian_norm)
cv2.Laplacian(src, ddepth, dst=None, ksize=None, scale=None, delta=None, borderType=None) -> dst
- src: 입력 영상
- ddepth: 출력 영상의 데이터 타입
- ksize: 커널 크기(홀수)
- scale: 결과값에 곱하는 스케일
- delta: 결과값에 더하는 상수(밝기 보정 등)
- borderType: 가장자리 픽셀 확장 방식
Laplacian | Gaussian Blur + Laplacian (LoG) |
---|---|
![]() | ![]() |
- 라플라시안 필터는 노이즈에 영향을 많이 받으므로, 가우시안 필터와 함께 사용하는 것이 일반적이다.
4. Canny Edge 검출
개요
- Canny Edge 검출 방법은 4단계의 알고리즘을 적용해 잡음에 강하고, 엣지를 얇게, 연속적으로 찾기 위해 설계된 멀티 스텝 알고리즘이다.
- 좋은 엣지 검출 방법의 조건은 아래와 같다고 정의한다.
- Low Error Rate -실제 엣지는 놓치지 않고, 가짜 엣지는 최대한 줄이기(얇게=정확하게)
- Good Localization - 검출한 엣지가 실제 위치와 최대한 일치
- Minimal Response - 하나의 엣지에 여러 개의 반응(두꺼운 라인)이 생기지 않게
- 이러한 좋은 엣지 검출 방법을 위해 Canny Edge는 아래 4단계를 수행한다.
- Noise Reduction - 가우시안 필터링으로 노이즈 제거
- Gradient Calculation - Sobel Mask를 사용해 엣지 방향과 크기 구하기
- Non-Maximum Suppression (NMS) - 그래디언트 방향에서 검출된 엣지 중 가장 큰 값만 선택하고 나머지는 제거
- Edge Tracking by Hysteresis - 임계값(Low, High)을 지정하고, 임계값을 이용해 강한 엣지와 약한 엣지를 분류함
방법
1. Noise Reduction
- 영상의 노이즈가 엣지로 오인되는 것을 방지하기 위해 가우시안 필터로 스무딩한다.
- OpenCV의
cv2.Canny()
가 내부적으로 적용하는 가우시안 필터의 기본 커널 크기는 5×5이며 표준편차 는 1.4로 고정됨.
2. Gradient Calculation
- Sobel 필터를 이용해서 축 방향으로 각각 gradient 벡터를 구하고, 이를 통해 gradient의 크기와 방향을 구한다.
- 연속적인 방향()을 그대로 사용하지 않고, 4가지 대표 방향으로 단순화 한다. (0도, 45도, 90도, 135도)
3. Non-Maximum Suppression (NMS)
- 가우시안 블러 → Sobel로 구한 기울기 크기()는 엣지 주변이 두껍게 나온다.
- NMS는 기울기 방향(=엣지와 수직)으로 이웃(전후) 픽셀과 비교해, Local Maximum 만 남기고 나머지는 0으로 만들어 엣지를 얇게 만든다.
- 즉, 기울기 크기 맵에서 현재 좌표의 기울기 방향(4방향 중에 하나)으로 전/후 좌표의 값을 비교하여 현재 좌표보다 더 크다면 현재 좌표의 값을 0으로 매핑한다.
Canny Edge Detaction NMS Code
def nms_quantized(mag, gx, gy): H, W = mag.shape out = np.zeros_like(mag, dtype=mag.dtype) # 각도 [0, 180) angle = (np.rad2deg(np.arctan2(gy, gx)) + 180.0) % 180.0 for y in range(1, H-1): for x in range(1, W-1): a = angle[y, x] m = mag[y, x] # 0°, 45°, 90°, 135° 분기 if (a < 22.5) or (a >= 157.5): m1, m2 = mag[y, x-1], mag[y, x+1] # 수평 elif a < 67.5: m1, m2 = mag[y+1, x-1], mag[y-1, x+1] # 45° elif a < 112.5: m1, m2 = mag[y-1, x], mag[y+1, x] # 수직 else: m1, m2 = mag[y-1, x-1], mag[y+1, x+1] # 135° out[y, x] = m if (m >= m1 and m >= m2) else 0.0 return out
4. Edge Tracking by Hysteresis
- NMS까지 거치면 엣지가 1픽셀 두께로 얇아지지만, 약한 엣지(weak) 도 꽤 남아 있다 → 강한 엣지와 연결된 약한 엣지는 살리고, 고립된 약한 엣지는 버린다.
- 엣지의 특성 상 갑자기 나타나지 않고 엣지는 연결되어 있다는 것을 가정.
- , 두 개의 임계값을 사용하여 아래 그림과 같이 엣지로 판별할 수 있는 기준을 두 가지 영역으로 구분한다.
-
은 NMS의 결과라고 할 때,
-
: 충분히 강한 엣지로 즉시 확정(Strong)
-
: 약한 엣지 후보(Weak)
-
: 버림
-
Strong 픽셀들을 대상으로, 주변 8방향에 대한 픽셀값을 탐색하고, Weak 엣지가 존재한다면 해당 엣지를 Strong으로 승격한다.
-
이 때, 추적 중에 한 번 승격된 (Weak→Strong)도 이웃을 다시 살릴 수 있어 연결된 약한 엣지 사슬이 모두 살아난다.
-
따라서 최종적으로 위 그림과 같이 Strong을 포함하여 연결된 Weak들이 함께 살아남게 된다.
OpenCV
import cv2
# 영상 읽기
image = cv2.imread("../road.jpg", cv2.IMREAD_GRAYSCALE)
# Canny 연산
canny = cv2.Canny(gray, 100, 200)
cv2.imshow("canny", canny)
cv2.Canny(image, threshold1, threshold2, edges=None, apertureSize=None, L2gradient=None) -> edges
- image: 입력 영상.
- threshold1: 하단 임계값.
- threshold2: 상단 임계값.
- edges: 에지 영상.
- apertureSize: 소벨 연산을 위한 커널 크기. 기본값은 3.
- L2gradient: True이면 L2 norm 사용, False이면 L1 norm 사용. 기본값은 False.
Sobel | Canny |
---|---|
![]() | ![]() |