最近在做有关无人机视觉的项目,由于superpoint网络和ros系统的联动比较困难复杂,故最后决定直接将superpoint的代码集成到C++里,用C++直接去调用,省去了节点之间通信的繁琐,故在此记录一下我一点一点摸索的历程。
什么是PyObject?
在一切的开始,你务必要知道什么是PyObject,简单来说,一切python里的东西都是PyObject,无论你是想要传参,还是接受返回值,还是调用某个函数,某个类等等,他们都是PyObject,你也必须用一个PyObject类型的指针来接受或传递他们,下面给出PyObject的定义
// object.h
/* Nothing is actually declared to be a PyObject, but every pointer to
* a Python object can be cast to a PyObject*. This is inheritance built
* by hand. Similarly every pointer to a variable-size Python object can,
* in addition, be cast to PyVarObject*.
*/
struct _object {
_PyObject_HEAD_EXTRA
Py_ssize_t ob_refcnt;
PyTypeObject *ob_type;
};
// pytypedefs.h
typedef struct _object PyObject;
// Include/pytypedefs.h
typedef struct _typeobject PyTypeObject;
// Include/cpython/object.h
struct _typeobject {
PyObject_VAR_HEAD
const char *tp_name;
Py_ssize_t tp_basicsize, tp_itemsize;
destructor tp_dealloc;
Py_ssize_t tp_vectorcall_offset;
getattrfunc tp_getattr;
setattrfunc tp_setattr;
PyAsyncMethods *tp_as_async;
reprfunc tp_repr;
PyNumberMethods *tp_as_number;
PySequenceMethods *tp_as_sequence;
PyMappingMethods *tp_as_mapping;
hashfunc tp_hash;
ternaryfunc tp_call;
reprfunc tp_str;
getattrofunc tp_getattro;
setattrofunc tp_setattro;
PyBufferProcs *tp_as_buffer;
unsigned long tp_flags;
const char *tp_doc;
traverseproc tp_traverse;
inquiry tp_clear;
richcmpfunc tp_richcompare;
Py_ssize_t tp_weaklistoffset;
getiterfunc tp_iter;
iternextfunc tp_iternext;
PyMethodDef *tp_methods;
PyMemberDef *tp_members;
PyGetSetDef *tp_getset;
PyTypeObject *tp_base;
PyObject *tp_dict;
descrgetfunc tp_descr_get;
descrsetfunc tp_descr_set;
Py_ssize_t tp_dictoffset;
initproc tp_init;
allocfunc tp_alloc;
newfunc tp_new;
freefunc tp_free;
inquiry tp_is_gc;
PyObject *tp_bases;
PyObject *tp_mro;
PyObject *tp_cache;
void *tp_subclasses;
PyObject *tp_weaklist;
destructor tp_del;
unsigned int tp_version_tag;
destructor tp_finalize;
vectorcallfunc tp_vectorcall;
unsigned char tp_watched;
};
其中你需要重点关注的是ob_type这个指针,我们后续常常用cout<<p->ob_type->tp_name来输出这个成员类型的名称,好帮助我们判断接下来应该使用什么类型对应的处理函数,比如list和tuple就是完全两种不同的数据结构,要调用两种数据类型来处理
python初始化
C++调用python的本质原理其实是启动一个python虚拟机,所以在调用python代码之前需要先对这个虚拟机进行一下初始化
首先要包含一下python的头文件,这个头文件只要你下载了python就是默认都有的,他的路径是/usr/include/pythonx.x(你的版本)/Python.h。一般情况下只需要#include<Python.h>就可以了,但是如果你有很多个python,可能会出现混淆,这个时候就建议你用一下绝对路径来指定python的头文件
一般情况下初始化的代码如下:
#include<Python.h>
int main()
{
Py_initlize();
import_array();//报错的话就把import_array替换成_import_array
return 0;
}
这里说一下这个import_array的事。他是对array数组进行初始化,不管你后面用不用到array都要调用一下这个函数,但是我在写代码的时候发现直接调用import_array会报错,网上查了一下说是import_array是一个宏定义,有一个返回值,因为这个返回值导致了问题,而import_array是调用了_import_array作为核心判断逻辑的,所以可以用_import_array进行代替,也可以修改一下import_array的宏定义,让他不要有返回值,具体操作网上有很多,读者如果遇到可以自行查阅一下
接着需要添加一下文件路径,这个路径就是你想要调用的python程序的绝对路径
PyRun_SimpleString("import sys");
PyRun_SimpleString("sys.path.append('your route/')");//你要调用的python程序的路径
PyRun_SimpleString("print('route append successfully')");
这里的PyRun_SimpleString函数可以根据字面意思理解,我觉得应该没有什么很难理解的地方,值得注意的就是,python正常是不区分单引号和双引号的,但在C++里如果你想调用python就只能在双引号里使用单引号,不能混淆
最后选择你想打开的py文件,文件名不需要带py
PyObject *pmodule=PyImport_ImportModule("demo");//打开你想要执行的python程序
python调用类及参数构建
由于本项目没有用到python函数的调用,用的都是类成员和方法的调用,故有关函数的调用不多赘述,有需要的读者可以上网上查阅,网上有很多
要调用一个类,首先需要找到这个类,并将其返回值存在一个PyObject类型的指针中,比如我下面这个例子
PyObject *superpointfrontend=PyObject_GetAttrString(pmodule,"SuperPointFrontend");//找到superpointfrontend这个类,并传回指针
pmodule就是前面指向这个程序的指针
找到这个类的话,接下来就是要对其进行初始化了,初始化在大多数情况下不可避免的要传入参数,所以这里说一下如何构建传入参数。
PyObject *argofsuperpoint=Py_BuildValue("(siddi)",weights_path,nms_dist,conf_thresh,nn_thresh,cuda);
利用Py_BuildValue函数,将返回值传回一个PyObject里,它的函数原型如下
PyObject* Py_BuildValue(const char *format, ...);
其中format是格式化字符串,用于指定后面的参数类型,对应关系如下
"i": 整数
"s": 字符串(const char*)
"d": 双精度浮点数
"f": 单精度浮点数
"O": 指向 PyObject 的指针
"()": 元组
"{}": 字典
"[]": 列表
后面是可变参数,参数的数量根据你输入的格式化字符串决定,所以上面的例子中,(siddi)就表示我要传入一个第一个元素是字符串,第二个元素是int,第三、四个元素都是double,最后是一个int的元组类型。
构建好参数之后,就可以愉快的调用并初始化类成员了,利用PyEval_CallObject函数即可
PyObject *fe=PyEval_CallObject(superpointfrontend,argofsuperpoint);
第一个参数是指向这个类的指针,第二个参数是指向参数的指针。用一个PyObject来接受返回值。
接着就要调用类的方法了,利用的是PyObject_CallMethod函数,这个函数的传参有两种方式,我会在下面说明
PyObject *left=PyObject_CallMethod(fe,"run","(O)",pyimg0);
来看一下他的函数原型
PyObject* PyObject_CallMethod(PyObject *o, const char *name, const char *format, ...);
第一个参数就是指向类的指针,这个指针是你初始化完之后的指针,第二个参数是你想要运行的方法的名字,第三个参数是格式化字符串,决定了如何传参,后面的都是可变参数,同上面所说一样
第一种方式,就是利用上面构造参数的方法,将参数构造好作为一个PyObject传入,例子所展示的方法就是这种方法,“pyimg0”这个参数提前被我构造好了
第二种方法,就是直接像Py_BuildValue函数一样传参,格式化字符串直接指定要传的参数,比如
PyObject *show=PyObject_CallMethod(test,"chuancan","sid","hahaha",100,3.1415926);
这样也是OK的
调用完毕之后,就该把返回的数据想办法转换成C++的格式,简单返回一个数还好说,如果是返回一个list,或者tuple就比较麻烦了,所以下面讲解一下面对这种情况该怎么办
首先,我们cout<<p->ob_type->tp_name看一下返回的数据类型是什么,这里我输出完之后发现返回的是一个tuple,根据python代码我知道返回值一共有三个,所以这个tuple里面有三个元素。
确定是tuple了之后,我们就需要用到PyTuple_GetItem从返回值里面提取我想要的数据了,函数原型如下
PyObject* PyTuple_GetItem(PyObject *p, Py_ssize_t pos);
第一个参数是我调用完方法之后的返回指针,第二个参数是我想要获取的元素的下标,从0开始算
我们用一个实例来讲解一下
PyObject leftpts,rightpts;
PyArg_Parse(PyTuple_GetItem(left,0),”O”,&leftpts);//”O”表示对象是一个python——object,这个是把left这个元组的第0个元素,也就是pts返回给leftpts
PyArg_Parse(PyTuple_GetItem(right,0),”O”,&rightpts);//”O”表示对象是一个python——object
需要先初始化一个PyObject,用来接受我获取的数据,数据类型仍然是一个PyObject类型,这个根据实际情况来,因为我返回的是一个list,所以后续我还要用list来处理它。根据这个例子,相信读者也很容易理解PyArg_Parse函数的作用,就是把一个元素转化成C++支持的数据结构类型,不过我这里貌似有点多余了……毕竟当时也是摸索着写的,在写这篇文章的过程中理解也在不断加深。
接着就是将这些内容提取出来,下面直接附上一段代码,代码是AI写的,我们可以看一下
int sizepts0=PyObject_Size(leftpts);
int sizepts1=PyObject_Size(rightpts);
PyObject *iterpts0=PyObject_GetIter(leftpts);
PyObject *iterpts1=PyObject_GetIter(rightpts);
//输出左目的pts
while(true)
{
PyObject *next=PyIter_Next(iterpts0);//数据的行数,在pts里分别为1,2,3行
if(!next)
{
break;
}
if(!PyList_Check(next))
{
int foosize0=PyObject_Size(next);//这一行的大小,也就是列数
cout<<foosize0;
PyObject *_iterpts0=PyObject_GetIter(next);//索引到这一行的第一个元素
while(true)
{
PyObject *next2=PyIter_Next(_iterpts0);//利用迭代器逐个输出
if(!next2)
{
break;
}
if(!PyList_Check(next2))
{
double foo=PyFloat_AsDouble(next2);
cout<<foo<<" ";
}
}
cout<<endl;
}
}
整段代码的核心逻辑说白了就是剥洋葱,一层一层抽丝剥茧,其实根据这段代码,我们可以推知他的数据结构

上面的逻辑相当于是一开始可以获得head指针,根据这个指针可以拿到一共有几个nodehead,接着逐层遍历,可以吧node1.1~node3.3给遍历完,带一点数据结构的知识
里面涉及到的函数我认为都可以根据函数名称推断功能,就不多讲了
变量与虚拟资源的释放
在最终调用完python之后,需要把涉及到的变量和python虚拟机都释放掉,避免发生内存泄漏的情况,用到的函数也很简单
Py_DECREF(p);//p:待释放的变量
Py_Finalize();//释放python虚拟机