debunking the waitKeyは遅いよね


# はじめに
これは、OpenCV Advent Calendar 2018 2日目の記事です。関連記事は[目次](https://qiita.com/advent-calendar/2018/opencv)にまとめられています。なお、本記事は筆者個人の意見であり、筆者の所属組織とは無関係です。

# ```cv::imshow```と```cv::waitKey```について
それぞれ、Windows10、OpenCV 4.0 を基準に説明します。みんな大好きOpenCV 4.0。OpenCV 4.0 万歳！

画像を10枚表示するループ文を考えましょう。

```c++

std::vector<cv::Mat> result;
function(result); // ここで10枚分の結果が格納される
for ( auto&& it : result )
{
    cv::imshow("window", it);
}
```

これを実行するとどうなるか、こんなウィンドウが表示されます。

<img width="397" alt="Screenshot 2018-11-26 19.17.35.png" src="https://qiita-image-store.s3.amazonaws.com/0/12162/dc12ec92-b9fa-fa6b-b211-4cd96acfeed6.png">

あれ？グレーのウィンドウが表示されるだけです。ただしく画像が表示されていません。
実はOpenCVの実装では、確実にウィンドウに画像を表示させるためには```cv::waitKey```を呼ぶ必要があるのです。

# ```cv::waitKey```について
さて、```cv::waitKey```ですが、引数```delay```を取るAPIです。
このAPIは、[公式ドキュメント](https://docs.opencv.org/4.0.0/d7/dfc/group__highgui.html#ga5628525ad33f52eab17feebcfba38bd7)を見ると、```delay```が```0```より大きければ```delay[ms]```だけ待ち、それ以外(```delay```が```0```か負の数)の場合はキー入力があるまで待ちます。

> The function waitKey waits for a key event infinitely (when delay≤0 ) or for delay milliseconds, when it is positive

通常、研究やプロトタイプを作成してる場合は、この挙動でなんら問題ないと思います。ところがWindows版のOpenCVには、短い待ち時間を指定した時の挙動に罠が潜んでいたのです。

# ```cv::waitKey```に潜んでいた罠
結論から言うと、```cv::waitKey```内部で使っているAPI```GetTickCount```の分解能が粗いため、1ms待つつもりのコードでも、**実際には```15ms```ぐらい待つ**、という罠です。私が見つけた中で、[分かり易いと思った記事](http://13mzawa2.hateblo.jp/entry/2015/12/28/180021)を紹介します。[OpenCV Advent Calendar 2016にも寄稿](http://13mzawa2.hateblo.jp/entry/2016/12/09/151205)してくれた[ざわーくらうと](https://twitter.com/13Mzawa2)さんの記事です。

- [cv::waitKey()の処理時間を計測してみた - 聞きかじりめも](http://13mzawa2.hateblo.jp/entry/2015/12/28/180021)

> なんと，cv::waitKey()はせいぜい15ms程度の精度しかないことが分かりました．

この記事にかかれている通り、従来のOpenCVのWindows版では```cv::waitKey```内部で```GetTickCount```を呼んでおり、こいつの分解能に足を引っ張られる形で```cv::waitKey```の分解能は```15ms```程度しかなかったのです。

# OpenCV 4.0
しかし、2018年、ようやくその実装に終止符が打たれます

実際にOpenCV 4.0 を使って、実験をしてみましょう。


```c++

for( int d = 1;d < 100;d++ )
{
    imshow(windowName, image);
    int64 begin = getTickCount();
    waitKey(d);
    int64 end = getTickCount();
    std::cout << d << '\t' << ((end - begin) * 1000) / getTickFrequency() << "[ms]" << std::endl;
}
```
[ざわーくらうとさんの記事](http://13mzawa2.hateblo.jp/entry/2015/12/28/180021)と同様の実験をしてみます。```waitKey```の前後でタイムスタンプを取得して経過時間を測定しています。この時の結果がこちらです。


![measurementWaitKey.png](https://qiita-image-store.s3.amazonaws.com/0/12162/b11ed7d2-0fab-ec4a-492c-a4d59fd8bcc1.png)

横軸に引数に指定した```d```を、縦軸に実際に待った時間をプロットしてあります。
待ち時間```d```に対して1回ずつしか測定してないので外れ値も含まれていますが、基本的には素直に、指定した時間の線形の待ち時間を提供してくれます。**あのひどい挙動はすでに無いのです**。


どこでこの改善が入ったのでしょうか？それは今年の6月にマージされた[PR#11714](https://github.com/opencv/opencv/pull/11714)で解決されています。

[highgui(win32): improve waitKey() timeout condition by alalek · Pull Request #11714 · opencv/opencv](https://github.com/opencv/opencv/pull/11714)


という訳で、Webを調べると、OpenCVの```waitKey```は遅い、という記事が見つかるし、確かに遅かったのですが、4.0系列ではその挙動は改善されています[^3]。

[^3]: なお、厳密な話をすれば2018年7月にリリースされた3.4.2には既に取り込まれていたので、3.4.2とそれ以降の3.4系列、並びに4.0系列でこの恩恵に預かれます

# おわりに
実は「```cv::waitKey```遅いでしょ、ほら遅いんだよ」というn番煎じの記事を書いてお茶を濁すつもりだった(n番煎じだけに)のですが、実験してみたら改善されていて驚きました。なのでタイトルも、かの有名な「遅いって言うな！」な論文のタイトルをもじりました。

なお、```waitKey```が高精度に待てるようになったからと言って、```imshow```で100FPS、200FPSの表示ができるようになったわけではないことに注意してください。

筆者は以下の環境で調べました。

- OpenCV 4.0.0
- Windows 10 64bit
- Visual Studio Community 2015 (Update 3)
- CMake 3.11.0
- Core i7 8650U

明日の投稿も私の予定で、タイトルは「OpenCVと環境変数(仮)」です

# 追記
かきあげてホッとしていたら、1日目担当の @dandelion1124 先生に[煽られたので、追記します](https://qiita.com/dandelion1124/items/eae435e601fba2adb0d1)。

> とてつもなくテクい話が読めそうなのですごく楽しみです！！

そうだったよ・・・@dandelion1124 先生はこう言う時に煽ってくる人だった！

さて、メインの文章内ではループの中で```waitKey```を使う場合に、OpenCV 4.0からは時間の分解能が細かくなることを示しました。
しかしメインループ内で、msオーダの処理を回してると、この```waitKey```のせいで数割パフォーマンスが遅くなります。純粋にパフォーマンスを追求するなら```waitKey```をループ内に入れないのですが、現実問題ループの中の経過を```imshow```で見せることが必要になる場合が多々あります。

という訳で、両立させる場合はスレッドを分けるのが必須となります。

ここではWindowsの実装をもとに実装例を紹介します。

```c++
unsigned __stdcall imageShowThread(void *parameter)
{
    messageBox* p = (messageBox*)parameter;
    cv::namedWindow(WINDOW_NAME);
    volatile int counter = *(p->frameCounter);
    while (p->loopFlag)
    {
        if (counter != *(p->frameCounter))
        {
            cv::imshow(WINDOW_NAME, *(p->image));
            counter = *(p->frameCounter);
        }
        int key = cv::waitKey(1);
        switch(key)
        {
        case 0:
            *(p->status) = 0;
            break;
        case 'q':
            *(p->status) = -1;
            *(p->loopFlag) = 0;
            break;
        default:
            break;
        }
    }
    cv::destroyWindow(WINDOW_NAME);
    return 0;
}
```

このようなコールバック関数を用意します。なお、```messageBox```構造体には以下のようにポインタを保持します。

```c++
struct _messageBox
{
    volatile cv::Mat *image
    volatile unsigned int *frameCounter;
    int *loopFlag;
    int *status;
};
typedef struct _messageBox messageBox;
```

ポイントはいくつかあって、

 - メインスレッドから```image```、```frameCounter```、```loopFlag```、```status```のポインタを共有する
 - ウィンドウの生成から破棄までメインループでなく、別スレッドで管理する
 - メインループから渡された画像はカウンタが更新された時にだけ表示する
 - キー入力に対するリアクションは押された一瞬だけ対応しようとすると難しいので、最後に押されたキーを保持しておく(何も押されていない時に返される-1を無視する)

と、非同期処理の最低限のポイントは抑えておきましょう。

でこれをメインスレッドから```_beginthreadex```でcallします

```c++
    cv::Mat gImage;
    static int gStatus= 0;
    static int gCounter = 0;
    static int loopFlag = 1;
    static messageBox threadMessage;
    threadMessage.loopFlag = &loopFlag;
    threadMessage.frameCounter= &gCounter;
    threadMessage.inputKey = &gStatus;
    threadMessage.image = &gImage;
    DWORD thResultId;
    HANDLE hThResult = (HANDLE)_beginthreadex(NULL, 0, imageShowThread, &threadMessage, 0, (unsigned int *)&thResultId);
    if (hThResult == 0)
    {
        std::cerr << "Failed to create result show thread" << std::endl;
        return -1;
    }
```

またメインスレッドでは

 - 結果を```gImage```に書き込み、書き込む度に```gCounter```をインクリメントする
 - statusに合わせて適切なリアクションを設定する


```c++
while(loopFlag)
{
    int status = gStatus;
    mainProcess(&gImage, status); // ここで結果をgImageに書き込む
    gCounter++;                   //  カウンタをインクリメントして画像を表示するトリガを入れる

}
```

こうすることで、メインループのオーバーヘッダを最低限にしつつ、別スレッドでウィンドウを表示できます。また、```gStatus```の中身を適切に処理することで最低限のインタラクションも可能です。簡単にまとめると、下記の図のようになります。

![asyncThread.png](https://qiita-image-store.s3.amazonaws.com/0/12162/419776d5-f21a-a93f-70e5-d73d11869cd6.png)

と言う訳で、これから明日の分の記事を書きます。