logo

히스토그램과 이미지 보정

실습 준비

import cv2 as cv
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
from yeom import to_pil

image = cv.imread("balloon.webp")

히스토그램

히스토그램은 픽셀 값마다 나타난 픽셀 수를 센 분포. 어두운 이미지는 분포가 왼쪽에, 밝은 이미지는 오른쪽에 치우치는 경향이 있음.

for channel, line_color in enumerate(("b", "g", "r")):
    hist = cv.calcHist(
        [image],
        [channel],
        None,
        [256],  # 0–255를 256개 구간으로 분할
        [0, 256],
    )
    plt.plot(hist, color=line_color)

plt.xlim([0, 256])
plt.xlabel("픽셀 값")
plt.ylabel("픽셀 수")
plt.show()

BGR 채널별 히스토그램

히스토그램 균일화

히스토그램 균일화는 좁은 밝기 범위에 몰린 픽셀 값을 넓게 재배치하여 전체 대비를 높이는 방법. cv.equalizeHist()는 8비트 1채널 이미지에 적용.

xray = cv.imread("xray.jpg", cv.IMREAD_GRAYSCALE)
Image.fromarray(xray)

균일화 전 영상

균일화 전 밝기 히스토그램

equalized = cv.equalizeHist(xray)
Image.fromarray(equalized)

균일화 후 영상

균일화 후 밝기 히스토그램

좁은 범위에 몰렸던 밝기 값이 넓게 퍼져 영상의 전체 대비가 커짐.

CLAHE

전체 이미지에 한 번만 균일화를 적용하면 일부 영역이 지나치게 어둡거나 밝아지고 잡음이 강조될 수 있음. CLAHE(Contrast Limited Adaptive Histogram Equalization)는 이미지를 작은 타일로 나누어 균일화하고 대비 증가에 제한을 둠.

clahe = cv.createCLAHE(
    clipLimit=2.0,  # 과도한 대비 증가를 제한
    tileGridSize=(8, 8),  # 영상을 가로·세로 8개 타일로 분할
)
clahe_result = clahe.apply(xray)
Image.fromarray(clahe_result)
  • clipLimit: 한 구간의 대비가 지나치게 커지는 것을 제한
  • tileGridSize: 이미지를 나눌 가로·세로 타일 수

CLAHE 적용 결과

영역별 밝기 분포를 반영하여 전역 균일화보다 국소 대비를 완만하게 보정.

컬러 이미지의 밝기 보정

low_contrast = cv.imread("low.jpg")
to_pil(low_contrast)

밝기 보정 전 컬러 이미지

B, G, R 채널을 각각 균일화하면 채널 사이의 비율이 달라져 원래 색상이 변할 수 있음.

b, g, r = cv.split(low_contrast)
equalized_b = cv.equalizeHist(b)
equalized_g = cv.equalizeHist(g)
equalized_r = cv.equalizeHist(r)
corrected_bgr = cv.merge((equalized_b, equalized_g, equalized_r))
to_pil(corrected_bgr)

BGR 채널을 각각 균일화하여 색이 변한 결과

색상과 채도를 유지하려면 HSV로 변환한 뒤 V 채널만 보정.

hsv = cv.cvtColor(low_contrast, cv.COLOR_BGR2HSV)
h, s, v = cv.split(hsv)
equalized_v = cv.equalizeHist(v)
corrected_hsv = cv.merge((h, s, equalized_v))
corrected = cv.cvtColor(corrected_hsv, cv.COLOR_HSV2BGR)
to_pil(corrected)

HSV의 V 채널만 균일화한 결과

H와 S 채널은 그대로 두고 V 채널의 분포만 넓혀 색상 변화를 완화.

룩업 테이블

룩업 테이블(look-up table, LUT)은 입력값에 대응하는 출력값을 미리 저장한 변환표. 8비트 이미지의 픽셀값은 0–255이므로 길이가 256인 배열로 모든 변환 결과 표현 가능.

입력 픽셀값 x → lut[x] → 출력 픽셀값

다음 LUT는 각 채널의 픽셀값을 0, 32, 64, …, 224의 8단계로 변환.

step = 32  # 256개 픽셀 값을 8단계로 나누는 간격
lut = (np.arange(256) // step * step).astype(np.uint8)
lut.shape
실행 결과
(256,)
lut[[0, 31, 32, 63, 255]]
실행 결과
array([  0,   0,  32,  32, 224], dtype=uint8)

cv.LUT()는 이미지의 각 픽셀값을 인덱스로 사용하여 변환표의 값으로 교체.

quantized = cv.LUT(image, lut)
to_pil(quantized)

각 채널의 픽셀값을 8단계로 변환한 이미지

감마 보정

선형 보정은 모든 픽셀에 같은 비율과 크기의 변화를 적용. 감마 보정은 픽셀 값을 비선형으로 변환하여 어두운 영역이나 밝은 영역을 선택적으로 조절.

  • : 어두운 영역을 더 많이 밝게 보정
  • : 밝은 영역을 더 많이 어둡게 보정

감마 함수로 0–255의 변환 결과를 LUT에 미리 저장하면 모든 픽셀에서 거듭제곱을 반복하지 않아도 됨.

gamma = 0.5  # 어두운 영역을 더 많이 밝히는 값
values = np.arange(256, dtype=np.float32) / 255
lut = np.clip((values ** gamma) * 255, 0, 255).astype(np.uint8)

gamma_corrected = cv.LUT(image, lut)
to_pil(gamma_corrected)

감마 값에 따른 밝기 변환 곡선

Previous
색 공간