6장. 평균 필터와 Gaussian 필터
실습 5 — 직접 만든 커널과 경계 견주기
cv2.filter2D 에 평균 커널을 직접 만들어 넣고 cv2.blur 와 같은지 확인합니다. 커널 숫자의 합이 1 이 아니면 밝기가 어떻게 바뀌는지 보고, 마지막으로 평균 필터와 Gaussian 필터가 경계 한 줄을 어떻게 바꾸는지 선 그래프로 견줍니다.
1단계. 평균 커널을 직접 만들어 걸기
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("ch06_board.png", cv2.IMREAD_GRAYSCALE)
# 아홉 칸 모두 1/9 인 평균 커널. filter2D 는 소수(float32) 커널을 받는다
mean_kernel = np.ones((3, 3), dtype=np.float32) / 9
print("커널 숫자의 합:", round(float(mean_kernel.sum()), 3))
# filter2D(그림, 결과 자료형, 커널): -1 은 원본과 같은 자료형(uint8)
mine = cv2.filter2D(img, -1, mean_kernel)
print("cv2.blur 와 똑같은가:", np.array_equal(mine, cv2.blur(img, (3, 3))))
# 크기 5 도 같은지 본다
mean5 = np.ones((5, 5), dtype=np.float32) / 25
print("크기 5 도 똑같은가:", np.array_equal(cv2.filter2D(img, -1, mean5), cv2.blur(img, (5, 5))))커널 숫자의 합: 1.0
cv2.blur 와 똑같은가: True
크기 5 도 똑같은가: True2단계. 커널 숫자의 합과 밝기
# 합이 2, 1, 0.5 인 커널 세 개를 견준다
kernels = {
"합 2 (모두 1/4.5)": np.ones((3, 3), dtype=np.float32) / 4.5,
"합 1 (모두 1/9)": np.ones((3, 3), dtype=np.float32) / 9,
"합 0.5 (모두 1/18)": np.ones((3, 3), dtype=np.float32) / 18,
}
print("원본의 평균 밝기:", round(float(img.mean()), 1))
for name, k in kernels.items():
out = cv2.filter2D(img, -1, k)
# 255 로 잘린 칸의 비율: 합이 1 보다 크면 밝은 칸이 넘쳐 잘린다
print(f"{name}: 평균 밝기 {float(out.mean()):.1f}, 255 인 칸 비율 {float((out == 255).mean()):.3f}")
# 가운데를 강조한 정수 무게 1-2-1 커널. 합 16 으로 나눠 합을 1 로 맞춘다
center = np.array([[1, 2, 1],
[2, 4, 2],
[1, 2, 1]], dtype=np.float32) / 16
by_hand = cv2.filter2D(img, -1, center)
gauss3 = cv2.GaussianBlur(img, (3, 3), 0)
diff = cv2.absdiff(by_hand, gauss3)
print("GaussianBlur(3) 와 가장 크게 다른 값:", int(diff.max()), "/ 다른 칸 수:", int((diff > 0).sum()))원본의 평균 밝기: 123.5
합 2 (모두 1/4.5): 평균 밝기 190.1, 255 인 칸 비율 0.541
합 1 (모두 1/9): 평균 밝기 123.5, 255 인 칸 비율 0.000
합 0.5 (모두 1/18): 평균 밝기 61.8, 255 인 칸 비율 0.000
GaussianBlur(3) 와 가장 크게 다른 값: 1 / 다른 칸 수: 14273단계. 경계 한 줄의 밝기 견주기
row = 165 # 어두운 바닥(60)과 밝은 바닥(200)이 만나는 가로줄
xs = np.arange(130, 190) # 경계(가로 160) 둘레의 60칸
mean9 = cv2.blur(img, (9, 9))
gauss9 = cv2.GaussianBlur(img, (9, 9), 0)
# 경계가 번진 폭: 밝기가 70~190 인 칸 수
for name, arr in [("원본", img), ("평균 9×9", mean9), ("Gaussian 9×9", gauss9)]:
line = arr[row, 130:190]
print(f"{name}: 번진 폭 {int(((line > 70) & (line < 190)).sum())}칸")
fig, ax = plt.subplots(figsize=(8, 3.6))
ax.plot(xs, img[row, 130:190], label="원본")
ax.plot(xs, mean9[row, 130:190], label="평균 필터 9×9")
ax.plot(xs, gauss9[row, 130:190], label="Gaussian 9×9")
ax.set_xlabel("가로 칸 번호")
ax.set_ylabel("밝기")
ax.set_ylim(40, 240)
ax.legend(loc="upper left")
ax.set_title("경계 한 줄의 밝기 (가로줄 165)")
plt.tight_layout()
plt.show()원본: 번진 폭 0칸
평균 9×9: 번진 폭 8칸
Gaussian 9×9: 번진 폭 4칸
코드 설명
cv2.filter2D(img, -1, mean_kernel)은 직접 만든 1/9 커널로 필터를 겁니다. 크기 3 과 5 모두cv2.blur와 똑같습니다(True).cv2.blur가 하는 일이 바로 "모든 칸이 같은 커널로 곱해 더하기"라는 뜻입니다.- 커널 숫자의 합이 2 면 평균 밝기가 123.5 에서 190.1 로 오르고 0.541(절반 넘는 칸)이 255 로 잘립니다. 합이 0.5 면 61.8 로 절반쯤 어두워집니다. 노이즈만 줄이려면 합을 1 로 맞춥니다.
- 1-2-1 정수 무게를 16 으로 나눈 커널은
cv2.GaussianBlur(img, (3, 3), 0)과 거의 같고, 반올림 방식 차이로 끝자리가 1 다른 칸이 1,427개 있습니다. - 경계 한 줄을 보면 번진 폭이 평균 필터 8칸, Gaussian 4칸입니다. 그래프에서 평균 필터(주황)는 가로 155~164 에 곧은 비탈로 넓게 번졌고, Gaussian(초록)은 경계 가까이에서 가파르게 올라 원본(파랑)의 계단에 더 가깝습니다. 그래프의 선 색은 Matplotlib 의 기본 색입니다.