views:

80

answers:

1

I am trying to expose a buffer of image pixel information (32 bit RGBA) through the Python 3.x buffer interface. After quite a bit of playing around, I was able to get this working like so:

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); 
}

And in python I can play with it like so:

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

And that works splendidly. However, it would be great if I could work with the full pixel value (int, 4 byte) instead of individual bytes, so I modified the buffer fetch like so:

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;
}

This actually returns the data and I can read it correctly, but any attempt to assign a value into it now fails!

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)

In case 1 the error informs me that 'int' does not support the buffer interface, which is a shame and a bit confusing (I did specify that the buffer format was "I" after all), but I can deal with that. In case 2 and 3 things get really weird, though: Both cases gime me an TypeError reading mismatching item sizes for "my.Image" and "bytes" (Where my.Image is, obviously, my image type)

This is very confusing to me, since the data I'm passing in is obviously the same size as what I get out of that element. It seems as though buffers simply stop allowing assignment if the itemsize is greater than 1. Of course, the documentation for this interface is really sparse and perusing through the python code doesn't really give any usage examples so I'm fairly stuck. Am I missing some snippit of documentation that states "buffers become essentially useless when itemsize > 1", am I doing something wrong that I can't see, or is this a bug in Python? (Testing against 3.1.1)

Thanks for any insight you can give on this (admittedly advanced) issue!

+1  A: 

I found this in the python code (in memoryobject.c in Objects) in the function 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;
}

that's the source of the latter two errors. It looks like the itemsize for even mv[0] is still not equal to itself.

Update

Here's what I think is going on. When you try to assign something in mv, it calls memory_ass_sub in Objects/memoryobject.c, but that function takes only a PyObject as input. This object is then changed into a buffer inside using the PyObject_GetBuffer function even though in the case of mv[0] it is already a buffer (and the buffer you want!). My guess is that this function takes the object and makes it into a simple buffer of itemsize=1 regardless of whether it is already a buffer or not. That is why you get the mismatching item sizes even for

mv[0] = mv[0]

The problem with the first assignment,

mv[0] = 0xFFFFFFFF

stems (I think) from checking if the int is able to be used as a buffer, which currently it isn't set-up for from what I understand.

In other words, the buffer system isn't currently able to handle item sizes bigger from 1. It doesn't look like it is so far off, but it would take a bit more work on your end. If you do get it working, you should probably submit the changes back to the main Python distribution.

Another Update

The error code from your first try at assigning mv[0] stems from the int failing the PyObject_CheckBuffer when PyObject_CheckBuffer is called on it. Apparently the system only handles copies from bufferable objects. This seems like it should be changed too.

Conclusion

Currently the Python buffer system can't handle items with itemsize > 1 as you guessed. Also, it can't handle assignments to a buffer from non-bufferable objects such as ints.

Justin Peel
Are you sure that `len` is supposed to be the number of items? That would explain a lot, but is also in direct conflict with the Python documentation (http://docs.python.org/3.1/c-api/buffer.html): `Py_ssize_t len - The total length of the memory in bytes.`I'll give it a try though. Thanks.
Toji
Yeah, I no longer think that is the problem.
Justin Peel
Thanks for doing the legwork on this! I'm still new to the Python C APIs so I have a hard time finding the bits that I need in the code. I had already guessed as much about the int issue (which isn't a big deal), but it's a little disappointing that the itemsize > 1 is borked. Given the time I may see if I can get that working on my own and if so I'll definitely submit the changes back in. Thanks again!
Toji
One final thought: In python an int isn't necessarily 32 bits, so that's probably why int to buffer assignment isn't allowed: because the size of an int can be unpredictable (or at least non-obvious to the uninformed user). Makes sense.
Toji