7장. Median 과 Bilateral 필터, 필터 비교
Bilateral 필터
조별 과제에서 "우리 조의 의견"을 정한다고 해 봅시다. 교실 전체의 의견을 다 섞으면 다른 조 생각까지 끼어듭니다. 그래서 가까이 앉은 친구이면서 같은 조인 친구의 의견만 크게 듣고, 다른 조 친구의 의견은 거의 듣지 않습니다. Bilateral 필터가 이렇게 섞습니다. 가까운 칸이면서 밝기가 비슷한 칸만 크게 섞고, 경계 건너편처럼 밝기가 크게 다른 칸은 거의 섞지 않습니다.
첫째 상자는 Gaussian 필터에서 본 거리 무게입니다. 둘째 상자가 Bilateral 필터에 더해진 색 차이 무게입니다. 셋째 상자처럼 두 무게를 곱하므로, 둘 가운데 하나라도 작으면 그 칸은 평균에 거의 들어가지 않습니다. 여기서 가까이 앉은 친구가 거리 무게, 같은 조 친구가 색 차이 무게입니다. 비유와 다른 점도 있습니다. 실제 필터는 조를 딱 잘라 나누지 않고, 밝기 차이가 클수록 무게를 조금씩 줄여 갑니다.
그림은 밝기 60 에서 200 으로 뛰는 경계 하나를 가로 한 줄로 줄여 본 것입니다. 연한 선이 노이즈 낀 줄, 초록 점선이 깨끗한 원본입니다. 다홍 선(Gaussian)은 자글자글함은 줄였지만 경계 앞뒤를 섞어 비탈을 만들었습니다. 보라 선(Bilateral)은 자글자글함도 줄이고 계단도 거의 그대로 지켰습니다.
숫자로 보는 두 무게
sigmaColor 를 30 으로 줬을 때, 가운데 칸과 밝기가 얼마나 다르면 무게가 얼마가 되는지 보입니다. OpenCV 는 종 모양(가우시안) 곡선으로 무게를 계산합니다.
| 가운데 칸과 밝기 차이 | 0 | 10 | 30 | 60 | 150 |
|---|---|---|---|---|---|
| 색 차이 무게 | 1.0 | 0.95 | 0.61 | 0.14 | 거의 0 |
밝기 차이가 sigmaColor(30)만큼이면 무게는 0.61, 그 두 배(60)면 0.14 로 줄고, 150 처럼 경계 건너편이면 0 에 가깝습니다.
짧은 예시
가로 한 줄의 다섯 칸으로 손계산해 봅니다. 가운데 칸은 100 이고, 왼쪽 둘은 같은 쪽(104, 98), 오른쪽 둘은 경계 건너편(250, 246)입니다. 계산을 쉽게 하려고 거리 무게는 모두 같다고 둡니다.
import numpy as np
row = np.array([104, 98, 100, 250, 246], dtype=float)
center = 100
# 그냥 평균: 경계 건너편 두 칸이 함께 섞인다
print(round(float(row.mean()), 1)) # 결과: 159.6
# 색 차이 무게: 밝기 차이가 클수록 작아지는 종 모양 곡선(sigmaColor 30)
# np.exp 는 종 모양 곡선의 값을 계산하는 데 쓰는 함수다. 여기서는 무게를 얻는 도구로만 본다
weight = np.exp(-((row - center) ** 2) / (2 * 30 ** 2))
print([round(float(w), 3) for w in weight]) # 결과: [0.991, 0.998, 1.0, 0.0, 0.0]
# 무게를 곱해 더한 뒤 무게의 합으로 나눈다(무게를 준 평균)
print(round(float((row * weight).sum() / weight.sum()), 1)) # 결과: 100.7그냥 평균은 159.6 으로 경계 건너편 쪽으로 끌려갔지만, 무게를 준 평균은 100.7 로 같은 쪽 칸만 섞었습니다. 6장에서 본 가중 평균 그대로, 무게 × 값을 모두 더하고 무게의 합으로 나눈 것입니다. Bilateral 필터는 이 무게를 칸마다 밝기 차이를 보고 새로 정한다는 점만 다릅니다.
cv2.bilateralFilter 의 인자
| 순서 | 인자 | 뜻 | 보통 값 |
|---|---|---|---|
| 1 | src | 고칠 그림. 흑백이나 컬러(채널 1 또는 3), uint8 이나 float32 | noisy |
| 2 | d | 들여다볼 이웃의 지름(칸 수). 0 이하로 주면 sigmaSpace 로 정한다 | 5~9 |
| 3 | sigmaColor | 밝기가 얼마나 달라도 섞을까. 클수록 다른 색도 섞는다 | 50~100 |
| 4 | sigmaSpace | 얼마나 먼 칸까지 섞을까. 클수록 먼 칸도 섞는다 | 50~100 |
import cv2
img = cv2.imread("ch07_sign.png")
# bilateralFilter(그림, d, sigmaColor, sigmaSpace)
smooth = cv2.bilateralFilter(img, # src: 컬러 그림도 된다
9, # d: 지름 9칸의 이웃을 본다
75, # sigmaColor: 밝기 차이 75 안팎까지 섞는다
75) # sigmaSpace: 거리 무게의 퍼짐
print(smooth.shape, smooth.dtype) # 결과: (240, 320, 3) uint8주의할 점
cv2.bilateralFilter는 원본 자리에 바로 덮어쓸 수 없습니다(OpenCV 공식 문서). 늘smooth = cv2.bilateralFilter(img, ...)처럼 새 이름으로 받습니다.- Salt & Pepper 의 점은 이웃과 밝기 차이가 아주 커서, Bilateral 필터가 보기에는 "경계 건너편"입니다. 그래서 점의 이웃은 점을 섞지 않고, 점 자신도 이웃을 거의 섞지 않아 점이 그대로 남습니다. 점 노이즈는 Median 필터의 몫입니다.