OpenCV Advent Calendarの21日目です。遅ればせながら投稿します。
本記事の概要
OpenCVを使った様々なC++コードをpythonで使いたい人は多いのではないでしょうか(少ない)。僕もその一人です。そこで本記事では、cv::Matであったり、std::vector<>みたいなデータを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から呼べるのでは、というのがモチベーションです。
最終的に、こんな感じになります。
cv::Mat aaaa() {
return cv::Mat();
}
std::vector<cv::Rect> bbbb() {
return std::vector<cv::Rect>(4);
}
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を返す事とします。
#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_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は以下のようになります。そのまんまなので、詳細は割愛します。
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ディレクトリに適当なスクリプトを用意します。
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に渡すことができました。めでたしめでたし。