OpenCV 비전 실무7장 · Median 과 Bilateral 필터, 필터 비교15 / 21← 이전목차다음 →MetaContents

7장. Median 과 Bilateral 필터, 필터 비교

실습 2 — 평균 필터와 Median 필터: 점 노이즈에서 견주기

작은 배열로 평균과 중앙값을 손계산과 대조한 뒤, 점 비율 10% 의 표지판에 평균 필터와 Median 필터를 걸어 원본과의 차이를 견줍니다. 글자 부분을 확대해 얼룩과 깨끗함을 눈으로도 봅니다.

1단계. 아홉 칸으로 평균과 중앙값 대조하기

Python
import cv2
import numpy as np

# 본문 손계산의 아홉 칸: 하얀 점(255) 하나가 섞였다
nine = np.array([90, 92, 93, 95, 255, 91, 94, 96, 93])
print("줄 세우기:", [int(v) for v in np.sort(nine)])
print("평균:", round(float(nine.mean()), 1), "/ 중앙값:", float(np.median(nine)))

# 하얀 점이 셋으로 늘면?
three = np.array([90, 255, 93, 95, 255, 91, 255, 96, 93])
print("점 셋 - 평균:", round(float(three.mean()), 1), "/ 중앙값:", float(np.median(three)))
출력
줄 세우기: [90, 91, 92, 93, 93, 94, 95, 96, 255]
평균: 111.0 / 중앙값: 93.0
점 셋 - 평균: 147.0 / 중앙값: 95.0

2단계. 5×5 그림에 두 필터 걸기

Python
# 밝기 100 의 고른 판에 하얀 점과 검은 점을 하나씩
tiny = np.full((5, 5), 100, dtype=np.uint8)
tiny[2, 2] = 255
tiny[0, 4] = 0

# blur = 평균 필터(6차시), (3, 3) 은 커널의 (가로, 세로)
print("평균 필터 3×3:")
print(cv2.blur(tiny, (3, 3)))
# medianBlur 의 둘째 인자는 홀수 하나
print("Median 필터 3:")
print(cv2.medianBlur(tiny, 3))
출력
평균 필터 3×3:
[[100 100 100  89  89]
 [100 117 117 106  89]
 [100 117 117 117 100]
 [100 117 117 117 100]
 [100 100 100 100 100]]
Median 필터 3:
[[100 100 100 100 100]
 [100 100 100 100 100]
 [100 100 100 100 100]
 [100 100 100 100 100]
 [100 100 100 100 100]]

3단계. 표지판에서 원본과의 차이 재기

Python
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

noisy = add_salt_pepper(gray, 0.10, 711)

def mae(a, b):
    """평균 절대 차이: absdiff 로 칸마다 |a - b| 를 구해 평균한다."""
    return round(float(cv2.absdiff(a, b).mean()), 2)

results = {
    "노이즈 그대로": noisy,
    "평균 3×3": cv2.blur(noisy, (3, 3)),
    "평균 5×5": cv2.blur(noisy, (5, 5)),
    "Median 3": cv2.medianBlur(noisy, 3),
    "Median 5": cv2.medianBlur(noisy, 5),
}
for name, out in results.items():
    # 남은 점: 결과에 0 이나 255 로 남은 칸
    left = int((out == 0).sum() + (out == 255).sum())
    print(f"{name:10} 평균 차이 {mae(gray, out):6.2f}  남은 점 {left:5d}")
출력
노이즈 그대로    평균 차이  12.50  남은 점  7557
평균 3×3     평균 차이  14.61  남은 점     0
평균 5×5     평균 차이  16.24  남은 점     0
Median 3   평균 차이   0.80  남은 점    10
Median 5   평균 차이   1.17  남은 점     0

4단계. 글자 부분 확대해 보기

Python
import matplotlib.pyplot as plt

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

# 글자 "GATE" 둘레만 잘라 크게 본다: [세로 시작:끝, 가로 시작:끝]
crop = (slice(50, 110), slice(40, 170))
panels = [("원본", gray), ("점 10%", noisy),
          ("평균 5×5", results["평균 5×5"]), ("Median 5", results["Median 5"])]
fig, axes = plt.subplots(1, 4, figsize=(12, 2.6))
for ax, (title, img) in zip(axes, panels):
    ax.imshow(img[crop], cmap="gray", vmin=0, vmax=255)
    ax.set_title(title)
    ax.axis("off")
plt.tight_layout()
plt.show()
원본, 점 10%, 평균 5×5, Median 5 를 건 표지판의 글자 부분을 나란히 확대한 결과

코드 설명

  • 1단계는 본문의 손계산 그대로입니다. 점 하나면 평균이 111 로 튀지만 중앙값은 93 이고, 점이 셋이어도 중앙값은 95 에 머뭅니다.
  • 2단계에서 cv2.blur 는 두 점을 둘레 칸까지 117 과 89 로 번지게 하고, cv2.medianBlur 는 25칸을 모두 100 으로 되돌립니다.
  • 3단계에서 평균 필터는 0 과 255 로 남은 점을 하나도 남기지 않았지만, 평균 차이는 노이즈 그대로(12.50)보다 오히려 커졌습니다(3×3 은 14.61, 5×5 는 16.24). 점을 지운 것이 아니라 둘레 칸에 회색 얼룩으로 펴 바르고 글자까지 흐렸기 때문입니다. Median 3 은 0.80, Median 5 는 1.17 로 크게 줄였습니다. Median 3 은 점이 10칸 남았는데도 차이가 더 작은데, 커널이 작아 글자를 덜 건드렸기 때문입니다.
  • cv2.absdiff(a, b).mean() 이 평균 절대 차이입니다. uint8 끼리 그냥 빼면 음수가 255 쪽으로 돌아가므로 absdiff 를 씁니다.
← 이전 절7장 · 실습 1 — Salt & Pepper 노이즈 만들기다음 절 →7장 · 실습 3 — Median 커널 크기별 결과와 원본과의 차이