Python
CMake
OpenCV
OpenCVDay 21

Python bindingのエラーを修正しながら仕組みを追う気の遠くなるような長い長い旅

はじめにのはじめに

本記事は超絶マニアックな誰得な作業ログです。読者を置いていくことが目標と言っても過言ではありません。記事のクオリティの低さたるや、格別です。

さて、pythonからOpenCVを使うというのは大分一般的になってきたと思います。ここで、多くの人は配布されているバイナリを使っていますが、contribや動画処理など、デフォルトでは入っていない機能を使おうとすると、ビルドが必要になります。

すると、大抵python bindingでコケて沼にはまります。コケた際に、OpenCVのcmakeファイルを読んで自力で解決している人は、非常に少ないのでしょうか。でも、人生、cmakeファイルを読まなくてはいけない時があるんです。本記事は、そんな時に、俺はこうやって解決した!という作業ログです。人それぞれエラーは違うので、そのまま適用できるケースは無いと思いますが、気合だけでも伝わればと思います。

ちなみにマニュアルを読めば早(ry

はじめに

よくこんな質問が寄せられます。
Q: OpenCVをビルドしたいのに、python bindingで失敗する

答えは、
A: cmake fileを読む
です。

失敗ケースとしては、condaを使ったら動かない、Macだと動かない・・・など色々ありますが、今回は各ケースに対する優しいケーススタディではなく、cmakeをガチムチで読むという話です。

最初のビルド

まずは成功画面、cmakeするとこんな感じでpython-bindingに成功します。

$ mkdir build
$ cd build
$ cmake ..

image.png

という事で、めでたし!・・という感じで記事を締めたいところですが、多くの場合はこんな感じで失敗します。

image.png

よくある原因は、conda内のpythonを使っていると、OpenCVがリンクを見つけられない事に起因します。こんな時の調査手順を載せます。なお、今回は、ワザと失敗させているため、色々未インストールのものとかも準備しておき、ログに出ても気づかなかった事にします

失敗表示されている場所

エラーメッセージでひとまずgitリポジトリ内を検索するとroot dierectory直下のCMakeLists.txtの最後の方にこんな記述が見られます。

CMakeList.txt
if(BUILD_opencv_python3)
status("")
status("  Python 3:")
status("    Interpreter:"     PYTHON3INTERP_FOUND  THEN "${PYTHON3_EXECUTABLE} (ver ${PYTHON3_VERSION_STRING})"       ELSE NO)
if(PYTHON3LIBS_VERSION_STRING)
  status("    Libraries:"   HAVE_opencv_python3  THEN  "${PYTHON3_LIBRARIES} (ver ${PYTHON3LIBS_VERSION_STRING})"   ELSE NO)
else()
  status("    Libraries:"   HAVE_opencv_python3  THEN  "${PYTHON3_LIBRARIES}"                                      ELSE NO)
endif()
status("    numpy:"         PYTHON3_NUMPY_INCLUDE_DIRS THEN "${PYTHON3_NUMPY_INCLUDE_DIRS} (ver ${PYTHON3_NUMPY_VERSION})" ELSE "NO (Python3 wrappers can not be generated)")
status("    packages path:" PYTHON3_EXECUTABLE         THEN "${PYTHON3_PACKAGES_PATH}"                                    ELSE "-")
endif()

ふむふむ、Librariesとnumpyが見つからないと言われているということは、HAVE_opencv_python3PYTHON3_NUMPY_INCLUDE_DIRSが空か、OFFになっているっぽいな、となります。

念のため、この付近で、上記2変数を表示させて見ましょう。

CMakeList.txt
if(BUILD_opencv_python3)
  status("HAVE_open_CV3????? ${HAVE_opencv_python3}")
  status("NUMPY dir????? ${PYTHON3_NUMPY_INCLUDE_DIRS}")
  status("")

とすると、

image.png

と表示されました。というわけで、HAVE_opencv_python3をONにするのと、PYTHON3_NUMPY_INCLUDE_DIRSにディレクトリ名を設定する旅に出ましょう。

注意点として、今回はcmakeファイルをいじりながらログを確認していきますが、前回時のcmakeのログが残ってしまうため、チェックの際はbuildディレクトリ以下は毎回全部消した方が確実です。

HAVE_opencv_python3をONにする旅

さて、また同じように、gitリポジトリ内を検索してみましょう。

image.png

 無い・・・・だと・・・・?

image.png

冗談です。頑張ります。手がかりを探すため、まずはCMakeLists.txtの中からpythonを検出している入り口を探しに行きます。すると、こんな記述が・・・!

CMakeList.txt
# --- Python Support ---
if(NOT IOS)
  include(cmake/OpenCVDetectPython.cmake)
endif()

さて、ここでPythonを探しているかを確認するため、この前後で、変数HAVE_opencv_python3PYTHON3_EXECUTABLEがどのように変化しているかを見てみましょう。

CMakeList.txt
if(NOT IOS)
status("HAVE_open_CV3????? ${HAVE_opencv_python3}") <- HAVE_opencv_python3を表示
status("exec????? ${PYTHON3_EXECUTABLE}") <- PYTHON3_EXECUTABLEを表示
include(cmake/OpenCVDetectPython.cmake)
status("HAVE_open_CV3????? ${HAVE_opencv_python3}")
status("exec????? ${PYTHON3_EXECUTABLE}")
endif()

image.png

上の????とあるところでは、exec側が何も設定されていませんが、下では/opt/conda/bin/python3/が設定されています。というわけで、ここで何かが起きていることは確認できます。なお、HAVE_opencv_python3はこの段階ではまだ何も入っておらず、この行より下で、何かのフラグに従ってON/OFFが決まる模様です。どこでHAVE_opencv_python3が生成されたかを引き続き探りましょう。と言ってもpythonの文字も特になく手がかりがありません。

各行に、messageを入れてログを確認しまくります。

message("1: HAVE_open_CV3????? ${HAVE_opencv_python3}")

こんな感じのコードを入れまくって、OFFに切り替わる瞬間を探します。・・・すると、以下の所でOFFフラグが生成されることがわかります。

CMakeList.txt
# OpenCV modules
message("321: HAVE_open_CV3????? ${HAVE_opencv_python3}")
add_subdirectory(modules)
message("322: HAVE_open_CV3????? ${HAVE_opencv_python3}")

一番上では、HAVE_open_CV3にはフラグが入っていませんが、一番下ではOFFが入力されています。

image.png

ということで、

add_subdirectory(modules)

がキーであることが分かります。まずはここを深掘って、HAVE_opencv_python3が切り替わる瞬間を探しましょう。modulesディレクトリ内のCMakeファイルを確認すると、内容はとてもシンプルで、

opencv/modules/CMakeLists.txt
add_definitions(-D__OPENCV_BUILD=1)

if(NOT OPENCV_MODULES_PATH)
  set(OPENCV_MODULES_PATH "${CMAKE_CURRENT_SOURCE_DIR}")
endif()

ocv_glob_modules(${OPENCV_MODULES_PATH} EXTRA ${OPENCV_EXTRA_MODULES_PATH})

となっています。ocv_glob_modulesが具体的に何かを処理しているようですね・・・ということで検索。・・・すると、cmake/OpenCVModule.cmakeの中で定義されているようです。中を軽く読んでみましょう。

長いけど、今回はHAVE_を探す旅なので、細かいところは飛ばしつつ、ファイル全体を見て行きましょう。とりあえず、ざっくりとHAVEの文字を探すと、たくさんヒットする。ここのどこかでHAVE_opencv_python3がON/OFFされる予感がします。さて、ocv_glob_modules関数を見てみると、大まかに、collect modules, __ocv_resolve_dependencies, create modulesから成っています。最初と最後は、add_subdirectoryで飛ばしているだけ、__ocv_resolve_dependenciesが怪しそうなので中を見てみましょう。

すると、すぐ見つかりました。

cmake/OpenCVModule.cmake
  foreach(m ${OPENCV_MODULES_DISABLED_USER})
    set(HAVE_${m} OFF CACHE INTERNAL "Module ${m} will not be built in current configuration")
  endforeach()
  foreach(m ${OPENCV_MODULES_BUILD})
    set(HAVE_${m} ON CACHE INTERNAL "Module ${m} will be built in current configuration")
  endforeach()

ここが、怪しい。ということで、お決まりのmessageチェックをしてみましょう。前後をさっきのmessageコマンドで挟む。

image.png

...人生は厳しい

ここに来た時点で、既にOFFが入っていた。もう少し前に戻ってみよう。最初のブロックのどこかがターゲットなので、ひたすらmessageを入れる。結果として、ここがターゲットであることが分かりました。

cmake/OpenCVModule.cmake
  message("4 HAVE_open_CV3 in ocv_resolve_dependencies ????? ${HAVE_opencv_python3}")
message("${__modpath} ${CMAKE_CURRENT_BINARY_DIR}/${mod}/.${mod}")
            add_subdirectory("${__modpath}" "${CMAKE_CURRENT_BINARY_DIR}/${mod}/.${mod}")
  message("5 HAVE_open_CV3 in ocv_resolve_dependencies ????? ${HAVE_opencv_python3}")

image.png

何をやっているか見てみましょう。

message("${__modpath} ${CMAKE_CURRENT_BINARY_DIR}/${mod}/.${mod}")

とすると・・・

image.png

みたいな感じで、/opencv/modules/pythonというディレクトリをaddすると、目的のフラグがON/OFFになるらしい。なお、後者の.pythonはadd_subdirectoryの定義上ここでは不要。というわけで、/opencv/modules/pythonを覗きましょう。

まず、/opencv/modules/pythonを見ると、CMakeLists.txtがいますので、ここの中に入って見ましょう。念のため、入り口と出口で目的のフラグが変化するかを確認するため、messageデバッグしましょう。ファイルの先頭と、最後でメッセージを表示させる。

image.png

というわけで、このどこかで目的のフラグがON/OFFされているらしいことが分かる。いつものようにmessageを出しまくって確認すると、

add_subdirectory(python3)

でフラグが変わることが分かる。さて、/opencv/modules/python/python3ディレクトリを漁って行きましょう。

/modules/python/python3/CMakeLists.txt
message("in /modules/python/python3/CMakeLists.txt 1 ????? ${HAVE_opencv_python3}")
if(NOT PYTHON3_INCLUDE_PATH OR NOT PYTHON3_NUMPY_INCLUDE_DIRS)
  ocv_module_disable(python3)
endif()
message("in /modules/python/python3/CMakeLists.txt 2 ????? ${HAVE_opencv_python3}")

set(the_description "The python3 bindings")
set(MODULE_NAME python3)
set(MODULE_INSTALL_SUBDIR python3)

set(PYTHON PYTHON3)

message("in /modules/python/python3/CMakeLists.txt 3 ????? ${HAVE_opencv_python3}")
include(../common.cmake)

message("in /modules/python/python3/CMakeLists.txt 4 ????? ${HAVE_opencv_python3}")
unset(MODULE_NAME)
unset(MODULE_INSTALL_SUBDIR)

message("in /modules/python/python3/CMakeLists.txt head ????? ${HAVE_opencv_python3}")
if(MSVC)
  ocv_warnings_disable(CMAKE_CXX_FLAGS /wd4996)
else()
  ocv_warnings_disable(CMAKE_CXX_FLAGS -Wdeprecated-declarations)
endif()
message("in /modules/python/python3/CMakeLists.txt head ????? ${HAVE_opencv_python3}")

こんな感じで、ところどころmessageを入れて、cmakeすると最初のmessageの時点で

image.png

と、一行表示されただけで他の行まで行きませんでした。なので、PYTHON3_INCLUDE_PATHPYTHON3_NUMPY_INCLUDE_DIRSが定義されていないと、ここで終了となるようです。

/modules/python/python3/CMakeLists.txt
if(NOT PYTHON3_INCLUDE_PATH OR NOT PYTHON3_NUMPY_INCLUDE_DIRS)
  ocv_module_disable(python3)
endif()

さて、フラグがセットされて、終了する様子を確認しましょう。ocv_module_disableを探して見てみると・・・

opencv/cmake/OpenCVModule.cmake
set(HAVE_${__modname} OFF CACHE INTERNAL "Module ${__modname} can not be built in current configuration")

という感じでHAVE_opencv_python3がOFFにされるのを確認できました。

さて、ここで目的は、PYTHON3_INCLUDE_PATHの設定と、PYTHON3_NUMPY_INCLUDE_DIRSの設定に変わります。これらの変数に何が入っているか、/modules/python/python3/CMakeLists.txtの先頭に以下を追加して確認すると・・・

/modules/python/python3/CMakeLists.txt
message("PYTHON3_INCLUDE_PATH ${PYTHON3_INCLUDE_PATH}")
message("PYTHON3_NUMPY_INCLUDE_DIRS ${PYTHON3_NUMPY_INCLUDE_DIR}")

image.png

てな感じで、NUMPYが未設定となっています。つまりNumpyを先に入れないと、HAVE_opencv_python3のチェックまで進めないことが分かります。

ちなみに、PYTHON3_INCLUDE_PATHが入っていない場合、apt-getなどでpythonを入れている場合は、python-devを入れれば、ここに値が入るはず。入らなければ、頑張って探しましょう!

進路変更、PYTHON3_NUMPY_INCLUDE_DIRを入れる旅に出る

さて、これについては、最初から最後まで何も設定されないので手掛かりがありません。とりあえず検索してみましょう。

image.png

なお、rootなのはDockerで作業しているからです。この中で、今まで通ってきた経路を選びましょう。怪しいのは、

cmake/OpenCVDetectPython.cmake
modules/python/common.cmake

でしょう。他のファイルは、これまでに確認済みで、NUMPYに設定するような記述はなかったはず。また、後者は、実はopencv/modules/python/python3/CMakeLists.txtの真ん中でincludeされており、まだ通っていない。という訳で、cmake/OpenCVDetectPython.cmakeを読みましょう。

さて、ザーッと読んでみると、こんな構成です。

opencv/cmake/OpenCVDetectPython.cmake
function(find_python preferred_version min_version library_env include_dir_env
         found executable version_string version_major version_minor
         libs_found libs_version_string libraries library debug_libraries
         debug_library include_path include_dir include_dir2 packages_path
         numpy_include_dirs numpy_version)
.
.
.
endfunction(find_python)

find_python(2.7 "${MIN_VER_PYTHON2}" PYTHON2_LIBRARY PYTHON2_INCLUDE_DIR
    PYTHON2INTERP_FOUND PYTHON2_EXECUTABLE PYTHON2_VERSION_STRING
    PYTHON2_VERSION_MAJOR PYTHON2_VERSION_MINOR PYTHON2LIBS_FOUND
    PYTHON2LIBS_VERSION_STRING PYTHON2_LIBRARIES PYTHON2_LIBRARY
    PYTHON2_DEBUG_LIBRARIES PYTHON2_LIBRARY_DEBUG PYTHON2_INCLUDE_PATH
    PYTHON2_INCLUDE_DIR PYTHON2_INCLUDE_DIR2 PYTHON2_PACKAGES_PATH
    PYTHON2_NUMPY_INCLUDE_DIRS PYTHON2_NUMPY_VERSION)

find_python(3.4 "${MIN_VER_PYTHON3}" PYTHON3_LIBRARY PYTHON3_INCLUDE_DIR
    PYTHON3INTERP_FOUND PYTHON3_EXECUTABLE PYTHON3_VERSION_STRING
    PYTHON3_VERSION_MAJOR PYTHON3_VERSION_MINOR PYTHON3LIBS_FOUND
    PYTHON3LIBS_VERSION_STRING PYTHON3_LIBRARIES PYTHON3_LIBRARY
    PYTHON3_DEBUG_LIBRARIES PYTHON3_LIBRARY_DEBUG PYTHON3_INCLUDE_PATH
    PYTHON3_INCLUDE_DIR PYTHON3_INCLUDE_DIR2 PYTHON3_PACKAGES_PATH
    PYTHON3_NUMPY_INCLUDE_DIRS PYTHON3_NUMPY_VERSION)

ここで、pythonを探してるらしいことはわかります。find_pythonの定義をよんでみると、

opencv/cmake/OpenCVDetectPython.cmake
#   numpy_include_dirs (variable): Output of found Python Numpy include dirs
#   numpy_version (variable): Output of found Python Numpy version

という記述が。という訳で、numpy_include_dirsに値がセットされているタイミング、条件を確認してみます。

まず、明らかに中間にある

opencv/cmake/OpenCVDetectPython.cmake
set(_numpy_include_dirs ${${numpy_include_dirs}})

が怪しいですね。_numpy_include_dirsを追っかけると、すぐ下に、

opencv/cmake/OpenCVDetectPython.cmake
execute_process(COMMAND "${_executable}" -c "import os; os.environ['DISTUTILS_USE_SDK']='1'; import numpy.distutils; print(os.pathsep.join(numpy.distutils.misc_util.get_numpy_include_dirs()))"
                          RESULT_VARIABLE _numpy_process
                          OUTPUT_VARIABLE _numpy_include_dirs
                          OUTPUT_STRIP_TRAILING_WHITESPACE)

という表記が!という訳で、ここにデバッグコードを入れて何をやっているか確認してみましょう。

message("${_executable} -c import os; os.environ['DISTUTILS_USE_SDK']='1'; import numpy.distutils; print(os.pathsep.join(numpy.distutils.misc_util.get_numpy_include_dirs()))")

というのを直前に入れると、

image.png

と出ました。ということで、numpyを呼び出そうとしたら、import errorが出ていることが確認できました。そこで、ようやくnumpyをインストールしてみます!(そんくらい先にやっとけというツッコミは、今回はそういう記事なので・・・ということで)

image.png

無事、エラーが消えました!さて、PYTHON3_NUMPY_INCLUDE_DIRがセットされたかを確認しましょう。一番最初のCMakeの画面に戻ると・・・・

image.png

numpyに値がセットされていました!!めでたし!さて、困ったことに、実はこれだけが原因だったらしく、pythonが認識されてしまいました。これだけだとnumpyインストールするだけかよ!となってしまいます。

ちなみに、結局HAVE_opencv_python3がどこでON/OFFになるか見つける前に動いてしまった。

が、ここまでは初心者コース、macに鞍替えしてもう一回やってみましょう

Macでやってみる。

めちゃめちゃ汚い環境と化している手元のMacでもう一度確認してみますと、この有様。何も見つからない、素晴らしいですね。

image.png

pythonはどこに行ったレベル。ちなみにnumpyもpythonも入っています。さて、もう一度同じ手順を踏む訳ですが、正直全部書くのが面倒になってきたので、message命令を入れまくって見つかった事にします

さて、先ほどのOpenCVDetectPython.cmakeで色々とチェックしているのがわかりましたので、ここを見ていきましょう。全部認識されていないので、頭から確認します。

cmake/OpenCVDetectPython.cmake
if(NOT ${found})
  message(WARNING, "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ${executable}")
  message(WARNING, "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ${${executable}}")
  ocv_check_environment_variables(${executable})

とすると

image.png

と表示されます。これは、executableには、PYTHON3_EXECUTABLEが入っており、ここに、何か値をセットすると、${${executable}}とすると、その値が実施されます。さて、PYTHON3_EXECUTABLEはどこでセットされるか。検索するとマニュアルがヒットし、

-# [optional] Building python. Set the following python parameters: 
- PYTHON2(3)_EXECUTABLE = <path to python> 
- PYTHON_INCLUDE_DIR = /usr/include/python<version> 
- PYTHON_INCLUDE_DIR2 = /usr/include/x86_64-linux-gnu/python<version> - PYTHON_LIBRARY = /usr/lib/x86_64-linux-gnu/libpython<version>.so 
- PYTHON2(3)_NUMPY_INCLUDE_DIRS = /usr/lib/python<version>/dist-packages/numpy/core/include/

と、cmakeコマンドを使う際に自分で入れろ、と書いてあります。

$ which python
/Users/peisuke/*************/python

として、自分のpythonのパスを調べて、cmake実施時に、コマンドに追加しましょう。

-D PYTHON3_EXECUTABLE=/Users/peisuke/*************/python

みたいに。すると・・・

image.png

として、実行スクリプトが認識されました!Summaryの所も、

image.png

ちょっと情報が増えました。次にいきましょう。SummaryからはInterpreterが発見されているけど、それ以外がないと言われているので下の方を見ていきましょう。すると、

cmake/OpenCVDetectPython.cmake
find_package(PythonLibs...)

みたいな記述があるので、その後ろに

      message(WARNING, "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ${PYTHON_LIBRARY}")
      message(WARNING, "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ${PYTHON_INCLUDE_DIR}")

を仕込んで見ると、

image.png

として、Includeディレクトリ、とLibが見つかっているのは確認できます。Libraryが見つかっているのに、Summaryに表示されないということは、先ほどと同様Numpyが原因と考えられます。先ほどはNumpyのインストールだけで認識されましたが、今回は既にNumpyは入っているのに見つからない。Mac+Conda+pyenvとかゴチャゴチャ入っていて、うまくincludeパスが見つかっていない模様です。なので、手動でnumpyのディレクトリを設定しましょう。

python -c "import numpy;print(numpy.get_include())"

とすると、ディレクトリが表示されるので、cmakeのパスに加えてみましょう。

-D PYTHON3_NUMPY_INCLUDE_DIRS=XXXXXXXXXX

結果は・・・

image.png

認識されました!

他にうまくいかないケースとしては、PYTHON_LIBRARYPYTHON_INCLUDE_DIRが見つからないケースです。無理やり設定してもいいですし、find_package(PythonLibs)が見つけられるように、cmakeがデフォルトで持っているFindPythonLib.cmakeに潜るのも一興ですね。少し大変ですが僕もたまにやります。問題を難しくしているのは、Numpyが見つからないだけなのに、PYTHON_LIBRARYが見つからないと怒られたりと、問題を分けられないことがしばしばある点です。その場合にも、この辺のコードを見て、どこが問題なのかをチェックすれば、いつでも解決できますね。

その他のケース

condaだと、PythonLibsが見つからない場合もよくある。python3.6を入れてるのであれば、例えば、

-D PYTHON_LIBRARY=/opt/conda/lib/libpython3.6m.so 
-D PYTHON_INCLUDE_DIR=/opt/conda/include/python3.6m/

をビルドオプションに入れると良かったりする。

おわりに

長くてダラダラした記事になってしまいましたが、個人的にpython bindingで失敗することは結構あるので、作業ログは取っておきたいなぁと思っており、ちょうど良い機会でした。多分ここまで読む人は世の中にいないと思いますが、これを機にpython bindingの達人を目指してみるのは如何でしょうか。

なお、この上級編として、OpenCV内のpython bindingのコードを切り貼りして、C++とpythonを繋げるマニアックな技を昨年のAdvent Calenderで公開しています(一年前はPython2.7を使っていたので、3系では動きませんが)。そちらも併せて読んでいただければと思います。では、よいお年を〜。