C++调用Python [OpenCV与Numpy]

C/C++调用Python [opencv与numpy]

目前的情况下,如果你有一个深度学习模型,很想在项目中使用,但模型是用python写的,项目使用的是C++,怎么办?直观的做法是从C++调用python解释器,本文遇到的情景是C++环境下有张图片,需要将其中一个区域(ROI)进行放大(超分辨率重建),放大算法是python环境下的函数(pytorch模型),之后在C++环境下进行后续处理,假设希望从C/C++端调用的python函数如下(暂不介绍超分辨率,用opencv的resize替代):

import cv2 as cv
def super_resolution(img, scale=4):
    height, width = img.shape[:2]
    dsize = (width*scale, height*scale)
    big_img = cv.resize(img, dsize)
    return big_img

先介绍环境配置,再讲从C/C++调用Python的关键操作。

1. 环境设置

以windows环境为例,开发时需要做好相关配置,我的环境:Windows10,VS2017 Community,Python3.6.4_x64,OpenCV3.4.1_x64。

OpenCV环境

官方文档

  1. Visual Studio配置包含目录(编译错),D:\Program Files\opencv3\build\include
  2. Visual Studio配置库目录(链接错),D:\Program Files\opencv3\build\x64\vc15\lib
  3. Visual Studio配置链接器输入(链接错):opencv_world341.lib
  4. 追加Path环境变量(运行错):Path=Path;D:\Program Files\opencv3\build\x64\vc15\bin,改完环境变量一定要重启Visual Studio才能生效。

下面的例子读取一张图片并显示。

//opencv_demo.cpp
#include<opencv/cv.hpp>
using namespace cv;

int main(int argc, char *argv[]){
    Mat img = imread("lena.jpg");
    imshow("lena", img);
    waitKey(0);
    destroyAllWindows();
    return 0;
}

Python环境

官方文档——Python和C相互调用

  1. Visual Studio配置包含目录(编译错):D:\Program Files\Python36\include
  2. Visual Studio配置库目录(链接错):D:\Program Files\Python36\libs
  3. 新增环境变量(运行错):PYTHONHOME=D:\Program Files\Python36,改完环境变量一定要重启Visual Studio才能生效。

下面的例子从C调用Python解释器,并执行Python代码,打印时间和日期。

//python_demo.cpp
// https://docs.python.org/3.6/extending/embedding.html#very-high-level-embedding
#include <Python.h> 

int main(int argc, char *argv[])
{
    wchar_t *program = Py_DecodeLocale(argv[0], NULL);
    if (program == NULL) {
        fprintf(stderr, "Fatal error: cannot decode argv[0]\n");
        exit(1);
    }
    Py_SetProgramName(program);  /* optional but recommended */
    Py_Initialize();
    PyRun_SimpleString("from time import time,ctime\n"
                       "print('Today is', ctime(time()))\n");
    if (Py_FinalizeEx() < 0) {
        exit(120);
    }
    PyMem_RawFree(program);
    getchar();
    return 0;
}

Numpy环境

官方文档——如何利用Numpy的C API

numpy更多C API

  1. Visual Studio头文件目录(编译错):D:\Program Files\Python36\Lib\site-packages\numpy\core\include
  2. 关键代码(运行错):在Py_Initialize();之后必须调用import_array();以加载所有numpy函数(C API),与加载dll类似。

下面的例子展示用numpy接口实现矩阵计算矩阵乘法,并验证结果。

// numpy_demo.cpp 
#include <Python.h> 
#include <iostream>
#include <numpy/arrayobject.h>
using namespace std;

int main(int argc, char *argv[])
{
    wchar_t *program = Py_DecodeLocale(argv[0], NULL);
    if (program == NULL) {
        fprintf(stderr, "Fatal error: cannot decode argv[0]\n");
        exit(1);
    }
    Py_SetProgramName(program);  /* optional but recommended */
    Py_Initialize();
    
    import_array();/* load numpy api */
    double array_1[2][3] = { { 2,5,6 },{ 5,6,5 } };
    npy_intp dims_1[] = { 2, 3 };
    PyObject *mat_1 = PyArray_SimpleNewFromData(2, dims_1, NPY_DOUBLE, array_1);

    double array_2[3][4] = { { 1,3,0,4 },{ 2,2,5,3 },{ 1,2,1,4 } };
    npy_intp dims_2[] = { 3, 4 };
    PyObject *mat_2 = PyArray_SimpleNewFromData(2, dims_2, NPY_DOUBLE, array_2);

    PyObject *prod = PyArray_MatrixProduct(mat_1, mat_2);

    PyArrayObject *mat_3;
    PyArray_OutputConverter(prod, &mat_3);
    npy_intp *shape = PyArray_SHAPE(mat_3);
    double *array_3 = (double*)PyArray_DATA(mat_3);

    cout << "numpy result:\n";
    for (int i = 0; i < shape[0]; i++) {
        for (int j = 0; j < shape[1]; j++) {
            cout << array_3[i*shape[1] + j] << "\t";
        }
        cout << endl;
    }
    cout << "\nC result:\n";
    for (int i = 0; i < 2; i++) {
        for (int j = 0; j < 4; j++) {
            double t = 0;
            for (int k = 0; k < 3; k++)
                t += array_1[i][k] * array_2[k][j];
            cout << t << "\t";
        }
        cout << endl;
    }

    if (Py_FinalizeEx() < 0) {
        exit(120);
    }
    PyMem_RawFree(program);
    getchar();
    return 0;
}

2. C与Python的类型转换

翻译一段Python文档的原话

尽管处于不同的目的,但 扩展Python (Python调C,加速)和 内嵌Python (C调Python,方便)是完全一样的行为。

扩展Python要做的是:

  • 将数据从Python转换到C
  • 使用转换的值执行C函数的调用
  • 将数据从C转换到Python

当内嵌Python时,接口代码要做的是:

  • 将数据从C转换到Python
  • 使用转换的值执行Python接口函数的调用
  • 将调用返回的数据从Python转换到C

不管是扩展还是内嵌,代码都是C语言的,而这里的核心就是数据类型转换,C语言中,有bool,char,short,int,long,float,double 数组和指针等类型,在Python中有bool,int,float,str,list,tuple,setdict等类型,但Python的一切类型在C语言中皆为对象,也就是PyObject类型,所有Python类型都继承它,关于类型转换直接参考文档,下面简要介绍一下。

将C类型转换为Python类型一般使用Py_BuildValue,相反,将Python类型转换为C类型一般使用PyArg_ParsePyArg_ParseTuplePyArg_ParseTupleAndKeywords

基本类型转换Py_BuildValuePyArg_Parse

简单的类型转换例如:

// PyObject *Py_BuildValue(const char *format, ...);
// int PyArg_Parse(PyObject *args, const char *format, ...);

bool c_b;
PyObject *py_b = Py_BuildValue("b", true);/*C -> Python*/
PyArg_Parse(py_b, "b", &c_b);/*python -> C*/

int c_i;
PyObject *py_i = Py_BuildValue("i", 42);
PyArg_Parse(py_i, "i", &c_i);

double c_d;
PyObject *py_d = Py_BuildValue("d", 3.141592654);
PyArg_Parse(py_d, "d", &c_d);
    
const char *c_str;
PyObject *py_str = Py_BuildValue("u", "你好,世界!");
PyArg_Parse(py_str, "u", &c_str);

构造复杂的Python对象Py_BuildValue

以上都是基本类型的转换,另外,C调用Python函数时,如何构造高级数据结构如list,tuple,setdict呢?Py_BuildValue可以做到,演示如下,右边是PyObject*中包含的Python数据:

#define PY_SSIZE_T_CLEAN  /* Make "s#" use Py_ssize_t rather than int. */
#include <Python.h>

Py_BuildValue("")                        None
Py_BuildValue("i", 123)                  123
Py_BuildValue("iii", 123, 456, 789)      (123, 456, 789)
Py_BuildValue("s", "hello")              'hello'
Py_BuildValue("y", "hello")              b'hello'
Py_BuildValue("ss", "hello", "world")    ('hello', 'world')
Py_BuildValue("s#", "hello", 4)          'hell'
Py_BuildValue("y#", "hello", 4)          b'hell'
Py_BuildValue("()")                      ()
Py_BuildValue("(i)", 123)                (123,)
Py_BuildValue("(ii)", 123, 456)          (123, 456)
Py_BuildValue("(i,i)", 123, 456)         (123, 456)
Py_BuildValue("[i,i]", 123, 456)         [123, 456]
Py_BuildValue("{s,s}", "abc", "def")     {'abc', 'def'}
Py_BuildValue("{s:i,s:i}",
              "abc", 123, "def", 456)    {'abc': 123, 'def': 456}
Py_BuildValue("((ii)(ii)) (ii)",
              1, 2, 3, 4, 5, 6)          (((1, 2), (3, 4)), (5, 6))

解析参数中的Python对象PyArg_ParseTuple

反过来,在Python调用C函数时,会传递一系列参数,将这些参数解析为C中的对象,就要用到PyArg_ParseTuplePyArg_ParseTupleAndKeywords,首先,一个标准的C扩展函数是这样的:

static PyObject *
demo_func(PyObject *self, PyObject *args)
{
    int arg1;
    double arg2;
    const char *arg3;
    if (!PyArg_ParseTuple(args, "ids", &arg1, &arg2, &arg3))
        return NULL;
    /* based on the args, do something here */
    Py_RETURN_NONE;
}

其中self参数对于模块级别函数来说表示模块对象,如果是类方法则表示类对象。args参数包含了调用该扩展的参数列表,用PyArg_ParseTuple可以将其解析,更复杂的例子如下:

#define PY_SSIZE_T_CLEAN  /* Make "s#" use Py_ssize_t rather than int. */
#include <Python.h>

/* ======简单的参数解析====== */
int ok;
int i, j;
long k, l;
const char *s;
Py_ssize_t size;

ok = PyArg_ParseTuple(args, ""); /* No arguments */
    /* Python call: f() */
ok = PyArg_ParseTuple(args, "s", &s); /* A string */
    /* Possible Python call: f('whoops!') */
ok = PyArg_ParseTuple(args, "lls", &k, &l, &s); /* Two longs and a string */
    /* Possible Python call: f(1, 2, 'three') */
ok = PyArg_ParseTuple(args, "(ii)s#", &i, &j, &s, &size);
    /* A pair of ints and a string, whose size is also returned */
    /* Possible Python call: f((1, 2), 'three') */

/* ======具有默认值的可选参数====== */
const char *file;
const char *mode = "r";
int bufsize = 0;
ok = PyArg_ParseTuple(args, "s|si", &file, &mode, &bufsize);
/* A string, and optionally another string and an integer */
/* Possible Python calls:
   f('spam')
   f('spam', 'w')
   f('spam', 'wb', 100000) */


/* ======参数含有嵌套的tuple====== */
int left, top, right, bottom, h, v;
ok = PyArg_ParseTuple(args, "((ii)(ii))(ii)",
                      &left, &top, &right, &bottom, &h, &v);
/* A rectangle and a point */
/* Possible Python call:
   f(((0, 0), (400, 300)), (10, 10)) */

/* ======复数,并且一个函数名,解析失败时,在error message中
         会显示出错的函数名为myfunction====== */
Py_complex c;
ok = PyArg_ParseTuple(args, "D:myfunction", &c);
/* a complex, also providing a function name for errors */
/* Possible Python call: myfunction(1+2j) */

解析命名参数中的Python对象PyArg_ParseTupleAndKeywords

Python也支持命名参数,带名称的情况下可以改变参数的顺序,定义一个这样的扩展函数,就要用到PyArg_ParseTupleAndKeywords解析参数序列,定义一个函数如下,它包含一个必填的参数和三个可选的参数。

static PyObject *
keywdarg_parrot(PyObject *self, PyObject *args, PyObject *keywds)
{
    int voltage;
    const char *state = "a stiff";
    const char *action = "voom";
    const char *type = "Norwegian Blue";

    static char *kwlist[] = {"voltage", "state", "action", "type", NULL};

    if (!PyArg_ParseTupleAndKeywords(args, keywds, "i|sss", kwlist,
                                     &voltage, &state, &action, &type))
        return NULL;
    /* based on the args, do something here */
    Py_RETURN_NONE;
}

题外——const的使用

在学习PyArg_Parse函数时踩了一个坑,本来打算用char数组接受解析的数据,结果失败,原因是定义好的char数组的指针不可改变,代码如下

char c_str[256];
PyObject *py_str = Py_BuildValue("u", "你好,世界!");
PyArg_Parse(py_str, "u", c_str);
cout << c_str << endl;

正确的用法如上文,需要定义一个可以改变地址的指针,直接指向返回的字符串,这里将经常遗忘的c语言中const用法梳理:

const char d1 = 'a';/* 定义常量 */
char const d2 = 'a';/* char const和const char是一样的 */
const char *p1 = "hello";/* p1所指的内容不可改 */
p1 = "world";/* 但p1可以指向不同的地址 */
char const *p2 = "hello";/* char const *和const char *是一样的 */
char p3[256]="hello";/* p3指向的地址固定,但内容可通过p3更改 */
p3[0] = 'w';/* wello */
char *const p4 = p3;/* p4指向的地址固定,但内容可通过p4更改*/
p4[0] = 'b';/* bello */
const char *const p5 = p4;/* p5指向的地址固定,且内容不可通过p5更改*/
/* 综上:
1. char const和const char是一样的,一般使用const char
2. const char定义不更改的内容,常用于定义常量以及保护形参的内容不被更改
3. *const定义不可更改的指针,少见
*/

3. C调用Python

简单的例子

如何从C语言调用一个Python函数,

第一步,写一个Python文件(simple_module.py),包含一个函数(simple_func),功能为求和,放入PYTHONHOME/Lib/site-packages中,文件内容如下:

def simple_func(a,b):return a+b

第二步,写一个C文件,执行【Python解释器初始化、导入模块,导入函数,构造输入参数,调用函数,解析返回值,终止Python解释器】,文件内容如下(省略错误处理):

#include <Python.h> 

int main(int argc, char *argv[])
{
    wchar_t *program = Py_DecodeLocale(argv[0], NULL);
    if (program == NULL) {
        fprintf(stderr, "Fatal error: cannot decode argv[0]\n");
        exit(1);
    }
    Py_SetProgramName(program);  /* optional but recommended */
    Py_Initialize();

    PyObject *pName = PyUnicode_DecodeFSDefault("simple_module");
    PyObject *pModule = PyImport_Import(pName);/* 导入模块 */
    Py_DECREF(pName);
    PyObject *pFunc = PyObject_GetAttrString(pModule, "simple_func");/* 导入函数 */
    Py_DECREF(pModule);
    PyObject *pArgs = PyTuple_New(2);/* 初始化输入参数列表,长度为2 */
    PyTuple_SetItem(pArgs, 0, Py_BuildValue("i",100));/* 设置参数 */
    PyTuple_SetItem(pArgs, 1, Py_BuildValue("i",20));/* 设置参数 */
    PyObject *pRetValue = PyObject_CallObject(pFunc, pArgs);/* 调用 */
    Py_DECREF(pFunc);
    Py_DECREF(pArgs);
    int ret;
    PyArg_Parse(pRetValue, "i", &ret);
    Py_DECREF(pRetValue);
    printf("return value: %d", ret);

    if (Py_FinalizeEx() < 0) {
        exit(120);
    }
    PyMem_RawFree(program);
    getchar();
    return 0;
}

引用计数

上面的例子中多处调用Py_DECREF,这是对当前对象的引用计数执行减一,每个PyObject对象都有一个引用计数,用于垃圾回收,如果不能在恰当的时候增加(Py_INCREF)或减少(Py_DECREF)引用计数,则会发生:

  1. 你要访问的数据已经被释放
  2. 或者内存泄漏

关于这部分的内容,我只能说,看文档,多练习,个人粗浅的理解是 一般情况,生成的对象用完后立即销毁;知道哪里会偷偷减少引用,但该对象还要使用就要提前增加引用。

opencv与numpy类型转换

手头的工具已经能够解决本文最初的图像放大问题了,终于到这部分了,先把文章开头的函数加入simple_module.py文件中,Python代码解决。

import cv2 as cv
def simple_func(a,b):return a+b
def super_resolution(img, scale=4):
    height, width = img.shape[:2]
    dsize = (width*scale, height*scale)
    big_img = cv.resize(img, dsize)
    return big_img

比demo更复杂一点的是,C++下的图片是Mat类型,Python下的图片是np.ndarray类型,它们之间如何转换,幸运的是numpy提供了C API,可以方便地进行转换,PyArray_SimpleNewFromData可以将void*的数据转换成np.ndarrayPyArray_DATA可以获取PyArrayObject对象的数据指针void*,结合Matdata属性和构造方法可以轻松将结果还原。唯一需要注意的是,opencv裁剪后的ROI图像的数据共享了原来图像的数据,只是新建了一个Mat头信息,因此裁剪区域的数据在内存中并不连续,用isContinuous可以检测,在调用PyArray_SimpleNewFromData之前需要将裁剪图像拷贝到连续内存中才能获得正确的np.ndarray。以下是C++关键代码。

/* 读图 */
Mat img = imread("lena.jpg");
/* 手动选择与裁剪 */
Rect rect = selectROI(img, false);
Mat sml_img(img, rect);
/* 导入模块和函数 */
PyObject *pName = PyUnicode_DecodeFSDefault("simple_module");
PyObject *pModule = PyImport_Import(pName);
PyObject *pFunc = PyObject_GetAttrString(pModule, "super_resolution");
/* 准备输入参数 */
PyObject *pArgs = PyTuple_New(2);
if (!sml_img.isContinuous()) {sml_img = sml_img.clone();}
npy_intp dims[] = { sml_img.rows, sml_img.cols, 3 };
PyObject *pValue = PyArray_SimpleNewFromData(3, dims, NPY_UINT8, sml_img.data);
PyTuple_SetItem(pArgs, 0, pValue);/* pValue的引用计数被偷偷减一,无需手动再减 */
PyTuple_SetItem(pArgs, 1, Py_BuildValue("i",4));/* 图像放大4倍 */
/* 调用函数 */
PyObject *pRetValue = PyObject_CallObject(pFunc, pArgs);
/* 解析返回结果 */
PyArrayObject *ret_array;
PyArray_OutputConverter(pRetValue, &ret_array);
npy_intp *shape = PyArray_SHAPE(ret_array);
Mat big_img(shape[0], shape[1], CV_8UC3, PyArray_DATA(ret_array));
/* 释放所有 */
Py_DECREF(...)

4. 如何写Python扩展

你可能想加速核心代码或者是利用现成的C代码,如上文所说,写扩展和调用Python很相似,步骤就是1.写所需功能的C实现,2. 写指导如何编译打包的setup.py文件,3.编译打包安装。就可以用了,具体操作看官网,或者这个

posted @ 2022-09-29 19:33  MasonLee  阅读(1432)  评论(0编辑  收藏  举报