Python 3.x中项大小大于1的缓冲区赋值

2 投票
1 回答
1037 浏览
提问于 2025-04-15 19:29

我正在尝试通过Python 3.x的缓冲区接口来暴露一个图像像素信息的缓冲区(32位RGBA格式)。经过一番尝试,我终于搞定了,代码如下:

int Image_get_buffer(PyObject* self, Py_buffer* view, int flags)
{
    int img_len;
    void* img_bytes;

    // Do my image fetch magic
    get_image_pixel_data(self, &img_bytes, &img_len);

    // Let python fill my buffer
    PyBuffer_FillInfo(view, self, img_bytes, img_len, 0, flags); 
}

在Python中,我可以这样使用它:

mv = memoryview(image)
print(mv[0]) # prints b'\x00'
mv[0] = b'\xFF' # set the first pixels red component to full
mx[0:4] = b'\xFF\xFF\xFF\xFF' # set the first pixel to white

这工作得很好。不过,如果我能处理完整的像素值(整数,4字节),而不是单独的字节,那就更好了。所以我修改了缓冲区的获取方式,如下:

int Image_get_buffer(PyObject* self, Py_buffer* view, int flags)
{
    int img_len;
    void* img_bytes;

    // Do my image fetch magic
    get_image_pixel_data(self, &img_bytes, &img_len);

    // Fill my buffer manually (derived from the PyBuffer_FillInfo source)
    Py_INCREF(self);
    view->readonly = 0;
    view->obj = self;
    view->buf = img_bytes;
    view->itemsize = 4;
    view->ndim = 1;
    view->len = img_len;
    view->suboffsets = NULL;

    view->format = NULL;
    if ((flags & PyBUF_FORMAT) == PyBUF_FORMAT)
        view->format = "I";

    view->shape = NULL;
    if ((flags & PyBUF_ND) == PyBUF_ND)
    {
        Py_ssize_t shape[] = { (int)(img_len/4) };
        view->shape = shape;
    }

    view->strides = NULL;
    if((flags & PyBUF_STRIDED) == PyBUF_STRIDED)
    {
        Py_ssize_t strides[] = { 4 };
        view->strides = strides;
    }

    return 0;
}

这样确实能返回数据,我也能正确读取,但现在任何尝试给它赋值的操作都失败了!

mv = memoryview(image)
print(mv[0]) # prints b'\x00\x00\x00\x00'
mv[0] = 0xFFFFFFFF # ERROR (1)
mv[0] = b'\xFF\xFF\xFF\xFF' # ERROR! (2)
mv[0] = mv[0] # ERROR?!? (3)

在第一种情况下,错误提示我说 'int' 不支持缓冲区接口,这让我有点失望,也有点困惑(毕竟我确实指定了缓冲区格式是"I"),不过我可以处理这个问题。然而在第二和第三种情况下,事情就变得非常奇怪了:这两种情况都给我返回了一个类型错误,提示 “my.Image”和“bytes”的项目大小不匹配(其中 my.Image 显然是我的图像类型)。

这让我很困惑,因为我传入的数据显然和我从那个元素中得到的数据大小是一样的。看起来如果项目大小大于1,缓冲区就不再允许赋值了。当然,这个接口的文档非常稀少,翻阅Python代码也没有找到什么使用示例,所以我有点无从下手。我是不是漏掉了什么文档片段,说明“当项目大小大于1时,缓冲区基本上就没用了”,还是我做错了什么看不出来,或者这就是Python的一个bug?(我测试的是3.1.1版本)

谢谢你能给我提供的任何见解(虽然这是个比较高级的问题)!

1 个回答

1

我在Python代码中(在Objects文件夹的memoryobject.c里)发现了这个,具体是在memory_ass_sub这个函数里:

/* XXX should we allow assignment of different item sizes
   as long as the byte length is the same?
   (e.g. assign 2 shorts to a 4-byte slice) */
if (srcview.itemsize != view->itemsize) {
    PyErr_Format(PyExc_TypeError,
        "mismatching item sizes for \"%.200s\" and \"%.200s\"", 
        view->obj->ob_type->tp_name, srcview.obj->ob_type->tp_name);
    goto _error;
}

这就是后面两个错误的来源。看起来即使是mv[0]的itemsize也不等于它自己。

更新

我觉得事情是这样的。当你试图在mv中赋值时,它会调用Objects/memoryobject.c里的memory_ass_sub函数,但这个函数只接受一个PyObject作为输入。这个对象会通过PyObject_GetBuffer函数被转变成一个缓冲区,尽管在mv[0]的情况下,它已经是一个缓冲区了(而且正是你想要的那个缓冲区!)。我猜这个函数会把对象变成一个简单的itemsize=1的缓冲区,不管它之前是不是缓冲区。这就是为什么即使是

mv[0] = mv[0]

第一个赋值的问题,

mv[0] = 0xFFFFFFFF

我认为是因为它在检查这个整数是否可以作为缓冲区使用,而目前来看,它并没有被设置为可以使用。

换句话说,缓冲区系统目前无法处理item size大于1的情况。看起来离解决不远,但你可能还需要做一些额外的工作。如果你能搞定,应该把这些改动提交回主Python版本。

另一个更新

你第一次尝试给mv[0]赋值时出现的错误代码,是因为在调用PyObject_CheckBuffer时,整数没有通过检查。显然,系统只处理可以缓冲的对象的副本。这似乎也应该改进。

结论

目前Python的缓冲区系统无法处理item size大于1的情况,正如你猜测的那样。此外,它也无法从不可缓冲的对象(比如整数)中进行赋值。

撰写回答