OpenCV-Python バインディングの仕組み

目的

このチュートリアルでは以下について学ぶ:

  • OpenCV-Python バインディングの作り方
  • 新しいOpenCVのモジュールをどのようにPythonに拡張するか?

どのようにしてOpenCV-Python バインディングが作られるか?

OpenCVではすべてのアルゴリズムがC++で実装されているが,C以外の言語、例えばPythonやJavaで使うことも可能である.これはバインディング生成器(bindings generator)で実現される.バインディング生成器は、C++の関数をPythonのような別の言語から呼び出せるようにする「橋渡し」である。その裏で起きていることの全体像を把握するには,Python/C APIの知識が必要である.C++の関数をPythonで使えるよう拡張する例をPythonの公式ドキュメントに見ることができる.OpenCVの全関数をPythonで使うようにするためにラッパー(wrapper, 「ラップ」ともいう)関数を手作業で書くのはとても時間がかかる.そのためOpenCVでは賢い方法を採用している。つまりmodules/python/src2ディレクトリにあるPythonスクリプトを使って、C++のヘッダファイルから自動的にラッパー関数を生成している.これについて詳しく見ていこう.

まず初めに CMakeの設定(スクリプト)ファイルであるmodules/python/CMakeFiles.txt によりPythonに拡張するOpenCVのモジュールを調べる.これにより自動で、拡張すべきモジュールをみな調べ、そのヘッダ・ファイル情報を取得する.このヘッダ・ファイルには対応するモジュールのクラス,関数,定数などがすべてリストされている.

次に,このヘッダ・ファイルが modules/python/src2/gen2.py というPythonスクリプトに渡される.これがPythonのバインディング生成器のスクリプトである.このスクリプトは modules/python/src2/hdr_parser.py という別なPythonスクリプトを呼び出し、ヘッダ・ファイルを分析する。そして、ヘッダ・ファイルから得られた情報をPythonのリスト型オブジェクトに分割する.これらのリスト群に、特定の関数やクラスなどに関する詳細な情報すべてが保持されているのである.例えば,OpenCVの関数を解析し、その関数名,戻り値の型,入力引数,入力引数の型といった情報を含むリストを得る.こうして最終的に出来上がるリストには、ヘッダ・ファイルで定義されている関数,構造体,クラスなどの詳細な情報すべてが含まれている.

しかし,ヘッダの分析スクリプトはヘッダ・ファイル内の全部の関数やクラスを分析しているわけではない.Pythonにどの関数を導入するかはOpenCVの開発者が指定する.この指定をするためのマクロが関数宣言の最初に付け加えられており,ヘッダ・ファイルの分析器に対してどの関数を分析するか分かるようになっている.このマクロは関数をコード化する開発者によって追加される.端的に言うと,開発者がどの関数をPythonに追加するかを決めるのである.このマクロの詳細については次の節で説明する.

ヘッダ・ファイルの分析器は、最終的に分析した関数の巨大なリストを返す.我々の生成器スクリプト(gen2.py)は,ヘッダ分析器によって分析された関数,クラス,列挙子,構造体すべてのラッパー関数を作成する(これらのヘッダ・ファイルはコンパイル中に build/modules/python/ フォルダ内に pyopencv_generated_*.h として格納される).しかし,OpenCVの基本的なデータ型であるMat, Vec4i, Sizeといったデータ型は手入力で拡張しなければならない.例えばMat型はNumpyのarrayに,Sizeは整数2つからなるtupleに、というように変換する.同様に,複雑な構造体,クラス,関数についても人手で変換しなければならない.そのような人手によるラッパー関数は全て modules/python/src2/pycv2.hpp に書かれている.(注: OpenCV3には見当たらない)

残る作業は Pythonのcv2 モジュールのためのラッパー関数のコンパイルである.例えば res = equalizeHist(img1,img2) という関数をPythonで呼ぶとしよう.それには入力引数として2つのNumpy配列を渡し,出力として別のNumpy配列が返ることを期待する.入力となるNumpy配列は cv::Mat 型のオブジェクトに変換された後、C++の equalizeHist() 関数が呼ばれる.そしてその処理の結果はNumpy配列に変換され変数res にセットされる.だから簡単に言えば,ほとんどすべての処理がC++で行われるため,処理速度はC++とほとんど同じである.

以上がOpenCV-Python バインディングの作り方の基本である.

新しいOpenCVのモジュールをどのようにしてPythonに拡張するか?

ヘッダ分析器は関数宣言に追加されたラッパー・マクロに基づいてヘッダ・ファイルを分析する.列挙子(enum)定数は自動的にラップされるため,ラッパー・マクロを必要としない.しかしそれ以外の関数やクラスなどはラッパー・マクロが必要である.

関数に対しては CV_EXPORTS_W マクロを使って拡張する.以下に例を示す.

CV_EXPORTS_W void equalizeHist( InputArray src, OutputArray dst );

ヘッダ分析器は InputArray, OutputArray というキーワードを基にして入出力引数を理解することができる。しかし時には入出力の引数をハードコードする必要があるかもしれない.そのような時は CV_OUTCV_IN_OUT のようなマクロを使う.

CV_EXPORTS_W void minEnclosingCircle( InputArray points,
                                     CV_OUT Point2f& center, CV_OUT float& radius );

巨大なクラスに対しては CV_EXPORTS_W マクロを使う.クラス・メソッドを拡張するには CV_WRAP マクロを使う.同様に,クラスのメンバ変数に対しては CV_PROP マクロを使う.

class CV_EXPORTS_W CLAHE : public Algorithm
{
public:
    CV_WRAP virtual void apply(InputArray src, OutputArray dst) = 0;

    CV_WRAP virtual void setClipLimit(double clipLimit) = 0;
    CV_WRAP virtual double getClipLimit() const = 0;
}

オーバーロードされた関数には CV_EXPORTS_AS マクロを使って新しい名前を与え,Pythonからはその新しい名前で使う必要がある.例として次のintegral関数を取り上げる.3つの関数が使用可能で,それぞれPythonで使うために名前が変更されている.これと同じように、オーバーロードされたメソッドに対しては CV_WRAP_AS マクロを使う.

//! computes the integral image
CV_EXPORTS_W void integral( InputArray src, OutputArray sum, int sdepth = -1 );

//! computes the integral image and integral for the squared image
CV_EXPORTS_AS(integral2) void integral( InputArray src, OutputArray sum,
                                        OutputArray sqsum, int sdepth = -1, int sqdepth = -1 );

//! computes the integral image, integral for the squared image and the tilted integral image
CV_EXPORTS_AS(integral3) void integral( InputArray src, OutputArray sum,
                                        OutputArray sqsum, OutputArray tilted,
                                        int sdepth = -1, int sqdepth = -1 );

小さなクラスや構造体に対しては CV_EXPORTS_W_SIMPLE マクロを使う.これらの構造体はC++の関数に対して値渡し(call by value)される.例えばKeyPoint, Matchなどがその例である.これらのメソッドに対しては CV_WRAP マクロを,またメンバ変数に対しては CV_PROP_RW を使って拡張する.

class CV_EXPORTS_W_SIMPLE DMatch
{
public:
    CV_WRAP DMatch();
    CV_WRAP DMatch(int _queryIdx, int _trainIdx, float _distance);
    CV_WRAP DMatch(int _queryIdx, int _trainIdx, int _imgIdx, float _distance);

    CV_PROP_RW int queryIdx; // query descriptor index
    CV_PROP_RW int trainIdx; // train descriptor index
    CV_PROP_RW int imgIdx;   // train image index

    CV_PROP_RW float distance;
};

他には CV_EXPORTS_W_MAP を使ってPythonの辞書(dictionary)型に変換されるクラスや構造体がある.Moments()がその例である.

class CV_EXPORTS_W_MAP Moments
{
public:
    //! spatial moments
    CV_PROP_RW double  m00, m10, m01, m20, m11, m02, m30, m21, m12, m03;
    //! central moments
    CV_PROP_RW double  mu20, mu11, mu02, mu30, mu21, mu12, mu03;
    //! central normalized moments
    CV_PROP_RW double  nu20, nu11, nu02, nu30, nu21, nu12, nu03;
};

ここで紹介したものがOpenCVで利用可能な拡張のための主なマクロである.一般的に,開発者が適切な位置に適切なマクロを書きさえすれば,後はバインディング生成器スクリプトが変換を行ってくれる.生成器スクリプトがラッパー生成に失敗するという例外的な状況が発生することもある。そのような関数に対しては人手で対処しなければならず、それにはヘッダを拡張するためのpyopencv_*.hppを自分で書いてヘッダを拡張子、その結果を対象モジュールのmisc/pythonディレクトリに入れなければならない。しかし,殆どの場合、OpenCVの実装ガイドラインに従って書いたコードであれば自動的にラッパーが生成できるはずである.