翻译自How OpenCV-Python Bindings Works?
目标
学习
- OpenCV-Python bindings是如何生成的
- 如何为Python扩展新的opencv模块
OpenCV-Python bindings是如何生成的
在OpenCV里,所有算法都是用C++实现的。但是这些算法可以在别的语言里使用,比如Python,Java等。这就是通过bindings生成器实现的。这个生成器产生一个C++和Python之间的桥梁,让用户可以在Python里调用C++函数。要完整了解后台发生了什么,需要Python/C API的知识,在Python官方文档里有一个扩展C++的函数到Python里的简单例子。通过手工写包装函数的方法在OpenCV里把所有函数扩展到Python是个很费时的任务,所以OpenCV用了个更智能的办法。OpenCV用一些Python脚本从C++的头文件自动生成这些包装函数,这些脚本放在modules/python/src2。我们来看看他们做了什么。
首先,modules/python/CMakeFiles.txt是一个CMake脚本,用来检查要扩展到Python的模块。它会自动检查所有要扩展的模块,并抓取他们的头文件。这些头文件包含那个模块的所有类,函数,常量的列表等。
然后,这些头文件被传给Python脚本modules/python/src2/gen2.py。这就是那个Python bindings生成脚本。它会调用另外一个Python脚本modules/python/src2/hdr_parser.py。这是头文件语法分析器脚本。这个头文件语法分析器把整个头文件拆分成小的Python列表。这些列表包含所有特定函数和类的详细内容。比如,一个函数会被解析成一个包含函数名,返回类型,输入参数,参数类型等的列表。最后的列表会包含那个头文件里所有的函数,结构体,类等的详细信息。
但是头文件语法分析器并不解析头文件里所有的函数和类。开发人员得指定哪些函数是要导出到Python的。为此在这些声明开始添加了一些宏,好让头文件语法分析器能识别要解析的函数。这些宏是由要写特定函数的开发人员添加的。简单来说,开发人员来决定哪些函数要扩展到Python,而哪些不用。
头文件语法分析器会返回一个最后的解析了的函数的大列表。我们的生成器脚本(gen2.py)会为所有被分析器解析的 函数/类/枚举/结构体 创建包装函数(你可以在编译阶段在build/modules/python/目录找到类似于pyopencv_generated_*.h这样的头文件)。但是有些基础的OpenCV数据类型比如Mat, Vec4i, Size 需要手工扩展。比如,一个Mat类型应该被扩展成Numpy数组,Size应该被扩展成两个整数的元组等。类似的,有些复杂的 结构体/类/函数 需要被手工扩展。所有这种手工扩展函数都放在modules/python/src2/cv2.cpp。
现在唯一剩下的事情是编译这些包装文件成cv2模块。所以当你在Python里调用一个函数时,比如 res = equalizeHist(img1, img2)
。你传入两个numpy数组,并希望输出另一个numpy数组。这些numpy数组被转换成了cv::Mat并在C++里调用equalizeHist()函数。最后的结果res被转换回Numpy数组。简单来说,就是几乎所有的运算都是在C++里完成的,这使执行速度也和C++里几乎一样。
这就是OpenCV-Python binding 如何产生的基本介绍。
如何在Python里扩展新的模块
头文件语法分析器基于一些添加在函数声明里的包装的宏来分析头文件。枚举常量不需要任何包装宏,他们是被自动包装的。但是剩下的函数和类需要包装宏。
函数使用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里能够通过新名字调用函数。下面的例子,有三个函数,每个在Python里都带一个前缀。类似的CV_WRAP_AS可以被用来包装重载方法。
CV_EXPORTS_W void integral( InputArray src, OutputArray sum, int sdepth = -1 );
CV_EXPORTS_AS(integral2) void integral( InputArray src, OutputArray sum,
OutputArray sqsum, int sdepth = -1, int sqdepth = -1 );
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的原生字典。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里主要的扩展宏,一般来说,开发人员得在合适的位置放上合适的宏。剩下的工作就由生成器脚本完成。有时候可能有意外情况生成器脚本没法创建包装器,这样的函数需要手工处理。但是大多数情况,用OpenCV编码指导写的代码应该就能被生成器脚本自动包装了。