CLAHE 알고리즘을 WPF OpenCV 프로젝트에 구현하겠습니다. 지난 포스팅(#19)에서는 히스토그램 평활화(Equalize)를 통해 영상의 명암비를 전체적으로 높이는 방법을 다뤘습니다.
확실히 이미지가 밝아지고 선명해지긴 했지만, 혹시 뭔가 “과하다”는 느낌 못 받으셨나요? 너무 어두운 배경에 있던 노이즈(Noise)까지 덩달아 선명해지거나, 밝은 부분이 하얗게 날아가 버리는 현상 말이죠.
오늘은 바로 그 문제를 해결해 주는 ‘업그레이드된 평활화’, CLAHE (Contrast Limiting Adaptive Histogram Equalization)에 대해 간략하게 정리하고, 구현해 보겠습니다.
CLAHE vs Equalize
지난 포스팅(#19)에 쓴 Cv2.EqualizeHist는 전역(Global) 방식입니다. 이미지 전체를 한 통으로 보고, “어두운 건 밝게, 밝은 건 더 밝게!” 하며 일괄적으로 적용해 버립니다.
이러다 보니 두 가지 문제가 생깁니다.
- Detail 손실: 작은 영역마다 밝기 특성이 다른데, 모조리 무시하고 전체 평균으로 맞춰버립니다.
- Noise 증폭: 배경처럼 색이 균일한 곳(히스토그램이 뾰족한 곳)을 강제로 펴다 보니, 숨어 있던 자글자글한 노이즈가 눈에 띄게 올라옵니다.
CLAHE (Contrast Limiting Adaptive Histogram Equalization)
CLAHE는 이 문제를 아주 똑똑하게 해결합니다.
- 적응형(Adaptive) – 구역 나누기: 이미지를 작은 격자(Tile, 보통 8×8)로 잘게 쪼갭니다. 그리고 각 구역별로 평활화를 따로따로 진행합니다. (Adaptive Threshold와 비슷하죠?)
- 대비 제한(Contrast Limiting) – 자르기: 특정 구역에서 히스토그램의 빈도수가 너무 높게 치솟으면(즉, 너무 밋밋한 영역이면), 지정된 한계선(Clip Limit) 위로 튀어나온 부분을 싹둑 잘라냅니다. 그리고 자른 픽셀들을 주변에 골고루 나눠줍니다. 이렇게 하면 과도한 대비 증가(노이즈)를 막을 수 있습니다.
- 보간(Interpolation): 구역 별로 따로 놀면 바둑판처럼 경계선이 생기겠죠? 이중 선형 보간법(Bilinear Interpolation)으로 경계를 부드럽게 살살 문질러줍니다.
구현 요약
CLAHE는 Equalize보다 설정할 게 조금 더 있습니다.
- AlgorithmParameters.cs:
ClaheParams클래스 추가. (ClipLimit, TileGridSize) - MainViewModel.cs: 메뉴 등록 및 파라미터 연결.
- MainWindow.xaml: 슬라이더 UI 추가.
- OpenCVService.cs:
Cv2.CreateCLAHE및 적용하고, 결과에 대한 히스토그램을 계산.
Step 1: Model (AlgorithmParameters)
AlgorithmParameters.cs 파일에 ClaheParams 클래스를 생성하고 CLAHE 설정 값을 정의합니다.
- ClipLimit: 대비를 얼마나 제한할지 결정합니다. (값이 클수록 Equalize에 가까워집니다.)
- TileGridSize: 이미지를 몇 개의 타일로 쪼갤지 결정합니다.
public class ClaheParams : AlgorithmParameters
{
// 대비 제한 값 (기본 40.0)
// 값이 클수록 대비가 강해지지만 노이즈도 증가합니다.
private double _clipLimit = 40.0;
public double ClipLimit
{
get => _clipLimit;
set { if (_clipLimit != value) { _clipLimit = value; OnPropertyChanged(); } }
}
// 타일 그리드 크기 (기본 8 -> 8x8 격자)
private int _tileGridSize = 8;
public int TileGridSize
{
get => _tileGridSize;
set
{
if (value < 1) value = 1;
if (_tileGridSize != value) { _tileGridSize = value; OnPropertyChanged(); }
}
}
}
Step 2: ViewModel
MainViewModel.cs코드에는 생성자 함수에 CLAHE 메뉴를 추가하고 알고리즘 선택 시 알고리즘 선택 시 ClaheParams 객체를 생성 후 팝업 로직을 연결합니다.
public MainViewModel()
{
_cvServices = new OpenCVService();
AlgorithmList = new ObservableCollection<string>
{
"Threshold",
"Otsu Threshold",
"Adaptive Threshold",
"Histogram",
"Normalize",
"Equalize",
"CLAHE"
};
}
private void CreateParametersForAlgorithm(string algoName)
{
// 선택된 이름에 따라 적절한 설정 클래스 생성
switch (algoName)
{
case "Threshold":
// 이진화 설정을 담을 그릇을 새로 만듭니다. (기본값 128 등 포함)
CurrentParameters = new ThresholdParams();
break;
case "Adaptive Threshold":
CurrentParameters = new AdaptiveThresholdParams();
break;
case "Otsu Threshold":
// Otsu는 별도 설정이 필요 없으므로 null
CurrentParameters = new OtsuParams();
break;
case "Histogram":
CurrentParameters = new HistogramParams();
break;
case "Normalize":
CurrentParameters = new NormalizeParams();
break;
case "Equalize":
CurrentParameters = new EqualizeParams();
break;
case "CLAHE":
CurrentParameters = new ClaheParams();
break;
default:
CurrentParameters = null; // 설정이 필요 없는 경우
break;
}
}
MainViewModel.cs 파일 내에 ApplyAlgorithm () 함수도 아래와 같이 수정해 주세요. 별다른건 아니고 CLAHE 알고리즘 처리 후 히스토그램 그래프를 그리기 위해 필요한 부분입니다. (잘 알죠?)
private async void ApplyAlgorithm(object obj)
{
if (string.IsNullOrEmpty(SelectedAlgorithm)) return;
if (IsBusy) return; // 작업 중 중복 실행 방지
try
{
IsBusy = true;
AnalysisResult = "Processing...";
// 비동기 처리 호출
string result = await _cvServices.ProcessImageAsync(SelectedAlgorithm, CurrentParameters);
if (result == "This Image is Gray Image.")
{
MessageBox.Show("Gray 영상입니다.", "Gray Image", MessageBoxButton.OK);
}
AnalysisResult = result;
ShowOriginal = false;
UpdateDisplay();
// 히스토그램 알고리즘의 경우, 팝업 윈도우 표시
//if(SelectedAlgorithm == "Histogram" && _cvServices.LastHistogramData != null)
if((SelectedAlgorithm == "Histogram" || SelectedAlgorithm == "Normalize" || SelectedAlgorithm == "Equalize" || SelectedAlgorithm == "CLAHE")
&& _cvServices.LastHistogramData != null)
{
HistogramWindow histWin = new HistogramWindow(_cvServices.LastHistogramData, _cvServices.LastHistogramChannel);
histWin.Owner = Application.Current.MainWindow; // 부모 창 설정
histWin.Show();
}
}
catch (Exception ex)
{
AnalysisResult = "처리 중 에러: " + ex.Message;
}
finally
{
IsBusy = false;
}
}
Step 3: UI(View)
MainWindow.xaml코드에 ClaheParams에서 사용할 UI Element들(ClipLimit과 TileGridSize)을 아래와 같이 추가합니다.
<DataTemplate DataType="{x:Type local:ClaheParams}">
<StackPanel>
<TextBlock Text="CLAHE Settings" Margin="0,0,0,5" FontWeight="Bold"/>
<TextBlock Text="Clip Limit (대비 제한)" Margin="0,5,0,0"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions>
<Slider Grid.Column="0" Minimum="1" Maximum="100" Value="{Binding ClipLimit}" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Text="{Binding ClipLimit}" Margin="5,0,0,0" TextAlignment="Center" />
</Grid>
<TextBlock Text="Tile Grid Size (격자 크기)" Margin="0,5,0,0"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions>
<Slider Grid.Column="0" Minimum="1" Maximum="32" Value="{Binding TileGridSize}" VerticalAlignment="Center" IsSnapToTickEnabled="True" TickFrequency="1"/>
<TextBox Grid.Column="1" Text="{Binding TileGridSize}" Margin="5,0,0,0" TextAlignment="Center" />
</Grid>
<TextBlock Text="* Clip Limit: 클수록 대비 강해짐 (노이즈 주의)" Foreground="Gray" FontSize="11" Margin="0,5,0,0"/>
<TextBlock Text="* Tile Size: 이미지를 나누는 격자 크기 (예: 8x8)" Foreground="Gray" FontSize="11" Margin="0,2,0,0"/>
</StackPanel>
</DataTemplate>
Step 4: OpenCVService
OpenCVService.cs입니다. CLAHE는 Cv2.EqualizeHist처럼 함수 한 번 호출이 아니라, 객체를 생성(CreateCLAHE)하고 적용(Apply)하는 방식입니다.
case "CLAHE":
if (parameters is ClaheParams claheParams)
{
// 1. [그레이스케일 변환] CLAHE도 1채널 이미지에 적용합니다.
using (Mat gray = new Mat())
{
Cv2.CvtColor(_srcImage, gray, ColorConversionCodes.BGR2GRAY);
// 2. [CLAHE 객체 생성]
// ClipLimit과 GridSize를 설정하여 객체를 만듭니다.
using (var clahe = Cv2.CreateCLAHE(claheParams.ClipLimit, new OpenCvSharp.Size(claheParams.TileGridSize, claheParams.TileGridSize)))
{
// 3. [CLAHE 적용]
clahe.Apply(gray, _destImage);
}
// 4. [히스토그램 계산] 결과 확인용
CalculateHistogramForPopup(_destImage);
// 5. [채널 복구] 디스플레이용 BGR 변환
Cv2.CvtColor(_destImage, _destImage, ColorConversionCodes.GRAY2BGR);
resultMessage += $": CLAHE (Clip:{claheParams.ClipLimit}, Grid:{claheParams.TileGridSize})";
}
}
break;
실행 및 결과 비교: Equalize vs CLAHE
자, 이제 프로젝트 빌드 후 실행해서 차이를 확인해 볼 시간입니다. 같은 이미지를 놓고 비교해 보세요.
먼저 아래 이미지는 로딩 한 원본 이미지와 원본 이미지의 히스토그램을 보여줍니다.


아래의 이미지는 위 이미지를 이전 포스팅(#19)의 Equalize를 적용한 이미지와 히스토그램 그래프입니다.


이제 이번에 구현한 CLAHE 를 적용한 이미지를 봐야겠죠.


분석 결과:
- Equalize: 전체적으로 밝아졌지만, 기둥 등 원래 밝았던 부분이 하얗게 날아가거나 벽면의 질감이 부자연스럽게 거칠어 보입니다.
- CLAHE: 훨씬 자연스럽습니다. 어두운 곳의 디테일은 살아나면서도, 밝은 곳의 질감은 유지되고 있습니다. 히스토그램을 봐도 Equalize처럼 한쪽 벽에 붙어버리는(Clipping) 현상 없이 비교적 고르게 분포된 것을 볼 수 있습니다.
항상 하던 이야기 이지만, 세상에 공짜는 없습니다. CLAHE는 Equalize보다 연산량이 많아 속도는 조금 더 느립니다. 하지만 품질이 훨씬 좋기 때문에 의료 영상(X-ray)이나 정밀 검사에서는 거의 필수적으로 사용됩니다.
다음 포스팅에서는 이미지의 픽셀 값이 아니라, 위치와 모양을 바꾸는 기하학적 변환(Geometry Transform – 이동, 확대/축소, 회전)에 대해 알아보겠습니다.
참조 자료
[Post #19] 히스토그램 평활화: [WPF OpenCV Project #19] – Equalize 구현 및 정규화와의 차이
CLAHE: OpenCV Docs – CLAHE Class – 타일 그리드 및 클립 리밋 설명
Adaptive Histogram Equalization: Wikipedia – AHE – CLAHE의 수학적 원리와 보간법 설명