C#でWPF学習中「OpenCVSharp – フィルタをバックグラウンドで実行」

コンピュータ
フィルタの処理に時間がかかるようになってきたのでフィルタをバックグラウンドで実行してみたいと思います。

実行環境

Windows10 2004
dotnet –version 5.0.104
Visual Studio Code
PowerShell 5.1

プロジェクトの作成

mkdir プロジェクト名
cd プロジェクト名
dotnet new wpf
dotnet add package Microsoft.Xaml.Behaviors.Wpf
dotnet add package ReactiveProperty.WPF
dotnet add package OpenCvSharp4.Windows
code .

ソースコード

ファイル名:MainWindow.xaml

<Window x:Class="WpfSample15OpenCVFilter.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:i="clr-namespace:Microsoft.Xaml.Behaviors;assembly=Microsoft.Xaml.Behaviors"
        xmlns:interactivity="clr-namespace:Reactive.Bindings.Interactivity;assembly=ReactiveProperty.WPF"
        xmlns:local="clr-namespace:WpfSample15OpenCVFilter"
        mc:Ignorable="d"
        Title="MainWindow" Height="600" Width="800">
    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="2*"/>
            <ColumnDefinition Width="AUTO"/>
            <ColumnDefinition Width="2*"/>
        </Grid.ColumnDefinitions>
        <x:Code>
            <![CDATA[
                private void PreviewDragOrver(object sender, DragEventArgs e)
                {
                    e.Effects = (e.Data.GetDataPresent(DataFormats.FileDrop)) ? DragDropEffects.Copy : DragDropEffects.None;
                    e.Handled = true;
                }
                private void OriginalImage_SizeChanged(object sender, SizeChangedEventArgs e)
                {
                    OriginalCanvas.Height = e.NewSize.Height;
                    OriginalCanvas.Width = e.NewSize.Width;
                }
                private void ModifiedImage_SizeChanged(object sender, SizeChangedEventArgs e)
                {
                    ModifiedCanvas.Height = e.NewSize.Height;
                    ModifiedCanvas.Width = e.NewSize.Width;
                }
            ]]>
        </x:Code>
        <ScrollViewer
            HorizontalScrollBarVisibility="Visible" 
            VerticalScrollBarVisibility="Visible" 
            Margin="10,10,10,0"
            Background="Azure"
            Grid.Column="0">
            <Canvas
                Name="OriginalCanvas"
                Margin="0,0,0,0"
                RenderTransformOrigin="0.5,0.5"
                Background="AliceBlue">
                <Canvas.RenderTransform>
                    <ScaleTransform ScaleX="{Binding ZoomScale.Value}" ScaleY="{Binding ZoomScale.Value}"/>
                </Canvas.RenderTransform>
                <Image
                    Name="OriginalImage" 
                    Stretch="None"
                    SizeChanged="OriginalImage_SizeChanged"
                    Source="{Binding OriginalImage.Value}">
                </Image>
            </Canvas>
        </ScrollViewer>
        <StackPanel
            Grid.Column="1">
            <Expander
                Header="ぼかし処理"
                IsExpanded="True"
                IsEnabled="{Binding FilterEnabled.Value}"
                BorderBrush="Gray">
                <StackPanel>
                    <StackPanel Orientation=" Horizontal">
                        <Label Content="回数:" />
                        <TextBox Text="{Binding BlurNumberOfTimes.Value}" />
                    </StackPanel>
                    <Slider
                        Minimum="0"
                        Maximum="32"
                        Value="{Binding BlurNumberOfTimes.Value}"
                        TickPlacement="Both" />
                </StackPanel>
            </Expander>
            <Expander
                Header="ノンローカルミーンフィルタ"
                IsExpanded="True"
                IsEnabled="{Binding FilterEnabled.Value}"
                BorderBrush="Gray">
                <StackPanel>
                    <StackPanel Orientation=" Horizontal">
                        <Label Content="h:" />
                        <TextBox Text="{Binding NonLocalMeanH.Value}" />
                    </StackPanel>
                    <Slider
                        Minimum="0"
                        Maximum="32"
                        Value="{Binding NonLocalMeanH.Value}"
                        TickPlacement="Both" />
                </StackPanel>
            </Expander>
            <Expander
                Header="ラプラシアンフィルタ"
                IsExpanded="True"
                IsEnabled="{Binding FilterEnabled.Value}"
                BorderBrush="Gray">
                <StackPanel>
                    <StackPanel Orientation=" Horizontal">
                        <Label Content="ksize:" />
                        <TextBox Text="{Binding LaplacianKsize.Value}" />
                    </StackPanel>
                    <Slider
                        SmallChange="2"
                        LargeChange="2"
                        Minimum="0"
                        Maximum="15"
                        Value="{Binding LaplacianKsize.Value}"
                        TickPlacement="Both" />
                </StackPanel>
            </Expander>
            <Expander
                Header="アンシャープマスキングフィルタ"
                IsExpanded="True"
                IsEnabled="{Binding FilterEnabled.Value}"
                BorderBrush="Gray">
                <StackPanel>
                    <StackPanel Orientation=" Horizontal">
                        <Label Content="K:" />
                        <TextBox Text="{Binding UnsharpMaskingK.Value}" />
                    </StackPanel>
                    <Slider
                        SmallChange="10"
                        LargeChange="10"
                        Minimum="0"
                        Maximum="150"
                        Value="{Binding UnsharpMaskingK.Value}"
                        TickPlacement="Both" />
                </StackPanel>
            </Expander>
            <Expander
                Header="ズーム"
                IsExpanded="True"
                IsEnabled="{Binding FilterEnabled.Value}"
                BorderBrush="Gray">
                <StackPanel>
                    <StackPanel Orientation=" Horizontal">
                        <Label Content="倍率:" />
                        <TextBox Text="{Binding ZoomScale.Value}" />
                    </StackPanel>
                    <Slider
                        Minimum="1"
                        Maximum="16"
                        Value="{Binding ZoomScale.Value}"
                        TickPlacement="Both" />
                </StackPanel>
            </Expander>
            <Button
                Content="フィルター処理"
                IsEnabled="{Binding FilterEnabled.Value}"
                Command="{Binding FilterCommand}" />
            <Button
                Content="クリア"
                IsEnabled="{Binding FilterEnabled.Value}"
                Command="{Binding ClearCommand}" />
            <Label
                AllowDrop="True"
                PreviewDragOver="PreviewDragOrver" 
                Content="こちらにファイルをドラックアンドドロップ" >
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="PreviewDrop">
                        <interactivity:EventToReactiveCommand Command="{Binding PreviewDropCommand}" />
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </Label>    
        </StackPanel>
        <ScrollViewer
            HorizontalScrollBarVisibility="Visible" 
            VerticalScrollBarVisibility="Visible" 
            Margin="10,10,10,0"
            Background="Azure"
            Grid.Column="2">
            <Canvas
                Name="ModifiedCanvas"
                Margin="0,0,0,0"
                RenderTransformOrigin="0.5,0.5"
                Background="AliceBlue">
                <Canvas.RenderTransform>
                    <ScaleTransform ScaleX="{Binding ZoomScale.Value}" ScaleY="{Binding ZoomScale.Value}"/>
                </Canvas.RenderTransform>
                <Image 
                    Name="ModifiedImage"
                    SizeChanged="ModifiedImage_SizeChanged"
                    Stretch="None"
                    Source="{Binding ModifiedImage.Value}"
                    RenderTransformOrigin="0.5,0.5">
                </Image>
            </Canvas>
        </ScrollViewer>
    </Grid>
</Window>

ファイル名:MainWindowViewModel.cs

using System.Diagnostics;
using System;
using System.Windows;
using Reactive.Bindings;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;

using System.Windows.Media.Imaging;
using OpenCvSharp;
using OpenCvSharp.WpfExtensions;

using System.Threading.Tasks;

namespace WpfSample15OpenCVFilter
{
    public class MainWindowViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public ReactiveCommand<DragEventArgs> PreviewDropCommand { get; } = new ReactiveCommand<DragEventArgs>();

        // オリジナル画像のOpenCVオブジェクト
        private Mat _orginalMat;

        // オリジナル画像の表示用
        public ReactiveProperty<WriteableBitmap> OriginalImage { get; private set;} = new ReactiveProperty<WriteableBitmap>();

        // フィルター処理可能フラグ
        public ReactiveProperty<bool> FilterEnabled { get; private set;} = new ReactiveProperty<bool>(false);

        // ぼかし処理の繰り返し回数
        public ReactiveProperty<int> BlurNumberOfTimes { get; private set;} = new ReactiveProperty<int>(6);

        // ノンローカルミーンフィルタ
        public ReactiveProperty<int> NonLocalMeanH { get; private set;} = new ReactiveProperty<int>(12);

        // ズーム倍率
        public ReactiveProperty<int> ZoomScale { get; private set;} = new ReactiveProperty<int>(1);
        
        // 加工画像のOpenCVオブジェクト
        private Mat _modifiedMat;

        // 加工画像の表示用
        public ReactiveProperty<WriteableBitmap> ModifiedImage { get; private set;} = new ReactiveProperty<WriteableBitmap>();

        // フィルター処理
        public AsyncReactiveCommand FilterCommand { get; } = new AsyncReactiveCommand();

        // クリア処理
        public ReactiveCommand ClearCommand { get; } = new ReactiveCommand();

        // ラプラシアンフィルタksize
        public ReactiveProperty<int> LaplacianKsize { get; private set;} = new ReactiveProperty<int>(1);

        // アンシャープマスキングフィルタK
        public ReactiveProperty<int> UnsharpMaskingK { get; private set;} = new ReactiveProperty<int>(15);

        private WriteableBitmap loadPicture(string path)
        {
            if (_orginalMat != null) _orginalMat.Dispose();
            _orginalMat = new Mat(path, ImreadModes.Grayscale);
            var bitmapSource = (WriteableBitmap)BitmapSourceConverter.ToBitmapSource(_orginalMat);
            bitmapSource.Freeze();
            return bitmapSource;
            //OriginalImage.Value = bitmapSource;
        }
        protected virtual void OnPropertyChanged(string name)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(name));
        }

        // 加工画像の初期化
        private void initializeModifiedImage()
        {
            if (_modifiedMat != null) _modifiedMat.Dispose();
            _modifiedMat = null;
            ModifiedImage.Value = null;
        }

        // フィルター処理
        private WriteableBitmap filters()
        {
            double[,] kernel = { {      0.0,  1.0/16.0,      0.0},
                                    { 1.0/16.0, 12.0/16.0, 1.0/16.0},
                                    {      0.0,  1.0/16.0,      0.0},
            };

            _modifiedMat = _orginalMat.Clone();

            // ぼかし処理
            for(int x=0; x<BlurNumberOfTimes.Value; ++x)
            {
                Cv2.Filter2D(_modifiedMat, _modifiedMat, -1, InputArray.Create(kernel));
            }

            // ローカルミーンフィルタ
            Cv2.FastNlMeansDenoising(_modifiedMat, _modifiedMat, (float)NonLocalMeanH.Value);

            // 奇数の場合のみ処理
            if (LaplacianKsize.Value  % 2 == 1)
            {
                // ラプラシアンフィルタ
                Mat edge = _modifiedMat.Clone();
                Cv2.Laplacian(_modifiedMat, edge, MatType.CV_8UC1, LaplacianKsize.Value);                
                /*
                Cv2.Laplacian(_modifiedMat, edge, MatType.CV_64FC1, LaplacianKsize.Value);

                // 8bitに変換
                edge = edge * 255;
                edge.ConvertTo(edge, MatType.CV_8UC1);
                */

                // 減算
                _modifiedMat = _modifiedMat - edge;                

                // ローカルミーンフィルタ
                Cv2.FastNlMeansDenoising(_modifiedMat, _modifiedMat, (float)NonLocalMeanH.Value);
            }

            // アンシャープマスキングフィルタ
            double k = (double)UnsharpMaskingK.Value / 10.0;
            double[,] unsharpKernel = { { -k/9.0,        -k/9.0, -k/9.0},
                                        { -k/9.0, 1.0+8.0*k/9.0, -k/9.0},
                                        { -k/9.0,        -k/9.0, -k/9.0},
            };
            if (UnsharpMaskingK.Value > 0)
            {
                Cv2.Filter2D(_modifiedMat, _modifiedMat, -1, InputArray.Create(unsharpKernel));
            }

            // Mat => WritableBitmap 変換
            var b = (WriteableBitmap)BitmapSourceConverter.ToBitmapSource(_modifiedMat);
            b.Freeze();

            return b;
        }

        public MainWindowViewModel()
        {
            PropertyChanged += (o, e) => {};

            PreviewDropCommand.Subscribe(async e => {
                if (e.Data.GetDataPresent(DataFormats.FileDrop) == false) return;

                var path = ((string[])e.Data.GetData(DataFormats.FileDrop))[0];
                FilterEnabled.Value = false;
                var b = await Task.Run(() => loadPicture(path));
                OriginalImage.Value = b;
                FilterEnabled.Value = true;
            });

            // フィルター処理
            FilterCommand.Subscribe(async _ => {
                
                // ボタン類の無効化
                FilterEnabled.Value = false;

                initializeModifiedImage();

                // フィルター処理
                var b = await Task.Run(() => filters());

                ModifiedImage.Value = b;

                // ボタン類の有効化
                FilterEnabled.Value = true;
            });

            // クリア処理
            ClearCommand.Subscribe(_=>{
                initializeModifiedImage();                
            });
        }
    }
}

他のソースファイルに変更なし。

使い方

操作方法は以前の記事のサンプルと同じになります。
C#でWPF学習中「OpenCVSharp」
PythonでOpenCVを使った画像加工をしているのですが、好みの画像となる設定を探すため、フィルターに引き渡す値の調整をし何度もスクリプトを実行しています。スクリプトだと、その調整作業が面倒なのでWPFで簡単なGUIを作ってみました。...

機能的にはアンシャープマスキングフィルタの記事のサンプルと同じです。
C#でWPF学習中「OpenCVSharp - アンシャープマスキングフィルタで先鋭化」
アンシャープマスキングフィルタを組み込んでみました。実行環境Windows10 2004dotnet --version 5.0.104Visual Studio CodePowerShell 5.1プロジェクトの作成...
変更点はバックグラウンド実行させたいフィルタ処理を別メソッドfilters()に取り出します。filters()呼び出し用のTaskを作成します。
filtersの結果を受け取りフォームに表示させるためawaitをつけてfilters()のTaskを実行します。
awaitを含むメソッドにはasyncを付与しておきます。
とりあえずこれだけでfilters()が別スレッドで実行するようになりました。

恥ずかしながらasyncとawaitを使うのは今回が初めてです。GUIで重たい処理をする場合使う手法だとは知ってはいましたが、そういった処理をすることが無いので使う機会がありませんでした。
sleepなどで適当な処理でシミュレーションをすることは出来ますが、今一つしっくりこない感じでした。
今回のプログラムではフィルタ処理を行っている最中はボタンなどが利用不可となっています。別スレッドで実行するfilters()内で参照しているメンバー変数が書き換えられると不都合が生じるような気がします。対策する場合、根本的に修正する必要になりそうなので、とりあえず今回はここまでとします。

コメント