はじめに
これは、OpenCV Advent Calendar 2016 1日目の記事です。関連記事は目次にまとめられています。
今年のOpenCV Advent Calendar は執筆時点でまだ担当者が埋まってない日もあります。「OpenCV」という文言が含まれている技術記事なら十分ですので、是非皆様、参加のほどをよろしくお願いします。
のっけから0時投稿に失敗するとは・・・
今日のお題はOpenCVのtestです。昨年のAdvent Calendar の26日目 に滑り込んでRNGに関する紹介を書きました。そのときはRNGがテストに使われることだけ紹介して、テストの中身の説明は丸っと割愛しましたが、そのやり残しの点について、今年は踏み込んで行きたいと思います。
Google Testとは
- https://github.com/google/googletest
- Googleが提供する、C++のテストのためのフレームワークです
#そもそも、「テスト」って何?
- 例えば、ある処理をする関数が合ったとします
- 内部でのアルゴリズムを変えた場合、今までと同じ挙動する関数なのか、「テスト」する必要があります
- 通常、大量の入力データ/出力データの組を保持しておいて、それらが修正の前後で変化するかチェックすることになります
- もしくは、同等の処理をする別の関数と、同じ挙動をするか比較します
- 基本的には、データを関数に渡して、結果が期待通りか、そうでないか、機械的に判定する枠組みをテスト、と呼びます
- 類似の枠組みは大量にありますが、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", >est_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",
>est_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
とします) - プリプロセスはその特性上、ヘッダファイルに対しては行なえませんので、拡張子
cpp
かc
のファイルを開きます - ソリューションエクスプローラから、同じく
a.cpp
を探し出します。- ソリューションエクスプローラは
Ctrl+Alt+L
で表示されます。もしくは表示(V)→ソリューションエクスプローラ(p)
- ソリューションエクスプローラは
- ソリューションエクスプローラから
a.cpp
を右クリックし、プロパティを表示します - 下記画像だと、英語版になってますが、Preprocess to a File と書かれた場所のオプションを**Yes (/P)**にします
-
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で走らせてみた」です
備考
筆者は以下の環境で試しました
- 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