1. Qiita
  2. 投稿
  3. OpenCV

オリジナルC++ライブラリのOpenCVデータをPythonに渡す

  • 0
    いいね
  • 0
    コメント

    OpenCV Advent Calendarの21日目です。遅ればせながら投稿します。

    本記事の概要

    OpenCVを使った様々なC++コードをpythonで使いたい人は多いのではないでしょうか(少ない)。僕もその一人です。そこで本記事では、cv::Matであったり、std::vector<cv::Rect>みたいなデータをpythonにnumpy形式で渡せるようにします。もちろん単にC++のコードを呼び出すだけならば何も問題はありません。今回の肝は、普段OpenCVをPythonで使っているのと同じように、Matやvectorをnumpyに変換して渡す点です。

    C++で書いたライブラリをpythonで呼べるようにする仕組みとしてはBoost.Pythonが有名ですが、Boost.PythonでC++のMatのような複雑なクラスを独自に変換しようとすると、かなり大変です。それに加え、Matだけではなく、PointやRect、その他諸々のクラスを全部移植しようとしたら死んでしまいます。一方で、そもそもOpenCVの関数はpythonから簡単に呼び出せるようになっており、Matであったり、Rectのvectorがpythonではnumpyの共通形式で簡単に扱えます。この仕組みを使えば、簡単に独自のC++ライブラリをpythonから呼べるのでは、というのがモチベーションです。

    最終的に、こんな感じになります。

    c++側
    cv::Mat aaaa() {
      return cv::Mat();
    }
    std::vector<cv::Rect> bbbb() {
      return std::vector<cv::Rect>(4);
    }
    
    python側
    import xxxx
    mat = xxxx.aaaa
    vec = xxxx.bbbb
    

    あと、すみません、今回はwindowsでは動作確認していません。ちょっとやって上手くいかないなーと、一瞬で諦めた記憶があります。頑張ればできるかも。Ubuntuでは過去に動いた実績はあります。現状のテスト環境として、手元のMacで動作確認できています。

    仕組み

    今回はBoost.Pythonを利用します。Boost.Pythonとは、C++のクラスや関数をラップしてPythonから利用できるようにする素敵なライブラリです。

    http://d.hatena.ne.jp/moriyoshi/20091214/1260779899

    にBoost.Pythonの仕組みやメリットが詳しく書いてくれていますので、ここでは割愛。そもそも自分がBoost.Python詳しくない・・・。

    さて、いきなり種明かしですが、OpenCVはビルド時に、modules/python/common.cmakeの中の

    add_custom_command(
       OUTPUT ${cv2_generated_hdrs}
       COMMAND ${PYTHON_EXECUTABLE} "${PYTHON_SOURCE_DIR}/src2/gen2.py" ${CMAKE_CURRENT_BINARY_DIR} "${CMAKE_CURRENT_BINARY_DIR}/headers.txt"
       DEPENDS ${PYTHON_SOURCE_DIR}/src2/gen2.py
       DEPENDS ${PYTHON_SOURCE_DIR}/src2/hdr_parser.py
       DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/headers.txt
       DEPENDS ${opencv_hdrs})
    
    ocv_add_library(${the_module} MODULE ${PYTHON_SOURCE_DIR}/src2/cv2.cpp ${cv2_generated_hdrs})
    

    にあるようにgen2.pyというのを呼び出して、OpenCVの各関数をBoost.Pythonで使えるように変換しています。さらにその下のcv2.cppを使い、MatやRectなどをC++からnumpyに変換しています。例えば、Matの変換は下記となります。

    PyObject* pyopencv_from(const Mat& m)
    {
        if( !m.data )
            Py_RETURN_NONE;
        Mat temp, *p = (Mat*)&m;
        if(!p->u || p->allocator != &g_numpyAllocator)
        {
            temp.allocator = &g_numpyAllocator;
            ERRWRAP2(m.copyTo(temp));
            p = &temp;
        }
        PyObject* o = (PyObject*)p->u->userdata;
        Py_INCREF(o);
        return o;
    }
    

    読んでみればわかると思いますが、Py_INCREFで参照カウント増やしたりしていますし、途中にあるg_numpyAllocatorでメモリを割り当てる際に、

        UMatData* allocate(int dims0, const int* sizes, int type, void* data, size_t* step, int flags, UMatUsageFlags usageFlags) const
        {
            if( data != 0 )
            {
                CV_Error(Error::StsAssert, "The data should normally be NULL!");
                // probably this is safe to do in such extreme case
                return stdAllocator->allocate(dims0, sizes, type, data, step, flags, usageFlags);
            }
            PyEnsureGIL gil;
    
            int depth = CV_MAT_DEPTH(type);
            int cn = CV_MAT_CN(type);
            const int f = (int)(sizeof(size_t)/8);
            int typenum = depth == CV_8U ? NPY_UBYTE : depth == CV_8S ? NPY_BYTE :
            depth == CV_16U ? NPY_USHORT : depth == CV_16S ? NPY_SHORT :
            depth == CV_32S ? NPY_INT : depth == CV_32F ? NPY_FLOAT :
            depth == CV_64F ? NPY_DOUBLE : f*NPY_ULONGLONG + (f^1)*NPY_UINT;
            int i, dims = dims0;
            cv::AutoBuffer<npy_intp> _sizes(dims + 1);
            for( i = 0; i < dims; i++ )
                _sizes[i] = sizes[i];
            if( cn > 1 )
                _sizes[dims++] = cn;
            PyObject* o = PyArray_SimpleNew(dims, _sizes, typenum);
            if(!o)
                CV_Error_(Error::StsError, ("The numpy array of typenum=%d, ndims=%d can not be created", typenum, dims));
            return allocate(o, dims0, sizes, type, step);
        }
    

    などと、色々やっています。これを自分でやるのは大変なので、今回はこの辺のコードをそのまま使ってC++形式のMatなどをpythonに変換しましょう。

    依存ライブラリ

    すみません、自分の環境に色々入れちゃっているので、下記で足りるかは分かりませんが、足りなかったらコメント入れてくれたら可能な範囲で調べます。これ入れたら動いたよーというコメント歓迎です。

    OpenCV3 with python
    numpy
    boost
    boost-python
    

    プロジェクトの作成

    サンプルということで、新しいプロジェクトを作っていきましょう。今回は、sample_projectという名前でいきます。適当な場所にsample_projectを作成します。

    mkdir sample_project
    cd sample_project
    

    ライブラリのコードを含むディレクトリを作成します。今回はboost_opencvで。

    mkdir boost_opencv
    

    pythonテストスクリプトコードを入れるフォルダを作成します。

    mkdir scripts
    

    OpenCVの流用について

    今回はここがメインになります。と言っても流用はとても簡単です。先ほどのcv2を自分のライブラリディレクトリにコピーします。大きな理由はないですが、cv2.cppのコードをヘッダファイル的にリネームしておきます。

    cd boost_opencv
    wget  https://raw.githubusercontent.com/opencv/opencv/3.1.0/modules/python/src2/cv2.cpp
    mv cv2.cpp cv2.hpp
    

    今回使いたいのは、ほんの一部で、先ほど説明したgen2.pyが自動生成する部分は利用しないので、色々修正していきます。まずは、コードの色々な所に散っている以下のインクルード部分をコメントアウトしてやります。

    #include "pyopencv_generated_types.h"
    #include "pyopencv_generated_funcs.h"
    #include "pyopencv_generated_ns_reg.h"
    #include "pyopencv_generated_type_reg.h"
    

    次に、初期化部分を丸々コメントアウトします。初期化は別途やるので削ってOKです。削り始めは、

    1351 #if PY_MAJOR_VERSION >= 3
    1352 extern "C" CV_EXPORTS PyObject* PyInit_cv2();
    1353 static struct PyModuleDef cv2_moduledef =
    1354 {
    1355     PyModuleDef_HEAD_INIT,
    1356     MODULESTR,
    1357    "Python wrapper for OpenCV.",
    

    で、ここから最後まで全てコメントアウトします。但し、現時点での情報ですので、そのうち変わるとは思います。その場合、一旦ここは飛ばして、後ほどのビルドステップでエラーが出た所を削れば良いと思います。これで流用部は完了です。

    ライブラリのコードサンプル

    ここでは、簡単な事例として、Matを返す関数と、Rectのvectorを返す関数を用意します。ここでは、3x3のゼロ初期化行列行列と、Rectを4つ持ったvectorを返す事とします。

    boost_opencv/main.cpp
    #include <boost/python.hpp>
    #include "cv2.hpp"
    
    PyObject* GetImage()
    {
      cv::Mat cv_img(3, 3, CV_32F, cv::Scalar(0.0));
      return pyopencv_from(cv_img);
    }
    
    PyObject* GetObject(int index) {
      vector_Rect2d rects;
      rects.push_back(cv::Rect2d(0,0,0,0));
      rects.push_back(cv::Rect2d(0,0,0,0));
      rects.push_back(cv::Rect2d(0,0,0,0));
      rects.push_back(cv::Rect2d(0,0,0,0));
      return pyopencv_from(rects);
    }
    
    BOOST_PYTHON_MODULE(libboost_opencv)
    {
      using namespace boost::python;
      import_array();
      def("GetImage", &GetImage);
      def("GetObject", &GetObject);
    }
    

    ここで最も重要なのは、pyopencv_fromです。これが、Matやvectorをnumpyに変換するコードになります。ちなみにvector_Rect2dは、Rect2dのvectorで、cv2.cppのどこかに定義されています。また、numpyを使う作法として、import_array()を呼び出すのを忘れずに。またBoost.Pythonの作法ですが、BOOST_PYTHON_MODULEの引数に正しいライブラリ名を入れる必要があります。

    ビルド準備

    これで、基本的な準備はできたので、CMakeLists.txtを書いていきます。まずは、ルートのCMakeLists.txtを下記のようにします。

    CMakeLists.txt
    cmake_minimum_required(VERSION 3.7)
    
    project(sample_project)
    
    set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake-modules)
    set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/lib)
    
    find_package(OpenCV REQUIRED)
    find_package(Boost REQUIRED COMPONENTS python)
    
    find_package(PythonLibs REQUIRED)
    # in the case of homebrew python
    #set(PYTHON_INCLUDE_DIRS "/usr/local/Cellar/python/2.7.12_2/Frameworks/Python.framework/Versions/2.7/include/python2.7/")
    #set(PYTHON_LIBRARIES "/usr/local/Cellar/python/2.7.12_2/Frameworks/Python.framework/Versions/2.7/lib/libpython2.7.dylib")
    
    find_package(PythonInterp REQUIRED)
    find_package(Numpy REQUIRED)
    
    add_subdirectory(boost_opencv)
    

    注意点としてPythonLibsを読み込む時に、ubuntuとかならそのままやれば良いのですが、MacでHomebrewを使ってpythonをインストールしていた場合、ここが上手くいかないようです。僕は面倒なので直接パスを入れました・・・。

    さて、そのままだと、Numpyが見つかりませんと言われるので、Numpyのインクルードパスとライブラリパスを持ってきましょう。最終的にNUMPY_INCLUDE_DIRが設定されていれば、その方法はなんでも良いのですが、僕はCaffeのFindNumPy.cmakeを流用しました。

    mkdir cmake-modules
    cd cmake-modules
    wget https://raw.githubusercontent.com/BVLC/caffe/master/cmake/Modules/FindNumPy.cmake
    

    なお、caffeのFindNumPy.cmakeはcaffe用の設定が最後にされているっぽいので、よくわからないですがコメントアウトしておきましょう。

    # caffe_clear_vars(__result __output __error_value __values __ver_check __error_value)
    

    次に、ライブラリ用のboost_opencv/CMakeLists.txtは以下のようになります。そのまんまなので、詳細は割愛します。

    boost_opencv/CMakeLists.txt
    project(boost_opencv)
    
    include_directories(${OpenCV_INCLUDE_DIRS} ${Boost_INCLUDE_DIRS} ${PYTHON_INCLUDE_DIRS} ${NUMPY_INCLUDE_DIR})
    add_library(boost_opencv SHARED main.cpp) 
    target_link_libraries(boost_opencv ${OpenCV_LIBS} ${Boost_LIBRARIES} ${PYTHON_LIBRARIES})
    

    ビルド

    これは、そのまんまですね。ビルドディレクトリを作って、cmakeとmakeするだけ。

    mkdir build
    cd build
    cmake ..
    make
    

    すると、libディレクトリ内にlibboost_opencv.so(Macならlibboost_opencv.dylib)が作られます。あとはこれをpythonでimportして呼ぶだけ。

    ライブラリ呼び出し

    これは本当にシンプルですが、一応貼っておきます。決まりは無いですが、scriptsディレクトリに適当なスクリプトを用意します。

    scripts/test.py
    import sys
    sys.path.append('../lib')
    import libboost_opencv_lib
    print(libboost_opencv_lib.GetImage())
    print(libboost_opencv_lib.GetObject(0))
    

    ちなみに、Macで作られるdylibはpythonがimportしてくれないので、拡張子をsoに変えたら読み込んでくれました。結果は、

    [[ 0.  0.  0.]
     [ 0.  0.  0.]
     [ 0.  0.  0.]]
    [[ 0.  0.  0.  0.]
     [ 0.  0.  0.  0.]
     [ 0.  0.  0.  0.]
     [ 0.  0.  0.  0.]]
    

    ということで、できました。これにて、C++のMatやRectのvectorをnumpyに変換してpythonに渡すことができました。めでたしめでたし。

    この投稿は OpenCV Advent Calendar 201621日目の記事です。
    Comments Loading...