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

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

프로젝트 1 — 노이즈 종류에 맞는 필터 고르기 표

강사가 "필터는 노이즈에 따라 골라야 한다"는 것을 수업에서 보여 주려고 합니다. 말로 설명하는 대신, 노이즈 세 가지(가우시안, 점, 둘이 섞인 것)마다 네 필터를 같은 조건으로 돌려 차이와 시간을 표로 만들고, 노이즈마다 추천 필터를 자동으로 고른 뒤, 비교 격자 그림 한 장으로 저장하는 스크립트를 만들어 보세요.

쓸 그림은 ch07_parts.png 입니다. 컨베이어 벨트 위에 부품이 놓인 모습을 도형으로 그려 만든 연습용 가상 그림입니다.

파일내용
ch07_parts.png어두운 벨트(밝기 60) 위의 와셔(210), 육각 너트(170), 구멍 뚫린 긴 판(230), 판 위의 얇은 금(120), 글자 "A-07" (320×240, 흑백)
노이즈만드는 법씨앗
가우시안표준편차 20 의 가우시안 노이즈721
점점 비율 10% 의 Salt & Pepper722
섞임표준편차 15 의 가우시안 노이즈(씨앗 723)를 더한 뒤 점 비율 5%(씨앗 724)723, 724

요구사항

  1. ch07_parts.png 를 흑백으로 읽고, 위 표대로 노이즈 세 가지를 만듭니다. 노이즈를 더하는 함수는 실습에서 쓴 것과 같이 씨앗을 받게 합니다.
  2. 네 필터(평균 5×5, Gaussian 5×5, Median 5, Bilateral 9·75·75)를 노이즈마다 돌려, 평균 차이(소수 둘째 자리), PSNR(소수 둘째 자리), 걸린 시간(밀리초, 20번 평균, 소수 셋째 자리)을 표 하나로 찍습니다.
  3. 노이즈마다 PSNR 이 가장 큰 필터를 추천 필터로 고르고, 그 필터가 노이즈 그대로보다 PSNR 을 몇 dB 올렸는지 찍습니다.
  4. 추천 필터가 네 필터 가운데 가장 빠른 필터보다 몇 배 느린지 찍습니다.
  5. 행은 노이즈 세 가지, 열은 "노이즈 그대로"와 네 필터인 3행 5열 격자 그림을 만들고, 칸 제목에 필터 이름과 PSNR 을, 추천 필터의 칸 제목에는 "추천"을 붙여 out_ch07_compare.jpg 로 저장하고 띄웁니다. 칸이 많아 png 로는 파일이 커지므로 jpg 로 저장합니다.

힌트

  1. 노이즈는 {"가우시안": ..., "점": ..., "섞임": ...} 사전에, 필터는 {"평균": lambda im: ..., ...} 사전에 담으면 이중 반복문 하나로 끝납니다.
  2. 섞임 노이즈는 add_salt_pepper(add_gaussian(gray, 15, 723), 0.05, 724) 처럼 함수를 겹쳐 부릅니다.
  3. 표는 pd.DataFrame(rows) 로 만들고, 노이즈마다 1등은 part.loc[part["PSNR"].idxmax()] 로 고릅니다.
  4. 시간은 time.perf_counter() 를 반복문 앞뒤에서 읽어 빼고, 반복 횟수로 나눕니다. 컴퓨터마다 다르므로 결론에는 "몇 배"로 적습니다.
  5. 격자는 plt.subplots(3, 5) 로 만들고, plt.savefig 를 plt.show() 보다 먼저 부릅니다.

채점 기준

항목기준점수
노이즈 만들기세 가지 노이즈를 정한 씨앗으로 만든다15
필터와 재기네 필터를 같은 조건으로 돌려 세 가지 숫자를 정해진 자리수로 찍는다30
추천노이즈마다 PSNR 1등을 고르고 오른 dB 를 찍는다20
시간 비교추천 필터가 가장 빠른 필터의 몇 배인지 찍는다10
격자 그림3행 5열에 PSNR 과 "추천"을 적어 저장하고 띄운다25

코드의 흐름

노이즈 셋 만들기, 필터 넷 돌리기, 재기, 추천, 격자 저장의 다섯 단계를 이은 흐름도

그림의 다섯 단계를 예시 풀이의 단계와 같은 순서로 짰습니다. 둘째 단계에서 네 필터를 모두 같은 노이즈 그림에 돌려야 공정하게 견줄 수 있으므로, 노이즈는 씨앗을 고정해 한 번만 만들고 사전에 담아 둡니다. 넷째 단계의 추천은 PSNR 하나로 고르고, 시간은 따로 찍어 강사가 함께 판단하게 합니다.

실제 데이터로 해 보려면

실제 사진에는 깨끗한 원본이 없으므로, 깨끗하게 찍힌 사진에 이 스크립트처럼 노이즈를 일부러 더해 견주는 방법을 그대로 씁니다. 연습용 사진은 AI Hub(aihub.or.kr)처럼 이미지 데이터를 공개한 곳에서 제조 부품, 문서 같은 주제를 골라 내려받습니다. 내려받기 전에 데이터마다 이용 약관(상업적 이용 가능 여부, 재배포 금지 등)을 확인하고, 사람의 얼굴이나 차량 번호가 찍힌 사진은 개인정보 처리 기준을 먼저 확인합니다. 실제 카메라의 노이즈는 가우시안과 점이 섞인 데다 밝기에 따라 세기가 달라서, 이 장의 결과와 순위가 다를 수 있습니다. 자기 사진으로 다시 재어 고릅니다.

결과 보기 (예시 풀이)

1단계. 노이즈 세 가지 만들기

Python
import time
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

gray = cv2.imread("ch07_parts.png", cv2.IMREAD_GRAYSCALE)

def add_gaussian(src, std, seed):
    """가우시안 노이즈: 칸마다 종 모양 난수를 더하고 0~255 로 자른다."""
    rng = np.random.default_rng(seed)
    return np.clip(src.astype(np.float64) + rng.normal(0, std, src.shape), 0, 255).astype(np.uint8)

def add_salt_pepper(img, amount, seed):
    """Salt & Pepper 노이즈: 칸의 amount 비율을 0 이나 255 로 바꾼다."""
    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

# 요구사항 1: 씨앗을 고정해 한 번만 만들어 둔다
noises = {
    "가우시안": add_gaussian(gray, 20, 721),
    "점": add_salt_pepper(gray, 0.10, 722),
    "섞임": add_salt_pepper(add_gaussian(gray, 15, 723), 0.05, 724),
}
for name, im in noises.items():
    print(f"{name:5} 노이즈 그대로: 평균 차이 {cv2.absdiff(gray, im).mean():.2f}, PSNR {cv2.PSNR(gray, im):.2f}")
출력
가우시안  노이즈 그대로: 평균 차이 15.72, PSNR 22.26
점     노이즈 그대로: 평균 차이 12.70, PSNR 14.81
섞임    노이즈 그대로: 평균 차이 17.61, PSNR 17.07

2단계. 네 필터를 돌려 표 만들기

Python
filters = {
    "평균": lambda im: cv2.blur(im, (5, 5)),
    "Gaussian": lambda im: cv2.GaussianBlur(im, (5, 5), 0),
    "Median": lambda im: cv2.medianBlur(im, 5),
    "Bilateral": lambda im: cv2.bilateralFilter(im, 9, 75, 75),
}

def time_ms(func, im, repeat=20):
    start = time.perf_counter()
    for _ in range(repeat):
        func(im)
    return (time.perf_counter() - start) / repeat * 1000

# 요구사항 2: 노이즈 × 필터마다 세 숫자를 잰다
rows, outputs = [], {}
for nname, noisy in noises.items():
    for fname, func in filters.items():
        out = func(noisy)
        outputs[(nname, fname)] = out
        rows.append({
            "노이즈": nname,
            "필터": fname,
            "평균 차이": round(float(cv2.absdiff(gray, out).mean()), 2),
            "PSNR": round(cv2.PSNR(gray, out), 2),
            "시간(ms)": round(time_ms(func, noisy), 3),
        })
table = pd.DataFrame(rows)
print(table.to_string(index=False))
출력
 노이즈        필터  평균 차이  PSNR  시간(ms)
가우시안        평균   6.94 25.16   0.159
가우시안  Gaussian   6.44 27.31   0.076
가우시안    Median   5.39 29.05   0.321
가우시안 Bilateral   4.15 31.61   2.282
   점        평균  12.18 22.91   0.163
   점  Gaussian  11.64 23.16   0.061
   점    Median   0.66 30.93   0.400
   점 Bilateral   9.93 17.97   2.395
  섞임        평균   9.45 24.10   0.157
  섞임  Gaussian   9.01 25.01   0.059
  섞임    Median   4.50 29.74   0.397
  섞임 Bilateral   6.56 21.91   2.368

3단계. 노이즈별 추천 필터 고르기

Python
# 요구사항 3, 4: PSNR 1등을 추천하고, 오른 dB 와 속도를 함께 적는다
speed = table.groupby("필터", sort=False)["시간(ms)"].mean()
fastest_name = speed.idxmin()
picks = {}
for nname, part in table.groupby("노이즈", sort=False):
    best = part.loc[part["PSNR"].idxmax()]
    picks[nname] = best["필터"]
    gain = best["PSNR"] - cv2.PSNR(gray, noises[nname])          # 노이즈 그대로보다 오른 dB
    times = speed[best["필터"]] / speed[fastest_name]              # 가장 빠른 필터의 몇 배
    print(f"[{nname}] 추천: {best['필터']} (PSNR {best['PSNR']}, {gain:+.2f} dB) "
          f"/ 가장 빠른 {fastest_name} 의 {times:.1f}배 시간")
출력
[가우시안] 추천: Bilateral (PSNR 31.61, +9.35 dB) / 가장 빠른 Gaussian 의 35.9배 시간
[점] 추천: Median (PSNR 30.93, +16.12 dB) / 가장 빠른 Gaussian 의 5.7배 시간
[섞임] 추천: Median (PSNR 29.74, +12.67 dB) / 가장 빠른 Gaussian 의 5.7배 시간

4단계. 비교 격자 그림 저장하기

Python
fig, axes = plt.subplots(3, 5, figsize=(15, 7.2))
for r, (nname, noisy) in enumerate(noises.items()):
    axes[r, 0].imshow(noisy, cmap="gray", vmin=0, vmax=255)
    axes[r, 0].set_title(f"{nname} ({cv2.PSNR(gray, noisy):.1f} dB)")
    for c, fname in enumerate(filters, start=1):
        psnr = cv2.PSNR(gray, outputs[(nname, fname)])
        mark = " 추천" if picks[nname] == fname else ""
        axes[r, c].imshow(outputs[(nname, fname)], cmap="gray", vmin=0, vmax=255)
        # 추천 칸은 제목을 굵게 해 눈에 띄게 한다
        axes[r, c].set_title(f"{fname} ({psnr:.1f} dB){mark}",
                             fontweight="bold" if mark else "normal")
for ax in axes.flat:
    ax.axis("off")
plt.tight_layout()
# 요구사항 5: 저장을 먼저 하고 띄운다
# 칸이 15개라 png 로 저장하면 수백 KB 가 되므로, 사진에 알맞은 jpg 로 작게 저장한다
# pil_kwargs: 그림 저장을 맡는 Pillow 에 넘길 설정. quality 는 jpg 품질(2차시)
plt.savefig("out_ch07_compare.jpg", dpi=80, pil_kwargs={"quality": 85})
print("저장: out_ch07_compare.jpg")
plt.show()
출력
저장: out_ch07_compare.jpg
부품 그림의 세 가지 노이즈에 네 필터를 건 결과를 3행 5열로 놓고 PSNR 과 추천을 적은 비교 격자

표를 보면 노이즈마다 1등이 다릅니다. 가우시안 노이즈에서는 Bilateral 이 PSNR 31.61 로 노이즈 그대로(22.26)보다 9.35 dB 올려 추천되었고, Median(29.05), Gaussian(27.31), 평균(25.16) 순이었습니다. 점 노이즈에서는 Median 이 30.93 으로 16.12 dB 올렸고, Bilateral 은 17.97 로 노이즈 그대로(14.81)와 큰 차이가 없습니다. 섞임 노이즈도 Median 이 29.74 로 추천되었습니다. 섞인 점이 남아 있으면 다른 세 필터는 점을 번지게 하거나 그대로 두기 때문입니다. Bilateral 은 섞임에서 21.91 로 넷 가운데 꼴찌입니다. 1등은 평균 차이로 매겨도 같았지만, 점과 섞임 노이즈에서 Bilateral 은 평균 차이로는 2등(9.93, 6.56), PSNR 로는 꼴찌였습니다. 대부분의 칸은 거의 건드리지 않아 평균 차이는 작지만, 남겨 둔 점이 0 과 255 로 크게 틀려 제곱을 쓰는 PSNR 에서 무겁게 셈해지기 때문입니다. 6장의 프로젝트에서 본 것처럼 두 잣대가 다른 답을 낼 수 있으니 둘을 함께 봅니다. 시간은 교안 PC 에서 Gaussian 이 가장 빨랐고, Median 은 그 다섯 배 남짓, Bilateral 은 서른 배 넘게 걸렸습니다. 숫자는 컴퓨터마다 다르지만 이 그림 한 장에 몇 밀리초라, 그림이 크거나 아주 많을 때만 시간을 따로 따져 보면 됩니다. 수업에서 함께 짚을 점이 하나 더 있습니다. 격자 그림을 크게 보면 Median 칸에서는 판 위의 얇은 금이 거의 사라졌습니다(실습 3). PSNR 1등이라도 흠집을 찾아야 하는 사진이면 그대로 쓰기 어렵다는 뜻입니다.

← 이전 절7장 · 실습 5 — 네 필터와 두 노이즈 비교표다음 절 →7장 · 정리