6장. 평균 필터와 Gaussian 필터
커널 크기의 효과
모둠 평균을 세 명으로 내면 그 모둠의 특징이 남고, 서른 명으로 내면 반 전체 평균에 가까워져 모둠마다의 차이가 사라집니다. 커널 크기도 같습니다. 크게 잡을수록 노이즈는 잘 누그러지지만, 글자나 줄처럼 원래 있던 가는 무늬도 함께 흐려집니다.
왼쪽에서 오른쪽으로 커널이 커집니다. 3 은 지글거림이 조금 남지만 글자가 또렷하고, 15 는 고른 면이 매끈하지만 오른쪽 위의 가는 줄무늬가 회색 덩어리가 되었습니다. 여기서 모둠의 사람 수가 커널 크기이고, 모둠마다의 차이가 사진의 가는 무늬입니다.
두 상자는 커널 크기를 고를 때 저울질하는 두 가지입니다. 노이즈를 줄이는 힘과 무늬를 지키는 힘은 서로 반대로 움직이므로, 쓰임에 맞는 가운데 자리를 찾아야 합니다.
크기마다 달라지는 것
| 커널 크기 | 섞는 칸 수 | 노이즈 | 글자·줄무늬 | 계산 |
|---|---|---|---|---|
| 3×3 | 9 | 조금 줄어든다 | 거의 그대로 | 가볍다 |
| 5×5 | 25 | 눈에 띄게 줄어든다 | 조금 흐려진다 | 가볍다 |
| 9×9 | 81 | 많이 줄어든다 | 가는 줄이 뭉개진다 | 보통 |
| 15×15 | 225 | 거의 안 보인다 | 큰 글자만 남는다 | 무겁다 |
크기는 왜 홀수일까
3×3 커널에는 한가운데 칸이 있어서, 새 값을 쓸 자리가 딱 정해집니다. 4×4 커널은 가운데가 네 칸 사이에 끼어 어느 칸에 쓸지 애매합니다. cv2.blur 는 짝수 크기도 받기는 하지만 가운데를 한쪽으로 치우쳐 잡아 결과가 반 칸 밀리고, cv2.GaussianBlur 는 짝수 크기를 아예 오류로 막습니다. 그래서 커널 크기는 3, 5, 7, 9 처럼 홀수로 씁니다.
짧은 예시
import cv2
import numpy as np
img = cv2.imread("ch06_board.png", cv2.IMREAD_GRAYSCALE)
rng = np.random.default_rng(600)
noisy = np.clip(img + rng.normal(0, 20, img.shape), 0, 255).astype(np.uint8)
for k in (3, 5, 9, 15):
out = cv2.blur(noisy, (k, k))
# absdiff: absolute difference(절대 차이). 칸마다 |원본 - 결과| 를 구한다
mae = float(cv2.absdiff(out, img).mean())
print(f"크기 {k:2}: 원본과의 평균 차이 {mae:.2f}")이 코드를 돌리면 크기 3 에서 9.38 로 가장 작고, 15 에서는 22.84 로 오히려 노이즈 낀 그림(15.81)보다 커집니다. 이 안내판 그림은 가는 줄무늬와 글자가 많아서, 크게 흐리면 노이즈보다 무늬를 잃는 손해가 더 큽니다. cv2.absdiff 는 뒤의 "얼마나 좋아졌나 재기"에서 다시 봅니다.
주의할 점
- "클수록 좋다"는 없습니다. 사진에 가는 무늬가 많을수록 작은 커널이 알맞습니다.
img + rng.normal(...)처럼 소수 배열을 더하면 결과가 저절로 소수(float64)가 됩니다. 그래도 자르고(np.clip)uint8로 바꾸는 일은 빼먹지 않습니다.