C#のWPFでOpenCVSharpのフィルターを任意の順番で実行するアプリケーション

コンピュータ

OpenCVには画像を加工してくれるフィルターが沢山あります。欲しい画像が出来上がるまでフィルターのパラメーターの調整作業をする場合GUIがあると便利です。フィルターが1つの場合やフィルターの実行手順が決まっている場合のプログラムは以前作成していますが、今回は複数のフィルターを任意の順番で適用するアプリケーションにしてみたいと思います。

操作説明

フィルターの実行手順

  1. 左側の灰色領域に画像ファイルをドラッグアンドドロップします。
  2. 「追加項目を選択」下のコンボボックスからフィルターを選びます。(現在対応しているフィルターは3つ)
  3. 「+」ボタンでコンボボックスで選択されているフィルターがリストボックスに追加されます。
  4. リストボックスのフィルターを選択するとフィルターのパラメータを入力することができます。
  5. 「▶」ボタンでフィルターの実行します。停止(キャンセル)する場合「■」ボタンを押します。
  6. フィルター実行前の画像に戻す場合「🔙」ボタンを押します。
  7. 表示されている画像を保存する場合「💾」ボタンを押します。プログラムを実行しているディレクトリにimg日時.pngファイルが作成されます。

その他の機能

  • 表示している画像上でマウスホイールを回すと画像が拡大縮小します。
  • 画像上でマウスを押した状態で移動(ドラッグ)すると画像が移動します。
  • 「↑」「↓」ボタンによるフィルターの実行順番の変更及び「-」ボタンでフィルターの削除
  • 終了時フィルターを自動セーブし次回起動時にフィルターを復帰する。

感想

画像の拡大縮小移動部分(不具合有)とリストボックスが比較的簡単に実装できる点はWPFとReactiveCollectionの良い点だと思います。この辺りをWinFormsで実装しようと思うとUIのパーツ一つ作るだけで息切れを起こしてしまい、アプリケーション作成を断念しまいがちになります。

試してみて初めて気が付いたのですが、表示画像が境界線をオーバーしています。想定外ですがこれはこれで面白そうな見た目になるのでこのままにしておこうかと思います。

今後は不具合を修正しつつ、こちらをベースにフィルターの数を増やして行きたいと思います。

プロジェクトの作成

mkdir OpenCVFilterGUI
cd OpenCVFilterGUI
dotnet new wpf
dotnet add package Microsoft.Xaml.Behaviors.Wpf
dotnet add package ReactiveProperty.WPF
dotnet add package OpenCvSharp4.Windows
dotnet add package OpenCvSharp4.Extensions
dotnet add package OpenCvSharp4.WpfExtensions

ソースコード

ファイル名:MainWindow.xaml

<Window x:Class="OpenCVFilterGUI.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:OpenCVFilterGUI"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Loaded">
            <interactivity:EventToReactiveCommand Command="{Binding WindowLoadedCommand}" />
        </i:EventTrigger>
        <i:EventTrigger EventName="Closed">
            <interactivity:EventToReactiveCommand Command="{Binding WindowClosedCommand}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="3*" />
            <ColumnDefinition Width="5" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <StackPanel
            Margin="10"
            Grid.Column="0">
            <Canvas
                Width="{Binding ActualWidth, RelativeSource={RelativeSource FindAncestor, AncestorType=StackPanel}}"
                Height="{Binding ActualHeight, RelativeSource={RelativeSource FindAncestor, AncestorType=StackPanel}}"
                Background="LightGray"
                AllowDrop="True">          
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="DragOver">
                        <interactivity:EventToReactiveCommand Command="{Binding DragOverCommand}" />
                    </i:EventTrigger>
                    <i:EventTrigger EventName="Drop">
                        <interactivity:EventToReactiveCommand Command="{Binding DropCommand}" />
                    </i:EventTrigger>
                    <i:EventTrigger EventName="MouseWheel">
                        <interactivity:EventToReactiveCommand Command="{Binding MouseWheelCommand}" />
                    </i:EventTrigger>
                </i:Interaction.Triggers>
                <Thumb
                    Canvas.Left="{Binding ThumbLeft.Value}"
                    Canvas.Top="{Binding ThumbTop.Value}">
                    <Thumb.RenderTransform>
                        <ScaleTransform ScaleX="{Binding ZoomScale.Value}" ScaleY="{Binding ZoomScale.Value}"/>
                    </Thumb.RenderTransform>
                    <i:Interaction.Triggers>
                        <i:EventTrigger EventName="DragCompleted">
                            <interactivity:EventToReactiveCommand Command="{Binding DragCompletedCommand}" />
                        </i:EventTrigger>
                        <i:EventTrigger EventName="DragStarted">
                            <interactivity:EventToReactiveCommand Command="{Binding DragStartedCommand}" />
                        </i:EventTrigger>
                        <i:EventTrigger EventName="DragDelta">
                             <interactivity:EventToReactiveCommand Command="{Binding DragDeltaCommand}" />
                        </i:EventTrigger>
                    </i:Interaction.Triggers>
                    <Thumb.Template>
                        <ControlTemplate TargetType="Thumb">
                            <Image
                                Stretch="None"
                                Source="{Binding PictureView.Value}">
                            </Image>
                        </ControlTemplate>
                    </Thumb.Template>
                </Thumb>
            </Canvas>
        </StackPanel>
        <GridSplitter
            Grid.Column="1"
            HorizontalAlignment="Stretch"
            Background="Blue" />
        <StackPanel
            Margin="10"
            Grid.Column="2">
            <WrapPanel
                Orientation="Horizontal"
                Margin="10">
                <Button
                    Width="30"
                    Height="30"
                    Content="▶️"
                    ToolTip="実行"
                    Command="{Binding ExecuteCommand}" />
                <Button
                    Width="30"
                    Height="30"
                    Content="⏹️"
                    ToolTip="停止"
                    Command="{Binding StopCommand}" />
                <Button
                    Width="30"
                    Height="30"
                    Content="🔙"
                    ToolTip="戻る"
                    Command="{Binding GobackCommand}" />
                <Button
                    Width="30"
                    Height="30"
                    Content="💾"
                    ToolTip="保存"
                    Command="{Binding SaveCommand}" />
                <Button
                    Width="30"
                    Height="30"
                    Content="🆑"
                    ToolTip="クリア"
                    Command="{Binding ClearCommand}" />
            </WrapPanel>
            <Label>追加項目の選択</Label>
            <ComboBox
                    DisplayMemberPath="Name"
                    SelectedValuePath="Id"
                    SelectedValue="{Binding FilterBaseSelected.Value}"
                    SelectedIndex="{Binding FilterBaseSelectedIndex.Value}"
                    ItemsSource="{Binding FilterBase}" />
            <StackPanel
                Orientation="Horizontal">
                <ListBox
                        DisplayMemberPath="Name"
                        SelectedValuePath="Id"
                        SelectedIndex="{Binding MethodListIndex.Value}"
                        ItemsSource="{Binding MethodList}">
                </ListBox>
                <StackPanel
                    Margin="10">
                    <Button
                        Width="30"
                        Height="30"
                        Content="➕"
                        ToolTip="追加"
                        Command="{Binding AddCommand}" />
                    <Button
                        Width="30"
                        Height="30"
                        Content="➖"
                        ToolTip="削除"
                        Command="{Binding RemoveCommand}" />
                    <Button
                        Width="30"
                        Height="30"
                        Content="⬆️"
                        ToolTip="上へ"
                        Command="{Binding UpCommand}" />
                    <Button
                        Width="30"
                        Height="30"
                        Content="⬇️"
                        ToolTip="下へ"
                        Command="{Binding DownCommand}" />
                </StackPanel>
            </StackPanel>
            <StackPanel
                Visibility="{Binding GaussianVisibility.Value}">
                <Label>ガウシアンフィルタ</Label>
                <WrapPanel
                    Orientation="Horizontal"
                    Margin="10">
                    <Label>カーネルサイズ:</Label>
                    <ComboBox SelectedValuePath="Tag" SelectedValue="{Binding Path=GaussianKernelSize.Value}">
                        <ComboBoxItem Content="無効" Tag="0" />
                        <ComboBoxItem Content="3x3" Tag="3" />
                        <ComboBoxItem Content="5x5" Tag="5" />
                        <ComboBoxItem Content="7x7" Tag="7" />
                    </ComboBox>
                </WrapPanel>
            </StackPanel>
            <StackPanel
                Visibility="{Binding NonLocalMeanVisibility.Value}"
            >
                <Label>ノンローカルミーンフィルタ</Label>
                <WrapPanel
                    Orientation="Horizontal"
                    Margin="10">
                    <Label>H:</Label>
                    <Label Content="{Binding ElementName=Slider1, Path=(Slider.Value)}" />
                    <Slider
                        x:Name="Slider1"
                        Width = "200"
                        Minimum="0"
                        Maximum="30"
                        IsSnapToTickEnabled="True"
                        TickFrequency="1"
                        SmallChange="2"
                        LargeChange="5"
                        Value="{Binding NonLocalMeanH.Value}"
                        TickPlacement="Both" />
                </WrapPanel>
            </StackPanel>
            <StackPanel
                Visibility="{Binding UnSharpVisibility.Value}"
            >
                <Label>アンシャープフィルタ</Label>
                <WrapPanel
                    Orientation="Horizontal"
                    Margin="10">
                    <Label>K:</Label>
                    <TextBox
                        Width="50"
                        Text="{Binding UnSharpK.Value}" />
                </WrapPanel>
            </StackPanel>
            <StackPanel
                Visibility="{Binding ResizeVisibility.Value}">
                <Label>リサイズフィルタ</Label>
                <WrapPanel
                    Orientation="Horizontal"
                    Margin="10">
                    <Label>Scale:</Label>
                    <TextBox
                        Width="50"
                        Text="{Binding ResizeScale.Value}" />
                    <ComboBox SelectedValuePath="Tag" SelectedValue="{Binding Path=ResizeInterpolationFlags.Value}">
                        <ComboBoxItem Content="Nearest" Tag="0" />
                        <ComboBoxItem Content="Linear" Tag="1" />
                        <ComboBoxItem Content="Cubic" Tag="2" />
                        <ComboBoxItem Content="Area" Tag="3" />
                        <ComboBoxItem Content="Lanczos4" Tag="4" />
                    </ComboBox>
                </WrapPanel>
            </StackPanel>
        </StackPanel>
    </Grid>
</Window>

ファイル名:MainWindowViewModel.cs

using System;
using System.ComponentModel;
using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System.Reactive.Disposables;

using System.Diagnostics;
using System.Windows.Media.Imaging;
using System.Windows;
using System.Windows.Input;
using System.IO;
using System.Reactive.Linq;
using OpenCvSharp.WpfExtensions;
using System.Windows.Controls.Primitives;

namespace OpenCVFilterGUI;

public class MainWindowViewModel : INotifyPropertyChanged, IDisposable
{
    public event PropertyChangedEventHandler? PropertyChanged;
    protected virtual void OnPropertyChanged(string name)
        =>PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    private CompositeDisposable Disposable { get; } = new ();

    public AsyncReactiveCommand<EventArgs> WindowLoadedCommand { get; }
    public AsyncReactiveCommand<EventArgs> WindowClosedCommand { get; }
    public ReactiveProperty<BitmapSource?> PictureView { get; private set; }
    public ReactiveProperty<BitmapSource?> _backupPicutre  { get; private set; } = new();
    public ReactiveCommand<DragEventArgs> DragOverCommand { get; }
    public ReactiveCommand<DragEventArgs> DropCommand { get; }
    public ReactiveProperty<double> ZoomScale { get; private set; } = new(1.0d);
    public ReactiveCommand<MouseWheelEventArgs> MouseWheelCommand { get; }
    public ReactiveProperty<double> ThumbLeft { get; private set; } = new(0.0d);
    public ReactiveProperty<double> ThumbTop { get; private set; } = new(0.0d);
    public ReactiveCommand<DragDeltaEventArgs> DragDeltaCommand { get; }
    public ReactiveCommand<DragCompletedEventArgs> DragCompletedCommand { get; }
    public ReactiveCommand<DragStartedEventArgs> DragStartedCommand { get; }
    public ReactiveProperty<Guid> FilterBaseSelected { get; set; } = new ();
    public ReactiveProperty<int> FilterBaseSelectedIndex { get; set; } = new ();
    public ReactiveCollection<OpenCVFilter> FilterBase { get; set; } =
    [
        OpenCVFilter.GenrateGaussianFilter(),
        OpenCVFilter.GenrateNonLocalMeanFilter(),
        OpenCVFilter.GenrateUnsharpFilter(),
        OpenCVFilter.GenrateBlurFilter(),
        OpenCVFilter.GenrateMedianFilter(),
        OpenCVFilter.GenrateResizeFilter(),
    ];

    public ReactiveProperty<int> MethodListIndex { get; set; } = new();
    public ReactiveCollection<OpenCVFilter> MethodList { get; set; } = [];

    public AsyncReactiveCommand ExecuteCommand { get; }
    public AsyncReactiveCommand StopCommand { get; }
    public ReactiveCommand GobackCommand { get; }
    public ReactiveCommand SaveCommand { get; }
    public ReactiveCommand ClearCommand { get; }
    public ReactiveCommand AddCommand { get; }
    public ReactiveCommand RemoveCommand { get; }
    public ReactiveCommand UpCommand { get; }
    public ReactiveCommand DownCommand { get; }

    public ReactiveProperty<Visibility> GaussianVisibility { get; private set; } = new(Visibility.Collapsed);
    public ReactiveProperty<Visibility> NonLocalMeanVisibility { get; private set; } = new(Visibility.Collapsed);
    public ReactiveProperty<Visibility> UnSharpVisibility { get; private set; } = new(Visibility.Collapsed);

    public ReactiveProperty<int> GaussianKernelSize { get; private set; } = new(3);
    public ReactiveProperty<int> NonLocalMeanH { get; private set; } = new(15);
    public ReactiveProperty<double> UnSharpK { get; private set; } = new(1.0d);
    public ReactiveProperty<Visibility> ResizeVisibility { get; private set; } = new(Visibility.Collapsed);
    public ReactiveProperty<double> ResizeScale { get; private set; } = new(4.0d);
    public ReactiveProperty<int> ResizeInterpolationFlags { get; private set; } = new(0);

    const string jsonPath = @".\filters.json";
    CancellationTokenSource? cts;
    public MainWindowViewModel()
    {
        PropertyChanged += (o, e) => {};
        
        WindowLoadedCommand = new AsyncReactiveCommand<EventArgs>()
            .WithSubscribe(async e =>
            {
                Debug.Print("Loaded");
                if (File.Exists(jsonPath))
                {
                    var filters = await OpenCVFilter.LoadAsync(jsonPath);
                    MethodList.AddRangeOnScheduler(filters);
                }
            });
        WindowClosedCommand = new AsyncReactiveCommand<EventArgs>()
            .WithSubscribe(async e =>
            {
                Debug.Print("Closed");
                await OpenCVFilter.SaveAsync(jsonPath, MethodList);
                this.Dispose();
            });      
        
        PictureView = new ReactiveProperty<BitmapSource?>();
        

        DragOverCommand = new ReactiveCommand<DragEventArgs>()
            .WithSubscribe(e=>
            {
                if (e.Data.GetDataPresent(DataFormats.FileDrop))
                {
                    e.Effects = DragDropEffects.Copy;
                    e.Handled = true;
                }
                else
                {
                    e.Effects = DragDropEffects.None;
                    e.Handled = false;
                }
            }).AddTo(Disposable);
        
        var LoadPicture = new Func<string, BitmapImage> ((path)=>
        {
            BitmapImage bi = new();

            using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read))
            using(var ms = new MemoryStream())
            {
                fs.CopyTo(ms);
                ms.Seek(0, SeekOrigin.Begin);
                bi.BeginInit();
                bi.CacheOption = BitmapCacheOption.OnLoad;
                bi.StreamSource = ms;
                bi.EndInit();
                bi.Freeze();
            }
            return bi; 
        });

        DropCommand = new ReactiveCommand<DragEventArgs>()
            .WithSubscribe(async e=>
            {
                var files = (string[])e.Data.GetData(DataFormats.FileDrop);

                PictureView.Value = await Task.Run(() =>
                {
                    var b = LoadPicture(files[0]);

                    return OpenCVFilter.ConvertToDPI(b, 25.4d / 0.1845d);
                });
            }).AddTo(Disposable);
        
        MouseWheelCommand = new ReactiveCommand<MouseWheelEventArgs>()
            .WithSubscribe(e =>
            {
                // ホイール
                double x = ZoomScale.Value;

                if (e.Delta < 0)
                    x = x - 0.25d;
                else
                    x = x + 0.25d;
                
                if (x < 0.25d)
                    x = 0.25d;

                if (x > 8.0d)
                    x = 8.0d;
                
                ZoomScale.Value = x;
            });

            DragDeltaCommand = new ReactiveCommand<DragDeltaEventArgs>()
                .WithSubscribe(e =>
                {
                    if (PictureView.Value is null) return;

                });
            DragStartedCommand = new ReactiveCommand<DragStartedEventArgs>()
                .WithSubscribe(e =>
                {
                    if (PictureView.Value is null) return;

                });
            DragCompletedCommand = new ReactiveCommand<DragCompletedEventArgs>()
                .WithSubscribe(e =>
                {
                    if (PictureView.Value is null) return;

                    ThumbLeft.Value = ThumbLeft.Value + e.HorizontalChange;
                    ThumbTop.Value = ThumbTop.Value + e.VerticalChange;
                });

            AddCommand = new ReactiveCommand()
                .WithSubscribe(()=>
                {
                    var obj = FilterBase[FilterBaseSelectedIndex.Value];
                    if (obj is null) return;
                    MethodList.AddOnScheduler((OpenCVFilter)obj.Clone());
                });
            
            RemoveCommand = new ReactiveCommand()
                .WithSubscribe(()=>{
                    if (MethodList.Count == 0) return;
                    if (MethodListIndex.Value < 0) return;
                    MethodList.RemoveAtOnScheduler(MethodListIndex.Value);
                });
            
            UpCommand = new ReactiveCommand()
                .WithSubscribe(()=>{
                    if (MethodList.Count <= 1) return;
                    if (MethodListIndex.Value <= 0) return;

                    MethodList.MoveOnScheduler(MethodListIndex.Value, MethodListIndex.Value-1);
                    MethodListIndex.Value -= 1;
                });
            
            DownCommand = new ReactiveCommand()
                .WithSubscribe(()=>{
                    if (MethodList.Count <= 1) return;
                    if (MethodListIndex.Value >= (MethodList.Count-1)) return;

                    MethodList.MoveOnScheduler(MethodListIndex.Value, MethodListIndex.Value+1);
                    MethodListIndex.Value += 1;
                });

            List<ReactiveProperty<Visibility>> Visibilities =
            [
                GaussianVisibility,
                NonLocalMeanVisibility,
                UnSharpVisibility,
            ];
            MethodListIndex.Subscribe(x=>
            {
                if (MethodList.Count <= 0)
                {
                    MethodListIndex.Value = -1;
                    return;
                }
                if (x < 0) return;
                var obj = MethodList[x];
                if (obj is null) return;
                foreach(var v in Visibilities)
                {
                    v.Value = Visibility.Collapsed;
                }
                if (obj.Name == "GaussianFilter")
                {
                    GaussianVisibility.Value = Visibility.Visible;
                    GaussianKernelSize.Value = obj.IntParam["Ksize"];
                }
                else if (obj.Name == "BlurFilter")
                {
                    Debug.Print(obj.Name);
                    GaussianVisibility.Value = Visibility.Visible;
                    GaussianKernelSize.Value = obj.IntParam["Ksize"];
                }
                else if (obj.Name == "MedianFilter")
                {
                    Debug.Print(obj.Name);
                    GaussianVisibility.Value = Visibility.Visible;
                    GaussianKernelSize.Value = obj.IntParam["Ksize"];
                }
                else if (obj.Name == "NonLocalMeanFilter")
                {
                    NonLocalMeanVisibility.Value = Visibility.Visible;
                    NonLocalMeanH.Value = obj.IntParam["H"];
                }
                else if (obj.Name == "UnsharpFilter")
                {
                    UnSharpVisibility.Value = Visibility.Visible;
                    UnSharpK.Value = obj.DoubleParam["K"];
                }
                else if (obj.Name == "ResizeFilter")
                {
                    ResizeVisibility.Value = Visibility.Visible;
                    ResizeScale.Value = obj.DoubleParam["Scale"];
                    ResizeInterpolationFlags.Value = obj.IntParam["InterpolationFlags"];
                }
            });
            GaussianKernelSize.Subscribe(x=>
            {
                int i = MethodListIndex.Value;
                if (i < 0) return;

                MethodList[i].IntParam["Ksize"] = x;
            });
            NonLocalMeanH.Subscribe(x=>
            {
                int i = MethodListIndex.Value;
                if (i < 0) return;

                MethodList[i].IntParam["H"] = x;
            });
            UnSharpK.Subscribe(x=>
            {
                int i = MethodListIndex.Value;
                if (i < 0) return;

                MethodList[i].DoubleParam["K"] = x;
            });
            ResizeScale.Subscribe(x=>
            {
                int i = MethodListIndex.Value;
                if (i < 0) return;

                MethodList[i].DoubleParam["Scale"] = x;
            });
            ResizeInterpolationFlags.Subscribe(x=>
            {
                int i = MethodListIndex.Value;
                if (i < 0) return;

                MethodList[i].IntParam["InterpolationFlags"] = x;
            });

            var imageSetFlag = PictureView.Select(x => x is not null);
            var executeFlag = new ReactiveProperty<bool>(true);
            ExecuteCommand = executeFlag.CombineLatest(imageSetFlag, (x,y) => x & y)
                .ToAsyncReactiveCommand()
                .WithSubscribe(async () =>
                {

                    if (PictureView.Value is null) return;
                    cts = new();
                    _backupPicutre.Value = (BitmapSource)PictureView.Value.Clone();
                    executeFlag.Value = false;

                    var mat = await OpenCVFilter.ExecuteAsync(MethodList, _backupPicutre.Value, cts.Token);

                    cts?.Dispose();
                    cts = null;
                    if (mat is not null)
                    {
                        var bi = BitmapSourceConverter.ToBitmapSource(mat);
                        PictureView.Value = OpenCVFilter.ConvertToDPI(bi, _backupPicutre.Value.DpiX);
                    }
                    executeFlag.Value = false;
                });
            StopCommand = executeFlag
                .Select(e => !e)
                .CombineLatest(imageSetFlag, (x,y) => x & y)
                .ToAsyncReactiveCommand()
                .WithSubscribe(async () =>
                {
                    if (cts is not null)
                    {
                        cts?.Cancel();  // キャンセルを実行。
                        while(cts is not null) await Task.Delay(10);
                        return; // 戻る
                    }               
                });
            GobackCommand = _backupPicutre.Select(x => x is not null).ToReactiveCommand()
                .WithSubscribe(()=>
                {
                    PictureView.Value = _backupPicutre.Value;
                    _backupPicutre.Value = null;
                    executeFlag.Value = true;
                });
            SaveCommand = imageSetFlag.ToReactiveCommand()
                .WithSubscribe(()=>
                {
                    var image = PictureView.Value;
                    if (image is null) return;

                    string noewstr = DateTime.Now.ToString("yyyyMMddhhmmss");
                    string filename = Path.Join(".", $"img_{noewstr}.png");
                    using (var stream = new FileStream(filename, FileMode.Create))
                    {
                        var encoder = new PngBitmapEncoder();
                        encoder.Frames.Add(BitmapFrame.Create(image));
                        encoder.Save(stream);
                    }
                });
            ClearCommand = new ReactiveCommand()
                .WithSubscribe(()=>
                {
                    PictureView.Value = null;
                    _backupPicutre.Value = null;
                    executeFlag.Value = true;
                });
    }
    public void Dispose()
    {
        Disposable.Dispose();
    }
}

ファイル名:OpenCVFilter.cs

using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Drawing.Drawing2D;
using System.IO;
using System.Reflection;
using System.Reflection.Metadata.Ecma335;
using System.Text;
using System.Text.Json;
using System.Windows.Media.Imaging;
using OpenCvSharp;
using OpenCvSharp.Extensions;
using OpenCvSharp.WpfExtensions;

public class OpenCVFilter
{
    public static BitmapSource
    ConvertToDPI(BitmapSource src, double dpi)
    {
        if (src.DpiX == dpi && src.DpiY == dpi) return src;
        
        int width = src.PixelWidth;
        int height = src.PixelHeight;
        int stride = (width * src.Format.BitsPerPixel + 7) / 8;
        byte[] pixelData = new byte[stride * height];
        src.CopyPixels(pixelData, stride, 0);

        var dest = System.Windows.Media.Imaging.BitmapImage.Create(
            width,
            height,
            dpi,
            dpi,
            System.Windows.Media.PixelFormats.Bgra32,
            null,
            pixelData,
            stride);
        dest.Freeze();

        return dest;
    }

    public static async Task<Mat> ExecuteAsync(IEnumerable<OpenCVFilter> filters, BitmapSource bi, CancellationToken token)
    {
        var mat = BitmapSourceConverter.ToMat(bi);
        var result = await Task.Run(()=>
        {
            try
            {
                foreach(var filter in filters)
                {
                    token.ThrowIfCancellationRequested();
                    using var src = mat;
                    mat = filter.Execute(src);
                }
            }
            catch (OperationCanceledException ex)
            {
                Debug.Print($"{ex.Message}");
                return mat;
            }
            return mat;
        }, token);
        return result;
    }
    public static async Task SaveAsync(string path, IEnumerable<OpenCVFilter> filters)
    {
        var encoding = Encoding.GetEncoding("utf-8");
        if (File.Exists(path)) File.Delete(path);
        using (var writer = new StreamWriter(path, false, encoding))
        {
            var jsonStr = JsonSerializer.Serialize(filters);
            await writer.WriteAsync(jsonStr);
        }
    }
    public static async Task<IEnumerable<OpenCVFilter>> LoadAsync(string path)
    {
        var encoding = Encoding.GetEncoding("utf-8");
        using var reader = new StreamReader(path, encoding);
        string jsonStr = "";
        while (reader.Peek() >= 0)
        {
            var buff = await reader.ReadLineAsync();
            if (buff is null) continue;
            jsonStr += buff;
        }            
        var filters = JsonSerializer.Deserialize<List<OpenCVFilter>>(jsonStr);
        if (filters is null) return new List<OpenCVFilter>();
        return filters;
    }

    public string Name {get; set;} = "";
    public Guid Id { get; set; } = Guid.NewGuid();
    public Dictionary<string, int> IntParam {get; set;} = [];
    public Dictionary<string, double> DoubleParam {get; set;} = [];

    // コンストラクタ
    public OpenCVFilter()
    {
        // privateにすることで直接newさせないようにしたいがJSONデシリアライズで失敗する。
    }
    // クローン
    public OpenCVFilter Clone()
    {
        return (OpenCVFilter)MemberwiseClone();
    }
    // フィルターの実行
    public Mat Execute(Mat src)
    {
        // 自身のtype型を取得
        Type type = this.GetType();
        // this.Nameにセットしたフィルター名のメソッドを取得
        MethodInfo? method = type.GetMethod(this.Name);
        // メソッドを実行
        var mat = method?.Invoke(this, [src]);
        if (mat is null) return new Mat();
        return (Mat)mat;
    }
    // ガウシアンフィルターの生成
    static public OpenCVFilter GenrateGaussianFilter()
    {
        // 初期値をセット
        OpenCVFilter obj = new()
        {
            Name = "GaussianFilter",
        };
        obj.IntParam["Ksize"] = 7;

        return obj;
    }
    // ガウシアンフィルターを実行
    public Mat GaussianFilter(Mat src)
    {
        // とりあえずコンソールに文字を出力してみる。
        var ksize = IntParam["Ksize"];
        Console.WriteLine($"{Name} Ksize={ksize}");
        Mat dst = new();
        Cv2.GaussianBlur(src, dst, new OpenCvSharp.Size(ksize, ksize), 1.0d);
        return dst;
    }
    // アンシャープマスキングフィルター生成
    static public OpenCVFilter GenrateUnsharpFilter()
    {
        // 初期値をセット
        OpenCVFilter obj = new()
        {
            Name = "UnsharpFilter",
        };
        obj.DoubleParam["K"] = 5.5;

        return obj;
    }
    // アンシャープマスキングフィルターを実行
    public Mat UnsharpFilter(Mat src)
    {
        // とりあえずコンソールに文字を出力してみる。
        var k = DoubleParam["K"];
        Console.WriteLine($"{Name} K={k}");
        Mat dst = new();
        double[,] kernel =
        {
            { -k/9.0d,        -k/9.0d, -k/9.0d},
            { -k/9.0d, 1.0+8.0*k/9.0d, -k/9.0d},
            { -k/9.0d,        -k/9.0d, -k/9.0d},
        };
        // 画像フィルタ(アンシャープマスキングフィルタ)
        Cv2.Filter2D(src, dst, -1, InputArray.Create(kernel));
        return dst;
    }
    // ノンローカルミーンフィルター生成
    static public OpenCVFilter GenrateNonLocalMeanFilter()
    {
        // 初期値をセット
        OpenCVFilter obj = new()
        {
            Name = "NonLocalMeanFilter",
        };
        obj.IntParam["H"] = 11;

        return obj;
    }
    // ノンローカルミーンフィルターを実行
    public Mat NonLocalMeanFilter(Mat src)
    {
        var h = IntParam["H"];
        Console.WriteLine($"{Name} H={h}");
        Mat dst = new();
        Cv2.FastNlMeansDenoising(src, dst, h);
        return dst;
    }
    // ぼかしフィルター生成
    static public OpenCVFilter GenrateBlurFilter()
    {
        // 初期値をセット
        OpenCVFilter obj = new()
        {
            Name = "BlurFilter",
        };
        obj.IntParam["Ksize"] = 3;

        return obj;
    }
    // ぼかしフィルターを実行
    public Mat BlurFilter(Mat src)
    {
        var ksize = IntParam["Ksize"];
        Console.WriteLine($"{Name} Ksize={ksize}");
        Mat dst = new();
        Cv2.Blur(src, dst, new OpenCvSharp.Size(ksize, ksize));
        return dst;
    }
    // メディアンフィルター生成
    static public OpenCVFilter GenrateMedianFilter()
    {
        // 初期値をセット
        OpenCVFilter obj = new()
        {
            Name = "MedianFilter",
        };
        obj.IntParam["Ksize"] = 3;

        return obj;
    }
    // メディアンフィルターを実行
    public Mat MedianFilter(Mat src)
    {
        var ksize = IntParam["Ksize"];
        Console.WriteLine($"{Name} Ksize={ksize}");
        Mat dst = new();
        Cv2.MedianBlur(src, dst, ksize);
        return dst;
    }
    // リサイズフィルター生成
    static public OpenCVFilter GenrateResizeFilter()
    {
        // 初期値をセット
        OpenCVFilter obj = new()
        {
            Name = "ResizeFilter",
        };
        obj.DoubleParam["Scale"] = 4.0d;
        obj.IntParam["InterpolationFlags"] = (int)InterpolationFlags.Nearest;
        
        // 0.InterpolationFlags.Nearest
        // 1.InterpolationFlags.Linear
        // 2.InterpolationFlags.Cubic
        // 3.InterpolationFlags.Area
        // 4.InterpolationFlags.Lanczos4

        return obj;
    }
    // リサイズフィルターを実行
    public Mat ResizeFilter(Mat src)
    {
        Mat dst = new();
        double scale = DoubleParam["Scale"];
        InterpolationFlags interpolationFlags = (InterpolationFlags)IntParam["InterpolationFlags"];

        Cv2.Resize(src, dst, new OpenCvSharp.Size(0, 0), scale, scale, interpolationFlags);
        return dst;
    }
}

追記:20240514
・縮小状態で移動すると画像が消える問題を応急処置しました。その影響でドラッグ時の軌跡が表示されず一気に画像が移動します。
追記:20240516
・フィルターの再実行が出来ないようにボタンを修正
・リサイズフィルターを追加

コメント