まえがき
以前、こんな記事を書いたことがあります。
色々な方法でWindowsのGUIの自動操作を行う方法を記載しましたが、PowerShellで画像認識を利用した自動操作については逃げました。
今回は宿題として残っていたPowerShellとOpenCVを使用して画像認識での自動操作を行ってみます。
考え方としてはスクリーンキャプチャした内容をMatに変換してTemplate Matchingを行うだけです。
OpenCVの.NET用のラッパー
OpenCVには.NET用のラッパーとしてOpenCvSharpが存在します。
https://github.com/shimat/opencvsharp/releases
このライブラリをNugetまたは上記のページからダウンロードしてください。
注意点として、ネイティブのDLLを使うことになるので32bit、64bitのどちらのプロセスで動作しているか、意識してDLLを利用してください。
C#のサンプル
VisualStudio 2019の.NET Framework4.0で作成したサンプルは以下のようになります。
using OpenCvSharp;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace OpenCv
{
public class GuiAuto
{
// https://culage.hatenablog.com/entry/20130611/1370876400
[DllImport("user32.dll")]
extern static uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
[StructLayout(LayoutKind.Sequential)]
struct INPUT
{
public int type;
public MOUSEINPUT mi;
}
[StructLayout(LayoutKind.Sequential)]
struct MOUSEINPUT
{
public int dx;
public int dy;
public int mouseData;
public int dwFlags;
public int time;
public IntPtr dwExtraInfo;
}
const int MOUSEEVENTF_LEFTDOWN = 0x0002;
const int MOUSEEVENTF_LEFTUP = 0x0004;
static public void Click()
{
//struct 配列の宣言
INPUT[] input = new INPUT[2];
//左ボタン Down
input[0].mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
//左ボタン Up
input[1].mi.dwFlags = MOUSEEVENTF_LEFTUP;
//イベントの一括生成
SendInput(2, input, Marshal.SizeOf(input[0]));
}
static public void Move(int x, int y)
{
var pt = new System.Drawing.Point(x, y);
System.Windows.Forms.Cursor.Position = pt;
}
public class TemplateResult
{
public int TargetWidth { set; get; }
public int TargetHeight { set; get; }
public List<OpenCvSharp.Point> MatchList { set; get; }
public TemplateResult()
{
this.MatchList = new List<OpenCvSharp.Point>();
}
}
static public TemplateResult MatchTemplate(int ScreenNo, string targetPath, double threshold)
{
TemplateResult result = new TemplateResult();
var screen = Screen.AllScreens[ScreenNo];
Bitmap bitmap = new Bitmap(screen.Bounds.Width, screen.Bounds.Height);
Graphics graphics = Graphics.FromImage(bitmap as Image);
graphics.CopyFromScreen(screen.Bounds.X, screen.Bounds.Y, 0, 0, bitmap.Size);
using (var targetImg = Cv2.ImRead(targetPath))
using (var img = OpenCvSharp.Extensions.BitmapConverter.ToMat(bitmap))
using (var img3ch = img.CvtColor(ColorConversionCodes.BGRA2BGR))
{
result.TargetWidth = targetImg.Width;
result.TargetHeight = targetImg.Height;
var tmplRet = img3ch.MatchTemplate(targetImg, TemplateMatchModes.CCoeffNormed);
double minVal, maxVal;
OpenCvSharp.Point minLoc, maxLoc;
tmplRet.MinMaxLoc(out minVal, out maxVal, out minLoc, out maxLoc);
Mat thresholdRet = tmplRet.Threshold(threshold, 1.0, ThresholdTypes.Tozero);
while (true)
{
thresholdRet.MinMaxLoc(out minVal, out maxVal, out minLoc, out maxLoc);
if (maxVal < threshold)
{
break;
}
result.MatchList.Add(maxLoc);
thresholdRet.FloodFill(maxLoc, 0);
}
}
return result;
}
static public bool ClickImg(int ScreenNo, string targetPath, double threshold, int offsetX, int offsetY)
{
TemplateResult tmplRet = MatchTemplate(ScreenNo, targetPath, threshold);
if (tmplRet.MatchList.Count == 0)
{
return false;
}
var screen = Screen.AllScreens[ScreenNo];
Move(screen.Bounds.X + tmplRet.MatchList[0].X, screen.Bounds.Y + tmplRet.MatchList[0].Y);
Click();
return true;
}
static public bool ClickImg(int ScreenNo, string targetPath, double threshold)
{
TemplateResult tmplRet = MatchTemplate(ScreenNo, targetPath, threshold);
if (tmplRet.MatchList.Count == 0)
{
return false;
}
var screen = Screen.AllScreens[ScreenNo];
Move(screen.Bounds.X + tmplRet.MatchList[0].X + tmplRet.TargetWidth/ 2, screen.Bounds.Y + tmplRet.MatchList[0].Y + tmplRet.TargetHeight / 2);
Click();
return true;
}
}
class Program
{
static void Main(string[] args)
{
Console.ReadLine();
var targetPath = @"target.bmp";
GuiAuto.ClickImg(0, targetPath, 0.75);
}
}
}
このサンプルはスクリーン上に存在するtarget.bmpの画像を検索してクリックするものとなっています。
やっている内容としてはOpenCvのチュートリアルのTemplate Matchingと似たようなことです。
MatchTemplateは複数の類似画像の位置を取得できるようにFloodFillを実施してループしていますが、常に最も一致した画像だけを取得するならループは不要です。
あとは、取得した位置をもとにマウスを移動してクリックしています。
なお、マルチディスプレイを考慮しているので、ClickImgのScreenNoを変更することで別のスクリーンを検索することが可能です。
スクリーン上の画像の取得は.NETのよくあるキャプチャ処理で、取得したBitmapオブジェクトはOpenCvSharp.Extensions.BitmapConverter.ToMatで行っています。
OpenCvSharpは.NET2.0でも動作するのですが、どうも.NET2.0ではOpenCvSharp.Extensions.dllを提供していないようです。
自前でBitmapConvert.csと同様な処理を実装すればできるかもしれませんが、.NET3.5までは簡単にできましたが、.NET2.0ではうまくいきませんでした。
PowerShell 5.1の例
Windows10 Home + PowerShell5.1でもC#と同様のことが行えます。
OpenCvSharpExtern.dllは使用するPowerShellがx86の場合はx86,x64の場合はx64を使用してください。
次に以下のようなスクリプトを記述して実行します。
$source = @"
using OpenCvSharp;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;
public class GuiAuto
{
// https://culage.hatenablog.com/entry/20130611/1370876400
[DllImport("user32.dll")]
extern static uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
[StructLayout(LayoutKind.Sequential)]
struct INPUT
{
public int type;
public MOUSEINPUT mi;
}
[StructLayout(LayoutKind.Sequential)]
struct MOUSEINPUT
{
public int dx;
public int dy;
public int mouseData;
public int dwFlags;
public int time;
public IntPtr dwExtraInfo;
}
const int MOUSEEVENTF_LEFTDOWN = 0x0002;
const int MOUSEEVENTF_LEFTUP = 0x0004;
static public void Click()
{
//struct 配列の宣言
INPUT[] input = new INPUT[2];
//左ボタン Down
input[0].mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
//左ボタン Up
input[1].mi.dwFlags = MOUSEEVENTF_LEFTUP;
//イベントの一括生成
SendInput(2, input, Marshal.SizeOf(input[0]));
}
static public void Move(int x, int y)
{
var pt = new System.Drawing.Point(x, y);
System.Windows.Forms.Cursor.Position = pt;
}
public class TemplateResult
{
public int TargetWidth { set; get; }
public int TargetHeight { set; get; }
public List<OpenCvSharp.Point> MatchList { set; get; }
public TemplateResult()
{
this.MatchList = new List<OpenCvSharp.Point>();
}
}
static public TemplateResult MatchTemplate(int ScreenNo, string targetPath, double threshold)
{
TemplateResult result = new TemplateResult();
var screen = Screen.AllScreens[ScreenNo];
Bitmap bitmap = new Bitmap(screen.Bounds.Width, screen.Bounds.Height);
Graphics graphics = Graphics.FromImage(bitmap as Image);
graphics.CopyFromScreen(screen.Bounds.X, screen.Bounds.Y, 0, 0, bitmap.Size);
using (var targetImg = Cv2.ImRead(targetPath))
using (var img = OpenCvSharp.Extensions.BitmapConverter.ToMat(bitmap))
using (var img3ch = img.CvtColor(ColorConversionCodes.BGRA2BGR))
{
result.TargetWidth = targetImg.Width;
result.TargetHeight = targetImg.Height;
var tmplRet = img3ch.MatchTemplate(targetImg, TemplateMatchModes.CCoeffNormed);
double minVal, maxVal;
OpenCvSharp.Point minLoc, maxLoc;
tmplRet.MinMaxLoc(out minVal, out maxVal, out minLoc, out maxLoc);
Mat thresholdRet = tmplRet.Threshold(threshold, 1.0, ThresholdTypes.Tozero);
while (true)
{
thresholdRet.MinMaxLoc(out minVal, out maxVal, out minLoc, out maxLoc);
if (maxVal < threshold)
{
break;
}
result.MatchList.Add(maxLoc);
thresholdRet.FloodFill(maxLoc, 0);
}
}
return result;
}
static public bool ClickImg(int ScreenNo, string targetPath, double threshold, int offsetX, int offsetY)
{
TemplateResult tmplRet = MatchTemplate(ScreenNo, targetPath, threshold);
if (tmplRet.MatchList.Count == 0)
{
return false;
}
var screen = Screen.AllScreens[ScreenNo];
Move(screen.Bounds.X + tmplRet.MatchList[0].X, screen.Bounds.Y + tmplRet.MatchList[0].Y);
Click();
return true;
}
static public bool ClickImg(int ScreenNo, string targetPath, double threshold)
{
TemplateResult tmplRet = MatchTemplate(ScreenNo, targetPath, threshold);
if (tmplRet.MatchList.Count == 0)
{
return false;
}
var screen = Screen.AllScreens[ScreenNo];
Move(screen.Bounds.X + tmplRet.MatchList[0].X + tmplRet.TargetWidth/ 2, screen.Bounds.Y + tmplRet.MatchList[0].Y + tmplRet.TargetHeight / 2);
Click();
return true;
}
}
"@
$dllPath = Split-Path $MyInvocation.MyCommand.Path
Set-Item Env:Path "$Env:Path;$dllPath"
Write-Host $currentDir
$assemblies = @(
"$dllPath\OpenCVSharp.dll",
"$dllPath\OpenCvSharp.Extensions.dll",
"System.Runtime",
"System.Windows.Forms",
"System.Drawing"
)
Add-Type -TypeDefinition $source -ReferencedAssemblies $assemblies
Add-Type -Path "$dllPath\OpenCVSharp.dll"
Add-Type -Path "$dllPath\OpenCVSharp.Extensions.dll"
[GuiAuto]::ClickImg(0, "C:\dev\ps\opencv\target.bmp", 0.75)
実行結果
初期状態のWindows7のPowerShellでできないか?
難しいです。
理由として初期状態のWindows7では.NET3.5とPowerShell2.0が入っていますが、このPowerShell2.0はどんな新しい.NET Frameworkが入っていても.NET2.0を使用してしまいます。
PowerShellでdllを読み込む際の注意点
https://qiita.com/icoxfog417/items/e0d29bed109071888f19
このため、BitmapConvert.csと同様の処理が、うまく実装できませんでした。
やるなら、.NET Framework3.5でコマンドラインツールを作成して、PowerShellから呼び出す用な形になると思います(当然、起動時にオーバーヘッドがかかります)
まとめ
画像認識とかいうと難しく考えがちですが、OpenCvを利用すれば、わりと簡単に画像を利用した自動操作を自前でつくれます。
ただし、あまり古すぎる環境だと辛いです。