目录
- 服务器环境
- 项目内容
- 实现方式
- Python.h
- C调用Python流程
- 图片传参
- 项目总代码
- 调用流程
- 打包指令
- 测试图片
- 打印结果
- 总结
最近接到个新需求,由于系统主体是C编写的,现在要调用python进行torch模型运算需要将图片先落盘再将路径传给python函数作为入参,图片一多硬盘的IO压力就大,以致处理一张图片需要的耗时大大加长。所以期望能让C直接调用python,图片直接在内存空间中传递不做落盘的操作,经过我三天的全网搜集尝试倒腾,终于实现了这个需求,写一篇博客记录一下也给接到同样需求的人指一条明路!
服务器环境
服务器必须具备C语言和Python3.6及以上的环境,并且将torch做运算需要用到的三方库都安装好,即python直接调用无误可正常输出结果。我本次的python项目是卡片检测,这个教程就姑且以我的项目为例。
项目内容
some_torch_project/
|-- test.jpg
|-- pyCall.c
|-- py_tobeCalled.py
|-- requirements.txt
`-- weights
`-- best.pt
我将主要要用到的文件目录罗列在了上表中,首先我们需要一个调用python函数的c文件pyCall.c,一张测试的图片test.jpg以及我们原有的torch深度学习项目。
实现方式
Python.h
C调用Python的核心是Python.h库文件,这个库文件在我们安装好Python后在Python指定路径中能搜寻到(/usr/local/include/python3.x),注意这个Python版本必须和你安装好三方库调用项目的Python环境对应上。
这里罗列下稍后将用到的所有库中函数:
-
Py_Initialize()
:初始化Python,在使用Python系统前,必须使用Py_Initialize对其进行初始化。它会载入Python的内建模块并添加系统路径到模块搜索路径中。这个函数没有返回值,检查系统是否初始化成功需要使用Py_IsInitialized; -
PyRun_SimpleString(char*)
:把输入的字符串作为Python代码直接运行,返回0表示成功,-1表示有错。大多时候错误都是因为字符串中有语法错误; -
PyImport_ImportModule(char*)
:导入Python脚本文件,以py脚本的名称为入参并将其作为寻找依据,可以理解为python的import some_module; -
PyDict_GetItemString(PyObject*,char*)
:导入脚本文件中对应的函数名,以函数的名称作为入参; -
PyTuple_New(int)
:创建Python入参的tuple空元组; -
Py_BuildValue(char*, ...)
:把C的变量转换成一个Python对象。当需要从 C++传递变量到Python时,需要使用这个函数; -
PyEval_CallObject(PyObject*,PyObject*)
:调用找到的Python函数,入参分别为4的返回值和5创建的入参元组; -
PyArg_Parse(PyObject*, ...)
:将Python的变量转换成C的变量格式可供C语言识别; -
Py_DECREF(PyObject*)
:释放PyObject变量的内存; -
Py_Finalize()
:结束Python调用。
C调用Python流程
这里简单介绍下使用C调用Python传参一个字符串的方式和流程:
#include <Python.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
int main(int argc, char** argv)
{
// 初始化Python
//在使用Python系统前,必须使用Py_Initialize对其
//进行初始化。它会载入Python的内建模块并添加系统路
//径到模块搜索路径中。这个函数没有返回值,检查系统
//是否初始化成功需要使用Py_IsInitialized。
Py_Initialize();
// 检查初始化是否成功
if ( !Py_IsInitialized() ) {
return -1;
}
// 添加当前路径
//把输入的字符串作为Python代码直接运行,返回0
//表示成功,-1表示有错。大多时候错误都是因为字符串
//中有语法错误。
PyRun_SimpleString("import sys");
PyRun_SimpleString("print('---import sys---')");
PyRun_SimpleString("sys.path.append('./')");
PyObject *pModule,*pDict,*pFunc,*pArg;
// 载入名为py_tobeCalled.py的脚本
pModule = PyImport_ImportModule("py_tobeCalled");
if ( !pModule ) {
printf("can't find py_tobeCalled.py");
getchar();
return -1;
}
pDict = PyModule_GetDict(pModule);
if ( !pDict ) {
PyRun_SimpleString("print('no pDict')");
return -1;
}
// 找出函数名为process_main的函数
printf("----------------------\n");
pFunc = PyDict_GetItemString(pDict, "process_main");
if ( !pFunc || !PyCallable_Check(pFunc) ) {
printf("can't find function [process_main]");
getchar();
return -1;
}
// 参数进栈
PyObject *args = PyTuple_New(1);
// PyObject* Py_BuildValue(char *format, ...)
// 把C++的变量转换成一个Python对象。当需要从
// C++传递变量到Python时,就会使用这个函数。此函数
// 有点类似C的printf,但格式不同。常用的格式有
// s 表示字符串,
// i 表示整型变量,
// f 表示浮点数,
// O 表示一个Python对象。
char *path = './test.jpg'
PyTuple_SetItem(args, 0, Py_BuildValue("s", (const char*)path));
// 调用Python函数
pArg = PyEval_CallObject(pFunc, args);
// 输出调用结果,string转为c可以理解的char*格式后输出
// 最终返回结果存放在result1变量中
char *result1;
PyArg_Parse(pArg, "s", &result1);//python类型转c++类型
printf("Detection res: %s\n",result1)
// 释放内存
Py_DECREF(args);
Py_DECREF(pModule);
// 关闭Python
Py_Finalize();
return 0;
}
图片传参
我在实现Python函数简单的调用后,测试了一下将图片路径作为字符串传参给torch项目让他找到图片后读取、模型运算、输出结果后返回结果给C并且在C这端输出运算结果,这么操作没有问题完全可实现。但是我们的要求是不落盘,那就不会存在一个图片的路径,图片只会存在在内存空间里,为了实现图片的传参先后尝试了三种方法:二进制流、opencv的Mat数组、Base64编码,分别说一下三种方式的尝试结果。
- 二进制流:二进制流对于C语言端来说实现最为简单,只要通过rb的格式去fopen一个图片就能获取到图片的二进制流,但是怎么把二进制流转变成string字符串这个点真是结结实实的难到了我,搜遍百度谷歌后,得出一个普遍的答案是创建一个结构体后把二进制流放在结构体内再进行传参,但是由于本人C水平有限,试错成本高时间又比较紧,放弃。
- opencv的Mat数组:本以为opencv是最有希望的,毕竟是一个在C端和Python端都存在的一个库,对于图片解析的风格应该都一致啊,在网上也搜到绝大多数C调用Python并传参图片的教程都是以opencv Mat转numpy.array完成的。就在我装完opencv准备ctrlC+V尝试的时候,发现编译C文件一堆报错,再一细看网上的代码,居然全是C++的相关代码,可是人家C根本就没有Mat这个概念啊,人家只有Iplmage格式啊,我要强行要用只能自己去解析一遍Iplmage结构体内容并想办法把这个格式转成对应大小的三维数组,再次因为C水平不到家,放弃。
- Base64编码:经过和擅长C的同事讨论,他认为二进制流在转换成Python可理解的string格式时出现了转换函数无法理解的字符导致了转换失败。既然二进制流转string费劲,那我何不直接转Python完全可以理解的base64编码呢,遂在网上找了个C语言将二进制流转换为base64编码的教程并且照做了一下,生成后转换成Python的string也很顺利,在Python端对base64进行解码后成功获取到了图片并运算返回了结果,至此完成了图片在内存中的交互,成功!
这里放一下base64转码的代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
const char * base64char = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
// 图片二进制流编码成为base64方便python理解并提取内容
char * base64_encode( const unsigned char * bindata, char * base64, int binlength )
{
int i, j;
unsigned char current;
for ( i = 0, j = 0 ; i < binlength ; i += 3 )
{
current = (bindata[i] >> 2) ;
current &= (unsigned char)0x3F;
base64[j++] = base64char[(int)current];
current = ( (unsigned char)(bindata[i] << 4 ) ) & ( (unsigned char)0x30 ) ;
if ( i + 1 >= binlength )
{
base64[j++] = base64char[(int)current];
base64[j++] = '=';
base64[j++] = '=';
break;
}
current |= ( (unsigned char)(bindata[i+1] >> 4) ) & ( (unsigned char) 0x0F );
base64[j++] = base64char[(int)current];
current = ( (unsigned char)(bindata[i+1] << 2) ) & ( (unsigned char)0x3C ) ;
if ( i + 2 >= binlength )
{
base64[j++] = base64char[(int)current];
base64[j++] = '=';
break;
}
current |= ( (unsigned char)(bindata[i+2] >> 6) ) & ( (unsigned char) 0x03 );
base64[j++] = base64char[(int)current];
current = ( (unsigned char)bindata[i+2] ) & ( (unsigned char)0x3F ) ;
base64[j++] = base64char[(int)current];
}
base64[j] = '\0';
return base64;
}
int main(int argc, char** argv)
{
//以二进制方式打开图像
FILE *fp = fopen("./test.jpg", "rb");
if(fp == NULL) {
perror("Img opening failed");
return -1;
}
fseek(fp, 0, SEEK_END);
long int size = ftell(fp);
rewind(fp);
//根据图像数据长度分配内存buffer
char* ImgBuffer=(char*)malloc( size* sizeof(char));
fread(ImgBuffer, size, 1, fp);
fclose(fp);
//创建图像base64编码buffer
char* imgbuffer_b64;
char *ret1;
unsigned int length;
imgbuffer_b64 = (char *)malloc((size/4+1)*16/3);
if (NULL == imgbuffer_b64)
{
printf("memory_error");
exit(2);
}
ret1 = base64_encode(ImgBuffer, imgbuffer_b64, size);
free(ImgBuffer);
length = strlen(imgbuffer_b64);
free(imgbuffer_b64);
return 0;
}
项目总代码
#include <Python.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
const char * base64char = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
// 图片二进制流编码成为base64方便python理解并提取内容
char * base64_encode( const unsigned char * bindata, char * base64, int binlength )
{
int i, j;
unsigned char current;
for ( i = 0, j = 0 ; i < binlength ; i += 3 )
{
current = (bindata[i] >> 2) ;
current &= (unsigned char)0x3F;
base64[j++] = base64char[(int)current];
current = ( (unsigned char)(bindata[i] << 4 ) ) & ( (unsigned char)0x30 ) ;
if ( i + 1 >= binlength )
{
base64[j++] = base64char[(int)current];
base64[j++] = '=';
base64[j++] = '=';
break;
}
current |= ( (unsigned char)(bindata[i+1] >> 4) ) & ( (unsigned char) 0x0F );
base64[j++] = base64char[(int)current];
current = ( (unsigned char)(bindata[i+1] << 2) ) & ( (unsigned char)0x3C ) ;
if ( i + 2 >= binlength )
{
base64[j++] = base64char[(int)current];
base64[j++] = '=';
break;
}
current |= ( (unsigned char)(bindata[i+2] >> 6) ) & ( (unsigned char) 0x03 );
base64[j++] = base64char[(int)current];
current = ( (unsigned char)bindata[i+2] ) & ( (unsigned char)0x3F ) ;
base64[j++] = base64char[(int)current];
}
base64[j] = '\0';
return base64;
}
int main(int argc, char** argv)
{
// 初始化Python
//在使用Python系统前,必须使用Py_Initialize对其
//进行初始化。它会载入Python的内建模块并添加系统路
//径到模块搜索路径中。这个函数没有返回值,检查系统
//是否初始化成功需要使用Py_IsInitialized。
Py_Initialize();
// 检查初始化是否成功
if ( !Py_IsInitialized() ) {
return -1;
}
// 添加当前路径
//把输入的字符串作为Python代码直接运行,返回0
//表示成功,-1表示有错。大多时候错误都是因为字符串
//中有语法错误。
PyRun_SimpleString("import sys");
PyRun_SimpleString("print('---import sys---')");
PyRun_SimpleString("sys.path.append('./')");
PyObject *pName,*pModule,*pDict,*pFunc,*pArgs, *pArg;
//以二进制方式打开图像
FILE *fp = fopen("./test.jpg", "rb");
if(fp == NULL) {
perror("Img opening failed");
return -1;
}
fseek(fp, 0, SEEK_END);
long int size = ftell(fp);
rewind(fp);
//根据图像数据长度分配内存buffer
char* ImgBuffer=(char*)malloc( size* sizeof(char));
fread(ImgBuffer, size, 1, fp);
fclose(fp);
//创建图像base64编码buffer
char* imgbuffer_b64;
char *ret1;
unsigned int length;
imgbuffer_b64 = (char *)malloc((size/4+1)*16/3);
if (NULL == imgbuffer_b64)
{
printf("memory_error");
exit(2);
}
ret1 = base64_encode(ImgBuffer, imgbuffer_b64, size);
free(ImgBuffer);
length = strlen(imgbuffer_b64);
// 载入名为py_tobeCalled的脚本
pModule = PyImport_ImportModule("py_tobeCalled");
if ( !pModule ) {
printf("can't find py_tobeCalled.py");
getchar();
return -1;
}
pDict = PyModule_GetDict(pModule);
if ( !pDict ) {
PyRun_SimpleString("print('no pDict')");
return -1;
}
// 找出函数名为process_main的函数
printf("----------------------\n");
pFunc = PyDict_GetItemString(pDict, "process_main");
if ( !pFunc || !PyCallable_Check(pFunc) ) {
printf("can't find function [process_main]");
getchar();
return -1;
}
// 参数进栈
pArgs = PyTuple_New(1);
// PyObject* Py_BuildValue(char *format, ...)
// 把C++的变量转换成一个Python对象。当需要从
// C++传递变量到Python时,就会使用这个函数。此函数
// 有点类似C的printf,但格式不同。常用的格式有
// s 表示字符串,
// i 表示整型变量,
// f 表示浮点数,
// O 表示一个Python对象。
PyObject *args = PyTuple_New(1);
PyTuple_SetItem(args, 0, Py_BuildValue("s", (const char*)imgbuffer_b64));
// 调用Python函数
pArg = PyEval_CallObject(pFunc, args);
// 输出调用结果,string转为c可以理解的char*格式后输出
// 最终返回结果存放在result1变量中
char *result1;
PyArg_Parse(pArg, "s", &result1);//python类型转c++类型
printf("Detection res: %s\n",result1)
// 释放内存
Py_DECREF(pArgs);
Py_DECREF(pModule);
free(imgbuffer_b64);
// 关闭Python
Py_Finalize();
return 0;
}
调用流程
打包指令
// 提前找到Python.h文件所在目录,一般来说在安装好的python库文件夹中即/usr/local/include/python3.x
// 如若没有找到请全服务器搜索,通过 find / -name Python.h 查找
// 将.c文件打包成.o文件,需要通过-I参数指明Python.h所在目录,否则将因为找不到Python.h报错
gcc -c -I /usr/local/include/python3.7 pyCall.c -o pyCall.o
// 将.o文件打包成可运行文件a.out,需要指明python3.7-config的路径,请按照自己安装的路径修改
gcc pyCall.o $(/usr/local/bin/python3.7-config --ldflags)
// 运行
./a.out
测试图片
准备一张测试图片如下所示,需要完成的任务是输出图中出现的卡片种类:
打印结果
最终的结果输出是C语言端的printf打印出来的,可以看到我们已经完整的完成了这个流程,实现了C语言调用Python深度学习项目并传参图片的需求。
总结
这个功能点对于把C忘得差不多的我来说实现起来着实是曲折坎坷,全网的相关资料我都扒了个干干净净一篇篇看了过去,一次次的试错最终终于是找到了一条路把C和Python两端连接了起来。这篇博客主要贡献在于C调用Python传参图片的方式,网上别的资料都是C++使用opencv读取图片成Mat后传输给Python,如果你的需求是C++调用Python深度学习项目,我相信您在别的博客能找到更有用的方法,但是万一您不幸的接到了C调用的需求,希望我这边“缝合怪”博客可以帮上您!
最后吐槽一句,写惯了Python写C的代码是真的不习惯在每行的末尾加上分号啊,我编译报没有分号的错得有二十次了,太难了!
注:以上内容中出现的代码大部分来自于其他博客,但我翻遍全网看了所有教程,真的忘记哪个代码是来自哪个博客的了,对应的作者看到后请直接联系我,我会将您的博客链接附上,谢谢。