히스토그램과 이미지 보정
실습 준비
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()

히스토그램 균일화
히스토그램 균일화는 좁은 밝기 범위에 몰린 픽셀 값을 넓게 재배치하여 전체 대비를 높이는 방법. 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: 이미지를 나눌 가로·세로 타일 수

영역별 밝기 분포를 반영하여 전역 균일화보다 국소 대비를 완만하게 보정.
컬러 이미지의 밝기 보정
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)

색상과 채도를 유지하려면 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)

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)

감마 보정
선형 보정은 모든 픽셀에 같은 비율과 크기의 변화를 적용. 감마 보정은 픽셀 값을 비선형으로 변환하여 어두운 영역이나 밝은 영역을 선택적으로 조절.
- : 어두운 영역을 더 많이 밝게 보정
- : 밝은 영역을 더 많이 어둡게 보정
감마 함수로 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)
