Equalize (히스토그램 평탄화)와 관련하여 WPF OpenCV 프로젝트 추가해 보도록 하겠습니다.
지난 포스팅(#18)에서는 Normalize (정규화)를 통해 좁은 범위에 몰려 있는 픽셀들을 0~255 전체 구간으로 쫙 펴주는(Stretch) 작업을 했습니다.
하지만 정규화만으로는 해결되지 않는 ‘진짜 골 때리는 애들’ 이 있습니다. 바로 픽셀 값들이 한 곳에 극단적으로 뭉쳐 있는 경우입니다. 이럴 때 필요한 것이 바로 오늘 다룰 Histogram Equalization (히스토그램 평탄화) 입니다.
Equalize (평탄화) vs Normalize (정규화)
지난 포스팅의 성적 비유를 다시 가져와 보겠습니다. 이번에는 상황이 좀 바꿔보죠.
상황: 전교생 5명, 수학 점수: 70, 96, 98, 98, 100 (공부 겁나 잘하는 학생들만 모였네요!)
이 상태에서 지난 시간의 정규화 (Min-Max Normalize)를 적용하면 어떻게 될까요?
- 최소값(70)은 0점으로, 최대값(100)은 100점으로 이동합니다.
- 하지만 96, 98, 98점 맞은 학생들은 여전히 최상위권 점수 근처에 옹기종기 모여 있게 됩니다.
- 즉, “점수 격차(명암 대비)”가 여전히 뚜렷하지 않습니다.
이때 필요한 것이 평탄화(Equalize)입니다. 평탄화는 점수(픽셀 값) 자체의 크기보다는 “등수(누적 빈도)“를 중요하게 생각합니다. 1등부터 5등까지 줄을 세운 뒤, 가능한 점수 범위(0~100)에 아이들을 골고루 강제 배치하는 것이죠. 이렇게 하면 뭉쳐 있던 아이들의 점수 차이가 확실하게 벌어지게 됩니다.
수학적 원리
저도 수학이 굉장히 싫습니다. 나이가 들수록 더 싫어 지더군요.
하지만 아래의 원리는 알고 넘어가면 아주 유용합니다. 핵심은 누적 분포 함수(CDF)입니다.
- 세어 보기 (Histogram): 밝기 별로 픽셀이 몇 개인지 셉니다.
- 누적 하기 (Cumulative Sum): 어두운 값부터 개수를 차곡차곡 더합니다. (이게 바로 CDF입니다.)
- 매핑 하기 (Mapping): 누적된 값의 비율에 따라 새로운 밝기 값을 부여합니다.
핵심 공식:
어려워 보이죠? 쉽게 말해 “전체 픽셀 중 내 밑으로 몇 퍼센트나 있는지”를 따져서 그 비율만큼 밝기를 주는 것입니다. 내가 상위 10%라면, 밝기도 상위 10% 값인 230 정도를 주는 식이죠.
이 과정을 거치면 히스토그램 그래프가 전체적으로 평평하게 펴지면서 명암 대비(Contrast)가 획기적으로 개선됩니다. 아무튼 수학적 정리를 하긴 했지만, 우리는 잘 가져다 원할 때 잘 쓰면 땡~ 입니다. 이제 WPF OpenCV 프로젝트에 구현을 해볼까요?
구현 순서 요약
OpenCV에서는 Cv2.EqualizeHist() 함수 한 방이면 끝납니다. 하지만 이 함수는 치명적인 제약 사항이 하나 있는데요. 그게 뭐냐면, 바로 요거 입니다.
주의:
Cv2.EqualizeHist는 오직 GrayScale(1-채널) 이미지에만 적용할 수 있음.
따라서 구현 순서는 다음과 같습니다.
- AlgorithmParameters.cs: 빈 파라미터 클래스 생성 (설정값이 필요 없음)
- MainViewModel.cs: 메뉴 추가 및 연결
- OpenCVService.cs: (중요)
BGR -> Gray 변환후 평탄화 적용 - MainWindow.xaml: EqualizeParams 클래스 객체의 데이터 바인딩이 없으나, 설명 내용을 표시.
Step 1 & 2: Parameter 및 ViewModel 설정
Equalize(평탄화)는 별도의 파라미터(Alpha, Beta 등)가 필요 없습니다. 기존 프로젝트에서 진행했던 형식을 맞추기 위해 AlgorithmParameters.cs 파일에 비어 있는 클래스를 정의하였습니다.
public class EqualizeParams : AlgorithmParameters
{
// EqualizeHist는 별도 설정 파라미터가 없습니다.
}
MainViewModel.cs 파일에서 MainViewModel 클래스의 생성자 함수내에 Equalize 메뉴를 추가하고, EqualizeParams 클래스를 생성 하는 부분을 CreateParameterForAlgorithm () 함수에 추가하도록 하죠. 실행 후 히스토그램 팝업 윈도우를 띄우는 로직을 ApplyAlgorithm() 함수에 연결합니다.
// 1. 알고리즘 목록에 추가: MainViewWindow() 함수
AlgorithmList = new ObservableCollection<string>
{
// ... 기존 목록 ...
"Normalize",
"Equalize" // [추가]
};
// 2. 파라미터 생성 로직 연결: CreateParameterForAlgorithm () 함수
case "Equalize":
CurrentParameters = new EqualizeParams();
break;
// 3. 팝업 창 띄우기 로직 수정:
if((SelectedAlgorithm == "Histogram" || SelectedAlgorithm == "Normalize" || SelectedAlgorithm == "Equalize")
&& _cvServices.LastHistogramData != null)
{
// ... 팝업 띄우기 코드 ...
}
Step 3: OpenCVService
OpenCVService.cs 파일에 컬러 이미지가 들어오더라도 반드시 그레이스케일로 변환해서 처리해야 에러가 나지 않겠죠? 치명적인 제약 조건에서 이야기한 이유 때문이죠. 그리고 ProcessImageAsync() 함수내에 Equalize 처리 함수를 추가해서 해당 결과 이미지에 대해 자동으로 히스토그램을 계산하도록 하고, 팝업으로 히스토그램 그래프를 보여줄 데이터를 준비하도록 코드를 업데이트 하도록 하겠습니다.
case "Equalize":
if (parameters is EqualizeParams)
{
// 1. [그레이스케일 변환] 필수! EqualizeHist는 1채널만 지원함
using (Mat gray = new Mat())
{
Cv2.CvtColor(_srcImage, gray, ColorConversionCodes.BGR2GRAY);
// 2. [히스토그램 평활화 실행]
// gray: 원본(그레이), _destImage: 결과
Cv2.EqualizeHist(gray, _destImage);
// 3. [히스토그램 재계산]
// 평활화 후 분포가 어떻게 변했는지 확인하기 위함
CalculateHistogramForPopup(_destImage);
// 4. [결과 이미지 채널 복구]
// 화면 출력을 위해 Gray(1채널) -> BGR(3채널)로 다시 변환
Cv2.CvtColor(_destImage, _destImage, ColorConversionCodes.GRAY2BGR);
resultMessage += $": Equalize";
}
}
break;
Step 4: MainWindow.xaml
EqualizeParams 클래스에는 아무런 Property 변수를 가지지 않기 때문에 UI(View) 영역에서 처리할 내용(Data Binding)이 없긴 합니다. 다만, 기존 프로젝트 진행 형식에 맞춰 아래와 같이 해당 알고리즘에 대해 사용자에게 알릴 내용을 표시하도록 DataTeamplate을 추가합니다.
<DataTemplate DataType="{x:Type local:EqualizeParams}">
<StackPanel>
<TextBlock Text="Equalize Settings" Margin="0,0,0,5" FontWeight="Bold"/>
<TextBlock Text="* 히스토그램 평활화는 파라미터 설정이 없습니다." Foreground="Gray" FontSize="11" Margin="0,5,0,0" TextWrapping="Wrap"/>
<TextBlock Text="* 적용 후 히스토그램 팝업이 뜹니다." Foreground="Blue" FontSize="11" Margin="0,5,0,0" TextWrapping="Wrap"/>
</StackPanel>
</DataTemplate>
실행 결과/분석
이제 빌드 후 실행해 볼까요? 명암 대비가 낮아 뿌옇게 보이던 사진에 Equalize를 적용해 봅시다.




결과 분석:
- 이미지: 어두운 부분과 밝은 부분의 차이가 극명해지면서, 숨어 있던 구름의 디테일이나 그림자가 선명하게 살아난 것이 확인이 되죠?
- 히스토그램: 가운데에 뾰족하게 뭉쳐 있던 그래프가 좌우로 넓게 퍼진 것을 볼 수 있는데, 이것이 바로 누적 분포를 이용한 재배치의 효과입니다.
하지만 세상 만사가 늘 그렇듯이, Equalize에도 단점은 있습니다. 배경 노이즈까지 너무 선명하게 만들어버리거나, 너무 밝아져서 이미지가 날아가 버리는 경우가 있죠. 이를 보완하기 위해 나온 기술이 바로 CLAHE (Contrast Limiting Adaptive Histogram Equalization)입니다.
다음 포스팅(#20)에서는 이 CLAHE에 대해 알아보고 구현해 보도록 하겠습니다.
참고 자료
[Post #18] 정규화(Normalize): [WPF OpenCV Project #18] – 정규화 구현 (MinMax)
Cv2.EqualizeHist: OpenCV 공식 문서 (EqualizeHist)
Histogram Equalization 원리: Wikipedia – Histogram equalization (CDF를 이용한 픽셀 매핑 원리)
OpenCV API: OpenCV Docs – EqualizeHist