使用OpenCV自带gen2.py等工具生成C++的Python binding示例

27 篇文章 7 订阅
15 篇文章 0 订阅

前言

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_PHANTOMCV_WRAP_MAPPABLECV_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

区间代码。避免相应部分出现报错。
在这里插入图片描述
在这里插入图片描述
第五处:
在这里插入图片描述

编译生成

在这里插入图片描述

  1. 先抓取头文件解析并生成.h文件

    python3 gen2.py build
    

    注:可能需要手动在build目录下新建一个空文件“pyopencv_custom_headers.h”。

  2. 编译生成.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”报错。
    在这里插入图片描述

  3. 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

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TracelessLe

❀点个赞加个关注再走吧❀

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值