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

コンピュータ

画像の一致部分を緑枠で囲むプログラムです。

実行環境構築

プロジェクトの作成

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

ソースプログラム

namespace TemplateMatchingSample;

using OpenCvSharp;
using OpenCvSharp.Extensions;
public partial class Form1 : Form
{
    Mat? templ = null;
    Mat? view = null;

    public Form1()
    {
        InitializeComponent();

        Text = "テンプレートマッチング";
        Size = new System.Drawing.Size(850, 800);

        var fnt = new Font("MS UI Gothic", 12);
        var tb = new Bitmap(100, 100);
        using (var g = Graphics.FromImage(tb)) {
            g.FillRectangle(Brushes.White, 0, 0, tb.Width, tb.Height);
            g.DrawString("こちらにD&D", fnt,  Brushes.Blue, 0.0f, 0.0f);
        }
        var vb = new Bitmap(800, 600);
        using (var g = Graphics.FromImage(vb)) {
            g.FillRectangle(Brushes.White, 0, 0, vb.Width, vb.Height);
            g.DrawString("こちらにD&D", fnt,  Brushes.Green, 0.0f, 0.0f);
        }
        
        var ud = new SplitContainer {
            Dock = DockStyle.Fill,
            Orientation = Orientation.Horizontal,   // 上下
            Panel1MinSize = 120,
        };
        var picboxT = new PictureBox {
            Location = new System.Drawing.Point(10, 10),
            Size = new System.Drawing.Size(tb.Width, tb.Height),
            AllowDrop = true,
            Image = tb,
        };
        var picboxV = new PictureBox {
            Location = new System.Drawing.Point(10, 10),
            Size = new System.Drawing.Size(vb.Width, vb.Height),
            AllowDrop = true,
            Image = vb,
        };
        var execBtn = new RadioButton {
            Text = "フィルターOFF",
            Appearance = Appearance.Button,
            AutoCheck = false,
            Location = new System.Drawing.Point(120, 10),
            Size = new System.Drawing.Size(180, 50),
        };

        picboxT.DragEnter += (sender, e) => {
            if (e.Data == null) return;
            if(!e.Data.GetDataPresent(DataFormats.FileDrop)) return;
            e.Effect = DragDropEffects.Copy;
        };
        picboxT.DragDrop += (sender, e) => {
            if (e.Data == null) return;
            if(!e.Data.GetDataPresent(DataFormats.FileDrop)) return;
            string path = ((string[])e.Data.GetData(DataFormats.FileDrop))[0];
            if (templ is not null) templ.Dispose();
            templ = Cv2.ImRead(path);
            if (picboxT.Image is not null) picboxT.Image.Dispose();
            picboxT.Image = BitmapConverter.ToBitmap(templ);
        };
        picboxV.DragEnter += (sender, e) => {
            if (e.Data == null) return;
            if(!e.Data.GetDataPresent(DataFormats.FileDrop)) return;
            e.Effect = DragDropEffects.Copy;
        };
        picboxV.DragDrop += (sender, e) => {
            if (e.Data == null) return;
            if(!e.Data.GetDataPresent(DataFormats.FileDrop)) return;
            string path = ((string[])e.Data.GetData(DataFormats.FileDrop))[0];
            if (view is not null) view.Dispose();
            view = Cv2.ImRead(path);
            if (picboxV.Image is not null) picboxV.Image.Dispose();
            picboxV.Image = BitmapConverter.ToBitmap(view);
        };
        execBtn.Click += (sender, e) => {
            if (templ is null || view is null) return;
            if (execBtn.Checked == true) {
                if (picboxV.Image is not null) picboxV.Image.Dispose();
                picboxV.Image = BitmapConverter.ToBitmap(view);

                execBtn.Text = "フィルターOff";
                execBtn.Checked = false;
            } else {
                var view_gray = new Mat();
                var templ_gray = new Mat();
                var result = new Mat();
                Cv2.CvtColor(view, view_gray, ColorConversionCodes.RGB2GRAY);
                Cv2.CvtColor(templ, templ_gray, ColorConversionCodes.RGB2GRAY);

                var result_view = (Mat)view.Clone();
                var result_tmp = (Mat)view_gray.Clone();

                while (true) {

                    Cv2.MatchTemplate(result_tmp, templ_gray, result, TemplateMatchModes.CCoeffNormed);

                    double minval, maxval, threshold = 0.8;
                    Point minloc, maxloc;

                    Cv2.MinMaxLoc(result, out minval, out maxval, out minloc, out maxloc);
                    if (maxval >= threshold) {
                        Cv2.Rectangle(result_view, new Rect(maxloc.X, maxloc.Y, templ.Width, templ.Height), new Scalar(0, 255, 0), 3);
                        Cv2.Rectangle(result_tmp, new Rect(maxloc.X, maxloc.Y, templ.Width, templ.Height), new Scalar(0, 0, 255), 3);
                    }
                    else
                        break;

                }
                if (picboxV.Image is not null) picboxV.Image.Dispose();
                picboxV.Image = BitmapConverter.ToBitmap(result_view);

                execBtn.Text = "フィルターOn";
                execBtn.Checked = true;
            }            
        };
        
        ud.Panel1.Controls.Add(picboxV);
        ud.Panel2.Controls.AddRange(new Control[] {picboxT, execBtn});
        Controls.Add(ud);
    }
}

実行

dotnet run

対象画像

テンプレート画像(対象画像からロゴを切り抜き)

フォームを起動

対象画像を上部にテンプレート画像を下部にドラッグアンドドロップ

フィルタボタンを押す。

MatchTemplate()の結果にマッチした位置情報が複数セットされているようですが、筆者には個別に取得する方法を見つけることが出来ませんでした。結果からMinMaxLoc()で最大の位置情報が取得できるようなので、その位置情報から枠線を描画し、枠線を描画した画像にMatchTemplate()を実行、マッチしなくなるまで繰り返しています。
何度もMatchTemplate()をするので効率も悪いですし、オリジナル画像に枠線を加筆するので、マッチングにも影響がありそうです。(テンプレートによっては枠線を描画した部分にもマッチングする可能性も)

コメント