前言
想要在c++中部署训练好的深度学习模型方法可能有很多种,但是在低版本c++中部署深度学习模型似乎方法比较单一。由于进入项目时,大部分代码都是在vs2010上编译的,需要在程序中调用深度学习模型进行图片识别,采用调用python接口的方式进行部署及封装。vs2010版本为32位,python版本为python3.7.5 32位。除此以外需要安装cython,pyinstaller和vs2017,这三个工具是为了将python程序直接转译为可执行代码,加快运行速度用的。
过程
1.vs2010导入python库
这部分我就不写了,网上太多的例子。不过需要注意一点的是,如果vs2010是debug模式就要导入对应的dll,如python37_d.dll等,这个在单独安装python的时候会有一个选项,默认是不安装,这个需要自己选上才会在安装文件夹中出现。如果是安装anaconda的,这个可能没有。
要注意在编译的时候,会提示出现缺少inttypes.h头文件的错误,网上会有大量资源提供inttypes.h下载,但是要下载对应自己vs2010版本的文件,同时对于vs2010版,还需要安装对应版本的boost库,这时候才能正常调用python接口,我的boost库的版本是1.55版。这里我提供一个下载资源,有需要的自己下:
https://gitee.com/liuzhic_enfi/cxx_call_python.git
2.代码实践
这一块其实写的人不少,但有些方法不同意,而且很容易给后期打包的时候出现bug。 首先是启动阶段,如果是需要用到循环识别多图,这部分代码要写到循环外,如果要封装为函数,记得要返回里面模型的指针变量。
Py_SetPythonHome(L"./");
Py_Initialize();
if (Py_IsInitialized()==0)
{
printf("init python failed");
}
PyEval_InitThreads();
PyRun_SimpleString("import sys");
PyRun_SimpleString("sys.path.append('./')");
model = load_model();
其中load_model()函数里面的内容是
PyObject* load_model()
{
PyObject* pModule = PyImport_ImportModule("testpython1");
PyObject *pDict = NULL;
pDict = PyModule_GetDict(pModule);
PyObject* pFunc = NULL;
pFunc = PyDict_GetItemString(pDict, "build");
PyObject* model =PyObject_CallObject(pFunc, NULL);
return model;
}
循环读图中最重要的内容是将图片所在地址copy到numpy所在地址 这里附上for循环里面cv::Mat格式转为adarray格式的代码:
cv::Mat img = color_list[0].clone();
cv::cvtColor(img,img,cv::COLOR_BGR2RGB);
int iChannels = img.channels();
int iRows = img.rows;
int iCols = img.cols * 3;
uchar* CArrays = new uchar[img.rows * img.cols * 3];
if (img.isContinuous())
{
iCols *= iRows;
iRows = 1;
}
uchar* p;
int id = -1;
for (int i = 0; i < iRows; i++)
{
p = img.ptr<uchar>(i);
for (int j = 0; j < iCols; j++)
{
CArrays[++id] = p[j];
}
}
import_array1(-1);
npy_intp Dims[3] = { img.rows, img.cols, 3 };
PyObject *PyArray = PyArray_SimpleNewFromData(3, Dims, NPY_UBYTE, CArrays);
PyObject *ArgList = PyTuple_New(2);
PyTuple_SetItem(ArgList, 0, PyArray);
PyTuple_SetItem(ArgList, 1, model);
这一部分我一直心里感觉不舒服,是因为我只能用逐个遍历像素的方式传递地址,真正大佬都用std::memcpy()函数,不知后期有没有大佬能够用这个函数提升一下这段代码整体效率。 附:今天我把这段遍历像素的for循环改了,可以提升一部分代码效率: 原:
uchar* p;
int id = -1;
for (int i = 0; i < iRows; i++)
{
p = img.ptr<uchar>(i);
for (int j = 0; j < iCols; j++)
{
CArrays[++id] = p[j];
}
}
替换为:
std::memcpy(CArrays,img.ptr<uchar>(0),int(img.rows * img.cols * 3));
然后是向python函数导入变量和导出结果部分的代码:
PyObject* pModule = PyImport_ImportModule("testpython1");
PyObject *pDict = NULL;
pDict = PyModule_GetDict(pModule);
PyObject* pFunc2 = NULL;
pFunc2 = PyDict_GetItemString(pDict, "run");
PyObject* pp2 = PyObject_CallObject(pFunc2, ArgList);
int res = 0;
PyArg_Parse(pp2, "i", &res);
delete []CArrays ;
CArrays =nullptr;
现在循环内部就完成了,跳出循环后注意添加:
Py_Finalize();
3.打包
c++的打包让我一直很难受,因为不像python中的pyinstaller工具,可以自动找到并复制库文件(不过也是,python库文件放置那么统一,找不到也难)。这个打包时需要将python安装文件里面的大部分内容(主要是安装文件夹中的那些文件以及所用库的文件),boost库放置在工程文件夹中的bin文件夹中,这里我先提前声明,因为这里面我只知道一部分有用,剩下的文件我也不清楚,所以我就把所有文件考到bin文件夹下了。python自身代码可以利用pyinstaller打包为文件夹形式,并将文件夹内所有文件考到bin文件夹下。 如果移植到其他电脑上发现没有调用python接口,注意代码中这句:
Py_SetPythonHome(L"./");
设置python环境在本文件夹就可以了,基本上这句没问题都能启动,后期找不到的文件也会报错提示。这里用pyinstaller打包python的原因不仅仅是它会将程序编译为可执行文件,同时还会自动配置环境,像onnxruntime本来不支持win7环境,但是用pyinstaller打包封装后的程序是可以在win7上运行的。
|