OpenCVのバインディング用ラッパーマクロ - wagavulin/opencvr GitHub Wiki

OpenCVのヘッダには各種言語バインディング用のマクロが使われています。これらのマクロの付いたヘッダをhdr_parser.pyで読み込むとクラス・enum・関数の情報を抽出することができます。Pythonバインディングはこれを使って自動生成されておりopencvrも同様にしているのですが、出力結果の詳細が分かっていないとバインディングコードの生成スクリプトは書けません。以下使ってみて分かったことのメモです。パーサのコードを読んだわけではないので間違っているかもしれません。使用したバージョンはOpenCV-4.7.0です。

基本

hdr_paser.pyにはCppHeaderParserクラスが定義されており、そのparse()メソッドでヘッダを解析します。結果はリストがネストしたツリー構造になります。ヘッダの各要素に対してどのような情報が出力されるかを見ていきます。

メソッド

/** Comment */
CV_EXPORTS_W int test1(char a, short b=0);

上の宣言に対する結果は以下のようにリストのツリー構造になります。

[
  "test1",     # [0]: 関数名
  "int",       # [1]: 戻り値型1
  [],          # [2]: 関数追加情報
  [           # [3]: 引数情報のリスト
    [
      "char",  # [3][n][0]: 引数型
      "a",     # [3][n][1]: 仮引数名
      "",      # [3][n][2]: デフォルト値
      []       # [3][n][3]: 引数追加情報
    ],
    [
      "short",
      "b",
      "0",
      []
    ]
  ],
  "int",       # [4]: 戻り値型2
  "Comment"    # [5]: Doxygenコメント
]

上記のとおり関数名、戻り値型、引数などの情報があるのが分かると思います。関数追加情報、引数追加情報、戻り値型が2つある点については後で説明します。

参照・ポインタ・constなど

次は以下の関数を解析結果です。

CV_EXPORTS_W const ClassA& test2(int a, CV_OUT int& b, CV_IN_OUT int& c, CV_OUT int* d,
                                 CV_IN_OUT int* e, const ClassA* f, ClassA&& g);
['test2',
 'ClassA',
 [],
 [['int', 'a', '', []],
  ['int', 'b', '', ['/O', '/Ref']],
  ['int', 'c', '', ['/IO', '/Ref']],
  ['int*', 'd', '', ['/O']],
  ['int*', 'e', '', ['/IO']],
  ['ClassA*', 'f', '', ['/C']],
  ['ClassA', 'g', '', ['/RRef']]],
 'ClassA',
 '']

引数追加情報のところに色々追加されています。

  • /O: 出力用引数。CV_OUTに対応。
  • /IO: 入出力用引数。CV_IN_OUTに対応。
  • /Ref: (左辺値)参照。
  • /RRef: 右辺値参照。
  • /C: const。

CV_OUTCV_IN_OUTによって引数が出力用か入出力用かが分かるようになっています。Python/Rubyは多値を返せるのでより自然な形に直してバインドします。例えば以下の関数を考えます。

CV_EXPORTS_W int func1(int a, CV_IN_OUT int& b, CV_OUT int& c);

これをRubyにバインドする場合は func1(a, b) #=> (ret, new_b, c) という形にします。bは入出力両方に使われるのでnew_bを戻り値の1つとして返します。cは出力専用なので引数として渡す必要はなく戻り値にします。よって戻り値はfunc1本来の戻り値と合わせて3つになります。

その他以下2つポイントがあります。

  • ポインタに関しては追加情報には現れない。単に型がint*のようになるだけ。
  • 戻り値についてはconstかどうかの情報はない(OpenCVにconst参照を返す関数は多分ないと思います)。

名前空間

OpenCVのAPIは名前空間が使われているので当然その対応もされています。しかし規則は結構複雑です。まずは以下の宣言を解析させてみます。関数test3()test4()test5()いずれも引数・戻り値はC1型ですが、書き方を変えています。

namespace cv {
namespace Ns1 {
class C1 {};
CV_EXPORTS_W C1 test3(C1 a);
CV_EXPORTS_W cv::Ns1::C1 test4(cv::Ns1::C1 a);
CV_EXPORTS_W ::cv::Ns1::C1 test5(::cv::Ns1::C1 a);
}}

結果は以下のようになります。

['cv.Ns1.test3',
 'C1',
 [],
 [['C1',
   'a',
   '',
   []]],
 'C1',
 '']
['cv.Ns1.test4',
 'Ns1_C1',
 [],
 [['Ns1_C1',
   'a',
   '',
   []]],
 'cv::Ns1::C1',
 '']
['cv.Ns1.test5',
 '_Ns1_C1',
 [],
 [['_Ns1_C1',
   'a',
   '',
   []]],
 '::cv::Ns1::C1',
 '']

ここから以下のことが分かります。上で説明したとおり戻り値型は2箇所に現れます。

  • 関数名
    • グローバル名前空間から始まり、ドット繋ぎである (cv.NS1.test3など)
  • 引数
    • _繋ぎである
    • 型名が修飾されていない場合、解析結果にも名前空間は付かない (test3のC1)
    • 型名が修飾されている場合、cvは省略される (test4のNs1_C1, test5の_Ns1_C1)
    • ::から始まる場合は_から始まる (test5の_Ns1_C1)
  • 戻り値型1
    • 引数と同じ規則
  • 戻り値型2
    • 宣言されたとおりになる

バインディングコードを自動生成するときの注意点をいくつか挙げておきます。

  • ドット繋ぎ/_繋ぎ、cvがあるかなど、書式が異なるところがあるためその点を意識しておかないとうまく動きません。
    • 上の例には出てきませんが、std名前空間も同様に省略されます。
  • test3のように型名が修飾されていない場合、引数・戻り値型を見るだけでは正確な型が分かりません。例えばcv::Ns2::C1クラスがあったとき、どちらのC1クラスなのかを判別する必要があります。実際、Paramsという名前のクラスはあちこちにあるので名前空間を意識しないと正しくバインドできません。
    • 同様の問題は列挙型にもあり、TypesFlagsMODEなどの名前は複数現れるので注意が必要です。

列挙型

C++の列挙型には以下のような種類があります。

  • scoped enumかunscoped enumか
  • 名前があるか(ただしscoped enumは無名にはできない)
  • クラスに属するか

これらを踏まえてまずはクラスに属さない列挙型を見てみます。

namespace cv {
namespace Ns1 {
enum MyEnum1 { AAA, BBB };
enum class MyEnum2 { CCC=10, DDD=20 };
enum { EEE, FFF };
CV_EXPORTS_W MyEnum1 test6(MyEnum1 a);
}}
['enum cv.Ns1.MyEnum1',                         # [0]: 列挙型名
 '',                                            # [1]: 未使用
 [],                                            # [2]: 未使用
 [                                              # [3]: 列挙子の情報
  ['const cv.Ns1.AAA', '0', [], [], None, ''],  # [3][n][m]: 順に列挙子名、列挙値。残りは未使用
  ['const cv.Ns1.BBB', '1', [], [], None, '']
 ],
 None,                                          # [4]: 未使用
 ''],                                           # [5]: 未使用
['enum class cv.Ns1.MyEnum2',
 '',
 [],
 [
  ['const cv.Ns1.MyEnum2.CCC', '10', [], [], None, ''],
  ['const cv.Ns1.MyEnum2.DDD', '20', [], [], None, '']
 ],
 None,
 ''],
['enum cv.Ns1.<unnamed>',
 '',
 [],
 [
  ['const cv.Ns1.EEE', '0', [], [], None, ''],
  ['const cv.Ns1.FFF', '1', [], [], None, '']
 ],
 None,
 ''],

クラス内で定義された場合は以下のようになります。名前の途中にクラス名が入ること以外、上と同様です。

namespace cv {
namespace Ns1 {
class CV_EXPORTS_W C2 {
public:
    enum class MyEnum3 { GGG, HHH };
};
}}
['enum class cv.Ns1.C2.MyEnum3',
  '',
  [],
  [['const cv.Ns1.C2.MyEnum3.GGG', '0', [], [], None, ''],
   ['const cv.Ns1.C2.MyEnum3.HHH', '1', [], [], None, '']],
  None,
  '']

以下まとめと補足。

  • 列挙型名はenum 名前で、cvを含むドット繋ぎ
  • 列挙子名はconst 名前で、cvを含むドット繋ぎ
  • scoped enumの列挙子名は名前空間.クラス名.列挙子名になる
  • 無名の場合は<unnamed>になる
  • クラス内で定義された場合は名前の途中にクラス名が入る
  • CV_EXPORTS_Wを付けなくても対象になる
  • Doxygenコメントは出力されない
  • underlying typeの指定には対応しておらず、指定されているとエラーになる(OpenCVでは指定されていることはないはず)

クラス

基本

まずはコンストラクタ・デストラクタと公開メンバ変数を持つクラスです。

namespace cv {
namespace Ns1 {
/** Comment-for-C3 */
class CV_EXPORTS_W C3 {
public:
    CV_WRAP C3();
    CV_WRAP ~C3();
    CV_PROP int m_a;
    CV_PROP_RW int m_b;
};
 [
  'class cv.Ns1.C3',           # [0]: クラス名
  '',                          # [1]: クラス追加情報
  [],                          # [2]: 多分未使用
  [                            # [3]: 公開メンバ変数
   ['int', 'm_a', '', []],     # [3][n][x]: 左から順に型名、変数名、未使用、追加情報
   ['int', 'm_b', '', ['/RW']]
  ],
  None,                        # [4]: 多分未使用
  'Comment-for-C3'             # [5]: Doxygenコメント
 ],
 ['cv.Ns1.C3.C3', '', [], [], None, '']  # コンストラクタ

ほぼ見ての通りなので解説することはあまりないですが、

  • クラス名はclass 名前となり、cv付きのドット繋ぎ。
  • クラスに属する関数はCV_EXPORTS_WではなくCV_WRAPを使います。
  • クラス宣言のDoxygenコメントは抽出されます。
  • デストラクタは抽出されません。
  • コンストラクタがない場合もあります。例えばcv::Stitcherクラスのコンストラクタは隠蔽されており、代わりにスタティック関数create()`でインスタンスを生成します。
  • 公開メンバ変数はCV_PROPまたはCV_PROP_RWを使います。CV_PROP_RWのときは追加情報のところに/RWが付きます。

メンバ関数

メンバ関数については特に難しいことはありません。以下2つの関数を上記C3クラスに追加してみます。

namespace cv {
namespace Ns1 {
/** Comment-for-C3 */
class CV_EXPORTS_W C3 {
public:
    CV_WRAP C3();
    CV_WRAP ~C3();
    CV_WRAP void method1();        # 追加: メンバ関数
    CV_WRAP static void method2(); # 追加: staticメンバ関数
    CV_PROP int m_a;
    CV_PROP_RW int m_b;
};
}}
 ['cv.Ns1.C3.method1', 'void', [], [], 'void', ''],
 ['cv.Ns1.C3.method2', 'void', ['/S'], [], 'void', '']

staticメンバ関数には/Sが付きます。非staticの方は特に追加情報はありません。

継承

次は継承です。以下のようにC4, C5, C6クラスを定義してみます。public, private, protected継承それぞれ試しており、またC6は多重継承しています。なおコードをすべて書くと長くなるので、上で示したcv::Ns1::C2cv::ns1::C3クラスの宣言は省略します。

namespace cv { namespace Ns1 {
class CV_EXPORTS_W C4 : public C3 {};
class CV_EXPORTS_W C5 : private C3 {};
class CV_EXPORTS_W C6 : public C2, protected C3 {};
}}
['class cv.Ns1.C4', ': cv::Ns1::C3', [], [], None, ''],                   # public継承
['class cv.Ns1.C5', ': cv::Ns1::private, cv::Ns1::C3', [], [], None, ''], # private継承
['class cv.Ns1.C6',
 ': cv::Ns1::C2, cv::Ns1::protected, cv::Ns1::C3',                        # publicとprotectedの多重継承
 [], [], None, '']
  • 継承に関する情報はクラス追加情報のところに現れます。
  • public継承の場合は: 親クラスで、cv付きの::繋ぎです。
    • クラス名はドット繋ぎなのに親クラスは::繋ぎなので注意が必要です。
  • private, protected継承の場合はなぜかcv::Ns1::protectedなどが付き、親クラス名とはカンマで区切られます。
  • 多重継承の場合はカンマで区切られます。

publicかつ単一継承のときは問題ないですが、そこから外れるとかなり見にくくなっています。正直private・protected・多重継承についてはあまり考慮されていないと思います。OpenCVで使われているかは分かりませんが。

仮想関数

継承ができたら今度は仮想関数です。通常の仮想関数の他に純粋仮想関数と仮想でない関数のオーバーライドも試してみます。

namespace cv { namespace Ns1 {
class CV_EXPORTS_W P1 {
public:
    CV_WRAP         void method1();     # 非仮想関数
    CV_WRAP virtual void method2();     # 仮想関数
    CV_WRAP virtual void method3() = 0; # 純粋仮想関数
};
class CV_EXPORTS_W C7 : public P1 {
public:
    CV_WRAP void method1();
    CV_WRAP void method2() override;
    CV_WRAP void method3() override;
};
}}

結果

 ['class cv.Ns1.P1', '', [], [], None, ''],
 ['cv.Ns1.P1.method1', 'void', [], [], 'void', ''],
 ['cv.Ns1.P1.method2', 'void', ['/V'], [], 'void', ''],
 ['cv.Ns1.P1.method3', 'void', ['/V', '/PV'], [], 'void', ''],
 ['class cv.Ns1.C7', ': cv::Ns1::P1', [], [], None, ''],
 ['cv.Ns1.C7.method1', 'void', [], [], 'void', ''],
 ['cv.Ns1.C7.method2', 'void', [], [], 'void', ''],
 ['cv.Ns1.C7.method3', 'void', [], [], 'void', '']

上記の通り、仮想関数は/Vが、純粋仮想関数には/V/PVが付きます。一方オーバーライドした関数には何も付かず、そこだけを見てもオーバーライドしたものかを判別することはできません。また、あるクラスがインスタンス化できるかとうかも簡単には分かりません(そのクラスの情報を見ただけでは未実装の純粋仮想関数がないかを判定できないため)。

関数名の変更(CV_EXPOTS_AS, CV_WRAP_AS)

いくつかの関数はCV_EXPORTS_W/CV_WRAPではなくCV_EXPORTS_AS(synonym)/CV_WRAP_AS(synonym)が指定されています。これは関数のバインディング先を別の名前にすることを意味します。この情報は関数追加情報のところ=synonymという形式で付きます。

namespace cv { namespace Ns1 {
CV_WRAP_AS(test7_renamed) void test7();
}}

結果

['cv.Ns1.test7', 'void', ['=test7_renamed'], [], 'void', '']

関数名を変換する理由は見たところ2つあります。1つ目は演算子をオーバーロードしている場合です。例えばcv::FileNodeクラスはoperator[](const String& nodename)が定義されており、node["aaa"]のようにすることができます。この関数にはCV_WRAP_AS(getNode)が付いているのでPython/Rubyではnode.getNode("aaa")となります。Python/Rubyは演算子のオーバーロードができるので演算子を定義した方が良い気もしますが。

もう一つの理由はバインディング後に引数が同じになってしまう場合です。例えばcv::CascadeClassifierクラスにはdetectMultiScale()関数がオーバーロードによって3つ定義されていますが、そのうちの2つを見てみます。

CV_WRAP void detectMultiScale(InputArray image,                       // 1
                              CV_OUT std::vector<Rect>& objects,      // 2
                              double scaleFactor = 1.1,               // 3
                              int minNeighbors = 3, int flags = 0,    // 4, 5
                              Size minSize = Size(),                  // 6
                              Size maxSize = Size());                 // 7
CV_WRAP_AS(detectMultiScale2) void detectMultiScale(InputArray image, // 1
                              CV_OUT std::vector<Rect>& objects,      // 2
                              CV_OUT std::vector<int>& numDetections, // 3
                              double scaleFactor=1.1,                 // 4
                              int minNeighbors=3, int flags=0,        // 5, 6
                              Size minSize=Size(),                    // 7
                              Size maxSize=Size());                   // 8

この2つのdetectMultiScale()の違いは、2つ目の方に第3引数としてnumDetectionsが追加されていることだけです。このnumDetectionsにはCV_OUTが付いているのでPython/Rubyでは引数には含まれず戻り値になります。するとこの2つの関数の引数が全く同じなり、区別できなくなります。そこで2つ目の方はCV_WRAP_ASによってdetectMultiScale2()という名前でバインドすることになります。

その他のラッパーマクロ

CV_EXPORTS_W, CV_OUTのようなラッパーマクロはcore/cvdef.hに定義されていますが、上で説明したもの意外のマクロもあります。以下定義部分をそのまま引用します。

/* special informative macros for wrapper generators */
#define CV_EXPORTS_W CV_EXPORTS
#define CV_EXPORTS_W_SIMPLE CV_EXPORTS
#define CV_EXPORTS_AS(synonym) CV_EXPORTS
#define CV_EXPORTS_W_MAP CV_EXPORTS
#define CV_IN_OUT
#define CV_OUT
#define CV_PROP
#define CV_PROP_RW
#define CV_WRAP
#define CV_WRAP_AS(synonym)
#define CV_WRAP_MAPPABLE(mappable)
#define CV_WRAP_PHANTOM(phantom_header)
#define CV_WRAP_DEFAULT(val)

CV_EXPORTS

CV_EXPORTSは言語バインディング用のラッパーではなく主にWindowsのDLLにおけるシンボルの公開に関する設定です。あるDLLに含まれるクラス・関数が外部から呼び出すためには関数・クラスの宣言の前に__declspec(dllexport)という宣言が必要です。通常この宣言は直接書くのではなく何らかのマクロでラップします。OpenCVではこの目的のためにCV_EXPORTSが使われています。

CV_EXPORTS_Wはこれまで示したとおり、言語バインディングの対象にするかどうかを決めます。hdr_parser.pyは両者を区別していて、CV_EXPORTSの場合は対象から外します。一方C++コンパイラにとっては両者は同じものです。上記のとおり、CV_EXPORTS_Wは単にCV_EXPORTSになるように定義されているためです

まとめると以下のようになります。

  • CV_EXPORTS_W: DLLから公開し、かつ言語バインディングを提供する
  • CV_EXPORTS: DLLから公開するが言語バインディングは提供しない
  • 何も付けない: DLLから非公開かつ言語バインディングもしない

DLLから公開しないが言語バインディングは提供するという設定はできません。DLLから非公開だとそもそもバインディングを提供できないからです。

CV_EXPORTS_W_SIMPLE

クラス・構造体の宣言に対してCV_EXPORTS_Wの代わりに使われているところがあります。クラス追加情報に/Simpleが付きますが、目的は不明です。

class CV_EXPORTS_W_SIMPLE C8 {};
['class C8', '', ['/Simple'], [], None, '']

CV_EXPORTS_W_MAP

いくつかのクラス・構造体で使われていますが(調べた範囲では3つ)詳細は不明です。"CV_EXPORTS_W_MAP to export to python native dictionnaries"というコメントがあったのでクラスではなくディクショナリにするのかもしれません。

CV_WRAP_MAPPABLE, CV_WRAP_PHANTOM, CV_WRAP_DEFAULT

これも詳細不明です。Ubuntu-22.04パッケージのOpenCV-4.5.4と手元でビルドしたOpenCV-4.7.0を調べてみましたが使われている箇所はありませんでした。

各種情報

  • private継承、protected継承: 使用箇所なし
  • 多重継承
    • cv.detail.GraphCutSeamFindercv::detail::GraphCutSeamFinderBasecv::detail::SeamFinderを継承している。
    • cv.Feature2Dはパース結果に継承先が2つ出るが、実際の宣言は以下であり、Algorithmだけである。
#ifdef __EMSCRIPTEN__
class CV_EXPORTS_W Feature2D : public Algorithm
#else
class CV_EXPORTS_W Feature2D : public virtual Algorithm
#endif
  • virtual継承: 要調査

hdr_parser.pyの出力に関して調べたことは以上になりますが、スクリプトの中身を見たわけではないので足りないところがあるかもしれません。今後分かったら追記します。

⚠️ **GitHub.com Fallback** ⚠️