Source code for psychopy.colors

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""Classes and functions for working with colors.
"""

__all__ = [
    "colorExamples",
    "colorNames",
    "colorSpaces",
    "isValidColor",
    "hex2rgb255",
    "Color"
]

import re
from math import inf
from psychopy import logging
import psychopy.tools.colorspacetools as ct
from psychopy.tools.mathtools import infrange
import numpy as np


# Dict of examples of Psychopy Red at 12% opacity in different formats
colorExamples = {
    'named': 'crimson',
    'hex': '#F2545B',
    'hexa': '#F2545B1E',
    'rgb': (0.89, -0.35, -0.28),
    'rgba': (0.89, -0.35, -0.28, -0.76),
    'rgb1': (0.95, 0.32, 0.36),
    'rgba1': (0.95, 0.32, 0.36, 0.12),
    'rgb255': (242, 84, 91),
    'rgba255': (242, 84, 91, 30),
    'hsv': (357, 0.65, 0.95),
    'hsva': (357, 0.65, 0.95, 0.12),
}

# Dict of named colours
colorNames = {
    "none": (0, 0, 0, 0),
    "transparent": (0, 0, 0, 0),
    "aliceblue": (0.882352941176471, 0.945098039215686, 1),
    "antiquewhite": (0.96078431372549, 0.843137254901961, 0.686274509803922),
    "aqua": (-1, 1, 1),
    "aquamarine": (-0.00392156862745097, 1, 0.662745098039216),
    "azure": (0.882352941176471, 1, 1),
    "beige": (0.92156862745098, 0.92156862745098, 0.725490196078431),
    "bisque": (1, 0.788235294117647, 0.537254901960784),
    "black": (-1, -1, -1),
    "blanchedalmond": (1, 0.843137254901961, 0.607843137254902),
    "blue": (-1, -1, 1),
    "blueviolet": (0.0823529411764705, -0.662745098039216, 0.772549019607843),
    "brown": (0.294117647058824, -0.670588235294118, -0.670588235294118),
    "burlywood": (0.741176470588235, 0.443137254901961, 0.0588235294117647),
    "cadetblue": (-0.254901960784314, 0.23921568627451, 0.254901960784314),
    "chartreuse": (-0.00392156862745097, 1, -1),
    "chestnut": (0.607843137254902, -0.27843137254902, -0.27843137254902),
    "chocolate": (0.647058823529412, -0.176470588235294, -0.764705882352941),
    "coral": (1, -0.00392156862745097, -0.372549019607843),
    "cornflowerblue": (-0.215686274509804, 0.168627450980392, 0.858823529411765),
    "cornsilk": (1, 0.945098039215686, 0.725490196078431),
    "crimson": (0.725490196078431, -0.843137254901961, -0.529411764705882),
    "cyan": (-1, 1, 1),
    "darkblue": (-1, -1, 0.0901960784313725),
    "darkcyan": (-1, 0.0901960784313725, 0.0901960784313725),
    "darkgoldenrod": (0.443137254901961, 0.0509803921568628, -0.913725490196078),
    "darkgray": (0.325490196078431, 0.325490196078431, 0.325490196078431),
    "darkgreen": (-1, -0.215686274509804, -1),
    "darkgrey": (0.325490196078431, 0.325490196078431, 0.325490196078431),
    "darkkhaki": (0.482352941176471, 0.435294117647059, -0.16078431372549),
    "darkmagenta": (0.0901960784313725, -1, 0.0901960784313725),
    "darkolivegreen": (-0.333333333333333, -0.16078431372549, -0.631372549019608),
    "darkorange": (1, 0.0980392156862746, -1),
    "darkorchid": (0.2, -0.607843137254902, 0.6),
    "darkred": (0.0901960784313725, -1, -1),
    "darksalmon": (0.827450980392157, 0.176470588235294, -0.0431372549019607),
    "darkseagreen": (0.12156862745098, 0.474509803921569, 0.12156862745098),
    "darkslateblue": (-0.435294117647059, -0.52156862745098, 0.0901960784313725),
    "darkslategray": (-0.631372549019608, -0.380392156862745, -0.380392156862745),
    "darkslategrey": (-0.631372549019608, -0.380392156862745, -0.380392156862745),
    "darkturquoise": (-1, 0.615686274509804, 0.63921568627451),
    "darkviolet": (0.16078431372549, -1, 0.654901960784314),
    "deeppink": (1, -0.843137254901961, 0.152941176470588),
    "deepskyblue": (-1, 0.498039215686275, 1),
    "dimgray": (-0.176470588235294, -0.176470588235294, -0.176470588235294),
    "dimgrey": (-0.176470588235294, -0.176470588235294, -0.176470588235294),
    "dodgerblue": (-0.764705882352941, 0.129411764705882, 1),
    "firebrick": (0.396078431372549, -0.733333333333333, -0.733333333333333),
    "floralwhite": (1, 0.96078431372549, 0.882352941176471),
    "forestgreen": (-0.733333333333333, 0.0901960784313725, -0.733333333333333),
    "fuchsia": (1, -1, 1),
    "gainsboro": (0.725490196078431, 0.725490196078431, 0.725490196078431),
    "ghostwhite": (0.945098039215686, 0.945098039215686, 1),
    "gold": (1, 0.686274509803922, -1),
    "goldenrod": (0.709803921568627, 0.294117647058824, -0.749019607843137),
    "gray": (0.00392156862745097, 0.00392156862745097, 0.00392156862745097),
    "grey": (0.00392156862745097, 0.00392156862745097, 0.00392156862745097),
    "green": (-1, 0.00392156862745097, -1),
    "greenyellow": (0.356862745098039, 1, -0.631372549019608),
    "honeydew": (0.882352941176471, 1, 0.882352941176471),
    "hotpink": (1, -0.176470588235294, 0.411764705882353),
    "indigo": (-0.411764705882353, -1, 0.0196078431372548),
    "ivory": (1, 1, 0.882352941176471),
    "khaki": (0.882352941176471, 0.803921568627451, 0.0980392156862746),
    "lavender": (0.803921568627451, 0.803921568627451, 0.96078431372549),
    "lavenderblush": (1, 0.882352941176471, 0.92156862745098),
    "lawngreen": (-0.0274509803921569, 0.976470588235294, -1),
    "lemonchiffon": (1, 0.96078431372549, 0.607843137254902),
    "lightblue": (0.356862745098039, 0.694117647058824, 0.803921568627451),
    "lightcoral": (0.882352941176471, 0.00392156862745097, 0.00392156862745097),
    "lightcyan": (0.756862745098039, 1, 1),
    "lightgoldenrodyellow": (0.96078431372549, 0.96078431372549, 0.647058823529412),
    "lightgray": (0.654901960784314, 0.654901960784314, 0.654901960784314),
    "lightgreen": (0.129411764705882, 0.866666666666667, 0.129411764705882),
    "lightgrey": (0.654901960784314, 0.654901960784314, 0.654901960784314),
    "lightpink": (1, 0.427450980392157, 0.513725490196078),
    "lightsalmon": (1, 0.254901960784314, -0.0431372549019607),
    "lightseagreen": (-0.749019607843137, 0.396078431372549, 0.333333333333333),
    "lightskyblue": (0.0588235294117647, 0.615686274509804, 0.96078431372549),
    "lightslategray": (-0.0666666666666667, 0.0666666666666667, 0.2),
    "lightslategrey": (-0.0666666666666667, 0.0666666666666667, 0.2),
    "lightsteelblue": (0.380392156862745, 0.537254901960784, 0.741176470588235),
    "lightyellow": (1, 1, 0.756862745098039),
    "lime": (-1, 1, -1),
    "limegreen": (-0.607843137254902, 0.607843137254902, -0.607843137254902),
    "linen": (0.96078431372549, 0.882352941176471, 0.803921568627451),
    "magenta": (1, -1, 1),
    "maroon": (0.00392156862745097, -1, -1),
    "mediumaquamarine": (-0.2, 0.607843137254902, 0.333333333333333),
    "mediumblue": (-1, -1, 0.607843137254902),
    "mediumorchid": (0.458823529411765, -0.333333333333333, 0.654901960784314),
    "mediumpurple": (0.152941176470588, -0.12156862745098, 0.717647058823529),
    "mediumseagreen": (-0.529411764705882, 0.403921568627451, -0.113725490196078),
    "mediumslateblue": (-0.0352941176470588, -0.184313725490196, 0.866666666666667),
    "mediumspringgreen": (-1, 0.96078431372549, 0.207843137254902),
    "mediumturquoise": (-0.435294117647059, 0.63921568627451, 0.6),
    "mediumvioletred": (0.56078431372549, -0.835294117647059, 0.0431372549019609),
    "midnightblue": (-0.803921568627451, -0.803921568627451, -0.12156862745098),
    "mintcream": (0.92156862745098, 1, 0.96078431372549),
    "mistyrose": (1, 0.788235294117647, 0.764705882352941),
    "moccasin": (1, 0.788235294117647, 0.419607843137255),
    "navajowhite": (1, 0.741176470588235, 0.356862745098039),
    "navy": (-1, -1, 0.00392156862745097),
    "oldlace": (0.984313725490196, 0.92156862745098, 0.803921568627451),
    "olive": (0.00392156862745097, 0.00392156862745097, -1),
    "olivedrab": (-0.16078431372549, 0.113725490196078, -0.725490196078431),
    "orange": (1, 0.294117647058824, -1),
    "orangered": (1, -0.458823529411765, -1),
    "orchid": (0.709803921568627, -0.12156862745098, 0.67843137254902),
    "palegoldenrod": (0.866666666666667, 0.819607843137255, 0.333333333333333),
    "palegreen": (0.192156862745098, 0.968627450980392, 0.192156862745098),
    "paleturquoise": (0.372549019607843, 0.866666666666667, 0.866666666666667),
    "palevioletred": (0.717647058823529, -0.12156862745098, 0.152941176470588),
    "papayawhip": (1, 0.874509803921569, 0.670588235294118),
    "peachpuff": (1, 0.709803921568627, 0.450980392156863),
    "peru": (0.607843137254902, 0.0431372549019609, -0.505882352941176),
    "pink": (1, 0.505882352941176, 0.592156862745098),
    "plum": (0.733333333333333, 0.254901960784314, 0.733333333333333),
    "powderblue": (0.380392156862745, 0.756862745098039, 0.803921568627451),
    "purple": (0.00392156862745097, -1, 0.00392156862745097),
    "red": (1, -1, -1),
    "rosybrown": (0.474509803921569, 0.12156862745098, 0.12156862745098),
    "royalblue": (-0.490196078431373, -0.176470588235294, 0.764705882352941),
    "saddlebrown": (0.0901960784313725, -0.458823529411765, -0.850980392156863),
    "salmon": (0.96078431372549, 0.00392156862745097, -0.105882352941176),
    "sandybrown": (0.913725490196079, 0.286274509803922, -0.247058823529412),
    "seagreen": (-0.63921568627451, 0.0901960784313725, -0.317647058823529),
    "seashell": (1, 0.92156862745098, 0.866666666666667),
    "sienna": (0.254901960784314, -0.356862745098039, -0.647058823529412),
    "silver": (0.505882352941176, 0.505882352941176, 0.505882352941176),
    "skyblue": (0.0588235294117647, 0.615686274509804, 0.843137254901961),
    "slateblue": (-0.168627450980392, -0.294117647058823, 0.607843137254902),
    "slategray": (-0.12156862745098, 0.00392156862745097, 0.129411764705882),
    "slategrey": (-0.12156862745098, 0.00392156862745097, 0.129411764705882),
    "snow": (1, 0.96078431372549, 0.96078431372549),
    "springgreen": (-1, 1, -0.00392156862745097),
    "steelblue": (-0.450980392156863, 0.0196078431372548, 0.411764705882353),
    "tan": (0.647058823529412, 0.411764705882353, 0.0980392156862746),
    "teal": (-1, 0.00392156862745097, 0.00392156862745097),
    "thistle": (0.694117647058824, 0.498039215686275, 0.694117647058824),
    "tomato": (1, -0.223529411764706, -0.443137254901961),
    "turquoise": (-0.498039215686275, 0.756862745098039, 0.631372549019608),
    "violet": (0.866666666666667, 0.0196078431372548, 0.866666666666667),
    "wheat": (0.92156862745098, 0.741176470588235, 0.403921568627451),
    "white": (1, 1, 1),
    "whitesmoke": (0.92156862745098, 0.92156862745098, 0.92156862745098),
    "yellow": (1, 1, -1),
    "yellowgreen": (0.207843137254902, 0.607843137254902, -0.607843137254902)
}

# Convert all named colors to numpy arrays
for key in colorNames:
    colorNames[key] = np.array(colorNames[key])

# Dict of regexpressions/ranges for different formats
colorSpaces = {
    'named': re.compile("|".join(list(colorNames))), # A named colour space
    'hex': re.compile(r'#[\dabcdefABCDEF]{6}'), # Hex
    'rgb': [infrange(-1, 1), infrange(-1, 1), infrange(-1, 1)], # RGB from -1 to 1
    'rgba': [infrange(-1, 1), infrange(-1, 1), infrange(-1, 1), infrange(0, 1)],  # RGB + alpha from -1 to 1
    'rgb1': [infrange(0, 1), infrange(0, 1), infrange(0, 1)],  # RGB from 0 to 1
    'rgba1': [infrange(0, 1), infrange(0, 1), infrange(0, 1), infrange(0, 1)],  # RGB + alpha from 0 to 1
    'rgb255': [infrange(0, 255, 1), infrange(0, 255, 1), infrange(0, 255, 1)], # RGB from 0 to 255
    'rgba255': [infrange(0, 255, 1), infrange(0, 255, 1), infrange(0, 255, 1), infrange(0, 1)], # RGB + alpha from 0 to 255
    'hsv': [infrange(0, 360, 1), infrange(0, 1), infrange(0, 1)], # HSV with hue from 0 to 360 and saturation/vibrancy from 0 to 1
    'hsva': [infrange(0, 360, 1), infrange(0, 1), infrange(0, 1), infrange(0, 1)], # HSV with hue from 0 to 360 and saturation/vibrancy from 0 to 1 + alpha from 0 to 1
    # 'rec709TF': [infrange(-4.5, 1), infrange(-4.5, 1), infrange(-4.5, 1)], # rec709TF adjusted RGB from -4.5 to 1
    # 'rec709TFa': [infrange(-4.5, 1), infrange(-4.5, 1), infrange(-4.5, 1), infrange(0, 1)], # rec709TF adjusted RGB from -4.5 to 1 + alpha from 0 to 1
    'srgb': [infrange(-1, 1), infrange(-1, 1), infrange(-1, 1)],  # srgb from -1 to 1
    'srgba': [infrange(-1, 1), infrange(-1, 1), infrange(-1, 1), infrange(0, 1)], # srgb from -1 to 1 + alpha from 0 to 1
    'lms': [infrange(-1, 1), infrange(-1, 1), infrange(-1, 1), infrange(0, 1)],  # LMS from -1 to 1
    'lmsa': [infrange(-1, 1), infrange(-1, 1), infrange(-1, 1), infrange(0, 1)],  # LMS + alpha from 0 to 1
    'dkl': [infrange(-inf, inf), infrange(-inf, inf), infrange(-inf, inf), infrange(0, 1)], # DKL placeholder: Accepts any values
    'dkla': [infrange(-inf, inf), infrange(-inf, inf), infrange(-inf, inf), infrange(0, 1)], # DKLA placeholder: Accepts any values + alpha from 0 to 1
    'dklCart': [infrange(-inf, inf), infrange(-inf, inf), infrange(-inf, inf), infrange(0, 1)],
    # Cartesian DKL placeholder: Accepts any values
    'dklaCart': [infrange(-inf, inf), infrange(-inf, inf), infrange(-inf, inf), infrange(0, 1)],
    # Cartesian DKLA placeholder: Accepts any values + alpha from 0 to 1
}

# Create subgroups of spaces for easy reference
integerSpaces = []
strSpaces = []
for key, val in colorSpaces.items():
    if isinstance(val, re.compile("").__class__):
        # Add any spaces which are str to a list
        strSpaces.append(key)
    elif isinstance(val, (list, tuple)):
        # Add any spaces which are integer-only to a list
        for cell in val:
            if isinstance(cell, infrange):
                if cell.step == 1 and key not in integerSpaces:
                    integerSpaces.append(key)

alphaSpaces = [
    'rgba', 'rgba1', 'rgba255', 'hsva', 'srgba', 'lmsa', 'dkla', 'dklaCart']
nonAlphaSpaces = list(colorSpaces)
for val in alphaSpaces:
    nonAlphaSpaces.remove(val)


[docs]class Color: """A class to store color details, knows what colour space it's in and can supply colours in any space. Parameters ---------- color : ArrayLike or None Color values (coordinates). Value must be in a format applicable to the specified `space`. space : str or None Colorspace to interpret the value of `color` as being within. contrast : int or float Factor to modulate the contrast of the color. conematrix : ArrayLike or None Cone matrix for colorspaces which require it. Must be a 3x3 array. """ def __init__(self, color=None, space=None, contrast=None, conematrix=None): self._cache = {} self._renderCache = {} self.contrast = contrast if isinstance(contrast, (int, float)) else 1 self.alpha = 1 self.valid = False self.conematrix = conematrix # defined here but set later self._requested = None self._requestedSpace = None self.set(color=color, space=space)
[docs] def validate(self, color, space=None): """ Check that a color value is valid in the given space, or all spaces if space==None. """ # Treat None as a named color if color is None: color = "none" if isinstance(color, str): if color == "": color = "none" # Handle everything as an array if not isinstance(color, np.ndarray): color = np.array(color) if color.ndim <= 1: color = np.reshape(color, (1, -1)) # If data type is string, check against named and hex as these override other spaces if color.dtype.char == 'U': # Remove superfluous quotes for i in range((len(color[:, 0]))): color[i, 0] = color[i, 0].replace("\"", "").replace("'", "") # If colors are all named, override color space namedMatch = np.vectorize( lambda col: bool(colorSpaces['named'].fullmatch( str(col).lower()))) # match regex against named if all(namedMatch(color[:, 0])): space = 'named' # If colors are all hex, override color space hexMatch = np.vectorize( lambda col: bool(colorSpaces['hex'].fullmatch(str(col)))) # match regex against hex if all(hexMatch(color[:, 0])): space = 'hex' # If color is a string but does not match any string space, it's invalid if space not in strSpaces: self.valid = False # Error if space still not set if not space: self.valid = False raise ValueError("Please specify a color space.") # Check that color space is valid if not space in colorSpaces: self.valid = False raise ValueError("{} is not a valid color space.".format(space)) # Get number of columns if color.ndim == 1: ncols = len(color) else: ncols = color.shape[1] # Extract alpha if set if space in strSpaces and ncols > 1: # If color should only be one value, extract second row self.alpha = color[:, 1] color = color[:, 0] ncols -= 1 elif space not in strSpaces and ncols > 3: # If color should be triplet, extract fourth row self.alpha = color[:, 3] color = color[:, :3] ncols -= 1 elif space not in strSpaces and ncols == 2: # If color should be triplet but is single value, extract second row self.alpha = color[:, 1] color = color[:, 1] ncols -= 1 # If single value given in place of triplet, duplicate it if space not in strSpaces and ncols == 1: color = np.tile(color, (1, 3)) # ncols = 3 # unused? # If values should be integers, round them if space in integerSpaces: color.round() # Finally, if array is only 1 long, remove extraneous dimension if color.shape[0] == 1: color = color[0] return color, space
[docs] def set(self, color=None, space=None): """Set the colour of this object - essentially the same as what happens on creation, but without having to initialise a new object. """ # If input is a Color object, duplicate all settings if isinstance(color, Color): self._requested = color._requested self._requestedSpace = color._requestedSpace self.valid = color.valid if color.valid: self.rgba = color.rgba return # Store requested colour and space (or defaults, if none given) self._requested = color self._requestedSpace = space # Validate and prepare values color, space = self.validate(color, space) # Convert to lingua franca if space in colorSpaces: self.valid = True setattr(self, space, color) else: self.valid = False raise ValueError("{} is not a valid color space.".format(space))
[docs] def render(self, space='rgb'): """Apply contrast to the base color value and return the adjusted color value. """ if space not in colorSpaces: raise ValueError(f"{space} is not a valid color space") # If value is cached, return it rather than doing calculations again if space in self._renderCache: return self._renderCache[space] # Transform contrast to match rgb contrast = self.contrast contrast = np.reshape(contrast, (-1, 1)) contrast = np.hstack((contrast, contrast, contrast)) # Multiply adj = np.clip(self.rgb * contrast, -1, 1) buffer = self.copy() buffer.rgb = adj self._renderCache[space] = getattr(buffer, space) return self._renderCache[space]
def __repr__(self): """If colour is printed, it will display its class and value. """ if self.valid: if self.named: return (f"<{self.__class__.__module__}." f"{self.__class__.__name__}: {self.named}, " f"alpha={self.alpha}>") else: return (f"<{self.__class__.__module__}." f"{self.__class__.__name__}: " f"{tuple(np.round(self.rgba, 2))}>") else: return (f"<{self.__class__.__module__}." f"{self.__class__.__name__}: Invalid>") def __bool__(self): """Determines truth value of object""" return self.valid def __len__(self): """Determines the length of object""" if len(self.rgb.shape) > 1: return self.rgb.shape[0] else: return int(bool(self.rgb.shape)) # -------------------------------------------------------------------------- # Rich comparisons # def __eq__(self, target): """`==` will compare RGBA values, rounded to 2dp""" if isinstance(target, Color): return np.all(np.round(target.rgba, 2) == np.round(self.rgba, 2)) elif target == None: return self._requested is None else: return False def __ne__(self, target): """`!=` will return the opposite of `==`""" return not self == target # -------------------------------------------------------------------------- # Operators # def __add__(self, other): buffer = self.copy() # If target is a list or tuple, convert it to an array if isinstance(other, (list, tuple)): other = np.array(other) # If target is a single number, add it to each rgba value if isinstance(other, (int, float)): buffer.rgba = self.rgba + other # If target is an array, add the arrays provided they are viable if isinstance(other, np.ndarray): if other.shape in [(len(self), 1), self.rgb.shape, self.rgba.shape]: buffer.rgba = self.rgba + other # If target is a Color object, add together the rgba values if isinstance(other, Color): if len(self) == len(other): buffer.rgba = self.rgba + other.rgba return buffer def __sub__(self, other): buffer = self.copy() # If target is a list or tuple, convert it to an array if isinstance(other, (list, tuple)): other = np.array(other) # If target is a single number, subtract it from each rgba value if isinstance(other, (int, float)): buffer.rgba = self.rgba - other # If target is an array, subtract the arrays provided they are viable if isinstance(other, np.ndarray): if other.shape in [(len(self), 1), self.rgb.shape, self.rgba.shape]: buffer.rgba = self.rgba - other # If target is a Color object, add together the rgba values if isinstance(other, Color): if len(self) == len(other): buffer.rgb = self.rgb - other.rgb return buffer # -------------------------------------------------------------------------- # Methods and properties #
[docs] def copy(self): """Return a duplicate of this colour""" return self.__copy__()
def __copy__(self): return self.__deepcopy__() def __deepcopy__(self): dupe = self.__class__( self._requested, self._requestedSpace, self.contrast) dupe.rgba = self.rgba dupe.valid = self.valid return dupe
[docs] def getReadable(self, contrast=4.5/21): """ Get a color which will stand out and be easily readable against this one. Useful for choosing text colors based on background color. Parameters ---------- contrast : float Desired perceived contrast between the two colors, between 0 (the same color) and 1 (as opposite as possible). Default is the w3c recommended minimum of 4.5/21 (dividing by 21 to adjust for sRGB units). Returns ------- colors.Color A contrasting color to this color. """ # adjust contrast to sRGB contrast *= 21 # get value as rgb1 rgb = self.rgb1 # convert to srgb srgb = rgb**2.2 * [0.2126, 0.7151, 0.0721] # apply contrast adjustment if np.sum(srgb) < 0.5: srgb = (srgb + 0.05) * contrast else: srgb = (srgb + 0.05) / contrast # convert back rgb = (srgb / [0.2126, 0.7151, 0.0721])**(1/2.2) # cap rgb = np.clip(rgb, 0, 1) # Return new color return Color(rgb, "rgb1")
@property def alpha(self): """How opaque (1) or transparent (0) this color is. Synonymous with `opacity`. """ return self._alpha @alpha.setter def alpha(self, value): # Treat 1x1 arrays as a float if isinstance(value, np.ndarray): if value.size == 1: value = float(value[0]) else: try: value = float(value) # If coercible to float, do so except (TypeError, ValueError) as err: raise TypeError( "Could not set alpha as value `{}` of type `{}`".format( value, type(value).__name__ ) ) value = np.clip(value, 0, 1) # Clip value(s) to within range # Set value self._alpha = value # Clear render cache self._renderCache = {} @property def contrast(self): if hasattr(self, "_contrast"): return self._contrast @contrast.setter def contrast(self, value): # Set value self._contrast = value # Clear render cache self._renderCache = {} @property def opacity(self): """How opaque (1) or transparent (0) this color is (`float`). Synonymous with `alpha`. """ return self.alpha @opacity.setter def opacity(self, value): self.alpha = value def _appendAlpha(self, space): # Get alpha, if necessary transform to an array of same length as color alpha = self.alpha if isinstance(alpha, (int, float)): if len(self) > 1: alpha = np.tile([alpha], (len(self), 1)) else: alpha = np.array([alpha]) if isinstance(alpha, np.ndarray) and len(self) > 1: alpha = alpha.reshape((len(self), 1)) # Get color color = getattr(self, space) # Append alpha to color return np.append(color, alpha, axis=1 if color.ndim > 1 else 0) #---spaces--- # Lingua franca is rgb @property def rgba(self): """Color value expressed as an RGB triplet from -1 to 1, with alpha values (0 to 1). """ return self._appendAlpha('rgb') @rgba.setter def rgba(self, color): self.rgb = color @property def rgb(self): """Color value expressed as an RGB triplet from -1 to 1. """ if not self.valid: return if hasattr(self, '_franca'): rgb = self._franca return rgb else: return np.array([0, 0, 0]) @rgb.setter def rgb(self, color): # Validate color, space = self.validate(color, space='rgb') if space != 'rgb': setattr(self, space, color) return # Set color self._franca = color # Clear outdated values from cache self._cache = {'rgb': color} self._renderCache = {} @property def rgba255(self): """Color value expressed as an RGB triplet from 0 to 255, with alpha value (0 to 1). """ return self._appendAlpha('rgb255') @rgba255.setter def rgba255(self, color): self.rgb255 = color @property def rgb255(self): """Color value expressed as an RGB triplet from 0 to 255. """ if not self.valid: return # Recalculate if not cached if 'rgb255' not in self._cache: self._cache['rgb255'] = np.round(255 * (self.rgb + 1) / 2) return self._cache['rgb255'] @rgb255.setter def rgb255(self, color): # Validate color, space = self.validate(color, space='rgb255') if space != 'rgb255': setattr(self, space, color) return # Iterate through values and do conversion self.rgb = 2 * (color / 255 - 0.5) # Clear outdated values from cache self._cache = {'rgb255': color} self._renderCache = {} @property def rgba1(self): """Color value expressed as an RGB triplet from 0 to 1, with alpha value (0 to 1). """ return self._appendAlpha('rgb1') @rgba1.setter def rgba1(self, color): self.rgb1 = color @property def rgb1(self): """Color value expressed as an RGB triplet from 0 to 1. """ if not self.valid: return # Recalculate if not cached if 'rgb1' not in self._cache: self._cache['rgb1'] = (self.rgb + 1) / 2 return self._cache['rgb1'] @rgb1.setter def rgb1(self, color): # Validate color, space = self.validate(color, space='rgb1') if space != 'rgb1': setattr(self, space, color) return # Iterate through values and do conversion self.rgb = 2 * (color - 0.5) # Clear outdated values from cache self._cache = {'rgb1': color} self._renderCache = {} @property def hex(self): """Color value expressed as a hex string. Can be a '#' followed by 6 values from 0 to F (e.g. #F2545B). """ if not self.valid: return if 'hex' not in self._cache: # Map rgb255 values to corresponding letters in hex hexmap = {10: 'a', 11: 'b', 12: 'c', 13: 'd', 14: 'e', 15: 'f'} # Handle arrays if self.rgb255.ndim > 1: rgb255 = self.rgb255 # Iterate through rows of rgb255 self._cache['hex'] = np.array([]) for row in rgb255: rowHex = '#' # Convert each value to hex and append for val in row: dig = hex(int(val)).strip('0x') rowHex += dig if len(dig) == 2 else '0' + dig # Append full hex value to new array self._cache['hex'] = np.append( self._cache['hex'], [rowHex], 0) else: rowHex = '#' # Convert each value to hex and append for val in self.rgb255: dig = hex(int(val))[2:] rowHex += dig if len(dig) == 2 else '0' + dig # Append full hex value to new array self._cache['hex'] = rowHex return self._cache['hex'] @hex.setter def hex(self, color): # Validate color, space = self.validate(color, space='hex') if space != 'hex': setattr(self, space, color) return if len(color) > 1: # Handle arrays rgb255 = np.array([""]) for row in color: if isinstance(row, np.ndarray): row = row[0] row = row.strip('#') # Convert string to list of strings hexList = [row[:2], row[2:4], row[4:6]] # Convert strings to int hexInt = [int(val, 16) for val in hexList] # Convert to array and append rgb255 = np.append(rgb255, np.array(hexInt), 0) else: # Handle single values if isinstance(color, np.ndarray): # Strip away any extraneous numpy layers color = color[(0,)*color.ndim] color = color.strip('#') # Convert string to list of strings hexList = [color[:2], color[2:4], color[4:6]] # Convert strings to int hexInt = [int(val, 16) for val in hexList] # Convert to array rgb255 = np.array(hexInt) # Set rgb255 accordingly self.rgb255 = rgb255 # Clear outdated values from cache self._cache = {'hex': color} self._renderCache = {} @property def named(self): """The name of this color, if it has one (`str`). """ if 'named' not in self._cache: self._cache['named'] = None # If alpha is 0, then we know that the color is None if isinstance(self.alpha, np.ndarray): invis = all(self.alpha == 0) elif isinstance(self.alpha, (int, float)): invis = self.alpha == 0 else: invis = False if invis: self._cache['named'] = 'none' return self._cache['named'] self._cache['named'] = np.array([]) # Handle array if len(self) > 1: for row in self.rgb: for name, val in colorNames.items(): if all(val[:3] == row): self._cache['named'] = np.append( self._cache['named'], [name], 0) continue self._cache['named'] = np.reshape(self._cache['named'], (-1, 1)) else: rgb = self.rgb for name, val in colorNames.items(): if name == 'none': # skip None continue if all(val[:3] == rgb): self._cache['named'] = name continue return self._cache['named'] @named.setter def named(self, color): # Validate color, space = self.validate(color=color, space='named') if space != 'named': setattr(self, space, color) return # Retrieve named colour if len(color) > 1: # Handle arrays for row in color: row = str(np.reshape(row, ())) # Enforce str if str(row).lower() in colorNames: self.rgb = colorNames[str(row).lower()] if row.lower() == 'none': self.alpha = 0 else: color = str(np.reshape(color, ())) # Enforce str if color.lower() in colorNames: self.rgb = colorNames[str(color).lower()] if color.lower() == 'none': self.alpha = 0 # Clear outdated values from cache self._cache = {'named': color} self._renderCache = {} @property def hsva(self): """Color value expressed as an HSV triplet, with alpha value (0 to 1). """ return self._appendAlpha('hsv') @hsva.setter def hsva(self, color): self.hsv = color @property def hsv(self): """Color value expressed as an HSV triplet. """ if 'hsva' not in self._cache: self._cache['hsv'] = ct.rgb2hsv(self.rgb) return self._cache['hsv'] @hsv.setter def hsv(self, color): # Validate color, space = self.validate(color=color, space='hsv') if space != 'hsv': setattr(self, space, color) return # Apply via rgba255 self.rgb = ct.hsv2rgb(color) # Clear outdated values from cache self._cache = {'hsv': color} self._renderCache = {} @property def lmsa(self): """Color value expressed as an LMS triplet, with alpha value (0 to 1). """ return self._appendAlpha('lms') @lmsa.setter def lmsa(self, color): self.lms = color @property def lms(self): """Color value expressed as an LMS triplet. """ if 'lms' not in self._cache: self._cache['lms'] = ct.rgb2lms(self.rgb) return self._cache['lms'] @lms.setter def lms(self, color): # Validate color, space = self.validate(color=color, space='lms') if space != 'lms': setattr(self, space, color) return # Apply via rgba255 self.rgb = ct.lms2rgb(color, self.conematrix) # Clear outdated values from cache self._cache = {'lms': color} self._renderCache = {} @property def dkla(self): """Color value expressed as a DKL triplet, with alpha value (0 to 1). """ return self._appendAlpha('dkl') @dkla.setter def dkla(self, color): self.dkl = color @property def dkl(self): """Color value expressed as a DKL triplet. """ if 'dkl' not in self._cache: raise NotImplementedError( "Conversion from rgb to dkl is not yet implemented.") return self._cache['dkl'] @dkl.setter def dkl(self, color): # Validate color, space = self.validate(color=color, space='dkl') if space != 'dkl': setattr(self, space, color) return # Apply via rgba255 self.rgb = ct.dkl2rgb(color, self.conematrix) # Clear outdated values from cache self._cache = {'dkl': color} self._renderCache = {} @property def dklaCart(self): """Color value expressed as a cartesian DKL triplet, with alpha value (0 to 1). """ return self.dklCart @dklaCart.setter def dklaCart(self, color): self.dklCart = color @property def dklCart(self): """Color value expressed as a cartesian DKL triplet. """ if 'dklCart' not in self._cache: self._cache['dklCart'] = ct.rgb2dklCart(self.rgb) return self._cache['dklCart'] @dklCart.setter def dklCart(self, color): # Validate color, space = self.validate(color=color, space='dklCart') if space != 'dkl': setattr(self, space, color) return # Apply via rgba255 self.rgb = ct.dklCart2rgb(color, self.conematrix) # Clear outdated values from cache self._cache = {'dklCart': color} self._renderCache = {} @property def srgb(self): """ Color value expressed as an sRGB triplet """ if 'srgb' not in self._cache: self._cache['srgb'] = ct.srgbTF(self.rgb) return self._cache['srgb'] @srgb.setter def srgb(self, color): # Validate color, space = self.validate(color=color, space='srgb') if space != 'srgb': setattr(self, space, color) return # Apply via rgba255 self.rgb = ct.srgbTF(color, reverse=True) # Clear outdated values from cache self._cache = {'srgb': color} self._renderCache = {}
# removing for now # @property # def rec709TF(self): # if 'rec709TF' not in self._cache: # self._cache['rec709TF'] = ct.rec709TF(self.rgb) # return self._cache['rec709TF'] # # @rec709TF.setter # def rec709TF(self, color): # # Validate # color, space = self.validate(color=color, space='rec709TF') # if space != 'rec709TF': # setattr(self, space, color) # return # # Apply via rgba255 # self.rgb = ct.rec709TF(color, reverse=True) # # Clear outdated values from cache # self._cache = {'rec709TF': color} # self._renderCache = {} # ------------------------------------------------------------------------------ # Legacy functions # # Old reference tables colors = colorNames # colorsHex = {key: Color(key, 'named').hex for key in colors} # colors255 = {key: Color(key, 'named').rgb255 for key in colors} # Old conversion functions
[docs]def hex2rgb255(hexColor): """Depreciated as of 2021.0 Converts a hex color string (e.g. "#05ff66") into an rgb triplet ranging from 0:255 """ col = Color(hexColor, 'hex') if len(hexColor.strip('#')) == 6: return col.rgb255 elif len(hexColor.strip('#')) == 8: return col.rgba255
[docs]def isValidColor(color, space='rgb'): """Depreciated as of 2021.0 """ logging.warning( "DEPRECIATED: While psychopy.colors.isValidColor will still roughly " "work, you should use a Color object, allowing you to check its " "validity simply by converting it to a `bool` (e.g. `bool(myColor)` or " "`if myColor:`). If you use this function for colors in any space " "other than hex, named or rgb, please specify the color space.") try: buffer = Color(color, space) return bool(buffer) except: return False
if __name__ == "__main__": pass

Back to top