Contrast Limited Adaptive Histogram Equalization(CLAHE)은 Adaptive Histogram Equalization(AHE)에서 국소적인 contrast와 함께 잡음까지 과도하게 증폭되는 문제를 개선하기 위해 제안된 적응형 히스토그램 평활화 기법이다. 일반적인 히스토그램 평활화는 영상 전체에 하나의 변환을 적용하므로 조명이 불균일한 영상에서는 효과가 제한적일 수 있다. 반면 AHE는 작은 영역별로 히스토그램 평활화를 수행하여 국소적인 contrast를 크게 향상시키지만, 잡음까지 함께 증폭시키는 단점이 있다. CLAHE는 이러한 문제를 해결하기 위해 고안되었다.
CLAHE는 영상을 tile이라 불리는 작은 영역으로 분할한 뒤, 각 tile에 대해 독립적으로 히스토그램 평활화를 수행한다. 이때 히스토그램의 특정 구간에 픽셀이 과도하게 집중되어 있으면, 미리 설정한 clip limit을 초과하는 빈도수를 잘라내고(clipping), 잘라낸 빈도수는 나머지 히스토그램 bin에 균등하게 재분배한다. 이 과정을 통해 국소적인 contrast는 향상시키면서도 잡음의 과도한 증폭을 효과적으로 억제할 수 있다.
각 tile를 독립적으로 처리하면 tile의 경계에서 밝기가 불연속적으로 변할 수 있다. 이를 방지하기 위해 CLAHE는 인접한 tile에서 얻은 결과를 bilinear 보간하여 경계를 자연스럽게 연결한다.
Gray 영상에 CLAHE를 적용하면 국소적인 contrast가 향상되어 어두운 영역이나 밝은 영역에 숨어 있는 세부 구조를 보다 선명하게 확인할 수 있다. 컬러 영상에도 CLAHE를 적용할 수 있으나, RGB의 각 채널에 독립적으로 적용하면 색상이 왜곡될 수 있다. 따라서 일반적으로는 영상을 Lab, YCbCr 또는 HSV와 같은 색 공간으로 변환한 후 밝기 성분(예를 들어 Lab의 L 채널, YCbCr의 Y 채널, 또는 HSV의 V 채널)에만 CLAHE를 적용한다. 특히 Lab의 L 채널이나 YCbCr의 Y 채널에 적용하는 방법이 원래의 색상을 가장 잘 유지하므로 널리 사용된다.
CLAHE의 성능은 tile의 크기와 clip limit의 선택에 크게 영향을 받는다. Clip limit이 너무 크면 일반적인 AHE와 비슷해져 잡음이 다시 증폭될 수 있고, 반대로 너무 작으면 contrast 향상 효과가 감소한다. 따라서 영상의 특성에 따라 적절한 tile 크기와 clip limit을 선택하는 것이 중요하다.
아래는 RGB 영상의 각 채널에 CLAHE 알고리즘을 적용한 결과이다.


rgn: 히스토그램을 구하는 이미지 상의 영역;
tile: adaptive HE가 적용되는 영역
첫 행/열 tile의 폭/높이 = rgn 폭/높이의 절반
마지막 tile 크기 = 이미지의 나머지 폭/높이
cliplimit = 각 영역의 히스토그램 슬롯당 들어가는 최대 픽셀 수
/* "Graphics Gems IV" 의 code를 수정함;
** 2023-01-13 updated: 이미지 사이즈가 rgn 사이즈의 정수배가 되지 않을 때 문제 해결;*/
int CLAHE ( BYTE* image, int width, int height,
BYTE gMin/*0*/, BYTE gMax/*255*/, int rgn_nx/*16*/, int rgn_ny/*16*/,
int bins/*256*/, double fcliplimit )
{
std::vector<int> map_array(rgn_nx * rgn_ny * bins);
int rgn_xsz = width / rgn_nx;
int rgn_ysz = height / rgn_ny;
int rgn_area = rgn_xsz * rgn_ysz;
int clipLimit = int( fcliplimit * ( rgn_xsz * rgn_ysz ) / bins );
clipLimit = ( clipLimit < 1 ) ? 1 : clipLimit;
//
/* calculate greylevel mappings for each contextual region */
for (int iy = 0; iy < rgn_ny; iy++) {
for (int ix = 0; ix < rgn_nx; ix++) {
int *hist = &map_array[bins * ( iy * rgn_nx + ix )];
int start = (iy * rgn_ysz) * width + ix * rgn_xsz;
RgnHistogram (&image[start], width, rgn_xsz, rgn_ysz, hist, bins);
ClipHistogram ( hist, bins, clipLimit );
//convert hist to cuimulative histogram normalized to [gMin=0, gMax=255]
MapHistogram ( hist, gMin, gMax, bins, rgn_area );
}
}
/* bilinearInterp greylevel mappings to get CLAHE image */
/* 첫열/행에 해당하는 타일은 rgn의 절반 크기만 처리하므로 ix(iy)는 0~rgn_nx(rgn_ny)까지임*/
int szx, szy, ixl, ixr, iyt, iyb;
BYTE *first_tile = &image[0]; // 같은 iy를 갖는 첫 타일의 주소;
for (int iy = 0; iy <= rgn_ny; iy++ ) {
if ( iy == 0 ) {
// 첫 타일은 절반만;
szy = rgn_ysz / 2; iyt = iyb = 0;
} else if ( iy == rgn_ny ) {
// 마지막 타일은 나머지 전부;
szy = height - ((rgn_ny-1)*rgn_ysz + rgn_ysz/2);
iyt = rgn_ny - 1; iyb = iyt;
} else {
szy = rgn_ysz; iyt = iy - 1; iyb = iyt + 1;
}
BYTE *ptile = &first_tile[0]; //각 타일의 시작 주소;
for (int ix = 0; ix <= rgn_nx; ix++ ) {
if ( ix == 0 ) {
//첫 타일은 절반만;
szx = rgn_xsz / 2; ixl = ixr = 0;
} else if ( ix == rgn_nx ) {
//마지막 타일은 나머지 전부;
szx = width - ((rgn_nx-1)*rgn_xsz + rgn_xsz/2);
ixl = rgn_nx - 1; ixr = ixl;
} else {
szx = rgn_xsz; ixl = ix - 1; ixr = ixl + 1;
}
// cumulative histogram data;
int *LU = &map_array[bins * ( iyt * rgn_nx + ixl )];
int *RU = &map_array[bins * ( iyt * rgn_nx + ixr )];
int *LB = &map_array[bins * ( iyb * rgn_nx + ixl )];
int *RB = &map_array[bins * ( iyb * rgn_nx + ixr )];
BilinearInterp (ptile, width, LU, RU, LB, RB, szx, szy );
ptile += szx;
}
first_tile += szy * width;
}
return 0;
}
void ClipHistogram ( int* hist, int gLevels, int clipLimit ) {
int excess = 0;
for (int i = 0; i < gLevels; i++ ) { /* calculate total number of excess pixels */
int diff = hist[i] - clipLimit;
if ( diff > 0 ) excess += diff; /* excess in current bin */
};
/* clip histogram and redistribute excess pixels in each bin */
int incr = excess / gLevels; /* average binincrement */
int upper = clipLimit - incr; /* bins larger than upper set to cliplimit */
for (int i = 0; i < gLevels; i++ ) {
if ( hist[i] > clipLimit ) hist[i] = clipLimit; /* clip bin */
else {
if ( hist[i] > upper ) { /* high bin count */
excess -= hist[i] - upper;
hist[i] = clipLimit;
} else { /* low bin count */
excess -= incr;
hist[i] += incr;
}
}
}
while ( excess ) { /* redistribute remaining excess */
int start = 0;
while ( excess && start < gLevels) {
int step = gLevels / excess;
if ( step < 1 ) step = 1; /* step size at least 1 */
for (int i = start; i < gLevels && excess; i += step)
if (hist[i] < clipLimit) {
hist[i]++; excess--;
}
start++;
}
}
}
void BilinearInterp ( BYTE * image, int stride,
int* LU, int* RU, int* LB, int* RB,
int rgn_xsz, int rgn_ysz ) {
int area = rgn_xsz * rgn_ysz; /* Normalization factor */
for (int y = 0, yrev = rgn_ysz; y < rgn_ysz; y++, yrev--) {
BYTE *line = &image[y * stride];
for (int x = 0, xrev = rgn_xsz; x < rgn_xsz; x++, xrev-- ) {
int v = line[x]; /* get histogram bin value */
v = (yrev * ( xrev * LU[v] + x * RU[v] ) + y * ( xrev * LB[v] + x * RB[v] ) ) / area;
line[x] = v > 255 ? 0xFF: v < 0 ? 0x00: v;
}
//image += stride - rgn_xsz; // goto the beginning of rgn's next-line;
}
}
void RgnHistogram ( BYTE* image, int stride, int rgn_xsz, int rgn_ysz,
int* hist, int gLevels) {
// image is pointing the beginnign of a given region;
for (int i = 0; i < gLevels; i++ ) hist[i] = 0;
for (int y = 0; y < rgn_ysz; y++ ) {
BYTE *line = &image[y * stride];
for (int x = 0; x < rgn_xsz; x++)
hist[line[x]]++;
}
}
// transform hist[] to a normalized cumulative histogram;
void MapHistogram ( int* hist, BYTE gMin, BYTE gMax, int gLevels, int rgn_area ){
double scale = double( gMax - gMin ) / rgn_area;
int iMin = int(gMin);
for (int i = 0, sum = 0; i < gLevels; i++ ) {
sum += hist[i];
int a = int(iMin + sum * scale );
hist[i] = a > gMax ? gMax: a < gMin ? gMin: a;
}
}
CLAHE (2)
픽셀값의 분포가 좁은 구간에 한정된 영상은 낮은 명암 대비(contrast)를 가지게 된다. 이 경우 픽셀값을 가능한 넓은 구간에 재분포하도록 변환시키면 전체적으로 훨씬 높은 contrast를 가지는 영상
kipl.tistory.com
'Image Recognition' 카테고리의 다른 글
| Watershed Segmentation (0) | 2021.02.27 |
|---|---|
| Local Ridge Orientation (0) | 2021.02.21 |
| Fixed-Point Bicubic Interpolation (1) | 2021.01.19 |
| Distance Transform (0) | 2021.01.16 |
| FFT 알고리즘의 재귀적 구현(a recursive FFT implementation) (0) | 2021.01.14 |

