OpenCV 비전 실무5장 · 영상 향상과 학습용 전처리17 / 21← 이전목차다음 →MetaContents

5장. 영상 향상과 학습용 전처리

실습 4 — 히스토그램 평활화와 CLAHE 비교

역광으로 앞쪽이 어두운 실내 그림(ch05_backlit.png)을 흑백으로 읽어, 전체 평활화(cv2.equalizeHist)와 CLAHE 를 견줍니다. 어두운 책장이 얼마나 살아나는지, 밝은 창이 얼마나 날아가는지, 작은 잡티가 얼마나 커지는지를 숫자로 잽니다.

1단계. 전체 평활화

Python
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.088

2단계. CLAHE

Python
# 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.0

3단계. 세 그림과 히스토그램 견주기

Python
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()
역광 원본, 전체 평활화, CLAHE 결과와 각각의 히스토그램

4단계. 잡티를 더하고 clipLimit 바꿔 보기

Python
# 실제 사진처럼 작은 잡티를 더한다. 씨앗을 고정해 늘 같은 잡티가 나오게 한다
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
잡티를 넣은 역광 그림에 clipLimit 1, 2, 4, 8 로 CLAHE 를 한 결과

코드 설명

  • 전체 평활화는 책장 부분의 표준편차를 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 근처에서 시작해 그림을 보며 고릅니다.
← 이전 절5장 · 실습 3 — 감마 보정과 LUT다음 절 →5장 · 실습 5 — 컬러 사진 평활화: 채널마다와 밝기 채널만