C++ 與 Python 的介面:Cython的初次使用要點總結
我在用機器學習/深度學習對點雲進行分類時,需要對原始點雲資料進行增強(Data Aumentation),但原始點雲資料為PCD檔案,我後續還要用PCL點雲庫(C++)進行特徵提取等操作,因此就想在C++中進行。資料增強的程式碼當然也可以用C++寫,但想學習用一下Cython介面就用了Python(當然Python寫起來也簡單==)。。。 這部分程式碼詳見我的這篇部落格: https://blog.csdn.net/shaozhenghan/article/details/81265817
但文章中並沒有很詳細地介紹,在C/C++中呼叫Cython編寫的函式時,怎樣向這個Python函式傳遞引數(C/C++ to Python),以及怎樣接受返回值放進C/C++ 變數中(Pyhton to C/C++)。
這裡主要想記錄一下過程和一些細節,遇到的坑。
首先將原先的 .py 改成 .pyx,除了檔名字尾外,在需要在C/C++中呼叫的即生成介面的函式那,把 def 改為 cdef, 後面再加上其public關鍵字,其他部分不用變。(原先的程式碼見:https://blog.csdn.net/shaozhenghan/article/details/81265817)
如下所示:
cdef public augment_data(point, rotation_angle, sigma, clip):
# -*- coding: utf-8 -*- ####################################### ########## Data Augmentation ########## ####################################### import numpy as np ########### # 繞Z軸旋轉 # ########### # point: vector(1*3) # rotation_angle: scaler 0~2*pi def rotate_point (point, rotation_angle): point = np.array(point) cos_theta = np.cos(rotation_angle) sin_theta = np.sin(rotation_angle) rotation_matrix = np.array([[cos_theta, sin_theta, 0], [-sin_theta, cos_theta, 0], [0, 0, 1]]) rotated_point = np.dot(point.reshape(-1, 3), rotation_matrix) return rotated_point ################### # 在XYZ上加高斯噪聲 # ################## def jitter_point(point, sigma=0.01, clip=0.05): assert(clip > 0) point = np.array(point) point = point.reshape(-1,3) Row, Col = point.shape jittered_point = np.clip(sigma * np.random.randn(Row, Col), -1*clip, clip) jittered_point += point return jittered_point ##################### # Data Augmentation # ##################### cdef public augment_data(point, rotation_angle, sigma, clip): return jitter_point(rotate_point(point, rotation_angle), sigma, clip).tolist()
注意我把最後一行加上了 .tolist() 後面再解釋。
然後在命令列中:$ cython name.pyx 自動生成 原始碼 name.c 和 介面name.h。 開啟name.c,可以看到,函式的形參與返回值都是 PyObject * 型別,即Python的動態型別特性:
__PYX_EXTERN_C PyObject *augment_data(PyObject *, PyObject *, PyObject *, PyObject *); /*proto*/
寫一個C++ 原始檔測試一下:輸入點座標(1,2,3),繞Z軸旋轉3.14即180度。正太分佈噪聲均值0,方差0.01,並且限制在+-0.05 之間。
#include <Python.h>
#include "data_aug.h"
#include <iostream>
int main(int argc, char const *argv[])
{
PyObject *point;
PyObject *angle;
PyObject *sigma;
PyObject *clip;
PyObject *augmented_point;
Py_Initialize();
initdata_aug();
// 浮點形資料必須寫為1.0, 2.0 這樣的,否則Py_BuildValue()精度損失導致嚴重錯誤
point = Py_BuildValue("[f,f,f]", 1.0, 2.0, 3.0);
angle = Py_BuildValue("f", 3.14);
sigma = Py_BuildValue("f", 0.01);
clip = Py_BuildValue("f", 0.05);
augmented_point = augment_data(point, angle, sigma, clip);
float x=0.0, y=0.0, z=0.0;
PyObject *pValue = PyList_GetItem(augmented_point, 0);
PyObject *pValue_0 = PyList_GET_ITEM(pValue, 0);
PyObject *pValue_1 = PyList_GET_ITEM(pValue, 1);
PyObject *pValue_2 = PyList_GET_ITEM(pValue, 2);
x = PyFloat_AsDouble(pValue_0);
y = PyFloat_AsDouble(pValue_1);
z = PyFloat_AsDouble(pValue_2);
std::cout << PyList_Size(pValue) << std::endl;
std::cout << x << "\n" << y << "\n" << z << std::endl;
Py_Finalize();
return 0;
}
注意:
必須有 下面三個語句:
Py_Initialize();
initdata_aug();
Py_Finalize();
CMakeLists.txt 這樣寫:
cmake_minimum_required(VERSION 2.8 FATAL_ERROR)
project(data_aug)
add_executable (data_aug test_data_aug.cpp data_aug.c)
執行結果:
-1.00187
-1.99964
3.01885
符合 (1,2,3)繞Z軸旋轉180度並加上微小噪聲的結果。
C++中向Python 函式傳遞引數,主要用到 Py_BuildValue() ,特別注意,當裡面的值是浮點數時,一定寫成1.0 而非1,否則會導致結果完全錯誤。如:
// 浮點形資料必須寫為1.0, 2.0 這樣的,否則Py_BuildValue()精度損失導致嚴重錯誤
point = Py_BuildValue("[f,f,f]", 1.0, 2.0, 3.0);
具體Py_BuildValue()的用法在下面的參考文獻裡。
C++ 接受Python 函式的返回值,主要用到 PyList_GetItem()以及 資料型別轉換 PyFloat_AsDouble 等。因為用到PyList_GetItem()所以我把pyx檔案中最後一行加上了 .tolist(),把numpy陣列變為list列表。
特別注意:Python函式返回的列表 augmented_point 為 [ [ x, y, z ] ],所以augmented_point的size為1!若用 PyObject *pValue = PyList_GetItem(augmented_point, 1); 則會發生記憶體洩漏!Segmentation error:段錯誤(核心已轉儲)
所以用下面的語句才依次提取出 x, y, z:
PyObject *pValue = PyList_GetItem(augmented_point, 0);
PyObject *pValue_0 = PyList_GET_ITEM(pValue, 0);
PyObject *pValue_1 = PyList_GET_ITEM(pValue, 1);
PyObject *pValue_2 = PyList_GET_ITEM(pValue, 2);
x = PyFloat_AsDouble(pValue_0);
y = PyFloat_AsDouble(pValue_1);
z = PyFloat_AsDouble(pValue_2);
另外:
Cython 中的函式形參以及返回值型別也可以使用靜態型別,Cython的靜態型別關鍵字!例如:
cdef public char great_function(const char * a,int index):
return a[index]
這樣的好處是,生成的C程式碼長這樣:
__PYX_EXTERN_C DL_IMPORT(char) great_function(char const *, int);
更加C風格,基本沒有Python的痕跡了。
這樣的侷限性是:當Cython的函式中使用的是列表List或者字典dict等Python獨有的型別時,就很難用C的型別關鍵字了。
所以個人認為使用Python動態型別 PyObject * ,結合Py_BuildValue() 更方便。
具體PyList_GetItem 和 PyFloat_AsDouble 的用法見下面參考文獻。
我遇到問題時找到了幾篇很好的參考文獻: