3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ゼロから作るRPA その2 画像一致によるクリック編(OpenCV)

Posted at

#はじめに
RPAではどんなに頑張ってもクリックさせることができないものとかがよく出てきます。
そんなときの最終手段が、画像一致によるクリックです。
ここでは、その実装方法を説明します。

#概要
OpenCVにはcv::matchTemplateという、テンプレートとそれに重なった画像領域とを比較する関数が用意されています。
その関数を利用し、デスクトップ上からマッチする部分を検出し、最もマッチする部分にマウスカーソルを移動して、クリック動作をします。

意外と簡単ですね。

#環境

  • Windows 10 Pro
  • Visual Studio 2019
  • OpenCV 4.1.1

#OpenCVに関する設定
まずは、OpenCVのサイトからダウンロードします。

https://opencv.org/releases/
image.png

Visual Studio 2019で開発しているので、Windows版をダウンロードします。
opencv-4.1.1-vc14_vc15.exeというファイルがダウンロードされるので、ダブルクリックをしたら解凍が始まります。
image.png
解凍が終わると二つのフォルダが出来上がっているはずです。

次に、ヘッダファイルとライブラリファイルの位置をVisualStudioに設定します。

image.png

ライブラリを、「リンカー」の「入力」の「追加の依存ファイル」に記述します。
Debugビルドは、opencv_world411d.libを、
Releaseビルドは、opencv_world411.libをそれぞれ指定しましょう。

image.png

最後にOpenCVのDLLの位置にパスを通すか、EXEファイルと同じディレクトリにコピーしてください。
ここでは、EXEと同じディレクトリにコピーする例のキャプチャを載せておきます。
image.png

以上で、OpenCVの設定は終わりです。これで、C++から好きなだけOpenCVが使えます。

#OpenCVでの物体検出
前述したとおり、OpenCVでの物体検出には、cv::matchTemplate関数を利用します。

matchTemplate
void matchTemplate(const Mat& image, const Mat& templ, Mat& result, int method)

image – テンプレートの探索対象となる画像 。8ビットまたは32ビットの浮動小数点型。
templ – 探索されるテンプレート画像。
result – 比較結果のマップ。
method – 比較手法の指定

この関数は、imageに指定した画像内で、templ内で指定した画像にどれだけマッチするかを、左上から1ドットずつずらしながらそれぞれ比較し、そのマッチ度合いのパーセンテージをresultに返却します。

実際にやってみましょう!
####ダイアログにボタンを追加する
まずは、画面にボタンを一つ追加します。この辺りは、もう手慣れたもんですね。
image.png

ダイアログコールバックプロシージャに上記ボタンクリック時のメッセージ処理を追加します。

DaphRPApp.cppのDialogProc内
	case WM_COMMAND:
		switch (LOWORD(wp)) 
		{
		case IDC_REC_BTN:
			StartMouseHook(hDlgWnd);
			return FALSE;
		case IDC_PLAY_BTN:
			PlayMouseEvent();
			return FALSE;
		case IDC_IMG_BTN:
			ClickImage();
			return FALSE;
		default:
			return FALSE;
		}

IDC_IMG_BTNに、ClickImageを呼び出す処理を追加しました。
ClickImageに、OpenCVでの物体検知を実装していきます。
####検出用の画像を用意する
今回用意した画像は、以下の画像です。button.pngという名前で保存しました。
image.png
この画像は、Qiitaのヘッダ部分にある「コミュニティ」プルダウンの画像です。クリックに成功したら、プルダウンがペローンって表示されると思います。

####探索対象のデスクトップ取得処理を追加
今回の探索対象は、デスクトップ全体です。デスクトップ全体の画像をプログラム内でキャプチャし、それを探索対象として先ほどの関数のimageに指定します。

まずは、その取得のコードです。

DaphRPApp.cpp
cv::Mat Hwnd2Mat(HWND hwnd, int nPosLeft, int nPosTop, int nDestWidth, int nDestHeight)
{
	HDC hwindowDC = GetDC(hwnd);
	HDC hwindowCompatibleDC = CreateCompatibleDC(hwindowDC);

	HBITMAP           hbwindow = NULL;
	cv::Mat           src;
	BITMAPINFOHEADER  bi;

	SetStretchBltMode(hwindowCompatibleDC, COLORONCOLOR);

	RECT windowsize;
	GetClientRect(hwnd, &windowsize);

	int srcheight = windowsize.bottom;
	int srcwidth = windowsize.right;

	src.create(nDestHeight, nDestWidth, CV_8UC3);

	hbwindow = CreateCompatibleBitmap(hwindowDC, nDestWidth, nDestHeight);
	if (hbwindow == NULL)
	{
		src.data = NULL;
		return src;
	}

	bi.biSize = sizeof(BITMAPINFOHEADER);
	bi.biWidth = nDestWidth;
	bi.biHeight = -nDestHeight;
	bi.biPlanes = 1;
	bi.biBitCount = 24;
	bi.biCompression = BI_RGB;
	bi.biSizeImage = 0;
	bi.biXPelsPerMeter = 0;
	bi.biYPelsPerMeter = 0;
	bi.biClrUsed = 0;
	bi.biClrImportant = 0;

	SelectObject(hwindowCompatibleDC, hbwindow);

	StretchBlt(hwindowCompatibleDC, 0, 0, nDestWidth, nDestHeight, hwindowDC, nPosLeft, nPosTop, nDestWidth, nDestHeight, SRCCOPY);
	GetDIBits(hwindowCompatibleDC, hbwindow, 0, nDestHeight, src.data, (BITMAPINFO*)& bi, DIB_RGB_COLORS);

	if (hbwindow != NULL)
	{
		DeleteObject(hbwindow);
		hbwindow = NULL;
	}

	if (hwindowCompatibleDC != NULL)
	{
		DeleteDC(hwindowCompatibleDC);
		hwindowCompatibleDC = NULL;
	}

	if (hwindowDC != NULL)
	{
		ReleaseDC(hwnd, hwindowDC);
		hwindowDC = NULL;
	}

	return src;
}

このコードは、参考にしたサイトに書いてあるリンクのサイトからおかりしてきました。ありがとうございます。
とはいえ、キャプチャ処理は、Win32での画像処理の基本で、ほかのサイトにもいろいろとサンプルがあります。いつもと違うところは、返却値がcv::Matなので、GetDIBitsの対象が、src.dataになっているところです。

この関数を、先ほど定義したClickImage()関数に追加していきましょう。

####画像の検出の実験
まずは、検出対象の画像ファイルを読み込みます。

ClickImage()
	cv::Mat srcimg = cv::imread("button.png");

次に、探索対象のデスクトップイメージを先ほど作った関数で取得します。

ClickImage()
	int dwidth = GetSystemMetrics(SM_CXSCREEN);
	int dheight = GetSystemMetrics(SM_CYSCREEN);

	dwidth += (4 - (dwidth % 4));

	cv::Mat srcDesktop = Hwnd2Mat(GetDesktopWindow(), 0, 0, dwidth, dheight);

GetSystemMetricsで、現在のデスクトップの幅と高さを取得しています。ビットマップ取得の際は、幅を4の倍数で指定しなければならないため、dwidth += (4 - (dwidth % 4))で取得幅を調整しています。

最後に物体検出関数を呼び出します。

ClickImage()
	cv::Mat result;
	cv::matchTemplate(srcDesktop, srcimg, result, cv::TM_CCORR_NORMED);

ここでは、比較手法にTM_CCORR_NORMED(正規化相互相関)を指定しています。このあたりの詳細はOpenCVのドキュメントを読んでみてください。

これで、resultに、それぞれのドットでのマッチ結果のパーセンテージなどが指定されたマップが返却されたことでしょう。

####本当にマッチしたか、矩形を書いて確認する
マッチ結果のパーセンテージ上位5個の矩形を、デスクトップ上に書いてみましょう。
resultマップ内で、パーセンテージがある閾値を超えるものだけを、パーセンテージをキーにして別のマップに追加します。そうすることで、上から順番に見ることができ、また、対象の数を減らすことができます。

ClickImage()
	std::map<float, cv::Point, std::greater<float> > dectetedPointMap;
	const float threshold = 0.95f;     // TODO:ここも指定できるようにしたい

	for (int y = 0; y < result.rows; ++y)
	{
		for (int x = 0; x < result.cols; ++x)
		{
			if (result.at<float>(y, x) > threshold)
			{
				if (dectetedPointMap.find(result.at<float>(y, x)) != dectetedPointMap.end())
					continue;

				dectetedPointMap.insert(std::pair<float, cv::Point>(result.at<float>(y, x), cv::Point(x, y)));
			}
		}
	}

dectetedPointMapに、パーセンテージが0.95を超えるものだけが追加されました。
上位5つの矩形を書きます。

ClickImage()
	HWND hwnd = GetDesktopWindow();
	HDC hdc = GetDC(hwnd);
	int count = 0;

	HBRUSH hOldBrush = (HBRUSH)SelectObject(hdc, GetStockObject(NULL_BRUSH));
	HPEN hNewPen = (HPEN)CreatePen(PS_INSIDEFRAME, 4, RGB(0x99, 0x66, 0x00));
	HPEN hOldPen = (HPEN)SelectObject(hdc, hNewPen);

	for (auto i : dectetedPointMap)
	{
		// 四角を書く
		Rectangle(hdc, i.second.x, i.second.y, i.second.x + srcimg.cols, i.second.y + srcimg.rows);
		if (++count > 5)
			break;
	}

	SelectObject(hdc, hOldBrush);
	SelectObject(hdc, hOldPen);
	DeleteObject(hNewPen);
	ReleaseDC(hwnd, hdc);

dectetedPointMapの上位5個に対して、そのxとyから、探索する画像の幅と高さ分だけの矩形を、Rectangle関数で描画しています。

実際に動かしてみると、いくつかの個所が矩形で囲われています。これが、OpenCVがマッチした上位5カ所です。

image.png

ちょっと場所をずらしてみましょう。
場所をずらしても、きちんと、「コミュニティ」の場所を認識しています。

image.png

真ん中あたりにしてみましたが、大丈夫ですね。

#検出した画像部分をクリック
矩形を描画するコードをコメントアウトして、検出した画像の中央をマウスでクリックするコードを記述します。
マウスのクリックは、その1の4でやったマウスの再現の時に使ったSendInput関数を使えば簡単にできます。
マウスのクリックなので、DOWNとUPの両方を一度に指定しています。

ClickImage()
	if (dectetedPointMap.size() > 0)
	{
		int clickX = dectetedPointMap.cbegin()->second.x + srcimg.cols / 2;
		int clickY = dectetedPointMap.cbegin()->second.y + srcimg.rows / 2;

		INPUT inp[2] = { 0 };
		inp[0].type = INPUT_MOUSE;
		inp[0].mi.time = 0;
		inp[0].mi.dwExtraInfo = 0;
		inp[0].mi.dx = clickX * (65535 / GetSystemMetrics(SM_CXSCREEN));
		inp[0].mi.dy = clickY * (65535 / GetSystemMetrics(SM_CYSCREEN));
		inp[0].mi.mouseData = 0;
		inp[0].mi.dwFlags = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE | MOUSEEVENTF_LEFTDOWN;
		inp[1].type = INPUT_MOUSE;
		inp[1].mi.time = 0;
		inp[1].mi.dwExtraInfo = 0;
		inp[1].mi.dx = 0;
		inp[1].mi.dy = 0;
		inp[1].mi.mouseData = 0;
		inp[1].mi.dwFlags = MOUSEEVENTF_LEFTUP;

		SendInput(2, inp, sizeof(INPUT));
	}

想定通り、ドロップダウンがペローンってなりました。
image.png

#動画で見てみましょう
画像では、楽しさの伝わり方がいまいちなので、動画に撮って見ました。
いくつかの場所に移動してみて、それぞれでペローンってなるかを実験しています。

Youtube
https://youtu.be/EAod0QlvcJk

#つづく
OpenCVを使った画像クリック機能があれば、ほかの方法で認識できないような場所でも確実にクリックすることができるようになります。
ただ、解像度などが変更された場合にクリックできなくなったりするかもしれません。そのあたりは別途実験してみたいと思います。

ゼロから作るRPA その1
https://qiita.com/yasunari_matsuo/items/b1e56ad06c6a7843dfae
https://qiita.com/yasunari_matsuo/items/a1d294f09ac21e9508b6
https://qiita.com/yasunari_matsuo/items/14fe987a162936f7a1ae
https://qiita.com/yasunari_matsuo/items/8a528066cda871c345d4

#参考にしたサイト
http://opencv.jp/opencv-2svn/cpp/imgproc_object_detection.html
https://taiyakisun.hatenablog.com/entry/2018/09/11/233728

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?