Source code for pisense.array

# vim: set et sw=4 sts=4 fileencoding=utf-8:
#
# Alternative API for the Sense HAT
# Copyright (c) 2016-2018 Dave Jones <dave@waveform.org.uk>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#     * Redistributions of source code must retain the above copyright
#       notice, this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above copyright
#       notice, this list of conditions and the following disclaimer in the
#       documentation and/or other materials provided with the distribution.
#     * Neither the name of the copyright holder nor the
#       names of its contributors may be used to endorse or promote products
#       derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

"""
Defines the :class:`ScreenArray` class for representing the RGB pixel array
as a specialized :class:`~numpy.ndarray`.
"""

from __future__ import (
    unicode_literals,
    absolute_import,
    print_function,
    division,
)

import sys
import os
import time
import struct
import fcntl
import termios

from pkg_resources import require, VersionConflict, DistributionNotFound
try:
    # Check whether we're dealing with an old numpy version which doesn't
    # implement ndarray.__array_ufunc__; in this case we have to do something
    # rather different to implement standard binary operations for ScreenArray
    require('numpy>=1.13')
    _has_array_ufunc = True
except VersionConflict:
    _has_array_ufunc = False
except DistributionNotFound:
    # This will occur on RTD where we've mocked numpy; pretend we've got a
    # recent version of numpy installed
    _has_array_ufunc = True
import numpy as np
from colorzero import Color

from .formats import (
    color_dtype,
    buf_to_rgb,
    iter_to_rgb,
)

# Make Py2's str and range equivalent to Py3's
native_str = str  # pylint: disable=invalid-name
str = type('')  # pylint: disable=redefined-builtin,invalid-name

DEFAULT_GAMMA = [0, 0, 0, 0, 0, 0, 1, 1,
                 2, 2, 3, 3, 4, 5, 6, 7,
                 8, 9, 10, 11, 12, 14, 15, 17,
                 18, 20, 21, 23, 25, 27, 29, 31]

LOW_GAMMA = [0, 1, 1, 1, 1, 1, 1, 1,
             1, 1, 1, 1, 1, 2, 2, 2,
             3, 3, 3, 4, 4, 5, 5, 6,
             6, 7, 7, 8, 8, 9, 10, 10]


[docs]def array(data=None, shape=(8, 8)): """ Use this function to construct a new :class:`ScreenArray` and fill it with an initial source of *data*, which can be: * A single :class:`~colorzero.Color`. The resulting array will have the specified *shape*. * A list of :class:`~colorzero.Color` values. The resulting array will have the specified *shape*. * An :class:`~PIL.Image.Image`. The resulting array will have the shape of the image (the *shape* parameter is ignored). * Any compatible :class:`~numpy.ndarray`. In this case the shape of the array is preserved (the *shape* parameter is ignored). """ if data is None: result = ScreenArray(shape) result[...] = 0 elif isinstance(data, Color): result = ScreenArray(shape) result[...] = data else: try: result = buf_to_rgb(data) except TypeError: result = iter_to_rgb(data, shape) result = result.view(color_dtype, ScreenArray) return result
class ScreenArray(np.ndarray): """ By some curious numpy magic, anything I write here disappears from the __doc__ property. """ # pylint: disable=too-few-public-methods def __new__(cls, shape=(8, 8)): # pylint: disable=protected-access result = np.ndarray.__new__(cls, shape=shape, dtype=color_dtype) result._screen = None return result @staticmethod def _to_ndarray(v): return ( np.ascontiguousarray(v).view(np.float32, np.ndarray).reshape(v.shape + (3,)) if isinstance(v, np.ndarray) and v.dtype == color_dtype else np.ascontiguousarray(v).view(v.dtype, np.ndarray).reshape(v.shape) if isinstance(v, np.ndarray) else v ) @classmethod def _from_ndarray(cls, v): if ( isinstance(v, np.ndarray) and v.dtype == np.float32 and len(v.shape) == 3 and v.shape[-1] == 3): return v.view(color_dtype, cls).squeeze() return v if _has_array_ufunc: # XXX For numpy >=1.13; this ensures all ufuncs called against a # ScreenArray will treat the array as a 3-dimensional array of single # precision floats (compatible with just about everything) instead of # a 2-dimensional array of structures (which no ufunc can deal with) def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): inputs = [self._to_ndarray(v) for v in inputs] try: v, = kwargs['out'] except (KeyError, ValueError): pass else: kwargs['out'] = (self._to_ndarray(v),) result = super(ScreenArray, self).__array_ufunc__( ufunc, method, *inputs, **kwargs) return self._from_ndarray(result) else: # XXX For numpy <1.13; unfortunately there doesn't seem to be a # universal way of handling ufunc overrides prior to 1.13 (and Raspbian # Jessie and Stretch are both below this version). However, we can # override the standard operators and some common methods to provide # similar functionality which is probably good enough for the majority # of use-cases @classmethod def _call_ufunc(cls, ufunc, *inputs, **kwargs): inputs = [cls._to_ndarray(v) for v in inputs] try: v, = kwargs['out'] except (KeyError, ValueError): pass else: kwargs['out'] = (cls._to_ndarray(v),) return cls._from_ndarray(ufunc(*inputs, **kwargs)) __add__ = lambda self, other: self._call_ufunc(np.add, self, other) __sub__ = lambda self, other: self._call_ufunc(np.subtract, self, other) __mul__ = lambda self, other: self._call_ufunc(np.multiply, self, other) __truediv__ = lambda self, other: self._call_ufunc(np.true_divide, self, other) __floordiv__ = lambda self, other: self._call_ufunc(np.floor_divide, self, other) __mod__ = lambda self, other: self._call_ufunc(np.mod, self, other) __pow__ = lambda self, other: self._call_ufunc(np.power, self, other) __radd__ = lambda self, other: self._call_ufunc(np.add, other, self) __rsub__ = lambda self, other: self._call_ufunc(np.subtract, other, self) __rmul__ = lambda self, other: self._call_ufunc(np.multiply, other, self) __rtruediv__ = lambda self, other: self._call_ufunc(np.true_divide, other, self) __rfloordiv__ = lambda self, other: self._call_ufunc(np.floor_divide, other, self) __rmod__ = lambda self, other: self._call_ufunc(np.mod, other, self) __rpow__ = lambda self, other: self._call_ufunc(np.power, other, self) __iadd__ = lambda self, other: self._call_ufunc(np.add, self, other, out=(self,)) __isub__ = lambda self, other: self._call_ufunc(np.subtract, self, other, out=(self,)) __imul__ = lambda self, other: self._call_ufunc(np.multiply, self, other, out=(self,)) __itruediv__ = lambda self, other: self._call_ufunc(np.true_divide, self, other, out=(self,)) __ifloordiv__ = lambda self, other: self._call_ufunc(np.floor_divide, self, other, out=(self,)) __imod__ = lambda self, other: self._call_ufunc(np.mod, self, other, out=(self,)) __ipow__ = lambda self, other: self._call_ufunc(np.power, self, other, out=(self,)) __neg__ = lambda self: self._call_ufunc(np.negative, self) __pos__ = lambda self: self __abs__ = lambda self: self._call_ufunc(np.abs, self) __lt__ = lambda self, other: self._call_ufunc(np.less, self, other) __le__ = lambda self, other: self._call_ufunc(np.less_equal, self, other) __eq__ = lambda self, other: self._call_ufunc(np.equal, self, other) __ne__ = lambda self, other: self._call_ufunc(np.not_equal, self, other) __gt__ = lambda self, other: self._call_ufunc(np.greater, self, other) __ge__ = lambda self, other: self._call_ufunc(np.greater_equal, self, other) # XXX Python 2.7 compat __div__ = __truediv__ __rdiv__ = __rtruediv__ __idiv__ = __itruediv__ def clip(self, a_min, a_max, out=None): if out is not None: out = self._to_ndarray(out) result = self._to_ndarray(self).clip(a_min, a_max, out) return self._from_ndarray(result) # TODO implement matmul for image transforms? def __array_finalize__(self, obj): # pylint: disable=protected-access,attribute-defined-outside-init if obj is None: return self._screen = getattr(obj, '_screen', None) def __setitem__(self, index, value): # pylint: disable=protected-access super(ScreenArray, self).__setitem__(index, value) if self._screen: # If we're a slice of the original pixels value, find the parent # that contains the complete array and send that to the setter orig = self while orig.shape != (8, 8) and orig.base is not None: orig = orig.base self._screen.array = orig def __setslice__(self, i, j, sequence): # pragma: no cover # pylint: disable=protected-access super(ScreenArray, self).__setslice__(i, j, sequence) if self._screen: orig = self while orig.shape != (8, 8) and orig.base is not None: orig = orig.base self._screen.array = orig @staticmethod def _term_supports_color(): try: stdout_fd = sys.stdout.fileno() except (AttributeError, IOError) as exc: return False else: is_a_tty = os.isatty(stdout_fd) is_windows = sys.platform.startswith('win') return is_a_tty and not is_windows @staticmethod def _term_size(): "Returns the size (cols, rows) of the console" try: buf = fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, '12345678') row, col = struct.unpack(native_str('hhhh'), buf)[0:2] return (col, row) except IOError: # Don't try and get clever with ctermid; this can work but gives # false readings under things like IDLE. Just try the environment # and fall back to a sensible default if that fails try: return (int(os.environ['COLUMNS']), int(os.environ['LINES'])) except KeyError: return (80, 24) def __format__(self, format_spec): # Parse the format_spec; we don't calculate defaults for colors and # width unless they're not explicitly specified elements = '\u2588\u2588' # ██ colors = None width = None overflow = '\u00BB' # » for section in format_spec.split(':'): section = section.lstrip() if not section.rstrip(): pass elif section.startswith('e'): elements = section[1:] elif section.startswith('c'): colors = section[1:].strip().lower() elif section.startswith('w'): width = int(section[1:].strip()) elif section.startswith('o'): overflow = section[1:] else: raise ValueError('invalid section in array format spec: %s' % section) if colors is None: colors = '16m' if self._term_supports_color() else '0' if width is None: width = self._term_size()[0] if len(elements) * self.shape[1] > width: x_limit = (width - len(overflow)) // len(elements) else: x_limit = None overflow = '' if colors == '0': space = ' ' * len(elements) return '\n'.join( ''.join( elements if Color(*c).lightness >= 1/4 else space for c in row[:x_limit] ) + overflow for row in self ) else: return '\n'.join( ''.join( '{color_dtype:{colors}}{elements}{color_dtype:0}'.format( color_dtype=Color(*c), colors=colors, elements=elements) for c in row[:x_limit] ) + overflow for row in self ) def show(self, element=None, colors=None, width=None, overflow=None): """ By some curious numpy magic, anything I write here disappears from the __doc__ property. """ specs = [] if element is not None: specs.append('e' + element) if colors is not None: specs.append('c' + str(colors)) if width is not None: specs.append('w' + str(width)) if overflow is not None: specs.append('o' + overflow) print('{self:{spec}}'.format(self=self, spec=':'.join(specs))) def copy(self, order='C'): # pylint: disable=missing-docstring,protected-access result = super(ScreenArray, self).copy(order) result._screen = None return result