11장. Canny 와 Edge 비교
실습 2 — Canny 의 단계 따라가기
cv2.Canny 가 안에서 하는 일을 NumPy 로 한 단계씩 따라 해 봅니다. 블러, Sobel 크기와 방향, 비최대 억제까지 직접 하고 cv2.Canny 결과와 모양을 견줍니다.
1단계. 한 줄로 비최대 억제 연습하기
import cv2
import numpy as np
def nms_row(row):
"""한 줄의 기울기 크기에서 왼쪽·오른쪽 이웃보다 크거나 같은 칸만 남긴다."""
row = np.asarray(row, dtype=np.float64)
padded = np.pad(row, 1) # 양 끝에 0 을 붙인다
left, right = padded[:-2], padded[2:]
return np.where((row >= left) & (row >= right), row, 0)
# 본문의 손계산 예
print("본문 예:", [int(v) for v in nms_row([10, 40, 90, 60, 20])])
# 실제 그림: 블러한 도형 그림의 세로 70 줄, 원의 왼쪽 테두리를 가로지르는 칸들
gray = cv2.imread("ch11_set/shapes.png", cv2.IMREAD_GRAYSCALE)
blur = cv2.GaussianBlur(gray, (5, 5), 0)
# cv2.Sobel(그림, 결과 자료형, 1, 0): 가로 방향 기울기 gx. CV_64F 는 음수도 담는 실수
gx = cv2.Sobel(blur, cv2.CV_64F, 1, 0, ksize=3)
piece = np.abs(gx[70, 26:35]) # 크기만 보려고 절댓값
print("원 테두리의 |gx|:", [int(v) for v in piece])
print("꼭대기만 남기면:", [int(v) for v in nms_row(piece)])본문 예: [0, 0, 90, 0, 0]
원 테두리의 |gx|: [0, 7, 58, 174, 264, 221, 98, 18, 0]
꼭대기만 남기면: [0, 0, 0, 0, 264, 0, 0, 0, 0]2단계. 기울기의 크기와 방향 구하기
gy = cv2.Sobel(blur, cv2.CV_64F, 0, 1, ksize=3) # 세로 방향 기울기
# 크기: Canny 의 기본값처럼 |gx| + |gy| (L1)
mag = np.abs(gx) + np.abs(gy)
# 방향: 10차시의 cv2.phase 로 각도(0~360도)를 구해 0~180 으로 접고, 네 갈래로 반올림한다
angle = cv2.phase(gx, gy, angleInDegrees=True) % 180
direction = (np.round(angle / 45) % 4 * 45).astype(int)
print("가장 큰 기울기 크기:", int(mag.max()))
strong = mag > 50
print("크기가 50 을 넘는 칸 비율:", round(float(strong.mean()), 3))
# 크기가 50 을 넘는 칸이 네 갈래에 얼마나 나뉘었나
for d in [0, 45, 90, 135]:
print(f" 방향 {d:3}도: {int((strong & (direction == d)).sum())}칸")가장 큰 기울기 크기: 426
크기가 50 을 넘는 칸 비율: 0.068
방향 0도: 1595칸
방향 45도: 937칸
방향 90도: 2022칸
방향 135도: 688칸3단계. 그림 전체에 비최대 억제하기
def nms(mag, direction):
"""방향마다 견줄 이웃 두 칸을 정해, 둘 다보다 크거나 같은 칸만 남긴다."""
h, w = mag.shape
padded = np.pad(mag, 1) # 가장자리 칸도 이웃을 갖도록 0 을 두른다
# 방향 → (이웃1 의 세로·가로 칸 차이, 이웃2 의 세로·가로 칸 차이). 세로 번호는 아래로 갈수록 커진다
pairs = {0: ((0, -1), (0, 1)), # 왼쪽, 오른쪽
45: ((-1, -1), (1, 1)), # 왼쪽 위, 오른쪽 아래
90: ((-1, 0), (1, 0)), # 위, 아래
135: ((-1, 1), (1, -1))} # 오른쪽 위, 왼쪽 아래
out = np.zeros_like(mag)
for d, ((y1, x1), (y2, x2)) in pairs.items():
# 그림 전체를 한 칸 밀어 놓은 판 = 칸마다 그 쪽 이웃의 값
n1 = padded[1 + y1:1 + y1 + h, 1 + x1:1 + x1 + w]
n2 = padded[1 + y2:1 + y2 + h, 1 + x2:1 + x2 + w]
keep = (direction == d) & (mag >= n1) & (mag >= n2)
out[keep] = mag[keep]
return out
thin = nms(mag, direction)
canny = cv2.Canny(blur, 50, 150)
print("크기 > 50 인 칸 비율:", round(float((mag > 50).mean()), 3))
print("비최대 억제 뒤 > 50 인 칸 비율:", round(float((thin > 50).mean()), 3))
print("cv2.Canny(50, 150) 칸 비율:", round(float((canny > 0).mean()), 3))
# 선 두께: 세로 70 줄의 가로 20~40 칸(원의 왼쪽 테두리)에서 흰 칸이 몇 개인가
for name, im in [("크기 > 50", mag > 50), ("비최대 억제 뒤", thin > 50), ("Canny", canny > 0)]:
print(f" {name}: 테두리를 가로지르는 칸 {int(im[70, 20:40].sum())}개")크기 > 50 인 칸 비율: 0.068
비최대 억제 뒤 > 50 인 칸 비율: 0.031
cv2.Canny(50, 150) 칸 비율: 0.015
크기 > 50: 테두리를 가로지르는 칸 5개
비최대 억제 뒤: 테두리를 가로지르는 칸 1개
Canny: 테두리를 가로지르는 칸 1개4단계. 네 장을 나란히 보기
import matplotlib.pyplot as plt
plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False
fig, axes = plt.subplots(1, 4, figsize=(13, 3.2))
panels = [("1단계 블러", blur, 255),
("2단계 기울기 크기", mag, None), # 크기는 255 를 넘으므로 범위를 자동으로
("3단계 비최대 억제", thin, None),
("cv2.Canny 결과", canny, 255)]
for ax, (title, im, vmax) in zip(axes, panels):
ax.imshow(im, cmap="gray", vmin=0, vmax=vmax)
ax.set_title(title)
ax.axis("off")
plt.tight_layout()
plt.show()
코드 설명
nms_row는 본문 손계산처럼[10, 40, 90, 60, 20]에서 90 만 남깁니다. 실제 원 테두리를 가로지르는 |gx| 는 0, 7, 58, 174, 264, 221, 98, 18, 0 으로 산 모양이고, 꼭대기 264 하나만 남습니다.- 크기는 Canny 의 기본값처럼 |gx| + |gy| 로 쟀고, 가장 큰 값은 426 입니다. 50 을 넘는 칸을 방향으로 나누면 90도 2022칸, 0도 1595칸, 45도 937칸, 135도 688칸입니다. 네모의 위·아래 테두리는 밝기가 세로로 바뀌어 90도, 왼쪽·오른쪽 테두리는 0도로 들어가고, 원·타원·세모의 비스듬한 부분이 45도와 135도로 나뉩니다.
nms는 그림 전체를 이웃 쪽으로 한 칸 밀어 놓은 판(padded[...])과 견줘, 반복문 없이 모든 칸을 한꺼번에 비교합니다. 50 을 넘는 칸이 0.068 에서 0.031 로 절반 아래가 되고, 원 테두리를 가로지르는 칸이 5개에서 1개로 줄어듭니다.cv2.Canny도 1개입니다.- 직접 한 비최대 억제(0.031)가
cv2.Canny(0.015)보다 많은 까닭은 4단계 이력 임계값을 하지 않았기 때문입니다. 셋째 그림에는 오른쪽 아래의 약한 네모가 남아 있지만, 넷째 그림의 Canny 는 high(150)를 넘는 칸이 없는 그 네모를 지웠습니다.