Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

はじめに

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

今日のお題はOpenCVのtestです。昨年のAdvent Calendar26日目 に滑り込んでRNGに関する紹介を書きました。そのときはRNGがテストに使われることだけ紹介して、テストの中身の説明は丸っと割愛しましたが、そのやり残しの点について、今年は踏み込んで行きたいと思います。

Google Testとは

そもそも、「テスト」って何?

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

その他テクい話

  • だんでらいおん先生に煽られる前にもうちょっと読んで役に立つ話を書いておこうと思います
  • C++は、コンパイルする前にプリプロセスが行われます
  • 皆さんもプリプロセスだけを行った状態のソースコードが読みたかったりすることってありませんか?筆者はよくあります。
  • Visual Stuio の場合とgcc + CMakeでの方法を下に書いておきます

Visual Studio でプリプロセスを行う方法

  • プリプロセスを行いたいソースコードを開いた状態にします。(以降、a.cppとします)
  • プリプロセスはその特性上、ヘッダファイルに対しては行なえませんので、拡張子cppcのファイルを開きます
  • ソリューションエクスプローラから、同じくa.cppを探し出します。
    • ソリューションエクスプローラはCtrl+Alt+Lで表示されます。もしくは表示(V)ソリューションエクスプローラ(p)
  • ソリューションエクスプローラからa.cppを右クリックし、プロパティを表示します
  • 下記画像だと、英語版になってますが、Preprocess to a File と書かれた場所のオプションをYes (/P)にします capture_001_21102016_134541.png
  • Ctrl+F7を押して、ファイル単体をコンパイルします
  • OpenCV + Visual Studio の組み合わせだと、以下の通りそれぞれ出力されます(DebugReleaseはそれぞれのビルドのコンフィグにより変わります)
  • また、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で走らせてみた」です

備考

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

  • 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
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away