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

11장. Canny 와 Edge 비교

실습 4 — 노이즈에서 블러 크기, apertureSize, L2gradient 견주기

도형 그림에 6장에서 쓴 것과 같은 꼴의 노이즈를 더하고, 블러 크기와 cv2.Canny 의 두 인자를 바꿔 가며 정밀도·재현율·F1 로 견줍니다.

1단계. 노이즈 더하고 점수 함수 만들기

Python
import cv2
import numpy as np

gray = cv2.imread("ch11_set/shapes.png", cv2.IMREAD_GRAYSCALE)
answer = cv2.imread("ch11_set/shapes_edge.png", cv2.IMREAD_GRAYSCALE)

# 6차시와 같은 꼴의 노이즈: 씨앗 1111, 표준편차 20
rng = np.random.default_rng(1111)
noisy = np.clip(gray.astype(np.float64) + rng.normal(0, 20, gray.shape), 0, 255).astype(np.uint8)

def edge_score(edges, answer):
    """허용 거리 1칸으로 (정밀도, 재현율, F1) 을 돌려준다."""
    kernel = np.ones((3, 3), np.uint8)
    near_answer = cv2.dilate(answer, kernel)        # 넓힌 정답: 정밀도를 낼 때 쓴다
    near_found = cv2.dilate(edges, kernel)          # 넓힌 찾은 경계: 재현율을 낼 때 쓴다
    found, real = edges > 0, answer > 0
    precision = (found & (near_answer > 0)).sum() / max(found.sum(), 1)
    recall = (real & (near_found > 0)).sum() / real.sum()
    f1 = 2 * precision * recall / (precision + recall) if precision + recall > 0 else 0.0
    return round(float(precision), 3), round(float(recall), 3), round(float(f1), 3)

print("깨끗한 그림, 블러 없이:", edge_score(cv2.Canny(gray, 50, 150), answer))
print("노이즈 낀 그림, 블러 없이:", edge_score(cv2.Canny(noisy, 50, 150), answer))
출력
깨끗한 그림, 블러 없이: (1.0, 1.0, 1.0)
노이즈 낀 그림, 블러 없이: (0.069, 1.0, 0.13)

2단계. 블러 크기 바꾸기

Python
import pandas as pd

rows, blurred = [], {}
for k in [0, 3, 5, 7, 9]:
    # k = 0 은 블러 없이. 나머지는 k×k 가우시안 블러
    im = noisy if k == 0 else cv2.GaussianBlur(noisy, (k, k), 0)
    e = cv2.Canny(im, 50, 150)
    blurred[k] = e
    p, r, f = edge_score(e, answer)
    rows.append({"블러": "없음" if k == 0 else f"{k}×{k}", "경계 칸 비율": round(float((e > 0).mean()), 3),
                 "정밀도": p, "재현율": r, "F1": f})
print(pd.DataFrame(rows).to_string(index=False))
출력
 블러  경계 칸 비율   정밀도   재현율    F1
 없음    0.352 0.069 1.000 0.130
3×3    0.084 0.253 0.999 0.404
5×5    0.021 0.936 0.943 0.939
7×7    0.015 0.999 0.752 0.858
9×9    0.011 1.000 0.543 0.704

3단계. apertureSize 와 L2gradient 바꾸기

Python
blur5 = cv2.GaussianBlur(noisy, (5, 5), 0)
rows = []
# apertureSize 마다 같은 기준값(50, 150)으로 걸어 본다
for size in [3, 5, 7]:
    e = cv2.Canny(blur5, 50, 150, apertureSize=size)
    rows.append({"설정": f"apertureSize {size}", "경계 칸 비율": round(float((e > 0).mean()), 3),
                 "F1": edge_score(e, answer)[2]})
# L2gradient: True 면 √(gx² + gy²), False(기본값)면 |gx| + |gy|
for l2 in [False, True]:
    e = cv2.Canny(blur5, 50, 150, L2gradient=l2)
    rows.append({"설정": f"L2gradient {l2}", "경계 칸 비율": round(float((e > 0).mean()), 3),
                 "F1": edge_score(e, answer)[2]})
print(pd.DataFrame(rows).to_string(index=False))
출력
              설정  경계 칸 비율    F1
  apertureSize 3    0.021 0.939
  apertureSize 5    0.336 0.116
  apertureSize 7    0.313 0.123
L2gradient False    0.021 0.939
 L2gradient True    0.021 0.990

4단계. 블러 크기별 결과 보기

Python
import matplotlib.pyplot as plt

plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False

fig, axes = plt.subplots(2, 3, figsize=(10, 5.6))
axes[0, 0].imshow(noisy, cmap="gray", vmin=0, vmax=255)
axes[0, 0].set_title("노이즈 낀 그림 (표준편차 20)")
for ax, k in zip([axes[0, 1], axes[0, 2], axes[1, 0], axes[1, 1], axes[1, 2]], [0, 3, 5, 7, 9]):
    ax.imshow(blurred[k], cmap="gray", vmin=0, vmax=255)
    ax.set_title("블러 없이" if k == 0 else f"블러 {k}×{k}")
for ax in axes.flat:
    ax.axis("off")
plt.tight_layout()
plt.show()
노이즈 낀 도형 그림과 블러 없음, 3×3, 5×5, 7×7, 9×9 블러 뒤 Canny 결과를 2행 3열로 놓은 격자

코드 설명

  • 노이즈 낀 그림에 블러 없이 Canny 를 걸면 정밀도가 0.069 로, 찾은 칸의 93% 가 헛것입니다. 재현율은 1.0 이라 진짜 경계는 다 찾았지만 F1 은 0.13 에 그칩니다.
  • 블러를 3, 5, 7, 9 로 키우면 정밀도는 0.253, 0.936, 0.999, 1.0 으로 오르고 재현율은 0.999, 0.943, 0.752, 0.543 으로 내려갑니다. F1 은 5×5 에서 0.939 로 가장 높습니다. 격자에서 7×7 은 오른쪽 아래의 약한 네모를, 9×9 는 타원까지 놓쳤습니다.
  • apertureSize 를 5, 7 로 키우면 같은 기준값(50, 150)에서 경계 칸 비율이 0.336, 0.313 으로 뛰고 F1 이 0.116, 0.123 으로 떨어집니다. 커널이 커지면 기울기 크기가 커지므로, 5 나 7 을 쓰려면 기준값도 함께 키워야 합니다.
  • L2gradient=True 는 경계 칸 비율은 0.021 로 같은데 F1 이 0.939 에서 0.990 으로 올랐습니다. |gx| + |gy| 는 비스듬한 테두리에서 크기를 부풀리지만 √(gx² + gy²) 는 방향과 상관없이 크기를 재므로, 테두리를 따라 크기가 고르게 나온 덕으로 볼 수 있습니다.
← 이전 절11장 · 실습 3 — 이력 임계값의 효과다음 절 →11장 · 실습 5 — 자동 기준값 견주기: 중앙값 규칙과 Otsu 규칙