OpenCV各モジュールの依存関係

11 min read読了の目安(約10400字

OpenCV Advent Calendar 2020 の17日目です。

概要

OpenCVには常軌を逸したほどたくさんのモジュールがあり、それぞれに依存関係があります。
例えば、imgproccore に依存していたり、features2dimgproc に依存している、などです。これをすべて明らかにしてみましょう。

依存関係がわかると、例えば以下のようなうれしい場面があります。

  • OpenCVをビルドする際のCMakeで、必要なモジュールだけを選択することができる。
    • 使わないのを適当に-D BUILD_opencv_hoge=OFF とすると、欲しかったモジュールが実はそれに依存していて連鎖的にOFFになってしまったりする。
  • OpenCVを使用したアプリケーションをビルドする際・動かす際、どのライブラリが必要なのかわかる。
    • リンカのエラーが出て、C++のlibを1個足してみてはリビルド... ってやりますよね?(自分だけ?)

モジュールの一覧

OpenCV本体と、contribを相手にします。

2020/12/06 時点でのGitHub master (およそv4.5)を参照しています。

環境

  • Rust 1.50.0-nightly (1c389ffef 2020-11-24)
  • Graphviz

成果物

実装

後述しますが、実装が面倒だったため出力は若干不正確な点を含みます。ご了承ください。

https://github.com/shimat/opencv_dependency

OpenCV本体のモジュール依存グラフ

破線はOPTIONALな依存を示しています。

coreから依存している cudev や、stitchingから依存している cudaarithm などの cuda系モジュールは、contribにあります。本体からcontribへの逆参照(?)があるのです。(どうなってんだ)
OpenCV Dependencies

contribのモジュール依存グラフ

contribの中だけにとどめ、本体は含めていませんが、それでも小さくて見えないヤバさです。
OpenCV contrib Dependencies

Zennだと見えないので、こちらに置きました。

https://github.com/shimat/opencv_dependency/blob/main/output/opencv_contrib.png

Graphviz (DOT)

OpenCV本体

digraph "OpenCV Dependencies" {
  graph [
    rankdir=RL
  ]
  "calib3d" -> { "imgproc" "features2d" "flann" }
  "core" -> { "cudev" } [style="dashed"]
  "dnn" -> { "core" "imgproc" }
  "features2d" -> { "imgproc" }
  "features2d" -> { "flann" } [style="dashed"]
  "flann" -> { "core" }
  "gapi" -> { "imgproc" }
  "gapi" -> { "video" } [style="dashed"]
  "highgui" -> { "imgproc" "imgcodecs" }
  "highgui" -> { "videoio" } [style="dashed"]
  "imgcodecs" -> { "imgproc" }
  "imgproc" -> { "core" }
  "imgproc" -> { "core" }
  "ml" -> { "core" }
  "objdetect" -> { "core" "imgproc" "calib3d" }
  "photo" -> { "imgproc" }
  "photo" -> { "cudaarithm" "cudaimgproc" } [style="dashed"]
  "stitching" -> { "imgproc" "features2d" "calib3d" "flann" }
  "stitching" -> { "cudaarithm" "cudawarping" "cudafeatures2d" "cudalegacy" "cudaimgproc" } [style="dashed"]
  "ts" -> { "core" "imgproc" "imgcodecs" "videoio" "highgui" }
  "video" -> { "imgproc" }
  "video" -> { "calib3d" "dnn" } [style="dashed"]
  "videoio" -> { "imgproc" "imgcodecs" }
}

contrib

digraph "OpenCV Dependencies" {
  graph [
    rankdir=RL
  ]
  "alphamat" -> { "core" "imgproc" }
  "aruco" -> { "core" "imgproc" "calib3d" }
  "bgsegm" -> { "core" "imgproc" "video" "calib3d" }
  "bioinspired" -> { "core" }
  "bioinspired" -> { "highgui" } [style="dashed"]
  "ccalib" -> { "core" "imgproc" "calib3d" "features2d" "highgui" }
  "cnn_3dobj" -> { "core" "imgproc" }
  "cnn_3dobj" -> { "features2d" "viz" "calib3d" } [style="dashed"]
  "cudaarithm" -> { "core" }
  "cudaarithm" -> { "cudev" } [style="dashed"]
  "cudabgsegm" -> { "video" }
  "cudacodec" -> { "core" "videoio" }
  "cudacodec" -> { "cudev" } [style="dashed"]
  "cudafeatures2d" -> { "features2d" "cudafilters" "cudawarping" }
  "cudafilters" -> { "imgproc" "cudaarithm" }
  "cudaimgproc" -> { "imgproc" }
  "cudaimgproc" -> { "cudev" "cudaarithm" "cudafilters" } [style="dashed"]
  "cudalegacy" -> { "core" "video" }
  "cudalegacy" -> { "objdetect" "imgproc" "calib3d" "cudaarithm" "cudafilters" "cudaimgproc" } [style="dashed"]
  "cudaobjdetect" -> { "objdetect" "cudaarithm" "cudawarping" }
  "cudaobjdetect" -> { "cudalegacy" } [style="dashed"]
  "cudaoptflow" -> { "video" "optflow" "cudaarithm" "cudawarping" "cudaimgproc" }
  "cudaoptflow" -> { "cudalegacy" } [style="dashed"]
  "cudastereo" -> { "calib3d" }
  "cudawarping" -> { "core" "imgproc" }
  "cudawarping" -> { "cudev" } [style="dashed"]
  "cvv" -> { "core" "imgproc" "features2d" }
  "datasets" -> { "core" "imgcodecs" "ml" "flann" }
  "datasets" -> { "text" } [style="dashed"]
  "dnn_objdetect" -> { "core" "imgproc" "dnn" }
  "dnn_objdetect" -> { "highgui" "imgcodecs" } [style="dashed"]
  "dnn_superres" -> { "core" "imgproc" "dnn" }
  "dnn_superres" -> { "quality" } [style="dashed"]
  "dpm" -> { "core" "imgproc" "objdetect" }
  "dpm" -> { "highgui" } [style="dashed"]
  "face" -> { "core" "imgproc" "objdetect" "calib3d" }
  "freetype" -> { "core" "imgproc" }
  "fuzzy" -> { "imgproc" "core" }
  "hdf" -> { "core" }
  "hfs" -> { "core" "imgproc" }
  "img_hash" -> { "imgproc" "core" }
  "intensity_transform" -> { "core" "imgproc" }
  "line_descriptor" -> { "imgproc" }
  "line_descriptor" -> { "features2d" } [style="dashed"]
  "mcc" -> { "core" "imgproc" "calib3d" "dnn" }
  "optflow" -> { "core" "imgproc" "calib3d" "video" "ximgproc" "imgcodecs" "flann" }
  "ovis" -> { "core" "imgproc" "calib3d" }
  "phase_unwrapping" -> { "core" "imgproc" }
  "plot" -> { "core" "imgproc" }
  "quality" -> { "core" "imgproc" "ml" }
  "rapid" -> { "core" "imgproc" "calib3d" }
  "reg" -> { "imgproc" "core" }
  "rgbd" -> { "core" "calib3d" "imgproc" }
  "rgbd" -> { "viz" } [style="dashed"]
  "saliency" -> { "imgproc" "features2d" }
  "sfm" -> { "core" "calib3d" "features2d" "xfeatures2d" "imgcodecs" }
  "shape" -> { "core" "imgproc" "calib3d" }
  "stereo" -> { "imgproc" "features2d" "core" "tracking" }
  "structured_light" -> { "core" "imgproc" "calib3d" "phase_unwrapping" }
  "structured_light" -> { "viz" } [style="dashed"]
  "superres" -> { "imgproc" "video" "optflow" }
  "superres" -> { "videoio" "cudaarithm" "cudafilters" "cudawarping" "cudaimgproc" "cudaoptflow" "cudacodec" } [style="dashed"]
  "surface_matching" -> { "core" "flann" }
  "text" -> { "ml" "imgproc" "core" "features2d" "dnn" }
  "tracking" -> { "imgproc" "core" "video" "plot" }
  "tracking" -> { "dnn" "datasets" "highgui" } [style="dashed"]
  "videostab" -> { "imgproc" "features2d" "video" "photo" "calib3d" }
  "videostab" -> { "cudawarping" "cudaoptflow" "videoio" } [style="dashed"]
  "viz" -> { "core" }
  "xfeatures2d" -> { "core" "imgproc" "features2d" "calib3d" }
  "xfeatures2d" -> { "shape" "ml" "cudaarithm" } [style="dashed"]
  "ximgproc" -> { "core" "imgproc" "calib3d" "imgcodecs" "video" }
  "xobjdetect" -> { "core" "imgproc" "objdetect" "imgcodecs" }
  "xphoto" -> { "core" "imgproc" "photo" }
}

手法

依存関係の調べ方

OpenCVのリポジトリにある CMakeLists.txt に書いてあります。ocv_define_module または ocv_add_module というCMakeの命令を読めばわかります。

例えばhighguiでの例を見てみます。

https://github.com/opencv/opencv/blob/541a09b7acd647ee9a2170c4e3d62272b158a888/modules/highgui/CMakeLists.txt#L5
ocv_add_module(highgui opencv_imgproc opencv_imgcodecs OPTIONAL opencv_videoio WRAP python java)

ここから、以下のように判断できます。

  • このモジュールは highgui
  • 必須の依存モジュールは opencv_imgproc opencv_imgcodecs
  • 無くても可のオプション(OPTIONAL)として opencv_videoio
  • pythonjavaラッパーを構築する

このことから、ocv_define(add)_module の箇所をGitHubから取得してきて解釈すれば行けそうだとわかります。CMakeの機能だけでこのへんのデータを抽出できたりするのかもしれませんが、調べる気が起きなかったので自分で頑張ることにします。

なお、これは見た目で判断しただけで、OpenCVのCMakeLists.txtを全部読み込んだわけではないので細部は間違っているかもしれません。一応以下に参考になる記述は示しておきます。

(参考) ocv_add_module, ocv_define_moduleの定義

https://github.com/opencv/opencv/blob/541a09b7acd647ee9a2170c4e3d62272b158a888/cmake/OpenCVModule.cmake#L124
https://github.com/opencv/opencv/blob/541a09b7acd647ee9a2170c4e3d62272b158a888/cmake/OpenCVModule.cmake#L1057

コメントによると、ocv_define_moduleocv_add_moduleの簡易版のような位置づけらしいです。正直、大して違わないように見えますが・・・。

# declare new OpenCV module in current folder
# Usage:
#   ocv_add_module(<name> [INTERNAL|BINDINGS] [REQUIRED] [<list of dependencies>] [OPTIONAL <list of optional dependencies>] [WRAP <list of wrappers>])
# Example:
#   ocv_add_module(yaom INTERNAL opencv_core opencv_highgui opencv_flann OPTIONAL opencv_cudev)
# short command for adding simple OpenCV module
# see ocv_add_module for argument details
# Usage:
# ocv_define_module(module_name  [INTERNAL] [EXCLUDE_CUDA] [REQUIRED] [<list of dependencies>] [OPTIONAL <list of optional dependencies>] [WRAP <list of wrappers>])

グラフの描画: Graphviz

グラフの描画は Graphviz を使うことにします。Graphvizの詳細は省略しますが、簡単な定義(DOT言語)によってグラフを描画してくれるものです。今回の目標はこのDOT形式の文字列を吐くこととします。

以下は簡単な例です。説明不要と思います。

digraph "OpenCV Dependencies" {
  graph [
    rankdir=RL
  ]
  "imgproc" -> "core"
  "ml" -> "core"
  "features2d" -> "imgproc"  
}

Simple DOT Sample

Graphvizは様々な方法で簡単に触ることができます。環境構築も難しくありませんし、さらに手っ取り早いのは例えば WebGraphviz だと思います。

Graphvizは様々なオプションでグラフをデコれるのですが、本題では無いのでほぼ素の状態で済ませました。ちなみにWebGraphvizはあまりデコるオプションには対応していません。

実装について

再掲ですが、ここに書きました。そちらをご参照ください。勉強がてらRustで書いており、だいぶよろしくない実装とは思います。

https://github.com/shimat/opencv_dependency

処理は単純で、CMakeの記述を少し書き換えてDOT形式にするだけです。
CMakeの例:

ocv_define_module(highgui opencv_imgproc opencv_imgcodecs OPTIONAL opencv_videoio)

DOT出力:

"highgui" -> { "imgproc" "imgcodecs" }
"highgui" -> { "videoio" } [style="dashed"]

制限事項

CMakeLists.txt はかなり表現の自由度が高く、完璧に解釈はできていません。例えばこれはstitchingのCMakeLists.txtの例です。

set(STITCHING_CONTRIB_DEPS "opencv_xfeatures2d")
if(BUILD_SHARED_LIBS AND BUILD_opencv_world AND OPENCV_WORLD_EXCLUDE_EXTRA_MODULES)
  set(STITCHING_CONTRIB_DEPS "")
endif()
ocv_define_module(stitching opencv_imgproc opencv_features2d  opencv_flann
                  OPTIONAL opencv_cudaarithm opencv_cudawarping opencv_cudafeatures2d opencv_cudalegacy opencv_cudaimgproc ${STITCHING_CONTRIB_DEPS}
                  WRAP python)

STITCHING_CONTRIB_DEPSという変数を利用していますが、この解釈は諦めてスキップしています。従って成果物として示した依存グラフではopencv_xfeatures2dへの依存が欠けていると思います。

その他、CMakeのコメント#が途中に有ったり改行が自在に入ったり、INTERNALって何だろう...などいろいろ完璧にするには壁が多いです。

また、python, java, objcなどラッパーモジュールは除外しました。自分的に今回求めたいものではなかったというのもありますが、ocv_define_moduleの解釈がまた面倒だったのが主な要因です。

おわりに

  • 何度でも言いますがcontribはヤバい。どこまでモジュールを増やすんでしょう。contribの中でも依存の階層構造が結構あるのが驚きでした。
  • 調べるモジュールのリストはハードコーディングしており、本当はGit(Hub)をスクレイピングして動的に調べられるとより良いですね。
  • この依存グラフをインタラクティブに触れる感じなSPAでも作って、必要なモジュールをGUIでポチポチ押したらOpenCVビルド用のCMakeパラメータを吐いてくれる、みたいなのが欲しい機運です。まじめな話、contribがいくら肥大しようともCMakeで欲しいのだけ好きに選べばいいよねという整理なのでしょうが、最初に述べたように各モジュールのON/OFFは依存関係の正確な理解を要します。これだけ依存が複雑ですとそれもままならないレベルです。
  • stitching はcudaに依存しすぎと判明したのでcontribに追いやってほしいですね。
  • cvとcxcoreだけだった時代に戻ってほしい老害です。

この記事に贈られたバッジ