BasicDifferential, Roberts, Prewitt, Sobel, Scharr, Laplacian, Canny Edge 를 이용한 Edge Detection (경계 처리)를 주제로 OpenCvSharp에서 제공하는 함수들을 이용해서 한꺼번에 순차적으로 간단히 개념을 정리하고, WPF OpenCV 프로젝트에 구현해 보겠습니다.
지난 포스팅(#30)에서 Average Blur, Box Filter, Gaussian Blur, Median Blur, Bilateral Filter 들을 OpenCvSharp에서 제공하는 함수들을 이용해서 처리하는 방법을 나름 깊이 있게 다뤘습니다. 이번 포스팅에서 다룰 Edge Detection (경계 처리) 은 이미지 내에서 밝기가 급격하게 변하는 지점을 찾는 방법입니다. 사실 다루기는 저도 싫어하지만, 수학적으로 Derivative (미분) 이라는 공통적인 내용을 사용하여 변화율이 큰 지점을 찾아내는 것이 핵심입니다.
아무튼 여러 내용을 다뤄야 하기에 거두절미 하고 바로 진행 할게요.
Theory
Basic Differential Filter
가장 원초적인 형태의 경계 검출 방식입니다. 디지털 이미지에서의 미분은 연속적이지 않으므로 Difference (차분)으로 계산합니다. 쉽게 이야기 하면, 현재 픽셀과 바로 옆 또는 아래에 있는 픽셀들 간의 차이를 구하는 거죠. 수학적으로 아래와 같이 모델링이 가능합니다.
사용하는 OpenCVSharp 에서는 별다른 함수를 제공하진 않지만, 이전 포스팅 (Convolution) 에 다뤗던 Cv2.Filter2D() 함수를 사용하고, Kernel Matrix (커널 행렬)을 활용하여 Differential Filter 을 구현합니다.
Cv2.Filter2D() 함수는 이전에 살펴봤기 때문에 따로 다루지는 않지만, Differential Filter 구현을 위해 Kernel Matrix (커널 행렬) 은 보통 [-1, 1] 또는 [[-1], [1]] 형태로 설정하여 사용합니다. 그리고, Cv2.Filter2D() 함수의 ddepth 파라미터는 결과 이미지의 정밀도 값을 설정하는 것이라고 이전에 설명했었는데요. (기억이 안난다면 이전 포스팅을 참고해 주세요.) 미분 결과는 음수가 나올 수 있기 때문에 MatType.CV16S 를 사용해서 데이터를 보존한 뒤 ConvertScaleAbs 로 절대 값을 취해 8bit 로 변환합니다. 비교적 단순한 구조이기에 이미지에 노이즈가 전혀 없고, 경계가 매우 뚜렸한 이미지에 적당한 방법입니다. 그냥 학습용으로 미분의 기본 원리를 확인 하고자 할때 사용한다 라고 알고 있으면 될것 같네요.
실제 복잡한 이미지에서는 노이즈에 취약하기 때문에 거의 사용하지 않는 방법이긴 해요.
Roberts Cross Filter
사람 이름을 그대로 가져온 필터로, Lawrence Roberts가 제안한 방식으로, 2 x 2 크기의 아주 작은 Kernel Matrix (커널 행렬)을 사용합니다. 이 알고리즘은 이미지의 대각선 방향으로 픽셀 값 차이를 계산해서 경계를 찾는 방법입니다. 수학적으로 아래와 같이 모델링이 가능합니다.
Roborts 알고리즘도 Cv2.Filter2D() 함수를 사용하고, Kernel Matrix (커널 행렬)을 활용하여 구현합니다. Kernel Matrix 는 [[1, 0], [0, -1]] 또는 [[0,1], [-1, 0]] 을 사용하구요, Kernel 크기가 작아서 연산 속도가 정말 압도적으로 빠름 빠름~~ 합니다. 가끔 자원 (Resource)의 제한이 있는 Embedded System 환경에서 적용하기 적당합니다. 수평이나, 수직 외에 대각선 방향의 급격한 변화를 찾아 내는데 탁월하죠.
Prewitt Filter
이것도 사람의 이름에서 딴 필터 입니다. Judith M. S. Prewitt 이 제안한 방식으로 3 x 3 크기를 사용해서 미분 필터의 노이즈에 대한 취약성을 보완한 필터인데요. 한 방향의 차이를 구할 때 인접한 3개 행 또는 열에 대해 합을 구하고, 평균을 내어 계산 하는 방식입니다.
수학적으로 아래와 같이 모델링이 가능합니다.
아래에서 다루겠지만, Sobel filter 와 유사하지만 가중치를 주진 않습니다. 주로 수평 또는 수직 방향으로 정렬된 경계선을 뚜렷하게 검출할 때 Sobel (소벨) 보다 더 깔끔한 결과를 줄때가 있다고 하더군요. 모든게 좋을 수 없기에, 대각선 검출에 약합니다.
Sobel Filter
아마도, 1차 미분 기반 필터에서 가장 대중적으로 사용되는 필터인거 같네요. Irwin Sobel 이 제안한 방식으로 중심 픽셀에 두 배의 가중치를 주어 주변 보다 현재 위치의 변화를 더 강조하는 방식인데요. 수평, 수직, 대각선 경계 검출에 강한 Kernel Matrix 를 아래와 같이 제안했다고 하더군요.
Sobel Filter는 앞서 설명한 다른 경계 검출 필터와 달리 OpenCvSharp 에서 Cv2.Sobel() 이라는 전용 함수를 제공합니다. Cv2.Sobel() 함수 원형은 아래와 같습니다.
void Cv2.Sobel
(
InputArray src,
OutputArray dst,
MatType ddepth,
int dx,
int dy,
int ksize = 3,
double scale = 1,
double delta = 0,
BorderTypes borderType = BorderTypes.Default
);
주요 파라미터를 살펴 보죠.
dx / dy (int): 각각 x 방향과 y 방향의 미분 차수이며, x 방향 경계를 찾으려면 dx=1, dy=0, y 방향 경계를 찾으려면 dx=0, dy=1을 입력합니다.
ksize (int): 확장된 소벨 커널의 크기이며, 1, 3, 5, 7과 같은 홀수여야 합니다. 값이 클수록 더 넓은 영역을 참고하므로 노이즈에 강해지지만 경계가 뭉툭해질 수 있습니다. 특별히 -1을 입력하면 아래에서 언글할 3 x 3 샤르(Scharr) 필터로 동작합니다.
scale (double): 계산된 미분 값에 곱할 선택적 계수로, 기본값은 1입니다. 화면이 너무 검게 나온다면 이 값을 높여 엣지 강도를 증폭시킬 수 있습니다.
delta (double): 결과 값에 마지막으로 더해지는 값으로, 기본값은 0입니다. 이미지 전체의 밝기를 조절하는 용도로 쓰입니다.
borderType (BorderTypes): 이미지 테두리 부분의 픽셀을 어떻게 처리할지 결정하는 방식입니다.
Sobel Filter 를 이용하는 경우는 범용적으로 사용되는데요, 대부분의 산업용 비전 검사에서 경계의 대략적인 위치를 찾을 때 많이 사용된다고 봐도 무방할 듯 하네요.
Scharr Filter
앞서 언급했던 Sobel Filter는 단점이 있는데요. kernel 의 크기가 작은 경우나, 커널의 크기가 크더라도 그 중심에서 멀어질수록 Edge(경계) 의 방향성의 정확도가 떨어지는 단점을 있습니다. 그래서 Scharr 라는 사람이 Sobel 보다 가중치를 훨씬 정교하게 부여해서 Edge (경계선)가 어느 각도로 있든 동일한 강도로 검출되도록 설계하였다고 합니다. Scharr Filter에서 사용하는 Kernel Matrix는 아래와 같습니다.
Scharr Filter는 앞서 설명한 Soble Filter 처럼 OpenCvSharp 에서 Cv2.Scharr() 이라는 전용 함수를 제공합니다. Cv2.Scharr() 함수 원형은 아래와 같습니다.
void Cv2.Scharr
(
InputArray src,
OutputArray dst,
MatType ddepth,
int dx,
int dy,
double scale = 1,
double delta = 0,
BorderTypes borderType = BorderTypes.Default
);
주요 파라미터를 살펴 보죠.
dx / dy (int): 각각 x 방향과 y 방향의 미분 차수이며, x 방향 경계 검출: dx=1, dy=0, y 방향 경계 검출: dx=0, dy=1 을 사용합니다. 주의 해야 할 내용이 있는데요. 한 방향씩만 계산 가능하기 때문에, dx + dy는 반드시 1이어야 합니다.
scale (double): 계산된 미분 값에 곱해지는 가중치(비율)를 의미하며, 기본값은 1입니다.
delta (double): 결과 영상에 일괄적으로 더해지는 값입니다. 기본값은 0입니다.
borderType (BorderTypes): 가장자리 픽셀 확장 방식입니다.
Scharr() 함수와 Sobel() 함수의 차이를 잠깐 언급하고 갈께요. 가장 큰 차이점은 대략 눈치 챘을거라 생각하는데, 뭐냐 하면, ksize 파라미터가 없습니다. Sobel() 함수는 사용자가 커널 크기(3, 5, 7 등)를 선택할 수 있습니다. 하지만, Scharr() 함수는 커널 크기가 3 x 3으로 고정되어 있습니다. 이는 3 x 3 영역에서 수학적으로 가장 완벽한 회전 불변성(이미지가 돌아가도 엣지 강도가 일정함)을 갖도록 설계되었기 때문입니다. Scharr Filter는 이미지 경계선의 각도를 정밀하게 측정해야 하는 경우와 Sobel filter에서 놓치는 아주 미세한 변화를 잡고 싶을 때 사용된다는 점 기억해 주세요.
Gaussian Filter
Gaussian Filter 는 이미 이전 포스팅에서 다뤘기에 별다르게 다루진 않을게요. 이전 포스팅을 참고해 주세요. 사실 Gaussian Filter 는 Edge Detection 이라기 보다는 전처리에 사용되는 필터 입니다. 다만 앞서 다뤘던 Sobel filter 또는 아래에 다룰 Canny Edge 적용 전에 이미지에 반드시 다뤄야 하는 필수 단계로 사용됩니다. 노이즈를 먼저 제거해야 가짜 Edge 가 발생하지 않을 거니까요. Gaussian Filter는 이걸로 마무리하겠습니다.
Canny Edge Detection
John Canny 라는 사람이 제안한 알고리즘으로, 가장 완벽하다고 평가받는 엣지 검출기입니다. 단순히 미분만 하는 게 아니라 4단계 과정을 거치게 되는데, 차례대로 알아보죠.
1단계: 노이즈 제거를 위해 Gaussian Blur(가우시안 블러) 적용.
2단계: Gradiant (그레이디언트)를 계산합니다. 앞서 설명한 Sobel Filter 등으로 강도와 방향을 탐색.
3단계: Non-Maximum Suppression(비최대 억제) 를 통해 Edge 두께를 1 Pixel로 얇게 만듭니다. 그레디언트 방향에서 검출된 edge 중에 가장 큰 값만 선택하고 나머지는 제거 합니다.
4단계: Hysteresis Thresholding(이중 임계값) 을 적용하여 진짜 Edge와 가짜 Edge를 구분합니다. 두 개의 경계값(min, max)을 지정해서 경계 영역에 있는 픽셀들 중 큰 경계 값(max) 밖의 pixel과 연결성이 없는 pixel을 제거하는 거죠.
정리하면, Canny Edge Detection 은 내부적으로 가우시안 블러(전처리) → 소벨 연산(그레이디언트 추출) → 비최대 억제 → 히스테리시스 임계값 처리의 과정을 거칩니다.
OpenCvSharp 에서는 Cv2.Canny() 이라는 전용 함수를 제공합니다. Cv2.Canny() 함수는 단순히 미분만 하는 Sobel() 이나 Scharr()와 달리, 앞서 설명한 히스테리시스 임계값(Hysteresis Thresholding)이라는 기법을 사용하여 엣지의 연결성을 고려합니다.
Cv2.Canny() 함수 원형은 아래와 같습니다.
void Cv2.Canny
(
InputArray image,
OutputArray edges,
double threshold1,
double threshold2,
int apertureSize = 3,
bool L2gradient = false
);
주요 파라미터를 살펴 보도록 하죠.
image: 8비트 입력 영상입니다.
edges: 결과 엣지 맵입니다 (단일 채널 8비트).
threshold1 (Lower): 히스테리시스 임계값 처리에서의 낮은 임계값입니다. 이 값보다 낮은 변화량은 엣지가 아닌 것으로 간주합니다.
threshold2 (Upper): 높은 임계값입니다. 이 값보다 높은 변화량은 확실한 강한 엣지(Strong Edge)로 간주합니다. 두 임계값 사이의 값은 강한 엣지와 연결되어 있을 때만 엣지로 인정됩니다.
apertureSize: 그레이디언트 계산을 위한 소벨 연산자의 커널 크기입니다 (기본값 3).
L2gradient: 엣지 강도를 계산할 때 L2 Norm 을 사용하지 L1 Norm 을 사용하지 결정합니다.
Canny Edge 에 대해 조금 덧붙이자면, 앞서 언급한것 처럼 가장 완벽하다고 평가 받는 Edge Detection 으로, threshold1 (낮은 값)과 threshold2 (높은 값)의 비율을 보통 1:2 또는 1:3 정도로 설정하면 노이즈에 강하면서도 끊기지 않는 엣지를 얻을 수 있습니다. 예를 들어, threshold1=50, threshold2=150으로 설정하면 강도가 150 이상인 곳은 무조건 엣지가 되고, 50~150 사이인 곳은 150 이상인 곳과 연결되어 있을 때만 엣지로 남게 됩니다. 이미지가 복잡한 배경을 가지고 있을 때 물체의 정확하고, 정밀한 외곽선(경계선)을 따야 할 경우, 여러 고민하지 말고 사용하세요. 그리고 OCR (문자 인식) 이나, 물체의 형상 인식 전 단계에 아주 유용합니다.
가끔 이런 질문을 하더군요. Cv2.Canny() 함수에서 threshold1과 threshold2의 순서를 바꾸어 입력했을 때의 동작은 어떻게 되냐구요. 결론부터 말씀드리면, OpenCV 내부적으로 두 값의 크기를 비교하여 작은 값을 하단 임계값(min), 큰 값을 상단 임계값(max)으로 자동 교체(Swap)하여 처리합니다. 따라서 사용자가 실수로 min 150, max 50으로 입력하더라도, 라이브러리 내부에서는 min 50, max 150으로 설정한 것과 완전히 동일한 결과를 출력합니다.
구현 요약
위 에서 사용할 Edge Detect (경계 검출) 들에 대해 간략히 (?) 정리했으니까 이제 구현을 해야겠죠. 늘 그래왔던 것처럼 WPF OpenCV 프로젝트에 구현할 내용을 아래와 같이 요약하고 단계 별로 구현 해 보도록 하죠.
AlgorithmParameters.cs: EdgeDetectionParams 클래스와 EdgeDetectionType 열거형을 추가하여 각 알고리즘 필터별 파라미터(임계값, 커널 크기 등)를 정의합니다.
MainViewModel.cs: 알고리즘 목록에 “Edge Detection“를 추가하고, 선택 시 파라미터 객체를 생성하도록 연결하도록 하죠.
MainWindow.xaml: “Edge Detection” 선택 시 나타날 UI(콤보박스, 슬라이더 등)를 DataTemplate에 추가하고, 선택할 수 있도록 구현 합니다.
OpenCVService.cs: BasicDifferential, Roberts, Prewitt, Sobel, Scharr, Laplacian, Canny Edge 에대한 OpenCvSharp 함수 이용해서 실제로 수행하는 로직을 구현합니다.
Step 1: AutoFilterParams (Model)
AlgorithmParameter.cs 파일에 먼저 EdgeDetectionType 열거형 데이터를 추가합니다. EdgeDetectionParams 클래스에는 적용할 Filter 함수들의 파라미터를 아래의 코드와 같이 정의해서 UI와 연결 되도록 하죠.
public enum EdgeDetectionType
{
BasicDifferential, // 기본 미분 필터
Roberts, // 로버츠 교차 필터
Prewitt, // 프리윗 필터
Sobel, // 소벨 필터
Scharr, // 샤르 필터
Laplacian, // 라플라시안 필터
Canny // 캐니 엣지
}
public class EdgeDetectionParams : AlgorithmParameters
{
private EdgeDetectionType _selectedType = EdgeDetectionType.Sobel;
public EdgeDetectionType SelectedType
{
get => _selectedType;
set { if (_selectedType != value) { _selectedType = value; OnPropertyChanged(); } }
}
// 공통: 커널 크기 (Sobel, Scharr, Laplacian, Canny용)
private int _ksize = 3;
public int KSize
{
get => _ksize;
set
{
// Sobel, Laplacian 등은 홀수만 가능 (1, 3, 5, 7)
if (value % 2 == 0) value++;
if (value < 1) value = 1;
if (value > 7) value = 7;
if (_ksize != value) { _ksize = value; OnPropertyChanged(); }
}
}
// Canny 전용: Threshold1 (낮은 임계값)
private double _cannyTh1 = 50;
public double CannyTh1
{
get => _cannyTh1;
set { if (_cannyTh1 != value) { _cannyTh1 = value; OnPropertyChanged(); } }
}
// Canny 전용: Threshold2 (높은 임계값)
private double _cannyTh2 = 150;
public double CannyTh2
{
get => _cannyTh2;
set { if (_cannyTh2 != value) { _cannyTh2 = value; OnPropertyChanged(); } }
}
// Sobel/Laplacian 전용: Scale
private double _scale = 1.0;
public double Scale
{
get => _scale;
set { if (_scale != value) { _scale = value; OnPropertyChanged(); } }
}
// Sobel/Laplacian 전용: Delta
private double _delta = 0.0;
public double Delta
{
get => _delta;
set { if (_delta != value) { _delta = value; OnPropertyChanged(); } }
}
public List<EdgeDetectionType> EdgeTypeSource { get; } = Enum.GetValues(typeof(EdgeDetectionType)).Cast<EdgeDetectionType>().ToList();
}
Step 2: MainWindow.xaml (View)
MainWindow.xaml 파일에는 EdgeDetectionParams 의 Property 속성을 UI에 연결하고, 사용자가 “Edge Detection” 선택 시 나타날 UI(콤보박스, 슬라이더 등)를 아래의 코드와 같이 DataTemplate에 추가해서, 선택된 필터를 설정합니다.
<!-- EdgeDetectionParams 템플릿 -->
<DataTemplate DataType="{x:Type local:EdgeDetectionParams}">
<StackPanel>
<TextBlock Text="Edge Detection Settings" Margin="0,0,0,5" FontWeight="Bold"/>
<TextBlock Text="Algorithm Type" Margin="0,5,0,0"/>
<ComboBox ItemsSource="{Binding EdgeTypeSource}"
SelectedItem="{Binding SelectedType}"
Margin="0,2,0,0" Height="25"/>
<Separator Margin="0,10,0,10"/>
<!-- Kernel Size (Canny, Sobel, Laplacian 전용) -->
<StackPanel>
<StackPanel.Style>
<Style TargetType="StackPanel">
<Setter Property="Visibility" Value="Visible"/>
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedType}" Value="BasicDifferential"><Setter Property="Visibility" Value="Collapsed"/></DataTrigger>
<DataTrigger Binding="{Binding SelectedType}" Value="Roberts"><Setter Property="Visibility" Value="Collapsed"/></DataTrigger>
<DataTrigger Binding="{Binding SelectedType}" Value="Prewitt"><Setter Property="Visibility" Value="Collapsed"/></DataTrigger>
<DataTrigger Binding="{Binding SelectedType}" Value="Scharr"><Setter Property="Visibility" Value="Collapsed"/></DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
<TextBlock Text="Kernel Size (1, 3, 5, 7)" Margin="0,5,0,0"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions>
<Slider Grid.Column="0" Minimum="1" Maximum="7" Value="{Binding KSize}"
VerticalAlignment="Center" TickFrequency="2" IsSnapToTickEnabled="True" />
<TextBox Grid.Column="1" Text="{Binding KSize}" Margin="5,0,0,0" TextAlignment="Center" />
</Grid>
</StackPanel>
<!-- Sobel, Laplacian 전용 Scale/Delta -->
<StackPanel>
<StackPanel.Style>
<Style TargetType="StackPanel">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedType}" Value="Sobel"><Setter Property="Visibility" Value="Visible"/></DataTrigger>
<DataTrigger Binding="{Binding SelectedType}" Value="Scharr"><Setter Property="Visibility" Value="Visible"/></DataTrigger>
<DataTrigger Binding="{Binding SelectedType}" Value="Laplacian"><Setter Property="Visibility" Value="Visible"/></DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
<TextBlock Text="Scale" Margin="0,5,0,0"/>
<TextBox Text="{Binding Scale}" Margin="0,2,0,0"/>
<TextBlock Text="Delta" Margin="0,5,0,0"/>
<TextBox Text="{Binding Delta}" Margin="0,2,0,0"/>
</StackPanel>
<!-- Canny 전용 Thresholds -->
<StackPanel>
<StackPanel.Style>
<Style TargetType="StackPanel">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedType}" Value="Canny"><Setter Property="Visibility" Value="Visible"/></DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
<TextBlock Text="Canny Lower Threshold" Margin="0,5,0,0"/>
<Slider Minimum="0" Maximum="255" Value="{Binding CannyTh1}" VerticalAlignment="Center" />
<TextBlock Text="Canny Upper Threshold" Margin="0,5,0,0"/>
<Slider Minimum="0" Maximum="255" Value="{Binding CannyTh2}" VerticalAlignment="Center" />
</StackPanel>
<TextBlock Text="* 경계 검출은 내부적으로 Gray 변환 후 처리됩니다." FontSize="10" Foreground="Gray" Margin="0,10,0,0" TextWrapping="Wrap"/>
</StackPanel>
</DataTemplate>
Step 3: MainViewModel (ViewModel)
MainViewModel.cs 파일의 MainViewModel() 생성자 함수의 알고리즘 목록에 Edge Detection 를 추가하고, 사용자가 Edge Detection 를 선택했을 때, CreateParametersForAlgorithm() 함수에서 EdgeDetectionParams 객체의 파라미터 객체를 생성해 연결 하도록 아래 코드로 업데이트 합니다.
public MainViewModel()
{
_cvServices = new OpenCVService();
AlgorithmList = new ObservableCollection<string>
{
"Threshold",
"Otsu Threshold",
"Adaptive Threshold",
"Histogram",
"Normalize",
"Equalize",
"CLAHE",
"Geometric Transformation",
"Affine Transform",
"Perspective Transform",
"Lens Distortion (Remap)",
"Camera Calibration",
"Manual Filter",
"Auto Filter",
"Edge Detection" // [추가] 경계 검출 항목 추가
};
}
private void CreateParametersForAlgorithm(string? algoName)
{
switch (algoName)
{
// ... 기존 case들 유지 ...
case "Manual Filter":
CurrentParameters = new ManualFilterParams();
break;
case "Auto Filter":
CurrentParameters = new AutoFilterParams();
break;
case "Edge Detection": // [추가]
CurrentParameters = new EdgeDetectionParams();
break;
default:
CurrentParameters = null;
break;
}
}
// ... 나머지 코드 유지 ...
}
Step 4: OpenCVService (model)
OpenCVService.cs 파일에서는 ProcessImageAsync() 함수에 Cv2.Sobel(), Cv2.Scharr(), Cv2.Canny() 함수와 같이 OpenCvSharp에서 제공하는 함수를 이용해서 영상 처리 하는 코드를 아래와 같이 추가하면 됩니다. switch 구문 내에 공통 코드가 중복 되더라도 그대로 작성할까 하다가, 외부로 중복되는 코드를 밖으로 빼서 함수 하나를 추가했습니다. ApplyCustomEdgeFilter() 라는 함수도 추가해 주세요.
public async Task<string> ProcessImageAsync(string algorithm, AlgorithmParameters? parameters)
{
// ........... 기존 코드 유지 ..........
switch (algorithm)
{
case "Edge Detection":
if (parameters is EdgeDetectionParams edgeParams)
{
using (Mat gray = new Mat())
{
Cv2.CvtColor(_srcImage, gray, ColorConversionCodes.BGR2GRAY);
double s = edgeParams.Scale;
double d = edgeParams.Delta;
switch (edgeParams.SelectedType)
{
case EdgeDetectionType.BasicDifferential:
ApplyCustomEdgeFilter(gray, _destImage,
new float[] { 0, 0, 0, -1, 1, 0, 0, 0, 0 }, // X
new float[] { 0, -1, 0, 0, 1, 0, 0, 0, 0 }, // Y
s, d);
break;
case EdgeDetectionType.Roberts:
ApplyCustomEdgeFilter(gray, _destImage,
new float[] { 1, 0, 0, -1 }, // X
new float[] { 0, 1, -1, 0 }, // Y
s, d, 2); // 2x2 Kernel
break;
case EdgeDetectionType.Prewitt:
ApplyCustomEdgeFilter(gray, _destImage,
new float[] { -1, 0, 1, -1, 0, 1, -1, 0, 1 },
new float[] { -1, -1, -1, 0, 0, 0, 1, 1, 1 },
s, d);
break;
case EdgeDetectionType.Sobel:
using (Mat dx = new Mat())
using (Mat dy = new Mat())
using (Mat dx8u = new Mat())
using (Mat dy8u = new Mat())
{
// 16S로 미분 후 사용자의 Scale을 적용하여 8U로 변환
Cv2.Sobel(gray, dx, MatType.CV_16S, 1, 0, edgeParams.KSize);
Cv2.Sobel(gray, dy, MatType.CV_16S, 0, 1, edgeParams.KSize);
Cv2.ConvertScaleAbs(dx, dx8u, s, d);
Cv2.ConvertScaleAbs(dy, dy8u, s, d);
Cv2.AddWeighted(dx8u, 1.0, dy8u, 1.0, 0, _destImage);
}
break;
case EdgeDetectionType.Scharr:
using (Mat dx = new Mat())
using (Mat dy = new Mat())
using (Mat dx8u = new Mat())
using (Mat dy8u = new Mat())
{
Cv2.Scharr(gray, dx, MatType.CV_16S, 1, 0);
Cv2.Scharr(gray, dy, MatType.CV_16S, 0, 1);
Cv2.ConvertScaleAbs(dx, dx8u, s, d);
Cv2.ConvertScaleAbs(dy, dy8u, s, d);
Cv2.AddWeighted(dx8u, 1.0, dy8u, 1.0, 0, _destImage);
}
break;
case EdgeDetectionType.Laplacian:
using (Mat lap = new Mat())
{
Cv2.Laplacian(gray, lap, MatType.CV_16S, edgeParams.KSize);
Cv2.ConvertScaleAbs(lap, _destImage, s, d);
}
break;
case EdgeDetectionType.Canny:
Cv2.Canny(gray, _destImage, edgeParams.CannyTh1, edgeParams.CannyTh2, edgeParams.KSize);
break;
}
resultMessage += $": {edgeParams.SelectedType}";
}
}
break;
}
// .. 기존 코드 유지 ...
private void ApplyCustomEdgeFilter(Mat src, Mat dst, float[] dataX, float[] dataY, double scale, double delta, int kSize = 3)
{
using (Mat kX = new Mat(kSize, kSize, MatType.CV_32F))
using (Mat kY = new Mat(kSize, kSize, MatType.CV_32F))
using (Mat dx = new Mat())
using (Mat dy = new Mat())
using (Mat dx8u = new Mat())
using (Mat dy8u = new Mat())
{
Marshal.Copy(dataX, 0, kX.Data, dataX.Length);
Marshal.Copy(dataY, 0, kY.Data, dataY.Length);
Cv2.Filter2D(src, dx, MatType.CV_16S, kX);
Cv2.Filter2D(src, dy, MatType.CV_16S, kY);
// 시각화를 위해 Scale 적용 및 절대값 변환
Cv2.ConvertScaleAbs(dx, dx8u, scale, delta);
Cv2.ConvertScaleAbs(dy, dy8u, scale, delta);
// 두 결과를 합산 (0.5 대신 1.0을 사용하여 선명도 유지)
Cv2.AddWeighted(dx8u, 1.0, dy8u, 1.0, 0, dst);
}
}
실행 및 확인
한꺼번에 7개의 Edge Detection (BasicDifferential, Roberts, Prewitt, Sobel, Scharr, Laplacian, Canny Edge) 을 추가하였습니다. 이전 포스팅도 그랬지만, 추가된 내용에 비해 코드량은 그다지 많지 않았던것 같은데, 어떻게 느꼈는지 모르겠네요. 아마도 OpenCV 에서 제공하는 함수를 이용해서 관련 기능들을 구현했기때문 이겠죠. 이제 빌드 후 실행 해서 이미지들의 Edge Detection 처리 결과를 살펴 보도록 하죠.
첫 번째는 원본 이미지와 Basic Differenctial 처리 이미지 입니다.


두 번째는 Roberts 처리 이미지 입니다.

세 번째는 Prewitt 처리 이미지 입니다.

네 번째는 Sobel 처리 이미지 입니다.
파라미터 중에서 Scale: 2 로 설정하고, 나머지 파라미터 값은 기본 값을 사용했습니다.
엣지가 검출되긴 하지만 선이 두껍고, 배경의 자잘한 노이즈도 같이 검출됩니다.

다섯번째는 Scharr 처리 이미지 입니다.
파라미터 중에서 Scale: 1 로 설정하고, 나머지 파라미터 값은 기본 값을 사용했습니다.

여섯 번째는 Laplacian 처리 이미지 입니다. 파라미터는 기본 값으로 적용했습니다.
엣지가 매우 날카롭지만, 노이즈에 너무 민감해서 점들이 많이 보입니다.

일곱 번째는 Canny Edge 처리 이미지 입니다.
파라미터는 기본값으로 적용했습니다. (Low Threshold=50, Upper Threshold=150)
선이 실처럼 얇고 끊김이 없으며, 불필요한 노이즈가 거의 없습니다. 우리가 원하는 물체의 윤곽(Contour)을 따기에 가장 완벽합니다.
(Canny Low Threshold 와 Canny Upper Threshold 관련해서 제가 Slider 만 적용하고 TextBox 를 구현하지 않았군요. ㅠㅠ 추후에 해당 부분도 업데이트 하도록 하겠습니다. 각자 알아서 업데이트 하셔도 되구요. 어렵지 않습니다. 기존에 많이 썻구요, xaml 코드에서 kernel Size 부분을 참고해 주세요.^^)

빌드해서 검증해 보면 알겠지만, Canny Edge Detection 이 거의 끝판왕이긴 하네요.
BasicDifferential, Roberts, Prewitt 알고리즘은 코드 상에 고정된 Kernel matrix 를 사용하고 있습니다.
그외의 Edge Detection Filter 마다 사용자가 설정 할 수 있도록 만들었으니까, 설정 값을 변경해 가면서 Edge Detection 을 얼마나 효과적으로 구분해 내는지 경험해 보면 좋겠네요.
다음 포스팅에서는 Morphology (모폴로지)에 대해 이번 포스팅 처럼 OpenCV 함수를 이용해서 다뤄 보도록 하겠습니다.
참고 자료
[Post #30] 블러링: [WPF OpenCV Project #30] – 노이즈 제거 (Canny의 전처리 과정)
Canny: OpenCV Docs – Canny – 캐니 엣지 상세 파라미터
Sobel: OpenCV Docs – Sobel – 소벨 미분 함수
Edge Detection: Wikipedia – Edge detection – 엣지 검출의 수학적 원리