#はじめに
「前回」の記事では、YOLOv3のWindowsへのインストールと、数枚のデスクトップ画像を使用した訓練を説明しました。
さらに、「この投稿」でpythonからYOLOv3(darknet)を呼び出すWindowsのDLLをビルドし、darknet.pyでそれをどのように呼び出しているかを説明しました。
今回は、そのDLLをC++から呼び出して、デスクトップのアイコンの位置を判別し、すべてをゴミ箱に移動するプログラムを作成したいと思います。
※強化学習によるデスクトップアイコン削除の投稿は「こちら」です。
#前提
過去の記事に示した以下のことが出来上がってる前提で話を進めます。
- CUDA、cuDNN、OpenCVがインストールされていること
-
yolo_cpp_dll.dll
のビルドが済んでいること - デスクトップアイコンの訓練結果のモデルがあること
#環境
- Windows10 Pro
- Visual Studio 2019
- CUDA 10.0
- cuDNN 7.3.0.29
- OpenCV 4.1.1
#VisualStudioでプロジェクトを作る
定期的にデスクトップを監視して、デスクトップにアイコンが置かれたら、YOLOv3がそのアイコンを検知、そしてマウスが自動的にアイコンをゴミ箱に移動する。
というアプリケーションを作りたいので、タスクトレイ常駐型のアプリケーションを作成します。
####新規プロジェクト作成
「Windowsデスクトップアプリケーション」を選んで次へをクリックしてプロジェクトを作成してください。
####タスクトレイ用アイコンの登録
タスクトレイ用のアイコンをプロジェクトに追加します。アイコンは今マイブームのICOOON MONOから借用します。
まずは、プロジェクトで右クリックして表示されたメニューで「追加」-「リソース」を選択します。
「Icon」を選択して、「新規作成」をクリックします。
いい感じに、編集します。大きさ的に32×32 8ビットを編集します。
残りのアイコンを全部消します。消すときは、右クリックして「イメージタイプの削除」を選択してください。
####タスクトレイコードの追加
タスクトレイに追加するには、Shell_NotifyIcon
関数を使用します。
BOOL Shell_NotifyIcon(
_In_ DWORD dwMessage,
_In_ PNOTIFYICONDATA lpdata
);
タスクトレイに追加するには、dwMessage
にNIM_ADD
を指定します。
NOTIFYICONDATA
を使用するには、Shellapi.h
をインクルードします。
#include <Shellapi.h>
NOTIFYICONDATA
に指定する定数を2つ定義しておきます。
#define WM_TASKTRAY WM_USER + 21
#define ID_TASKTRAY 0
そしたら、InitInstance
にタスクトレイに追加するコードを追加します。
アイコンのIDを使用するので、resource.h
もインクルードしておいてください。
Windowは表示されないよう、ShowWindow
にSW_HIDE
を指定するよう変更しておきます。
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
hInst = hInstance; // グローバル変数にインスタンス ハンドルを格納する
HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);
if (!hWnd)
{
return FALSE;
}
ShowWindow(hWnd, SW_HIDE);
UpdateWindow(hWnd);
NOTIFYICONDATA nif;
// タスクトレイに登録
nif.cbSize = sizeof(NOTIFYICONDATA);
nif.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_ICON1));
nif.hWnd = hWnd;
nif.uCallbackMessage = WM_TASKTRAY;
nif.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
nif.uID = ID_TASKTRAY;
wcscpy_s(nif.szTip, 128, L"デスクトップアイコンターミネーター");
Shell_NotifyIcon(NIM_ADD, &nif);
return TRUE;
}
ソリューションプラットフォームをx64
に変更してビルドして実行してみます。
タスクトレイに表示されました。黒いのでわかりにくいですね。
####タスクトレイアイコンに右クリックメニューをつける
まずは、タスクトレイアイコンのイベント処理をするコードを追加します。
case WM_TASKTRAY:
if (wParam == ID_TASKTRAY)
{
break;
}
break;
右クリックしたときに表示されるメニューを作ります。
下記のようにメニューを追加します。
WndProc
のcase WM_TASKTRAY
部分に、右クリックされたらメニューを表示するコードを追加します。
case WM_TASKTRAY:
if (wParam == ID_TASKTRAY)
{
switch (lParam)
{
case WM_RBUTTONDOWN:
{
POINT pt;
GetCursorPos(&pt);
HMENU menu = LoadMenu(hInst, MAKEINTRESOURCE(IDR_MENU1));
TrackPopupMenu(GetSubMenu(menu, 0), TPM_LEFTALIGN, pt.x, pt.y, NULL, hWnd, NULL);
DestroyMenu(menu);
}
break;
}
break;
}
break;
メニューのQuitが選択された時の処理をWndProc
のWM_COMMAND
に追記します。
case WM_COMMAND:
{
int wmId = LOWORD(wParam);
// 選択されたメニューの解析:
switch (wmId)
{
case IDM_ABOUT:
DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
break;
case IDM_EXIT:
DestroyWindow(hWnd);
break;
case ID_MENU_QUIT:
NOTIFYICONDATA nif;
// タスクトレイから削除
nif.cbSize = sizeof(NOTIFYICONDATA);
nif.hWnd = hWnd;
nif.uID = ID_TASKTRAY;
Shell_NotifyIcon(NIM_DELETE, &nif);
DestroyWindow(hWnd);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
}
break;
タスクトレイからアイコンを削除して、終了しています。
本質ではないところに結構時間がかかってしまいました。でもそれがプログラムの楽しいところの中の一つです。
#YOLOv3を呼び出す
スレッドを起動して、1秒おきにデスクトップをYOLOv3で識別する、というコードを追加していきます。
タスクトレイアイコンを表示したあたりに、スレッドを起動するコードを追記します。
まずは、スレッド起動時にスレッドになる関数を作成しましょう。
####スレッドを作成する
_beginthreadex
を使用するので、下記のインクルードを追加します。
#include <process.h>
スレッドとして起動される関数を定義します。
unsigned __stdcall desktopwatcher(void*)
{
}
呼び出し側のコードも追加します。
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
hInst = hInstance; // グローバル変数にインスタンス ハンドルを格納する
HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);
if (!hWnd)
{
return FALSE;
}
ShowWindow(hWnd, SW_HIDE);
UpdateWindow(hWnd);
NOTIFYICONDATA nif;
// タスクトレイに登録
nif.cbSize = sizeof(NOTIFYICONDATA);
nif.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_ICON1));
nif.hWnd = hWnd;
nif.uCallbackMessage = WM_TASKTRAY;
nif.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
nif.uID = ID_TASKTRAY;
wcscpy_s(nif.szTip, 128, L"デスクトップアイコンターミネーター");
Shell_NotifyIcon(NIM_ADD, &nif);
unsigned int thID;
HANDLE hTh;
hTh = (HANDLE)_beginthreadex(NULL, 0, desktopwatcher, NULL, 0, &thID);
return TRUE;
}
追加したのは、hTh = (HANDLE)_beginthreadex(NULL, 0, desktopwatcher, NULL, 0, &thID);
のあたりです。
desktopwatcher
関数をスレッドとして起動しています。
起動したら、WaitForSingleObject
等で終了を待たずに次の処理に進めています。
####YOLOv3を準備する
まず、プロジェクトにdarknet
のインクルードとライブラリパスへの参照を追加します。
darknet.h
のインクルードを追加する。
#include "darknet.h"
YOLOv3を使う準備は完了しました。
####デスクトップの画面キャプチャを読み込む
まずは、darknet内の識別処理を見てみます。この処理は、YOLOv3をWindowsでpythonから呼び出してみる(デスクトップアイコン識別)で説明した通り、以下のようになっています。
float *network_predict_image(network *net, image im)
{
//image imr = letterbox_image(im, net->w, net->h);
float *p;
if(net->batch != 1) set_batch_network(net, 1);
if (im.w == net->w && im.h == net->h) {
// Input image is the same size as our net, predict on that image
p = network_predict(*net, im.data);
}
else {
// Need to resize image to the desired size for the net
image imr = resize_image(im, net->w, net->h);
p = network_predict(*net, imr.data);
free_image(imr);
}
return p;
}
この、第二引数のimage im
に画像を渡せばいいようです。で、このim
がどのように準備されているかを見てみると、darknet.py
の中で、以下の通り作られています。
load_image = lib.load_image_color
load_image.argtypes = [c_char_p, c_int, c_int]
load_image.restype = IMAGE
def detect(net, meta, image, thresh=.5, hier_thresh=.5, nms=.45, debug= False):
"""
Performs the meat of the detection
"""
#pylint: disable= C0321
im = load_image(image, 0, 0)
if debug: print("Loaded image")
ret = detect_image(net, meta, im, thresh, hier_thresh, nms, debug)
free_image(im)
if debug: print("freed image")
return ret
上記より、darknet
のload_image_color
を使って画像を読み込んでいます。この関数は、ファイルパスを指定するとそれを読み込むようですが、今回はバイト配列を直接指定したいので、似たような関数がないか探してみます。
image load_image_stb(char *filename, int channels)
これの実装を真似すればできるような気がしますので、作ってみます。
image Hwnd2Image(HWND hwnd, int nPosLeft, int nPosTop, int nDestWidth, int nDestHeight)
{
int channels = 3;
HDC hwindowDC = GetDC(hwnd);
HDC hwindowCompatibleDC = CreateCompatibleDC(hwindowDC);
HBITMAP hbwindow = NULL;
image im;
BITMAPINFOHEADER bi;
SetStretchBltMode(hwindowCompatibleDC, COLORONCOLOR);
RECT windowsize;
GetClientRect(hwnd, &windowsize);
int srcheight = windowsize.bottom;
int srcwidth = windowsize.right;
im = make_image(nDestWidth, nDestHeight, channels);
char* data = new char[nDestWidth * nDestHeight * channels];
memset(data, 0, nDestWidth * nDestHeight * channels);
hbwindow = CreateCompatibleBitmap(hwindowDC, nDestWidth, nDestHeight);
if (hbwindow == NULL)
{
im.data = NULL;
return im;
}
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, 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;
}
copy_image_from_bytes(im, data);
delete [] data;
return im;
}
make_image
でimage
を作成して、そこに、copy_image_from_bytes
で、読み込んできたビットマップを設定しています。
デスクトップのビットマップを取得する処理は、ゼロから作るRPA その2 画像一致によるクリック編(OpenCV)で書いたデスクトップの画面キャプチャを取得するコードを流用します。この中では、cv::Matに値を設定していますが、今回は取得したビット列をload_image_stb
を真似してimage
に変換するようにしてみましょう。
####ウェイトを読み込む
network_predict_image
の第1引数はnetwork
です。ここには訓練済みのウェイトを指定するので、その読み込みを実装します。
読み込みはload_network_custom
が使用されています。
netMain = load_net_custom(configPath.encode("ascii"), weightPath.encode("ascii"), 0, 1) # batch size = 1
YOLOv3では以下のように定義されています。
network *load_network_custom(char *cfg, char *weights, int clear, int batch)
{
printf(" Try to load cfg: %s, weights: %s, clear = %d \n", cfg, weights, clear);
network* net = (network*)calloc(1, sizeof(network));
*net = parse_network_cfg_custom(cfg, batch, 0);
if (weights && weights[0] != 0) {
load_weights(net, weights);
}
if (clear) (*net->seen) = 0;
return net;
}
これらからわかる通り、コンフィグとウェイトのファイルを指定して、network
を読み込めばいいです。
いったんフルパスで実装します。後ほど、いい感じのパス指定に書き直します。
network* net = load_network_custom(const_cast<char*>("D:\\Projects\\python\\desktopyolov3\\cfg\\yolov3-voc.cfg"),
const_cast<char*>("D:\\Projects\\python\\desktopyolov3\\data\\backup\\yolov3-voc_last.weights"), 0, 1);
####識別する
すべての準備がそろったので識別処理を呼び出します。
float* p = network_predict_image(net, im);
int num = 0;
detection* dets = get_network_boxes(net, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), 0.5, 0.5, NULL, 0, &num, 0);
ビルドしてみると、以下のエラーが出力されました。
1>D:\Projects\python\darknet\include\darknet.h(17,10): fatal error C1083: include ファイルを開けません。'pthread.h':No such file or directory
pthread
はUNIXのものなので、Windowsには存在しません。darknetで用意してくれていますので、以下のビルドバスを追加します。
※パスは適宜、自環境用に読み替えてください。
D:\Projects\python\darknet\3rdparty\pthreads\include
さらに、プリプロセッサの定義に_TIMESPEC_DEFINED
を追加してください。
そしてビルドをすると、ビルドが成功しました。成功しない場合は適宜対応してください。
yolo_cpp_dll.dll
とpthreadVC2.dll
を出来上がったEXEと同じディレクトリに配置するか、パスを通してください。
ブレークポイントを置いて、試しに実行してみます。
識別もして、何かしらのバウンディングボックスが返却されました。
きちんと識別されているかを確かめるために、バウンディングボックスの矩形をデスクトップに書く処理を追加します。
do_nms_sort(dets, num, 2, .45);
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(0x00, 0xff, 0x00));
HPEN hNewPenTrash = (HPEN)CreatePen(PS_INSIDEFRAME, 4, RGB(0x00, 0x00, 0xff));
for (int j = 0; j < num; j++)
{
for (int i = 0; i < 2; i++)
{
if (dets[j].prob[i] > 0)
{
HPEN hOldPen = (HPEN)SelectObject(hdc, i == 0 ? hNewPenTrash : hNewPen);
box b = dets[j].bbox;
Rectangle(hdc,
b.x - b.w / 2,
b.y - b.h / 2,
b.x + b.w / 2,
b.y + b.h / 2);
SelectObject(hdc, hOldPen);
}
}
}
SelectObject(hdc, hOldBrush);
DeleteObject(hNewPen);
ReleaseDC(hwnd, hdc);
ここでの注意は、bbox
の座標は、中心X、中心Y、幅、高さだということです。それを意識して矩形を書いています。
上記コードを追加して、実際に動かしてみます。
#デスクトップアイコンを削除する
各アイコンの中心座標がわかったので、アイコンの中心座標から、ゴミ箱の中心座標にマウスをドラッグアンドドロップしてアイコンをすべて削除するコードを追加します。
####アイコンからゴミ箱までドラッグアンドドロップするコードを実装する
sx
とsy
がアイコンの中心位置、dx
とdy
がゴミ箱の中心位置です。
アイコンの中心位置にまずマウスを持っていきます。
次に、左ボタンをダウンします。
そこから3ステップ使ってマウスをゴミ箱まで移動します。
左ボタンをアップします。
マウスの動きが見えるように、すべての処理にSleep
を0.1秒ずつ入れています。
void DragAndDrop(float sx, float sy, float dx, float dy)
{
INPUT inp[1] = { 0 };
inp[0].type = INPUT_MOUSE;
inp[0].mi.time = 0;
inp[0].mi.dwExtraInfo = 0;
inp[0].mi.dx = sx * (65535 / GetSystemMetrics(SM_CXSCREEN));
inp[0].mi.dy = sy * (65535 / GetSystemMetrics(SM_CYSCREEN));
inp[0].mi.mouseData = 0;
inp[0].mi.dwFlags = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE;
SendInput(1, inp, sizeof(INPUT));
Sleep(100);
inp[0].type = INPUT_MOUSE;
inp[0].mi.time = 0;
inp[0].mi.dwExtraInfo = 0;
inp[0].mi.dx = 0;
inp[0].mi.dy = 0;
inp[0].mi.mouseData = 0;
inp[0].mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
SendInput(1, inp, sizeof(INPUT));
Sleep(100);
float slidex = (dx - sx) / 3;
float slidey = (dy - sy) / 3;
for (int i = 1; i <= 3; i++)
{
inp[0].type = INPUT_MOUSE;
inp[0].mi.time = 0;
inp[0].mi.dwExtraInfo = 0;
inp[0].mi.dx = (sx + slidex * i) * (65535 / GetSystemMetrics(SM_CXSCREEN));
inp[0].mi.dy = (sy + slidey * i) * (65535 / GetSystemMetrics(SM_CYSCREEN));
inp[0].mi.mouseData = 0;
inp[0].mi.dwFlags = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE;
SendInput(1, inp, sizeof(INPUT));
Sleep(100);
}
inp[0].type = INPUT_MOUSE;
inp[0].mi.time = 0;
inp[0].mi.dwExtraInfo = 0;
inp[0].mi.dx = 0;
inp[0].mi.dy = 0;
inp[0].mi.mouseData = 0;
inp[0].mi.dwFlags = MOUSEEVENTF_LEFTUP;
SendInput(1, inp, sizeof(INPUT));
Sleep(100);
SendInput(1, inp, sizeof(INPUT));
}
####マウス移動関数を呼び出す
まずは、アイコンとゴミ箱の位置を保持する変数を定義します。
std::vector<box> iconboxes;
box* trashbox = NULL;
矩形描画処理に、位置を保持するコードも追加します。
for (int j = 0; j < num; j++)
{
for (int i = 0; i < 2; i++)
{
if (dets[j].prob[i] > 0)
{
if (i == 0)
{
if (!trashbox)
trashbox = &dets[j].bbox;
}
else
iconboxes.push_back(dets[j].bbox);
HPEN hOldPen = (HPEN)SelectObject(hdc, i == 0 ? hNewPenTrash : hNewPen);
box b = dets[j].bbox;
Rectangle(hdc,
b.x - b.w / 2,
b.y - b.h / 2,
b.x + b.w / 2,
b.y + b.h / 2);
SelectObject(hdc, hOldPen);
}
}
}
マウス移動を、識別したアイコンごとに呼び出します。
if (trashbox)
{
for (auto b : iconboxes)
{
DragAndDrop(b.x, b.y, trashbox->x, trashbox->y);
}
}
#動かしてみる
実際に動かしてみました。割と面白い動きをします。
訓練データが5枚だけの画像なので、認識率があまり高くないため途中で止まったりしますが、認識率を上げていけば確実に削除してくれるようになると思います。
YOLOv3でWindowsのデスクトップアイコンを識別し、ゴミ箱にすべてを移動するAIを作ってみた https://t.co/ivPrgYfMet via @YouTube
— teamm (@teamm89201677) October 24, 2019
#####こちらからYoutubeで直接見られます。
https://youtu.be/fLwWfjfARpU
#おわりに
強化学習で作った時と比べて、動きが機械的に感じます。やはり、生き物に近いのは強化学習なんだなぁと勝手に実感しています。
ただ、確実性から行くと、YOLOv3版のほうが高いので、これをRPAとかに組み込めば、いろいろと面白いことができそうです。
ゼロから作るRPAシリーズにもYOLOv3の識別とかを組み込んでいきたいと思います。
#####YOLOv3シリーズは以下から参照できます。
Yolo v3+Windows 10でデスクトップのアイコンを識別してみる その1
https://qiita.com/yasunari_matsuo/items/78695e9f53bc6b589daa
(おまけ)Yolo v3+Windows 10でデスクトップのアイコンを動的に識別してみる
https://qiita.com/yasunari_matsuo/items/f0989523849f6ae40483#_reference-8747cd277d7aeccfdef1
YOLOv3をWindowsでpythonから呼び出してみる(デスクトップアイコン識別)
https://qiita.com/yasunari_matsuo/items/ebdb704ffff5db21f466
#####強化学習を使ったデスクトップアイコン削除は以下です。
デスクトップのアイコンをすべて削除してくれるAIを強化学習(DQN)でつくってみた
https://qiita.com/yasunari_matsuo/items/8ab661ca4a449495703f
#ソースコード
#####今回のソースコードはこちらからダウンロードできます。
GitHub
https://github.com/yasunarim/DesktopIconTerminator
#参考にしたサイト
http://7ujm.net/C++/NTFYICON.html
https://ohwhsmm7.blog.fc2.com/?no=84