4
1

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.

AIでデスクトップアイコンを自動削除してくれるツールを作ったった(YOLOv3+Windows 10)

Last updated at Posted at 2019-10-24

#はじめに
前回」の記事では、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デスクトップアプリケーション」を選んで次へをクリックしてプロジェクトを作成してください。

image.png

####タスクトレイ用アイコンの登録
タスクトレイ用のアイコンをプロジェクトに追加します。アイコンは今マイブームのICOOON MONOから借用します。
まずは、プロジェクトで右クリックして表示されたメニューで「追加」-「リソース」を選択します。
image.png
「Icon」を選択して、「新規作成」をクリックします。
image.png

いい感じに、編集します。大きさ的に32×32 8ビットを編集します。

image.png

残りのアイコンを全部消します。消すときは、右クリックして「イメージタイプの削除」を選択してください。
image.png

####タスクトレイコードの追加
タスクトレイに追加するには、Shell_NotifyIcon関数を使用します。

Shell_NotifyIcon
BOOL Shell_NotifyIcon(
  _In_ DWORD           dwMessage,
  _In_ PNOTIFYICONDATA lpdata
);

タスクトレイに追加するには、dwMessageNIM_ADDを指定します。
NOTIFYICONDATAを使用するには、Shellapi.hをインクルードします。

framework.h
#include <Shellapi.h>

NOTIFYICONDATAに指定する定数を2つ定義しておきます。

DesktopIconTerminator.cpp
#define WM_TASKTRAY   WM_USER + 21
#define ID_TASKTRAY 0

そしたら、InitInstanceにタスクトレイに追加するコードを追加します。
アイコンのIDを使用するので、resource.hもインクルードしておいてください。

Windowは表示されないよう、ShowWindowSW_HIDEを指定するよう変更しておきます。

DesktopIconTerminator.cpp
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に変更してビルドして実行してみます。
タスクトレイに表示されました。黒いのでわかりにくいですね。
image.png

####タスクトレイアイコンに右クリックメニューをつける
まずは、タスクトレイアイコンのイベント処理をするコードを追加します。

WndProc
	case WM_TASKTRAY:
		if (wParam == ID_TASKTRAY)
		{
			break;
		}
		break;

右クリックしたときに表示されるメニューを作ります。
image.png
下記のようにメニューを追加します。
image.png
WndProccase WM_TASKTRAY部分に、右クリックされたらメニューを表示するコードを追加します。

WndProc
	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が選択された時の処理をWndProcWM_COMMANDに追記します。

WndProc
    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を使用するので、下記のインクルードを追加します。

framework.h
#include <process.h>

スレッドとして起動される関数を定義します。

DesktopIconTerminator.cpp
unsigned __stdcall desktopwatcher(void*)
{

}

呼び出し側のコードも追加します。

DesktopIconTerminator.cpp
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のインクルードとライブラリパスへの参照を追加します。

インクルードディレクトリに追加する。
image.png

ライブラリディレクトリに追加する。
image.png

ライブラリをリンカの「追加の依存ファイル」に追加する。
image.png

darknet.hのインクルードを追加する。

DesktopIconTerminator.cpp
#include "darknet.h"

YOLOv3を使う準備は完了しました。

####デスクトップの画面キャプチャを読み込む
まずは、darknet内の識別処理を見てみます。この処理は、YOLOv3をWindowsでpythonから呼び出してみる(デスクトップアイコン識別)で説明した通り、以下のようになっています。

network.c
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の中で、以下の通り作られています。

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

上記より、darknetload_image_colorを使って画像を読み込んでいます。この関数は、ファイルパスを指定するとそれを読み込むようですが、今回はバイト配列を直接指定したいので、似たような関数がないか探してみます。

image.c
image load_image_stb(char *filename, int channels)

これの実装を真似すればできるような気がしますので、作ってみます。

DesctopIconTerminator.cpp
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_imageimageを作成して、そこに、copy_image_from_bytesで、読み込んできたビットマップを設定しています。

デスクトップのビットマップを取得する処理は、ゼロから作るRPA その2 画像一致によるクリック編(OpenCV)で書いたデスクトップの画面キャプチャを取得するコードを流用します。この中では、cv::Matに値を設定していますが、今回は取得したビット列をload_image_stbを真似してimageに変換するようにしてみましょう。

####ウェイトを読み込む
network_predict_imageの第1引数はnetworkです。ここには訓練済みのウェイトを指定するので、その読み込みを実装します。
読み込みはload_network_customが使用されています。

darknet.py
netMain = load_net_custom(configPath.encode("ascii"), weightPath.encode("ascii"), 0, 1)  # batch size = 1

YOLOv3では以下のように定義されています。

parsser.c
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を読み込めばいいです。
いったんフルパスで実装します。後ほど、いい感じのパス指定に書き直します。

DesktopIconTerminator.cpp
    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);

####識別する
すべての準備がそろったので識別処理を呼び出します。

DesktopIconTerminator.cpp
        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を追加してください。

image.png

そしてビルドをすると、ビルドが成功しました。成功しない場合は適宜対応してください。
yolo_cpp_dll.dllpthreadVC2.dllを出来上がったEXEと同じディレクトリに配置するか、パスを通してください。
ブレークポイントを置いて、試しに実行してみます。

ネットワークの読み込みに成功しました。
image.png

画像の読み込みもしているようです。
image.png

識別もして、何かしらのバウンディングボックスが返却されました。
image.png

きちんと識別されているかを確かめるために、バウンディングボックスの矩形をデスクトップに書く処理を追加します。

DesktopIconTerminator.cpp
        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、幅、高さだということです。それを意識して矩形を書いています。
上記コードを追加して、実際に動かしてみます。

image.png
きちんと識別処理が動きました。

#デスクトップアイコンを削除する
各アイコンの中心座標がわかったので、アイコンの中心座標から、ゴミ箱の中心座標にマウスをドラッグアンドドロップしてアイコンをすべて削除するコードを追加します。

####アイコンからゴミ箱までドラッグアンドドロップするコードを実装する
sxsyがアイコンの中心位置、dxdyがゴミ箱の中心位置です。
アイコンの中心位置にまずマウスを持っていきます。
次に、左ボタンをダウンします。
そこから3ステップ使ってマウスをゴミ箱まで移動します。
左ボタンをアップします。

マウスの動きが見えるように、すべての処理にSleepを0.1秒ずつ入れています。

DesktopIconTerminator.cpp
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));
}

####マウス移動関数を呼び出す
まずは、アイコンとゴミ箱の位置を保持する変数を定義します。

desktopwatcher
        std::vector<box> iconboxes;
        box* trashbox = NULL;

矩形描画処理に、位置を保持するコードも追加します。

desktopwatcher
        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);
                }
            }
        }

マウス移動を、識別したアイコンごとに呼び出します。

desktopwatcher
        if (trashbox)
        {
            for (auto b : iconboxes)
            {
                DragAndDrop(b.x, b.y, trashbox->x, trashbox->y);
            }
        }

#動かしてみる
実際に動かしてみました。割と面白い動きをします。
訓練データが5枚だけの画像なので、認識率があまり高くないため途中で止まったりしますが、認識率を上げていけば確実に削除してくれるようになると思います。

#####こちらから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

4
1
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
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?