WPF OpenCV 프로젝트 #30: Filters (Average Blur, Box Filter, Gaussian Blur, Median Blur, Bilateral Filter)

Average Blur, Box Filter, Gaussian Blur, Median Blur, Bilateral Filter 들을 이번 포스팅을 통해OpenCvSharp에서 제공하는 함수들을 이용해서 한꺼번에 순차적으로 간단히 개념을 정리하고, WPF OpenCV 프로젝트에 구현해 보겠습니다.
이전 포스팅(#29)에서 Filter (Blur, Sharpen, Edge) 효과를 Kernel Matrix (커널 행렬: 3 x 3)을 사용자가 직접 만들어서 Cv2.filter2D() 함수를 이용해서 처리하는 방법을 나름 깊이 있게 다뤘습니다. 사용자가 적용하려는 filter 효과를 위해 작성하는 kernel Matrix (커널 행렬)를 어떻게 만들어야 하는 지에 대해서 제가 중요한 부분을 짚어 줬던 것 기억하나요? 기억이 잘 나지 않는다면 꼭 다시 돌아가 슬쩍 읽어 보고 오도록 하세요. 사실 지난 포스팅에서 filter 효과를 이미지에 적용하기 위해 사용한 kernel matrix (커널 행렬)은 3×3 행렬로 크기는 고정해서 적용했었습니다. 이유를 꼭 밝혀야 한다면, 제가 일일이 만들기 귀찮아서 였습니다. 커널 행렬을 크기 마다 만들어서 이미지에 적용하고, 이미지 캡쳐 떠서 포스팅 자료를 만들기가 여간 귀찮은게 아니더라구요. (죄송합니다. ㅠ.ㅠ)
사실 아주 섬세하게 효과를 줘야 하는 경우나, 특별한 경우가 아니면 지난 포스팅에서 사용한 Manual Filter 방식으로 구현을 하지 않기 때문입니다. OpenCV에서 제공하는 함수를 쓰면 kernel matrix (커널 행렬)도 원하는 크기로 만들 수 있고, 알아서 원하는 적당한 filter 효과를 적용할 수 있으니까요. 그럼 이번 포스팅에서 다루려는 Average Blur, BoxFilter, Gaussian Blur, Median Blur, Bilateral Filter 에 대해 알아보고, OpenCV에서 제공하고 함수들에 대해 파라미터와 사용방법을 간략히 정리해 보도록 하겠습니다.

Theory

먼저 이전 포스팅에서 다뤘던 부분과 중복 되는 부분이 있긴 한데, 이것도 조금 정리 하겠습니다.

Filtering & Convolution

영상 처리에서 ‘블러링(Blurring)’ 또는 ‘스무딩(Smoothing)’은 이미지를 흐릿하게 만들어 노이즈를 줄이거나 날카로운 부분을 부드럽게 만드는 작업이라고 지난 포스팅에 언급했었습니다. 대부분의 필터는 컨볼루션(Convolution, 합성곱) 연산을 통해 이루어집니다. 그 중에서 두 가지만 정리하겠습니다. 자세한 건 지난 포스팅을 참고해 주세요.
Kernel (or mask) : 픽셀 주변의 값을 어떻게 섞을지 결정하는 작은 행렬 (ex: 3×3, 5×5, …)입니다.
Anchor : kernel 의 중심점으로, 이 점이 현재 계산하고 있는 픽셀 위치가 됩니다.

Average Blur

이름에서도 쉽게 알 수 있듯이 가장 직관적이고 단순한 필터 입니다. 특정 픽셀의 값을 주변 픽셀들의 평균값으로 대체하는 거죠. 수학적으로는 아래의 수식 처럼 모든 요소가 1/N (N은 커널의 크기)인 커널을 이미지와 Convolution (컨볼루션)하는 것입니다.

K=19[111111111]K=\frac{1}{9}\begin{bmatrix}1&1&1\\1&1&1\\1&1&1\end{bmatrix}

모든 픽셀의 가중치가 같으니까, 이미지가 전체적으로 평평해지고(Flatten) 부드러워지는 효과를 볼 수 있죠. OpenCV에서는 Cv2.Blur() 라는 함수를 제공하는데요. 이 함수를 한번 살펴보죠.

Cv2.Blur(src, dst, ksize)
src: 입력 이미지
dst: 출력 이미지
ksize (kernel size): 커널 행렬의 크기(가로, 세로) 입니다. 프로젝트에 구현할 kernelSize 속성과 연결됩니다. 커널 행렬의 값이 클수록 이미지가 더 많이 뭉개지고 흐려 집니다. (일반적으로 3, 5, 7, … 처럼 홀수를 사용합니다.)
함수가 단순한 만큼 계산이 매우 빠름, 빠름 하고, 단순합니다. 근데 이 함수를 이미지에 적용하면, 이미지의 edge(경계선) 까지 뭉개버려서 이미지가 또렷하지가 않아지죠. 그냥 단순히 이미지를 부드럽게 만들고 싶거나, 그럴일은 없겠지만 시스템 리소스가 부족하고 빠른 연산이 필요할 때 사용하세요.

Box Filter

Average Blur일반화된 형태인데, 기본적으로 Average Blur와 거의 동일합니다. 다만 합계를 구한 뒤에 평균을 내지 않거나 (Normalize = false), 결과 데이터의 깊이 (Depth)를 조절할 수 있는 옵션을 제공합니다. 아래는 OpenCV에서는 Cv2.BoxFilter() 라는 함수를 제공하는데, 이 함수를 한번 살펴보죠.

Cv2.BoxFilter(src, dst, ddepth, ksize, anchor, normalize)
depth: 출력 이미지의 데이터 타입을 지정하는 것으로 -1이면 입력과 동일합니다.
normalize:
true: 픽셀합을 커널 행렬의 크기로 나눠 버립니다. 이렇게 하면 Average Blur 과 동일하게 됩니다.
false: 나누지 않고 합만 구합니다. 다만 값이 엄청 커질 수 있겠죠. 이렇게 설정하면 적분 영상(Integral Image)를 계산하거나, 주변 픽셀의 밀도를 구할 때 사용하기도 합니다.

Gaussian Blur

Average Blur가 모든 픽셀을 공평하게 섞어 버린 다면, Gaussian Blur“가까운 픽셀은 많이, 먼 픽셀은 적게” 반영합니다. 중심에서 멀어질수록 가중치가 종 모양(Bell Curve)으로 줄어드는 정규 분포(Gaussian Distribution) 확률 함수를 사용합니다. Gaussian 에 대한 수식은 아래와 같은데요. 그냥 그렇구나 하구 쓰윽~ 지나가면 됩니다.

G(x)=12πσ2ex22σ2G(x)=\frac{1}{\sqrt{2\pi \sigma ^2}}e^{-\frac{x^2}{2\sigma ^2}}

δ(Sigma): 표준 편차. 종 모양이 얼마나 옆으로 퍼지는지를 결정하게 되구요. 값이 클수록 더 넓은 영역의 픽셀을 참조하게 되니까 더 흐려지게 됩니다. OpenCV에서는 Cv2.GaussianBlur() 라는 함수를 제공하는데, 이 함수를 한번 살펴보죠.

Cv2.GaussianBlur(src, dst, ksize, sigmaX, sigmaY)
ksize: kernal Matrix 크기로 반드시 홀수여야 합니다. 만약 0으로 설정하면 Sigma 값에 의해 자동 계산.
sigmaX: X축 방향의 표준편차 입니다.
sigmaY: Y축 방향의 표준편차인데, 0으로 설정되면 sigmaX와 같은 값으로 설정됩니다.

Guassian BlurAverage Blur 에 비해 이미지를 훨씬 자연스럽게 블러링하게 하죠. 다른 뜻이 아니라 edge (경계선) 부분이 Average Blur 에 비해 덜 뭉개집니다. 주로 Camera Sensor(카메라 센서) noise 제거에 아주 탁월하다 알려져 있습니다. 다른 포스팅에서 다루겟지만, Canny Edge Detection Edge 검출 전에 노이즈를 줄이기 위해 거의 표준적으로 사용하곤 한답니다.

Median Blur

Median Blur 라는 필터는 Convolution(컨볼루션)의 더하기 또는 곱하기를 하지 않습니다. 대신에 통계적 방법을 사용하는데요. 커널 영역 내의 픽셀 값들을 크기순으로 줄 세운(Sorting) 뒤, 정중앙에 있는 값(중앙값, Median)을 선택하도록 하는 거죠. 예를 들어 3×3 영역의 값이 아래와 같다고 가정하죠.

[10, 20, 20, 20, 100, 20, 20, 20, 20] (100은 튀는 노이즈)

이것을 오름 차순으로 정렬하면, [10, 20, 20, 20, 20, 20, 20, 20, 100] 되죠. 이 중에서 중앙 값 20을 선택 하게 되고 100이라는 노이즈를 완전히 제거해 버리는 겁니다.
OpenCV에서는 Cv2.MedianBlur() 라는 함수를 제공하는데, 이 함수를 한번 살펴보죠.

Cv2.MedianBlur(src, dst, ksize)
ksize : kernel matrix 크기로 반드시 1보다 큰 홀수를 사용해야 합니다.

Median Blur 는 edge(경계선)를 보존하는 능력이 만랩입니다. 흐려지지 않고 노이즈만 쏘옥~ 빼버립니다. 그래서 이미지에 흰 점이나 검은 점이 콕콕 박혀있는 이미지에서 그것들을 노이즈로 보고 제거하는데 최강 입니다. 보통 바코드 인식하는 프로그램을 개발한다면, 이미지 전처리에 사용되곤 한답니다.

Bilateral Filter

Bilateral Filter 의 이름에서 알 수 있듯이 양방향 거름 종이 인데요. 위 에서 언급한 필터들에 비해 가장 강력하지만 계산량이 겁나 많은 필터입니다. 기능을 한 줄로 요약하면 “엣지는 살리고, 평탄한 부분만 뭉갠다” 라고 할 수 있겠네요. 이렇게 하기 위해서 Bilateral Filter두 가지 가우시안 필터를 결합 하게 됩니다.

첫 번째는 Space Gaussian (공간 가우시안)으로, 거리가 가까운 픽셀에 가중치를 줍니다.
(일반적인 Gasussian Blur)
두 번째는 Color Gaussian (색상 가우시안)으로 픽셀 값(색상 및 밝기)의 차이가 적은 픽셀에만 가중치를 부여하게 합니다.
종합해보면, “거리가 가깝더라도 Edge(경계선) 처럼 색상 차이가 너무 크면 썩지 않는다“는 거죠.
OpenCV에서는 Cv2.BilateralFilter() 라는 함수를 제공하는데, 이 함수를 한번 살펴보죠.

Cv2.BilateralFilter(src, dst, d, sigmaColor, sigmaSpace)
d (Diameter): 필터링에 이용할 픽셀의 지름으로 보통 5 ~ 9 정도를 사용합니다.
sigmaColor: 색상 공간의 표준편차 값인데, 이 값이 크면 색상 차이가 큰 픽셀도 섞여서 엣지가 뭉개질 수 있습니다.
sigmaSpace: 좌표 공간의 표준편차 값으로 이 값이 크면 멀리 있는 픽셀도 서로 영향을 줍니다.

Bilateral Filter 는 Edge Preserving (경계선 보존) 성능이 가장 뛰어나다고 하더군요. 다만, 연산 속도는 느림~ 느림~ 입니다. 포토샵을 통해서 이미지의 뽀샤시~ 효과를 인물 사진에 많이 하곤 하죠? 사람의 피부 톤을 부드럽게 만들고, 눈/코/입 부분에 경계선을 또렷하게 남길 때 많이 사용하는 필터입니다.

구현 요약

위 에서 사용할 필터들에 대해 간략히 (?) 정리했으니까 이제 구현을 해야겠죠. 늘 그래왔던 것처럼 WPF OpenCV 프로젝트에 구현할 내용을 아래와 같이 요약하고 단계 별로 구현 해 보도록 하죠.
AlgorithmParameters.cs: AutoFilterParams 클래스와 AutoFilterType 열거형을 추가하여 필터별 파라미터를 정의합니다.
MainViewModel.cs: 알고리즘 목록에 “Auto Filter”를 추가하고, 선택 시 파라미터 객체를 생성하도록 연결하도록 하죠.
MainWindow.xaml: “Auto Filter” 선택 시 나타날 UI(콤보박스, 슬라이더 등)를 DataTemplate에 추가하고, 선택된 필터 종류에 따라 필요한 슬라이더만 보이도록 트리거를 설정합니다.
OpenCVService.cs: Cv2.Blur, Cv2.BoxFilter, Cv2.GaussianBlur, Cv2.MedianBlur, Cv2.BilateralFilter를 실제로 수행하는 로직을 구현합니다.

Step 1: AutoFilterParams (Model)

AlgorithmParameter.cs 파일에 먼저 AutoFilterType 열거형 데이터를 추가합니다. AutoFilterParams 클래스에는 적용할 Filter 함수들의 파라미터를 아래의 코드와 같이 정의해서 UI와 연결 되도록 하죠.

public enum AutoFilterType
{
    AverageBlur,        // Cv2.Blur
    BoxFilter,          // Cv2.BoxFilter
    GaussianBlur,       // Cv2.GaussianBlur
    MedianBlur,         // Cv2.MedianBlur
    BilateralFilter     // Cv2.BilateralFilter
}

public class AutoFilterParams : AlgorithmParameters
{
    private AutoFilterType _selectedFilterType = AutoFilterType.AverageBlur;
    public AutoFilterType SelectedFilterType
    {
        get => _selectedFilterType;
        set
        {
            if (_selectedFilterType == value) return;

            _selectedFilterType = value;
            OnPropertyChanged();
        }
    }

    // Kernel Size (for Blur, BoxFilter, GaussianBlur, MedianBlur)
    private int _kernelSize = 3;
    public int KernelSize
    {
        get => _kernelSize;
        set
        {
            if(value == _kernelSize) return;

            // 커널 크기는 홀수만 허용 (3,5,7,...)
            if (value % 2 == 0) value++;
            if (value < 1) value = 1;

            _kernelSize = value;
            OnPropertyChanged();
        }
    }

    // Gaussian Blur 전용.
    private double _sigmaX = 1.0;
    public double SigmaX
    {
        get => _sigmaX;
        set
        {
            if (_sigmaX == value) return;

            _sigmaX = value;
            OnPropertyChanged();
        }
    }

    private double _sigmaY = 0.0;
    public double SigmaY
    {
        get => _sigmaY;
        set
        {
            if (_sigmaY == value) return;

            _sigmaY = value;
            OnPropertyChanged();
        }
    }

    // Bilateral Filter 전용
    private int _diameter = 9; // 필터링에 사용될 이웃 픽셀의 지름 (음수면 sigmaSpace로 계산)
    public int Diameter
    {
        get => _diameter;
        set 
        {
            if (_diameter == value) return; 

            _diameter = value; 
            OnPropertyChanged(); 
        }
    }

    private double _sigmaColor = 75.0; // 색공간 표준편차 (클수록 색 차이가 큰 픽셀도 섞임)
    public double SigmaColor
    {
        get => _sigmaColor;
        set 
        {
            if (_sigmaColor == value) return; 
            
            _sigmaColor = value; 
            OnPropertyChanged();
        }
    }

    private double _sigmaSpace = 75.0; // 좌표공간 표준편차 (클수록 멀리 있는 픽셀도 영향을 줌)
    public double SigmaSpace
    {
        get => _sigmaSpace;
        set 
        {
            if (_sigmaSpace == value) return; 

            _sigmaSpace = value; 
            OnPropertyChanged();
        }
    }

    public List<AutoFilterType> FilterTypeSource { get; } = Enum.GetValues(typeof(AutoFilterType))
        .Cast<AutoFilterType>()
        .ToList();
}

Step 2: MainWindow.xaml (View)

MainWindow.xaml 파일에는 AutoFilterParamsProperty 속성을 UI에 연결하고, 사용자가 “Auto Filter” 선택 시 나타날 UI(콤보박스, 슬라이더 등)를 아래의 코드와 같이 DataTemplate에 추가해서, 선택된 필터 종류에 따라 필요한 슬라이더만 보이도록 트리거를 설정합니다.

<!--AutoFilterParams 템플릿-->
<DataTemplate DataType="{x:Type local:AutoFilterParams}">
    <StackPanel>
        <TextBlock Text="Auto Filter Settings" Margin="0,0,0,5" FontWeight="Bold"/>

        <!-- 필터 종류 선택 -->
        <TextBlock Text="Filter Algorithm" Margin="0,5,0,0"/>
        <ComboBox ItemsSource="{Binding FilterTypeSource}"
          SelectedItem="{Binding SelectedFilterType}"
          Margin="0,2,0,0" Height="25"/>

        <Separator Margin="0,10,0,10"/>

        <!-- 공통 Kernel Size (Bilateral 제외) -->
        <!-- DataTrigger를 사용하여 Bilateral일 때는 숨기기 -->
        <StackPanel>
            <StackPanel.Style>
                <Style TargetType="StackPanel">
                    <Setter Property="Visibility" Value="Visible"/>
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding SelectedFilterType}" Value="BilateralFilter">
                            <Setter Property="Visibility" Value="Collapsed"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </StackPanel.Style>

            <TextBlock Text="Kernel Size (ksize)" Margin="0,5,0,0"/>
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="50"/>
                </Grid.ColumnDefinitions>
                <!-- 커널 크기는 홀수여야 함. SmallChange=2로 설정 -->
                <Slider Grid.Column="0" Minimum="1" Maximum="31" Value="{Binding KernelSize}" 
                VerticalAlignment="Center" TickFrequency="2" IsSnapToTickEnabled="True" SmallChange="2" LargeChange="2"/>
                <TextBox Grid.Column="1" Text="{Binding KernelSize}" Margin="5,0,0,0" TextAlignment="Center" />
            </Grid>
            <TextBlock Text="* 홀수 값만 가능 (1, 3, 5...)" FontSize="10" Foreground="Gray"/>
        </StackPanel>

        <!-- Gaussian Blur 추가 옵션 -->
        <StackPanel>
            <StackPanel.Style>
                <Style TargetType="StackPanel">
                    <Setter Property="Visibility" Value="Collapsed"/>
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding SelectedFilterType}" Value="GaussianBlur">
                            <Setter Property="Visibility" Value="Visible"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </StackPanel.Style>

            <TextBlock Text="Sigma X" Margin="0,5,0,0"/>
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="50"/>
                </Grid.ColumnDefinitions>
                <Slider Grid.Column="0" Minimum="0.1" Maximum="10.0" Value="{Binding SigmaX}" VerticalAlignment="Center" />
                <TextBox Grid.Column="1" Text="{Binding SigmaX, StringFormat=F1}" Margin="5,0,0,0" TextAlignment="Center" />
            </Grid>

            <TextBlock Text="Sigma Y" Margin="0,5,0,0"/>
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="50"/>
                </Grid.ColumnDefinitions>
                <Slider Grid.Column="0" Minimum="0.0" Maximum="10.0" Value="{Binding SigmaY}" VerticalAlignment="Center" />
                <TextBox Grid.Column="1" Text="{Binding SigmaY, StringFormat=F1}" Margin="5,0,0,0" TextAlignment="Center" />
            </Grid>
            <TextBlock Text="* SigmaY = 0이면 SigmaX와 동일" FontSize="10" Foreground="Gray"/>
        </StackPanel>     
        <!-- Bilateral Filter 옵션 -->
        <StackPanel>
            <StackPanel.Style>
                <Style TargetType="StackPanel">
                    <Setter Property="Visibility" Value="Collapsed"/>
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding SelectedFilterType}" Value="BilateralFilter">
                            <Setter Property="Visibility" Value="Visible"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </StackPanel.Style>

            <TextBlock Text="Diameter (d)" Margin="0,5,0,0"/>
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="50"/>
                </Grid.ColumnDefinitions>
                <Slider Grid.Column="0" Minimum="1" Maximum="20" Value="{Binding Diameter}" VerticalAlignment="Center" />
                <TextBox Grid.Column="1" Text="{Binding Diameter}" Margin="5,0,0,0" TextAlignment="Center" />
            </Grid>
            <TextBlock Text="* 이웃 픽셀 지름" FontSize="10" Foreground="Gray"/>

            <TextBlock Text="Sigma Color" Margin="0,5,0,0"/>
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="50"/>
                </Grid.ColumnDefinitions>
                <Slider Grid.Column="0" Minimum="10" Maximum="150" Value="{Binding SigmaColor}" VerticalAlignment="Center" />
                <TextBox Grid.Column="1" Text="{Binding SigmaColor, StringFormat=F0}" Margin="5,0,0,0" TextAlignment="Center" />
            </Grid>
            <TextBlock Text="* 색상 공간 표준편차" FontSize="10" Foreground="Gray"/>

            <TextBlock Text="Sigma Space" Margin="0,5,0,0"/>
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="50"/>
                </Grid.ColumnDefinitions>
                <Slider Grid.Column="0" Minimum="10" Maximum="150" Value="{Binding SigmaSpace}" VerticalAlignment="Center" />
                <TextBox Grid.Column="1" Text="{Binding SigmaSpace, StringFormat=F0}" Margin="5,0,0,0" TextAlignment="Center" />
            </Grid>
            <TextBlock Text="* 좌표 공간 표준편차" FontSize="10" Foreground="Gray"/>
        </StackPanel>
    </StackPanel>
</DataTemplate>

Step 3: MainViewModel (ViewModel)

MainViewModel.cs 파일의 MainViewModel() 생성자 함수의 알고리즘 목록에 Auto Filter 를 추가하고, 사용자가 Auto Filter 를 선택했을 때, CreateParametersForAlgorithm() 함수에서 AutoFilterParams 객체의 파라미터 객체를 생성해 연결 하도록 아래 코드로 업데이트 합니다.

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",
    };
}
private void CreateParametersForAlgorithm(string? algoName)
{
    // 선택된 이름에 따라 적절한 설정 클래스 생성
    switch (algoName)
    {
        // .......... 기존 코드 유지 ............

        case "Auto Filter":
            CurrentParameters = new AutoFilterParams();
            break;

        default:
            CurrentParameters = null; // 설정이 필요 없는 경우
            break;
    }
}

Step 4: OpenCVService (model)

OpenCVService.cs 파일에서는 ProcessImageAsync() 함수에 Cv2.Blur, Cv2.BoxFilter, Cv2.GaussianBlur, Cv2.MedianBlur, Cv2.BilateralFilter 를 이용해서 영상 처리 하는 코드를 아래와 같이 추가하면 됩니다.

public async Task<string> ProcessImageAsync(string algorithm, AlgorithmParameters? parameters)
{
    // ........ 이전 코드 유지 ............
    await Task.Run(() => 
    {
        if (algorithm != "Histogram")
        {
           // ........ 이전 코드 유지 ............
        }

        switch (algorithm)
        {            
           // ......... 이전 코드 유지 .............
            case "Auto Filter":
                if (parameters is AutoFilterParams autoParams)
                {
                    int k = autoParams.KernelSize;

                    switch (autoParams.SelectedFilterType)
                    {
                        case AutoFilterType.AverageBlur:
                            // 가장 기본적인 평균 블러
                            Cv2.Blur(_srcImage, _destImage, new OpenCvSharp.Size(k, k));
                            resultMessage += $": Blur (Kernel: {k}x{k})";
                            break;

                        case AutoFilterType.BoxFilter:
                            // BoxFilter (normalized=true이면 Blur와 동일)
                            // ddepth = -1 (입력과 동일)
                            Cv2.BoxFilter(_srcImage, _destImage, -1, new OpenCvSharp.Size(k, k), normalize: true);
                            resultMessage += $": BoxFilter (Kernel: {k}x{k})";
                            break;

                        case AutoFilterType.GaussianBlur:
                            // Gaussian Blur
                            Cv2.GaussianBlur(_srcImage, _destImage, new OpenCvSharp.Size(k, k),
                                autoParams.SigmaX, autoParams.SigmaY);
                            resultMessage += $": Gaussian (Kernel: {k}x{k}, Sigma: {autoParams.SigmaX:F1}/{autoParams.SigmaY:F1})";
                            break;

                        case AutoFilterType.MedianBlur:
                            // Median Blur (소금후추 노이즈 제거에 탁월)
                            // ksize는 1보다 큰 홀수여야 함
                            if (k % 2 == 0) k++;
                            if (k < 1) k = 1;

                            Cv2.MedianBlur(_srcImage, _destImage, k);
                            resultMessage += $": Median (Kernel: {k})";
                            break;

                        case AutoFilterType.BilateralFilter:
                            // Bilateral Filter (엣지 보존 스무딩)
                            // 매우 느릴 수 있으므로 주의. srcImage가 Color여야 함.
                            Cv2.BilateralFilter(_srcImage, _destImage,
                                autoParams.Diameter,
                                autoParams.SigmaColor,
                                autoParams.SigmaSpace);
                            resultMessage += $": Bilateral (d:{autoParams.Diameter}, sColor:{autoParams.SigmaColor}, sSpace:{autoParams.SigmaSpace})";
                            break;
                    }
                }
                break;

        }
    });

    // ........ 이전 코드 유지 ............

    return resultMessage;
}

실행 및 영상 처리 확인

한꺼번에 5개의 Filter(Average Blur, BoxFilter, Gaussian Blur, Median Blur, Bilateral Filter) 효과에 대해 추가하였습니다. 추가된 내용에 비해 코드량은 그다지 많지 않았던것 같은데, 어떻게 느꼈는지 모르겠네요. 이번 포스팅 서두에 언급한 것 처럼 OpenCV 에서 제공하는 함수를 이용해서 관련 기능들을 구현했습니다. 지난 포스팅과 가장 큰 차이점은 kernel matrix (커널 행렬) 직접 코드로 작성하지 않았다는 것과, kernel matrix의 크기도 사용자가 설정할 수 있도록 했다는 거죠. 단순히 blur, edge, sharpen 효과 보다 더 심화된 필터 효과를 다룰 수 있도록 했습니다. 이제 빌드 후 실행 해서 이미지들의 필터링 효과를 알아보도록 하죠.

첫 번째는 원본 이미지와 Average Blur 적용 이미지 입니다. Average Blur 필터의 효과를 눈에 띄게 하기 위해 Kernel matrix 크기를 19로 설정하였습니다.

원본 이미지
Average Blur

두 번째는 Box Filter 효과를 적용한 결과 이미지 입니다. kernel matrix 크기는 19로 동일하게 설정하였습니다.

Box Filter 
kernel Matrix 19

세 번째는 Gaussian Blur 필터 효과를 적용한 이미지이며, kernel matrix 크기는 19로 동일하게 적용하였고, SigmaX 값을 4.7 로 설정하였습니다. (설정 값을 변경 하면서 테스트 해보세요.)

Gaussian Blur

네 번째는 Median Blur 필터 효과를 적용한 이미지이며, kernel matrix 크기는 19로 동일하게 적용 하였습니다.

Median Blur

다섯번째는 BilateralFilter 효과를 적용한 이미지 이며, Diameter는 15, Sigma Color 75, Sigma Space 75 로 설정 하였습니다. (설정 값을 변경하면서 테스트 해보세요.)

Bilateral Filter

실행 되는 이미지를 보긴 했는데, 알쏭달쏭 할까 싶어 다음과 같이 정리 합니다. 이미지를 가지고 테스트 하면서 아래의 요약 내용과 함께 영상 처리된 이미지를 감상해 보세요 ^^;
Average / Box: 이미지가 전체적으로 균일하게 뭉개집니다. 사각형 형태로 빛이 번지는 느낌(Artifact)이 날 수 있습니다.
Gaussian: 평균 블러보다 중심부가 더 선명하고 부드럽게 퍼집니다. 초점이 나간 듯한 자연스러운 느낌입니다. (그냥 흐리게 하고 싶다 -> Gaussian Blur)
Median: 이미지의 자잘한 점이나 잡티가 감쪽같이 사라집니다. 마치 수채화 그림처럼 변합니다.
(점(Salt & Pepper) 노이즈를 없애고 싶다 -> Median Blur)
Bilateral: 가장 신기한 녀석입니다. 얼굴 피부 같은 평탄한 곳은 뽀얗게 뭉개지는데, 눈코입의 윤곽선은 선명하게 남아있습니다. (윤곽선은 살리고 피부만 뽀얗게 하고 싶다 -> Bilateral Filter)

각각의 Filter 마다 사용자가 설정 할 수 있도록 만들었으니까, 설정 값을 변경해 가면서 Filter 효과를 경험해 보면 좋겠네요. 다음 포스팅에서는 Edge detection (경계 검출) 에 대해 이번 포스팅 처럼 OpenCV 함수를 이용해서 다뤄 보도록 하겠습니다.

참고 자료

[Post #29] 필터 기초: [WPF OpenCV Project #29] – Convolution과 Kernel의 원리
GaussianBlur: OpenCV Docs – 가우시안 블러 상세
BilateralFilter: OpenCV Docs – 양방향 필터 상세
Image Smoothing: OpenCV Python Tutorial – 다양한 블러링 기법 비교

댓글 남기기