Skip to main content
 首页 » 编程设计

python之传递/返回 Cython Memoryviews 与 NumPy 数组

2025年05月04日13duanxz

我正在编写 Python 代码来加速二值图像中标记对象的区域属性函数。以下代码将在给定对象索引的情况下计算二值图像中标记对象的边界像素数。 main() 函数将循环遍历二进制图像“掩码”中的所有标记对象,并计算每个对象的边界像素数。

我想知道在此 Cython 代码中传递或返回我的变量的最佳方法是什么。这些变量要么在 NumPy 数组中,要么在类型化的内存 View 中。我搞砸了以不同格式传递/返回变量,但无法推断出最好/最有效的方法是什么。我是 Cython 的新手,所以 Memoryviews 对我来说仍然相当抽象,这两种方法之间是否存在差异仍然是个谜。我正在处理的图像包含 100,000 多个标记对象,因此此类操作需要相当高效。

总结:

对于非常重复的计算,我何时/应该将我的变量作为类型化的 Memoryviews 而不是 NumPy 数组传递/返回?有没有最好的方法或者它们完全相同?

%%cython --annotate 
 
import numpy as np 
import cython 
cimport numpy as np 
 
DTYPE = np.intp 
ctypedef np.intp_t DTYPE_t 
 
@cython.boundscheck(False) 
@cython.wraparound(False) 
def erode(DTYPE_t [:,:] img): 
 
    # Image dimensions 
    cdef int height, width, local_min 
    height = img.shape[0] 
    width = img.shape[1] 
 
    # Padded Array 
    padded_np = np.zeros((height+2, width+2), dtype = DTYPE) 
    cdef DTYPE_t[:,:] padded = padded_np 
    padded[1:height+1,1:width+1] = img 
 
    # Eroded image 
    eroded_np = np.zeros((height,width),dtype=DTYPE) 
    cdef DTYPE_t[:,:] eroded = eroded_np 
 
    cdef DTYPE_t i,j 
    for i in range(height): 
        for j in range(width): 
            local_min = min(padded[i+1,j+1], padded[i,j+1], padded[i+1,j],padded[i+1,j+2],padded[i+2,j+1]) 
            eroded[i,j] = local_min 
    return eroded_np 
 
 
@cython.boundscheck(False) 
@cython.wraparound(False) 
def border_image(slice_np): 
 
    # Memoryview of slice_np 
    cdef DTYPE_t [:,:] slice = slice_np 
 
    # Image dimensions 
    cdef Py_ssize_t ymax, xmax, y, x 
    ymax = slice.shape[0] 
    xmax = slice.shape[1] 
 
    # Erode image 
    eroded_image_np = erode(slice_np) 
    cdef DTYPE_t[:,:] eroded_image = eroded_image_np 
 
    # Border image 
    border_image_np = np.zeros((ymax,xmax),dtype=DTYPE) 
    cdef DTYPE_t[:,:] border_image = border_image_np 
    for y in range(ymax): 
        for x in range(xmax): 
            border_image[y,x] = slice[y,x]-eroded_image[y,x] 
    return border_image_np.sum() 
 
 
@cython.boundscheck(False) 
@cython.wraparound(False) 
def main(DTYPE_t[:,:] mask, int numobjects, Py_ssize_t[:,:] indices): 
 
    # Memoryview of boundary pixels 
    boundary_pixels_np = np.zeros(numobjects,dtype=DTYPE) 
    cdef DTYPE_t[:] boundary_pixels = boundary_pixels_np 
 
    # Loop through each object 
    cdef Py_ssize_t y_from, y_to, x_from, x_to, i 
    cdef DTYPE_t[:,:] slice 
    for i in range(numobjects): 
        y_from = indices[i,0] 
        y_to = indices[i,1] 
        x_from = indices[i,2] 
        x_to = indices[i,3] 
        slice = mask[y_from:y_to, x_from:x_to] 
        boundary_pixels[i] = border_image(slice) 
 
    return boundary_pixels_np 

请您参考如下方法:

Memoryviews 是 Cython 的新成员,与原来的 np.ndarray 相比有所改进。句法。出于这个原因,他们略微偏爱。不过,您使用的通常不会有太大区别。这里有一些注意事项:

速度

就速度而言,它非常差别不大 - 我的经验是作为函数参数的 memoryviews 稍微慢一些,但几乎不值得担心。

一般性

Memoryviews 旨在与任何具有 Python 缓冲区接口(interface)的类型一起工作(例如标准库 array 模块)。输入 np.ndarray仅适用于 numpy 数组。原则上,memorviews 可以支持偶数 wider range of memory layouts这可以使与 C 代码的接口(interface)更容易(实际上我从未真正看到它有用)。

作为返回值

当从 Cython 返回一个数组来编写 Python 代码时,用户可能更喜欢 numpy 数组而不是 memoryview。如果您正在使用内存 View ,您可以执行以下任一操作:

return np.asarray(mview) 
return mview.base 

易于编译

如果您使用 np.ndarray您必须使用 np.get_include() 设置包含目录在你的setup.py文件。您不必使用内存 View 执行此操作,这通常意味着您可以跳过 setup.py只需使用 cythonize命令行命令或 pyximport对于更简单的项目。

并行化

与 numpy 数组相比,这是内存 View 的大优势(如果您想使用它的话)。它不需要全局解释器锁来获取内存 View 的切片,但它需要一个 numpy 数组。这意味着以下代码大纲可以与内存 View 并行工作:

cdef void somefunc(double[:] x) nogil: 
     # implementation goes here 
 
cdef double[:,:] 2d_array = np.array(...) 
for i in prange(2d_array.shape[0]): 
    somefunc(2d_array[i,:]) 

如果您不使用 Cython 的并行功能,则这不适用。

cdef类

您可以将内存 View 用作 cdef 的属性类而不是np.ndarray秒。您(当然)可以将 numpy 数组用作无类型 object属性代替。