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环境
- Visual Studio配置包含目录(编译错),
D:\Program Files\opencv3\build\include
- Visual Studio配置库目录(链接错),
D:\Program Files\opencv3\build\x64\vc15\lib
- Visual Studio配置链接器输入(链接错):
opencv_world341.lib
- 追加
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环境
- Visual Studio配置包含目录(编译错):
D:\Program Files\Python36\include
- Visual Studio配置库目录(链接错):
D:\Program Files\Python36\libs
- 新增环境变量(运行错):
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环境
- Visual Studio头文件目录(编译错):
D:\Program Files\Python36\Lib\site-packages\numpy\core\include
- 关键代码(运行错):在
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
,set
和dict
等类型,但Python的一切类型在C语言中皆为对象,也就是PyObject
类型,所有Python类型都继承它,关于类型转换直接参考文档,下面简要介绍一下。
将C类型转换为Python类型一般使用Py_BuildValue
,相反,将Python类型转换为C类型一般使用PyArg_Parse
,PyArg_ParseTuple
和PyArg_ParseTupleAndKeywords
,
基本类型转换Py_BuildValue
和PyArg_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
,set
和dict
呢?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_ParseTuple
和PyArg_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)引用计数,则会发生:
- 你要访问的数据已经被释放
- 或者内存泄漏
关于这部分的内容,我只能说,看文档,多练习,个人粗浅的理解是 一般情况,生成的对象用完后立即销毁;知道哪里会偷偷减少引用,但该对象还要使用就要提前增加引用。
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.ndarray
,PyArray_DATA
可以获取PyArrayObject
对象的数据指针void*
,结合Mat
的data
属性和构造方法可以轻松将结果还原。唯一需要注意的是,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.编译打包安装。就可以用了,具体操作看官网,或者这个