通過在C++程序調用Python3所提供的C接口可以實現調用Python程序所實現的功能。在C++調用深度學習訓練好的模型時,如果不使用一些部署手段,這種C++調用Python接口的方式雖然大大犧牲了效率,但是可以說是也是一種取巧的方法。這裏記錄一下C++如何去調用Python3的接口,作爲一個總結。
操作系統:Ubuntu16.04
構建工具:CMake
要想在 C++ 中調用 Python3 ,必須在 Cmake 中添加一些 Python3 的動態鏈接庫:
可以在 include_directories
中加入:
/usr/include/python3.5
/usr/lib/python3/dist-packages/numpy/core/include/numpy
在 target_link_libraries
中加入:
/usr/lib/x86_64-linux-gnu/libboost_python-py35.so
/usr/lib/x86_64-linux-gnu/libstdc++.so.6
/usr/lib/x86_64-linux-gnu/libpython3.5m.so
1. 調用Python3中的函數
int testFunction1(){
Py_Initialize();
if(!Py_IsInitialized()){
cout << "[Error] Init error" << endl;
return -1;
}
string change_dir = "sys.path.append('../scripts')";
PyRun_SimpleString("import sys");
PyRun_SimpleString(change_dir.c_str());
PyObject *pModule = PyImport_ImportModule("testFunction");
if (pModule == nullptr){
cout <<"[Error] Import module error" << endl;
return -1;
}
cout << "[INFO] Get Module" << endl;
PyObject *pFunction = PyObject_GetAttrString(pModule, "func");
if (pFunction == nullptr){
cout << "[Error] Import function error" << endl;
return -1;
}
cout << "[INFO] Get Function" << endl;
PyObject *args = PyTuple_New(1);
PyObject *args1 = PyUnicode_FromString("../air.jpg");
PyTuple_SetItem(args, 0, args1);
PyObject *pRet = PyObject_CallObject(pFunction, args);
int res = 999;
if (pRet){
PyArg_Parse(pRet,"i", &res);
cout << res << endl;
}
Py_DecRef(pModule);
Py_DecRef(pFunction);
Py_DecRef(args);
Py_DecRef(args1);
Py_DecRef(pRet);
Py_Finalize();
return 1;
}
代碼中值得注意的是有很多固定寫法:
Py_Initialize()
:Python的初始化
PyRun_SimpleString
:執行簡單的Python語句
Py_DecRef
: 釋放資源
Py_Finalize
: 結束Python
其中../scripts
是存放Python代碼的路徑,路徑是相對於c++ bin文件的路徑而言。
PyImport_ImportModule("testFunction");
調用了名爲 testFunction
的 py 程序。
PyObject *pFunction = PyObject_GetAttrString(pModule, "func2");
得到python程序中的 函數func
的PyObjet指針 。
PyObject *args = PyTuple_New(1);
PyObject *args1 = PyUnicode_FromString("../air.jpg");
PyTuple_SetItem(args, 0, args1);
這裏,是封裝函數參數,放到一個Py元組中。
PyObject *pRet = PyObject_CallObject(pFunction, args);
這裏是調用了剛纔 func
函數,並傳遞了參數,同時得到一個返回對象。程序利用了PyArg_Parse(pRet,"i", &res);
進行對 pRet 的 Py 對象進行解析,解析成 int 類型。由此得到正確的返回值。
可以看一下testFunction.py程序:
import numpy as np
import tensorflow as tf
import os
from PIL import Image
def func(pic):
modelDir = "/home/tyl/Code/Cprojects/TestDemo/TestCppPython/TestPython3/model"
sess = tf.Session()
saver = tf.train.import_meta_graph(os.path.join(modelDir,"model.ckpt.meta"))
saver.restore(sess, tf.train.latest_checkpoint(modelDir))
image = Image.open(pic)
imageArray = np.array(image)
print("圖像大小: ",imageArray.shape)
inputX = sess.graph.get_tensor_by_name('x:0')
inputY = sess.graph.get_tensor_by_name('y:0')
op = sess.graph.get_tensor_by_name('op_to_store:0')
add_on_op = tf.multiply(op,2)
ret = sess.run(add_on_op,{inputX:5,inputY:5})
sess.close()
print("TF模型計算得到: ", ret)
return 1
Python程序對傳遞進來的字符串(也就是圖像名)進行圖像讀取並顯示大小,同時調用了 TensorFlow
完成類一個簡單運算,最後打印結果,返回整型 1。
通過 C++ 調用,就能夠完成該Python函數的運行,同時得到返回值。當然這是最簡單的一個 C++ 調用 Python3 的例子。
2. 調用Python3中的類
一般面向對象,皆使用類,因此 C++ 調用 Python3 也需要對類的調用進行支持。
int testClass(){
Py_Initialize();
if(!Py_IsInitialized()){
cout << "[Error] Init error" << endl;
return -1;
}
string change_dir = "sys.path.append('../scripts')";
string model_dir = "../model";
PyRun_SimpleString("import sys");
PyRun_SimpleString(change_dir.c_str());
PyObject *pModule = PyImport_ImportModule("testClass");
if (pModule == nullptr){
cout <<"[Error] Import module error" << endl;
return -1;
}
cout << "[INFO] Get Module" << endl;
PyObject *pClass = PyObject_GetAttrString(pModule, "TestDemo");
if (pClass == nullptr){
cout << "[Error] Import class error" << endl;
return -1;
}
cout << "[INFO] Get Class" << endl;
PyObject *args1 = Py_BuildValue("(s)", model_dir.c_str());
PyObject *pInstance = PyObject_Call(pClass,args1, nullptr); //創建實例
assert(pInstance != nullptr);
PyObject *args2 = Py_BuildValue("(s)", "../air.jpg");
PyObject *pRet = PyObject_CallMethod(pInstance,"evaluate", "O", args2);
if (pRet != nullptr){
int res = 999;
PyArg_Parse(pRet,"i", &res);
cout << "成功返回參數: " << res << endl;
}
Py_DecRef(pModule);
Py_DecRef(pClass);
Py_DecRef(pInstance);
Py_DecRef(args1);
Py_DecRef(args2);
Py_DecRef(pRet);
Py_Finalize();
return 1;
}
有了上面調用函數的例子,調用類就變得簡單了。該程序主要是調用testClass.py 中的 TestDemo 類。 和調用函數不同的這兩句代碼:
PyObject *pInstance = PyObject_Call(pClass,args1, nullptr); //創建實例
PyObject *pRet = PyObject_CallMethod(pInstance,"evaluate", "O", args2);
其中PyObject_Call
是創建一個類的實例,同時可以傳遞 Python類中__init__
的參數,得到實例對象,通過 PyObject_CallMethod
調用類的成員方法 evaluate
, 同樣的可以傳遞參數。
其中testClass.py
代碼爲:
from PIL import Image
import numpy as np
import tensorflow as tf
import os
class TestDemo:
def __init__(self, model_dir):
self.sess = tf.Session()
saver = tf.train.import_meta_graph(os.path.join(model_dir,"model.ckpt.meta"))
saver.restore(self.sess, tf.train.latest_checkpoint(model_dir))
print("類初始化成功")
def evaluate(self, pic):
print("進入評估...")
image = Image.open(pic)
imageArray = np.array(image)
print("圖像大小: ", imageArray.shape)
inputX = self.sess.graph.get_tensor_by_name('x:0')
inputY = self.sess.graph.get_tensor_by_name('y:0')
op = self.sess.graph.get_tensor_by_name('op_to_store:0')
add_on_op = tf.multiply(op,2)
ret = self.sess.run(add_on_op,{inputX:5,inputY:5})
self.sess.close()
print("TF模型計算得到: ", ret)
return 1
實現的功能和之前的測試調用函數是一樣的功能,只不過封裝到類中。
3. 特殊返回對象處理(Map)
對於Python特殊的數據結構的返回, Python3 API也有相應的處理,使用最多的就是之前封裝傳參的PyTuple對象。另外還會有對列表進行處理的 PyList對象。而這裏記錄處理比較複雜的Map類型。
int testMap(){
Py_Initialize();
if(!Py_IsInitialized()){
cout << "[Error] Init error" << endl;
return -1;
}
string change_dir = "sys.path.append('../scripts')"; //路徑相對於c++ bin文件的路徑而言
PyRun_SimpleString("import sys");
PyRun_SimpleString(change_dir.c_str());
PyObject *pModule = PyImport_ImportModule("testFunction");
if (pModule == nullptr){
cout <<"[Error] Import module error" << endl;
return -1;
}
cout << "[INFO] Get Module" << endl;
PyObject *pFunction = PyObject_GetAttrString(pModule, "func2");
if (pFunction == nullptr){
cout << "[Error] Import function error" << endl;
return -1;
}
cout << "[INFO] Get Function" << endl;
PyObject *pDict = PyObject_CallObject(pFunction, nullptr);
PyObject *pKeys = PyDict_Keys(pDict);
for (Py_ssize_t i=0; i<PyDict_Size(pDict); i++){
PyObject *key = PyList_GetItem(pKeys, i);
string key_s = PyUnicode_AsUTF8(key);
cout << key_s << ": ";
PyObject *pValue = PyDict_GetItem(pDict, key);
for (Py_ssize_t j=0; j<PyList_Size(pValue); j++){
PyObject *v = PyList_GetItem(pValue, j);
if (PyLong_Check(v)){
long v_l = PyLong_AsLong(v);
cout << v_l << " " ;
}else if(PyFloat_Check(v)){
double v_d = PyFloat_AsDouble(v);
cout << v_d << " ";
}
}
cout << endl;
}
Py_DecRef(pModule);
Py_DecRef(pFunction);
Py_DecRef(pDict);
Py_DecRef(pKeys);
Py_Finalize();
return 1;
}
值得注意的是這句:
PyObject *pKeys = PyDict_Keys(pDict);
這是對返回對象進行了字典鍵值的提取,真正得到鍵內容還得繼續處理。 而代碼中 PyDict_Size(pDict)
得到字典的鍵值對數量。
PyObject *key = PyList_GetItem(pKeys, i);
pKeys
相當於得到所有的鍵,存成List類型,PyList_GetItem
從List中取出第 i 個元素,然後再解析這個對象,就拿到了第 i 個鍵。解析採用了 PyUnicode_AsUTF8
解析的是字符串。
PyObject *pValue = PyDict_GetItem(pDict, key);
這句代碼相當於從字典中 根據鍵拿到所謂的值,而值可以是列表形式,因此解析 pValue
時 具體情況具體分析了。上述代碼演示了值爲列表的解析過程。
Python代碼非常簡單,返回類一個字典,鍵爲字符串,值爲列表類型:
def func2():
m = {}
m["person"] = [1, 2, 3, 4]
m["chair"] = [5, 6, 7, 8]
return m
4. OpenCV的Mat圖像的傳遞
想要利用C++調用深度學習網絡推斷模型進行目標檢測或者進行圖像語義分割,若通過C++調用Python3的接口這種方式,那麼必定需要傳遞OpenCV的Mat圖像,通過Python 的 numpy 去承接圖像。
int testMat(){
Py_Initialize();
if(!Py_IsInitialized()){
cout << "[Error] Init error" << endl;
return -1;
}
string change_dir = "sys.path.append('../scripts')";
string model_dir = "../model";
PyRun_SimpleString("import sys");
PyRun_SimpleString(change_dir.c_str());
PyObject *pModule = PyImport_ImportModule("testMat");
if (pModule == nullptr){
cout <<"[Error] Import module error" << endl;
return -1;
}
cout << "[INFO] Get Module" << endl;
PyObject *pClass = PyObject_GetAttrString(pModule, "TestMat");
if (pClass == nullptr){
cout << "[Error] Import class error" << endl;
return -1;
}
cout << "[INFO] Get Class" << endl;
PyObject *args1 = Py_BuildValue("(s)", model_dir.c_str());
PyObject *pInstance = PyObject_Call(pClass,args1, nullptr); //創建實例
assert(pInstance != nullptr);
cv::Mat image = cv::imread("../air.jpg",CV_LOAD_IMAGE_UNCHANGED);
NumpyAPI::NDArrayConverter *cvt = new NumpyAPI::NDArrayConverter();
PyObject *pyImage = cvt->toNDArray(image.clone());
assert(pyImage != nullptr);
PyObject *pRetImage = PyObject_CallMethod(pInstance,
"evaluate",
"(O)",
pyImage);
if (pRetImage != nullptr){
cv::Mat retImage = cvt->toMat(pRetImage);
cv::imshow("image", retImage);
cv::waitKey();
}
Py_DecRef(pModule);
Py_DecRef(pClass);
Py_DecRef(pInstance);
Py_DecRef(args1);
Py_Finalize();
return 1;
代碼中進行了依次Mat轉換成PyObject的過程,然後在傳遞這個PyObject到類的成員方法。如下代碼所示:
cv::Mat image = cv::imread("../air.jpg",CV_LOAD_IMAGE_UNCHANGED);
NumpyAPI::NDArrayConverter *cvt = new NumpyAPI::NDArrayConverter();
PyObject *pyImage = cvt->toNDArray(image.clone());
Python代碼如下所示:
class TestMat:
def __init__(self, model_dir):
self.sess = tf.Session()
saver = tf.train.import_meta_graph(os.path.join(model_dir,"model.ckpt.meta"))
saver.restore(self.sess, tf.train.latest_checkpoint(model_dir))
print("類初始化成功")
def evaluate(self, image):
print("圖像大小: ", image.shape)
inputX = self.sess.graph.get_tensor_by_name('x:0')
inputY = self.sess.graph.get_tensor_by_name('y:0')
op = self.sess.graph.get_tensor_by_name('op_to_store:0')
add_on_op = tf.multiply(op,2)
ret = self.sess.run(add_on_op,{inputX:5,inputY:5})
self.sess.close()
print("TF模型計算得到: ", ret)
return image
這裏能夠成功傳遞圖像,但沒有利用網絡來對圖像處理。
tips: 具體的相關代碼能夠在這裏找到。