OpenCVSharp「テンプレートマッチング」を試す。2「WPF」

コンピュータ

WPFでテンプレートマッチングを行うプログラムをGUIを作成してみました。

また、複数のマッチングをするため何回もマッチング処理を実行していましたが、今回は一度のマッチング処理ですむようにしてみました。

プロジェクトの作成

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

ソースプログラム

ファイル名:MainWindow.xaml

<Window x:Class="TemplateMatchingSample3.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:TemplateMatchingSample3"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <DockPanel>
        <StatusBar
            DockPanel.Dock="Bottom"
            VerticalAlignment="Bottom">
            <StatusBarItem>
                <Slider
                    x:Name="Slider1"
                    Width="120"
                    Minimum="0.1"
                    Maximum="8"
                    SmallChange="1"
                    LargeChange="2"
                    Value="1" />
            </StatusBarItem>
            <StatusBarItem
                Content="{Binding ElementName=Slider1,Path=Value}">
            </StatusBarItem>
        </StatusBar>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="2*"/>
                <ColumnDefinition Width="5" />
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <ScrollViewer
                x:Name = "ScrollViewer1"
                Background = "Black"
                AllowDrop = "True"
                Grid.Column="0"
                HorizontalScrollBarVisibility="Auto"
                VerticalScrollBarVisibility="Auto">
                <Image
                    x:Name="Image1"
                    Stretch="Uniform"
                >
                    <Image.LayoutTransform>
                        <ScaleTransform
                            ScaleX="{Binding ElementName=Slider1,Path=Value}"
                            ScaleY="{Binding ElementName=Slider1,Path=Value}"/>
                    </Image.LayoutTransform>
                </Image>
            </ScrollViewer>
            <GridSplitter
                Grid.Column="1"
                HorizontalAlignment="Stretch"
                Background="Gray" />
            <StackPanel
                Grid.Column="2"
            >
                <ScrollViewer
                    x:Name = "ScrollViewer2"
                    Background = "Black"
                    AllowDrop = "True"
                    Margin="8"
                    Width="200"
                    Height="200"
                    HorizontalScrollBarVisibility="Auto"
                    VerticalScrollBarVisibility="Auto">
                
                    <Image
                        x:Name="Image2"
                        Stretch="Uniform"
                    >
                        <Image.LayoutTransform>
                            <ScaleTransform
                                ScaleX="{Binding ElementName=Slider1,Path=Value}"
                                ScaleY="{Binding ElementName=Slider1,Path=Value}"/>
                        </Image.LayoutTransform>
                    </Image>
                </ScrollViewer>

                <Button
                    x:Name="RunBtn"
                    Margin="8"
                    Padding="8">
                    実行
                </Button>
            </StackPanel>
        </Grid>
    </DockPanel>
</Window>

ファイル名:MainWindow.xaml.cs

using System.Diagnostics;
using System.Drawing;
using System.Windows;
using System.Windows.Media.Imaging;

using OpenCvSharp;
using OpenCvSharp.Extensions;
using OpenCvSharp.WpfExtensions;

namespace TemplateMatchingSample3;

/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : System.Windows.Window
{
    public MainWindow()
    {
        InitializeComponent();

        BitmapSource targetBmp = new BitmapImage();
        BitmapSource templateBmp = new BitmapImage();


        ScrollViewer1.Drop += async (s, e) =>
        {
            if (e.Data is null) return;
            var files = e.Data.GetData(DataFormats.FileDrop) as string[] ?? [];
            if (files.Length == 0) return;
            string file = files[0];

            var bi = await Task.Run(()=>
            {
                using var fs = new System.IO.FileStream(
                    file, System.IO.FileMode.Open, System.IO.FileAccess.Read);
                BitmapImage bi = new();
                bi.BeginInit();
                bi.StreamSource = fs;
                bi.CacheOption = BitmapCacheOption.OnLoad;
                bi.CreateOptions = BitmapCreateOptions.None;
                bi.EndInit();
                bi.Freeze();
                return bi;
            });
            targetBmp = bi;
            Image1.Source = targetBmp;
        };
        ScrollViewer2.Drop += async (s, e) =>
        {
            if (e.Data is null) return;
            var files = e.Data.GetData(DataFormats.FileDrop) as string[] ?? [];
            if (files.Length == 0) return;
            string file = files[0];

            var bi = await Task.Run(()=>
            {
                using var fs = new System.IO.FileStream(
                    file, System.IO.FileMode.Open, System.IO.FileAccess.Read);
                BitmapImage bi = new();
                bi.BeginInit();
                bi.StreamSource = fs;
                bi.CacheOption = BitmapCacheOption.OnLoad;
                bi.CreateOptions = BitmapCreateOptions.None;
                bi.EndInit();
                bi.Freeze();
                return bi;
            });
            templateBmp = bi;
            Image2.Source = templateBmp;
        };
        RunBtn.Click += async (s, e) =>
        {
            RunBtn.IsEnabled = false;

            var bi = await Task.Run(()=>
            {
                using Mat img1 = BitmapSourceConverter.ToMat(targetBmp);
                using Mat img2 = BitmapSourceConverter.ToMat(templateBmp);
                using Mat img3 = (Mat)img1.Clone();
                using Mat result = new();

                if (img1.Channels() > 1)
                    Cv2.CvtColor(img1, img1, ColorConversionCodes.RGB2GRAY);
                if (img2.Channels() > 1)
                    Cv2.CvtColor(img2, img2, ColorConversionCodes.RGB2GRAY);

                //double minval, maxval, threshold = 0.8;
                //OpenCvSharp.Point minloc, maxloc;
                float threshold = 0.8f;

                Cv2.MatchTemplate(img1, img2, result, TemplateMatchModes.CCoeffNormed);

                for (int y = 0; y < result.Rows; y++)
                {
                    for (int x = 0; x < result.Cols; x++) {
                        if(result.At<float>(y, x) >= threshold) {  
                            img3.Rectangle(new OpenCvSharp.Rect(x, y, img2.Width, img2.Height), new Scalar(0, 255, 0, 255), 2);  
                        }  

                    }
                }
                /*
                double minval, maxval;
                double thr = 0.8;
                OpenCvSharp.Point minloc, maxloc;
                Cv2.MinMaxLoc(result, out minval, out maxval, out minloc, out maxloc);
                if (maxval >= thr) {
                    Cv2.Rectangle(img3, new OpenCvSharp.Rect(maxloc.X, maxloc.Y, img2.Width, img2.Height), new Scalar(0, 255, 0, 255), 2);
                }
                */
                var bi = BitmapSourceConverter.ToBitmapSource(img3);
                bi.Freeze();
                return bi;
            });

            Image1.Source = bi;

            RunBtn.IsEnabled = true;
        };
    }
}

実行

起動すると以下のようなウィンドウが表示されます。

左側にマンチングさせたい画像をドラックアンドドロップ右側にテンプレート画像をドラックアンドドロップ。

実行ボタンを押すとテンプレートマッチングが実行されて検出された部分が緑色の枠で囲まれます。

画面左下のスライダーを動かすと画像が拡大縮小します。

感想

OpenCVの情報をネット検索するとpythonのサンプルコードが良く見つかります。基本的に同じOpenCVなのでC#のOpenCVSharpでも情報を活用することが出来ますが、pythonの場合画像オブジェクトがnumpyの配列で表現しますが、C#の場合Matオブジェクトになります。numpyベースの操作が出来ない点がつらいですが、頑張ればC#でも同じように動作するコードをプログラミングすることが出来ます。ただOpenCVの関数でカーネルなど配列を引数で渡す場合、C#ではどのようにするのが良いでしょうか?画像オブジェクトとしてMatを渡す場合と、C#の配列をInputArrayやOutputArrayなどに変換する2通りがあるようです。今回テンプレートマッチの結果はMatオブジェクトで受け取り、各座標にはfloat型でマッチ度?がセットされているので、閾値以上がマッチとみなし枠線を描画しています。閾値を変更することでマッチされる範囲も変更されると思われます。
(閾値用のスライダーを用意すべきでした。)

今回はWPFでGUIを作ってみましたが、XAML+コードビハインドであればWinFormsと同じ程度の労力でコードが書けるような気がします。MVVMを導入しようとすると再利用を意識したクラス設計をしたくなり、中々指が進まないですが、コードビハインドで作ると割り切ってしまえば、XAMLで配置したコントロールに紐づくイベントをコードビハインドで記述するだけになります。

手の込んだコントロールを作ろうとすると相当ハードルが高いですが、XAMLをHTMLと見立てて見た目をデザインすると、シンプルなViewが出来上がり結果プログラムの生産性が上がるような気がします。やりたいことと出来ることの折り合いをXAMLでつけて上げると良い感じがします。

コメント