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

11장. Canny 와 Edge 비교

실습 5 — 자동 기준값 견주기: 중앙값 규칙과 Otsu 규칙

도형, 부품, 글자 세 그림(ch11_set/)에서 고정 기준값(50, 150), 중앙값 규칙, Otsu 규칙을 견줍니다. 깨끗한 그림과 노이즈 낀 그림에서 F1 이 어떻게 달라지는지 봅니다.

1단계. 두 규칙을 함수로 만들기

Python
import cv2
import numpy as np
import pandas as pd

def median_rule(im, sigma=0.33):
    """중앙값의 (1 - sigma) 배를 low, (1 + sigma) 배를 high 로. 경험 규칙이다"""
    med = float(np.median(im))
    return int(max(0, (1 - sigma) * med)), int(min(255, (1 + sigma) * med))

def otsu_rule(im):
    """8차시의 Otsu 가 고른 값을 high, 그 절반을 low 로. 경험 규칙이다"""
    t, _ = cv2.threshold(im, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    return int(0.5 * t), int(t)

names = ["shapes", "parts", "letters"]
images = {n: cv2.imread(f"ch11_set/{n}.png", cv2.IMREAD_GRAYSCALE) for n in names}
answers = {n: cv2.imread(f"ch11_set/{n}_edge.png", cv2.IMREAD_GRAYSCALE) for n in names}
for n in names:
    print(f"{n:7}: 중앙값 규칙 {median_rule(images[n])}, Otsu 규칙 {otsu_rule(images[n])}")
출력
shapes : 중앙값 규칙 (117, 234), Otsu 규칙 (55, 110)
parts  : 중앙값 규칙 (127, 252), Otsu 규칙 (60, 120)
letters: 중앙값 규칙 (45, 90), Otsu 규칙 (37, 75)

2단계. 세 그림에서 F1 견주기

Python
def edge_f1(edges, answer):
    """허용 거리 1칸의 F1"""
    kernel = np.ones((3, 3), np.uint8)
    found, real = edges > 0, answer > 0
    p = (found & (cv2.dilate(answer, kernel) > 0)).sum() / max(found.sum(), 1)
    r = (real & (cv2.dilate(edges, kernel) > 0)).sum() / real.sum()
    return round(float(2 * p * r / (p + r)), 3) if p + r > 0 else 0.0

def compare(imgs):
    """그림마다 5×5 블러 뒤 세 가지 기준값으로 Canny 를 걸어 F1 표를 만든다"""
    rows = []
    for n in names:
        b = cv2.GaussianBlur(imgs[n], (5, 5), 0)
        for rule, (low, high) in [("고정 50, 150", (50, 150)),
                                  ("중앙값 규칙", median_rule(b)),
                                  ("Otsu 규칙", otsu_rule(b))]:
            rows.append({"그림": n, "규칙": rule, "low": low, "high": high,
                         "F1": edge_f1(cv2.Canny(b, low, high), answers[n])})
    return pd.DataFrame(rows)

clean_table = compare(images)
print(clean_table.to_string(index=False))
출력
     그림         규칙  low  high    F1
 shapes 고정 50, 150   50   150 0.859
 shapes     중앙값 규칙  117   232 0.503
 shapes    Otsu 규칙   66   133 0.859
  parts 고정 50, 150   50   150 1.000
  parts     중앙값 규칙  126   251 1.000
  parts    Otsu 규칙   71   142 1.000
letters 고정 50, 150   50   150 1.000
letters     중앙값 규칙   46    91 1.000
letters    Otsu 규칙   63   127 1.000

3단계. 노이즈 낀 그림에서 다시 견주기

Python
# 세 그림에 표준편차 20 의 노이즈를 더한다. 씨앗 1112 로 고정
rng = np.random.default_rng(1112)
noisy = {n: np.clip(images[n].astype(np.float64) + rng.normal(0, 20, images[n].shape), 0, 255).astype(np.uint8)
         for n in names}
noisy_table = compare(noisy)
print(noisy_table.to_string(index=False))

# 규칙마다 세 그림의 평균 F1
summary = pd.DataFrame({"깨끗한 그림": clean_table.groupby("규칙")["F1"].mean(),
                        "노이즈 낀 그림": noisy_table.groupby("규칙")["F1"].mean()}).round(3)
print()
print(summary)
출력
     그림         규칙  low  high    F1
 shapes 고정 50, 150   50   150 0.950
 shapes     중앙값 규칙  116   231 0.704
 shapes    Otsu 규칙   66   133 0.968
  parts 고정 50, 150   50   150 0.994
  parts     중앙값 규칙  126   251 0.993
  parts    Otsu 규칙   70   141 0.995
letters 고정 50, 150   50   150 0.992
letters     중앙값 규칙   45    90 0.586
letters    Otsu 규칙   63   127 0.987

            깨끗한 그림  노이즈 낀 그림
규칙
Otsu 규칙      0.953     0.983
고정 50, 150   0.953     0.979
중앙값 규칙       0.834     0.761

4단계. 노이즈 낀 글자 그림의 결과 보기

Python
import matplotlib.pyplot as plt

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

b = cv2.GaussianBlur(noisy["letters"], (5, 5), 0)
fig, axes = plt.subplots(1, 4, figsize=(13, 2.9))
axes[0].imshow(noisy["letters"], cmap="gray", vmin=0, vmax=255)
axes[0].set_title("노이즈 낀 글자 그림")
for ax, (rule, (low, high)) in zip(axes[1:], [("고정", (50, 150)), ("중앙값 규칙", median_rule(b)),
                                              ("Otsu 규칙", otsu_rule(b))]):
    ax.imshow(cv2.Canny(b, low, high), cmap="gray", vmin=0, vmax=255)
    ax.set_title(f"{rule} ({low}, {high})")
for ax in axes:
    ax.axis("off")
plt.tight_layout()
plt.show()
노이즈 낀 글자 그림과 고정 기준값, 중앙값 규칙, Otsu 규칙으로 건 Canny 결과를 나란히 놓은 네 그림

코드 설명

  • 1단계는 원본으로, 2단계부터는 5×5 블러한 그림으로 규칙을 계산하므로 값이 조금 다릅니다. 도형 그림의 Otsu 규칙은 원본에서 (55, 110), 블러 뒤에는 (66, 133) 입니다.
  • 깨끗한 그림에서 세 그림의 평균 F1 은 고정 50, 150 과 Otsu 규칙이 0.953 으로 같고, 중앙값 규칙은 0.834 입니다. 중앙값 규칙은 밝은 바탕의 도형 그림에서 high 가 232 까지 올라가 약한 타원과 네모를 놓쳤습니다(F1 0.503).
  • 노이즈 낀 그림에서는 Otsu 규칙이 0.983 으로 가장 높고, 고정 기준값이 0.979, 중앙값 규칙이 0.761 입니다. 중앙값 규칙은 어두운 바탕의 글자 그림에서 (45, 90) 으로 너무 낮아 노이즈를 경계로 잡았습니다(F1 0.586). 4단계 그림에서 바탕에 흩어진 고리 모양이 그 헛것입니다.
  • 도형 그림은 고정 기준값의 F1 이 깨끗할 때 0.859, 노이즈 낀 뒤 0.950 으로 오히려 올랐습니다. 노이즈가 약한 네모의 테두리에서 기울기를 키워 high 를 넘는 칸이 생겼기 때문입니다. 경험 규칙의 점수는 그림과 노이즈에 따라 이렇게 흔들리므로, 몇 장은 눈으로 확인합니다.
← 이전 절11장 · 실습 4 — 노이즈에서 블러 크기, apertureSize, L2gradient 견주기다음 절 →11장 · 프로젝트 1 — 경계 검출 알고리즘 비교 보고서