OpenCV 비전 실무11장 · Canny 와 Edge 비교15 / 21← 이전목차다음 →MetaContents

11장. Canny 와 Edge 비교

실습 2 — Canny 의 단계 따라가기

cv2.Canny 가 안에서 하는 일을 NumPy 로 한 단계씩 따라 해 봅니다. 블러, Sobel 크기와 방향, 비최대 억제까지 직접 하고 cv2.Canny 결과와 모양을 견줍니다.

1단계. 한 줄로 비최대 억제 연습하기

Python
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단계. 기울기의 크기와 방향 구하기

Python
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단계. 그림 전체에 비최대 억제하기

Python
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단계. 네 장을 나란히 보기

Python
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()
도형 그림의 블러, 기울기 크기, 직접 한 비최대 억제, cv2.Canny 결과를 나란히 놓은 네 그림

코드 설명

  • 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)를 넘는 칸이 없는 그 네모를 지웠습니다.
← 이전 절11장 · 실습 1 — cv2.Canny 기본과 두 기준값 격자다음 절 →11장 · 실습 3 — 이력 임계값의 효과