我的AI之路(30)--C++调用Python版AI模型步骤、遇到的问题及解决

      机器人移动过程中需识别目标并根据距离作出相应处理,这些功能的移动和获取距离数据的代码都是C++写的,目标识别功能则由一个采用自己收集和标注的数据训练出的py-faster-rcnn模型来实现,这里功能上的整合实现就涉及到需要使用C++代码调用AI模型的python代码。

     https://docs.python.org/2/c-api/index.html给出了一些Python C/C++的API的说明, https://docs.python.org/2/extending/embedding.html 这里给出了C/C++代码调用Python的C/C++ API (py3c) 的主要步骤的说明:

    (1)首先需要调用Py_Initialize()做初始化;

    (2)调用PyImport_ImportModule()或PyImport_Import()来import需要调用的python函数所在的module并获得module的句柄;

    (3)调用PyObject_GetAttrString()来获得需要调用的python函数的句柄;

    (4)调用函数有传入的参数的话,调用PyTuple_New()创建一个tuple作为传入参数的容器,为每个参数调用Py_BuildValue(),并为每个参数调用PyTuple_SetItem()把参数加入容器;

    (5)以上面第三步获得的函数句柄和第四步获得的参数容器作为参数调用PyObject_CallObject();

    (6)根据函数调用返回值的数据类型,调用对应的函数,如PyLong_AsLong()、PyString_AsString()、PyBytes_AsString()等函数把返回值转换成C/C++的数据类型;

    (7)最后记得调用Py_Finalize()释放资源。

     简单的代码示例:  

     Py_Initialize();    // 初始化

    // 将Python工作路径切换到待调用模块所在目录,一定要保证路径名的正确性
    string path = "~/project/aimodule";
    string chdir_cmd = string("sys.path.append(\"") + path + "\")";
    const char* cstr_cmd = chdir_cmd.c_str();
    PyRun_SimpleString("import sys");
    PyRun_SimpleString(cstr_cmd);

    // 加载模块
    PyObject* moduleName = PyBytes_FromString("testaimodule"); //这个转换不是必须的,
    PyObject* pModule = PyImport_Import(moduleName);                //PyImport可以直接传入C格式的字符串
    if (!pModule) // 加载模块失败
    {
        cout << "Python get module "<<moduleName<< "failed." << endl;
        return -1;
    }
    // 加载函数
    PyObject* pv = PyObject_GetAttrString(pModule, "detect");
    if (!pv || !PyCallable_Check(pv))  // 验证是否加载成功
    {
        cout << "Can't find the funftion !" << endl;
        return -1;
    }
    // 设置参数

    PyObject* args = PyTuple_New(2);   // 2个参数
    PyObject* arg1 = Py_BuildValue("i", 100);
    PyObject* arg2 = Py_BuildValue("i", 300);
    PyTuple_SetItem(args, 0, arg1);
    PyTuple_SetItem(args, 1, arg2);

    // 调用函数
    PyObject* pRet = PyObject_CallObject(pv, args);

    // 获取返回值
    if (pRet)  
    {
        long result = PyLong_AsLong(pRet);
        ... //后续其他处理
    }

    Py_Finalize();      // 释放资源

以上API的详细规则可以查阅相关在线文档。

     需要指出的很重要的一点是,https://docs.python.org/2/extending/embedding.html里面给的代码对每个PyObject指针都调用了Py_DECREF()释放,这个可能是早期的做法,我开始也按照示例,对每个PyObject指针用完即调用Py_DECREF()做释放,使用python2.7执行代码时发现,如果执行一遍两遍含有Py_DECREF()调用代码的函数(函数内的代码在开始调用了Py_Initialize(),最后调用了Py_Finalize())没有任何问题,如果循环调用这个函数多次,程序会崩溃退出,报Program terminated with signal SIGSEGV, Segmentation fault的严重错误,反复看代码没任何问题啊,都是按照示例来一步一步写的,百思不得其解,后来想到这个是地址访问错误,会不会是发生了指针二次释放造成的,于是我试着把这个函数内的所有Py_DECREF()调用代码都注释掉,结果发现程序则可以循环调用这个函数无数次都没见崩溃,这点说明,调用Py_Finalize()回收资源就可以了,不要为每个PyObject*调用Py_DECREF()做资源释放,调用了反而会导致指针的二次释放引起SIGSEGV错误。

     另外一个重要问题是,C++调用一个python代码的自定义module A的函数,而这个文件里又import了没有安装到python系统模块所在的路径的自定义的其他模块,比如module B时,在运行程序时PyImport_Import()或PyImport_ImportModule()导入module A时总是返回空值,也就是没有找到module A,而这个文件其实是存在的,并且无论是在C++代码中还是在python代码中调用sys.path.append()把路径都加入到了搜索路径中,但是这个错误还是一直存在,但是如果用python写测试代码调用module A,手工执行测试代码没任何问题,说明在python下可以找到module A的,为何使用C++调用时就找不着了呢?郁闷了一阵,想起python还有个全局性设置搜索路径的办法就是设置环境变量PYTHONPATH,于是通过export PYTHONPATH=... 方式把module B所在的路径也加入到PYTHONPATH中,再执行C++调用代码,竟然顺利执行成功,也就是说能找到module B了!

    为何C++调用python代码时,设置模块搜索路径只有export PYTHONPATH=...起作用,而sys.path.append()不起作用,我猜原因是前者对C++和Python的进程都起作用,后者只对Python进程起作用,而Python的C/C++ API的内部实现在模块搜索方面可能有bug。

     另外要说明的是Python这些C/C++ API(Py3C)的实现在python2和python3中有差异,API也有变化,比如Python2的PyString_AsString()在Python3中就不存在了,需要使用PyUnicode_AsUTF8() (字节数据可使用PyBytes_AsString())来替代,

https://py3c.readthedocs.io/en/latest/reference.html这里对py2c和py3c的差别做了详细列举,很方便参考,另外也可以阅读 python安装后include目录中的头文件,比如python3的include下的unicodeobject.h和bytesobject.h中就有前面说的两个函数的声明。

     如果你的机器上同时安装了python2和python3,那么在编译C/C++调用程序时注意搜索对应版本的头文件和链接对应的库,如果使用python2.7,在编译时需要使用 -I<python2.7的include路径> -lpython2.7参数使用python2.7的头文件和库,使用python3.6,则使用-I<python3.6的include路径> -lpython36。

发布了61 篇原创文章 · 获赞 90 · 访问量 11万+

猜你喜欢

转载自blog.csdn.net/XCCCCZ/article/details/93133122
今日推荐