在Cython中返回一个结构体数组

6

我正在尝试在Cython中返回一个结构体数组。

// .pyx

from libc.stdint cimport uint8_t

cdef extern from "<apriltag.h>":
    cdef struct apriltag_detection:
        int id
        double c[2]
        double p[4][2]

    ctypedef apriltag_detection apriltag_detection_t

cdef extern from "tag36h11_detector/tag36h11_detector.h":
    apriltag_detection_t* scan_frame(int width, int height, uint8_t* data);

cdef class Detection:
    # how do I "link" this to the struct defined above?
    def __cinit__(self):
        pass
    def __dealloc__(self):
        pass

def detect(width, height, frame):
    return scan_frame(width, height, frame)

理想情况下,我希望能在Python代码中调用detect函数,并获得一个Detection对象列表,其中Detection是对C结构体apriltag_detection的包装类,该结构体被typedef为apriltag_detection_t
我遇到了以下编译错误:

tag36h11_detector.pyx:22:21: 无法将'apriltag_detection_t *'转换为Python对象

我在文档中找不到有关返回结构体指针或结构体数组的参考信息。 更新3:
// .h
typedef struct detection_payload {
    int size;
    apriltag_detection_t** detections;
} detection_payload_t;

我正在尝试将上述结构体转换为一个Python对象,其中包含size和一个Python列表,该列表包含apriltag_detection_t对象。
// .pyx

cdef extern from "<apriltag.h>":
    cdef struct apriltag_detection:
        int id
        double c[2]
        double p[4][2]

    ctypedef apriltag_detection apriltag_detection_t

cdef extern from "tag36h11_detector/tag36h11_detector.h":
    cdef struct detection_payload:
        int size
        apriltag_detection_t** detections
    ctypedef detection_payload detection_payload_t
    detection_payload* scan_frame(int width, int height, uint8_t* data)

...


cdef class Detection:
    cdef apriltag_detection* _d

    def __cinit__(self):
        self._d = NULL
    cdef _setup(self, apriltag_detection* d):
        self._d = d
    def __dealloc__(self):
        self._d = NULL
    property id:
        def __get__(self):
            return self._d.id
    property c:
        def __get__(self):
            return self._d.c
    property p:
        def __get__(self):
            return self._d.p

cdef Detection_create(apriltag_detection_t* d):
    return Detection()._setup(d)

cdef class DetectionPayload:
    cdef detection_payload* _p

    def __cinit__(self):
        self._p = NULL
    cdef _setup(self, detection_payload* p):
        self._p = p
        self.size = p.size
        self.detections = []
        for i in range(0, self.size):
            apriltag_detection_t* detection = self._p.detections[i]
            d = Detection_create(detection)
            self.detections+=[d]
    def __dealloc__(self):
        _p = NULL
    property size:
        def __get__(self):
            return self.size
    property detections:
        def __get__(self):
            return self.detections

我在这一行遇到了几个语法错误:

apriltag_detection_t* detection = self._p.detections[I]

具体来说,关于指针apriltag_detection_t*

更新2

现在这个编译并导入没问题了。数组方面还没有进展。

from libc.stdint cimport uint8_t

cdef extern from "<apriltag.h>":
    cdef struct apriltag_detection:
        int id
        double c[2]
        double p[4][2]

    ctypedef apriltag_detection apriltag_detection_t

cdef extern from "tag36h11_detector/tag36h11_detector.h":
    apriltag_detection_t* scan_frame(int width, int height, uint8_t* data);

cdef class Detection:
    cdef apriltag_detection* _d

    def __cinit__(self):
        self._d = NULL
    cdef _setup(self, apriltag_detection* d):
        self._d = d
    def __dealloc__(self):
        self._d = NULL
    property id:
        def __get__(self):
            return self._d.id
    property c:
        def __get__(self):
            return self._d.c
    property p:
        def __get__(self):
            return self._d.p

cdef Detection_create(apriltag_detection_t* d):
    return Detection()._setup(d)

def detect(width, height, frame):
    cdef apriltag_detection_t* detection = scan_frame(width, height, frame)
    return Detection_create(detection)

更新1

我尝试按照下面链接的帖子进行操作,目前我已经得到了以下结果。

from libc.stdint cimport uint8_t

cdef extern from "<apriltag.h>":
    cdef struct apriltag_detection:
        int id
        double c[2]
        double p[4][2]

    ctypedef apriltag_detection apriltag_detection_t

cdef extern from "tag36h11_detector/tag36h11_detector.h":
    apriltag_detection_t* scan_frame(int width, int height, uint8_t* data);

cdef class Detection:
    cdef apriltag_detection* _d;
    def __cinit__(self):
        self._d = NULL
    def _setup(self, apriltag_detection* d):
        self._d = d
    def __dealloc__(self):
        self._d = NULL
    property id:
        def __get__(self):
            return self._t.id
    property c:
        def __get__(self):
            return self._t.c
    property p:
        def __get__(self):
            return self._t.p

cdef Detection_create(apriltag_detection_t* d):
    return Detection()._setup(d)

def detect(width, height, frame):
    return <Detection>scan_frame(width, height, frame)

虽然这比之前更接近了,但我仍然遇到错误:

tag36h11_detector.pyx:33:30: 无法将'apriltag_detection_t *'转换为Python对象

出错的代码行为:

cdef Detection_create(apriltag_detection_t* d):
    return Detection()._setup(d)

此外,我不知道如何返回Python列表...

虽然这不是解决您当前问题的方法,但可以帮助您避免下一个问题 ;) 因为“frame”不是指针。http://cython.readthedocs.io/en/latest/src/tutorial/array.html - ead
顺便问一下,如果 scan_frame 返回一个数组,你怎么知道数组中有多少个标签? - ead
啊..我不知道!也许我应该返回一个包含大小并指向apriltag_detection_t数组的结构体? - Carpetfizz
那将是解决这个问题的一种简洁的方式! - ead
@DavidW,这就解决了问题,谢谢! - Carpetfizz
显示剩余8条评论
1个回答

5

看起来您已经解决了问题,但我仍然想回答这个问题。首先,因为我想尝试一下,其次是因为我认为您在内存管理方面存在一些问题,我想指出。

我们将包装以下简单的C接口:

//creator.h
typedef struct {
   int mult;
   int add;
} Result;

typedef struct {
   int size;
   Result *arr;
} ResultArray;

ResultArray create(int size, int *input){
   //whole file at the end of the answer
}

该函数处理输入数组并返回一个C结构体数组,同时返回该数组中元素的数量。

我们的包装pyx文件如下:

#result_import.pyx (verion 0)
cdef extern from "creator.h":
     ctypedef struct Result:
          int mult
          int add
     ctypedef struct ResultArray:
          int size
          Result *arr
     ResultArray create(int size, int *input)

def create_structs(int[::1] input_vals):
    pass

最值得注意的部分:我使用了memoryview(int[::1])来传递输入数组,这有两个优点:
  1. 在 Python 端可以使用任何支持 memory view 的东西(numpy,自 Python3 起的 array),这比使用 numpy 数组更灵活。
  2. 通过 [::1] 确保输入是连续的。
在测试脚本中,我使用了numpy,但也可以使用内置数组。
#test.py
import result_import
import numpy as np

a=np.array([1,2,3],dtype='int32')
result=result_import.create_structs(a)
for i,el in enumerate(result):
    print  i, ": mult:", el.mult, " add:", el.add

现在还没有任何功能,但是所有的设置都已经完成。

第一个场景:我们只想要普通的Python对象,不需要太花哨!一种可能的方式是:

#result_import.pyx (verion 1)
#from cpython cimport array needed for array.array in Python2
from libc.stdlib cimport free
....
class PyResult:
    def __init__(self, mult, add):
       self.mult=mult
       self.add=add


def create_structs(int[::1] input_vals):
    cdef ResultArray res=create(len(input_vals), &input_vals[0])
    try:
        lst=[]
        for i in range(res.size):
            lst.append(PyResult(res.arr[i].mult, res.arr[i].add))
    finally:
        free(res.arr)
    return lst  

我将整个数据转换为Python对象,使用一个简单的列表。非常简单,但有两件值得注意的事情:
  1. 内存管理:我负责释放在res.arr中分配的内存。因此,我使用try...finally确保即使抛出异常也会发生。
  2. 仅将此指针设置为NULL是不够的,我必须调用free函数。
现在我们的test.py可以工作了-很好!
第二种情况:如果我只需要其中一些元素并将它们全部转换,则效率低下。此外,我在内存中保留所有元素两次(至少在某些时间内)-这是朴素方法的缺点。因此,我希望在程序的其他地方按需创建PyResult对象。
让我们编写一个包装器列表:
#result_import.pyx (verion 2)
...
cdef class WrappingList:
    cdef int size
    cdef Result *arr

    def __cinit__(self):
        self.size=0
        self.arr=NULL

    def __dealloc__(self):
        free(self.arr)
        print "deallocated"#just a check

    def __getitem__(self, index):
        if index<0 or index>=self.size:
            raise IndexError("list index out of range")
        return PyResult(self.arr[index].mult, self.arr[index].add)


def create_structs(int[::1] input_vals):
    cdef ResultArray res=create(len(input_vals), &input_vals[0])
    lst=WrappingList()
    lst.size, lst.arr=res.size, res.arr
    return lst 

所以,WrappingList类的行为很像一个列表,它保留整个C数组而不进行复制,并且仅在需要时创建PyResult对象。值得一提的是:
  1. __dealloc__在销毁WrapperingList对象时被调用——这是我们释放由C代码给出的内存的地方。在test.py的结尾处,我们应该看到“已释放”,否则就出了问题...
  2. __getitem__用于迭代。

第三种情况:Python代码不仅应该读取结果,还应该更改结果,以便将更改后的数据传回C代码。为此,让我们将PyResult作为代理:

#result_import.pyx (verion 3, last)
...
cdef class PyResult:
    cdef Result *ptr #ptr to my element
    def __init__(self):
       self.ptr=NULL

    @property
    def mult(self):
        return self.ptr.mult

    @mult.setter
    def mult(self, value):
        self.ptr.mult = value

    @property
    def add(self):
        return self.ptr.add

    @add.setter
    def add(self, value):
        self.ptr.add = value


cdef class WrappingList:  
    ...
    def __getitem__(self, index):
        if index>=self.size:
            raise IndexError("list index out of range")
        res=PyResult()
        res.ptr=&self.arr[index]
        return res

现在,PyResult 对象拥有指向相应元素的指针,并且可以直接在 C 数组中更改它。但是,我需要提醒一下一些潜在的问题:
  1. 这有点不安全: PyResult 不应该比父WrappingList 对象存在的时间更长。您可以通过在 PyResult 类中添加对父对象的引用来解决此问题。
  2. 访问元素(addmult)的成本相当高,因为每次都必须创建、注册然后删除一个新的 Python 对象。
让我们更改测试脚本,看看代理对象的实际效果:
#test.py(second version)
import result_import
import numpy as np

a=np.array([1,2,3],dtype='int32')
result=result_import.create_structs(a)
for i,el in enumerate(result):
    print  i, ": mult:", el.mult, " add:", el.add
    el.mult, el.add=42*i,21*i

# now print changed values:
for i,el in enumerate(result):
    print  i, ": mult:", el.mult, " add:", el.add

还有很多需要改进的地方,但我想这个回答已经足够了:)


附件:

糟糕的creator.h - 需要检查malloc的结果:

//creator.h
typedef struct {
   int mult;
   int add;
} Result;

typedef struct {
   int size;
   Result *arr;
} ResultArray;

ResultArray create(int size, int *input){
   ResultArray res;
   res.size=size;
   res.arr=(Result *)malloc(size*sizeof(Result));//todo: check !=0
   for(int i=0;i<size;i++){
       res.arr[i].mult=2*input[i];
       res.arr[i].add=2+input[i]; 
   }
   return res;
}

setup.py:

from distutils.core import setup, Extension
from Cython.Build import cythonize

setup(ext_modules=cythonize(Extension(
            name='result_import',
            sources = ["result_import.pyx"]
    )))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接