1. 程式人生 > >C_C++呼叫Python [opencv與numpy]

C_C++呼叫Python [opencv與numpy]

C/C++呼叫Python [opencv與numpy]

目前的情況下,如果你有一個深度學習模型,很想在專案中使用,但模型是用python寫的,專案使用的是C++,怎麼辦?直觀的做法是從C++呼叫python直譯器,本文遇到的情景是C++環境下有張圖片,需要將其中一個區域(ROI)進行放大(超解析度重建),放大演算法是python環境下的函式(pytorch模型),之後在C++環境下進行後續處理,假設希望從C/C++端呼叫的python函式如下(暫不介紹超解析度,用opencv的resize替代):
import cv2 as cv
def super_resolution(img, scale=
4): height, width = img.shape[:2] dsize = (width*scale, height*scale) big_img = cv.resize(img, dsize) return big_img

先介紹環境配置,再講從C/C++呼叫Python的關鍵操作。

1. 環境設定

以windows環境為例,開發時需要做好相關配置,我的環境:Windows10,VS2017 Community,Python3.6.4_x64,OpenCV3.4.1_x64。

OpenCV環境

官方文件

  1. Visual Studio配置包含目錄(編譯錯),D:\Program Files\opencv3\build\include
  2. Visual Studio配置庫目錄(連結錯),D:\Program Files\opencv3\build\x64\vc15\lib
  3. Visual Studio配置連結器輸入(連結錯):opencv_world341.lib
  4. 追加Path環境變數(執行錯):Path=Path;D:\Program Files\opencv3\build\x64\vc15\bin,改完環境變數一定要重啟Visual Studio才能生效。

下面的例子讀取一張圖片並顯示。

//opencv_demo.cpp
#include<opencv/cv.hpp>
using namespace cv;

int
main(int argc, char *argv[]){ Mat img = imread("lena.jpg"); imshow("lena", img); waitKey(0); destroyAllWindows(); return 0; }

Python環境

  1. Visual Studio配置包含目錄(編譯錯):D:\Program Files\Python36\include
  2. Visual Studio配置庫目錄(連結錯):D:\Program Files\Python36\libs
  3. 新增環境變數(執行錯):PYTHONHOME=D:\Program Files\Python36,改完環境變數一定要重啟Visual Studio才能生效。

下面的例子從C呼叫Python直譯器,並執行Python程式碼,列印時間和日期。

//python_demo.cpp
// https://docs.python.org/3.6/extending/embedding.html#very-high-level-embedding
#include <Python.h> 

int main(int argc, char *argv[])
{
    wchar_t *program = Py_DecodeLocale(argv[0], NULL);
    if (program == NULL) {
        fprintf(stderr, "Fatal error: cannot decode argv[0]\n");
        exit(1);
    }
    Py_SetProgramName(program);  /* optional but recommended */
    Py_Initialize();
    PyRun_SimpleString("from time import time,ctime\n"
                       "print('Today is', ctime(time()))\n");
    if (Py_FinalizeEx() < 0) {
        exit(120);
    }
    PyMem_RawFree(program);
    getchar();
    return 0;
}

Numpy環境

  1. Visual Studio標頭檔案目錄(編譯錯):D:\Program Files\Python36\Lib\site-packages\numpy\core\include
  2. 關鍵程式碼(執行錯):在Py_Initialize();之後必須呼叫import_array();以載入所有numpy函式(C API),與載入dll類似。

下面的例子展示用numpy介面實現矩陣計算矩陣乘法,並驗證結果。

// numpy_demo.cpp 
#include <Python.h> 
#include <iostream>
#include <numpy/arrayobject.h>
using namespace std;

int main(int argc, char *argv[])
{
	wchar_t *program = Py_DecodeLocale(argv[0], NULL);
	if (program == NULL) {
		fprintf(stderr, "Fatal error: cannot decode argv[0]\n");
		exit(1);
	}
	Py_SetProgramName(program);  /* optional but recommended */
	Py_Initialize();
	
	import_array();		/* load numpy api */
	double array_1[2][3] = { { 2,5,6 },{ 5,6,5 } };
	npy_intp dims_1[] = { 2, 3 };
	PyObject *mat_1 = PyArray_SimpleNewFromData(2, dims_1, NPY_DOUBLE, array_1);

	double array_2[3][4] = { { 1,3,0,4 },{ 2,2,5,3 },{ 1,2,1,4 } };
	npy_intp dims_2[] = { 3, 4 };
	PyObject *mat_2 = PyArray_SimpleNewFromData(2, dims_2, NPY_DOUBLE, array_2);

	PyObject *prod = PyArray_MatrixProduct(mat_1, mat_2);

	PyArrayObject *mat_3;
	PyArray_OutputConverter(prod, &mat_3);
	npy_intp *shape = PyArray_SHAPE(mat_3);
	double *array_3 = (double*)PyArray_DATA(mat_3);

	cout << "numpy result:\n";
	for (int i = 0; i < shape[0]; i++) {
		for (int j = 0; j < shape[1]; j++) {
			cout << array_3[i*shape[1] + j] << "\t";
		}
		cout << endl;
	}
	cout << "\nC result:\n";
	for (int i = 0; i < 2; i++) {
		for (int j = 0; j < 4; j++) {
			double t = 0;
			for (int k = 0; k < 3; k++)
				t += array_1[i][k] * array_2[k][j];
			cout << t << "\t";
		}
		cout << endl;
	}

	if (Py_FinalizeEx() < 0) {
		exit(120);
	}
	PyMem_RawFree(program);
	getchar();
	return 0;
}

2. C與Python的型別轉換

儘管處於不同的目的,但 擴充套件Python (Python調C,加速)和 內嵌Python (C調Python,方便)是完全一樣的行為。

擴充套件Python要做的是:

  • 將資料從Python轉換到C
  • 使用轉換的值執行C函式的呼叫
  • 將資料從C轉換到Python

當內嵌Python時,介面程式碼要做的是:

  • 將資料從C轉換到Python
  • 使用轉換的值執行Python介面函式的呼叫
  • 將呼叫返回的資料從Python轉換到C

不管是擴充套件還是內嵌,程式碼都是C語言的,而這裡的核心就是資料型別轉換,C語言中,有bool,char,short,int,long,float,double 陣列和指標等型別,在Python中有bool,int,float,str,list,tuple,setdict等型別,但Python的一切型別在C語言中皆為物件,也就是PyObject型別,所有Python型別都繼承它,關於型別轉換直接參考文件,下面簡要介紹一下。

基本型別轉換Py_BuildValuePyArg_Parse

簡單的型別轉換例如:

// PyObject *Py_BuildValue(const char *format, ...);
// int PyArg_Parse(PyObject *args, const char *format, ...);

bool c_b;
PyObject *py_b = Py_BuildValue("b", true);		/*C -> Python*/
PyArg_Parse(py_b, "b", &c_b);					/*python -> C*/

int c_i;
PyObject *py_i = Py_BuildValue("i", 42);
PyArg_Parse(py_i, "i", &c_i);

double c_d;
PyObject *py_d = Py_BuildValue("d", 3.141592654);
PyArg_Parse(py_d, "d", &c_d);
	
const char *c_str;
PyObject *py_str = Py_BuildValue("u", "你好,世界!");
PyArg_Parse(py_str, "u", &c_str);

構造複雜的Python物件Py_BuildValue

以上都是基本型別的轉換,另外,C呼叫Python函式時,如何構造高階資料結構如list,tuple,setdict呢?Py_BuildValue可以做到,演示如下,右邊是PyObject*中包含的Python資料:

#define PY_SSIZE_T_CLEAN  /* Make "s#" use Py_ssize_t rather than int. */
#include <Python.h>

Py_BuildValue("")                        None
Py_BuildValue("i", 123)                  123
Py_BuildValue("iii", 123, 456, 789)      (123, 456, 789)
Py_BuildValue("s", "hello")              'hello'
Py_BuildValue("y", "hello")              b'hello'
Py_BuildValue("ss", "hello", "world")    ('hello', 'world')
Py_BuildValue("s#", "hello", 4)          'hell'
Py_BuildValue("y#", "hello", 4)          b'hell'
Py_BuildValue("()")                      ()
Py_BuildValue("(i)", 123)                (123,)
Py_BuildValue("(ii)", 123, 456)          (123, 456)
Py_BuildValue("(i,i)", 123, 456)         (123, 456)
Py_BuildValue("[i,i]", 123, 456)         [123, 456]
Py_BuildValue("{s,s}", "abc", "def")     {'abc', 'def'}
Py_BuildValue("{s:i,s:i}",
              "abc", 123, "def", 456)    {'abc': 123, 'def': 456}
Py_BuildValue("((ii)(ii)) (ii)",
              1, 2, 3, 4, 5, 6)          (((1, 2), (3, 4)), (5, 6))

解析引數中的Python物件PyArg_ParseTuple

反過來,在Python呼叫C函式時,會傳遞一系列引數,將這些引數解析為C中的物件,就要用到PyArg_ParseTuplePyArg_ParseTupleAndKeywords,首先,一個標準的C擴充套件函式是這樣的:

static PyObject *
demo_func(PyObject *self, PyObject *args)
{
    int arg1;
    double arg2;
    const char *arg3;
    if (!PyArg_ParseTuple(args, "ids", &arg1, &arg2, &arg3))
        return NULL;
    /* based on the args, do something here */
    Py_RETURN_NONE;
}

其中self引數對於模組級別函式來說表示模組物件,如果是類方法則表示類物件。args引數包含了呼叫該擴充套件的引數列表,用PyArg_ParseTuple可以將其解析,更復雜的例子如下:

#define PY_SSIZE_T_CLEAN  /* Make "s#" use Py_ssize_t rather than int. */
#include <Python.h>

/* ======簡單的引數解析====== */
int ok;
int i, j;
long k, l;
const char *s;
Py_ssize_t size;

ok = PyArg_ParseTuple(args, ""); /* No arguments */
    /* Python call: f() */
ok = PyArg_ParseTuple(args, "s", &s); /* A string */
    /* Possible Python call: f('whoops!') */
ok = PyArg_ParseTuple(args, "lls", &k, &l, &s); /* Two longs and a string */
    /* Possible Python call: f(1, 2, 'three') */
ok = PyArg_ParseTuple(args, "(ii)s#", &i, &j, &s, &size);
    /* A pair of ints and a string, whose size is also returned */
    /* Possible Python call: f((1, 2), 'three') */

/* ======具有預設值的可選引數====== */
const char *file;
const char *mode = "r";
int bufsize = 0;
ok = PyArg_ParseTuple(args, "s|si", &file, &mode, &bufsize);
/* A string, and optionally another string and an integer */
/* Possible Python calls:
   f('spam')
   f('spam', 'w')
   f('spam', 'wb', 100000) */


/* ======引數含有巢狀的tuple====== */
int left, top, right, bottom, h, v;
ok = PyArg_ParseTuple(args, "((ii)(ii))(ii)",
                      &left, &top, &right, &bottom, &h, &v);
/* A rectangle and a point */
/* Possible Python call:
   f(((0, 0), (400, 300)), (10, 10)) */

/* ======複數,並且一個函式名,解析失敗時,在error message中
         會顯示出錯的函式名為myfunction====== */
Py_complex c;
ok = PyArg_ParseTuple(args, "D:myfunction", &c);
/* a complex, also providing a function name for errors */
/* Possible Python call: myfunction(1+2j) */

解析命名引數中的Python物件PyArg_ParseTupleAndKeywords

Python也支援命名引數,帶名稱的情況下可以改變引數的順序,定義一個這樣的擴充套件函式,就要用到PyArg_ParseTupleAndKeywords解析引數序列,定義一個函式如下,它包含一個必填的引數和三個可選的引數。

static PyObject *
keywdarg_parrot(PyObject *self, PyObject *args, PyObject *keywds)
{
    int voltage;
    const char *state = "a stiff";
    const char *action = "voom";
    const char *type = "Norwegian Blue";

    static char *kwlist[] = {"voltage", "state", "action", "type", NULL};

    if (!PyArg_ParseTupleAndKeywords(args, keywds, "i|sss", kwlist,
                                     &voltage, &state, &action, &type))
        return NULL;
    /* based on the args, do something here */
    Py_RETURN_NONE;
}

題外——const的使用

在學習PyArg_Parse函式時踩了一個坑,本來打算用char陣列接受解析的資料,結果失敗,原因是定義好的char陣列的指標不可改變,程式碼如下

char c_str[256];
PyObject *py_str = Py_BuildValue("u", "你好,世界!");
PyArg_Parse(py_str, "u", c_str);
cout << c_str << endl;

正確的用法如上文,需要定義一個可以改變地址的指標,直接指向返回的字串,這裡將經常遺忘的c語言中const用法梳理:

const char d1 = 'a';		/* 定義常量 */
char const d2 = 'a';		/* char const和const char是一樣的 */
const char *p1 = "hello";	/* p1所指的內容不可改 */
p1 = "world";				/* 但p1可以指向不同的地址 */
char const *p2 = "hello";	/* char const *和const char *是一樣的 */
char p3[256]="hello";		/* p3指向的地址固定,但內容可通過p3更改 */
p3[0] = 'w';				/* wello */
char *const p4 = p3;		/* p4指向的地址固定,但內容可通過p4更改*/
p4[0] = 'b';				/* bello */
const char *const p5 = p4;	/* p5指向的地址固定,且內容不可通過p5更改*/
/* 綜上:
1. char const和const char是一樣的,一般使用const char
2. const char定義不更改的內容,常用於定義常量以及保護形參的內容不被更改
3. *const定義不可更改的指標,少見
*/

3. C呼叫Python

簡單的例子

如何從C語言呼叫一個Python函式,

第一步,寫一個Python檔案(simple_module.py),包含一個函式(simple_func),功能為求和,放入PYTHONHOME/Lib/site-packages中,檔案內容如下:

def simple_func(a,b):return a+b

第二步,寫一個C檔案,執行【Python直譯器初始化、匯入模組,匯入函式,構造輸入引數,呼叫函式,解析返回值,終止Python直譯器】,檔案內容如下(省略錯誤處理):

#include <Python.h> 

int main(int argc, char *argv[])
{
	wchar_t *program = Py_DecodeLocale(argv[0], NULL);
	if (program == NULL) {
		fprintf(stderr, "Fatal error: cannot decode argv[0]\n");
		exit(1);
	}
	Py_SetProgramName(program);  /* optional but recommended */
	Py_Initialize();

	PyObject *pName = PyUnicode_DecodeFSDefault("simple_module");
	PyObject *pModule = PyImport_Import(pName);		/* 匯入模組 */
	Py_DECREF(pName);
	PyObject *pFunc = PyObject_GetAttrString(pModule, "simple_func");/* 匯入函式 */
	Py_DECREF(pModule);
	PyObject *pArgs = PyTuple_New(2);		/* 初始化輸入引數列表,長度為2 */
	PyTuple_SetItem(pArgs, 0, Py_BuildValue("i",100));	/* 設定引數 */
	PyTuple_SetItem(pArgs, 1, Py_BuildValue("i",20));	/* 設定引數 */
	PyObject *pRetValue = PyObject_CallObject(pFunc, pArgs);	/* 呼叫 */
	Py_DECREF(pFunc);
	Py_DECREF(pArgs);
	int ret;
	PyArg_Parse(pRetValue, "i", &ret);
	Py_DECREF(pRetValue);
	printf("return value: %d", ret);

	if (Py_FinalizeEx() < 0) {
		exit(120);
	}
	PyMem_RawFree(program);
	getchar();
	return 0;
}

引用計數

上面的例子中多處呼叫Py_DECREF,這是對當前物件的引用計數執行減一,