경계 검출
Sobel과 Canny 경계 검출 적용
이미지 불러오기
src = cv.imread("temple.webp")
gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY)
Sobel 경계 검출
Sobel 연산자: 주변 픽셀의 밝기 차이로 x·y 방향의 기울기(gradient) 계산. 미분 방향으로 밝기 차이를 구하고 수직 방향으로 [1, 2, 1] 가중 평균을 적용해 작은 잡음의 영향 감소.
3×3 Sobel 커널:
Kx=−1−2−1000121,Ky=−101−202−101sobel_x: 좌우 밝기 변화가 큰 수직 경계에 강하게 반응sobel_y: 상하 밝기 변화가 큰 수평 경계에 강하게 반응- 응답의 부호: 어두운 영역에서 밝은 영역으로 변하는 방향과 그 반대 방향 구분
sobel_x = cv.Sobel(gray, cv.CV_32F, 1, 0, ksize=3) # 3×3 x 방향 1차 미분
sobel_y = cv.Sobel(gray, cv.CV_32F, 0, 1, ksize=3) # 3×3 y 방향 1차 미분
sobel_magnitude = cv.magnitude(sobel_x, sobel_y)
float(sobel_magnitude.max()) # 경계 강도 최댓값
1104.171142578125
경계 강도의 최댓값은 8비트 범위인 255를 초과. cv.CV_32F는 음수와 255보다 큰 기울기를 보존.
각 픽셀의 두 기울기를 결합해 경계 강도 G와 기울기 방향 θ 계산.
G=Gx2+Gy2,θ=atan2(Gy,Gx)G가 클수록 밝기가 급격하게 변하는 위치. θ는 밝기가 가장 빠르게 증가하는 기울기 방향이며 실제 경계선의 진행 방향과 수직. Canny는 이 방향을 이용해 경계선을 얇게 정리.
화면 표시용 x·y 응답은 절댓값의 8비트 이미지로 변환하고 결합 결과는 0~255로 정규화. 이 과정에서 원래 기울기의 부호와 크기 범위는 변화.
sobel_x_view = cv.convertScaleAbs(sobel_x) # 절댓값을 8비트로 표시
sobel_y_view = cv.convertScaleAbs(sobel_y)
sobel_result = cv.normalize(
sobel_magnitude,
None,
0,
255,
cv.NORM_MINMAX, # 최솟값을 0, 최댓값을 255로 변환
cv.CV_8U,
)
to_pil(cv.hconcat([gray, sobel_x_view, sobel_y_view, sobel_result]))

기둥의 세로선은 Sobel X에서, 지붕과 돌의 가로선은 Sobel Y에서 상대적으로 강한 반응. 결합 결과에는 두 방향의 경계가 모두 유지.
Sobel 결과는 경계 후보의 강도를 연속값으로 표현. 같은 경계가 두껍게 나타나거나 작은 잡음까지 검출될 가능성.
Canny 경계 검출
Canny 경계 검출: Sobel 기울기를 얇고 연결된 이진 경계로 정리하는 방법. 주요 처리 순서:
- 잡음 완화: 가우시안 필터로 작은 밝기 변화를 줄여 거짓 경계 억제
- 기울기 계산: Sobel 연산으로 경계 강도 G와 기울기 방향 θ 계산
- 비최대 억제: 기울기 방향의 양쪽 픽셀과 경계 강도를 비교하고 국소 최댓값이 아닌 응답 제거
- 이중 임계값: 경계 강도를 강한 경계, 약한 경계, 제거 대상의 세 범주로 분류
- 히스테리시스: 강한 경계에 이어진 약한 경계만 유지하고 고립된 약한 응답 제거
비최대 억제(non-maximum suppression): Sobel에서 띠처럼 두껍게 나온 응답의 중심만 남기는 단계. 기울기 방향이 좌우라면 현재 픽셀의 강도를 왼쪽·오른쪽 픽셀과 비교. 현재 값이 가장 크지 않으면 0으로 제거.
이중 임계값(double threshold): 비최대 억제 결과를 다음 세 범주로 분류.
G≥upperlower≤G<upperG<lower→강한 경계→약한 경계→제거히스테리시스(hysteresis): 강한 경계에 연결된 약한 경계만 실제 경계로 판단. 강한 경계와 이어진 약한 경계는 유지하고 고립된 약한 경계는 잡음으로 제거.
OpenCV의 cv.Canny(): 기울기 계산부터 히스테리시스까지 내부 수행. 이 실습에서는 0보다 큰 Sobel 경계 강도의 90번째 백분위수를 상한값으로 사용하고 그 40%를 하한값으로 설정.
blurred_gray = cv.GaussianBlur(gray, (5, 5), 0) # 5×5 커널로 잡음 완화
canny_sobel_x = cv.Sobel(blurred_gray, cv.CV_32F, 1, 0, ksize=3) # 3×3 x 방향 기울기
canny_sobel_y = cv.Sobel(blurred_gray, cv.CV_32F, 0, 1, ksize=3) # 3×3 y 방향 기울기
canny_magnitude = cv.magnitude(canny_sobel_x, canny_sobel_y)
positive_magnitude = canny_magnitude[canny_magnitude > 0]
upper = float(np.percentile(positive_magnitude, 90)) # 강한 상위 10%의 경계 선택
lower = upper * 0.4 # 연결할 약한 경계의 시작점
canny_edges = cv.Canny(
blurred_gray,
lower,
upper,
L2gradient=True, # 유클리드 거리로 경계 강도 계산
)
lower감소: 더 약한 경계까지 연결 후보로 포함하여 검출량 증가upper증가: 강한 경계로 시작할 수 있는 픽셀이 줄어 검출량 감소- 경계 강도:
L2gradient=True이므로 Gx2+Gy2 사용. 기본값False는 ∣Gx∣+∣Gy∣로 근사 - 반환값: 경계는 255, 나머지는 0인
uint8이진 이미지
lower # 하한 임계값
94.90205078125001
upper # 상한 임계값
237.255126953125
검출 위치를 확인하기 위해 원본 이미지의 경계 픽셀을 빨간색으로 표시.
edge_overlay = src.copy() # 원본을 그대로 두고 사본에 표시
edge_overlay[canny_edges > 0] = (0, 0, 255) # BGR 순서이므로 빨간색
edge_pixels = cv.countNonZero(canny_edges)
edge_pixels # 경계 픽셀 수
33416
edge_pixels / canny_edges.size * 100 # 경계 픽셀 비율(%)
9.13245004154095
canny_comparison = cv.hconcat(
[
cv.cvtColor(blurred_gray, cv.COLOR_GRAY2BGR),
cv.cvtColor(canny_edges, cv.COLOR_GRAY2BGR),
edge_overlay,
]
)
to_pil(canny_comparison)

temple.webp에서는 전체 픽셀의 약 9.13%가 경계로 선택. 신전의 외곽과 기둥처럼 이어진 구조는 유지되고 평탄한 하늘 영역은 대부분 제거.
퀴즈
cv.Sobel(gray, cv.CV_32F, 1, 0)이 주로 강조하는 경계는?
퀴즈를 풀려면 대화형 기능을 불러와야 합니다.