WPFでOpenCVの画像フィルターを実行するGUIアプリの雛形

コンピュータ

GUIアプリからOpenCVの画像加工系のフィルターを実行するGUIアプリのプロトタイプを作成してみました。

対応フィルターは少ないですが、アプリとして使えそうなら後から追加する予定です。

ソースコード

ファイル名:GFilterUITemp01.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net8.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UseWPF>true</UseWPF>
  </PropertyGroup>
  
  <ItemGroup>
    <PackageReference Include="OpenCvSharp4" Version="4.11.0.20250507" />
    <PackageReference Include="OpenCvSharp4.runtime.win" Version="4.11.0.20250507" />
    <PackageReference Include="OpenCvSharp4.WpfExtensions" Version="4.11.0.20250507" />
  </ItemGroup>
</Project>

ファイル名:MainWindow.xaml.cs

using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media.Imaging;

using OpenCvSharp;
using OpenCvSharp.WpfExtensions;

using NxLib.Helper;

namespace GFilterUITemp01;

public partial class MainWindow : System.Windows.Window
{
    Mat? _srcMat;
    Mat? _dstMat;
    bool _isSrcView = false;

    public MainWindow()
    {
        InitializeComponent();

        // 画像拡大
        Img1.OnWheelZoom();

        // D&D
        Wiring.AcceptFiles(this, files =>
        {
            var bmp = BmpSrc.FromFile(files[0]);
            if (bmp is null) return;

            _srcMat?.Dispose();
            _srcMat = bmp.ToMat();
            _dstMat?.Dispose();
            _dstMat = null;

            Img1.Source = bmp;
            _isSrcView = true;
            StatusBarItem1.Content = $"D&D:{files[0]}";
        });

        // Ctrl+C = Copy
        Wiring.Hotkey(this, Key.C, ModifierKeys.Control,
            () =>
            {
                if (Img1.Source is BitmapSource bmp)
                {
                    ClipBD.SetPNG((BitmapSource)Img1.Source);
                    StatusBarItem1.Content = $"クリップボードへコピー";
                }
            },
            () => Img1.Source is not null);
        
        // Ctrl+V = Paste
        Wiring.Hotkey(this, Key.V, ModifierKeys.Control,
            () =>
            {
                var bmp = ClipBD.GetPNG();
                if (bmp is null) return;

                _srcMat?.Dispose();
                _srcMat = bmp.ToMat();
                _dstMat?.Dispose();
                _dstMat = null;

                Img1.Source = bmp;
                _isSrcView = true;
                StatusBarItem1.Content = $"クリップボードからペースト";
            },
            () =>
            {
                return true;
            });
            // ClosedイベントでDisposeする
            this.Closed += OnClosed;

        // Imageのクリックで表示画像の切り替え
        Img1.OnLeftClick(_ =>
        {
            if (_srcMat is null) return;

            if (_isSrcView)
            {
                // フィルタ処理
                if (_dstMat is null)
                {
                    string tag = combobox1.SelectedValue.ToString() ?? "";
                    switch (tag)
                    {
                        case "Gray":
                            _dstMat = Cv2Ex.ToGray(_srcMat);
                            break;
                        case "InvertGray":
                            _dstMat = Cv2Ex.InvertGray(_srcMat);
                            break;
                        case "Gaussian":
                            int n = int.Parse(GausianKernel.SelectedValue.ToString() ?? "15");
                            _dstMat = Cv2Ex.GaussianBlur(_srcMat, new OpenCvSharp.Size(n, n));
                            break;
                        default:
                            _dstMat = Cv2Ex.ToGray(_srcMat);
                            break;
                    }
                }
                Img1.Source = _dstMat.ToBitmapSource();
                StatusBarItem1.Content = "加工後画像を表示中";
            }
            else
            {
                Img1.Source = _srcMat.ToBitmapSource();
                StatusBarItem1.Content = "元画像を表示中";
            }
            _isSrcView = !_isSrcView;
        });
    }
    private void OnClosed(object? sender, EventArgs e)
    {
        // nullチェックしてDispose
        _srcMat?.Dispose();
        _dstMat?.Dispose();
    }

    private void FilterReset(object? sender, SelectionChangedEventArgs e)
    {
        if (sender is null) return;
        _dstMat?.Dispose();
        _dstMat = null;
    }
}

ファイル名:Helpers\BmpSrc.cs

// ビットマップソース
using System.IO;
using System.Windows.Media.Imaging;

namespace NxLib.Helper;
public static class BmpSrc
{
    // ファイルパスから読み込み(ロックしない/Freeze 済み)
    public static BitmapSource FromFile(string path, int? decodeW = null, int? decodeH = null)
    {
        using var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
        return FromStream(stream, decodeW, decodeH);
    }

    // ストリームから読み込み(必要なら)
    public static BitmapSource FromStream(Stream stream, int? decodeW = null, int? decodeH = null)
    {
        var bi = new BitmapImage();
        bi.BeginInit();
        bi.CacheOption  = BitmapCacheOption.OnLoad;   // EndInit後にstreamを閉じられる
        bi.StreamSource = stream;
        if (decodeW.HasValue) bi.DecodePixelWidth  = decodeW.Value;
        if (decodeH.HasValue) bi.DecodePixelHeight = decodeH.Value;
        bi.EndInit();
        bi.Freeze();
        return bi;
    }

    // 例外を投げたくない場合用
    public static bool TryFromFile(string path, out BitmapSource? bmp, int? decodeW = null, int? decodeH = null)
    {
        try { bmp = FromFile(path, decodeW, decodeH); return true; }
        catch { bmp = null; return false; }
    }
}

ファイル名:Helpers\ClipBD.cs

// クリップボード

using System.IO;
using System.Windows;
using System.Windows.Media.Imaging;

namespace NxLib.Helper;
public static class ClipBD
{
    public static void SetPNG(BitmapSource bmp)
    {
        var pngEnc = new PngBitmapEncoder();
        pngEnc.Frames.Add(BitmapFrame.Create(bmp));
        using var mem = new MemoryStream();
        pngEnc.Save(mem);
        Clipboard.SetData("PNG", mem);
    }

    public static BitmapSource? GetPNG()
    {
        var obj = (System.IO.MemoryStream)Clipboard.GetData("PNG");
        if (obj is null)
        {
            var bmp = Clipboard.GetImage();
            return bmp;
        }

        var bi = new BitmapImage();
        bi.BeginInit();
        bi.CacheOption = BitmapCacheOption.OnLoad;
        bi.StreamSource = obj;
        bi.EndInit();
        bi.Freeze();

        return bi;
    }
}

ファイル名:Helpers\Cv2Ex.cs

using OpenCvSharp;

namespace NxLib.Helper;

public static class Cv2Ex
{
    public static Mat GaussianBlur(Mat src, OpenCvSharp.Size ksize)
    {
        Mat dst = new Mat();
        Cv2.GaussianBlur(src, dst, ksize, 0);
        return dst;
    }
    // グレースケール化
    public static Mat ToGray(Mat src)
    {
        if (src.Empty()) return new Mat();

        Mat dst = new Mat();
        switch (src.Channels())
        {
            case 1:
                dst?.Dispose();
                return src.Clone();
            case 3:
                Cv2.CvtColor(src, dst, ColorConversionCodes.BGR2GRAY);
                break;
            case 4:
                Cv2.CvtColor(src, dst, ColorConversionCodes.BGRA2GRAY);
                break;
            default:
                throw new ArgumentException("Unsupported number of channels: " + src.Channels());
        }

        return dst;
    }

    // グレスケールを反転
    public static Mat InvertGray(Mat src)
    {
        if (src.Empty()) return new Mat();

        Mat gray = ToGray(src);
        Mat dst = new Mat(gray.Size(), gray.Type());

        for (int y = 0; y < gray.Rows; y++)
        {
            for (int x = 0; x < gray.Cols; x++)
            {
                byte value = gray.At<byte>(y, x);
                dst.Set(y, x, (byte)(255 - value));
            }
        }

        gray.Dispose();
        return dst;
    }
}

ファイル名:Helpers\Wiring.cs

using System.Windows;
using System.Windows.Input;
using System.Windows.Media;

namespace NxLib.Helper;
public static class Wiring
{
    // D&D: 指定拡張子だけ受け付ける(exts 省略可)
    public static void AcceptFiles(FrameworkElement el, Action<string[]> onFiles, params string[] exts)
    {
        el.AllowDrop = true;
        el.Drop += (_, e) =>
        {
            if (!e.Data.GetDataPresent(DataFormats.FileDrop)) return;

            var files = (string[])e.Data.GetData(DataFormats.FileDrop)!;

            if (exts is { Length: > 0 })
                files = files
                    .Where(f => exts.Any(x => f.EndsWith(x, StringComparison.OrdinalIgnoreCase)))
                    .ToArray();

            if (files.Length > 0)
                onFiles(files);
        };
    }
    // ホットキー登録
    public static void Hotkey(Window w, Key key, ModifierKeys mods, Action action, Func<bool>? canExecute = null)
    {
        var cmd = new RoutedUICommand();
        ExecutedRoutedEventHandler exec = (_, __) => action();
        CanExecuteRoutedEventHandler can = (_, e) => e.CanExecute = canExecute?.Invoke() ?? true;

        var cb = new CommandBinding(cmd, exec, can);
        var kb = new KeyBinding(cmd, key, mods);

        w.CommandBindings.Add(cb);
        w.InputBindings.Add(kb);
    }

    // ホイールで拡大するイベントを追加する拡張メソッド
    public static T OnWheelZoom<T>(this T el, bool consume = true)
    where T : FrameworkElement
    {
        el.PreviewMouseWheel += (_, e) =>
        {
            // Ctrlキーが押されている場合のみ拡大縮小
            if ((Keyboard.Modifiers & ModifierKeys.Control) == 0)
                return;

            // 現在の拡大率を取得
            ScaleTransform? st = el.LayoutTransform as ScaleTransform;
            if (st is null)
                st = new ScaleTransform(1.0, 1.0);

            double scale = st.ScaleX;

            // ホイールの方向で拡大縮小
            if (e.Delta > 0)
                scale *= 1.1;  // 拡大
            else
                scale /= 1.1;  // 縮小
            
            // 範囲制限
            scale = Math.Max(1.0, Math.Min(8.0, scale));

            // 拡大率を設定
            st.ScaleX = scale;
            st.ScaleY = scale;

            el.LayoutTransform = st;

            // スクロールイベントを親に伝えない
            if (consume) e.Handled = true;
        };
        return el;
    }
    // 左クリックで発火
    public static T OnLeftClick<T>(this T el, Action<Point> onClick, bool consume = true)
    where T : FrameworkElement
    {
        el.MouseLeftButtonUp += (_, e) =>
        {
            onClick(e.GetPosition(el));
            if (consume) e.Handled = true;
        };
        return el;
    }
}

ファイル名:MainWindow.xaml

<Window x:Class="GFilterUITemp01.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:local="clr-namespace:GFilterUITemp01"
        mc:Ignorable="d"
        AllowDrop="True"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <DockPanel>

            <StatusBar DockPanel.Dock="Bottom">
                <StatusBarItem
                    x:Name="StatusBarItem1"
                    Content="ステータス" />
            </StatusBar>

            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="5" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <ScrollViewer HorizontalScrollBarVisibility="Auto"
                            VerticalScrollBarVisibility="Auto">
                    <Image x:Name="Img1"
                        Stretch="None"
                        SnapsToDevicePixels="True"
                        RenderOptions.BitmapScalingMode="NearestNeighbor" />
                </ScrollViewer>
                <GridSplitter
                    Grid.Column="1"
                    HorizontalAlignment="Stretch" />
                <StackPanel
                    x:Name="RightPanel"
                    Margin="6"
                    Grid.Column="2">
                    <ComboBox
                        x:Name = "combobox1"
                        SelectionChanged="FilterReset"
                        SelectedValuePath="Tag"
                        SelectedValue = "Gray">
                        <ComboBoxItem Content="グレイスケール化" Tag="Gray" />
                        <ComboBoxItem Content="グレイ反転" Tag="InvertGray" />
                        <ComboBoxItem Content="ガウシアンフィルタ" Tag="Gaussian" />
                    </ComboBox>
                    <Expander Header="ガウシアンフィルタ設定"
                                IsExpanded="False"
                                Margin="0,6,0,0">
                            <StackPanel>
                                <StackPanel Orientation="Horizontal"
                                            VerticalAlignment="Center"
                                            Margin="30,6,0,0">

                                    <TextBlock Text="カーネルサイズ:" />
                                    <ComboBox
                                        x:Name = "GausianKernel"
                                        SelectedValuePath="Tag"
                                        SelectionChanged="FilterReset"
                                        Margin="6,0,0,0"
                                        SelectedValue = "15">
                                        <ComboBoxItem Content="15x15" Tag="15" />
                                        <ComboBoxItem Content="13x13" Tag="13" />
                                        <ComboBoxItem Content="11x11" Tag="11" />
                                        <ComboBoxItem Content="9x9" Tag="9" />
                                        <ComboBoxItem Content="7x7" Tag="7" />
                                        <ComboBoxItem Content="5x5" Tag="5" />
                                        <ComboBoxItem Content="3x3" Tag="3" />
                                    </ComboBox>
                                </StackPanel>
                            </StackPanel>
                    </Expander>
                </StackPanel>
            </Grid>

        </DockPanel>
    </Grid>
</Window>

実行イメージ

・起動

・画像ファイルををD&D

・画像をクリックでフィルター実行→もう一度クリックで元画像に戻る

その他機能

  • Ctrl + マウスホイール画像拡大
  • Ctr + C表示されている画像をクリップボードへコピー
  • Ctr + Vクリップボードから画像を貼り付け

コメント