5장. 영상 향상과 학습용 전처리
실습 4 — 히스토그램 평활화와 CLAHE 비교
역광으로 앞쪽이 어두운 실내 그림(ch05_backlit.png)을 흑백으로 읽어, 전체 평활화(cv2.equalizeHist)와 CLAHE 를 견줍니다. 어두운 책장이 얼마나 살아나는지, 밝은 창이 얼마나 날아가는지, 작은 잡티가 얼마나 커지는지를 숫자로 잽니다.
1단계. 전체 평활화
import cv2
import numpy as np
gray = cv2.imread("ch05_backlit.png", cv2.IMREAD_GRAYSCALE)
def report(name, g):
"""전체 평균, 책장 부분의 표준편차, 하얗게 날아간(250 초과) 칸의 비율을 찍는다."""
shelf = g[40:165, 15:115] # 책장 부분: 세로 40~164, 가로 15~114
print(f"{name:10} 평균 {round(float(g.mean()), 1):5} 책장 표준편차 {round(float(shelf.std()), 1):5}"
f" 250 넘는 칸 {round(float((g > 250).mean()), 3)}")
report("원본", gray)
# cv2.equalizeHist: 사진 전체의 히스토그램 하나로 평활화한다
eq = cv2.equalizeHist(gray)
report("전체 평활화", eq)원본 평균 74.9 책장 표준편차 13.0 250 넘는 칸 0.0
전체 평활화 평균 131.5 책장 표준편차 68.2 250 넘는 칸 0.0882단계. CLAHE
# cv2.createCLAHE: CLAHE 도구를 만든다. 처리는 apply 로 한다
clahe = cv2.createCLAHE(clipLimit=2.0, # 막대를 깎는 높이
tileGridSize=(8, 8)) # 바둑판 (가로 8칸, 세로 8칸)
cl = clahe.apply(gray)
report("CLAHE 2", cl)CLAHE 2 평균 81.7 책장 표준편차 18.4 250 넘는 칸 0.03단계. 세 그림과 히스토그램 견주기
import matplotlib.pyplot as plt
plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False
panels = [("원본 (역광)", gray), ("전체 평활화", eq), ("CLAHE (clipLimit 2)", cl)]
fig, axes = plt.subplots(2, 3, figsize=(11, 5.4))
for k, (title, g) in enumerate(panels):
axes[0, k].imshow(g, cmap="gray", vmin=0, vmax=255)
axes[0, k].set_title(title)
axes[0, k].axis("off")
hist = cv2.calcHist([g], [0], None, [32], [0, 256]).ravel()
axes[1, k].bar(np.arange(32) * 8, hist, width=8, align="edge")
axes[1, k].set_xlim(0, 256)
axes[1, k].set_xlabel("밝기")
plt.tight_layout()
plt.show()
4단계. 잡티를 더하고 clipLimit 바꿔 보기
# 실제 사진처럼 작은 잡티를 더한다. 씨앗을 고정해 늘 같은 잡티가 나오게 한다
rng = np.random.default_rng(509)
# normal(평균 0, 표준편차 2): 칸마다 -2~+2 쯤의 작은 흔들림
noisy = np.clip(gray.astype(np.float32) + rng.normal(0, 2, gray.shape), 0, 255).astype(np.uint8)
def wall_std(g):
"""고른 벽 부분(세로 5~19, 가로 120~159)의 표준편차: 잡티가 클수록 커진다."""
return round(float(g[5:20, 120:160].std()), 1)
print("잡티 넣은 원본: 벽", wall_std(noisy))
print("전체 평활화 : 벽", wall_std(cv2.equalizeHist(noisy)))
outs = {}
for clip in [1.0, 2.0, 4.0, 8.0]:
outs[clip] = cv2.createCLAHE(clipLimit=clip, tileGridSize=(8, 8)).apply(noisy)
print(f"CLAHE {clip} : 벽 {wall_std(outs[clip])}, 책장 {round(float(outs[clip][40:165, 15:115].std()), 1)}")
fig, axes = plt.subplots(1, 4, figsize=(12, 2.8))
for ax, clip in zip(axes, outs):
ax.imshow(outs[clip], cmap="gray", vmin=0, vmax=255)
ax.set_title(f"clipLimit {clip}")
ax.axis("off")
plt.tight_layout()
plt.show()잡티 넣은 원본: 벽 2.5
전체 평활화 : 벽 16.5
CLAHE 1.0 : 벽 4.3, 책장 20.2
CLAHE 2.0 : 벽 6.6, 책장 27.6
CLAHE 4.0 : 벽 10.1, 책장 37.8
CLAHE 8.0 : 벽 17.1, 책장 49.4
코드 설명
- 전체 평활화는 책장 부분의 표준편차를 13.0 에서 68.2 로 크게 키웠지만, 칸의 0.088 이 250 을 넘어 창이 하얗게 날아갔습니다. 그림에서는 벽의 밝기 변화가 세로 줄무늬로 드러났습니다.
- CLAHE(clipLimit 2)는 책장의 표준편차를 18.4 로 조금만 키우고, 250 넘는 칸은 0.0 그대로입니다. 평균도 74.9 에서 81.7 로 조금 올라, 원래 분위기를 지키면서 어두운 곳을 살렸습니다.
rng.normal(0, 2, gray.shape)는 평균 0, 표준편차 2 인 작은 흔들림을 칸마다 만듭니다. 6장에서 다룰 노이즈를 맛보기로 넣었습니다.- 잡티를 넣은 벽의 표준편차 2.5 가 전체 평활화에서는 16.5 로 커졌습니다. CLAHE 는 clipLimit 1, 2, 4, 8 에서 벽이 4.3, 6.6, 10.1, 17.1, 책장이 20.2, 27.6, 37.8, 49.4 입니다. clipLimit 을 올리면 책장이 더 살아나는 만큼 잡티도 커지고, 8 에서는 잡티가 전체 평활화만큼 커집니다. 처음에는 2 근처에서 시작해 그림을 보며 고릅니다.