AutoItで画像の曖昧検索がしたい
UWSCにはchkimgという画像検索関数が標準で用意されていました。また有志の方がchkimgxという画像の曖昧検索を行う外部ライブラリを作成されていたようですが、アップローダーなどでのみ配布されていたようで現在では入手が難しくなっています。
一方、AutoItには画像検索をする標準関数が存在せず、searchImageという外部ライブラリを使う必要があるようです。
しかしこのsearchImage、2008年頃までしかメンテされていないようで、自分の環境でもいまいちちゃんと動いてくれず、単にdllの配置場所を間違えてたかもしれない件は置いといて、まぁ自分で画像マッチング出来た方が曖昧さ加減を調整出来たり、判定方法を自由にカスタマイズ出来たり、今後のことを考えると自作した方がいいかなぁなんて思いまして、OpenCVを触ってみることにしました。
こちらの環境です。
OS | Windows 10 |
コンパイラ | Visual Studio Community 2017 |
CMake | 3.12.1(64-bit版) |
OpenCV, OpenCV contrib | 3.4.3 |
AutoIt | v3.3.14.5 |
Visual Studio Community 2017のインストール
なにはともあれ、コンパイラをインストールします。
CMakeのインストール
OpenCVのビルドには、CMakeも必要です。インストールします。
ご自分の環境合わせてインストールしてください。自分の場合は「cmake-3.12.2-win64-x64.msi」でした。
https://cmake.org/download/
CMakeインストール中に「Add CMake to the system PATH for all users」を選択して、環境変数PathにCMakeのパスを追加してください。
↑を忘れてしまった場合は、手動で環境変数Pathに以下のCMakeのパスを追加してください。
C:\Program Files\CMake\bin
OpenCV, OpenCV contribのビルド
まずは以下からソースコードをダウンロードしましょう。
OpenCV contrib
Releases · opencv/opencv_contrib · GitHub
「zip」と書かれたリンクをクリックしてダウンロードします。
上記ページのopencvのファイル名(例:opencv-3.4.3-vc14_vc15.exe)にあるvc14とかvc15とかが、
Visual Studioのバージョンに対応しているので、今後バージョンが上がった場合は注意して確認してみてください。
vc14 | Visual Studio 2015 |
vc15 | Visual Studio 2017 |
OpenCVのビルド
ここでは例として、「C:\temp」に上記ファイルをダウンロードします。
「opencv-3.4.3-vc14_vc15.exe」をダブルクリックすると「opencv」というフォルダ名で展開されます。
「opencv_contrib-3.4.3.zip」をダブルクリックすると「opencv_contrib-3.4.3」というフォルダ名で展開されます。
[スタート]メニューからcmake-guiを探して起動します。
[Where is the source code]に「C:/temp/opencv/sources」を指定します。
[Where to build the binaries]に「C:/temp/opencv/build」を指定します。
手動でパスを入力する場合、パス区切り文字は「/」にしてください。
入力したら[Configure]ボタンを押下します。
今回は、AutoItをデフォルト32-bitで動作させているので、OpenCVも32-bitでビルドします。
そのため今回は「Win64が付いていない」プロジェクトを選択します。
再びCMake-guiの画面に戻ります。
32-bit版の場合、cuda関連のビルドが通らないという噂がありますので、「WITH_CUDA」のチェックを外します(最近のは最初から外れてるかも)
また「OPENCV_EXTRA_MODULES_PATH」に「C:/temp/opencv_contrib-3.4.3/modules」を指定します。
何度も言いますが、パス区切り文字は「/」にしてください。
コンソールに「Configuring done」が出力されたら成功です。
続いて「Generate」ボタンを押下します。
C:\temp\opencv\build直下にOpenCV.slnが出来ているので、Visual Studioで開きます。
INSTALLプロジェクトをReleaseビルドしてください。
出力コンソールにこんな感じの表示が出れば成功です。
========== すべてリビルド: 134 正常終了、0 失敗、0 スキップ ==========
AutoItで使用するテンプレートマッチングを行うDLLの作成
Visual Studioで新規プロジェクトを作成します。
DLLのプロジェクトを作るか、プロジェクトの[構成プロパティ]→[全般]→[構成の種類]を「ダイナミック ライブラリ(.dll)」などにしてください。
同じプロジェクトのプロパティページの[C/C++]→[全般]→[追加のインクルードディレクトリ]に以下を入れます。
C:\temp\opencv\build\install\include
[リンカー]→[全般]→[追加のライブラリディレクトリ]に以下を入れます。
C:\temp\opencv\build\install\x86\vc15\lib
#include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> #include <map> using namespace std; #include <Windows.h> /** * @brief ウィンドウハンドルhwndのイメージcv::Matを作成する。 * @param [in] hwnd ウィンドウハンドル。デスクトップを対象にしたい場合、GetDesktopWindow()の戻り値を渡すべし。 * @param [in] nPosLeft 検索対象となる左上のx座標 * @param [in] nPosTop 検索対象となる左上のy座標 * @param [in] nDestWidth 検索対象となる範囲のnPosLeftからの幅 * @param [in] nDestHeight 検索対象となる範囲のnPosTopからの高さ */ 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; // get the height and width of the screen GetClientRect(hwnd, &windowsize); // ここはデスクトップの幅と高さ int srcheight = windowsize.bottom; int srcwidth = windowsize.right; src.create(nDestHeight, nDestWidth, CV_8UC3); // create a bitmap hbwindow = CreateCompatibleBitmap(hwindowDC, nDestWidth, nDestHeight); if (hbwindow == NULL) { src.data = NULL; return src; } bi.biSize = sizeof(BITMAPINFOHEADER); // http://msdn.microsoft.com/en-us/library/windows/window/dd183402%28v=vs.85%29.aspx 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; // use the previously created device context with the bitmap SelectObject(hwindowCompatibleDC, hbwindow); // copy from the window device context to the bitmap device context StretchBlt(hwindowCompatibleDC, 0, 0, nDestWidth, nDestHeight, hwindowDC, nPosLeft, nPosTop, nDestWidth, nDestHeight, SRCCOPY); // change SRCCOPY to NOTSRCCOPY for wacky colors ! GetDIBits(hwindowCompatibleDC, hbwindow, 0, nDestHeight, src.data, (BITMAPINFO *)&bi, DIB_RGB_COLORS); // copy from hwindowCompatibleDC to hbwindow // avoid memory leak if (hbwindow != NULL) { DeleteObject(hbwindow); hbwindow = NULL; } if (hwindowCompatibleDC != NULL) { DeleteDC(hwindowCompatibleDC); hwindowCompatibleDC = NULL; } if (hwindowDC != NULL) { ReleaseDC(hwnd, hwindowDC); hwindowDC = NULL; } return src; } extern "C" { /** * @brief 教師画像が指定されたデスクトップ範囲に存在するかあいまい検索(テンプレートマッチング)する * @param [in] pszFilePath 教師(テンプレート)画像のフルパス * @param [in] nPosLeft 検索対象となるデスクトップの左上のx座標 * @param [in] nPosTop 検索対象となるデスクトップの左上のy座標 * @param [in] nDestWidth 検索対象となる範囲のnPosLeftからの幅 * 4の倍数でないと必ずマッチングしないため、4の倍数に増やす方向で調整をする。 * @param [in] nDestHeight 検索対象となる範囲のnPosTopからの高さ * @param [out] pDetectedPosX 検知した場合のX座標。検知してない場合なにも入らない。 * @param [out] pDetectedPosY 検知した場合のY座標。検知してない場合なにも入らない。 * @retval 0 正常終了(見つかった) * @retval 1 正常終了(見つからなかった) * @retval 10 教師画像ロード失敗 * @retval 11 デスクトップ情報確保失敗 * @retval 12 テンプレートマッチングでエラー */ DllExport int searchImg(const char* pszFilePath, int nPosLeft, int nPosTop, int nDestWidth, int nDestHeight, int* pDetectedPosX, int* pDetectedPosY) { int nActualWidth = nDestWidth + (4 - (nDestWidth % 4)); // 4の倍数に増やす方向で合わせる cv::Mat template_image = cv::imread(pszFilePath); if (template_image.data == NULL) { return 10; } cv::Mat srcDesktop = hwnd2mat(GetDesktopWindow(), nPosLeft, nPosTop, nActualWidth, nDestHeight); if (srcDesktop.data == NULL) { return 11; } cv::Mat result; try { cv::matchTemplate(srcDesktop, template_image, result, cv::TM_CCORR_NORMED); } catch (cv::Exception ex) { return 12; } // Multiple results // 最もそれっぽい画像選ぶためのMap。同じ値の場合最初に検知したものを優先するため、insert自体しないこととする。 // 比較をgreator<float>にすることにより降順に並ぶ。 map<float, cv::Point, greater<float> > dectetedPointMap; const float threshold = 0.981f; // 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()) { // すでに同じ値がある場合、最初に見つかったものを採用するため、mapには追加しない continue; } dectetedPointMap.insert(pair<float, cv::Point>(result.at<float>(y, x), cv::Point(x,y))); } } } if (dectetedPointMap.empty()) { // 見つからなかった return 1; } else { // 最も似ている点(複数ある場合は最初に見つかった点)を採用する *pDetectedPosX = (dectetedPointMap.cbegin()->second.x + template_image.cols / 2); *pDetectedPosY = (dectetedPointMap.cbegin()->second.y + template_image.rows / 2); return 0; } } }
とりあえず複数見つかった場合は以下のルールに従って、単一の結果を返すようにします。返すデータはマッチングした画像の中心座標です。
- もっとも類似度の高いもの
- 最初に見つかったもの
ビルドしてdllを作成します。
includeやlibの設定が間違っていなければビルドが通るはず(通らなかったらごめんなさい)。
AutoItからDLLを呼び出す
AutoItのインストール手順などは省略します。
適当にAutoItのファイルを作成し、以下のDLLをAutoItのファイルと同じ位置に配置します。
上記で作成した自作のDLL |
opencv_ccalib342.dll |
opencv_core342.dll |
opencv_highgui342.dll |
opencv_imgcodecs342.dll |
opencv_imgproc342.dll |
AutoItのコードは以下です。
まずはDLL関数のラッパー関数。
; @param [in] $hDll DllOpen()済のハンドル。ここファイル名を渡すこともできるが、そうすると毎回オープン/クローズ ; するせい(?)なのか、連続して呼び出すとアプリケーションエラーが発生するので、必ずハンドルを渡すこと。 ; @param [in] $x デスクトップ上のx座標 ; @param [in] $y デスクトップ上のy座標 ; @param [in] $width $xからの幅 ; @param [in] $height $yからの幅 ; @retval -1 DllCallでエラー発生 ; @retval 0 イメージが正常に判定し、見つかった。イメージの中心座標が$detectedXと$detectedYに格納される。 ; @retval 1 イメージを正常に判定し、見つからなかった ; @retval 10 イメージのファイルが見つからなかった ; @retval 11 デスクトップ画面の取得で失敗 ; @retval 12 マッチング処理でエラー発生 Func searchImageCore($hDll, $imgFileName, $x, $y, $width, $height, ByRef $detectedX, ByRef $detectedY) Dim $ret = DllCall($hDll, _ "int:cdecl", _ "searchImg", _ "str", $imgFileName, _ "int", $x, _ "int", $y, _ "int", $width, _ "int", $height, _ "int*", $detectedX, _ "int*", $detectedY) If (@error <> 0) Then ConsoleWrite( "DllCall error!." & @LF ) MsgBox(0, "error", "DllCall returned " & @error & " imgFile=" & $imgFileName & @CRLF) Return -1 EndIf ; 0 -> 見つかった ; 1 -> 見つからなかった ; その他 -> エラー発生 ; 検知した座標はおそらく($x,$y)からのもの。今回ほしいのは実際のデスクトップ上の座標なので、加算して格納する。 If ($ret[0] = 0) Then $detectedX = $x + $ret[6] $detectedY = $y + $ret[7] EndIf Return $ret[0] EndFunc ;==>searchImageCore
次に、ラッパー関数を呼び出すAutoItコード。
Global $g_hDll = DllOpen( "<自作のDLLファイル名>.dll" ) Local detectedX = 0 Local detectedY = 0 Local $ret = searchImageCore($g_hDll, _ "abc.bmp", _ 0, _ 0, _ 1920, _ 1080, _ $detectedX, _ $detectedY) If ($ret = 0) Then ; 見つかった ConsoleWrite("searchImage detected! dx=" & $detectedX & " dy=" & $detectedY & @CRLF) Else ; 普通に見つからなかったか、なんらかのエラー発生。見つからなかったとして帰る。 If ($ret <> 1) Then ; 普通に見つからない場合以外はログを出しておく ConsoleWriteError("[Error] searchImage returned " & $ret & " imgFile=" & $imgFile & @CRLF) EndIf EndIf
思ったより長くなってしまった。あぁ疲れた…。