C++調用Python3詳細記錄

通過在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: 具體的相關代碼能夠在這裏找到。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章