takminの書きっぱなし備忘録 @はてなブログ

主にコンピュータビジョンなど技術について、たまに自分自身のことや思いついたことなど

Pythonからctypesを使ってOpenCVで作成したライブラリを呼び出してみた

例の如く、自分用の作業メモ。

Python初心者の僕が、PythonOpenCVで書かれたC/C++ライブラリとを連携する必要が出たため、色々調べてみました。ctypes、SWIG、Boost.Python、Cython等、方法がたくさんありすぎてどれを使うか悩みました。
以下に、PythonC/C++ライブラリを連携する方法について色々とリストアップされてます。(Boost.Pythonはないけど)

http://www.hexacosa.net/documents/python-extending/


で、今回はctypesを使ってみることにしました。理由は、Pythonをインストールすると標準でついているのと、以下の記事見ると一番評判が良さそうだったので。

http://stackoverflow.com/questions/145270/calling-c-c-from-python
http://stackoverflow.com/questions/1942298/wrapping-a-c-library-in-python-c-cython-or-ctypes


(実は、最初はCythonを試したんですが、インストール後うまく動かすことができず断念)


ctypesの使い方については、今回Python2.6.xを使用したため、以下の記事を参考にしました。

http://www.python.jp/doc/2.6/library/ctypes.html


とても簡単に解説すると、ctypesでは例えば、

import ctypes
mydll = ctypes.cdll.LoadLibrary('my.dll')

というコードを追加するだけで、後はmydllのメンバ関数として動的ライブラリ内のコードを直接呼び出すことができるようになります。ちなみに最初は静的ライブラリでやろうとしたんですが、やり方が見つからず。。。


厄介だったのは、今回OpenCVのcv::Mat型のデータをC++ライブラリとPython間でやり取りしなくてはならなかったため、その点についてかなり試行錯誤しました。OpenCV Pythonをインストールすれば簡単にできるんじゃないの?と考えていた自分が甘かった。。。


結論から言うと、ctypesではOpenCV Pythonのcv.CvMat型のデータは扱うことができないため、なんらかの他のデータ形式に変換してやる必要がありました。ここでは、CライブラリからPythonへは、cv::Matのサイズ(rows, cols)と、型(type())、それと配列領域の先頭データポインタ(void*)を渡し、PythonからCライブラリへも同様に全てCの標準で定義可能な型で渡してます。

また、ctypesではC++はサポートしていないようなので、クラス定義なども、一旦Cの形に変換してやる必要があります。



ちなみにctypes-OpenCVというライブラリもあるみたいですが、今回は使いませんでした。
http://code.google.com/p/ctypes-opencv/


何分、Python初心者なので、他にもっとスマートなやり方があったら教えて下さい。


作業環境:


というわけで、まずは共有ライブラリ(ここではDLL)を作成します。
DLLの作成方法は、このサイトを参考にしました。

http://msdn.microsoft.com/ja-jp/library/ms235636.aspx

DLLのコード例

この例では、MyClassというクラスを自分で定義し、そのMyClassに対してSetMyMat()とGetMyMat()という2つのメンバ関数を使用して、cv::Matのデータをやり取りすることを想定しています。

myclass.h
#if WIN32
#define DLLEXPORT extern "C" __declspec(dllexport)
#else
#define DLLEXPORT extern "C"
#endif

#include 

//! 新しいインスタンスを生成
DLLEXPORT MyClass* new_myclass();

//! オブジェクトを削除
DLLEXPORT void delete_myclass(MyClass* h_myclass);

//! cv::MatデータをMyClassへ渡す
DLLEXPORT int SetMvMat(MyClass* h_myclass, 
	void* my_data, int my_width, int my_height, int my_type);

//! cv::Mat型データを返す。
DLLEXPORT void* GetMyMat(MyClass* h_myclass, int* w, int* h, int* type);

class MyClass{
public:
	MyClass();
	~MyClass();

	void SetMyMat(cv::Mat& my_mat){MyMat = my_mat;};
	cv::Mat& GetMyMat(){return MyMat;};

private:
	cv::Mat MyMat;
};

myclass.cpp
#include "myclass.h"

// MyClassを生成し、ポインターを返す。
MyClass* new_myclass()
{
	return new MyClass();
}

// MyClassをdelete
void delete_myclass(MyClass* h_myclass)
{
	delete h_myclass;
}

//! cv::MatデータをMyClassへ渡す
void SetMvMat(MyClass* h_myclass, void* my_data, int my_width, int my_height, int my_type)
{
	cv::Mat mymat(my_height, my_width, my_type, my_data);
	h_myclass->SetMyMat(mymat); 
}

//! cv::Mat型データを返す。
void* GetMyMat(MyClass* h_myclass, int* w, int* h, int* type)
{
	mymat = h_myclass->GetMyMat();
	w = mymat.cols;
	h = mymat.rows;
	type = mymat.type();
	return (void*)mymat.data;
}

Pythonのコード例

Python側でDLL側のMyClassクラスをラップしたクラスを作成します。MyClassクラスはcv.CvMat型のデータをメンバ関数のSetMyMat()の引数として受け取り、GetMyMat()の返り値として返します。
これは例えば、以下のような形で扱うことができます。

sample.py
import MyClass
import cv2.cv as cv

# 新規MyClass作成
my_class = MyClass.MyClass()

# Mat型のデータを新規作成
my_data = cv.CreateMat(3,3,cv.CV_32SC1)
cv.Set(my_data, 7)

# 作成したmy_dataをセット
my_class.SetMyData(my_data)

# my_classからMat型データを取得
my_mat = my_class.GetMyMat()


MyClassのメンバ関数内部では、cv.CvMat型のデータをctypesで扱うことができるデータ型に変換しています。例えばctypes.c_intはint型データをDLLとやり取りするための型、ctypes.c_void_pはVoidポインターをDLLとやり取りするための型です。

MyClass.py
import ctypes
import cv2.cv as cv

# DLLで定義されたMyClassクラスのPythonラッパー
class MyClass:
    __my_dll = ctypes.cdll.LoadLibrary('MyDll.dll')
    __my_class = ctypes.c_void_p

    # MyClassを新規作成
    def __init__(self):
        # 関数の返り値の型を指定
        self.__my_dll.new_myclass.restype = ctypes.c_void_p
        # 64bit機の場合、下記でないとうまく行かない場合があります
        # self.__my_dll.new_myclass.restype = ctypes.POINTER(ctypes.c_long)

        # DLL内でクラスの新規作成
        self.__my_class = self.__my_dll.new_myclass()
        
    # MyClassを削除
    def __del__(self):
        self.__my_dll.delete_myclass(self.__my_class)

    #### DLLのSetMyMat()を呼び出し、cv.CvMatをセットする ####
    def SetMyMat(self, mymat):
        # パラメータをDLLへ渡すために、ctypesの指定するint型へ変換
        w = ctypes.c_int(mymat.cols)
        h = ctypes.c_int(mymat.rows)
        t = ctypes.c_int(mymat.type)

        # DLLのSetMyMat()呼び出し
        self.__my_dll.SetMyMat(self.__my_class, mymat.tostring(), w, h, t)
        

    #### DLLのMyClass.GetMyMat()を呼び出し、cv.CvMatを返す ####
    def GetMyMat(self):
        # DLLのMyClass.GetMyMatをGetFuncへセット
        GetFunc = self.__my_dll.GetMyMat

        # DLLのGetMyMat()の返り値の方をVoidポインターにセット
        GetFunc.restype = ctypes.c_void_p

        # GetMyMat()の引数の型をctypesで指定する型へ設定
        w_ctype = ctypes.c_int()
        h_ctype = ctypes.c_int()
        t_ctype = ctypes.c_int()

        # DLLのGetMyMat()を呼び出し(w,h,tは参照渡し)
        data_ptr = GetFunc(self.__my_class, ctypes.byref(w_ctype), ctypes.byref(h_ctype), ctypes.byref(t_ctype))

        # DLLのGetMyMat()で取得したデータをcv.CreateMat()用の引数に変換
        w = w_ctype.value
        h = h_ctype.value
        t = t_ctype.value

        # cv.CvMatのヘッダー情報を作成
        dest_mat = cv.CreateMatHeader(h,w,t)

        # データをメモリ領域を割り当ててコピー
        array_size = h * dest_mat.step
        mat_data = (ctypes.c_byte * array_size)()
        ctypes.memmove(mat_data, data_ptr, array_size)

        # cv.CvMatにデータをセット
        cv.SetData(dest_mat, mat_data, dest_mat.step)
        return dest_mat

というわけで、思いの外めんどくさかったです。