0、前言
有几个不同的选项可以将你的Python机器学习模型集成到你的C++ Qt客户端应用程序中。以下是一些可能的解决方案:
- 创建API: 将你的机器学习模型部署为一个API服务。你可以使用像Flask这样的轻量级Web框架来创建一个简单的HTTP服务。这样,你的Qt应用程序可以通过HTTP请求来与这个服务交互,并获取模型的预测结果。部署完成后,你需要在Qt中使用QNetworkAccessManager或其他HTTP客户端来呼叫远端API。
- 使用Python和Qt的集成: 如果你希望避免网络请求,可以考虑在你的应用程序中直接使用Python。这可以通过使用像PyQt5或PyOtherSide这样的库实现。
- C++机器学习库: 如果模型不是特别复杂,你可以考虑使用C++机器学习库(例如Shark、dlib或mlpack)重新实现你的模型。这样可以避免使用Python,但可能需要重新训练模型并调整算法。
- ⭐使用嵌入式Python解释器: 在你的Qt应用程序中嵌入or指定一个Python解释器。这允许你直接从C++代码中调用Python代码,并使用你的模型。
Py_SetPythonHome(L"D:\\Anaconda\\envs\\cp39_torch1_13_1_vision_0_14_cu117"); Py_Initialize();
通过嵌入式Python部署方法,目标机器(用户的机器)无需单独安装Python。这是因为所有必要的Python组件都应该被包含在你的应用程序中,作为该应用程序的一部分进行分发。这意味着Python解释器和所有必要的库、模块及其他依赖都被静态链接到应用程序或以其他形式捆绑在一起,用户不需要执行额外的安装步骤。
但是,这种方法也意味着你的Qt应用程序的部署包会更大,因为它可能需要包括Python解释器和额外的Python库。
- 将模型转换为ONNX: 如果你的机器学习模型支持导出为ONNX(Open Neural Network Exchange)格式,你可以将其转换为ONNX,然后在C++应用程序中使用支持ONNX的机器学习库(例如ONNX Runtime)来加载和运行模型。
- 使用远程服务器处理: 如果客户端应用程序的性能和网络延迟不是问题,你可以在远程服务器上托管你的Python模型,并通过网络请求进行交互。
每个选项都有其优缺点,你应该根据你的具体需求来决定使用哪一种。比如,第一个选项可能是最简单的开始方式,而第三个和第五个选项可能提供了最好的性能。最后选择哪种方法,取决于你的应用需求、性能考虑以及你对于不同技术的熟悉程度。
最近和之前在做的项目都是在Qt中用c++编码项目的开发,其中的功能涉及到机器学习的算法,之前有尝试过直接调用python,但是一直不成功, 再加上想到直接打包python程序也会更大,遂放弃。前面几篇博客讲到把Yolov8的目标检测和分割模型部署成c++的,是因为当时采用了上述的第五类方法,直接将算法由python转换为c++嵌入到程序中了,但是实际中这样虽然性能最好,但是很麻烦。这次的项目涉及到多种检测算法,且算法需要随着实验进行实时迭代,不可能说将python转换成c++了(这样十分麻烦,涉及到重新编写…配置环境…),于是这次打算再次采用第四种方法,在Qt项目中直接使用c++来调用python。为什么不适用创建API的方法呢?主要还是因为数据涉及到不能通过网络传输。总之,让我们快速进入正题,如何使用Qt调用python把!
一、Qt调用python
如果是vs2019,可以看看前面讲解在vs2019下部署yolov8中的配置依赖环境,都是一样的,这里因为换成你了Qtcreator,那就以qtcreator来讲:
注意,看一下自己的项目环境是否是64位,否则后面会报错:
1.1:pro文件增加python目录
INCLUDEPATH += -I D:\software\anaconda3\envs\yolov8\include
LIBS += -LD:\software\anaconda3\envs\yolov8\libs -lpython39
1.2:将Python集成到Qt中
工具->选项->环境->外部工具,添加->添加目录 (双击可任意更改名称这里更改为RunPy)->添加工具(双击可任意更改名称这里更改为Python3)。点击Python3,配置执行档、参数等配置:
- 执行档: python的安装目录,我这里(D:\software\anaconda3\envs\yolov8\python.exe),你自己找到自己安装的python.exe目录
- 参数:%{CurrentDocument:FilePath}
- 工作目录:%{CurrentDocument:Path}
1.3:将相关的文件拷贝到项目的可执行目录
- 所需拷贝文件:python相关的Dll文件、以及对应的你想要调用的py文件
🌸在Qt中创建一个Python脚本测试一下:
- 创建python脚本(或者前面已经将Python脚本移动到可执行目录下,这个时候在
Other Files
中也能看到)
这里我新建一个测试脚本test.py
:
import matplotlib.pyplot as plt
def temperImg():
plt.plot([1, 2, 1, 2])
plt.show()
temperImg()
- 选中文件->点击 工具->外部->RunPy->Python3,运行脚本
OK可以看到可以成功调用python:
1.4:添加环境变量
1.5:修改include文件夹中的object.h文件
修改include文件夹中的object.h文件,因为Python中slots是关键字,Qt中slots也是关键字,会冲突。
编译报错error: expected unqualified-id before ‘;’ token,这是与qt的slots关键字冲突,解决办法是将python中的slot取消宏定义,然后再恢复,如下图
#undef slots
PyType_Slot *slots; /* terminated by slot==0. */
#define slots Q_SLOTS
1.6:C++程序调用
具体流程大概包括以下几个步骤:
-
设置Python环境:
使用 Py_SetPythonHome() 指定Python环境路径。这个路径通常指向你希望使用的Python解释器的环境。 -
调用 Py_Initialize() 来初始化Python解释器。
检查Python解释器是否初始化成功: -
使用 Py_IsInitialized() 验证Python解释器是否已经成功初始化,如果没有,则输出相应的错误日志。
-
修改Python搜索路径:
通过 sys.path.append(‘./’) 来修改Python模块搜索路径。这样做通常是为了确保Python能够找到你的脚本所在的目录。 -
导入Python模块:
使用 PyImport_ImportModule(“testTunnel”) 来导入名为testTunnel的Python模块。 -
获取模块中函数的指针:
获得模块中名为judge_image的函数指针,以便能够调用该函数。 -
准备函数参数:
创建一个Python元组(PyTuple_New(1)),用以传递参数给Python函数。
将C++字符串(在这里是一个文件夹路径)转换为Python字符串,并通过 PyTuple_SetItem() 设置到参数元组中。 -
调用Python函数:
使用 PyEval_CallObject(pTest_1,pPara) 来调用Python函数,并传递参数元组pPara。这将执行Python脚本中的judge_image函数。 -
处理Python函数的返回值:
PyEval_CallObject() 返回的PyObject* pyValue是Python函数的返回值。需要根据实际情况处理这个返回值。
错误处理:
如果在任何步骤中遇到错误,使用 PyErr_Print() 打印Python错误信息,并进行适当的错误处理。 -
清理资源:
不要忘记在使用完Python对象后释放资源,比如使用 Py_DECREF() 减少引用计数,以及在最后使用 Py_Finalize() 终止Python解释器。
// 获取选中项的文本,这里假设是文件夹的完整路径
QString folderPath = item->text();
QList<QFileInfo> noTargetList;
Py_SetPythonHome(L"D:/software/anaconda3/envs/yolov8");
Py_Initialize();
if( !Py_IsInitialized() )
qDebug()<<"图片加载模块的Python初始化失败";
PyRun_SimpleString("import sys");
PyRun_SimpleString("sys.path.append('./')");//这一步很重要,修改Python路径
//创建模块指针
PyObject* pModule = PyImport_ImportModule("testTunnel");
if (!pModule)
qDebug()<<"图片加载模块的Python获取模块指针失败";
else {
qDebug()<<"successfuly obtained Module!";
}
const char* filename = PyModule_GetFilename(pModule);
if (filename == NULL) {
PyErr_Print();
Py_DECREF(pModule);
Py_Finalize();
}
//创建函数指针
PyObject* pTest_1 = PyObject_GetAttrString(pModule, "judge_image");
if (!pTest_1) {
// Failed to get function pointer
PyErr_Print(); // Print Python error indicator if available
qDebug() << "获取函数指针失败";
} else {
qDebug() << "Successfully obtained function pointer";
}
PyObject* pPara = PyTuple_New(1);
if (pPara == NULL) {
// 元组创建失败,进行错误处理
PyErr_Print();
// 继续下一个迭代
}
qDebug() << "fPath:"<<folderPath;
const char* cstr = folderPath.toUtf8().constData(); // 将 QString 转换为 C 字符串
PyTuple_SetItem(pPara, 0, Py_BuildValue("s",cstr)); //参数1为String型 "Hello"
PyObject* pyValue = PyEval_CallObject(pTest_1,pPara);
PyErr_Print();
qDebug() << "pyValue"<<pyValue;
...
// 在使用完 pModule 后减少其引用计数
Py_DECREF(pModule);
// 在使用完列表后,减少其引用计数
Py_DECREF(pyValue);
// 在使用完参数元组后,减少其引用计数
Py_DECREF(pPara);
// Py_Finalize();