Source code for sdl2.ext.pixelaccess

import ctypes
from .compat import UnsupportedError, experimental
from .array import MemoryView
from ..surface import SDL_MUSTLOCK, SDL_LockSurface, SDL_UnlockSurface, \
    SDL_Surface
from ..stdinc import Uint8
from .draw import prepare_color
from .sprite import SoftwareSprite
from .surface import _get_target_surface

try:
    import numpy
    _HASNUMPY = True
except ImportError:
    _HASNUMPY = False


__all__ = [
    "PixelView", "SurfaceArray", "pixels2d", "pixels3d", "surface_to_ndarray"
]

[docs]class PixelView(MemoryView): """A 2D memory view for reading and writing SDL surface pixels. This class uses a ``view[y][x]`` layout, with the y-axis as the first dimension and the x-axis as the second. ``PixelView`` objects currently do not support array slicing, but support negative indexing as of PySDL2 0.9.10. If the source surface is RLE-accelerated, it will be locked automatically when the view is created and you will need to re-lock the surface using :func:`SDL_UnlockSurface` once you are done with the view. .. warning:: The source surface should not be freed or deleted until the view is no longer needed. Accessing the view for a freed surface will likely cause Python to hard-crash. .. note:: This class is implemented on top of the :class:`~sdl2.ext.MemoryView` class. As such, it makes heavy use of recursion to access rows and will generally be much slower than the :mod:`numpy`-based :func:`~sdl2.ext.pixels2d` and :func:`~sdl2.ext.pixels3d` functions. Args: source (:obj:`~sdl2.SDL_Surface`, :obj:`~sdl2.ext.SoftwareSprite`): The SDL surface to access with the view. """ def __init__(self, source): if isinstance(source, SoftwareSprite): self._surface = source.surface # keep a reference, so the Sprite's not GC'd self._sprite = source elif isinstance(source, SDL_Surface): self._surface = source elif "SDL_Surface" in str(type(source)): self._surface = source.contents else: raise TypeError("source must be a Sprite or SDL_Surface") itemsize = self._surface.format.contents.BytesPerPixel if itemsize == 3: e = "Cannot open a 3 bytes-per-pixel surface using a PixelView." raise RuntimeError(e) if SDL_MUSTLOCK(self._surface): SDL_LockSurface(self._surface) pxbuf = ctypes.cast(self._surface.pixels, ctypes.POINTER(Uint8)) strides = (self._surface.h, self._surface.w) srcsize = self._surface.h * self._surface.pitch super(PixelView, self).__init__(pxbuf, itemsize, strides, getfunc=self._getitem, setfunc=self._setitem, srcsize=srcsize) def _getitem(self, start, end): if self.itemsize == 1: # byte-wise access return self.source[start:end] # move the pointer to the correct location src = ctypes.byref(self.source.contents, start) casttype = ctypes.c_ubyte if self.itemsize == 2: casttype = ctypes.c_ushort elif self.itemsize == 4: casttype = ctypes.c_uint return ctypes.cast(src, ctypes.POINTER(casttype)).contents.value def _setitem(self, start, end, value): target = None if self.itemsize == 1: target = ctypes.cast(self.source, ctypes.POINTER(ctypes.c_ubyte)) elif self.itemsize == 2: target = ctypes.cast(self.source, ctypes.POINTER(ctypes.c_ushort)) elif self.itemsize == 4: target = ctypes.cast(self.source, ctypes.POINTER(ctypes.c_uint)) value = prepare_color(value, self._surface) target[start // self.itemsize] = value
def _ndarray_prep(source, funcname, ndim): # Internal function for preparing SDL_Surfaces for casting to ndarrays if not _HASNUMPY: err = "'{0}' requires Numpy, which could not be found." raise UnsupportedError(err.format(funcname)) # Get SDL surface and extract required attributes psurface = _get_target_surface(source, argname="source") sz = psurface.h * psurface.pitch bpp = psurface.format.contents.BytesPerPixel if bpp < 1 or bpp > 4: err = "The bpp of the source surface must be between 1 and 4, inclusive" raise ValueError(err + " (got {0}).".format(bpp)) elif bpp == 3 and ndim == 2: err = "Surfaces with 3 bytes-per-pixel cannot be cast as 2D arrays." raise RuntimeError(err) # Handle 2D and 3D arrays differently where needed if ndim == 2: dtypes = { 1: numpy.uint8, 2: numpy.uint16, 4: numpy.uint32 } strides = (psurface.pitch, bpp) shape = psurface.h, psurface.w dtype = dtypes[bpp] else: strides = (psurface.pitch, bpp, 1) shape = psurface.h, psurface.w, bpp dtype = numpy.uint8 return (psurface, sz, shape, dtype, strides)
[docs]def pixels2d(source, transpose=True): """Creates a 2D Numpy array view for a given SDL surface. This function casts the surface pixels to a 2D Numpy array view, providing read and write access to the underlying surface. If the source surface is RLE-accelerated, it will be locked automatically when the view is created and you will need to re-lock the surface using :func:`SDL_UnlockSurface` once you are done with the array. By default, the array is returned in ``arr[x][y]`` format with the x-axis as the first dimension, contrary to PIL and PyOpenGL convention. To obtain an ``arr[y][x]`` array, set the ``transpose`` argument to ``False``. .. warning:: The source surface should not be freed or deleted until the array is no longer needed. Accessing the array for a freed surface will likely cause Python to hard-crash. .. note:: This function requires Numpy to be installed in the current Python environment. Args: source (:obj:`~sdl2.SDL_Surface`, :obj:`~sdl2.ext.SoftwareSprite`): The SDL surface to cast to a numpy array. transpose (bool, optional): Whether the output array should be transposed to have ``arr[x][y]`` axes instead of ``arr[y][x]`` axes. Defaults to ``True``. Returns: :obj:`numpy.ndarray`: A 2-dimensional Numpy array containing the integer color values for each pixel in the surface. Raises: RuntimeError: If Numpy could not be imported. """ sf, sz, shape, dtype, strides = _ndarray_prep(source, "pixels2d", ndim=2) if SDL_MUSTLOCK(sf): SDL_LockSurface(sf) pxbuf = ctypes.cast(sf.pixels, ctypes.POINTER(ctypes.c_ubyte * sz)) arr = SurfaceArray( shape, dtype, pxbuf.contents, 0, strides, "C", source, sf ) return arr.transpose() if transpose else arr
[docs]def pixels3d(source, transpose=True): """Creates a 3D Numpy array view for a given SDL surface. This function casts the surface pixels to a 3D Numpy array view, providing read and write access to the underlying surface. If the source surface is RLE-accelerated, it will be locked automatically when the view is created and you will need to re-lock the surface using :func:`SDL_UnlockSurface` once you are done with the array. By default, the array is returned in ``arr[x][y]`` format with the x-axis as the first dimension, contrary to PIL and PyOpenGL convention. To obtain an ``arr[y][x]`` array, set the ``transpose`` argument to ``False``. When creating a 3D array view, the order of the RGBA values for each pixel may be reversed for some common surface pixel formats (e.g. 'BGRA' for a ``SDL_PIXELFORMAT_ARGB8888`` surface). To correct this, you can call ``numpy.flip(arr, axis=2)`` to return a view of the array with the expected channel order. .. warning:: The source surface should not be freed or deleted until the array is no longer needed. Accessing the array for a freed surface will likely cause Python to hard-crash. .. note:: This function requires Numpy to be installed in the current Python environment. Args: source (:obj:`~sdl2.SDL_Surface`, :obj:`~sdl2.ext.SoftwareSprite`): The SDL surface to cast to a numpy array. transpose (bool, optional): Whether the output array should be transposed to have ``arr[x][y]`` axes instead of ``arr[y][x]`` axes. Defaults to ``True``. Returns: :obj:`numpy.ndarray`: A 3-dimensional Numpy array containing the values of each byte for each pixel in the surface. Raises: RuntimeError: If Numpy could not be imported. """ sf, sz, shape, dtype, strides = _ndarray_prep(source, "pixels3d", ndim=3) if SDL_MUSTLOCK(sf): SDL_LockSurface(sf) pxbuf = ctypes.cast(sf.pixels, ctypes.POINTER(ctypes.c_ubyte * sz)) arr = SurfaceArray( shape, dtype, pxbuf.contents, 0, strides, "C", source, sf ) return arr.transpose(1, 0, 2) if transpose else arr
[docs]def surface_to_ndarray(source, ndim=3): """Returns a copy of an SDL surface as a Numpy array. The main difference between this function and :func:`~sdl2.ext.pixels2d` or :func:`~sdl2.ext.pixels3d` is that it returns a copy of the surface instead of a view, meaning that modifying the returned array will not affect the original surface (or vice-versa). This function is also slightly safer, as it does not assume that the source surface has been kept in memory. When creating a 3D array copy, the order of the RGBA values for each pixel may be reversed for some common surface pixel formats (e.g. 'BGRA' for a ``SDL_PIXELFORMAT_ARGB8888`` surface). To correct this, you can call ``numpy.flip(arr, axis=2)`` to return a view of the array with the expected channel order. .. note:: Unlike :func:`~sdl2.ext.pixels2d` or :func:`~sdl2.ext.pixels3d`, this function always returns arrays with the y-axis as the first dimension (e.g. ``arr[y][x]``). .. note:: This function requires Numpy to be installed in the current Python environment. Args: source (:obj:`~sdl2.SDL_Surface`, :obj:`~sdl2.ext.SoftwareSprite`): The SDL surface to convert to a numpy array. ndim (int, optional): The number of dimensions for the returned array, must be either 2 (for a 2D array) or 3 (for a 3D array). Defaults to 3. Returns: :obj:`numpy.ndarray`: A Numpy array containing a copy of the pixel data for the given surface. Raises: RuntimeError: If Numpy could not be imported. """ if ndim not in [2, 3]: err = "Can only convert surfaces to 2D or 3D arrays (got {0})." raise ValueError(err.format(ndim)) funcname = "surface_to_array" sf, sz, shape, dtype, strides = _ndarray_prep(source, funcname, ndim) was_unlocked = sf.locked == 0 if SDL_MUSTLOCK(sf): SDL_LockSurface(sf) pxbuf = ctypes.cast(sf.pixels, ctypes.POINTER(ctypes.c_ubyte * sz)) tmp = numpy.ndarray(shape, dtype, pxbuf.contents, strides=strides) if was_unlocked and SDL_MUSTLOCK(sf): SDL_UnlockSurface(sf) return numpy.copy(tmp)
[docs]class SurfaceArray(numpy.ndarray if _HASNUMPY else object): """A Numpy array that keeps a reference to its parent SDL surface. This class is used to keep track of the original source object for :func:`~sdl2.ext.pixels2d` or :func:`~sdl2.ext.pixels3d` to prevent it from being automatically freed during garbage collection. It should never be used for any other purpose. """ def __new__(cls, shape, dtype=float, buffer_=None, offset=0, strides=None, order=None, source=None, surface=None): if _HASNUMPY: sfarray = numpy.ndarray.__new__( cls, shape, dtype, buffer_, offset, strides, order ) sfarray._source = source sfarray._surface = surface return sfarray else: return None def __array_finalize__(self, sfarray): if sfarray is None: return self._source = getattr(sfarray, '_source', None) self._surface = getattr(sfarray, '_surface', None)