11장. Canny 와 Edge 비교
실습 5 — 자동 기준값 견주기: 중앙값 규칙과 Otsu 규칙
도형, 부품, 글자 세 그림(ch11_set/)에서 고정 기준값(50, 150), 중앙값 규칙, Otsu 규칙을 견줍니다. 깨끗한 그림과 노이즈 낀 그림에서 F1 이 어떻게 달라지는지 봅니다.
1단계. 두 규칙을 함수로 만들기
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 견주기
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.0003단계. 노이즈 낀 그림에서 다시 견주기
# 세 그림에 표준편차 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.7614단계. 노이즈 낀 글자 그림의 결과 보기
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()
코드 설명
- 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 를 넘는 칸이 생겼기 때문입니다. 경험 규칙의 점수는 그림과 노이즈에 따라 이렇게 흔들리므로, 몇 장은 눈으로 확인합니다.