LoginSignup
9
0

More than 1 year has passed since last update.

OpenCVのstitchモジュールのバグを直した話

Last updated at Posted at 2022-12-23

はじめに

TL;DR

  • OpenCLのin-placeな使い方は、一部のカーネルでは対応していない

テストのFAIL

  • もとはstitchingモジュールのFAILから始まりました。
[ RUN      ] ExposureCompensate.SimilarityThreshold
/opencv/modules/stitching/test/test_exposure_compensate.cpp:69: Failure
Expected: (psnr_similarity_mask) > (300), actual: 38.0114 vs 300
[  FAILED  ] ExposureCompensate.SimilarityThreshold (56824 ms)
  • かいつまんで説明すると、stitchingモジュールの主な機能は2枚以上の画像を、うまいことつなぎ合わせる(stitch)ことです
  • 今回のテストはその中でも2枚の画像の露光条件が違った場合に2枚の画像の見た目を揃える機能です
  • テストで使う画像によって、類似度が300以上になってほしいのに、何故か38程度にしかなっておらず、degradedな状況に陥っています。
Expected: (psnr_similarity_mask) > (300), actual: 38.0114 vs 300
  • なお、もとのIssueはこちら、私が直したPRはこちらです
  • 本記事の殆どが、もとのIssueに書いてあることを再編集したものです

環境による切り分け

  • 手元にあるOpenCV用のビルドファーム(AWSのJenkinsと、リモートに置いてあるJetson、Raspberry Piなどで構成される)で当該テストを見ると、きれいに「テストがPASSする環境」、「テストがFAILする環境」に二分されました
テスト結果 ボード名
FAIL FireFly RK3399
FAIL ODROID C4
FAIL ODROID N2 plus
FAIL ODROID XU4
FAIL Tinkerboard
FAIL ROCK Pi 4C
PASS Jetson Nano
PASS Jetson TX1
PASS Jetson TX2
PASS Jetson AGX Xavier
PASS Jetson Xavier NX
PASS le potato
PASS Raspberry Pi 3
PASS Raspberry Pi 4
  • 個人的な感想ですが、テスト環境は多ければ多いほど好ましいです
    • デバッグは面で行う。これ大事です。
  • 今回もこの結果を見ただけで原因に心当たりが付きますが、それだとアドベントカレンダーの意味がないので、もうちょっと解説しましょう。
  • こけてる環境はすべてOpenCLを実行できるGPUを持っていました
ボード名 SoC GPU
firefly-rk3399 RK3399 Mali-T860 OpenCL 1.2
odroid-c4 S905X3 Mali-G31 OpenCL 2.0
odroid-n2-plus S922X Mali-G52 OpenCL 2.0
odroid-xu4 Exynos5422 Mali-T628 OpenCL 1.2
rock-pi-4c RK3399 Mali-T860 OpenCL 1.2
tinkerboard RK3288 Mali-T760 OpenCL 1.2
$ OPENCV_OPENCL_DEVICE=:gpu ./opencv_test_stitching
(中略)
[  FAILED  ] ExposureCompensate.SimilarityThreshold (56824 ms)

$ OPENCV_OPENCL_DEVICES=disabled ./opencv_test_stitching
(中略)
[       OK ] ExposureCompensate.SimilarityThreshold (93847 ms)
  • 上のコマンドを解説すると、冒頭でOPENCV_OPENCL_DEVICE環境変数を設定しています
  • 1回目のテストはFAILし、2回目の実行ではテストがPASSします
    • 1回目は:gpuという値を環境変数に設定し、これによりOpenCVは内部でGPUがOpenCLに対応しているか確認し、OpenCLがサポートされている環境であればGPUでカーネルを実行します
    • 2回目はdisabledという値を設定し、この場合はOpenCLのカーネルは呼ばれず、CPU版の実行に限定されます
  • OpenCL版の実行でコケ、CPU版では問題なく動くということなのであとは両者の挙動がどこで発生するか二分探索で絞り込んでいくだけです。

発生場所の特定

  • データに違いが発生するところを絞り込み続けると、最終的に以下の1行まで絞り込むことに成功しました
exposure_compensate.cpp
if (!similarities_.empty())
{
    CV_Assert(similarity_it != similarities_.end());
    UMat similarity = *similarity_it++;
    bitwise_and(intersect, similarity, intersect); // ←このbitwise_andがおかしい
}
  • ここでは配列になっているsimilaritiyの各要素にintersectでマスク処理を行っています1
  • ここが原因であることを補強するため、以下のようなコードも試してみました
if (!similarities_.empty())
{
    CV_Assert(similarity_it != similarities_.end());
    UMat similarity = *similarity_it++;
    bool flag = cv::ocl::useOpenCL();   // 現状のフラグを一度退避
    cv::ocl::setUseOpenCL(false);       // OpenCLを一時的にdisable
    bitwise_and(intersect, similarity, intersect); // カーネルを実行
    cv::ocl::setUseOpenCL(flag);        // OpenCLをもとの状態に復帰
}
  • 前後に合計3行追記しました
    • cv::ocl::setUseOpenCL()APIはtruefalseを取り、falseの場合はプラットフォームの対応状況に関わらず、OpenCLのカーネル実行を取りやめます
    • 前述のOPENCV_OPENCL_DEVICE=disabledを実行した場合も、内部的にはcv::ocl::setUseOpenCL(false)を呼んだことと等価です
    • また、cv::ocl::getUseOpenCL()によって現状のOpenCL使用の可否の状況を退避してあるので、このあとの処理に関しても影響が及ばないようになっています
    • 詳しくはdandelion1124先生が書いたUMatの内部処理でも解説されています
  • つまりこのカーネルをGPUで実行するとなにか予期しない結果になる、と結論づけられます

後日談というか今回のオチ

  • この関数の中を追わないといけないのかと思ってたのですが、OpenCV公式メンバの一人、alalek先生から以下のコメントが付きました

OpenCL kernels for operations with "in-place" src/dst buffers should be implemented with another kernels code. This is a bug to be fixed in core module.

  • 意訳すると
    • 「OpenCLの"in-place"な操作(src/dstが同一な状況)は別のカーネルとして実装されるべきです。これはcoreモジュール内で修正されるべきバグです」
    • とのことでした
  • もう少しこの"in-place"について説明すると、このbitwise_and関数は第1引数が入力、第3引数が出力です
  • このとき、第1引数と第3引数は同じポインタですので、同じ番地を指します
bitwise_and(intersect, similarity, intersect);
  • さて、OpenCVからOpenCLのカーネルを呼ぶとき、OpenCLへパラメータを渡す必要がありますが、以下のようにポインタは「書き込み専用」「読み込み専用」「読み書き両方許可」の3種類から属性を選んでポインタとともに渡すことができます
  • 以下は実際に到達するocl_binary_op関数内の実装です
modules/core/src/arithm.cpp
ocl::KernelArg src1arg = ocl::KernelArg::ReadOnlyNoSize(src1, cn, kercn);
ocl::KernelArg dstarg = haveMask ? ocl::KernelArg::ReadWrite(dst, cn, kercn) :
                                   ocl::KernelArg::WriteOnly(dst, cn, kercn);
  • haveMaskfalseなので、dstargにはocl::KernelArg::WriteOnly(dst...)が代入されます
    • このWriteOnlyはポインタとその属性、今回は書き込み専用というフラグ、それから幅の情報などを含めて一つのクラスに変換する関数です
  • 一方で、src1argにはocl::KernelArg::ReadOnlyNoSize(src1...)という値が設定されます
    • 今度はReadOnlyNoSizeという名前の通り、ポインタは読み込み専用として渡されます
  • 一見問題がないように見えますが、同一ポインタが2種類相反するフラグで渡された場合、確かになにか問題が起きそうな気がします
    • ここから先はOpenCLの仕様を調べればわかるかもしれませんが、割愛します
    • 憶測で書けば、「同一ポインタを別フラグで渡された場合の挙動」は実装依存なのではないかと想像します。つまり読み込み専用フラグが優先されるか、書き込み専用フラグが優先されるか、それとも自動的に読み書き両方許可になるのか、そこが実装依存なんだと思います。今回は「読み込み専用」のフラグが優先され、処理は実行されたがメモリに書き戻しが行われず、結果テストFAILにつながったように見えます。(憶測ここまで)
  • というわけで、今回は以下のように迂回策を入れました
// in-place operation has an issue. don't remove the swap
// detail https://github.com/opencv/opencv/issues/19184
Mat_<uchar> intersect_updated;
bitwise_and(intersect, similarity, intersect_updated);
std::swap(intersect, intersect_updated);
  • intersect_updatedというバッファを明示的に事前に設け、カーネル実行直後にstd::swapで参照を入れ替えました2

おわりに

  • OpenCVの4.5.1に含まれるバグを紹介しました。
    • バグが混入したのが4.5.1リリース前、バグfixが4.5.2リリース前なのでリリースされたもののうち、4.5.1だけが影響をうけるバグです
    • また、OPENCV_OPENCL_DEVICE=disabledを指定すれば迂回できます
  • 約2年前のネタを投稿している時点で結構ネタ切れ感が半端ないですが、カレンダーを埋めるために投稿しました。
  • 明日はとうとう最終日です。
  1. Similaritiesが配列。similarityが画像。

  2. 完全に余談ですが、迂回策な変更を入れる場合、OpenCVのメンテナから、将来のコミットで消えないようにissueのURLとともにコメントを追加するように勧められます。

9
0
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
9
0