OpenCV の gtest (Google Test, accuracy_testについて)

# はじめに
これは、OpenCV Advent Calendar 2016 1日目の記事です。関連記事は[目次](http://qiita.com/advent-calendar/2016/opencv)にまとめられています。
今年のOpenCV Advent Calendar は執筆時点でまだ担当者が埋まってない日もあります。「OpenCV」という文言が含まれている技術記事なら十分ですので、是非皆様、参加のほどをよろしくお願いします。
のっけから0時投稿に失敗するとは・・・

今日のお題はOpenCVのtestです。[昨年のAdvent Calendar](http://qiita.com/advent-calendar/2015/opencv) の[26日目](http://tessy.org/wiki/index.php?OpenCV%20Advent%20Calendar%202015%2026%C6%FC%CC%DC) に滑り込んで[RNGに関する紹介](http://tessy.org/wiki/index.php?OpenCV%20Advent%20Calendar%202015%2026%C6%FC%CC%DC)を書きました。そのときはRNGがテストに使われることだけ紹介して、テストの中身の説明は丸っと割愛しましたが、そのやり残しの点について、今年は踏み込んで行きたいと思います。

# Google Testとは
- https://github.com/google/googletest
- Googleが提供する、C++のテストのためのフレームワークです
![googletest.png](https://qiita-image-store.s3.amazonaws.com/0/12162/410dc97a-83f1-8734-8744-eeda9b2a5cd1.png "googletestのgithub")

# そもそも、「テスト」って何？
- 例えば、ある処理をする関数が合ったとします
- 内部でのアルゴリズムを変えた場合、今までと同じ挙動する関数なのか、「テスト」する必要があります
- 通常、大量の入力データ/出力データの組を保持しておいて、それらが修正の前後で変化するかチェックすることになります
- もしくは、同等の処理をする別の関数と、同じ挙動をするか比較します
- 基本的には、データを関数に渡して、結果が期待通りか、そうでないか、機械的に判定する枠組みをテスト、と呼びます
- 類似の枠組みは大量にありますが、OpenCV では Google Testを使っています。

# テストのコードの場所
- 試しに、Mat同士の足し算のテストコードを見てみましょう。
- ```modules/core/test/test_arithm.cpp```を例に説明します
- OpenCV内部では、実装されるアルゴリズムの種類によって、moduleに分かれているわけですが、その内部は
 - include
 - src
 - test
 - perf
- の4つがだいたい含まれています
- testディレクトリ以下に、挙動の一貫性を調べるテストが
- perfディレクトリ以下に、処理速度を調べるテストが含まれています
- perfに関しては次回以降触れていきます

# テストコードの読み方
- ちなみに、テストコードはひどく読みづらいです。初心者お断り状態です。
- 一部を抜粋してみましょう

```modules/core/test/test_arithm.cpp
INSTANTIATE_TEST_CASE_P(Core_Add, ElemWiseTest, ::testing::Values(ElemWiseOpPtr(new cvtest::AddOp)));
```

- なんとこの1行で終わりです
- ちなみに、同じディレクトリには、```test_main.cpp```という、大本の関数っぽいファイルが置かれています。
- ここから辿ったら挙動がわかるのではないか、と思ってこのファイルを覗くと、今度は情報量の少なさに絶望します

```modules/core/test/test_main.cpp
#ifdef _MSC_VER
# if _MSC_VER >= 1700
#  pragma warning(disable:4447) // Disable warning 'main' signature found without threading model
# endif
#endif


#include "test_precomp.hpp"

CV_TEST_MAIN("cv")
```
- なんとこれでファイル全体
- タネ明かしをすると、これはマクロで実行されているので、

```
::testing::internal::ParamGenerator<ElemWiseTest::ParamType> gtest_Core_AddElemWiseTest_EvalGenerator_() { return ::testing::Values(ElemWiseOpPtr(new cvtest::AddOp)); } int gtest_Core_AddElemWiseTest_dummy_ = ::testing::UnitTest::GetInstance()->parameterized_test_registry(). GetTestCasePatternHolder<ElemWiseTest>( "ElemWiseTest", "C:\\work\\opencv-fork\\modules\\core\\test\\test_arithm.cpp", 1438)->AddTestCaseInstantiation( "Core_Add", &gtest_Core_AddElemWiseTest_EvalGenerator_, "C:\\work\\opencv-fork\\modules\\core\\test\\test_arithm.cpp", 1438);
```
- もうちょっと整形すると

```
::testing::internal::ParamGenerator<ElemWiseTest::ParamType> gtest_Core_AddElemWiseTest_EvalGenerator_()
{
    return ::testing::Values(ElemWiseOpPtr(new cvtest::AddOp));
}
int gtest_Core_AddElemWiseTest_dummy_ = 
    ::testing::UnitTest::GetInstance()->parameterized_test_registry(). GetTestCasePatternHolder<ElemWiseTest>(
        "ElemWiseTest", 
        "C:\\work\\opencv-fork\\modules\\core\\test\\test_arithm.cpp", 
        1438
        )->AddTestCaseInstantiation(
            "Core_Add", 
            &gtest_Core_AddElemWiseTest_EvalGenerator_, 
            "C:\\work\\opencv-fork\\modules\\core\\test\\test_arithm.cpp", 
            1438
            );
```
- ココまで読みやすくしても非常に読みづらいですが、ここではtest caseを登録しています
- 「こういうテストをしてね」とgoogle testのframeworkに登録している訳です。
- 後にテストが実行される場合は、```cvtest::AddOp``` がコールされるので、以下がテストの実体です

```modules/core/test/test_arithm.cpp
struct AddOp : public BaseAddOp
{
    AddOp() : BaseAddOp(2, FIX_ALPHA+FIX_BETA+FIX_GAMMA+SUPPORT_MASK, 1, 1, Scalar::all(0)) {}
    void op(const vector<Mat>& src, Mat& dst, const Mat& mask)
    {
        if( mask.empty() )
            add(src[0], src[1], dst);
        else
            add(src[0], src[1], dst, mask);
    }
};
```
- なお、この```AddOp```構造体は```BaseAddOp```を継承しており、そちらには```refop```という関数が登録されております

```modules/core/test/test_arithm.cpp
struct BaseAddOp : public BaseElemWiseOp
{
    BaseAddOp(int _ninputs, int _flags, double _alpha, double _beta, Scalar _gamma=Scalar::all(0))
    : BaseElemWiseOp(_ninputs, _flags, _alpha, _beta, _gamma) {}

    void refop(const vector<Mat>& src, Mat& dst, const Mat& mask)
    {
        Mat temp;
        if( !mask.empty() )
        {
            cvtest::add(src[0], alpha, src.size() > 1 ? src[1] : Mat(), beta, gamma, temp, src[0].type());
            cvtest::copy(temp, dst, mask);
        }
        else
            cvtest::add(src[0], alpha, src.size() > 1 ? src[1] : Mat(), beta, gamma, dst, src[0].type());
    }
};
```
- こちらの```refop```の中では```cvtest::add```という関数が呼ばれています。
- ポイントは、この```add```関数は```cvtest```の実装であり、実際の```Mat```の足し算とは別のコードが実行されます。

```
namespace cvtest
{
//中略
void add(const Mat& _a, double alpha, const Mat& _b, double beta,
        Scalar gamma, Mat& c, int ctype, bool calcAbs)
{
// 中略
}
}
```
- これで、```Mat```の足し算が違う挙動を示すようになった場合、こちらの```cvtest::add```と結果が食い違うことになり、エラーが検出される、という流れです

# その他テクい話
- ~~[だんでらいおん先生]( https://twitter.com/ )に煽られる前に~~もうちょっと読んで役に立つ話を書いておこうと思います
- C++は、コンパイルする前にプリプロセスが行われます
- 皆さんもプリプロセスだけを行った状態のソースコードが読みたかったりすることってありませんか？筆者はよくあります。
- Visual Stuio の場合とgcc + CMakeでの方法を下に書いておきます
 
## Visual Studio でプリプロセスを行う方法
- プリプロセスを行いたいソースコードを開いた状態にします。(以降、```a.cpp```とします)
- プリプロセスはその特性上、ヘッダファイルに対しては行なえませんので、拡張子```cpp```か```c```のファイルを開きます
- ソリューションエクスプローラから、同じく```a.cpp```を探し出します。
  - ソリューションエクスプローラは```Ctrl+Alt+L```で表示されます。もしくは**表示(V)**→**ソリューションエクスプローラ(p)**
- ソリューションエクスプローラから```a.cpp```を右クリックし、プロパティを表示します
- 下記画像だと、英語版になってますが、**Preprocess to a File** と書かれた場所のオプションを**Yes (/P)**にします
![capture_001_21102016_134541.png](https://qiita-image-store.s3.amazonaws.com/0/12162/c15a15e3-7ca1-8b60-2cae-0c838537af42.png)
- ```Ctrl+F7```を押して、ファイル単体をコンパイルします
- OpenCV + Visual Studio の組み合わせだと、以下の通りそれぞれ出力されます(```Debug```と```Release```はそれぞれのビルドのコンフィグにより変わります)
- また、```core```以外のモジュールは適宜読み替えて下さい。

| ソースコード       |      出力先 |
|:-----------------|:------------------|
|modules/core/src/a.cpp       |build/modules/core/opencv_core.dir/Debug/a.i|
|modules/core/test/test_a.cpp |build/modules/core/opencv_core.dir/Debug/test/test_a.cpp.obj|
|modules/core/perf/perf_a.cpp |build/modules/core/perf_a.i|

   - test以下のファイルが何故か```obj```拡張子で出力されるのかは謎。バグ？PRチャンス？
   - perf以下のファイルも何故か```obj```拡張子としてコピーしようとするけれど謎。バグ？PRチャンス？

## GCC でプリプロセスを行う方法
- CMakeで作ったビルドディレクトリを、以下```build```とします
- ```modules/core/src/a.cpp```をプリプロセスする場合

```
cd build
cd modules/core
make src/a.cpp.i
# build/modules/core/CMakeFiles/opencv_core.dir/src/a.cpp.i に結果が出力される
```
  - ```modules/core/test/test_a.cpp```をプリプロセスする場合

```
cd build
cd modules/core
make test/test_a.cpp.i
# build/modules/core/CMakeFiles/opencv_test_core.dir/test/test_a.cpp.i に結果が出力される
```
  - ```modules/core/perf/perf_a.cpp```をプリプロセスする場合

```
cd build
cd modules/core
make perf/perf_a.cpp.i
# build/modules/core/CMakeFiles/opencv_perf_core.dir/perf/perf_a.cpp.i に結果が出力される
```
  - coreモジュール以外の場合は適宜読み替えて下さい

# テストの書き方について
通常OpenCVのテストを書かれることは滅多に無いと思います。が、ここまでの説明で分かる通り、かなり読みにくいので、書くのに一癖も二癖もあります。なので、実際にテストを書かれる際は、入力や出力が似ている関数のテスト（既に実装済み）を探してきてそれをベースに改修/実装するのが良いと思います


# まとめ
 - OpenCVのテストはGoogle Testをフレームワークとして使っている
 - 書き間違いを防ぐため、なるべくマクロの使い回しが行われており、初見だと非常に分かりづらい
 - プリプロセスすると、多少は分かり易い？のでプリプロセスだけする方法を紹介
 - 明日も筆者の担当で「[OpenCVをMIPSで走らせてみた](http://qiita.com/tomoaki_teshima/items/fee80c6f1243485ae5d4)」です

# 備考
筆者は以下の環境で試しました

- Windows
 - OpenCV masterブランチ (リビジョン8213e57f)
 - Visual Studio 2012 Update 5 (x64ビルド)
 - Windows 7 Ultimate 64bit
- Linux
 - OpenCV masterブランチ (リビジョン8213e57f)
 - Jetson TX1 (ARM 64bit)
 - GCC 5.4.0
 - Ubuntu 16.04 64bit