LoginSignup
23

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-12-23

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から利用できるようにする素敵なライブラリです。

に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
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に渡すことができました。めでたしめでたし。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23