1. 程式人生 > >C++ 與 Python 的介面:Cython的初次使用要點總結

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 的用法見下面參考文獻。

我遇到問題時找到了幾篇很好的參考文獻