OpenCV-Pythonの紐づけ(Bindings)がどのように動作するのか?

目的

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

  • どのようにOpenCV-Pythonの紐づけ(bindings)が生成されるか?
  • 新しいOpenCVのモジュールをどのようにPythonへ拡張するのか?

どのようにOpenCV-Pythonの紐づけ(bindings)が生成されるか?

OpenCvでは全アルゴリズムがC++で実装されていますが,PythonやJavaといった異なる言語で使う事が可能です.これはbindings generatorによって実現されます.bindings generatorはC++で実装された関数を別の言語から使用することを可能にするためのつながりを作ります.バックグラウンドで何が起きているのか全体像を把握するためには,Python/C APIの知識が必要です.C++の関数をPython使えるように拡張する簡単な例はPythonの公式ドキュメントを参照してください[1].OpenCVの全関数をPythonで使うようにするためにwrapper関数を手作業で書くのは時間がかかる作業になってしまいます.そのためOpenCVは賢い方法を採用し,modules/python/src2 フォルダに保存されているPythonスクリプトを使いC++のヘッダから自動的にwrapper関数を生成しています.この方法を詳しく見ていきましょう.

まず初めに CMakeの設定ファイルである modules/python/CMakeFiles.txt でOpenCVのモジュールをPythonへ拡張するか確認します.この設定ファイルは全モジュールを拡張するか自動で確認し対応するヘッダファイルの情報を取得します.ここで取得されたヘッダファイルは対応するモジュールの全クラス,関数,定数のリストを保持しています.

次に,これらのヘッダファイルが modules/python/src2/gen2.py というPythonスクリプトに渡されます.このスクリプトがPythonのbindings generatorのスクリプトです.このスクリプトは内部で modules/python/src2/hdr_parser.py というヘッダファイルをPythonのリスト型のオブジェクト群に分割します.これらのリスト群が特定の関数やクラスに関する詳細な情報を保持するわけです.例えば,ある関数をパースすると関数名,返戻値の型,入力引数,入力引数の型といった情報を含むリストが得られます.最終的なリストはそのヘッダファイル内で定義されている全関数,構造体,クラスの詳細な情報を含むものになります.

しかし,ヘッダのパーサーはヘッダファイル内の全関数/クラスをパースするわけではありません.Pythonに導入されるべき関数はC++のコードの開発者が指定します.この指定をするためのマクロが,ヘッダファイルのパーサーがパースされる関数を識別できるように関数宣言の初めに足されます.これらのマクロは関数を実装する開発者によって追加されます.端的に言うと,開発者がPythonに追加されるべき関数,そうではない関数を決めるのです.これらのマクロの詳細については次のセッションで説明します.

ヘッダファイルのパーサーはパースされた関数の巨大なリストを返します.我々のgeneratorスクリプト(gen2.py)は,パーサによってパースされた全関数,クラス,列挙子,構造体のwrapper関数を作成します(これらのヘッダファイルはコンパイル中に build/modules/python/ フォルダ内に pyopencv_generated_*.h として格納されます).しかし,OpenCVの基本的なデータ型であるMat, Vec4i, Sizeといったデータ型は手入力で拡張する必要があります.例えばMat型はNumpyのarray,Sizeは二つの整数のtupleといったようにです.同様に,複雑な構造体,クラス,関数についても自分で拡張する必要があります.そのようなマニュアルのwrapper関数は全て modules/python/src2/pycv2.hpp に書かれています.

残る作業は cv2 モジュールを我々に提供するwrapperファイルのコンパイルです.Pythonで例えば res = equalizeHist(img1,img2) という関数を呼ぶとしましょう.二つのnumpyのarrayを入力引数とし,出力として別のnumpyのarray型のオブジェクトが返されることを期待します.これらのnumpy arrayは cv::Mat 型のオブジェクトに変換された後にC++の equalizeHist() 関数が呼ばれます.最終的な処理の結果がnumpy arrayの res に変換されます.簡単に言うと,ほとんどすべての処理がC++で行われるため,処理速度はC++と同等の速度になります.

これがOpenCV-Pythonの紐づけ(bindings)が生成される基本的な方法です.

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

ヘッダのパーサーは関数宣言に追加されるwrapperマクロに基づいてヘッダファイルをパースします.列挙子定数は自動的にwrapされるため,wrapperマクロを必要としません.しかし残りの関数やクラスはwrapperマクロが必要です.

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

CV_EXPORTS_W void equalizeHist( InputArray src, OutputArray dst );

ヘッダパーサーは InputArray, OutputArray といったキーワードを基に入出力引数を理解しますが,入出力引数をハードコードする必要があるかもしれません.そのような時は CV_OUT, CV_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つの関数が使用可能で,それぞれ接尾辞がついています.同様に,オーバーロードされたメソッドに対しては 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++の関数に対して値として渡されます.例えば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で利用可能な拡張のための主なマクロです.一般的に,開発者が正しい位置に正しいマクロを書きさえすれば,後はbindings generatorスクリプトが変換を行ってくれます.generatorスクリプトがwrapper生成に失敗する例外的な状況が発生するかもしれませんが,OpenCVの実装ガイドラインに従って書いたコードであれば基本的には自動的にwrapperが生成できるはずです.