본문 바로가기
스터디/데이터사이언스

이미지 명암 대비 개선 방법들 (Histogram Normalization, Histogram Equalization, CLAHE)

by 궁금한 준이 2025. 12. 28.
728x90
반응형

이미지 명암 대비 개선 방법

카메라로 촬영한 이미지가 뿌옇거나, 역광, 어두운 부분 등으로 안보이는 경우가 있다.

이러한 원인은 대개 명암 대비(contrast)가 충분하지 않거나, 조명(illumination)이 불균일하기 때문이다.

이를 해결하는 고전적인 이미지 명암 대비 개선 방법을 소개한다. 

픽셀의 히스토그램 기반 방법이고, 히스토그램을 재배치(늘리기, 줄이기, 국소적 조정 등)하여 이미지를 더 잘 보이게 하는 방법이다.

Histogram Normalization (히스토그램 정규화)

픽셀값의 범위를 일정 구간으로 재스케일링하는 방법이다.

가장 많이 사용하는 방법은 min-max scaling이다. (contrast stretching이라고도 한다)

원본 이미지의 최솟값($I_{\text{min}}$), 최댓값($I_{\text{max}}$)을 기준으로 [0, 255]의 표준 범위로 선형변환한다.

\[ I'(x) = \cfrac{I(x) - I_{\min}}{I_{\max} - I_{\min}} (L - 1) \]

이때 $L$은 밝기 레벨이다. 8비트면 $L=256$이다.

분포 모양(shape)을 바꾸지 않고 명암 범위를 넓혀서 전역 대비를 개선한다.

 

계산이 단순하고 빠르다. 그리고 톤이 과장되는 일이 적다.

 

극단적으로 밝거나 어두운 이상치(outlier)가 있으면 $I_{\text{min}}$, $I_{\max}$가 이상치에 끌려가서 중간 영역의 대비가 충분히 늘어나지 않을 수 있다. 

이 경우 $I_{\min}$, $I_{\max}$ 대신에 percentile(1% ~ 99%)을 사용할 수 있다.

조명이 불균일한 경유 국소(local) 디테일이 크게 나아지지 않을 수 있다.

Histogram Equalization (히스토그램 평활화)

픽셀 값의 분포를 거의 균일(uniform)하게 가깝게 만들도록 밝기를 재매핑하는 방법이다. 

따라서 픽섹의 누적분포함수(CDF)를 활용하게된다.

특정 밝기 구간에 몰려 있으면, 그 구간을 펼친다.

픽셀이 거의 없는 구간은 압축한다.

 

히스토그램을 $p(r)$, 누적분포함수를 $\text{CDF}$라 하면 $\text{CDF}(r) = \sum_{k=0}^{r} p(k)$이고

변환함수(lookup table)은 

\[ s = T(r) = (L - 1)\text{CDF}(r) \]

이다. $r$은 입력 밝기 값, $s$는 출력 밝기 값이다.

 

결과적으로 전체 밝기 범위를 고르게(uniform) 한다.

어두운 이미지 또는 특정 톤으로 치우친 이미지에서 디테일이 살아날 수 있다.

그러나 노이즈가 강조되는 경우가 있다.

CLAHE 

Contrast Limited Adaptive Histogram Equalization의 줄임말이다.

국소 디테일 복원에 좋다. 

이때문에 의료, 위성/항공, 공장/제조 검사 등에 많이 사용된다.

 

이미지를 타일(예: 8X8 격자)로 나누고 각 타일에서 HE를 적용한다.

타일 $t$를 아래첨자로 두고

\[ s = T_t(r) = (L - 1)\text{CDF}_t(r) \]

이다.

 

타일 히스토그램에서 특정 bin이 너무 커지면 과증폭(노이즈 폭주)가 될 수 있기 때문에 clipping을 적용한다.

클리핑 한계값을 $C$라 하면

\[ h_t'(r) = \min \left( h_t(r), C \right) \]

이고 초과량(클리핑된 픽셀 수)은 모든 bin에 재분배한다.

\[ h_t''(r) = h_t'(r) + \cfrac{\sum_r (h_t(r) - h'_t(r)) }{L} \]

이렇게 얻은 새로운 히스토그램 $h''_t$로 $\text{CDF}$를 다시 만들고 HE 매핑을 수행한다.

 

마지막으로, 타일 경계 보간작업을 bilinear interpolation을 적용하여 경계선을 자연스럽게 한다.

Python code

색상 채널인 RGB를 이용하면 색상, 채도, 밝기 모두 변화되어 부자연스러운 결과를 얻을 수 있다.

밝기를 담당하는 YCrCb 또는 LAB로 분리하여 처리한다.

import cv2
import numpy as np
import matplotlib.pyplot as plt

# Load
image = cv2.imread("sample.jpg")
if image is None:
    raise FileNotFoundError("Could not load sample.jpg")

# BGR -> YCrCb and split
ycrcb = cv2.cvtColor(image, cv2.COLOR_BGR2YCrCb)
y, cr, cb = cv2.split(ycrcb)

# --- Apply methods on Y channel ---
# 0) Original (no change)
y_orig = y.copy()

# 1) Histogram Normalization (min-max on Y)
y_norm = cv2.normalize(y, None, 0, 255, cv2.NORM_MINMAX)

# 2) Histogram Equalization (on Y)
y_eq = cv2.equalizeHist(y)

# 3) CLAHE (on Y)
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
y_clahe = clahe.apply(y)

# --- Reconstruct color images (Y + original Cr/Cb) ---
def merge_y(y_new):
    ycrcb_new = cv2.merge([y_new, cr, cb])
    bgr_new = cv2.cvtColor(ycrcb_new, cv2.COLOR_YCrCb2BGR)
    rgb_new = cv2.cvtColor(bgr_new, cv2.COLOR_BGR2RGB)
    return rgb_new

rgb_orig  = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
rgb_norm  = merge_y(y_norm)
rgb_eq    = merge_y(y_eq)
rgb_clahe = merge_y(y_clahe)

# --- Plot helper: image + histogram of Y channel ---
def plot_image_and_hist(rgb_img, y_chan, title, ax_img, ax_hist):
    ax_img.imshow(rgb_img)
    ax_img.set_title(title)
    ax_img.axis("off")

    ax_hist.hist(y_chan.ravel(), bins=256, range=(0, 256), alpha=0.8)
    ax_hist.set_title("Histogram (Y channel)")
    ax_hist.set_xlabel("Intensity")
    ax_hist.set_ylabel("Count")
    ax_hist.set_xlim(0, 255)

# --- Layout: 4 rows x 2 cols (image, histogram) ---
fig, axes = plt.subplots(4, 2, figsize=(14, 18))

plot_image_and_hist(rgb_orig,  y_orig,  "Original (YCrCb -> Y unchanged)", axes[0, 0], axes[0, 1])
plot_image_and_hist(rgb_norm,  y_norm,  "Normalization on Y (min-max)",   axes[1, 0], axes[1, 1])
plot_image_and_hist(rgb_eq,    y_eq,    "Equalization on Y",              axes[2, 0], axes[2, 1])
plot_image_and_hist(rgb_clahe, y_clahe, "CLAHE on Y",                     axes[3, 0], axes[3, 1])

plt.tight_layout()
plt.show()

 

BGR 채널을 YCrCb로 분리한 후, Y채널에서 적용

import cv2
import numpy as np
import matplotlib.pyplot as plt

# Load
image = cv2.imread("sample.jpg")
if image is None:
    raise FileNotFoundError("Could not load sample.jpg")

# BGR -> LAB and split
lab = cv2.cvtColor(image, cv2.COLOR_BGR2LAB)
l, a, b = cv2.split(lab)

# --- Apply methods on L channel ---
# 0) Original (no change)
l_orig = l.copy()

# 1) Histogram Normalization (min-max on L)
l_norm = cv2.normalize(l, None, 0, 255, cv2.NORM_MINMAX)

# 2) Histogram Equalization (on L)
l_eq = cv2.equalizeHist(l)

# 3) CLAHE (on L)
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
l_clahe = clahe.apply(l)

# --- Reconstruct color images (L + original a/b) ---
def merge_l(l_new):
    lab_new = cv2.merge([l_new, a, b])
    bgr_new = cv2.cvtColor(lab_new, cv2.COLOR_LAB2BGR)
    rgb_new = cv2.cvtColor(bgr_new, cv2.COLOR_BGR2RGB)
    return rgb_new

rgb_orig  = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
rgb_norm  = merge_l(l_norm)
rgb_eq    = merge_l(l_eq)
rgb_clahe = merge_l(l_clahe)

# --- Plot helper: image + histogram of L channel ---
def plot_image_and_hist(rgb_img, l_chan, title, ax_img, ax_hist):
    ax_img.imshow(rgb_img)
    ax_img.set_title(title)
    ax_img.axis("off")

    ax_hist.hist(l_chan.ravel(), bins=256, range=(0, 256), alpha=0.8)
    ax_hist.set_title("Histogram (L channel)")
    ax_hist.set_xlabel("Intensity")
    ax_hist.set_ylabel("Count")
    ax_hist.set_xlim(0, 255)

# --- Layout: 4 rows x 2 cols (image, histogram) ---
fig, axes = plt.subplots(4, 2, figsize=(14, 18))

plot_image_and_hist(rgb_orig,  l_orig,  "Original (LAB -> L unchanged)", axes[0, 0], axes[0, 1])
plot_image_and_hist(rgb_norm,  l_norm,  "Normalization on L (min-max)",  axes[1, 0], axes[1, 1])
plot_image_and_hist(rgb_eq,    l_eq,    "Equalization on L",             axes[2, 0], axes[2, 1])
plot_image_and_hist(rgb_clahe, l_clahe, "CLAHE on L",                    axes[3, 0], axes[3, 1])

plt.tight_layout()
plt.show()

BGR을 LAB로 분리한 후, L채널에서 적용

 

원본 이미지를 보면, 상단 채광창은 매우 맑고, 밑에 조명도 밝다. 하지만 전체적인 벽화 그림은 어두운 영역에 묻혀서 디테일이 부족하다.

실제 히스토그램을 살펴보면 255 부근에 뾰족한 스파이크가 보인다.

이 경우 전역 처리는 별 효과를 가지지 못한다.

 

Normalization: 시각적으로 변화가 크지 않다. 255 근처의 포화 픽셀때문에 min-max 정규화가 소용이 없다.

Equalization: 히스토그램이 균일하게 퍼지면서 대비가 증가한다. 하지만 전체적으로 톤이 과장되어 벽화 질감이 세게 튀는 느낌이 난다. 

참고로, 히스토그램에 빗살처럼 Comb 형태가 나타나는데, 평활화 작업에 이산 밝기 레벨로 재매핑할 때, 특정 값에 픽셀이 몰리는 현상이다. 가시성은 높였으나, 자연스러움과 안정성은 좀 떨어진다. 

CHAHE: 중간밝기 영역 중심으로 히스토그램이 넓어지지만, 전 구간을 평활화하지는 않는다.

어두운 벽화 디테일이 살아나면서 과장감은 덜하기 때문에 좀 더 자연스럽다.

여전히 채광창은 스파이크가 존재한다.

사실 이미  포화된(saturated) 밝기 정보는 어떤 히스토그램 기법으로도 복원되지 않는다.

 

이 한계를 넘으려면 촬영 단계에서 브라케팅을 하거나 Retinex 알고리즘, 또는 딥러닝 기반 enhancement를 적용할 수 있다.

※ 사용한 샘플 이미지는 이탈리아 피렌체 대성당의 두모오 천장 사진이다. (본인이 직접 촬영)

 

반응형

References

[1] https://docs.opencv.org/4.x/d5/daf/tutorial_py_histogram_equalization.html

 

OpenCV: Histograms - 2: Histogram Equalization

Goal In this section, We will learn the concepts of histogram equalization and use it to improve the contrast of our images. Theory Consider an image whose pixel values are confined to some specific range of values only. For eg, brighter image will have al

docs.opencv.org

[2] https://stackoverflow.com/questions/25008458/how-to-apply-clahe-on-rgb-color-images

 

How to apply CLAHE on RGB color images?

CLAHE is Contrast Limited Adaptive Histogram Equalization and a source in C can be found at https://github.com/erich666/GraphicsGems/blob/master/gemsiv/clahe.c So far I have only seen some examples/

stackoverflow.com

[3] https://answers.opencv.org/question/86243/clahe-for-color-image-opencv-30/

 

 

728x90
반응형