7장. Median 과 Bilateral 필터, 필터 비교
실습 3 — Median 커널 크기별 결과와 원본과의 차이
점 비율 10% 와 20% 의 표지판에 Median 필터의 ksize 를 3, 5, 7, 9 로 바꿔 걸고, 원본과의 차이를 표와 그래프로 견줍니다. 부품 그림(ch07_parts.png)으로 Median 필터가 가는 선을 지우는 것도 확인합니다.
1단계. ksize 와 점 비율별 차이 표
import cv2
import numpy as np
import pandas as pd
gray = cv2.imread("ch07_sign.png", cv2.IMREAD_GRAYSCALE)
def add_salt_pepper(img, amount, seed):
rng = np.random.default_rng(seed)
r = rng.random(img.shape)
noisy = img.copy()
noisy[r < amount / 2] = 0
noisy[r > 1 - amount / 2] = 255
return noisy
rows = []
for amount in [0.10, 0.20]:
noisy = add_salt_pepper(gray, amount, 711)
for k in [3, 5, 7, 9]:
out = cv2.medianBlur(noisy, k)
rows.append({
"점 비율": f"{amount:.0%}",
"ksize": k,
"평균 차이": round(float(cv2.absdiff(gray, out).mean()), 2),
"PSNR": round(cv2.PSNR(gray, out), 2), # 클수록 원본에 가깝다
"남은 점": int((out == 0).sum() + (out == 255).sum()),
})
table = pd.DataFrame(rows)
print(table.to_string(index=False))점 비율 ksize 평균 차이 PSNR 남은 점
10% 3 0.80 28.67 10
10% 5 1.17 27.07 0
10% 7 1.73 25.14 0
10% 9 2.64 23.15 0
20% 3 1.98 24.15 163
20% 5 2.12 24.05 2
20% 7 2.64 23.06 0
20% 9 3.75 21.42 02단계. ksize 별 그림 보기
import matplotlib.pyplot as plt
plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False
noisy20 = add_salt_pepper(gray, 0.20, 711)
fig, axes = plt.subplots(1, 5, figsize=(14, 2.4))
panels = [("점 20%", noisy20)] + [(f"ksize {k}", cv2.medianBlur(noisy20, k)) for k in [3, 5, 7, 9]]
for ax, (title, img) in zip(axes, panels):
ax.imshow(img, cmap="gray", vmin=0, vmax=255)
ax.set_title(title)
ax.axis("off")
plt.tight_layout()
plt.show()
3단계. ksize 와 PSNR 의 관계 그래프
fig, ax = plt.subplots(figsize=(7, 3.6))
# 점 비율마다 선 하나: 가로는 ksize, 세로는 PSNR
for amount, part in table.groupby("점 비율"):
ax.plot(part["ksize"], part["PSNR"], marker="o", label=f"점 {amount}")
ax.set_xticks([3, 5, 7, 9])
ax.set_xlabel("ksize (커널 한 변의 칸 수)")
ax.set_ylabel("PSNR (dB, 클수록 원본에 가깝다)")
ax.legend(loc="upper right")
ax.set_title("Median 필터의 커널 크기와 PSNR")
plt.tight_layout()
plt.show()
4단계. 가는 선도 지워지는지 보기
# 부품 그림: 긴 판(밝기 230) 위에 굵기 1칸의 얇은 금(밝기 120)이 세로로 그어져 있다(가로 110)
parts = cv2.imread("ch07_parts.png", cv2.IMREAD_GRAYSCALE)
med3 = cv2.medianBlur(parts, 3)
print("금 위의 한 칸(세로 160, 가로 110) - 원본:", int(parts[160, 110]), "/ Median 3:", int(med3[160, 110]))
# 판 안(세로 152~198)에서 금의 밝기(120)로 남은 칸 수를 센다
line_before = int((parts[152:199, 110] == 120).sum())
line_after = int((med3[152:199, 110] == 120).sum())
print(f"금으로 남은 칸: 원본 {line_before}칸 → Median 3 {line_after}칸")금 위의 한 칸(세로 160, 가로 110) - 원본: 120 / Median 3: 230
금으로 남은 칸: 원본 47칸 → Median 3 0칸코드 설명
- 점 10% 에서는 ksize 3 이 PSNR 28.67 로 가장 높고, 커널을 키울수록 27.07, 25.14, 23.15 로 낮아집니다. 점은 3 에서 이미 거의 다 지워졌으므로(남은 점 10칸), 더 키우면 글자 모서리만 둥글게 만듭니다.
- 점 20% 에서는 ksize 3 이면 점이 163칸 남고, 5 면 2칸만 남습니다. PSNR 은 24.15 와 24.05 로 거의 같아서, 점이 많을 때는 눈으로 보기에 더 깨끗한 5 를 고를 만합니다. 9 까지 키우면 21.42 로 떨어집니다. 3단계 그래프에서 두 선이 모두 오른쪽으로 내려가는 것이 이 뜻입니다.
cv2.medianBlur(noisy, k)의 k 는 홀수 정수 하나입니다. 3, 5, 7, 9 처럼 키우면 커널이 3×3, 5×5, 7×7, 9×9 가 됩니다.- 4단계에서 부품 위의 얇은 금(굵기 1칸, 판 안에서 47칸)은 Median 3 한 번에 47칸이 모두 사라졌습니다. 금 위의 칸은 120 에서 판의 밝기 230 이 되었습니다. 3×3 아홉 칸 가운데 금은 세 칸뿐이라 늘 소수파이기 때문입니다. 흠집을 찾는 데이터라면 Median 필터가 흠집까지 지울 수 있습니다.