Page MenuHome

Added basic buffer protocol implementation for ImBuf objects
Needs RevisionPublic

Authored by Colin Basnett (cmbasnett) on Oct 15 2019, 9:07 AM.

Details

Summary

This simple implementation grants read/write access to the underlying pixel buffer of an ImBuf. If a float buffer exists, it returns a buffer for the float buffer, otherwise it uses the byte buffer. Here's a basic example of how to use it:

import imbuf

# load an iamge
im = imbuf.load('C:/some-file.png')

# grant memoryview access to underlying buffer
mv = memoryview(im)

# invert all values
for i in range(len(mv)):
    mv[i] = 255 - mv[i]

# write out the modified imbuf
imbuf.write(im)

Diff Detail

Event Timeline

Campbell Barton (campbellbarton) requested changes to this revision.EditedOct 16 2019, 4:09 AM

This has a likelyhood of exposing dangling pointers.

While I didn't look into the details, I think this would be a better approach:

with imbuf.buffer_color_bytes() as pixel_data:
    # Access pixels
    # Disallow operations which would re-allocate or resize the buffer.

This way:

  • Image access is limited to a scope, which gives us control over how it's accessed.
  • We can expose different buffer types (float, byte, zbuf).
  • We could have more advanced access (get a memory view on a sub-region for example).
This revision now requires changes to proceed.Oct 16 2019, 4:09 AM

Okay, I will look into exposing each buffer as a separate getter (float, byte, zbuf).

Any preference for how to expose the availability of these buffers? Maybe a set of has_float_buffer, has_byte_buffer, has_z_buffer properties? This would avoid the user needing to use the malloc flags and whatnot.

"Disallow operations which would re-allocate or resize the buffer".

For this, I assume that you mean some sort of lock that gets acquired for the lifetime of the buffer object, then attempting to do any operations while the buffer is locked (resize, crop etc.) would raise some sort of exception.

Let me know if you have any other notes.

Okay, I will look into exposing each buffer as a separate getter (float, byte, zbuf).
Any preference for how to expose the availability of these buffers? Maybe a set of has_float_buffer, has_byte_buffer, has_z_buffer properties? This would avoid the user needing to use the malloc flags and whatnot.

I wouldn't worry about this to begin with, the memory view can be a thin wrapper, so creating it shouldn't be slow.

"Disallow operations which would re-allocate or resize the buffer".

For this, I assume that you mean some sort of lock that gets acquired for the lifetime of the buffer object, then attempting to do any operations while the buffer is locked (resize, crop etc.) would raise some sort of exception.

Right, the PyImBuf would need to have either a lock flag or lock user-count's if we want to allow multiple buffers accessing the PyImBuf at once.

Starting with a lock flag is fine.

I came up with a slightly different approach that should accomplish the stated goals.

lock_buffers

Instead of individual functions to access specific buffers, the lock_buffers function locks *all* buffer allocation modifying operations (free, resize etc.), and returns a ImBufBufferAccess object that provides read/write memoryviews to all the underlying buffers (rect, zbuf, rect_float etc.) so long as it's used in a context-managed scope (ie. with block).

Example

import imbuf
  
image = imbuf.load(path)  
with image.lock_buffers() as buffers:
    color = buffers.color  # <memoryview at 0x123456789>
    # Modify the buffer to your heart's content.
    for i in range(len(color)):
        color[i] = 255 - color[i]
    # image.resize((100, 100))  # RuntimeError: operation forbidden while buffers are locked
image.resize((100, 100))  # Buffers are unlocked, good to go.

ImBufBufferAccess object

The following getters are available in the object returned by lock_buffers.

MemberDescription
colormemoryview to rect buffer or None
color_floatmemoryview to rect_float buffer or None
zbufmemoryview to zbuf buffer or None
zbuf_floatmemoryview to zbuf_float buffer or None

Context Management

Calling lock_buffers outside of a with statement has no effect, as the locking and memoryview initialization is performed in the __enter__ function to enforce the locking mechanism.

Attempting to access the buffer outside of a context-managed scope will result in an error

buffers = image.lock_buffers()
buffers.color  # RuntimeError: buffer access is only allowed in a context-managed scope

No Dangling Pointers

Dangling pointers are no longer possible with this scheme, as release is called on all the memoryviews in the __exit__ function:

dangle = None
with image.lock_buffers() as buffers:
    dangle = buffers.color
dangle[0] = 0x7f  # ValueError: operation forbidden on released memoryview object

Recursive Locking

Attempting to lock the buffers recursively will throw an error.

with image.lock_buffers() as buffers:
    with image.lock_buffers() as rbuffers:  # RuntimeError: buffers are already locked
        pass

Feedback?

I will submit the modified diff after feedback and clean-up. Let me know if you have any suggestions on naming or alternative methods.

Would prefer not to add the overhead of adding an intermediate object to access image data.

Instead we can have:

with imbuf.data_rgba_u8() as color_buf:
    # access 'color_buf' memory-view.

This is convenient in the same way opening a file in Python doesn't require knowledge of an intermediate object type (it's methods, attributes etc).

It someone wants to load multiple buffers at once, that can be done in Python.

eg:

with imbuf.data_rgba_u8() as color_buf, imbuf.data_depth_u32() as depth_buf:
   ...