OpenCV 비전 실무6장 · 평균 필터와 Gaussian 필터17 / 21← 이전목차다음 →MetaContents

6장. 평균 필터와 Gaussian 필터

실습 4 — Gaussian 필터와 sigma

cv2.getGaussianKernel 로 Gaussian 무게를 숫자로 보고 파스칼 삼각형과 맞춰 본 뒤, 크기 3 무게로 손계산한 값을 cv2.GaussianBlur 와 견줍니다. 마지막으로 시그마를 바꿔 가며 노이즈 낀 그림에 걸어 결과를 견줍니다.

1단계. 무게를 숫자로 보기

Python
import cv2
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

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

# getGaussianKernel(크기, 시그마): (크기, 1) 모양의 한 줄 무게. 시그마 0 은 크기에 맞춰 스스로 정한다
rows = []
for sigma in (0, 1, 2):
    w = cv2.getGaussianKernel(5, sigma).ravel()
    rows.append({"시그마": sigma, **{f"칸{i}": round(float(v), 3) for i, v in enumerate(w)},
                 "합": round(float(w.sum()), 3)})
print(pd.DataFrame(rows).to_string(index=False))

# 시그마 0 인 크기 5 무게에 16 을 곱하면 파스칼 삼각형의 넷째 줄이 나온다
w5 = cv2.getGaussianKernel(5, 0).ravel()
print("16 을 곱한 무게:", [round(float(v) * 16) for v in w5])
출력
 시그마    칸0    칸1    칸2    칸3    칸4   합
   0 0.062 0.250 0.375 0.250 0.062 1.0
   1 0.054 0.244 0.403 0.244 0.054 1.0
   2 0.152 0.222 0.251 0.222 0.152 1.0
16 을 곱한 무게: [1, 4, 6, 4, 1]

2단계. 1-2-1 무게로 손계산하고 견주기

Python
# 실습 2와 같은 5×5 그림: 고른 면(100) 가운데에 튄 칸(145)
a = np.full((5, 5), 100, dtype=np.uint8)
a[1:4, 1:4] = [[98, 103, 101], [97, 145, 99], [102, 100, 96]]

# 가로 무게(3×1)와 세로 무게(1×3)를 곱해 3×3 무게를 만든다. @ 는 줄끼리 곱해 표를 만드는 기호
g3 = cv2.getGaussianKernel(3, 0)
w33 = g3 @ g3.T
print("3×3 무게 × 16:", (w33 * 16).round().astype(int).tolist())

# 손계산: 무게를 곱해 모두 더한다
patch = a[1:4, 1:4].astype(np.float64)
hand = float((patch * w33).sum())
print("손계산:", round(hand, 2), "→ 반올림", round(hand))

# GaussianBlur(그림, 크기, 시그마): 같은 무게로 OpenCV 가 계산한다
print("GaussianBlur:", int(cv2.GaussianBlur(a, (3, 3), 0)[2, 2]))
print("같은 크기의 평균 필터:", int(cv2.blur(a, (3, 3))[2, 2]))
출력
3×3 무게 × 16: [[1, 2, 1], [2, 4, 2], [1, 2, 1]]
손계산: 110.94 → 반올림 111
GaussianBlur: 111
같은 크기의 평균 필터: 105

3단계. 시그마를 바꿔 노이즈 낀 그림에 걸기

Python
img = cv2.imread("ch06_board.png", cv2.IMREAD_GRAYSCALE)
rng = np.random.default_rng(600)
noisy = np.clip(img.astype(np.float64) + rng.normal(0, 20, img.shape), 0, 255).astype(np.uint8)
flat = (slice(165, 215), slice(30, 60))   # 실습 3과 같은 고른 면

rows, panels = [], [("노이즈 (표준편차 20)", noisy)]
for sigma in (1, 2, 4):
    # 크기는 9 로 두고 시그마만 바꾼다. 시그마가 클수록 무게가 넓게 퍼진다
    out = cv2.GaussianBlur(noisy, (9, 9), sigma)
    rows.append({"시그마": sigma,
                 "평균차이": round(float(cv2.absdiff(out, img).mean()), 2),
                 "흔들림": round(float(out[flat].std()), 2)})
    panels.append((f"Gaussian 9×9, 시그마 {sigma}", out))
print(pd.DataFrame(rows).to_string(index=False))

fig, axes = plt.subplots(1, 4, figsize=(12, 2.8))
for ax, (title, arr) in zip(axes, panels):
    ax.imshow(arr, cmap="gray", vmin=0, vmax=255)
    ax.set_title(title, fontsize=10)
    ax.axis("off")
plt.tight_layout()
plt.show()
출력
 시그마  평균차이  흔들림
   1  8.82 5.84
   2 11.95 3.16
   4 14.85 2.31
노이즈 낀 안내판과 크기 9 의 Gaussian 필터를 시그마 1, 2, 4 로 건 결과를 나란히 놓은 그림

코드 설명

  • cv2.getGaussianKernel(5, sigma) 의 무게는 시그마가 무엇이든 합이 1 입니다. 시그마 0 이면 가운데 칸이 0.375 이고, 16 을 곱하면 1, 4, 6, 4, 1 로 파스칼 삼각형의 넷째 줄과 같습니다.
  • 시그마가 커질수록 가운데 무게가 0.403(시그마 1)에서 0.251(시그마 2)로 줄고 양 끝 무게가 늘어, 평균 필터처럼 고르게 퍼집니다.
  • g3 @ g3.T 로 만든 3×3 무게에 16 을 곱하면 1-2-1 / 2-4-2 / 1-2-1 입니다. 손계산 110.94(반올림 111)와 cv2.GaussianBlur 의 결과 111 이 같고, 같은 크기의 평균 필터(105)보다 튄 값이 더 남습니다.
  • 크기 9 에서 시그마를 1, 2, 4 로 키우면 고른 면의 흔들림은 5.84, 3.16, 2.31 로 줄고, 원본과의 평균 차이는 8.82, 11.95, 14.85 로 커집니다. 시그마를 키우면 커널 크기를 키운 것처럼 노이즈는 더 지워지지만 무늬가 더 흐려집니다. 시그마 1 의 평균 차이 8.82 는 실습 3의 어느 평균 필터(가장 작은 값 9.38)보다도 작습니다.
← 이전 절6장 · 실습 3 — 커널 크기별 평균 필터다음 절 →6장 · 실습 5 — 직접 만든 커널과 경계 견주기