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

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から呼べるのでは、というのがモチベーションです。

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

```cpp:c++側
cv::Mat aaaa() {
  return cv::Mat();
}
std::vector<cv::Rect> bbbb() {
  return std::vector<cv::Rect>(4);
}
```

```py: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の中の

```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の変換は下記となります。

```cpp
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でメモリを割り当てる際に、

```cpp
    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が自動生成する部分は利用しないので、色々修正していきます。まずは、コードの色々な所に散っている以下のインクルード部分をコメントアウトしてやります。

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

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

```cpp
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を返す事とします。

```cpp: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を下記のようにします。

```cmake: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は以下のようになります。そのまんまなので、詳細は割愛します。

```cmake: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ディレクトリに適当なスクリプトを用意します。

```py:scripts/test.py
import sys
sys.path.append('../lib')
import libboost_opencv
print(libboost_opencv.GetImage())
print(libboost_opencv.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に渡すことができました。めでたしめでたし。