OpenCVSharp「ガウシアンフィルタ」を試す。

C# コンピュータ
C#

実行環境構築

プロジェクトの作成

mkdir プロジェクト名
cd プロジェクト名
dotnet new winforms
dotnet add package OpenCvSharp4.Windows
dotnet add package OpenCvSharp4.Extensions
code .

ソースプログラム

namespace GaussianBlurSample;

using OpenCvSharp;
using OpenCvSharp.Extensions;

public partial class Form1 : Form
{
    Bitmap? _bmp  = null;

    // フィルター(ガウシアンフィルタ)
    static Bitmap Filter(Bitmap b, int ksize, double sigmaX)
    {
        using var mat = BitmapConverter.ToMat(b);
        Cv2.GaussianBlur(mat, mat, new OpenCvSharp.Size(ksize, ksize), sigmaX);
        return BitmapConverter.ToBitmap(mat);
    }

    public Form1()
    {
        InitializeComponent();

        Text = "ガウシアンフィルタ";

        var sc = new SplitContainer {
            Dock = DockStyle.Fill,
            Orientation = Orientation.Horizontal,   // 上下
            Panel1MinSize = 100,
        };
        var picbox = new PictureBox {
            Dock = DockStyle.Fill,
            AllowDrop = true,
            SizeMode = PictureBoxSizeMode.Zoom,
        };
        picbox.DragEnter += (o, e) => {
            if (e.Data == null) return;
            if(!e.Data.GetDataPresent(DataFormats.FileDrop)) return;
            e.Effect = DragDropEffects.Copy;
        };
        var toClip = new Button {
            Text = "コピー",
            Location = new System.Drawing.Point(10, 10),
            Size = new System.Drawing.Size(180, 50),
        };
        var fromClip = new Button {
            Text = "貼り付け",
            Location = new System.Drawing.Point(200, 10),
            Size = new System.Drawing.Size(180, 50),
        };
        var filterExec = new RadioButton {
            Text = "フィルターOFF",
            Appearance = Appearance.Button,
            AutoCheck = false,
            Location = new System.Drawing.Point(410, 10),
            Size = new System.Drawing.Size(180, 50),
        };
        var sigmaXTrack = new TrackBar {
            Minimum = 0,
            Maximum = 20,
            Value = 0,
            Location = new System.Drawing.Point(370, 80),
            Size = new System.Drawing.Size(360, 50),
        };
        var sigmaXLabel = new Label {
            Text = "SigmaX:" + sigmaXTrack.Value,
            Location = new System.Drawing.Point(240, 80),
            Size = new System.Drawing.Size(120, 50),
        };
        var kernelSizelabel = new Label {
            Text = "カーネルサイズ:",
            Location = new System.Drawing.Point(10, 80),
            Size = new System.Drawing.Size(130, 50),
        };
        var kernelSize = new ComboBox {
            ItemHeight = 20,
            Location = new System.Drawing.Point(140, 80),
            Size = new System.Drawing.Size(100, 50),
        };
        kernelSize.Items.AddRange(new string[] {"3x3", "5x5", "7x7", "9x9"});

        Action<Bitmap> SetPicboxImage = ((Bitmap bmp) => {
            using var mat = BitmapConverter.ToMat(bmp);
            if (picbox.Image is not null) picbox.Image.Dispose();
            picbox.Image = BitmapConverter.ToBitmap(mat);
        });
        Action ResetBitmap = (()=>{
            if (_bmp is null) return;
            SetPicboxImage(_bmp);
        });
        Action<Bitmap> SetBitmap = ((Bitmap src) => {
            if (_bmp is not null) _bmp.Dispose();
            _bmp = src;
            ResetBitmap();
            filterExec.Checked = false;
        });

        picbox.DragDrop += (sender, e) => {
            // ファイルドロップ
            if (e.Data == null) return;
            if(!e.Data.GetDataPresent(DataFormats.FileDrop)) return;
            string[] files = (string[])e.Data.GetData(DataFormats.FileDrop);
            if (files.Length < 1) return;

            // 読み込み
            using var fs = new System.IO.FileStream(
                files[0], System.IO.FileMode.Open,  System.IO.FileAccess.Read);
            var obj = System.Drawing.Image.FromStream(fs);
            if (obj is not null) SetBitmap((Bitmap)obj);
        };
        toClip.Click += (sender, e) => {
            if (picbox.Image is null) return;
            var ms = new MemoryStream();
            picbox.Image.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
            Clipboard.SetData("PNG", ms);            
        };
        fromClip.Click += (sender, e) => {
            var obj = Clipboard.GetData("PNG");
            if (obj == null) return;
            var ms = (MemoryStream)obj;
            if (ms is not null) SetBitmap(new Bitmap(ms));
        };
        sigmaXTrack.ValueChanged += (sender, e) => {
            sigmaXLabel.Text = "SigmaX:" + sigmaXTrack.Value.ToString();
        };
        filterExec.Click += async (sender, e) => {
            if (_bmp is null) return;

            if (filterExec.Checked) {
                ResetBitmap();
                filterExec.Text = "フィルターOFF";
                filterExec.Checked = false;
            } else {
                // フィルター
                if (kernelSize.SelectedIndex < 0) return;
                int ksize = Convert.ToInt32(((string)kernelSize.Items[kernelSize.SelectedIndex]).Substring(0, 1));
                double sigmaX = (double)sigmaXTrack.Value;
                Bitmap? result = null;
                await Task.Run(()=>{
                    result = Filter(_bmp, ksize, sigmaX);
                });
                if (result != null) {
                    SetPicboxImage(result);
                    filterExec.Text = "フィルターON";
                    filterExec.Checked = true;
                }
            }
        };
        sc.Panel1.Controls.Add(picbox);
        sc.Panel2.Controls.Add(toClip);
        sc.Panel2.Controls.Add(fromClip);
        sc.Panel2.Controls.Add(filterExec);
        sc.Panel2.Controls.Add(sigmaXLabel);
        sc.Panel2.Controls.Add(sigmaXTrack);
        sc.Panel2.Controls.Add(kernelSizelabel);
        sc.Panel2.Controls.Add(kernelSize);
        Controls.Add(sc);

        this.Size = new System.Drawing.Size(800, 600);
    }
}

実行

dotnet run

感想

フィルターに渡すパラメタをフォームのコントローラーとして配置しています。パラメタの型がdoubleに対しコントロールのトラックバーの値はintだったりするので、小数点以下を設定することは出来ません。またカーネルは縦横の要素数をセットするOpenCVSharp.Size()型※なのですが、コントロールをコンボボックスで選択式にするとコンボボックスの値の文字列からSize()の要素をセットするようにしています。とりあえず動けば良いというう考え方でプログラミングしていますが、より正しいやり方を模索しています。

フォームの値とC#のプログラムがわの変数との紐づけを自前でプログラミングしようとすると結構煩雑です。おかげでWPFとXAMLにReactivePropertyの便利さが、なんとなく理解できました。このサンプルの目的はOpenCV(OpenCVSharp)のフィルタをC#ので作ったフォームで簡単に試すことが目的なのですが、コアであるOpenCVを扱う部分はFilter()メソッドの数行で他の部分はフォームの作成制御になり、そちらの方がソースコードの大部分を占めます。

フィルタを試すだけであればpython+OpenCVの方がお手軽なのですが、パラメタの調整にGUIが欲しいですし、画像の入出力をクリップボードを経由することで、既存のペイントソフトと組み合わせる使い方が出来ます。

読み込んだ画像をフィルタ処理前の画像として、Bitmapオブジェクトでフィールドに保存していますが、Matオブジェクトとの変換処理が多いように思えます。あとBitmapオブジェクトとMatオブジェクトのDispose()処理も多く抜けがありそうな気がします。PictureBoxに画像をセットする場合のみBitmapオブジェクトに変換するようして、プログラム内部では画像をMatオブジェクトで保持して方が良いような気がします。

あといくつかOpenCVのフィルタを同じようなフォームで作成したいと思いますが、いつか各フィルタを1つのフォームから任意の組み合わせで実行できるアプリケーションが作成出来たら良いなと思っています。

※System.Drawing.Size()と同じ名前なのでWinFormsで扱う場合どちらかをフルネームで記述する必要あり。

コメント