#はじめに
RPAではどんなに頑張ってもクリックさせることができないものとかがよく出てきます。
そんなときの最終手段が、画像一致によるクリックです。
ここでは、その実装方法を説明します。
#概要
OpenCVにはcv::matchTemplate
という、テンプレートとそれに重なった画像領域とを比較する関数が用意されています。
その関数を利用し、デスクトップ上からマッチする部分を検出し、最もマッチする部分にマウスカーソルを移動して、クリック動作をします。
意外と簡単ですね。
#環境
- Windows 10 Pro
- Visual Studio 2019
- OpenCV 4.1.1
#OpenCVに関する設定
まずは、OpenCVのサイトからダウンロードします。
Visual Studio 2019で開発しているので、Windows版をダウンロードします。
opencv-4.1.1-vc14_vc15.exe
というファイルがダウンロードされるので、ダブルクリックをしたら解凍が始まります。
解凍が終わると二つのフォルダが出来上がっているはずです。
次に、ヘッダファイルとライブラリファイルの位置をVisualStudioに設定します。
ライブラリを、「リンカー」の「入力」の「追加の依存ファイル」に記述します。
Debugビルドは、opencv_world411d.lib
を、
Releaseビルドは、opencv_world411.lib
をそれぞれ指定しましょう。
最後にOpenCVのDLLの位置にパスを通すか、EXEファイルと同じディレクトリにコピーしてください。
ここでは、EXEと同じディレクトリにコピーする例のキャプチャを載せておきます。
以上で、OpenCVの設定は終わりです。これで、C++から好きなだけOpenCVが使えます。
#OpenCVでの物体検出
前述したとおり、OpenCVでの物体検出には、cv::matchTemplate関数を利用します。
void matchTemplate(const Mat& image, const Mat& templ, Mat& result, int method)
image – テンプレートの探索対象となる画像 。8ビットまたは32ビットの浮動小数点型。
templ – 探索されるテンプレート画像。
result – 比較結果のマップ。
method – 比較手法の指定
この関数は、image
に指定した画像内で、templ
内で指定した画像にどれだけマッチするかを、左上から1ドットずつずらしながらそれぞれ比較し、そのマッチ度合いのパーセンテージをresultに返却します。
実際にやってみましょう!
####ダイアログにボタンを追加する
まずは、画面にボタンを一つ追加します。この辺りは、もう手慣れたもんですね。
ダイアログコールバックプロシージャに上記ボタンクリック時のメッセージ処理を追加します。
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
という名前で保存しました。
この画像は、Qiitaのヘッダ部分にある「コミュニティ」プルダウンの画像です。クリックに成功したら、プルダウンがペローンって表示されると思います。
####探索対象のデスクトップ取得処理を追加
今回の探索対象は、デスクトップ全体です。デスクトップ全体の画像をプログラム内でキャプチャし、それを探索対象として先ほどの関数のimage
に指定します。
まずは、その取得のコードです。
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()
関数に追加していきましょう。
####画像の検出の実験
まずは、検出対象の画像ファイルを読み込みます。
cv::Mat srcimg = cv::imread("button.png");
次に、探索対象のデスクトップイメージを先ほど作った関数で取得します。
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))
で取得幅を調整しています。
最後に物体検出関数を呼び出します。
cv::Mat result;
cv::matchTemplate(srcDesktop, srcimg, result, cv::TM_CCORR_NORMED);
ここでは、比較手法にTM_CCORR_NORMED(正規化相互相関)を指定しています。このあたりの詳細はOpenCVのドキュメントを読んでみてください。
これで、resultに、それぞれのドットでのマッチ結果のパーセンテージなどが指定されたマップが返却されたことでしょう。
####本当にマッチしたか、矩形を書いて確認する
マッチ結果のパーセンテージ上位5個の矩形を、デスクトップ上に書いてみましょう。
result
マップ内で、パーセンテージがある閾値を超えるものだけを、パーセンテージをキーにして別のマップに追加します。そうすることで、上から順番に見ることができ、また、対象の数を減らすことができます。
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つの矩形を書きます。
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カ所です。
ちょっと場所をずらしてみましょう。
場所をずらしても、きちんと、「コミュニティ」の場所を認識しています。
真ん中あたりにしてみましたが、大丈夫ですね。
#検出した画像部分をクリック
矩形を描画するコードをコメントアウトして、検出した画像の中央をマウスでクリックするコードを記述します。
マウスのクリックは、その1の4でやったマウスの再現の時に使ったSendInput
関数を使えば簡単にできます。
マウスのクリックなので、DOWNとUPの両方を一度に指定しています。
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));
}
#動画で見てみましょう
画像では、楽しさの伝わり方がいまいちなので、動画に撮って見ました。
いくつかの場所に移動してみて、それぞれでペローンってなるかを実験しています。
ゼロから作るRPA
— teamm (@teamm89201677) October 19, 2019
OpenCVで画像をクリックしてみた
https://t.co/1qrUxtfzmL via @YouTube
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