7장. Median 과 Bilateral 필터, 필터 비교
프로젝트 1 — 노이즈 종류에 맞는 필터 고르기 표
강사가 "필터는 노이즈에 따라 골라야 한다"는 것을 수업에서 보여 주려고 합니다. 말로 설명하는 대신, 노이즈 세 가지(가우시안, 점, 둘이 섞인 것)마다 네 필터를 같은 조건으로 돌려 차이와 시간을 표로 만들고, 노이즈마다 추천 필터를 자동으로 고른 뒤, 비교 격자 그림 한 장으로 저장하는 스크립트를 만들어 보세요.
쓸 그림은 ch07_parts.png 입니다. 컨베이어 벨트 위에 부품이 놓인 모습을 도형으로 그려 만든 연습용 가상 그림입니다.
| 파일 | 내용 |
|---|---|
ch07_parts.png | 어두운 벨트(밝기 60) 위의 와셔(210), 육각 너트(170), 구멍 뚫린 긴 판(230), 판 위의 얇은 금(120), 글자 "A-07" (320×240, 흑백) |
| 노이즈 | 만드는 법 | 씨앗 |
|---|---|---|
| 가우시안 | 표준편차 20 의 가우시안 노이즈 | 721 |
| 점 | 점 비율 10% 의 Salt & Pepper | 722 |
| 섞임 | 표준편차 15 의 가우시안 노이즈(씨앗 723)를 더한 뒤 점 비율 5%(씨앗 724) | 723, 724 |
요구사항
ch07_parts.png를 흑백으로 읽고, 위 표대로 노이즈 세 가지를 만듭니다. 노이즈를 더하는 함수는 실습에서 쓴 것과 같이 씨앗을 받게 합니다.- 네 필터(평균 5×5, Gaussian 5×5, Median 5, Bilateral 9·75·75)를 노이즈마다 돌려, 평균 차이(소수 둘째 자리), PSNR(소수 둘째 자리), 걸린 시간(밀리초, 20번 평균, 소수 셋째 자리)을 표 하나로 찍습니다.
- 노이즈마다 PSNR 이 가장 큰 필터를 추천 필터로 고르고, 그 필터가 노이즈 그대로보다 PSNR 을 몇 dB 올렸는지 찍습니다.
- 추천 필터가 네 필터 가운데 가장 빠른 필터보다 몇 배 느린지 찍습니다.
- 행은 노이즈 세 가지, 열은 "노이즈 그대로"와 네 필터인 3행 5열 격자 그림을 만들고, 칸 제목에 필터 이름과 PSNR 을, 추천 필터의 칸 제목에는 "추천"을 붙여
out_ch07_compare.jpg로 저장하고 띄웁니다. 칸이 많아 png 로는 파일이 커지므로 jpg 로 저장합니다.
힌트
- 노이즈는
{"가우시안": ..., "점": ..., "섞임": ...}사전에, 필터는{"평균": lambda im: ..., ...}사전에 담으면 이중 반복문 하나로 끝납니다. - 섞임 노이즈는
add_salt_pepper(add_gaussian(gray, 15, 723), 0.05, 724)처럼 함수를 겹쳐 부릅니다. - 표는
pd.DataFrame(rows)로 만들고, 노이즈마다 1등은part.loc[part["PSNR"].idxmax()]로 고릅니다. - 시간은
time.perf_counter()를 반복문 앞뒤에서 읽어 빼고, 반복 횟수로 나눕니다. 컴퓨터마다 다르므로 결론에는 "몇 배"로 적습니다. - 격자는
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단계. 노이즈 세 가지 만들기
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.072단계. 네 필터를 돌려 표 만들기
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.3683단계. 노이즈별 추천 필터 고르기
# 요구사항 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단계. 비교 격자 그림 저장하기
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
표를 보면 노이즈마다 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등이라도 흠집을 찾아야 하는 사진이면 그대로 쓰기 어렵다는 뜻입니다.