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
Image
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
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
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!
pixels
@pixels.setter
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:
cdef
a local variable as a memoryview too)What I dislike is:
pixels
object after the with block. You could avoid this by adding an extra layer of indirection, but that would be significantly slower in Cython.