マウスをポチれよ、さすれば与えられん


進捗や技術情報などを外部公開しながら開発を進める「公開型開発」という取り組みがあります。


 
この開発の技術情報をリンク情報システム株式会社の「2021新春アドベントカレンダー TechConnect!」のリレー記事として、紹介したいと思います。
「TechConnect!」は勝手に始めるアドベントカレンダーとして、勝手に作った engineer.hanzomon というグループによって記事をリレーしていくイベントになります。

マウスを操作すれば教えてくれる

  • 仕事が早い人はPC操作も早いことが多いですよね。
  • そんな方はキーワードショートカットを多用していており、マウス操作は必要最小限で済ましています。
  • そんなショートカットを調べるのは億劫ですよね。
  • いつものマウス操作にショートカットがあるのを教えてもらえたら便利ですよね。

そんな悩みを救うアプリの技術情報です。

どうやってマウス操作を判別するか

ショートカットをレコメンドするためには、どのようなマウス操作が行われたかを判別する必要があります。
現在はExcelのみを対象にしてますが、Excel以外のアプリにも対応したいため、汎用的な処理方式が求められます。
つまり、操作イベントを得るために対象アプリのAPIを利用することや、対象アプリに処理を組み込むような方式は最終手段にする必要があります。

①機械学習

クリックされた周辺画像を機械学習の画像分類で判別を行う案です。
この方式は、精度が保証し辛いことと、判別したい操作ごとに多くの学習データを作成するのが大変という問題があります。
特に精度の面は重要で、このアプリにおいては少々筋が悪いです。

②OCR

OCR方式は、クリックされた周辺画像を切り抜き、その画像をOCRにかけて文字列を抽出、判別します。
この方式は、セル、シートのコンテキストメニューの「挿入(I)...」において、OCRで同一の文字列である「挿入(I)...」が得られるため、セルの挿入なのか、シートの挿入なのか、判別できないという問題がありました。
また、コンテキストメニューの選択肢などの文字列が表示されているものには対応できますが、リボン内のアイコンクリックに対応するのは困難です。

③テンプレートマッチング

テンプレートマッチングは、対象画像を走査してテンプレート画像に一致する部分を見つける手法です。
つまり、画面キャプチャで取得したコンテキストメニューの画像と、事前に用意した行、列、セル、シートのコンテキストメニュー画像をマッチングすることで、マウス操作の判別が出来ます。
OCR方式では困難であったアイコンクリックも判別可能です。

テンプレートマッチングはパラダイスの予感がします。

テンプレートマッチングの実装

OpenCVSharpを利用したテンプレートマッチングの例です。

  • 素の画像データ同士でマッチングを行った場合、アイコンクリックの判定精度が低かったです。
  • 二値化を行うことで精度を高めることが出来ました。
  • 識別具合を視覚的に確認し易くするため、マッチング箇所を赤枠で表示します。
  • このプログラムでは、入力を画像ファイルとしていますが、実際のアプリではクリック周辺のBitmapなどを入力として扱います。
OpeJudger.cs
using System;
using System.Collections.Generic;
using System.IO;
using OpenCvSharp;

namespace MyApp
{
    /// <summary>
    /// マウス操作の判定クラス
    /// </summary>
    class OpeJudger
    {
        private const double MatchThreshold = 0.9;

        /// <summary>
        /// どのような操作を行った時の画像か判別する
        /// </summary>
        /// <param name="imgFilePath">判別したい画像ファイルのパス</param>
        /// <returns>マッチしたテンプレファイル名</returns>
        public string Exec(string imgFilePath)
        {
            var targetMat = new Mat(imgFilePath);

            // テンプレート画像で回してマッチするものを探す
            string[] templateFiles = Directory.GetFiles(@".\MatchTemplate", "*", SearchOption.TopDirectoryOnly);
            foreach (var fileName in templateFiles)
            {
                // マッチング
                var templateMat = new Mat(fileName);
                var match = this.Matching(targetMat, templateMat, out var maxPoint);
                // マッチなし
                if (match < 0)
                {
                    continue;
                }

                Console.WriteLine($"マッチしたテンプレ画像={fileName}");

                // 確認用の画像表示
                // マッチした箇所を赤で囲む
                targetMat.Rectangle(maxPoint, new Point(maxPoint.X + templateMat.Width, maxPoint.Y + templateMat.Height), Scalar.Red, 2, LineTypes.AntiAlias, 0);
                // マッチ度を画面上部に表示
                targetMat.Rectangle(new Point(0, 0), new Point(800, 60), Scalar.White, -1, LineTypes.AntiAlias, 0);
                Cv2.PutText(targetMat, match.ToString("0.##"), new Point(0, 50), HersheyFonts.HersheyPlain, 2, Scalar.Black, 1, LineTypes.AntiAlias);
                Cv2.ImShow("確認用", targetMat);
                Cv2.WaitKey(0);

                return fileName;
            }

            return "NoMatch";
        }

        /// <summary>
        /// テンプレートマッチングを実行する
        /// </summary>
        /// <param name="targetMat">探索対象とする画像</param>
        /// <param name="templateMat">テンプレート画像</param>
        /// <param name="matchPoint">探索対象画像内に存在するテンプレート画像の位置</param>
        /// <returns>マッチ度。マッチ無しの場合は負数を返す</returns>
        private double Matching(Mat targetMat, Mat templateMat, out Point matchPoint)
        {
            // 探索画像を二値化
            var targetBinMat = new Mat();
            Cv2.CvtColor(targetMat, targetBinMat, ColorConversionCodes.BGR2GRAY);
            Cv2.Threshold(targetBinMat, targetBinMat, 128, 255, ThresholdTypes.Binary);

            // テンプレ画像を二値化
            var templateBinMat = new Mat();
            Cv2.CvtColor(templateMat, templateBinMat, ColorConversionCodes.BGR2GRAY);
            Cv2.Threshold(templateBinMat, templateBinMat, 128, 255, ThresholdTypes.Binary);

            // マッチング
            var resultMat = new Mat();
            Cv2.MatchTemplate(targetBinMat, templateBinMat, resultMat, TemplateMatchModes.CCoeffNormed);

            // 一番マッチした箇所のマッチ具合(0~1)と、その位置を取得する(画像内でマッチした左上座標)
            Cv2.MinMaxLoc(resultMat, out _, out var maxVal, out _, out matchPoint);
            if (maxVal < MatchThreshold)
            {
                return -1.0;
            }

            // 閾値超えのマッチ箇所を強調させておく
            var binMat = new Mat();
            Cv2.Threshold(resultMat, binMat, MatchThreshold, 1.0, ThresholdTypes.Binary);

            return maxVal;
        }
    }
}

以下の形で利用します。

呼び出し側
var opeJudger = new OpeJudger();
var result = opeJudger.Exec(キャプチャした画像);
用意するテンプレート画像

以下の画像を MatchTemplate フォルダに格納します。
コンテキストメニューには「列の幅」を入れるなど、当該コンテキストメニューの特徴となる部分もテンプレ画像含めることがポイントになります。
コンテキストメニューだけでなく、アイコンクリックも判別出来ようにSave、Boldアイコンも用意しました。

実行結果

マウスクリックされた周辺画像を入力したところ、良好な結果を得ることができました。
きちんとテンプレート画像に合致する箇所が識別され、赤枠で囲まれています。


明日のリレー記事は @o-chang です!


リンク情報システム株式会社では一緒に働く仲間を随時募集しています!
また、お仕事のご依頼、ビジネスパートナー様も募集しております。お気軽にご連絡ください。
Facebook:https://ja-jp.facebook.com/lis.co.jp/
Twitter:https://twitter.com/liscojp/
公開型開発「PRODUCTICA」公式:https://twitter.com/Productica_lis