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

OpenCVで使われてるCMakeレシピ

More than 1 year has passed since last update.

はじめに

  • これは、OpenCV Advent Calendar 2018 19日目の記事です。12日目の記事です。空いてるのが寂しかったので前倒しで公開することにしました。
  • 関連記事は目次にまとめられています。
  • なお、本記事は筆者個人の意見であり、筆者の所属組織とは無関係です。
  • 先日、こんなことがありました。

TL;DR

  • OpenCVはビルド時にCMakeが使われるわけですが、様々なプラットフォームでトラブルなくビルドするためにあんな技やこんな技が使われていますよ、というお話。
  • @kumagi先生のおっしゃるように、OpenCVを読み解くとCMakeの使い方がわかる、って気もします。

cvconfig.h

  • 本題のkumagi先生が言っていた、config.hというのは、要は下記みたいなヘッダのことだと思っています。
config.h
#define HAVE_CAMERA_LIBRARY
#define HAVE_MATRIX_LIBRARY
#define HAVE_ETHER_GRAB_LIBRARY
//#define HAVE_EIGEN
  • このようなconfig.hがあったとして、全てのソースコードからこいつをincludeすれば、特定の機能をON/OFFすることが容易になります。
  • 私も学生時代、個人でソースコードを管理してましたが、当時の所属研究室ではマシンは共用で、日によって席を変えてビルドすることもあり、そうすると
    • 「このマシンにはこのライブラリが入ってない」
    • 「このマシンにはこのグラバのドライバが入ってない」
  • などの細かい差異でビルドができたりできなかったりしました。
  • なので前述のconfig.hみたいなのを用意してビルドできない問題を回避したりしてました。
  • このアプローチ自体はよくある話だと思いますし、この記事を読んでる方も「あるある」と頷いている人が多いのではないでしょうか。
  • 問題は、このconfig.hをどうやってメンテナンスするのか、ということです。
  • 前述のconfig.hは割と短いので、メンテナンスは大したこと無いですが、OpenCVみたいなライブラリを、どこでもオートマチックにビルドするためには、メンテナンスが大変です。

そこでCMakeですよ

  • CMakeには、このようなファイルを自動生成するための、configure_fileコマンドがあります。

  • configure_file — CMake 3.0.2 Documentation

  • このコマンドを使うと、CMakeの変数をC/C++の形に編集してくれるのです。

  • 実際の例を見てみましょう。OpenCVでは、以下のようなcvconfig.hがあります。当然中身は環境によって異なります。

cvconfig.h
#ifndef OPENCV_CVCONFIG_H_INCLUDED
#define OPENCV_CVCONFIG_H_INCLUDED

/* OpenCV compiled as static or dynamic libs */
#define BUILD_SHARED_LIBS

/* OpenCV intrinsics optimized code */
#define CV_ENABLE_INTRINSICS

/* OpenCV additional optimized code */
/* #undef CV_DISABLE_OPTIMIZATION */

/* Compile for 'real' NVIDIA GPU architectures */
#define CUDA_ARCH_BIN " 61"

/* OpenCL Support */
#define HAVE_OPENCL
/* #undef HAVE_OPENCL_STATIC */
/* #undef HAVE_OPENCL_SVM */

/* OpenEXR codec */
#define HAVE_OPENEXR
  • 当たり前ですが、このファイルはバージョン管理されていません。ですので、リポジトリにはcvconfig.hは存在しません。
  • その代り、cmake実行完了時点で自動生成されます。
  • cvconfig.hは存在しないと書きましたが、自動生成される前の「ファイルのもと」は存在していて、バージョン管理もされています。 cmake/templates/cvconfig.h.in (OpenCV 4.0.0版) がそうです。
  • ちょっと覗いてみましょう。
cvconfig.h.in
#ifndef OPENCV_CVCONFIG_H_INCLUDED
#define OPENCV_CVCONFIG_H_INCLUDED

/* OpenCV compiled as static or dynamic libs */
#cmakedefine BUILD_SHARED_LIBS

/* OpenCV intrinsics optimized code */
#cmakedefine CV_ENABLE_INTRINSICS

/* OpenCV additional optimized code */
#cmakedefine CV_DISABLE_OPTIMIZATION

/* Compile for 'real' NVIDIA GPU architectures */
#define CUDA_ARCH_BIN "${OPENCV_CUDA_ARCH_BIN}"

/* OpenCL Support */
#cmakedefine HAVE_OPENCL
#cmakedefine HAVE_OPENCL_STATIC
#cmakedefine HAVE_OPENCL_SVM

/* OpenEXR codec */
#cmakedefine HAVE_OPENEXR
  • 先程のcvconfig.hと見比べると、似ていますが、微妙に違います。#cmakedefine という、一見defineに見えるコマンドがずらりと並んでいます。そして、その右側にあるのはCMakeでも使われている変数があります。
  • このファイルを生成しているのはここです。
OpenCVGenHeaders.cmake
# platform-specific config file
configure_file("${OpenCV_SOURCE_DIR}/cmake/templates/cvconfig.h.in" "${OPENCV_CONFIG_FILE_INCLUDE_DIR}/cvconfig.h")
configure_file("${OpenCV_SOURCE_DIR}/cmake/templates/cvconfig.h.in" "${OPENCV_CONFIG_FILE_INCLUDE_DIR}/opencv2/cvconfig.h")
  • このファイルでは、configure_fileコマンドを呼び、その引数としてcvconfig.h.in(先程見た「ファイルのもと」)と、config.hの出力先の2つを指定しています。
  • こうすることで、CMake内の変数をもとに、defineが有効になったり無効になったファイルができあがる訳です。
  • これにより、cvconfig.hを管理しないでも、自動生成できます。かなり便利な機能ですので是非皆様も使ってみて下さい。
config.h を自作するなら、CMakeのconfigure_fileコマンドを使おう

inline と __inline__ と __inline

https://github.com/opencv/opencv/blob/master/3rdparty/libtiff/CMakeLists.txt#L39
foreach(inline_keyword "inline" "__inline__" "__inline")

  • C++の予約語の一つinline関数はご存知でしょうか。関数呼び出しでなく、関数自体は呼び元に埋め込むことで、主に実行速度の高速化に寄与するためのオプションです。
  • 実際にインライン展開(埋め込み)が行われるかどうかはコンパイラが判断するのですが、そのコンパイラのキーワードが実は決まって無い問題があるのです。
  • 前述の通りC++ではinlineというキーワードが決まっていますが、古いCコンパイラの規約にはこのinlineは無いのです。1
  • で、コンパイラによってはこのinlineキーワードを独自に対応していたりします。規約で決まっていないため、コンパイラによって、inlineだったり__inline__だったり__inlineだったりします。
  • こういうコンパイラ依存の機能を使う場合、よくある対処法はコンパイラごとにifdefで区切るアプローチです
sampleifdef.h
#if defined __GNUC__
#define FOO  Bar
#elif defined _MSC_VER
#define FOO  foo
#else
#define FOO
#endif
  • ifdef __GNUC___みたいなプリプロセッサで切り分けることが行われます。が、OpenCVでは2一味違ったアプローチが行われます。
  • OpenCV内の3rdparty以下にあるlibtiff内にはCのソースコードが含まれており、そこではinline関数が宣言されています。
  • そして、その時の識別子はinlineです。
tif_fax3.c
inline static int32
find0span(unsigned char* bp, int32 bs, int32 be)
  • これだとGCCでは識別子がinlineなので良いのですが、Visual Studio 2013とそれ以前では識別子が__inlineのため、このままだとコンパイルエラーになる(はず)です。
  • ところが実際にはなりません
  • 実際にはこのファイルからインクルードされるtif_config.hに以下のような記述があります。なお、このtif_config.hはCMake時に自動生成されるファイルです。
tif_config.h
/* Define to `__inline__' or `__inline' if that's what the C compiler
   calls it, or to nothing if 'inline' is not supported under any name.  */
#ifndef __cplusplus
#define inline inline
#endif
  • では、Visual Studio 2013ではどうなるのか、実際に見てみましょう。
tif_config.h
/* Define to `__inline__' or `__inline' if that's what the C compiler
   calls it, or to nothing if 'inline' is not supported under any name.  */
#ifndef __cplusplus
#define inline __inline
#endif
CMakeLists.txt
foreach(inline_keyword "inline" "__inline__" "__inline")
  if(NOT DEFINED C_INLINE)
    set(CMAKE_REQUIRED_DEFINITIONS_SAVE ${CMAKE_REQUIRED_DEFINITIONS})
    set(CMAKE_REQUIRED_DEFINITIONS ${CMAKE_REQUIRED_DEFINITIONS}
        "-Dinline=${inline_keyword}")
    check_c_source_compiles("
        typedef int foo_t;
        static inline foo_t static_foo() {return 0;}
        foo_t foo(){return 0;}
        int main(int argc, char *argv[]) {return 0;}"
      C_HAS_${inline_keyword})
  • キーポイントは、1行目に列挙されてるinline, __inline__, __inline識別子と、check_c_source_compilesコマンドです。
  • このコマンドは、ソースコードとオプションを渡し、実際にコンパイラでコンパイルした成否の結果を返す関数です。
  • これにより、CMake時点でコンパイラがどの識別子を使うのか、はたまたinline関数非対応なのかを知ることができます。
  • 前述の通り、コード自体はスッキリしたまま、ゴテゴテしたifdefを隠せるのは便利だと思います。
check_c_source_compiles を活用してifdefを減らそう!

ビルド時ダウンロード

  • 状況は様々だと思いますが、「このバイナリはダウンロードして配置するだけ」なバイナリってよくあると思います。
  • プログラムに埋め込むアイコンだけだったら良いのですが、往々にしてこういうバイナリをリポジトリに追加すると雪だるま式にみんな追加し始めて手に負えなくなります。
Gitにバイナリ、ダメ絶対。
  • とは言え、手間を考えると、バイナリをリポジトリに追加したくなるのも人情です。3
  • 手間を考えるとリポジトリにバイナリを追加したいし、リポジトリのサイズを考えるとこのバイナリはリポジトリに入れたくない。
  • こんなジレンマにハマったときは、個人的にOpenCVでどう対処してるかを参考にしています。
  • で、OpenCVでも実はIPPとかWindows用ffmpegとか、バイナリをビルド時に利用しています。
  • では、このバイナリはどこから来てるのか?
  • 結論から言えば、OpenCVではバイナリ格納専用リポジトリを用意して、そこにはどんどんバイナリファイルがpushされています。そのリポジトリがopencv_3rdpartyです。4
  • しかし、皆さんopencv_3rdpartyの存在をご存知なかったように、ここから手動でビルド済みバイナリをダウンロードされた方はまずいないと思います。
  • でも現にOpenCVではビルド時にIPPffmpegなどのビルド済みバイナリをダウンロードしてきます。では、どうやってダウンロードを実現しているのか?
はい、CMakeでやっています。
  • 具体的にはcmake/OpenCVDownload.cmakeに記述があります。
#  Download and optionally unpack a file
#
#  ocv_download(FILENAME p HASH h URL u1 [u2 ...] DESTINATION_DIR d [ID id] [STATUS s] [UNPACK] [RELATIVE_URL])
  • このocv_download関数内ではfileコマンドを呼んでいます
    file(DOWNLOAD "${DL_URL}" "${CACHE_CANDIDATE}"
         INACTIVITY_TIMEOUT 60
         TIMEOUT 600
         STATUS status
         LOG __log)
  • 具体的には、fileコマンドのDOWNLOADオプションを利用していて、これにはURL、ダウンロード用ディレクトリ、MD5ハッシュ値なんかを渡すと、
  1. 指定URLから指定したディレクトリにダウンロード
  2. MD5ハッシュ値を計算してバイナリを検証
  3. ファイルを展開
  • ぐらいまで自動でやってくれます。
  • なおOpenCVでは後方互換5のために、このあたりの機能は利用しておらず、自前で関数に組み込んでいます。しかし3.0以降のCMakeでは上記のオプションは全てよしなに取り扱ってくれます
  • なお、似た機能ではgit submoduleがあります。
  • これはあるリポジトリに、別のリポジトリの特定のコミットを結びつける方法で、複数のオープンソースプロジェクトを連携させるときに使ったりします。が、正直地獄を見ると思います。
  • 個人的にgit submoduleは百害あって一利なしで、以下のsakanazensenさんのツイートに私も強く同意します。
CMakeのFILEコマンドを使って便利に開発しましょう。

dispatch

  • OpenCV 3.3から、dispatch機能が追加されました。特定の命令セットを使わないように、OpenCVのロード時(≠ビルド時)に指定できる機能で、古いプロセッサ用にAVX命令をdisableしてOpenCVをビルドし直し、みたいな悲劇がなくなりました。
  • このdispatchはかなりCMakeの芸術的な出来になっているので、このdispatch機能がどうやって成り立っているか、明日別記事で解説しようと思います。(日付変更に伴い文章変更)

おわりに


  1. C99からinlineが規格に盛り込まれた模様 

  2. というか厳密にはlibtiffライブラリ 

  3. 余談ですが、私は数十KBのパワーポイントファイルとPDFをリポジトリに追加するのに猛反発して、結果昨年のOpenCVアドベントカレンダーに寄稿した「 ビルドしただけでパワーポイントを生成するCMake」が誕生しました 

  4. 厳密に言えばopencv_extraリポジトリもバイナリがどんどんpushされてるので、バイナリ用のリポジトリが2つもあります。 

  5. OpenCV 4.0からCMakeの最低バージョンが3.5.1に引き上げられましたが、3.0系や2.4系では2.8.12.2が最低バージョンであり、そのとき使えなかったコマンドが独自コマンドで提供されています。 

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