CodeSurgeon CodeSurgeon - 3 years ago 157
Python Question

Detecting partial updates of a python/cython class property

Here is the scenario:

As part of my refactoring process for my 2D rendering cython library, I am removing extraneous "getter/setter" functions to make it more user-friendly. One of the tasks my renderer does is display images on the screen. Currently, I load in an image and extract its pixel data using some

SDL2_image
functions. Then, whenever some image pixel data was modified through that "setter" function, I would notify OpenGL that the updated data would need to be copied from the CPU to the GPU. My goal is to make this "book-keeping" more hidden from the user by exposing an image's pixels as a directly modifiable property and have the CPU <=> GPU synchronization occur behind the scenes.

Current Code:

Here is the relevant cython code for my
Image
class (the implementation .pyx file is included only for completeness and is only tangentially related):

image.pxd

from libs.sdl2 cimport *
from libc.stdint cimport uint8_t, uint32_t, int64_t
from libc.stdlib cimport malloc, free
from cpython cimport bool
from cython.view cimport array as cvarray

cdef class Image:
cdef unsigned int[:, :, :] pixels
cdef readonly int width
cdef readonly int height


image.pyx

from libs.sdl2 cimport *
from libc.stdint cimport uint8_t, uint32_t, int64_t
from libc.stdlib cimport malloc, free
from cpython cimport bool
from cython.view cimport array as cvarray

cdef int rgba_size = 4

cdef class Image:

def __cinit__(self, int width, int height, unsigned int[:, :, :] pixels=None):
self.width = width
self.height = height
self.pixels = pixels

def __dealloc__(Image self):
pass

@property
def pixels(self):
return self.pixels

@pixels.setter
def pixels(self, unsigned int[:, :, :] new_pixels):
self.pixels = new_pixels
print("updated")

@staticmethod
def from_url(str image_url):
"""
1. Load in an image to an SDL_Surface using SDL_Image module
2. Convert the image to the SDL_PIXELFORMAT_ABGR8888 format (right channels for OpenGL's RGBA)
3. Flip the image as OpenGL spec states texture's BOTTOM-left = (0, 0)!
"""
cdef Image out
cdef char * err
cdef bytes b_image_url = bytes(image_url, "utf-8")
cdef SDL_Surface *surf = IMG_Load(b_image_url)
if <int>surf == 0:
err = IMG_GetError()
raise ValueError(err)

cdef SDL_Surface *rgba_surf = SDL_ConvertSurfaceFormat(surf, SDL_PIXELFORMAT_ABGR8888, 0)
cdef int width = rgba_surf.w
cdef int height = rgba_surf.h
cdef uint32_t *pixel_data = <uint32_t *>rgba_surf.pixels

cdef unsigned int[:, :, :] pixels
pixels = cvarray(shape=(width, height, rgba_size), itemsize=sizeof(int), format="I")
cdef int x, y, flipped = 0
cdef uint32_t abgr = 0
for y in xrange(height):
for x in xrange(width):
flipped = (height - 1 - y) * width + x
abgr = pixel_data[flipped]
pixels[x, y, 0] = abgr >> 0 & 0xFF
pixels[x, y, 1] = abgr >> 8 & 0xFF
pixels[x, y, 2] = abgr >> 16 & 0xFF
pixels[x, y, 3] = abgr >> 24 & 0xFF

SDL_FreeSurface(surf)
SDL_FreeSurface(rgba_surf)
out = Image(width, height, pixels=pixels)
return out


This is how I am trying to use my class:

from graphics.image import Image
a = Image.from_url("./images/crate.png")
a.pixels[0, 0, 0] = 255#please trigger something that says that pixels has been partially updated!


The problem of course is that since only a single value of
pixels
been modified, the
@pixels.setter
decorator is not triggered. The only way to trigger it seems to be replacing the entire memoryview.

TL;DR: Given an exposed, publicly-modifiable property of a class that is a list/array/np.arrray/memoryview/whatever with multiple sub-values, is there a way to detect transparently whether or not said property has been partially updated within that class?

Answer Source

I think you can get something reasonably efficient with a combination of properties and context managers (for a with statement)

Start by keeping your getter and setter as in the question. Modify your getter so it returns a read only numpy array:

@property
def pixels(self):
    r = np.asarray(self._pixels) # gets a view, not a copy
    r.flags['WRITEABLE'] = False
    return r

Therefore there shouldn't be any "unmonitored" changes. The setter remains the same and only allows complete replacement of the pixels.

Now define a function that returns a contex manager object:

def writeable_pixels(self):
    class WriteablePixelsCtx:
        def __enter__(self2):
            return self._pixels # get the underlying (writeable) memoryview
        def __exit__(self2,*args):
            # update changes 
            # handle any exceptions appropriately (see the main Python documentation on __exit__ for this)
   return WriteablePixelsCtx()

You can then do:

with im.writeable_pixels() as pixels:
    pixels[i,j] = something

and at the end of the block your changes are efficiently dealt with all at once by exit.

What I like about this is:

  • Possible to make a whole bunch of small changes with a big update at the end
  • Possible to write to pixels efficiently in Cython (it's a memoryview, so you just have to cdef a local variable as a memoryview too)

What I dislike is:

  • Two different axis mechanisms is a bit messy
  • Read access can't be made fast in Cython - memoryviews don't work with read only arrays
  • You can manage to write to the saved pixelsobject after the with block. You could avoid this by adding an extra layer of indirection, but that would be significantly slower in Cython.
Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download