前言
C++作为一种编译型语言,在其设计之初就偏重于性能、效率和灵活性,偏向应用于系统编程、嵌入式、资源受限的软件和系统。Python作为一种解释型语言,内置了如str, tuple, list, dict等常用数据结构,支持自动垃圾回收,拥有简洁的语法、丰富的内置库和第三方库,被越来越广泛地使用在各种场景中。但Python在高便捷性的同时无可避免的缺乏高性能。
在部分应用场景中,我们需要在Python的灵活性上架构应用,底层算法希望借助C++的高性能, 那么我们可以考虑将C++开发的模块做成Python的bindings供Python调用。
之前在博客《使用pybind11生成C++的Python binding示例》中提到了使用Pybind11用于C++和Python之间的接口转换,为C++代码创建Python bindings。而反观常用的OpenCV库,发现其提供了一套比较完善的Python bindings生成工具。
OpenCV的Python Bindings生成
OpenCV的所有算法实现均采用C++,但是可以很方便地扩展Python、Java等语言的bindings。
要生成Python的bindings,原始的操作方式是按照Python的格式规定写相关的扩展函数。但是OpenCV作为一个体量庞大的工程,这么低效的方式显然不可取,所以其采用了自动化生成的方式来做。采用的核心代码如下,主要是4个文件:
此外,opencv/modules/python/CMakeLists.txt
中定义了需要扩展的C++模块,所有需要扩展的模块的头文件将会被自动抓取。
所有的头文件被传入到gen2.py脚本中用于解析,该脚本会调用另一个脚本hdr_parser.py做头文件解析,hdr_parser.py会将完整的头文件解析成Python lists暂存,这些lists包含了定义的函数和类等细节,例如函数名、返回值类型、输入参数、参数类型等。最终的lists包含了头文件中定义的所有的函数、枚举类型、结构体、类等。
但是并非所有的函数和类都会被解析转换成bindings,需要开发者特别指定需要扩展的函数。在OpenCV中,通过添加特殊的宏(macros)来标记函数、类等,以使得转换脚本能够识别出来。
当hdr_parser.py返回解析得到的函数等lists后,gen2.py会针对这些值(所有的functions/classes/enums/structs)生成扩展函数并存放在.h文件中。具体生成的文件路径在“build/modules/python_bindings_generator/pyopencv_generated_*.h
”。
有一些基本的OpenCV数据类型如Mat、Vec4i、Size,需要手动编写转换代码。 例如Mat数据类型需要转换成Numpy数组, Size数据类型需要被转换成包含2个整数的tuple。还有一些复杂的structs/classes/functions等也需要手动扩展。相应的扩展规则在modules/python/src2/cv2.cpp编写.
指定扩展用到的宏:
①CV_EXPORTS_W
用于指定需要扩展的函数或者类:
CV_EXPORTS_W void equalizeHist( InputArray src, OutputArray dst );
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_WRAP
用于扩展类的成员方法。
③CV_PROP
用于扩展class fields(类成员变量)。
④CV_EXPORTS_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 );
⑤其他:
Small classes/structs能被CV_EXPORTS_W_SIMPLE标记扩展.。例如KeyPoint、Match等结构的值能被传值到C++函数中,这些方法被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;
};
一些其他的small classes/structs 能被CV_EXPORTS_W_MAP扩展到Python原生字典(dictionary)。
class CV_EXPORTS_W_MAP Moments
{
public:
CV_PROP_RW double m00, m10, m01, m20, m11, m02, m30, m21, m12, m03;
CV_PROP_RW double mu20, mu11, mu02, mu30, mu21, mu12, mu03;
CV_PROP_RW double nu20, nu11, nu02, nu30, nu21, nu12, nu03;
};
上述字典反映到Python调用时:
>>> import cv2
>>> cv2.moments(0)
{'m00': 0.0, 'm10': 0.0, 'm01': 0.0, 'm20': 0.0, 'm11': 0.0, 'm02': 0.0, 'm30': 0.0, 'm21': 0.0, 'm12': 0.0, 'm03': 0.0, 'mu20': 0.0, 'mu11': 0.0, 'mu02': 0.0, 'mu30': 0.0, 'mu21': 0.0, 'mu12': 0.0, 'mu03': 0.0, 'nu20': 0.0, 'nu11': 0.0, 'nu02': 0.0, 'nu30': 0.0, 'nu21': 0.0, 'nu12': 0.0, 'nu03': 0.0}
另外,InputArray指定的是需要输入的矩阵(如Mat、GpuMat、UMat),OutputArray指定的是需要输出的矩阵。有些参数类型例如float、Point2f等需要硬编码,则使用CV_OUT, CV_IN_OUT指定。
CV_EXPORTS_W void minEnclosingCircle( InputArray points,
CV_OUT Point2f& center, CV_OUT float& radius );
还有一些例如CV_WRAP_PHANTOM、CV_WRAP_MAPPABLE、CV_WRAP_DEFAULT较少用到,可以参考资料[2]中的描述。
当所有需要扩展的模块的头文件都被解析完毕并保存到.h文件后,OpenCV通过调用g++命令去解析.h文件并进一步生成真正的Python bindings。
pyopencv_custom_headers.h # 自定义头文件
pyopencv_generated_modules.h # 模块
pyopencv_generated_enums.h # 枚举型变量
pyopencv_generated_types_content.h # 函数内容
pyopencv_generated_funcs.h # 函数
pyopencv_generated_types.h # 函数参数类型
pyopencv_generated_include.h # 需要包含的头文件
pyopencv_signatures.json # 映射到Python调用时的扩展名,如"cv::cuda::CannyEdgeDetector": [{"name": "cv.cuda_CannyEdgeDetector"}]
pyopencv_generated_modules_content.h # 模块内容
自由扩展模块
以基于博客《How to convert your OpenCV C++ code into a Python module》提供的内容,使用OpenCV4.4.0版本提供的Python bindings生成工具进行函数扩展。由于原博客内容已不具有时效性,在新版的OpenCV下需要做一定的修改才能成功编译生成。
代码地址:https://github.com/TracelessLe/cv2_gen_python_bindings
核心代码只有两个qymodule.cpp和qymodule.hpp文件:
其中qymodule.hpp文件中内容:
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
namespace qy
{
CV_EXPORTS_W void fillHoles(Mat &mat);
class CV_EXPORTS_W Filters
{
public:
CV_WRAP Filters();
CV_WRAP void edge(InputArray im, OutputArray imedge);
};
}
qymodule.cpp文件中内容:
#include"qymodule.hpp"
namespace qy
{
void fillHoles(Mat &im)
{
Mat im_th;
// Binarize the image by thresholding
threshold(im, im_th, 128, 255, THRESH_BINARY);
// Flood fill
Mat im_floodfill = im_th.clone();
floodFill(im_floodfill, cv::Point(0,0), Scalar(255));
// Invert floodfilled image
Mat im_floodfill_inv;
bitwise_not(im_floodfill, im_floodfill_inv);
// Combine the two images to fill holes
im = (im_th | im_floodfill_inv);
}
void Filters::edge(InputArray im, OutputArray imedge)
{
// Perform canny edge detection
Canny(im,imedge,100,200);
}
Filters::Filters()
{
}
}
接下来说明对原始OpenCV提供的gen2.py、hdr_parser.py、cv2.cpp修改的地方,pycompat.hpp不做修改:
gen2.py
第一处:
第二处:
第三处:
第四处:
hdr_parser.py
只有一处:
cv2.cpp
这是主要需要修改的文件。
第一处:
拷贝cv2.cpp并重命名文件(qy.cpp)。
第二处:
第三处:(新增UMat类型的映射代码)
代码:
typedef struct {
PyObject_HEAD
UMat* um;
} cv2_UMatWrapperObject;
static bool PyObject_IsUMat(PyObject *o);
// UMatWrapper init - try to map arguments from python to UMat constructors
static int UMatWrapper_init(cv2_UMatWrapperObject *self, PyObject *args, PyObject *kwds)
{
self->um = NULL;
{
// constructor ()
const char *kwlist[] = {NULL};
if (PyArg_ParseTupleAndKeywords(args, kwds, "", (char**) kwlist)) {
self->um = new UMat();
return 0;
}
PyErr_Clear();
}
{
// constructor (rows, cols, type)
const char *kwlist[] = {"rows", "cols", "type", NULL};
int rows, cols, type;
if (PyArg_ParseTupleAndKeywords(args, kwds, "iii", (char**) kwlist, &rows, &cols, &type)) {
self->um = new UMat(rows, cols, type);
return 0;
}
PyErr_Clear();
}
{
// constructor (m, rowRange, colRange)
const char *kwlist[] = {"m", "rowRange", "colRange", NULL};
PyObject *obj = NULL;
int y0 = -1, y1 = -1, x0 = -1, x1 = -1;
if (PyArg_ParseTupleAndKeywords(args, kwds, "O(ii)|(ii)", (char**) kwlist, &obj, &y0, &y1, &x0, &x1) && PyObject_IsUMat(obj)) {
UMat *um_other = ((cv2_UMatWrapperObject *) obj)->um;
Range rowRange(y0, y1);
Range colRange = (x0 >= 0 && x1 >= 0) ? Range(x0, x1) : Range::all();
self->um = new UMat(*um_other, rowRange, colRange);
return 0;
}
PyErr_Clear();
}
{
// constructor (m)
const char *kwlist[] = {"m", NULL};
PyObject *obj = NULL;
if (PyArg_ParseTupleAndKeywords(args, kwds, "O", (char**) kwlist, &obj)) {
// constructor (UMat m)
if (PyObject_IsUMat(obj)) {
UMat *um_other = ((cv2_UMatWrapperObject *) obj)->um;
self->um = new UMat(*um_other);
return 0;
}
// python specific constructor from array like object
Mat m;
if (pyopencv_to(obj, m, ArgInfo("UMatWrapper.np_mat", 0))) {
self->um = new UMat();
m.copyTo(*self->um);
return 0;
}
}
PyErr_Clear();
}
PyErr_SetString(PyExc_TypeError, "no matching UMat constructor found/supported");
return -1;
}
static void UMatWrapper_dealloc(cv2_UMatWrapperObject* self)
{
if (self->um)
delete self->um;
#if PY_MAJOR_VERSION >= 3
Py_TYPE(self)->tp_free((PyObject*)self);
#else
self->ob_type->tp_free((PyObject*)self);
#endif
}
// UMatWrapper.get() - returns numpy array by transferring UMat data to Mat and than wrapping it to numpy array
// (using numpy allocator - and so without unnecessary copy)
static PyObject * UMatWrapper_get(cv2_UMatWrapperObject* self)
{
Mat m;
m.allocator = &g_numpyAllocator;
self->um->copyTo(m);
return pyopencv_from(m);
}
// UMatWrapper.handle() - returns the OpenCL handle of the UMat object
static PyObject * UMatWrapper_handle(cv2_UMatWrapperObject* self, PyObject *args, PyObject *kwds)
{
const char *kwlist[] = {"accessFlags", NULL};
int accessFlags;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "i", (char**) kwlist, &accessFlags))
return 0;
// return PyLong_FromVoidPtr(self->um->handle(accessFlags));
}
// UMatWrapper.isContinuous() - returns true if the matrix data is continuous
static PyObject * UMatWrapper_isContinuous(cv2_UMatWrapperObject* self)
{
return PyBool_FromLong(self->um->isContinuous());
}
// UMatWrapper.isContinuous() - returns true if the matrix is a submatrix of another matrix
static PyObject * UMatWrapper_isSubmatrix(cv2_UMatWrapperObject* self)
{
return PyBool_FromLong(self->um->isSubmatrix());
}
// UMatWrapper.context() - returns the OpenCL context used by OpenCV UMat
static PyObject * UMatWrapper_context(cv2_UMatWrapperObject*)
{
return PyLong_FromVoidPtr(cv::ocl::Context::getDefault().ptr());
}
// UMatWrapper.context() - returns the OpenCL queue used by OpenCV UMat
static PyObject * UMatWrapper_queue(cv2_UMatWrapperObject*)
{
return PyLong_FromVoidPtr(cv::ocl::Queue::getDefault().ptr());
}
static PyObject * UMatWrapper_offset_getter(cv2_UMatWrapperObject* self, void*)
{
return PyLong_FromSsize_t(self->um->offset);
}
static PyMethodDef UMatWrapper_methods[] = {
{"get", (PyCFunction)UMatWrapper_get, METH_NOARGS,
"Returns numpy array"
},
{"handle", (PyCFunction)UMatWrapper_handle, METH_VARARGS | METH_KEYWORDS,
"Returns UMat native handle"
},
{"isContinuous", (PyCFunction)UMatWrapper_isContinuous, METH_NOARGS,
"Returns true if the matrix data is continuous"
},
{"isSubmatrix", (PyCFunction)UMatWrapper_isSubmatrix, METH_NOARGS,
"Returns true if the matrix is a submatrix of another matrix"
},
{"context", (PyCFunction)UMatWrapper_context, METH_NOARGS | METH_STATIC,
"Returns OpenCL context handle"
},
{"queue", (PyCFunction)UMatWrapper_queue, METH_NOARGS | METH_STATIC,
"Returns OpenCL queue handle"
},
{NULL, NULL, 0, NULL} /* Sentinel */
};
static PyGetSetDef UMatWrapper_getset[] = {
{(char*) "offset", (getter) UMatWrapper_offset_getter, NULL, NULL, NULL},
{NULL, NULL, NULL, NULL, NULL} /* Sentinel */
};
static PyTypeObject cv2_UMatWrapperType = {
#if PY_MAJOR_VERSION >= 3
PyVarObject_HEAD_INIT(NULL, 0)
#else
PyObject_HEAD_INIT(NULL)
0, /*ob_size*/
#endif
"cv2.UMat", /* tp_name */
sizeof(cv2_UMatWrapperObject), /* tp_basicsize */
0, /* tp_itemsize */
(destructor)UMatWrapper_dealloc, /* tp_dealloc */
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_reserved */
0, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
0, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT, /* tp_flags */
"OpenCV 3 UMat wrapper. Used for T-API support.", /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
0, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
UMatWrapper_methods, /* tp_methods */
0, /* tp_members */
UMatWrapper_getset, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
(initproc)UMatWrapper_init, /* tp_init */
0, /* tp_alloc */
PyType_GenericNew, /* tp_new */
0, /* tp_free */
0, /* tp_is_gc */
0, /* tp_bases */
0, /* tp_mro */
0, /* tp_cache */
0, /* tp_subclasses */
0, /* tp_weaklist */
0, /* tp_del */
0, /* tp_version_tag */
#if PY_MAJOR_VERSION >= 3
0, /* tp_finalize */
#endif
};
static bool PyObject_IsUMat(PyObject *o) {
return (o != NULL) && PyObject_TypeCheck(o, &cv2_UMatWrapperType);
}
static bool pyopencv_to(PyObject* o, UMat& um, const ArgInfo& info) {
if (PyObject_IsUMat(o)) {
um = *((cv2_UMatWrapperObject *) o)->um;
return true;
}
Mat m;
if (!pyopencv_to(o, m, info)) {
return false;
}
m.copyTo(um);
return true;
}
// template<>
// bool pyopencv_to(PyObject* o, UMat& um, const char* name)
// {
// return pyopencv_to(o, um, ArgInfo(name, 0));
// }
template<>
PyObject* pyopencv_from(const UMat& m) {
PyObject *o = PyObject_CallObject((PyObject *) &cv2_UMatWrapperType, NULL);
*((cv2_UMatWrapperObject *) o)->um = m;
return o;
}
第四处:
注释无关扩展:KeyPoint、DMatch、OnMouse以及
#ifdef HAVE_OPENCV_HIGHGUI
*
*
*
#endif
区间代码。避免相应部分出现报错。
第五处:
编译生成
-
先抓取头文件解析并生成.h文件
python3 gen2.py build
注:可能需要手动在build目录下新建一个空文件“pyopencv_custom_headers.h”。
-
编译生成.so文件
g++ -shared -rdynamic -g -O3 -Wall -fPIC \ qy.cpp src/qymodule.cpp \ -DMODULE_STR=qy -DMODULE_PREFIX=pyopencv \ -DNDEBUG -DPY_MAJOR_VERSION=3 \ `pkg-config --cflags --libs opencv` \ `/root/miniconda3/envs/pytorch1.6/bin/python3.7m-config --includes --ldflags` \ -I . -I/root/miniconda3/envs/pytorch1.6/lib/python3.7/site-packages/numpy/core/include \ -L`/root/miniconda3/envs/pytorch1.6/bin/python3.7m-config --exec-prefix`/lib \ -Ibuild \ -fno-lto \ -o build/qy.so
注:请注意修改相应路径,同时需要注意参考资料[3]没有“-fno-lto”标记,可能导致“lto1: fatal error”报错。
-
Python导入并使用
import sys import cv2 #sys.path.append('build') import qy im = cv2.imread('holes.jpg', cv2.IMREAD_GRAYSCALE) imfilled = im.copy() qy.fillHoles(imfilled) filters = qy.Filters() imedge = filters.edge(im) cv2.imwrite("Originalimage.png", im) cv2.imwrite("PythonModuleFunctionExample.png", imfilled) cv2.imwrite("PythonModuleClassExample.png", imedge)
相应Python代码在qy_test.py中:
特别说明
① 感谢Satya Mallick的《Learn OpenCV》系列博客,以及相应代码。
②感谢Github用户Nerdyvedi,项目“GSOC-Opencv-matting”提供UMat转换部分的代码和工具代码结构解析,以及参考的编译命令。
③感谢CSDN用户callinglove博客《OpenCV-Python bindings是如何生成的(2)》提供的文件修改思路。
④关于更多OpenCV如何对C++代码扩展Python bindings可以参考文末【参考资料】一栏。
⑤该代码仅实现了基本的cv::Mat类型的传参和返回值,关于cv::cuda::GpuMat数据类型的传参和返回值暂时还未实现。
参考资料
[1] 使用pybind11生成C++的Python binding示例
[2] OpenCV Docs - How OpenCV-Python Bindings Works?
[3] Learn OpenCV - How to convert your OpenCV C++ code into a Python module
[4] GitHub - How OpenCV-Python Bindings Works?
[5] Docs » 1. OpenCV简介 » 1.7. OpenCV Python绑定
[6] GitHub - spmallick/learnopencv/tree/master/pymodule
[7] GitHub - opencv/tree/master/modules/python/src2
[8] CSDN - OpenCV-Python bindings是如何生成的(2)
[9] GitHub - Nerdyvedi/GSOC-Opencv-matting/tree/master/python-bindings/pymodule
[10] stackoverflow - How to solve: lto1: fatal error: bytecode stream in file ‘…’ generated with LTO version 6.0 instead of the expected 7.1
[11] GitHub - TracelessLe/cv2_gen_python_bindings
[12] Python Documentation » 扩展和嵌入 Python 解释器 » 使用 C 或 C++ 扩展 Python
[13] cffi 编写和分发模块
[14] stackoverflow - Writing Python bindings for C++ code that use OpenCV
[15] Github - Algomorph/pyboostcvconverter
[16] Linux 編譯 shared library 的方法和注意事項
[17] undefined symbol问题的查找、定位与解决方法
[18] GitHub - opencv/modules/core/misc/python/pyopencv_cuda.hpp