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

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

실습 4 — Bilateral 인자 바꿔 보기

가우시안 노이즈(표준편차 20)를 더한 컬러 표지판에 Gaussian 필터와 Bilateral 필터를 걸어, 노이즈를 얼마나 줄이고 경계를 얼마나 지키는지 견줍니다. 한 줄 밝기 그래프로 경계를 보고, sigmaColor 를 바꿔 가며 숫자가 어떻게 변하는지 표로 봅니다.

1단계. Gaussian 과 Bilateral 견주기

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

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

img = cv2.imread("ch07_sign.png")          # 이번에는 컬러로 읽는다

def add_gaussian(src, std, seed):
    """평균 0, 표준편차 std 의 가우시안 노이즈를 칸마다 더하고 0~255 로 자른다."""
    rng = np.random.default_rng(seed)
    # normal(평균, 표준편차, 모양): 종 모양으로 퍼진 난수
    noise = rng.normal(0, std, src.shape)
    # 6차시와 같은 꼴: 소수로 바꿔 더하고, clip 으로 0~255 에 잘라 넣은 뒤 uint8 로 바꾼다
    return np.clip(src.astype(np.float64) + noise, 0, 255).astype(np.uint8)

noisy = add_gaussian(img, 20, 712)
gauss = cv2.GaussianBlur(noisy, (5, 5), 0)          # 6차시의 Gaussian 필터
bilat = cv2.bilateralFilter(noisy,                   # src: 컬러 그림
                            9,                       # d: 지름 9칸
                            75,                      # sigmaColor
                            75)                      # sigmaSpace
for name, out in [("노이즈 그대로", noisy), ("Gaussian 5×5", gauss), ("Bilateral 9·75·75", bilat)]:
    print(f"{name:18} PSNR {cv2.PSNR(img, out):.2f}")

crop = (slice(50, 112), slice(44, 160))     # 글자 "GAT" 둘레
fig, axes = plt.subplots(1, 4, figsize=(12, 2.4))
for ax, (title, im) in zip(axes, [("원본", img), ("노이즈", noisy), ("Gaussian", gauss), ("Bilateral", bilat)]):
    ax.imshow(cv2.cvtColor(im[crop], cv2.COLOR_BGR2RGB))   # 컬러는 RGB 로 바꿔 띄운다
    ax.set_title(title)
    ax.axis("off")
plt.tight_layout()
plt.show()
출력
노이즈 그대로            PSNR 22.42
Gaussian 5×5       PSNR 22.98
Bilateral 9·75·75  PSNR 33.89
원본, 가우시안 노이즈, Gaussian 필터, Bilateral 필터를 건 표지판 글자 부분을 나란히 놓은 결과

2단계. 한 줄 밝기 그래프로 경계 보기

Python
# 흑백으로 바꿔 세로 80 줄(글자 G·A·T 를 가로지르는 줄)의 밝기를 꺼낸다
def row_of(im):
    return cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)[80, 40:170]

x = np.arange(40, 170)
fig, ax = plt.subplots(figsize=(10, 3.6))
ax.plot(x, row_of(img), label="원본", linewidth=2)
ax.plot(x, row_of(gauss), label="Gaussian 5×5")
ax.plot(x, row_of(bilat), label="Bilateral 9·75·75")
ax.set_ylim(0, 320)                       # 위쪽에 범례 자리를 둔다
ax.set_xlabel("가로 칸 번호 (세로 80 줄)")
ax.set_ylabel("밝기")
ax.legend(loc="upper center", ncol=3)
plt.tight_layout()
plt.show()

# 글자 G 의 왼쪽 테두리 둘레(가로 54~66) 값을 두 칸 걸러 찍는다
for name, im in [("원본", img), ("Gaussian", gauss), ("Bilateral", bilat)]:
    print(f"{name:9}", [int(v) for v in cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)[80, 54:67:2]])
출력
원본        [90, 90, 219, 250, 250, 250, 90]
Gaussian  [93, 100, 186, 244, 243, 214, 113]
Bilateral [92, 92, 218, 241, 244, 246, 90]
세로 80 줄의 밝기를 원본, Gaussian, Bilateral 로 겹쳐 그린 선 그래프

3단계. sigmaColor 를 바꿔 가며 재기

Python
import pandas as pd

def mid_ratio(im):
    """표지판 안쪽에서 초록(90 안팎)도 흰 글자(250 안팎)도 아닌 중간 밝기(121~219) 칸의 비율.
    경계가 번질수록 중간 밝기 칸이 늘어난다."""
    g = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)[45:158, 45:230]
    return round(float(((g > 120) & (g < 220)).mean()), 3)

rows = [{"설정": "원본", "PSNR": "-", "중간 밝기 비율": mid_ratio(img)},
        {"설정": "Gaussian 5×5", "PSNR": round(cv2.PSNR(img, gauss), 2), "중간 밝기 비율": mid_ratio(gauss)}]
for sc in [10, 40, 75, 150]:
    out = cv2.bilateralFilter(noisy, 9, sc, 75)      # sigmaColor 만 바꾼다
    rows.append({"설정": f"Bilateral sigmaColor {sc}",
                 "PSNR": round(cv2.PSNR(img, out), 2),
                 "중간 밝기 비율": mid_ratio(out)})
print(pd.DataFrame(rows).to_string(index=False))
출력
                      설정   PSNR  중간 밝기 비율
                      원본      -     0.037
            Gaussian 5×5  22.98     0.106
 Bilateral sigmaColor 10   22.6     0.042
 Bilateral sigmaColor 40  29.43     0.027
 Bilateral sigmaColor 75  33.89     0.014
Bilateral sigmaColor 150  32.79     0.014

4단계. sigmaColor 별 그림 보기

Python
fig, axes = plt.subplots(1, 4, figsize=(12, 2.4))
for ax, sc in zip(axes, [10, 40, 75, 150]):
    out = cv2.bilateralFilter(noisy, 9, sc, 75)
    ax.imshow(cv2.cvtColor(out[crop], cv2.COLOR_BGR2RGB))
    ax.set_title(f"sigmaColor {sc}")
    ax.axis("off")
plt.tight_layout()
plt.show()
sigmaColor 10, 40, 75, 150 으로 Bilateral 필터를 건 표지판 글자 부분을 나란히 놓은 결과

코드 설명

  • add_gaussian 은 rng.normal(0, 20, 모양) 으로 칸마다 플러스·마이너스 난수를 더하고 np.clip 으로 0~255 를 넘는 값을 잘라 냅니다. 컬러 그림이면 B, G, R 채널에 따로 난수가 더해집니다.
  • 1단계에서 PSNR 은 노이즈 그대로 22.42, Gaussian 5×5 는 22.98 로 조금밖에 오르지 않았고, Bilateral 은 33.89 로 11 넘게 올랐습니다. 이 그림은 글자와 테두리가 많아서, Gaussian 필터가 노이즈를 줄여 얻는 만큼을 경계를 흐려 거의 다 잃습니다.
  • 2단계에서 글자 G 의 왼쪽 테두리를 보면 원본은 90, 90, 219, 250 으로 곧장 오릅니다. Gaussian 은 93, 100, 186, 244 로 비탈이 생기고, 오른쪽 테두리도 214, 113 으로 번졌습니다. Bilateral 은 92, 92, 218, 241 이고 오른쪽 끝도 90 으로, 원본의 계단에 가깝습니다.
  • 3단계에서 sigmaColor 10 은 PSNR 22.60 으로 노이즈 그대로(22.42)와 거의 같습니다. 노이즈의 흔들림(표준편차 20)보다 작아 이웃을 거의 섞지 않기 때문입니다. 40 에서 29.43, 75 에서 33.89 로 가장 높고, 150 은 32.79 로 조금 내려갑니다. 중간 밝기 비율은 Gaussian 이 0.106 으로 원본(0.037)의 세 배 가까이 늘어 경계가 번졌음을 보여 줍니다. Bilateral 은 40 이상에서 0.027~0.014 로 원본보다도 작은데, 원본의 테두리를 부드럽게 그린 중간 칸들이 양쪽 가운데 더 비슷한 쪽 밝기로 끌려갔기 때문입니다.
← 이전 절7장 · 실습 3 — Median 커널 크기별 결과와 원본과의 차이다음 절 →7장 · 실습 5 — 네 필터와 두 노이즈 비교표