Convolution과 kernel 행렬로 Filter (Blur, Sharpen, Edge) 효과를 이미지에 적용하는 것에 대해 이번 포스팅에서 정리하고, WPF OpenCV 프로젝트에 구현하도록 하겠습니다.
이전 포스팅(#28)을 끝으로, Geometrix Transform(기하학적 변환)에 대해 마무리하고, 이번 포스팅 부터는 영상 처리에서 필터(Filter)에 대해 다루도록 할게요. 필터(Filter)라고 하면 정수기 내부에 있는 필터처럼 거름 망이나, 여과기 또는 공기청청기의 필터를 떠올릴 수 있습니다. 영상 처리에서도 굳이 표현 하자면, 대동 소이 하다 생각하는데요. 이미지의 픽셀 입력 값에서 원하지 않는 값은 걸러내고 원하는 값만 골라 내는 의미로 쓰입니다. 영상 처리에서 필터(filter)는 영상을 흐릿하게 만들거나, 또렷하게 만들어서 영상의 품질을 높이기도 하지만, Edge(엣지) 로 표현 하는 경계를 검출하고Edge의 방향을 알아 내어 대상물의 인식과 분리의 기본이 되는 정보를 뽑아 내기도 합니다. 영상 처리 라는 용어를 굳이 정의 하자면, 음~ 새로운 영상을 얻기 위해 기존 픽셀 값에 어떤 연산을 취해서 새로운 픽셀 값을 얻는 작업 쯤 되지 않을까요? 여기서 새로운 픽셀 값을 뽑아낼 때 기존 픽셀 값 하나가 아닌 그 픽셀과 주변 픽셀들의 값을 활용하는 방법을 공간 영역 필터 (Spacial Domain filter) 라고 하구요, 픽셀 값들의 차이를 주파수(Frequency)로 변환해서 활용하는 방법을 주파수 영역 필터(Frequency domain filter)라고 한답니다. WPF OpenCV 프로젝트에는 영상 처리에서 꼭 필요한 공간 영역 필터에 대해 다뤄 보도록 하죠.
Theory
Convolution (컨볼루션)
앞서 공간 영역 필터에 대해 언급을 했었습니다. 조금 더 들어 가보죠. 공간 영역 필터는 연산 대상이 되는 픽셀과 그 주변의 픽셀 값을 활용하게 되는데, 주변 픽셀들 중에서 어디 까지를 포함할 것인지, 그리고 결과 값을 어떻게 산출할 것인지를 결정해야 합니다. 이것을 영상 처리에서 Kernel(커널) 이라고 합니다. n x n 크기의 kernel(커널)은 Window(윈도우), filter(필터), Mask(마스크) 라고도 부릅니다. n x n 크기의 행렬로 구성된 kernel을 이용해서 입력 이미지의 각각의 픽셀을 곱하고, 그것을 모두 더한 값 하나를 결과 픽셀 값으로 결정하는데요. 이런 방법으로 입력 이미지의 마지막 픽셀 까지 반복하는 것을 Convolution (컨볼루션) 이라고 합니다. 글로만 적으니 이해가 잘 안될 수도 있으니 아래의 그림을 참고해 주세요.

위 이미지와 관련하여 수학적으로 표현 하면 아래와 같이 정리가 됩니다. 복잡해 보일 수 있는데 자세히 보면, 도장을 가지고 꾹꾹 찍으면서 옆으로 이동하는거라 생각하면 쉬울 겁니다.
Kernel Matrix : Convolution 연산
위 그림 처럼 3 x 3 크기의 Kernel로 Convolution 연산을 하는 것으로 하나의 픽셀 값이 결정되면 한 칸 이동해서 같은 연산을 반복하는 방법으로 마지막 픽셀 까지 적용하게 되는 거죠. 이렇게 적용하고 나면 커널에 지정한 값의 비중에 따라 주변 요소들의 값이 새롭게 반영된 이미지를 만들어 낼수 있는 겁니다. 딱 봐도 알겟지만, Kernel 의 크기와 값을 어떻게 하느냐에 따라 이미지에 필터를 적용한 효과가 달라지겠죠? 예를 들면 주변 요소 값들의 평균 값을 반영하면 전체적인 영상은 흐릿해지고 주변 요소 값들과의 차이를 반영하면 이미지가 또렸해지겠네요!
OpenCV Function
OpenCV에서는 위에서 본 설명 이미지 처럼 kernel을 사용하여 Convolution 연산을 쉽게 적용할 수 있도록 아래와 같은 함수를 제공하고 있습니다. 사용자가 원하는 Kernel 행렬을 직접 정의해서 Blur(흐림) 또는 또렸함(Sharpen) 효과를 적용할 수 있구요, 경계선 검출(Edge Detection) 효과도 낼 수 있도록 말이죠. 간략히 정리하면Cv2.Filter2D 는 사용자가 만든 kernel 행렬로 이미지를 마구 마구 주무르는 함수라고 알아두세요.
Cv2.Filter2D(
InputArray src, // 입력 이미지
OutputArray dst, // 결과 이미지 (여기에 저장됨)
MatType ddepth, // 결과 데이터의 깊이 (자료형)
InputArray kernel, // 3×3 등의 커널 행렬
Point? anchor = null, // 커널의 중심점 (생략 가능)
double delta = 0, // 추가로 더할 값 (생략 가능)
BorderTypes borderType = BorderTypes.Default // 테두리 처리 (생략 가능)
);
함수의 파라미터들 중에서 몇 가지만 살펴 보도록 하죠.
먼저 ddepth 는 결과 데이터의 깊이라고 주석을 달아 두었습니다. 이것은 결과 이미지 픽셀 하나가 가질 수 있는 숫자의 범위(데이터 타입)을 정해야 하는 것으로, Convolution 연산을 하다 보면 값이 255을 넘거나, 마이너스 값이 나올 수 있습니다. 일반적인 이미지(CV_8U)는 0~255까지만 저장 가능해서, 음수가 나오면 0으로, 255가 넘으면 이상한 값으로 잘릴 수 있습니다.
ddepth 에 -1 또는 MatType.CV_8U 을 주면, 입력 이미지와 동일한 자료형으로 만들어 달라고 하는 거죠. 보통 blur 효과나 Sharpen 효과를 줄 때 사용합니다.
ddepth 에 MatType.CV_16S 또는 CV_32F: 음수나 아주 큰 값도 저장할 수 있게 넉넉한 데이터 타입을 쓰게 해서 경계선 검출처럼 음수 값이 중요할 때 사용 하게 됩니다.
kernel 은 앞서 언급했던 것 처럼 3×3(또는 5×5) 행렬입니다. 이 행렬의 값에 따라 이미지가 흐려지기도 하고 선명해지기도 합니다. 사용자가 float 배열로 행렬을 만들어 전달합니다.
anchor 는 커널의 기준점으로 기본 값은 (-1, -1) 을 사용하며, 커널의 정중앙을 의미 합니다. 덧붙이자면, 3×3 행렬 중 어디를 기준으로 계산할지를 의미하는 것입니다. 별다른 이유가 없다면 기본 값 (정중앙)을 사용하는게 정신 건강에 좋습니다.
delta 는 연산 완료 후 모든 픽셀에 더 해주는 값으로, 결과 이미지가 너무 어두우면 밝게 올릴 때 사용하기도 합니다.
bordertype 은 이미지의 가장자리(테두리)의 픽셀은 kernel을 적용할 때 옆에 픽셀이 없어서 계산을 못하게되겠죠? 이때 가상의 픽셀을 어떻게 채울지 정하는 옵션으로 정신 건강상 기본 값을 사용하면 됩니다.
중요한것 한가지만 더 이야기 하고 WPF OpenCV 프로젝트에 구현하도록 하겠습니다.
사용자가 직접 kernel 행렬을 만들어서 filter 효과(Blur, Sharpen, Edge) 처리할 때 중요한 규칙으로, “kernel 내부 값의 합계(sum)” 이 매우 중요합니다.
Sum = 1 : 이미지 전체 밝기가 유지되므로, Blur와 Sharpen 효과에 적합.
Sum = 0 : 이미지의 배경이 검정색이 되어 Edge 검출에 적합.
구현 요약
이제 WPF OpenCV 프로젝트에 구현해야 하는 내용을 요약 정리하고 구현을 진행하겠습니다.
AlgorithmParameters.cs 파일에 ManualFilterParams 클래스를 생성합니다. 그리고 ManualFilter 에 사용될 필터들을 Enum 으로 추가하고, filter의 Kernel 선택 시 내부 데이터를 자동으로 변경하도록 클래스에 함수도 추가하도록 하죠.
UI(View)에서 ManualFilterParams 의 Property 속성을 UI에 연결하고, 사용되는 UI Element 에대한 DataTemplate을 구현합니다.
MainViewModel 에 Manual Filter 항목을 추가하고, ManualFilterParams 의 객체 생성 부분을 추가합니다.
OpenCVService 에 Manual Filter 선택 시 Cv2.Filter2D를 수행하는 로직 추가
Step 1: ManualFilterParams (Model)
AlgorithmParameters.cs 파일에 ManualFilterParams 클래스를 생성합니다. Cv2.Filter2D() 함수의 일부 파라미터들을 Property 변수로 멤버 변수를 만듭니다. 그리고, Filter 효과 Blur, Edge, Sharpen 을 Enum 으로 만들어 코드를 추가했습니다. Filter 선택에 따른 kernel 행렬을 자동으로 연결하기 위한 UpdateKernelValues() 라는 멤버 함수도 아래와 같이 추가하도록 하죠. 아! 그리고 기존에는 AlgorithmParameters 내에 생성되는 클래스에 생성자를 굳이 만들지 않고, 기본 생성자를 사용하도록 하였었는데요. ManualFilterParams 클래스에서는 생성자를 따로 만들어 파라미터에 기본 값을 넣을 수 있도록 UpdateKernelValues() 함수를 호출하도록 하였습니다. 참고하세요.
// Manual Filter parameter (Filter2D)
public enum FilterKernelType
{
Blur,
Edge,
Sharpen,
}
public class ManualFilterParams : AlgorithmParameters
{
private FilterKernelType _selectedKernelType = FilterKernelType.Blur;
public FilterKernelType SelectedKernelType
{
get => _selectedKernelType;
set
{
if (_selectedKernelType == value) return;
_selectedKernelType = value;
UpdateKernelValue();
OnPropertyChanged();
}
}
private string _kernelInfo = "";
public string KernelInfo
{
get => _kernelInfo;
private set
{
if (_kernelInfo == value) return;
_kernelInfo = value;
OnPropertyChanged();
}
}
// 실제 연산에 사용할 커널 데이터 배열
public float[] KernelData { get; private set; }
private string _ddepthInfo = "CV_8U";
public string DDepthInfo
{
get => _ddepthInfo;
private set
{
if (_ddepthInfo == value) return;
_ddepthInfo = value;
OnPropertyChanged();
}
}
private int _anchorX = -1;
public int AnchorX
{
get => _anchorX;
set
{
if (_anchorX == value) return;
_anchorX = value;
OnPropertyChanged();
}
}
private int _anchorY = -1;
public int AnchorY
{
get => _anchorY;
set
{
if (_anchorY == value) return;
_anchorY = value;
OnPropertyChanged();
}
}
private double _delta = 0.0;
public double Delta
{
get => _delta;
set
{
if (_delta == value) return;
_delta = value;
OnPropertyChanged();
}
}
private BorderTypes _borderType = BorderTypes.Default;
public BorderTypes BorderType
{
get => _borderType;
set
{
if (_borderType == value) return;
_borderType = value;
OnPropertyChanged();
}
}
public List<FilterKernelType> KernelTypeSource { get; } = Enum.GetValues(typeof(FilterKernelType)).Cast<FilterKernelType>().ToList();
public List<BorderTypes> BorderTypeSource { get; } = Enum.GetValues(typeof(BorderTypes)).Cast<BorderTypes>().ToList();
public ManualFilterParams()
{
UpdateKernelValue();
}
private void UpdateKernelValue()
{
switch (_selectedKernelType)
{
case FilterKernelType.Blur:
// 3x3 Average Blur (Sum=1)
KernelData = new float[] {
1/9f, 1/9f, 1/9f,
1/9f, 1/9f, 1/9f,
1/9f, 1/9f, 1/9f
};
KernelInfo = "[[1/9, 1/9, 1/9]\n [1/9, 1/9, 1/9]\n [1/9, 1/9, 1/9]]";
DDepthInfo = "CV_8U (-1)";
break;
case FilterKernelType.Edge:
// Laplacian-like Edge (Sum=0)
KernelData = new float[] {
0, -1, 0,
-1, 4, -1,
0, -1, 0
};
KernelInfo = "[[ 0, -1, 0]\n [-1, 4, -1]\n [ 0, -1, 0]]";
DDepthInfo = "CV_16S"; // 음수 표현 필요
break;
case FilterKernelType.Sharpen:
// Sharpening (Sum=1)
KernelData = new float[] {
-1, -1, -1,
-1, 9, -1,
-1, -1, -1
};
KernelInfo = "[[-1, -1, -1]\n [-1, 9, -1]\n [-1, -1, -1]]";
DDepthInfo = "CV_8U (-1)";
break;
}
}
}
위 코드에서 KernelInfo, DDepthInfo 변수의 Set 앞에 private 으로 지정한 이유를 알고 있나요? 사실 코드에 모든 것을 설명하지 못하기에 기존과 달라진 점을 이야기 할까 말까 하다가 그냥 몇 자 더 적기로 했습니다. private을 명시적으로 지정한 이유는 사용자가 다른 클래스의 프로퍼티 변수는 변경을 하여 지정할 수 있지만, KernelInfo와 DDepthInfo 는 고정된 데이터를 사용해서 사용자가 변경할 수 없도록 하기 위해서 입니다. public 으로 지정하던가, private을 삭제하여도 사실 상관은 없습니다. UI(XAML) 부분에 아래와 같이 막아 두었기에 상관은 없습니다. 왜 그렇게 했는지 알아는 두세요 ^^;
<TextBox Text=”{Binding KernelInfo, Mode=OneWay}” IsReadOnly=”True” … />
Step 2:
MainWindow.xaml 에서 ManualFilterParams 의 Property 속성을 UI에 연결하고, 사용되는UI Element(ComboBox, Slider, TextBox, …) 에 대한 DataTemplate을 아래와 같이 구현합니다.
<!--ManualFilterParams 템플릿 -->
<DataTemplate DataType="{x:Type local:ManualFilterParams}">
<StackPanel>
<TextBlock Text="Manual Filter Settings" Margin="0,0,0,5" FontWeight="Bold"/>
<TextBlock Text="Kernel Preset" Margin="0,5,0,0"/>
<ComboBox ItemsSource="{Binding KernelTypeSource}"
SelectedItem="{Binding SelectedKernelType}"
Margin="0,2,0,0" Height="25"/>
<TextBlock Text="Kernel Matrix (3x3)" Margin="0,10,0,0"/>
<TextBox Text="{Binding KernelInfo, Mode=OneWay}"
IsReadOnly="True"
Background="#EEEEEE"
Padding="3" Margin="0,2,0,0"
AcceptsReturn="True"
FontFamily="Consolas"/>
<TextBlock Text="Result Depth (Auto)" Margin="0,10,0,0"/>
<TextBox Text="{Binding DDepthInfo, Mode=OneWay}"
IsReadOnly="True"
Background="#EEEEEE"
Padding="3" Margin="0,2,0,0"/>
<Separator Margin="0,10,0,5"/>
<TextBlock Text="Anchor Point (X, Y)" Margin="0,5,0,0"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Margin="0,0,5,0">
<TextBlock Text="X (-1=Center)" FontSize="10" Foreground="Gray"/>
<TextBox Text="{Binding AnchorX}" TextAlignment="Center"/>
</StackPanel>
<StackPanel Grid.Column="1">
<TextBlock Text="Y (-1=Center)" FontSize="10" Foreground="Gray"/>
<TextBox Text="{Binding AnchorY}" TextAlignment="Center"/>
</StackPanel>
</Grid>
<TextBlock Text="Delta (Add Value)" Margin="0,5,0,0"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions>
<Slider Grid.Column="0" Minimum="-255" Maximum="255" Value="{Binding Delta}" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Text="{Binding Delta}" Margin="5,0,0,0" TextAlignment="Center" />
</Grid>
<TextBlock Text="Border Type" Margin="0,5,0,0"/>
<ComboBox ItemsSource="{Binding BorderTypeSource}"
SelectedItem="{Binding BorderType}"
Margin="0,2,0,0" Height="25"/>
</StackPanel>
</DataTemplate>
Step 3: MainViewModel (ViewModel)
MainViewModel.cs 파일의 MainViewModel() 생성자 함수내에 Manual Filter 항목을 추가하고, 사용자가 Manual Filter를 선택하였을 때, CreateParametersForAlgorithm() 함수에서 ManualFilterParams 의 객체 생성하여 연결 할수 있도록 아래와 같이 코드를 추가합니다.
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",
};
}
private void CreateParametersForAlgorithm(string? algoName)
{
// 선택된 이름에 따라 적절한 설정 클래스 생성
switch (algoName)
{
// .......... 기존 코드 유지 ..............
case "Manual Filter":
CurrentParameters = new ManualFilterParams();
break;
default:
CurrentParameters = null; // 설정이 필요 없는 경우
break;
}
}
Step 4: OpenCVService (model)
OpenCVService.cs 파일의 ProcessImageAsync() 함수에 Manual Filter 선택 시 Cv2.Filter2D() 함수를 사용해서 영상 처리하는 코드를 아래와 같이 추가합니다.
public async Task<string> ProcessImageAsync(string algorithm, AlgorithmParameters? parameters)
{
if (_srcImage == null || _srcImage.IsDisposed) return "Non Image";
string resultMessage = "Processing Complete";
bool updateDisplayImage = true;
await Task.Run(() =>
{
if (algorithm != "Histogram")
{
if (_destImage != null) _destImage.Dispose();
_destImage = _srcImage.Clone();
}
switch (algorithm)
{
// ............. 기존 코드 유지 ................
case "Manual Filter":
if (parameters is ManualFilterParams filterParams)
{
// kernel 행렬 생성 : Mat 클래스의 생성자가 public이 아니라서 외부에서 직접 호출 할수 없어 using Marshal로 데이터 복사.
// 먼저 Mat 객체 (kernel)를 생성한 후, Marshal.Copy를 사용하여 float 배열 데이터를 Mat의 Data 포인터로 복사합니다.
using (Mat kernel = new Mat(3, 3, MatType.CV_32F))
{
Marshal.Copy(filterParams.KernelData, 0, kernel.Data, filterParams.KernelData.Length);
OpenCvSharp.Point anchor = new OpenCvSharp.Point(filterParams.AnchorX, filterParams.AnchorY);
// ddepth 결정 (Edge 검출 시 음수 표현을 위해 16S 사용)
MatType ddepth = -1; // 기본값
if(filterParams.SelectedKernelType == FilterKernelType.Edge)
ddepth = MatType.CV_16S;
using (Mat tempDst = new Mat())
{
Cv2.Filter2D(_srcImage, tempDst, ddepth, kernel, anchor, filterParams.Delta, filterParams.BorderType);
// 결과 처리 (음수 값 처리)
if(ddepth == MatType.CV_16S)
Cv2.ConvertScaleAbs(tempDst, _destImage);
else
tempDst.CopyTo(_destImage);
}
}
resultMessage += $": {filterParams.SelectedKernelType} Filter";
}
break;
}
// ........... 기존 코드 유지 ........................
}
위 코드에서 어려운 부분은 없어 보이죠. 그런데 이전 포스팅에서도 가끔 using 문을 사용했었습니다. 별다르게 설명을 하지 않았죠. 그리고, Marshal.Copy 라는 함수도 사용했는데요. 지금 언급한 두부분에 대해 자세히 알고 있다면 건너뛰어 주세요. 이전 포스팅에서도 이것을 설명을 덧붙여야 하나? 하고 고민을 했었거든요. 그래서 생각난 김에 한번 언급하고 가려고 합니다.
위 코드에서 이 부분입니다.
using (Mat kernel = new Mat(3, 3, MatType.CV_32F))
{
Marshal.Copy(filterParams.KernelData, 0, kernel.Data, filterParams.KernelData.Length);
OpenCvSharp.Point anchor = new OpenCvSharp.Point(filterParams.AnchorX, filterParams.AnchorY);
…..
먼저 using () {} 이렇게 사용했는데요. using문은 C#에서 자원 관리를 위해 사용됩니다. C++, C의 경우 메모리를 할당하고 해제하는 것이 아주 당연하고 빈번하게 사용되어 집니다. 하지만 C#의 경우에는 가비지컬렉션이 알아서 자알~ 정리해 줍니다. 다만 OpenCVSharp 에서 Mat 같은 클래스는 Native Memory (C++ 영역)를 사용하기 때문에 메모리 누수를 방지 하기 위해서 사용이 끝난 후 반드시 해제(Dispose) 해 주어야 합니다. 이렇게 using (Mat kernel = new Mat(…)){} 이렇게 하면 블록이 끝나는 순간 해당 객체 즉 kernel 객체의 Dispose() 메서드를 자동으로 호출해서 메모리를 정리해 줍니다. Out of memory 와 같이 메모리에 사용하지 않은 이미지 데이터가 계속 쌓여 프로그램이 멈추거나 느려지는 것을 막아주죠. 위 코드상에 한 군데 더 있습니다.
using (Mat tempDst = new Mat())
{
Cv2.Filter2D(_srcImage, tempDst, ddepth, kernel, anchor, filterParams.Delta, filterParams.BorderType);
….
여기서도 사용했네요. 즉 srcImage 또는 dstImage 같은 Mat 클래스 객체는 이미 설계 단계에서 Mat 클래스 객체를 항상 사용한다는 것을 알고 있으니까 WPF OpenCV 프로젝트 프로그램이 실행을 멈추거나 닫힐때 Dispose() 함수를 호출하게 해 두었습니다. 하지만, 영상 처리를 하면서 Mat 클래스 객체를 임시로 추가 할때에는 번거롭게 생성하고, 다 쓴 후 Dispose() 하는 것이 많이 귀찮습니다. 그래서 Using 문을 사용한 겁니다. 아마 영상 처리 알고리즘을 개발할 때 아주 많이 사용하게 될 겁니다. ^^
언급 했던 두 가지 중 나머지인 Marshal.Copy() 에 대해 정리하고 정말 마무리 하겠습니다. Marshal 클래스와 Marshal.Copy 메서드는 C#과 같은 .NET 언어에서 메모리를 직접 다룰 때 사용하는 매우 중요한 도구입니다. 비유를 들자면, C# 은 자원 관리를 잘 해주는 언어입니다. 가비지 컬렉터(GC)가 있는 Managed 개발 언어인데요. 하지만 C++은 Unmanaged 언어 입니다. OpenCvSharp은 C++로 만들어져서 메모리를 직접 관리해야 합니다. 그래서 그 둘 사이는 서로 메모리를 사용하는 방식이 다릅니다. C#의 배열을 OpenCV함수에 그냥 던져 주거나, 반대로 가져오기가 쉽지 않은 거죠. 그래서 이 둘사이에 데이터를 주고 받을 때에는 무언가 필요하게 되는 것입니다. System.Runtime.InteropServices.Marshal 클래스는 이 둘 사이에서 데이터를 변환하거나 메모리를 직접 복사해 주는 역할을 합니다. 코드의 내용을 가지고 조금 덧붙이겠습니다.
filterParams.KernelData 는 C#에서 만든 float[] 배열이죠. 그리고, kernel 객체는 OpenCvSharp 의 Mat 객체입니다. 이 객체의 실제 데이터는 C++ 영역에 있기 때문에 kernel = kernelData 와 같이 직접적으로 연결될 수가 없는 겁니다. 이 문제를 처리하기 using (Mat kernel = new Mat(3, 3, MatType.CV_32F))와 같이 비어 있는 kernel 객체를 만들었고, 이 Mat 객체(kernel)의 메모리 공간 (kernel.Data 라는 주소)에 사용자가 선택하여 가지고 있던 숫자들 (kernelData)를 채워 넣는 거죠. 그래서 Marshal.Copy() 함수는 C# 배열의 데이터를 들어서 kernel.Data 가 가르키는 메모리 주소에 통째로 복사해 주는 역활을 합니다. 사실 다른 방법도 있긴 해요. 반복문을 돌면서 kernel.Set<float>(y, x, value) 와 같이 하나 하나 값을 넣을 수도 있지만, Marshal.Copy() 는 메모리 블록을 한번에 복사 하기 때문에 속도가 겁나~ 빠르고 효율적입니다.
요약 하면, C# 배열에 있는 데이터를 OpenCV 객체의 실제 메모리 주소로 직접 전송하기 위해 사용 한다! 라고 알고 있으면 될것 같네요.
후~~~ 좀 길었는데, 한번은 얘기하고 싶어 이렇게 정리합니다. 이제 빌드 하고 결과를 한번 보도록 하죠.
실행 및 영상 처리 확인
이제 빌드 후 이미지를 불러와 Filter 처리를 해보도록 하겠습니다.
먼저 이미지를 불러와 보도록 하죠.

이제 불러온 이미지를 Blur (흐릿하게) 필터를 써서 이미지에 적용해 보도록 하겠습니다. 필터 선택만 Blur로 하고, 나머지 파라미터들은 기본 값으로 적용하였습니다. 이미지가 흐려진것이 보이나요?

다음은 원본 이미지에 Sharpen 필터 행렬을 이미지에 적용해 보도록 하겠습니다. 위와 마찬가지로 나머지 파라미터들은 기본값을 그대로 유지 하였습니다. 원본 이미지에 비해 더 선명해 보이나요?

마지막으로 원본 이미지에 Edge 필터 행렬을 이미지에 적용해 보도록 하죠.

처음에 정리하면서 중요하다 언급하였던 kernel 행렬 값(sum)을 알려줬던 기준에 맞게 변경해 가면서 필터 검증을 해보면 테스트 하려는 이미지에 적합한 kernel 행렬을 찾을 수 있을 겁니다.
이번 포스팅에서는 사용자가 직접 kernel 행렬을 사용하여 filter (Blur, Sharpen, Edge) 처리 효과를 이미지에 적용하였는데요, 다음 포스팅에서는 OpenCvSharp 에서 제공하는 함수를 이용해서 평균 블러 효과를 적용하고 구현해 보도록 하겠습니다.
참고 자료
[Post #28] 캘리브레이션: [WPF OpenCV Project #28] – 왜곡 보정 (Geometric Transform의 끝)
Filter2D: OpenCV Docs – filter2D – 컨볼루션 연산 함수.
Convolution: Wikipedia – Kernel (image processing) – 다양한 커널 종류와 효과