C++ 与 Python 的接口:Cython的初次使用要点总结

我在用机器学习/深度学习对点云进行分类时,需要对原始点云数据进行增强(Data Aumentation),但原始点云数据为PCD文件,我后续还要用PCL点云库(C++)进行特征提取等操作,因此就想在C++中进行。数据增强的代码当然也可以用C++写,但想学习用一下Cython接口就用了Python(当然Python写起来也简单==)。。。 这部分代码详见我的这篇博客: https://blog.csdn.net/shaozhenghan/article/details/81265817

知乎有一篇文章,文章简要介绍了怎样用Cython编写函数并生成 *.c  和 *.h,然后用C/C++调用Cython中用Python语法写的函数:

https://www.zhihu.com/question/23003213

但文章中并没有很详细地介绍,在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 的用法见下面参考文献。

我遇到问题时找到了几篇很好的参考文献

https://www.ibm.com/developerworks/cn/linux/l-pythc/

https://www.cnblogs.com/DxSoft/archive/2011/04/01/2002676.html

https://blog.csdn.net/vampirem/article/details/12948955

https://www.zhihu.com/question/23003213

猜你喜欢

转载自blog.csdn.net/shaozhenghan/article/details/81264124