Adaptive Threshold (적응형 이진화)를 구현해 이미지에 있는 그림자 문제도 어느 정도 해결 가능하도록 WPF OpenCV 프로젝트에 적용해 보도록 하겠습니다.
지난 포스팅(#15)에서 우리는 Otsu 알고리즘을 구현했습니다. “자동으로 임계값을 찾아준다” 라는 정말 똑똑한 녀석이었죠. 하지만 그 똑똑한 Otsu에게도 치명적인 약점이 있었으니… 바로 “조명빨“을 심하게 탄다는 것입니다.
이미지 한쪽에 그림자가 져 있거나, 조명이 불균일하면 Otsu(전역 이진화)는 멍청해집니다. 그림자 부분을 전부 ‘물체’로 인식해버리거든요.
그래서 오늘은 이 문제를 해결해 줄 어느 정도 해결할 , 적응형 이진화(Adaptive Threshold)를 구현해 보겠습니다.
Adaptive Threshold (적응형 이진화)
기존의 Threshold가 “반 전체 평균 점수”로 우등생을 뽑는 방식이라면, Adaptive Threshold는 “각 분단별 1등”을 뽑는 방식과 비슷합니다.
- 핵심 원리: 이미지 전체에 하나의 임계 값을 쓰는 게 아니라, 픽셀마다 서로 다른 임계 값을 적용합니다.
- 어떻게? 내 주변(BlockSize) 친구들의 밝기 평균을 구하고, 거기서 조금 뺀 값(Constant C)을 나의 합격 기준(Threshold)으로 삼습니다.
- 장점: 그림자가 져서 어두운 곳에 있는 픽셀은 기준도 같이 낮아지므로, 글자나 물체를 아주 기가 막히게 찾아냅니다. (스탠드 불빛 아래 문서 인식에 최고죠!)
AdaptiveThresholdParams : Model 클래스
먼저 설정 값을 저장할 클래스를 만듭니다. 이번엔 챙겨야 할 옵션이 좀 많습니다. AdaptiveThresholdParams 클래스를 AlgorithmParameters.cs 파일 아래에 적당한 곳에 작성합니다.
public class AdaptiveThresholdParams : AlgorithmParameters
{
// 1. Block Size: 주변을 얼마나 넓게 볼 것인가?
private int _blockSize = 11;
public int BlockSize
{
get => _blockSize;
set
{
if(_blockSize == value) return;
// [중요] OpenCV 규칙: BlockSize는 반드시 '홀수'여야 합니다!
// 짝수가 들어오면 +1을 해서 강제로 홀수로 만듭니다.
if (value % 2 == 0) value++;
if (value < 3) value = 3; // 최소값 안전장치
_blockSize = value;
OnPropertyChanged();
}
}
// 2. Constant C: 평균에서 얼마나 뺄 것인가? (민감도 조절)
private double _constantC = 2.0;
public double ConstantC
{
get => _constantC;
set
{
if(_constantC == value) return;
_constantC = value;
OnPropertyChanged();
}
}
// 3. 적응형 방식 (평균 계산법): 산술 평균(Mean) vs 가중 평균(Gaussian)
private AdaptiveThresholdTypes _adaptiveMethod = AdaptiveThresholdTypes.MeanC;
public AdaptiveThresholdTypes AdaptiveMethod
{
get => _adaptiveMethod;
set
{
if (_adaptiveMethod == value) return;
_adaptiveMethod = value;
OnPropertyChanged();
}
}
// 4. 결과 타입 (흑백 vs 반전)
private ThresholdTypes _thresholdType = ThresholdTypes.Binary;
public ThresholdTypes ThresholdType
{
get => _thresholdType;
set
{
if (_thresholdType == value) return;
_thresholdType = value;
OnPropertyChanged();
}
}
// 콤보박스 바인딩용 리스트
public List<AdaptiveThresholdTypes> AdaptiveMethodSource { get; } = new List<AdaptiveThresholdTypes>
{
AdaptiveThresholdTypes.MeanC,
AdaptiveThresholdTypes.GaussianC
};
public List<ThresholdTypes> ThresholdTypesSource { get; } = new List<ThresholdTypes>
{
ThresholdTypes.Binary,
ThresholdTypes.BinaryInv
};
}
추가한 클래스 코드 내용에 머리를 아프게 할 만큼의 난이도 높은 부분은 없어 보이죠?
코드 중간에 if (value % 2 == 0) value++; 라는 부분이 있습니다. Adaptive Threshold는 “나(픽셀)를 중심으로 주변을 살피는” 알고리즘입니다. 내가 딱 한가운데 있으려면 전체 크기가 3×3, 5×5 처럼 홀수여야 대칭이 맞습니다. 짝수(예: 4×4)면 중심을 잡을 수가 없어서 OpenCV가 에러를 뱉습니다. (모르면 디버깅하느라 고생하는 포인트입니다!)
DataTemplate : UI(View) 구성
이제 사용자가 값을 조절할 수 있도록 슬라이더와 콤보박스를 배치합니다. MainWindow.xaml의 리소스 섹션에 아래 DataTemplate(템플릿)을 추가합니다. (이젠 익숙하시죠?)
<DataTemplate DataType="{x:Type local:AdaptiveThresholdParams}">
<StackPanel>
<TextBlock Text="Adaptive Threshold Settings" Margin="0,0,0,5" FontWeight="Bold"/>
<TextBlock Text="Adaptive Method" Margin="0,5,0,0"/>
<ComboBox ItemsSource="{Binding AdaptiveMethodSource}"
SelectedItem="{Binding AdaptiveMethod}"
Margin="0,2,0,0" Height="25"/>
<TextBlock Text="Threshold Type" Margin="0,5,0,0"/>
<ComboBox ItemsSource="{Binding ThresholdTypesSource}"
SelectedItem="{Binding ThresholdType}"
Margin="0,2,0,0" Height="25"/>
<TextBlock Text="Block Size (홀수만 가능)" Margin="0,5,0,0"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions>
<Slider Grid.Column="0" Maximum="99" Minimum="3" SmallChange="2" LargeChange="2"
TickFrequency="2" IsSnapToTickEnabled="True"
Value="{Binding BlockSize}" VerticalAlignment="Center"/>
<TextBox Grid.Column="1" Text="{Binding BlockSize}" Margin="5,0,0,0" TextAlignment="Center"/>
</Grid>
<TextBlock Text="Constant (C)" Margin="0,5,0,0" />
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions>
<Slider Grid.Column="0" Minimum="-20" Maximum="50" Value="{Binding ConstantC}"
VerticalAlignment="Center"/>
<TextBox Grid.Column="1" Text="{Binding ConstantC}" Margin="5,0,0,0" TextAlignment="Center"/>
</Grid>
</StackPanel>
</DataTemplate>
ViewModel & Service: 알고리즘 연결
이제 ViewModel에 알고리즘 이름을 등록하고, OpenCVService에서 실제 처리를 할 차례입니다.
MainViewModel.cs:
// 생성자
AlgorithmList = new ObservableCollection<string>
{
"Threshold",
"Otsu Threshold",
"Adaptive Threshold" // [추가]
};
// 파라미터 생성 함수 (CreateParametersForAlgorithm)
case "Adaptive Threshold":
CurrentParameters = new AdaptiveThresholdParams();
break;
OpenCVService.cs (KeyPoint):
// ProcessImageAsync 내부 switch문
case "Adaptive Threshold":
if (parameters is AdaptiveThresholdParams adParams)
{
using (Mat gray = new Mat())
{
// 1. 그레이스케일 변환
Cv2.CvtColor(_srcImage, gray, ColorConversionCodes.BGR2GRAY);
// 2. 적응형 이진화 실행
Cv2.AdaptiveThreshold(gray, _destImage, 255,
adParams.AdaptiveMethod, // 계산 방식 (Mean / Gaussian)
adParams.ThresholdType, // 결과 타입 (Binary / Inv)
adParams.BlockSize, // 블록 크기 (홀수)
adParams.ConstantC); // 보정 상수
resultMessage += $": {algorithm} (Block Size: {adParams.BlockSize}, C: {adParams.ConstantC})";
}
}
break;
어려운 코드는 전혀 없습니다. UI에서 받은 파라미터 4개를 그대로 Cv2.AdaptiveThreshold 함수에 넣어주기만 하면 끝입니다.
MeanC vs GaussianC : 실행 결과 및 비교
자, 이제 실행해서 결과를 비교해 봅시다. 특히 Adaptive Method에서 MeanC와 GaussianC의 차이를 눈여겨보세요. MeanC와 GaussianC 에 대해 조금 정리하면 이렇습니다.
MeanC (산술 평균): 주변 픽셀 값을 전부 똑같이 취급해서 평균을 냅니다. 속도는 빠르지만 Noise(노이즈)가 좀 생길 수 있습니다.
GaussianC (가중 평균): 나와 가까운 픽셀일수록 더 중요하게(가중치) 계산합니다. 훨씬 부드럽고 자연스러운 결과를 얻을 수 있습니다. (실제 동작해 보면 GaussianC가 훨씬 품질이 좋다는 걸 느끼실 겁니다.)



1번째 이미지는 원본 이미지를 보여줍니다. 2번째 이미지는 실행 후 Adaptive Threshold 알고리즘을 선택하면 Default 로 값으로 적용한 결과 이미지 이구요, 3번째 이미지는 AdaptiveMethod 만 GaussianC로 적용한 결과 이미지 입니다.



위의 4번째 이미지는 GaussianC Method에서 Block Size를 65로 조정하여 실행한 이미지 입니다. 5 번째 이미지는 GaussianC와 Block Size 11, Contant 7.0을 적용한 이미지이고, 6번째 이미지는 5번째 이미지도 동일하지만, Threshold Type만 BinaryInv 를 적용하여 실행한 이미지 입니다. 아래의 마지막 이미지는 대부분 동일하지만, Block Size를 55, ContantC 36.7 로 적용하여 실행한 결과 이미지 입니다. 빌드 후 이미지를 로드해서 각자 설정 가능한 파라미터들을 조절해 보면서 결과를 분석해 보면 좋을 듯 하네요 ^^;

이로써 조명이나 그림자 따위는 가볍게 무시해 버리는 강력한 이진화 도구를 손에 넣었습니다. 다음 시간에는 영상 분포를 한눈에 파악할 수 있는 히스토그램(Histogram) 기능을 구현해 보도록 하겠습니다.
참고 자료
Otsu 알고리즘: [WPF OpenCV Project #15] – 전역 이진화의 한계
DataTemplate: [WPF OpenCV Project #5] – UI 자동 생성 원리
OpenCV 공식 문서 (Adaptive Threshold API)
https://docs.opencv.org/4.x/d7/d1b/group__imgproc__misc.html#ga72b913f352e4a1b1b397736707afcde3