Source code for colorlib

# """
# Copyright 2005, Ross Ihaka. All Rights Reserved.
# Ported to python: Copyright 2018, Reto Stauffer.
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 
#    1. Redistributions of source code must retain the above copyright notice,
#       this list of conditions and the following disclaimer.
# 
#    2. 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.
# 
#    3. The name of the Ross Ihaka may not be used to endorse or promote
#       products derived from this software without specific prior written
#       permission.
# 
# THIS SOFTWARE IS PROVIDED BY THE REGENTS 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 ROSS IHAKA 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.
# """


import sys
import numpy as np
import inspect

[docs]class colorlib(object): """The colorlib class is a collection of methods used to convert or transform colors between different color spaces.""" # No initialization method, but some constants are specified here # Often approximated as 903.3 */ # static const double self.KAPPA = 24389.0/27.0; KAPPA = 24389.0/27.0 # Often approximated as 0.08856 # static const double EPSILON = 216.0/24389.0; # Also, instead of the oft-used approximation 7.787 we use (self.KAPPA / 116) EPSILON = 216.0/24389.0 # Default white spot XN = np.asarray([ 95.047]) YN = np.asarray([100.000]) ZN = np.asarray([108.883]) # Conversion function
[docs] def DEG2RAD(self, x): """DEG2RAD(x) ParameterConver degrees into radiant. Parameters ---------- x : float or array of floats values in degrees Returns ------- float or float array Returns input ``x`` in radiant. """ return np.pi / 180. * x
# Conversion function
[docs] def RAD2DEG(self, x): """RAD2DEG(x) ParameterConver radiant to degrees. Parameters ---------- x : float or array of floats values in radiant Returns ------- float or array of floats Returns input ``x`` in degrees. """ return 180. / np.pi * x
def _get_white_(self, __fname__, n, XN = None, YN = None, ZN = None): """_get_white_(__fname__, n, XN = None, YN = None, ZN = None) For some color conversion functions the "white" definition (default white color) has to be specified. This function checks and prepares the XN, YN, ZN definition. Defaults are used if the user does not specify a custom white splot. If set, XN, YN, ZN have to be of type np.ndarray either of length one (will be expanded to length "n") or of length n. Parameters ---------- __fname__ : str string, name of the parent method, only used if errors are dropped. @TODO get rid of this thing and write a proper exception. n : int integer, number of colors to which NX, NY, NZ will be expanded XN : None, numpy.ndarray either None (default) or an nd.array of length one or length n. White point specification for dimension X YN : None, numpy.ndarray see XN. White point specification for dimension Y YZ : None, numpy.ndarray see XN. White point specification for dimension Z Returns ------- list Returns a list ``[XN, YN, ZN]`` with three ``numpy.ndarrays`` of length ``n``. If the inputs XN, YN, ZN (or some) were ``None``: take class defaults. """ # Take defaults if not further specified if not XN: XN = self.XN if not YN: YN = self.YN if not ZN: ZN = self.ZN if isinstance(XN,float): XN = np.asarray([XN]) if isinstance(YN,float): YN = np.asarray([YN]) if isinstance(ZN,float): ZN = np.asarray([ZN]) # Checking type if not np.all(isinstance(x, np.ndarray) for x in [XN, YN, ZN]): raise ValueError("Inputs to {:s} have to be of class np.ndarray.".format(__fname__)) # Expand if required if len(XN) == 1 and not len(XN) == n: XN = np.repeat(XN, n) if len(YN) == 1 and not len(YN) == n: YN = np.repeat(YN, n) if len(ZN) == 1 and not len(ZN) == n: ZN = np.repeat(ZN, n) # Check if all lengths match if not np.all([len(x) == n for x in [XN, YN, ZN]]): raise ValueError("Inputs XN/YN/ZN to {:s} have to be of the same length.".format(__fname__)) return [XN, YN, ZN] def _check_input_arrays_(self, __fname__, **kwargs): """__check_input_arrays__(__fname__, **kwargs) Checks if all inputs in ``kwargs`` are of type ``numpy.ndarray`` and of the same length. If not, the script will drop some error messsages and stop. Parameters ---------- __fname__ : str name of the method who called this check routine. Only used to drop a useful error message if required kwargs : ... named keywords, objects to be checked Returns ------- bool Returns ``True`` if everything is ok, else a ValueError will be raised. """ # Message will be dropped if problems occur msg = "Problem while checking inputs \"{:s}\" to method \"{:s}\":".format( ", ".join(kwargs.keys()), __fname__) from numpy import asarray lengths = [] for key,val in kwargs.items(): try: val = asarray(val) except Exception as e: raise ValueError("input {:s} to {:s}".format(key, self.__class__.__name__) + \ " could not have been converted to numpy.ndarray") # Else append length and proceed lengths.append(len(val)) # Check if all do have the same length if not np.all([x == lengths[0] for x in lengths]): msg += " Arguments of different lengths: {:s}".format( ", ".join(["{:s} = {:d}".format(kwargs.keys()[i],lengths[i]) \ for i in range(0,len(kwargs))])) raise ValueError(msg) return True # ------------------------------------------------------------------- # ------------------------------------------------------------------- # ------------------------------------------------------------------- # ------------------------------------------------------------------- # ----- CIE-XYZ <-> Device dependent RGB ----- # # Gamma Correction # # The following two functions provide gamma correction which # can be used to switch between sRGB and linearized sRGB (RGB). # # The standard value of gamma for sRGB displays is approximately 2.2, # but more accurately is a combination of a linear transform and # a power transform with exponent 2.4 # # gtrans maps linearized sRGB to sRGB. # ftrans provides the inverse map.
[docs] def gtrans(self, u, gamma): """gtrans(u, gamma) Gamma Correction. Function ``gtrans`` and ``ftrans`` provide gamma correction which can be used to switch between sRGB and linearized sRGB (RGB). The standard value of gamma for sRGB displays is approximately ``2.2``, but more accurately is a combination of a linear transform and a power transform with exponent ``2.4`` gtrans maps linearized sRGB to sRGB, ftrans provides the inverse map. Parameters ---------- u : numpy.ndarray float array of length N gamma : float or numpy.ndarray gamma value. If float or `numpy.ndarray` of length one gamma will be recycled (if length ``u > 1``) Returns ------- numpy.ndarray Same length as input ``u``. """ __fname__ = inspect.stack()[0][3] # Name of this method # Input check if isinstance(gamma, float): gamma = np.asarray([gamma]) if len(gamma) == 1 and not len(gamma) == len(u): gamma = np.repeat(gamma, len(u)) # Checking inputs self._check_input_arrays_(__fname__, u = u, gamma = gamma) # Transform for i,val in np.ndenumerate(u): if val > 0.00304: u[i] = 1.055 * np.power(val, (1. / gamma[i])) - 0.055 else: u[i] = 12.92 * val return u
[docs] def ftrans(self, u, gamma): """ftrans(u, gamma) Gamma Correction. Function :py:func:`gtrans` and :py:func:`ftrans` provide gamma correction between the RGB (device independent) and sRGB (device dependent) color space. The standard value of gamma for sRGB displays is approximately ``2.2``, but more accurately is a combination of a linear transform and a power transform with exponent ``2.4``. :py:func:`gtrans` maps linearized sRGB to sRGB, :py:func:`ftrans` provides the inverse map. Parameters ---------- u : numpy.ndarray float array of length N gamma : float or numpy.ndarray gamma value. If float or `numpy.ndarray` of length one gamma will be recycled (if length ``u > 1``) Returns ------- numpy.ndarray Same length as input ``u``. """ __fname__ = inspect.stack()[0][3] # Name of this method # Input check if isinstance(gamma, float): gamma = np.asarray([gamma]) if len(gamma) == 1 and not len(gamma) == len(u): gamma = np.repeat(gamma, len(u)) # Checking inputs self._check_input_arrays_(__fname__, u = u, gamma = gamma) # Transform for i,val in np.ndenumerate(u): if val > 0.03928: u[i] = np.power((val + 0.055) / 1.055, gamma[i]) else: u[i] = val / 12.92 return u
[docs] def DEVRGB_to_RGB(self, R, G, B, gamma = 2.4): """DEVRGB_to_RGB(R, G, B, gamma = 2.4) Device dependent sRGB to device independent RGB. Parameters ---------- R : numpy.ndarray indensities for red (``[0.,1.]``) G : numpy.ndarray indensities for green (``[0.,1.]``) B : numpy.ndarray indensities for blue (``[0.,1.]``) gamma : float gamma adjustment. Returns ------- list Returns a list of `numpy.ndarrays` with adjusted R, G, and B values. """ __fname__ = inspect.stack()[0][3] # Name of this method # Input check if isinstance(gamma, float): gamma = np.asarray([gamma]) if len(gamma) == 1 and not len(gamma) == len(R): gamma = np.repeat(gamma, len(R)) # Checking inputs self._check_input_arrays_(__fname__, R = R, G = G, B = B, gamma = gamma) # Apply gamma correction return [self.ftrans(x, gamma) for x in [R, G, B]]
[docs] def RGB_to_DEVRGB(self, R, G, B, gamma = 2.4): """DEVRGB_to_RGB(R, G, B, gamma = 2.4) Device independent RGB to device dependent sRGB. Parameters ---------- R : numpy.ndarray indensities for red (``[0.,1.]``). G : numpy.ndarray indensities for green (``[0.,1.]``). B : numpy.ndarray indensities for blue (``[0.,1.]``). gamma : float gamma adjustment. Returns ------- list Returns a list of `numpy.ndarrays` with adjusted R, G, and B values. """ __fname__ = inspect.stack()[0][3] # Name of this method # Input check if isinstance(gamma, float): gamma = np.asarray([gamma]) if len(gamma) == 1 and not len(gamma) == len(R): gamma = np.repeat(gamma, len(R)) # Checking inputs self._check_input_arrays_(__fname__, R = R, G = G, B = B, gamma = gamma) # Apply gamma correction return [self.gtrans(x, gamma) for x in [R, G, B]]
# ------------------------------------------------------------------- # ------------------------------------------------------------------- # ------------------------------------------------------------------- # ------------------------------------------------------------------- ## ----- CIE-XYZ <-> Device independent RGB ----- ## R, G, and B give the levels of red, green and blue as values ## in the interval [0,1]. X, Y and Z give the CIE chromaticies. ## XN, YN, ZN gives the chromaticity of the white point.
[docs] def RGB_to_XYZ(self, R, G, B, XN = None, YN = None, ZN = None): """RGB_to_XYZ(R, G, B, XN = None, YN = None, ZN = None) Device independent RGB to XYZ. R, G, and B give the levels of red, green and blue as values in the interval ``[0.,1.]``. X, Y and Z give the CIE chromaticies. Parameters ---------- R : numpy.ndarray indensities for red (``[0.,1.]``) G : numpy.ndarray indensities for green (``[0.,1.]``) B : numpy.ndarray indensities for blue (``[0.,1.]``) XN, YN, ZN : None or numpy.ndarray chromaticity of the white point. If of length 1 the white point specification will be recycled if length of R/G/B is larger than one. If not specified (all three ``None``) default values will be used Returns ------- list Returns corresponding X/Y/Z coordinates of CIE chromaticies, a list of `numpy.ndarray`'s of the same length as the inputs (``[X, Y, Z]``). """ __fname__ = inspect.stack()[0][3] # Name of this method n = len(R) # Number of colors # Loading definition of white [XN, YN, ZN] = self._get_white_(__fname__, n, XN, YN, ZN) # Checking input self._check_input_arrays_(__fname__, R = R, G = G, B = B) # TODO only YN is used as in the original code. Is this correct, or # correct by accident? return [YN * (0.412453 * R + 0.357580 * G + 0.180423 * B), # X YN * (0.212671 * R + 0.715160 * G + 0.072169 * B), # Y YN * (0.019334 * R + 0.119193 * G + 0.950227 * B)] # Z
[docs] def XYZ_to_RGB(self, X, Y, Z, XN = None, YN = None, ZN = None): """XYZ_to_RGB(X, Y, Z, XN = None, YN = None, ZN = None) CIEXYZ to device independent RGB. R, G, and B give the levels of red, green and blue as values in the interval ``[0.,1.]``. X, Y and Z give the CIE chromaticies. Parameters ---------- X : numpy.ndarray values for the Z dimension Y : numpy.ndarray values for the Z dimension Z : numpy.ndarray values for the Z dimension XN, YN, ZN : None or numpy.ndarray chromaticity of the white point. If of length 1 the white point specification will be recycled if length of R/G/B is larger than one. If not specified (all three ``None``) default values will be used Returns ------- list Returns corresponding X/Y/Z coordinates of CIE chromaticies, a list of `numpy.ndarray`'s of the same length as the inputs (``[R, G, B]``). """ __fname__ = inspect.stack()[0][3] # Name of this method n = len(X) # Number of colors # Loading definition of white [XN, YN, ZN] = self._get_white_(__fname__, n, XN, YN, ZN) # Checking input self._check_input_arrays_(__fname__, X = X, Y = Y, Z = Z) # TODO only YN is used as in the original code. Is this correct, or # correct by accident? return [( 3.240479 * X - 1.537150 * Y - 0.498535 * Z) / YN, # R (-0.969256 * X + 1.875992 * Y + 0.041556 * Z) / YN, # G ( 0.055648 * X - 0.204043 * Y + 1.057311 * Z) / YN] # B
# ------------------------------------------------------------------- # ------------------------------------------------------------------- # ------------------------------------------------------------------- # ------------------------------------------------------------------- ## ----- CIE-XYZ <-> sRGB ----- ## R, G, and B give the levels of red, green and blue as values ## in the interval [0,1]. X, Y and Z give the CIE chromaticies. ## XN, YN, ZN gives the chromaticity of the white point.
[docs] def sRGB_to_XYZ(self, R, G, B, XN = None, YN = None, ZN = None): """sRGB_to_XYZ(R, G, B, XN = None, YN = None, ZN = None) sRGB to CIEXYZ. R, G, and B give the levels of red, green and blue as values in the interval ``[0.,1.]``. X, Y and Z give the CIE chromaticies. Parameters ---------- R : numpy.ndarray indensities for red (``[0.,1.]``) G : numpy.ndarray indensities for green (``[0.,1.]``) B : numpy.ndarray indensities for blue (``[0.,1.]``) XN, YN, ZN : None or numpy.ndarray chromaticity of the white point. If of length 1 the white point specification will be recycled if length of R/G/B is larger than one. If not specified (all three ``None``) default values will be used Returns ------- list Returns corresponding X/Y/Z coordinates of CIE chromaticies, a list of `numpy.ndarray`'s of the same length as the inputs (``[X, Y, Z]``). """ __fname__ = inspect.stack()[0][3] # Name of this method n = len(R) # Number of colors # Loading definition of white [XN, YN, ZN] = self._get_white_(__fname__, n, XN, YN, ZN) # Checking input self._check_input_arrays_(__fname__, R = R, G = G, B = B) # Transform R/G/B R = self.ftrans(R, 2.4) G = self.ftrans(G, 2.4) B = self.ftrans(B, 2.4) # Convert to X/Y/Z coordinates return[YN * (0.412453 * R + 0.357580 * G + 0.180423 * B), # X YN * (0.212671 * R + 0.715160 * G + 0.072169 * B), # Y YN * (0.019334 * R + 0.119193 * G + 0.950227 * B)] # Z
[docs] def XYZ_to_sRGB(self, X, Y, Z, XN = None, YN = None, ZN = None): """XYZ_to_sRGB(X, Y, Z, XN = None, YN = None, ZN = None) CIEXYZ to sRGB. R, G, and B give the levels of red, green and blue as values in the interval ``[0.,1.]``. X, Y and Z give the CIE chromaticies. Parameters ---------- X : numpy.ndarray values for the Z dimension Y : numpy.ndarray values for the Z dimension Z : numpy.ndarray values for the Z dimension XN, YN, ZN : None or numpy.ndarray chromaticity of the white point. If of length 1 the white point specification will be recycled if length of R/G/B is larger than one. If not specified (all three NA) default values will be used Returns ------- list Returns corresponding X/Y/Z coordinates of CIE chromaticies, a list of `numpy.ndarray`'s of the same length as the inputs (``[R, G, B]``). """ __fname__ = inspect.stack()[0][3] # Name of this method n = len(X) # Number of colors # Loading definition of white [XN, YN, ZN] = self._get_white_(__fname__, n, XN, YN, ZN) # Checking input self._check_input_arrays_(__fname__, X = X, Y = Y, Z = Z) # Transform and return return [self.gtrans(( 3.240479 * X - 1.537150 * Y - 0.498535 * Z) / YN, 2.4), # R self.gtrans((-0.969256 * X + 1.875992 * Y + 0.041556 * Z) / YN, 2.4), # G self.gtrans(( 0.055648 * X - 0.204043 * Y + 1.057311 * Z) / YN, 2.4)] # B
# ------------------------------------------------------------------- # ------------------------------------------------------------------- # ------------------------------------------------------------------- # ------------------------------------------------------------------- ## ----- CIE-XYZ <-> CIE-LAB ----- */
[docs] def LAB_to_XYZ(self, L, A, B, XN = None, YN = None, ZN = None): """LAB_to_XYZ(L, A, B, XN = None, YN = None, ZN = None) CIELAB to CIEXYZ. Parameters ---------- L : numpy.ndarray values for the L dimension A : numpy.ndarray values for the A dimension B : numpy.ndarray values for the B dimension XN, YN, ZN : None or numpy.ndarray chromaticity of the white point. If of length 1 the white point specification will be recycled if length of R/G/B is larger than one. If not specified (all three NA) default values will be used Returns ------- list Returns corresponding X/Y/Z coordinates of CIE chromaticies, a list of `numpy.ndarray`'s of the same length as the inputs (``[X, Y, Z]``). """ __fname__ = inspect.stack()[0][3] # Name of this method n = len(L) # Number of colors # Loading definition of white [XN, YN, ZN] = self._get_white_(__fname__, n, XN, YN, ZN) # Checking input self._check_input_arrays_(__fname__, L = L, A = A, B = B) # Result arrays X = np.ndarray(len(L), dtype = "float"); X[:] = 0. Y = np.ndarray(len(L), dtype = "float"); Y[:] = 0. Z = np.ndarray(len(L), dtype = "float"); Z[:] = 0. # Calculate Y for i,val in np.ndenumerate(L): if val <= 0: Y[i] = 0. elif val <= 8.0: Y[i] = val * YN[i] / self.KAPPA elif val <= 100.: Y[i] = YN[i] * np.power((val + 16.) / 116., 3.) else: Y[i] = YN[i] fy = np.ndarray(len(Y), dtype = "float") for i,val in np.ndenumerate(Y): if val <= (self.EPSILON * YN[i]): fy[i] = (self.KAPPA / 116.) * val / YN[i] + 16. / 116. else: fy[i] = np.power(val / YN[i], 1. / 3.) # Calculate X fx = fy + (A / 500.) for i,val in np.ndenumerate(fx): if np.power(val, 3.) <= self.EPSILON: X[i] = XN[i] * (val - 16. / 116.) / (self.KAPPA / 116.) else: X[i] = XN[i] * np.power(val, 3.) # Calculate Z fz = fy - (B / 200.) for i,val in np.ndenumerate(fz): if np.power(val, 3.) <= self.EPSILON: Z[i] = ZN[i] * (val - 16. / 116.) / (self.KAPPA / 116.) else: Z[i] = ZN[i] * np.power(val, 3) return [X, Y, Z]
[docs] def XYZ_to_LAB(self, X, Y, Z, XN = None, YN = None, ZN = None): """XYZ_to_LAB(X, Y, Z, XN = None, YN = None, ZN = None) CIEXYZ to CIELAB. Parameters ---------- X : numpy.ndarray values for the X dimension Y : numpy.ndarray values for the Y dimension Z : numpy.ndarray values for the Z dimension XN, YN, ZN : None or numpy.ndarray chromaticity of the white point. If of length 1 the white point specification will be recycled if length of R/G/B is larger than one. If not specified (all three NA) default values will be used Returns ------- list Returns corresponding L/A/B coordinates, a list of `numpy.ndarray`'s of the same length as the inputs (``[L, A, B]``). """ __fname__ = inspect.stack()[0][3] # Name of this method n = len(X) # Number of colors # Loading definition of white [XN, YN, ZN] = self._get_white_(__fname__, n, XN, YN, ZN) # Checking input self._check_input_arrays_(__fname__, X = X, Y = Y, Z = Z) # Support function def f(t, KAPPA, EPSILON): for i,val in np.ndenumerate(t): if val > EPSILON: t[i] = np.power(val, 1./3.) else: t[i] = (KAPPA / 116.) * val + 16. / 116. return t # Scaling xr = X / XN; yr = Y / YN; zr = Z / ZN; # Calculate L L = np.ndarray(len(X), dtype = "float"); L[:] = 0. for i,val in np.ndenumerate(yr): if val > self.EPSILON: L[i] = 116. * np.power(val, 1./3.) - 16. else: L[i] = self.KAPPA * val xt = f(xr, self.KAPPA, self.EPSILON); yt = f(yr, self.KAPPA, self.EPSILON); zt = f(zr, self.KAPPA, self.EPSILON); return [L, 500. * (xt - yt), 200. * (yt - zt)] # [L, A, B]
# ------------------------------------------------------------------- # ------------------------------------------------------------------- # ------------------------------------------------------------------- # -------------------------------------------------------------------
[docs] def XYZ_to_HLAB(self, X, Y, Z, XN = None, YN = None, ZN = None): """XYZ_to_HLAB(X, Y, Z, XN = None, YN = None, ZN = None) CIE-XYZ to Hunter LAB. .. note:: Note that the Hunter LAB is no longer part of the public API, but the code is still here in case needed. Parameters ---------- X : numpy.ndarray values for the Z dimension Y : numpy.ndarray values for the Z dimension Z : numpy.ndarray values for the Z dimension XN, YN, ZN : None or numpy.ndarray chromaticity of the white point. If of length 1 the white point specification will be recycled if length of R/G/B is larger than one. If not specified (all three ``None``) default values will be used Returns ------- list Returns corresponding Hunter LAB chromaticies, a list of `numpy.ndarray`'s of the same length as the inputs (``[L, A, B]``). """ __fname__ = inspect.stack()[0][3] # Name of this method n = len(X) # Number of colors # Loading definition of white [XN, YN, ZN] = self._get_white_(__fname__, n, XN, YN, ZN) # Checking input self._check_input_arrays_(__fname__, X = X, Y = Y, Z = Z) # Transform X = X / XN; Y = Y / YN; Z = Z / ZN; l = np.sqrt(Y); return [10. * l, 17.5 * (((1.02 * X) - Y) / l), 7. * ((Y - (0.847 * Z)) / l)] # [L, A, B]
[docs] def HLAB_to_XYZ(self, L, A, B, XN = None, YN = None, ZN = None): """HLAB_to_XYZ(L, A, B, XN = None, YN = None, ZN = None) Hunter LAB to CIE-XYZ. .. note:: Note that the Hunter LAB is no longer part of the public API, but the code is still here in case needed. Parameters ---------- L : numpy.ndarray values for the L dimension A : numpy.ndarray values for the A dimension B : numpy.ndarray values for the B dimension XN, YN, ZN : None or numpy.ndarray chromaticity of the white point. If of length 1 the white point specification will be recycled if length of R/G/B is larger than one. If not specified (all three NA) default values will be used Returns ------- list Returns corresponding CIE-XYZ chromaticies, a list of `numpy.ndarray`'s of the same length as the inputs (``[X, Y, Z]``). """ __fname__ = inspect.stack()[0][3] # Name of this method n = len(L) # Number of colors # Loading definition of white [XN, YN, ZN] = self._get_white_(__fname__, n, XN, YN, ZN) # Checking input self._check_input_arrays_(__fname__, L = L, A = A, B = B) # Transform vY = L / 10.; vX = (A / 17.5) * (L / 10); vZ = (B / 7) * (L / 10); vY = vY * vY; Y = vY * XN X = (vX + vY) / 1.02 * YN Z = - (vZ - vY) / 0.847 * ZN return [X, Y, Z]
# ------------------------------------------------------------------- # ------------------------------------------------------------------- # ------------------------------------------------------------------- # -------------------------------------------------------------------
[docs] def LAB_to_polarLAB(self, L, A, B): """LAB_to_polarLAB(L, A, B) Convert from CIELAB to the polar representation polarLAB. Parameters ---------- L : numpy.ndarray values for the L dimension of the CIELAB color space A : numpy.ndarray values for the A dimension of the CIELAB color space B : numpy.ndarray values for the B dimension of the CIELAB color space Returns ------- list Returns corresponding polar LAB chromaticies, a list of `numpy.ndarray`'s of the same length as the inputs (``[L, A, B]``). """ __fname__ = inspect.stack()[0][3] # Name of this method # Checking input self._check_input_arrays_(__fname__, L = L, A = A, B = B) # Compute H H = self.RAD2DEG(np.arctan2(B, A)) for i,val in np.ndenumerate(H): while val > 360.: val -= 360. while val < 0.: val += 360. H[i] = val # Compute C C = np.sqrt(A * A + B * B) return [L, C, H]
[docs] def polarLAB_to_LAB(self, L, C, H): """polarLAB_to_LAB(L, A, B) Convert form polarLAB to onvert CIELAB. Parameters ---------- L : numpy.ndarray values for the L dimension of the polar LAB color space A : numpy.ndarray values for the A dimension of the polar LAB color space B : numpy.ndarray values for the B dimension of the polar LAB color space Returns ------- list Returns corresponding CIELAB chromaticies, a list of `numpy.ndarray`'s of the same length as the inputs (``[L, A, B]``). """ __fname__ = inspect.stack()[0][3] # Name of this method # Checking input self._check_input_arrays_(__fname__, L = L, C = C, H = H) A = np.cos(self.DEG2RAD(H)) * C B = np.sin(self.DEG2RAD(H)) * C return [L, A, B]
# ------------------------------------------------------------------- # ------------------------------------------------------------------- # ------------------------------------------------------------------- # -------------------------------------------------------------------
[docs] def RGB_to_HSV(self, r, g, b): """RGB_to_HSV(r, g, b) Convert RGB to HSV. Parameters ---------- r : numpy.ndarray intensities for red (``[0.,1.]``) g : numpy.ndarray intensities for green (``[0.,1.]``) b : numpy.ndarray intensities for blue (``[0.,1.]``) Returns ------- list Returns a `numpy.ndarray` with the corresponding coordinates in the HSV color space (``[h, s, v]``). Same length as the inputs. """ __fname__ = inspect.stack()[0][3] # Name of this method # Checking input self._check_input_arrays_(__fname__, r = r, g = g, b = b) # Support function def gethsv(r, g, b): x = np.min([r, g, b]) y = np.max([r, g, b]) if y != x: f = g - b if r == x else b - r if g == x else r - g i = 3. if r == x else 5. if g == x else 1. h = 60. * (i - f /(y - x)) s = (y - x)/y v = y else: ####ifdef MONO ### *h = NA_REAL; *s = 0; *v = y; ####else ### *h = 0; *s = 0; *v = y; ####endif h = 0. s = 0. v = y return [h, s, v] # Result arrays h = np.ndarray(len(r), dtype = "float"); h[:] = 0. s = np.ndarray(len(r), dtype = "float"); s[:] = 0. v = np.ndarray(len(r), dtype = "float"); v[:] = 0. # Calculate h/s/v for i in range(0, len(r)): tmp = gethsv(r[i], g[i], b[i]) h[i] = tmp[0]; s[i] = tmp[1]; v[i] = tmp[2] return [h, s, v]
[docs] def HSV_to_RGB(self, h, s, v): """HSV_to_RGB(r, g, b) Convert RGB to HSV. Parameters ---------- h : nympy.ndarray hue values s : numpy.ndarray saturation v : numpy.ndarray value (the value-dimension of HSV) Returns ------- list Returns a `numpy.ndarray` with the corresponding coordinates in the RGB color space (``[r, g, b]``). Same length as the inputs. """ __fname__ = inspect.stack()[0][3] # Name of this method # Checking input self._check_input_arrays_(__fname__, h = h, s = s, v = v) # Support function def getrgb(h, s, v): # If Hue is not defined: if h == np.nan: return np.repeat(v, 3) # Convert to [0-6] h = h / 60. i = np.floor(h) f = h - i if (i % 2) == 0: # if i is even f = 1 - f m = v * (1 - s) n = v * (1 - s * f) if i in [0,6]: return [v, n, m] elif i == 1: return [n, v, m] elif i == 2: return [m, v, n] elif i == 3: return [m, n, v] elif i == 4: return [n, m, v] elif i == 5: return [v, m, n] else: import sys; sys.exit("Ended up in a non-defined ifelse with i = %d".format(i)) # Result arrays r = np.ndarray(len(h), dtype = "float"); r[:] = 0. g = np.ndarray(len(h), dtype = "float"); g[:] = 0. b = np.ndarray(len(h), dtype = "float"); b[:] = 0. for i in range(0,len(h)): tmp = getrgb(h[i], s[i], v[i]) r[i] = tmp[0]; g[i] = tmp[1]; b[i] = tmp[2] return [r, g, b]
# ------------------------------------------------------------------- # ------------------------------------------------------------------- # ------------------------------------------------------------------- # -------------------------------------------------------------------
[docs] def RGB_to_HLS(self, r, g, b): """RGB_to_HLS(r, g, b) Convert RGB to HLS. All r/g/b values in ``[0.,1.]``, h in ``[[0., 360.]``, l and s in ``[0., 1.]``. From: http://wiki.beyondunreal.com/wiki/RGB_To_HLS_Conversion. Parameters ---------- r : numpy.ndarray intensities for red (``[0.,1.]``) g : numpy.ndarray intensities for green (``[0.,1.]``) b : numpy.ndarray intensities for blue (``[0.,1.]``) Returns ------- list Returns a `numpy.ndarray` with the corresponding coordinates in the HLS color space (``[h, l, s]``). Same length as the inputs. """ __fname__ = inspect.stack()[0][3] # Name of this method # Checking input self._check_input_arrays_(__fname__, r = r, g = g, b = b) # Support function def gethls(r, g, b): min = np.min([r, g, b]) max = np.max([r, g, b]) l = (max + min)/2.; if max != min: if l < 0.5: s = (max - min) / (max + min) elif l >= 0.5: s = (max - min) / (2. - max - min) if r == max: h = (g - b) / (max - min); if g == max: h = 2. + (b - r) / (max - min); if b == max: h = 4. + (r - g) / (max - min); h = h * 60.; if h < 0.: h = h + 360.; if h > 360.: h = h - 360.; else: s = 0 ####ifdef MONO ### *h = NA_REAL; ####else ### *h = 0; ####endif h = 0; return [h, l, s] # Result arrays h = np.ndarray(len(r), dtype = "float"); h[:] = 0. l = np.ndarray(len(r), dtype = "float"); l[:] = 0. s = np.ndarray(len(r), dtype = "float"); s[:] = 0. for i in range(0,len(h)): tmp = gethls(r[i], g[i], b[i]) h[i] = tmp[0]; l[i] = tmp[1]; s[i] = tmp[2] return [h, l, s]
[docs] def HLS_to_RGB(self, h, l, s): """HLS_to_RGB(h, l, s) Convert RLS to HLS. All r/g/b values in ``[0.,1.]``, h in ``[[0., 360.]``, l and s in ``[0., 1.]``. From: http://wiki.beyondunreal.com/wiki/RGB_To_HLS_Conversion. Parameters ---------- h : nympy.ndarray hue values l : numpy.ndarray lightness s : numpy.ndarray saturation Returns ------- list Returns a `numpy.ndarray` with the corresponding coordinates in the RGB color space (``[r, g, b]``). Same length as the inputs. """ __fname__ = inspect.stack()[0][3] # Name of this method # Checking input self._check_input_arrays_(__fname__, h = h, l = l, s = s) # Support function qtrans def qtrans(q1, q2, hue): if hue > 360.: hue = hue - 360. if hue < 0: hue = hue + 360. if hue < 60.: return q1 + (q2 - q1) * hue / 60. elif hue < 180.: return q2 elif hue < 240.: return q1 + (q2 - q1) * (240. - hue) / 60. else: return q1 # Support function def getrgb(h, l, s): p2 = l * (1. + s) if l <= 0.5 else 1 + s - (l * s) p1 = 2 * l - p2 # If saturation is zero if (s == 0): return np.repeat(l, 3) # Else return [qtrans(p1, p2, h + 120.), # r qtrans(p1, p2, h), # g qtrans(p1, p2, h - 120.)] # b # Result arrays r = np.ndarray(len(h), dtype = "float"); r[:] = 0. g = np.ndarray(len(h), dtype = "float"); g[:] = 0. b = np.ndarray(len(h), dtype = "float"); b[:] = 0. for i in range(0,len(r)): tmp = getrgb(h[i], l[i], s[i]) r[i] = tmp[0]; g[i] = tmp[1]; b[i] = tmp[2] return [r, g, b]
# ------------------------------------------------------------------- # ------------------------------------------------------------------- # ------------------------------------------------------------------- # -------------------------------------------------------------------
[docs] def XYZ_to_uv(self, X, Y, Z): """XYZ_to_uv(X, Y, Z) CIE-XYZ to u and v. Parameters ---------- X : numpy.ndarray values for the Z dimension. Y : numpy.ndarray values for the Y dimension. Z : numpy.ndarray values for the Z dimension. Returns ------- list Returns a list of `numpy.ndarrays` containing u and v (``[u, v]``). """ __fname__ = inspect.stack()[0][3] # Name of this method # Checking input self._check_input_arrays_(__fname__, X = X, Y = Y, Z = Z) # Result array x = np.ndarray(len(X), dtype = "float"); x[:] = 0. y = np.ndarray(len(X), dtype = "float"); y[:] = 0. t = X + Y + Z idx = np.where(t != 0) x[idx] = X[idx] / t[idx]; y[idx] = Y[idx] / t[idx]; return [2.0 * x / (6. * y - x + 1.5), # u 4.5 * y / (6. * y - x + 1.5)] # v
[docs] def XYZ_to_LUV(self, X, Y, Z, XN = None, YN = None, ZN = None): """XYZ_to_LUV(X, Y, Z, XN = None, YN = None, ZN = None) CIE-XYZ to CIE-LUV. Parameters ---------- X : numpy.ndarray values for the X dimension Y : numpy.ndarray values for the Y dimension Z : numpy.ndarray values for the Z dimension XN, YN, ZN : None or numpy.ndarray chromaticity of the white point. If of length 1 the white point specification will be recycled if length of R/G/B is larger than one. If not specified (all three NA) default values will be used. Returns ------- list Returns a list of CIELUV coordinates (``[L, U, V]``) with the same length as the input arrays. """ __fname__ = inspect.stack()[0][3] # Name of this method n = len(X) # Number of colors # Loading definition of white [XN, YN, ZN] = self._get_white_(__fname__, n, XN, YN, ZN) # Checking input self._check_input_arrays_(__fname__, X = X, Y = Y, Z = Z) # Convert X/Y/Z and XN/YN/ZN to uv [u, v] = self.XYZ_to_uv(X, Y, Z ) [uN, vN] = self.XYZ_to_uv(XN, YN, ZN) # Calculate L L = np.ndarray(len(X), dtype = "float"); L[:] = 0. y = Y / YN for i,val in np.ndenumerate(y): L[i] = 116. * np.power(val, 1./3.) - 16. if val > self.EPSILON else self.KAPPA * val # Calculate U/V return [L, 13. * L * (u - uN), 13. * L * (v - vN)] # [L, U, V]
[docs] def LUV_to_XYZ(self, L, U, V, XN = None, YN = None, ZN = None): """LUV_to_XYZ(L, U, V, XN = None, YN = None, ZN = None) CIE-LUV to CIE-XYZ. Parameters ---------- L : numpy.ndarray values for the L dimension U : numpy.ndarray values for the U dimension V : numpy.ndarray values for the V dimension XN, YN, ZN : None or numpy.ndarray chromaticity of the white point. If of length 1 the white point specification will be recycled if length of R/G/B is larger than one. If not specified (all three NA) default values will be used Returns ------- list Returns a list of CIE-XYZ coordinates (``[X, Y, Z]``) with the same length as the input arrays. """ __fname__ = inspect.stack()[0][3] # Name of this method n = len(L) # Number of colors # Loading definition of white [XN, YN, ZN] = self._get_white_(__fname__, n, XN, YN, ZN) # Checking input self._check_input_arrays_(__fname__, L = L, U = U, V = V) # Result arrays X = np.ndarray(len(L), dtype = "float"); X[:] = 0. Y = np.ndarray(len(L), dtype = "float"); Y[:] = 0. Z = np.ndarray(len(L), dtype = "float"); Z[:] = 0. # Check for which values we do have to do the transformation def fun(L, U, V): return False if L <= 0. and U == 0. and V == 0. else True idx = np.where([fun(L[i], U[i], V[i]) for i in range(0, len(L))])[0] if len(idx) == 0: return [X, Y, Z] # Compute Y for i in idx: Y[i] = YN[i] * (np.power((L[i] + 16.)/116., 3.) if L[i] > 8. else L[i] / self.KAPPA) # Calculate X/Z from numpy import finfo, fmax # Avoiding division by zero eps = np.finfo(float).eps*10 L = fmax(eps, L) [uN, vN] = self.XYZ_to_uv(XN, YN, ZN) u = U / (13. * L) + uN v = V / (13. * L) + vN X = 9.0 * Y * u / (4 * v) Z = -X / 3. - 5. * Y + 3. * Y / v return [X, Y, Z]
## ----- LUV <-> polarLUV ----- */
[docs] def LUV_to_polarLUV(self, L, U, V): """LUV_to_polarLUV(L, U, V) LUV to polarLUV (HCL). Parameters ---------- L : numpy.ndarray values for the X dimension U : numpy.ndarray values for the Y dimension V : numpy.ndarray values for the Z dimension Returns ------- list Returns a list of polarLUV or HCL coordinates (``[L, C, H]``) with the same length as the input arrays. The HCL color space is simply the polar representation of the CIE-LUV color space. """ __fname__ = inspect.stack()[0][3] # Name of this method self._check_input_arrays_(__fname__, L = L, U = U, V = V) # Calculate polarLUV coordinates C = np.sqrt(U * U + V * V) H = self.RAD2DEG(np.arctan2(V, U)) for i,val in np.ndenumerate(H): while val > 360: val -= 360. while val < 0.: val += 360. H[i] = val return [L, C, H]
[docs] def polarLUV_to_LUV(self, L, C, H): """polarLUV_to_LUV(L, C, H) polarLUV (HCL) to LUV. Parameters ---------- L : numpy.ndarray values for the L or luminance dimension C : numpy.ndarray values for the C or chroma dimension H : numpy.ndarray values for the H or hue dimension Returns ------- list Returns a list of polarLUV or HCL coordinates (``[L, U, V]``) with the same length as the input arrays. """ __fname__ = inspect.stack()[0][3] # Name of this method # Checking input self._check_input_arrays_(__fname__, L = L, C = C, H = H) H = self.DEG2RAD(H) return [L, C * np.cos(H), C * np.sin(H)] # [L, U, V]
[docs] def sRGB_to_hex(self, r, g, b, fixup = True): """sRGB_to_hex(r, g, , fixup = True) sRGB colors to hex colors. Parameters ---------- r : numpy.ndarray intensities for red (``[0.,1.,]``) g : numpy.ndarray intensities for green (``[0.,1.,]``) b : numpy.ndarray intensities for blue (``[0.,1.,]``) fixup : bool whether or not the rgb values should be corrected if they lie outside the defined RGB space (outside ``[0.,1.,]``) Returns ------- list A list with hex colors as strings. """ # Color fixup: limit r/g/b to [0-1] def rgbfixup(r, g, b): def fun(x): return np.asarray([np.max([0, np.min([1, e])]) \ if np.isfinite(e) else np.nan for e in x]) return [fun(r), fun(g), fun(b)] def rgbcleanup(r, g, b): def fun(x): return np.asarray([e if np.logical_and(e >= 0., e <= 1.) else np.nan for e in x]) return [fun(r), fun(g), fun(b)] # Checking which r/g/b values are outside limits. # This only happens if fixup = FALSE. def validrgb(r, g, b): idxr = np.isfinite(r) idxg = np.isfinite(g) idxb = np.isfinite(b) return np.where(idxr * idxg * idxb)[0] # Support function to create hex coded colors def gethex(r, g, b): # Converts integers to hex string def applyfun(x): x = np.asarray(x * 255. + .5, dtype = np.int) return "#{:02x}{:02x}{:02x}".format(x[0], x[1], x[2]).upper() h = np.vstack([r,g,b]).transpose().flatten().reshape([len(r),3]) return np.apply_along_axis(applyfun, 1, h) # Let's do the conversion! if fixup: [r, g, b] = rgbfixup(r, g, b) else: [r, g, b] = rgbcleanup(r, g, b) # Create return array res = np.ndarray(len(r), dtype = "|S7"); res[:] = "" # Check valid r/g/b coordinates valid = validrgb(r,g,b) if len(valid) > 0: # Convert valid colors to hex res[valid] = gethex(r[valid], g[valid], b[valid]) # Create return list with NAN's for invalid colors res = [np.nan if len(x) == 0 else x.decode() for x in res] return res;
# RETO RETO RETO
[docs] def hex_to_sRGB(self, hex_, gamma = 2.4): """hex_to_sRGB(hex_, gamma = 2.4) Convert hex colors to sRGB. Parameters ---------- hex_ : str, list of str hex strings. gamma : float gamma correction factor. Returns ------- list Returns a list of numpy.ndarrays with the corresponding red, green, and blue intensities (``[0.,1.]``). """ if isinstance(hex_,str): hex_ = [hex_] hex_ = np.asarray(hex_) # Check for valid hex colors def validhex(hex_): from re import match ##return np.where([match("^#[A-Za-z0-9]{6}([0-9]{2})?$", x) is not None for x in hex_])[0] return np.where([match("^#[0-9A-Fa-f]{6}(o-9]{2})?$$", x) is not None for x in hex_])[0] # Convert hex to rgb def getrgb(x): def applyfun(x): return np.asarray([int(x[i:i+2], 16) for i in (1, 3 ,5)]) rgb = [applyfun(e) for e in x] rgb = np.vstack(rgb).transpose().flatten().reshape([3,len(x)]) return [rgb[0] / 255., rgb[1] / 255., rgb[2] / 255.] # Result arrays r = np.ndarray(len(hex_), dtype = "float"); r[:] = np.nan g = np.ndarray(len(hex_), dtype = "float"); g[:] = np.nan b = np.ndarray(len(hex_), dtype = "float"); b[:] = np.nan # Check valid hex colors valid = validhex(hex_) if not len(valid) == 0: # Decode valid hex strings rgb = getrgb(hex_[valid]) r[valid] = rgb[0] g[valid] = rgb[1] b[valid] = rgb[2] return [r, g, b]
# ------------------------------------------------------------------- # Color object base class # will be extended by the different color classes. # -------------------------------------------------------------------
[docs]class colorobject(object): """ This is the base class of all color objects and provides some default methods. """ # Allowed/defined color spaces ALLOWED = ["CIEXYZ", "CIELUV", "CIELAB", "polarLUV", "polarLAB", "RGB", "sRGB", "HCL", "HSV", "HLS", "hex"] # Used to store alpha if needed. Will only be used for some of # the colorobject objects as only few color spaces allow alpha # values. ALPHA = None # GAMMA GAMMA = 2.4 # Used to adjust RGB (DEVRGB_to_RGB and back). # Standard representation of colorspace colorobject objects. def __repr__(self, digits = 2): """__repr__(digits = 2) Standard representation of the :py:class:`colorobject` objects. Returns ------- str Returns a string of the colors/coordinates of the current object. """ dims = list(self._data_.keys()) # Dimensions # Sorting the dimensions from re import match if match("^(R|G|B|alpha){3,4}$", "".join(dims)): dims = ["R", "G", "B"] elif match("^(L|A|B|alpha){3,4}$", "".join(dims)): dims = ["L", "A", "B"] elif match("^(L|U|V|alpha){3,4}$", "".join(dims)): dims = ["L", "U", "V"] elif match("^(H|C|L|alpha){3,4}$", "".join(dims)): dims = ["H", "C", "L"] elif match("^(X|Y|Z|alpha){3,4}$", "".join(dims)): dims = ["X", "Y", "Z"] elif match("^(H|S|V|alpha){3,4}$", "".join(dims)): dims = ["H", "S", "V"] elif match("^(H|L|S|alpha){3,4}$", "".join(dims)): dims = ["H", "L", "S"] if self.hasalpha(): dims.append("alpha") elif "alpha" in dims: del dims[dims.index("alpha")] # Number of colors ncol = len(self._data_[dims[0]]) # Start creating the string: res = ["{:s} color object ({:d} colors)".format(self.__class__.__name__, ncol)] # Show header fmt = "".join(["{:>", "{:d}".format(digits+6), "s}"]) res.append(" " + "".join([fmt.format(x) for x in dims])) # Show data # In case of a hexcols object: string formatting and # nan-replacement beforehand. if self.__class__.__name__ == "hexcols": data = {} fmt = "".join(["{:", "{:d}.{:d}".format(6+digits, 3), "f}"]) for d in dims: ##self._data_.keys(): data[d] = np.ndarray(ncol, dtype = "|S7") for n in range(0, ncol): x = self._data_[d][n] if isinstance(x, float): data[d][n] = fmt.format(x) else: data[d][n] = x[0:7] fmt = "{:>8s}" else: fmt = "".join(["{:", "{:d}.{:d}".format(6+digits, digits), "f}"]) data = self._data_ # Print object content count = 0 for n in range(0, ncol): if (n % 10) == 0: tmp = "{:3d}:".format(n+1) else: tmp = " " for d in dims: tmp += fmt.format(data[d][n]) count += 1 res.append(tmp) if count >= 30 and ncol > 40: res.append("".join([" ......"]*len(dims))) res.append("And {:d} more [truncated]".format(ncol - count)) break return "\n".join(res) def __call__(self, fixup = True, rev = False): """object(fixup = True, rev = False) Default call of :py:class:`colorlib.colorobj` return hex colors, same as ``.colors()`` does. Returns ------- list Returns a list of hex colors. """ return self.colors(fixup = fixup, rev = rev)
[docs] def get_whitepoint(self): """get_whitepoint() A white point definition is used to adjust the colors. If not explicitly set via :py:func:`set_whitepoint` default values are used. This method returns the definition of the white point in use. Returns ------- dict Returns a dict with `X`, `Y`, `Z`, the white point specification for the three dimensions. Examples -------- >>> from colorspace.colorlib import hexcols >>> c = hexcols("#ff0000") >>> c.get_whitepoint() """ return {"X": self.WHITEX, "Y": self.WHITEY, "Z": self.WHITEZ}
[docs] def set_whitepoint(self, **kwargs): """set_whitepoint(**kwargs) A white point definition is used to adjust the colors. This method allows to set custom values. If not explicitly set a default specification is used. No return, stores the new definition on the object. :py:func:`get_whitepoint` can be used to get the current specification. Parameters ---------- X : float white specification for dimension X Y : float white specification for dimension Y Z : float white specification for dimension Z Examples -------- >>> from colorspace.colorlib import hexcols >>> c = hexcols("#ff0000") >>> c.set_whitepoint(X = 100., Y = 100., Z = 101.) >>> c.get_whitepoint() """ for key,arg in kwargs.items(): if key == "X": self.WHITEX = float(arg) elif key == "Y": self.WHITEY = float(arg) elif key == "Z": self.WHITEZ = float(arg) else: raise ValueError("error in {:s}.set_whitepoint: ".format(self.__class__name__) + \ "argument \"{:s}\" not recognized.".format(key))
def _check_if_allowed_(self, x): """_check_if_allowed_(x) Helper function to check if the transformation of the current object to ``x`` is allowed or not. raises a ValueError if the conversion is not possible. No return, raises an Exception if the the transformation is not possible. Should never happen except that the colorobject is not well defined. Parameters ---------- x : str name of the target color space """ if not x in self.ALLOWED: raise Exception("transformation from {:s}".format(self.__class__.__name__) + \ " to \"{:s}\" is unknown (not implemented).".format(x) + \ "The following are allowed: {:s}".format(", ".join(self.ALLOWED))) return def _transform_via_path_(self, via, fixup): """_transform_via_path_(via, fixup) Helper function to transform a colorobject into a new color space. Calls the .to method one or several times along 'a path' as specified by ``via``. No return, converts the current color space object (see method :py:func:`to`. Parameters ---------- via : list of strings the path via which the current color object should be transformed. an example: a :py:class:`hexcols` object can be transformed into CIEXYZ by specifying ``via = ["sRGB", "RGB", "CIEXYZ"]`` fixup : bool whether or not to correct invalid rgb values outside ``[0.,1.]`` if necessary """ for v in via: self.to(v, fixup = fixup) def _colorobject_check_input_arrays_(self, **kwargs): """_colorobject_check_input_arrays_(**kwargs) Checks if all inputs in **kwargs are of type np.ndarray OR lists (will be converted to ndarrays) and that all are of the same length If not, the script will drop some error messsages and stop. If ``alpha`` is given it is handled in a special way. If ``None`` it will simply be dropped (no alpha channel specified), else it is handled like the rest and has to fulfill the requirements (length, type). Parameters ---------- kwargs : ... named keywords, objects to be checked Returns ------- bool Returns ``True`` if all checks where fine, raises a ValueError if the inputs do not fulfil the requirements. """ # Message will be dropped if problems occur msg = "Problem while checking inputs \"{:s}\" to class \"{:s}\":".format( ", ".join(kwargs.keys()), self.__class__.__name__) res = {} lengths = [] for key,val in kwargs.items(): # No alpha provided, simply proceed if key == "alpha" and val is None: continue # If is list: convert to ndarray no matter how long the element is if isinstance(val,float) or isinstance(val,int): val = np.asarray([val]) elif isinstance(val,list): val = np.asarray(val) # For alpha, R, G, and B: check range if isinstance(self, RGB) or isinstance(self, sRGB): if np.max(val) > 1. or np.max(val) < 0.: raise ValueError("wrong values specified for " + \ "dimension {:s} in {:s}: ".format(key, self.__class__.__name__) + \ "values have to lie within [0.,1.]") # Check object type from numpy import asarray try: val = asarray(val) except Exception as e: raise ValueError("input {:s} to {:s}".format(key, self.__class__.__name__) + \ " could not have been converted to numpy.ndarray: {:s}".format(str(e))) # Else append length and proceed lengths.append(len(val)) # Append to result vector res[key] = val # Check if all do have the same length if not np.all([x == lengths[0] for x in lengths]): msg += "Arguments of different lengths: {:s}".format( ", ".join(["{:s} = {:d}".format(kwargs.keys()[i],lengths[i]) \ for i in range(0,len(kwargs))])) raise ValueError(msg) return res
[docs] def hasalpha(self): """hasalpha() Small helper function to check whether the current color object has alpha values or not. Returns ------- bool Returns ``True`` if alpha values are present, ``False`` if not. """ if not "alpha" in self._data_.keys(): return False elif self._data_["alpha"] is None: return False else: return True
[docs] def dropalpha(self): """dropalpha() Remove alpha information from the color object, if defined. """ if self.hasalpha(): del self._data_["alpha"] return
[docs] def specplot(self, **kwargs): """specplot(**kwargs) Plotting a specplot (see :py:func:`specplot.specplot`) of the current color object. Additional arguments can be used to control the specplot. Parameters ---------- kwargs : ... named list of additional arguments forwarded to the specplot function Examples -------- >>> from colorspace.colorlib import HCL >>> cols = HCL([260, 80, 30], [80, 0, 80], [30, 90, 30]) >>> cols.specplot() >>> cols.specplot(rgb = False) """ from copy import copy cols = copy(self) cols.to("hex") if isinstance(cols, colorobject): from .specplot import specplot specplot(cols.colors(), **kwargs)
[docs] def swatchplot(self): """swatchplot(n = 7) Interfacing the :py:func:`swatchplot.swatchplot` function. Plotting the spectrum of the current color palette. Examples -------- >>> from colorspace.colorlib import HCL >>> cols = HCL(H = [160, 210, 260, 310, 360], >>> C = [ 70, 40, 10, 40, 70], >>> L = [ 50, 70, 90, 70, 50]) >>> cols.swatchplot() """ from .swatchplot import swatchplot swatchplot(self.colors())
[docs] def colors(self, fixup = True, rev = False): """colors(fixup = True) Returns hex colors of the current :py:class:`colorobject`. Converts the colors into a :py:class:`hexcols` object to retrieve hex colors which are returned as list. If the object contains alpha values the alpha level is added to the hex string if and only if alpha is not equal to 1.0. Parameters ---------- fixup : bool whether or not to correct rgb values outside the defined range of ``[0., 1.]`` rev : bool return colors in reversed order? Returns ------- list Returns a list of hex colors. Examples -------- >>> from colorspace.colorlib import HCL >>> cols = HCL([0, 40, 80], [30, 60, 80], [85, 60, 35]) >>> cols.colors() """ from copy import copy x = copy(self) x.to("hex", fixup = fixup) if x.hasalpha(): res = x.get("hex_") # Appending alpha if alpha < 1.0 for i in range(0, len(res)): if self._data_["alpha"][i] < 1.0: res[i] += "{:02d}".format(int(self._data_["alpha"][i] * 100.)) # Return hex with alpha colors = res else: colors = x.get("hex_") if rev: colors.reverse() return [str(x) for x in colors]
[docs] def get(self, dimname = None): """get(dimname = None) Returns the current color coordinates. Either a single coordinate (if ``dimname`` is set) or all coordinates of the current :py:class:`colorobject`. The latter will return a dictionary containing the data with all defined dimensions. Parameters ---------- dimname : None, st can be set to only retrieve one very specific coordinate. Returns ------- dict or numpy.ndarray Either a dictionary or a single numpy.ndarray if input ``dimname`` was not specified. If a specific dimension is requested bu the dimension does not exist a ValueError is raised. Examples -------- >>> from colorspace.colorlib import HCL >>> cols = HCL([260, 80, 30], [80, 0, 80], [30, 90, 30]) >>> cols.get() >>> cols.get("H") """ # Return all coordinates from copy import copy if dimname is None: return copy(self._data_) # No string? elif not isinstance(dimname, str): raise ValueError("input dimname to {:s} ".format(self.__class__.__name__) + \ "has to be None or a string.") # Else only the requested dimension elif not dimname in self._data_.keys(): # Alpha channel never defined, return None (which # is a valid value for "no alpha") if dimname == "alpha": return None else: raise ValueError("{:s} has no dimension {:s}".format(self.__class__.__name__, dimname)) return copy(self._data_[dimname])
[docs] def set(self, **kwargs): """set(kwargs) Allows to manipulate the current colors. The named input arguments have to fulfil a specific set or requirements. If not, the function raises a ValueError. The dimension has to exist, and the new data have to be of the same type and of the same length to be accepted. No return, modifies the current object. Parameters ---------- kwargs : ... named arguments. The key is the name of the dimension to be changed, the value an object which fulfills the requirements (see description of this method) Examples -------- >>> from colorspace.colorlib import HCL >>> cols = HCL([260, 80, 30], [80, 0, 80], [30, 90, 30]) >>> print cols >>> cols.set(H = [150, 150, 30]) >>> print cols """ # Looping over inputs for key,vals in kwargs.items(): key.upper() # Return all coordinates if not key in self._data_.keys(): raise ValueError("{:s} has no dimension {:s}".format(self.__class__.__name__, key)) # New values do have to have the same length as the old ones, n = len(self.get(key)) t = type(self.get(key)[0]) from numpy import asarray try: vals = asarray(vals, dtype = t) except Exception as e: raise ValueError("problems converting new data to {:s} ".format(t) + \ " in {:s}: {:s}".format(self.__class__.__name__, str(e))) if not len(vals) == n: raise ValueError("number of values to be stored on the object " + \ "{:s} have to match the current dimension".format(self.__class__.__name__)) self._data_[key] = vals
def _cannot(self, from_, to): """_cannot(from_, to) Helper function, raises an exception if a transformation is not possible by definition. Parameters ---------- from_ : str name of the current color space to : str name of the target color space """ raise Exception("Cannot convert class \"{:s}\" to \"{:s}\".".format( from_, to)) return def _ambiguous(self, from_, to): """_ambiguous(from_, to) Helper function, raises an exception if a transformation is ambiguous by definition and thus, not possible. Parameters ---------- from_ : str name of the current color space. to : str name of the target color space. """ raise Exception("Ambiguous conversion from " + \ "\"{:s}\" to \"{:s}\" (object unchanged).".format(from_, to))
# ------------------------------------------------------------------- # PolarLUV or HCL object # -------------------------------------------------------------------
[docs]class polarLUV(colorobject): """polarLUV(H, C, L, alpha = None) polarLUV or HCL color object. The polar representation of the CIELUV (:class:`colorspace.CIELUV`) color space is also known as Hue-Chroma-Luminance (HCL) color space. polarLUV colors can be converted into: CIEXYZ , CIELUV , CIELAB , RGB , sRGB , polarLAB , hex. Not allowed (ambiguous) to convert into: HSV, HLS. Parameters ---------- L : numeric single value or vector for hue dimension ``[-360.,360.]`` U : numeric single value or vector for chroma dimension ``[0., 100.+]`` V : numeric single value or vector for luminance dimension ``[0., 100.]`` alpha : None or numeric single value or vector of numerics in ``[0.,1.]`` for the alpha channel (``0.`` means transparent, ``1.`` opaque). If ``None`` no transparency is added Examples -------- >>> from colorspace.colorlib import polarLUV, HCL >>> c = polarLUV(100., 30, 50.) >>> c = HCL(100., 30, 50.) # Equivalent to the command above >>> c = HCL([100.], [30.], [50.]) >>> c = HCL([100, 80], [30,50], [30,80]) >>> from numpy import asarray >>> c = HCL(asarray([100,80]), asarray([30,50]), asarray([30,80])) .. seealso:: This object extens the :py:class:`colorlib.colorobject` which provides some methods to e.g., extract color or to modify the whitepoint. """ def __init__(self, H, C, L, alpha = None): # Checking inputs, save inputs on object self._data_ = {} # Dict to store the colors/color dimensions tmp = self._colorobject_check_input_arrays_(H = H, C = C, L = L, alpha = alpha) for key,val in tmp.items(): self._data_[key] = val # White spot definition (the default) self.set_whitepoint(X = 95.047, Y = 100.000, Z = 108.883)
[docs] def to(self, to, fixup = True): """to(to, fixup = True) Transforms the colors into a new color space, if possible. No return, converts the object into a new color space and modifies the underlying object. After calling this method the original object will be of a different type. Parameters ---------- to : str name of the color space into which the colors should be converted (e.g., ``CIEXYZ``, ``HCL``, ``hex``, ``RGB``, ...) fixup : bool whether or not colors outside the defined rgb color space should be corrected if necessary """ self._check_if_allowed_(to) from . import colorlib clib = colorlib() # Nothing to do (converted to itself) if to in ["HCL", self.__class__.__name__]: return # This is the only transformation from polarLUV -> LUV elif to == "CIELUV": [L, U, V] = clib.polarLUV_to_LUV(self.get("L"), self.get("C"), self.get("H")) self._data_ = {"L" : L, "U" : U, "V" : V, "alpha" : self.get("alpha")} self.__class__ = CIELUV # The rest are transformations along a path elif to == "CIEXYZ": via = ["CIELUV", to] self._transform_via_path_(via, fixup = fixup) elif to == "CIELAB": via = ["CIELUV", "CIEXYZ", to] self._transform_via_path_(via, fixup = fixup) elif to == "RGB": via = ["CIELUV", "CIEXYZ", to] self._transform_via_path_(via, fixup = fixup) elif to == "sRGB": via = ["CIELUV", "CIEXYZ", to] self._transform_via_path_(via, fixup = fixup) elif to == "polarLAB": via = ["CIELUV", "CIEXYZ", "CIELAB", to] self._transform_via_path_(via, fixup = fixup) elif to == "hex": via = ["CIELUV", "CIEXYZ", "sRGB", to] self._transform_via_path_(via, fixup = fixup) elif to in ["HLS", "HSV"]: self._ambiguous(self.__class__.__name__, to) else: self._cannot(self.__class__.__name__, to)
# polarLUV is HCL, make copy HCL = polarLUV # ------------------------------------------------------------------- # CIELUV color object # -------------------------------------------------------------------
[docs]class CIELUV(colorobject): """CIELUV(L, U, V, alpha = None) CIELUV color object. polarLUV colors can be converted into: CIEXYZ, CIELUV, CIELAB, RGB, sRGB, polarLAB, hex. Not allowed (ambiguous) to convert into: HSV, HLS. Parameters ---------- L : numeric single value or multiple values for L-dimension U : numeric single value or multiple values for U-dimension V : numeric single value or multiple values for V-dimension alpha : None or numeric single value or vector of numerics in ``[0.,1.]`` for the alpha channel (``0.`` means transparent, ``1.`` opaque). If ``None`` no transparency is added Examples -------- >>> from colorspace.colorlib import CIELUV >>> c = CIELUV(0, 10, 10) >>> c = CIELUV([10, 30], [20, 80], [100, 40]) >>> from numpy import asarray >>> c = CIELUV(asarray([10, 30]), asarray([20, 80]), asarray([100, 40])) .. seealso:: This object extens the :py:class:`colorlib.colorobject` which provides some methods to e.g., extract color or to modify the whitepoint. """ def __init__(self, L, U, V, alpha = None): # checking inputs, save inputs on object self._data_ = {} # Dict to store the colors/color dimensions tmp = self._colorobject_check_input_arrays_(L = L, U = U, V = V, alpha = alpha) for key,val in tmp.items(): self._data_[key] = val # White spot definition (the default) self.set_whitepoint(X = 95.047, Y = 100.000, Z = 108.883)
[docs] def to(self, to, fixup = True): """to(to, fixup = True) Transforms the colors into a new color space, if possible. No return, converts the object into a new color space and modifies the underlying object. After calling this method the original object will be of a different type. Parameters ---------- to : str name of the color space into which the colors should be converted (e.g., ``CIEXYZ``, ``HCL``, ``hex``, ``RGB``, ...) fixup : bool whether or not colors outside the defined rgb color space should be corrected if necessary """ self._check_if_allowed_(to) from . import colorlib clib = colorlib() # Nothing to do (converted to itself) if to == self.__class__.__name__: return # Transformation from CIELUV -> CIEXYZ elif to == "CIEXYZ": [X, Y, Z] = clib.LUV_to_XYZ(self.get("L"), self.get("U"), self.get("V"), self.WHITEX, self.WHITEY, self.WHITEZ) self._data_ = {"X" : X, "Y" : Y, "Z" : Z, "alpha" : self.get("alpha")} self.__class__ = CIEXYZ # Transformation from CIELUV -> polarLUV (HCL) elif to in ["HCL","polarLUV"]: [L, C, H] = clib.LUV_to_polarLUV(self.get("L"), self.get("U"), self.get("V")) self._data_ = {"L" : L, "C" : C, "H" : H, "alpha" : self.get("alpha")} self.__class__ = polarLUV # The rest are transformations along a path elif to == "CIELAB": via = ["CIEXYZ", to] self._transform_via_path_(via, fixup = fixup) elif to == "RGB": via = ["CIEXYZ", to] self._transform_via_path_(via, fixup = fixup) elif to == "sRGB": via = ["CIEXYZ", "RGB", to] self._transform_via_path_(via, fixup = fixup) elif to == "polarLAB": via = ["CIEXYZ", "CIELAB", to] self._transform_via_path_(via, fixup = fixup) elif to == "hex": via = ["CIEXYZ", "RGB", "sRGB", to] self._transform_via_path_(via, fixup = fixup) elif to in ["HLS", "HSV"]: self._ambiguous(self.__class__.__name__, to) else: self._cannot(self.__class__.__name__, to)
# ------------------------------------------------------------------- # CIEXYZ color object # -------------------------------------------------------------------
[docs]class CIEXYZ(colorobject): """CIEXYZ(X, Y, Z, alpha = None) CIEXYZ color object. Allowes conversions to: :py:class:`CIEXYZ`, :py:class:`CIELUV`, :py:class:`CIELAB`, :py:class:`RGB`, :py:class:`polarLUV`, :py:class:`polarLAB`, :py:class:`hexcols`. Not possible are conversions to (ambiguous): :py:class:`HSV`, :py:class:`HLS`. Parameters ---------- X : numeric single value or multiple values for X-dimension Y : numeric single value or multiple values for Y-dimension Z : numeric single value or multiple values for Z-dimension alpha : None or numeric single value or vector of numerics in ``[0.,1.]`` for the alpha channel (``0.`` means transparent, ``1.`` opaque). If ``None`` no transparency is added Examples -------- >>> from colorspace.colorlib import CIEXYZ >>> c = CIEXYZ(80, 30, 10) >>> c = CIEXYZ([10, 0], [20, 80], [40, 40]) >>> from numpy import asarray >>> c = CIEXYZ(asarray([10, 0]), asarray([20, 80]), asarray([40, 40])) .. seealso:: This object extens the :py:class:`colorlib.colorobject` which provides some methods to e.g., extract color or to modify the whitepoint. """ def __init__(self, X, Y, Z, alpha = None): # checking inputs, save inputs on object self._data_ = {} # Dict to store the colors/color dimensions tmp = self._colorobject_check_input_arrays_(X = X, Y = Y, Z = Z, alpha = alpha) for key,val in tmp.items(): self._data_[key] = val # White spot definition (the default) self.set_whitepoint(X = 95.047, Y = 100.000, Z = 108.883)
[docs] def to(self, to, fixup = True): """to(to, fixup = True) Transforms the colors into a new color space, if possible. No return, converts the object into a new color space and modifies the underlying object. After calling this method the original object will be of a different type. Parameters ---------- to : str name of the color space into which the colors should be converted (e.g., ``CIEXYZ``, ``HCL``, ``hex``, ``RGB``, ...) fixup : bool whether or not colors outside the defined rgb color space should be corrected if necessary """ self._check_if_allowed_(to) from . import colorlib clib = colorlib() # Nothing to do (converted to itself) if to == self.__class__.__name__: return # Transformation from CIEXYZ -> CIELUV elif to == "CIELUV": [L, U, V] = clib.XYZ_to_LUV(self.get("X"), self.get("Y"), self.get("Z"), self.WHITEX, self.WHITEY, self.WHITEZ) self._data_ = {"L" : L, "U" : U, "V" : V, "alpha" : self.get("alpha")} self.__class__ = CIELUV # Transformation from CIEXYZ -> CIELAB elif to == "CIELAB": [L, A, B] = clib.XYZ_to_LAB(self.get("X"), self.get("Y"), self.get("Z"), self.WHITEX, self.WHITEY, self.WHITEZ) self._data_ = {"L" : L, "A" : A, "B" : B, "alpha" : self.get("alpha")} self.__class__ = CIELAB # Transformation from CIEXYZ -> RGB elif to == "RGB": [R, G, B] = clib.XYZ_to_RGB(self.get("X"), self.get("Y"), self.get("Z"), self.WHITEX, self.WHITEY, self.WHITEZ) self._data_ = {"R" : R, "G" : G, "B" : B, "alpha" : self.get("alpha")} self.__class__ = RGB # The rest are transformations along a path elif to == "polarLAB": via = ["CIELAB", to] self._transform_via_path_(via, fixup = fixup) elif to in ["HCL", "polarLUV"]: via = ["CIELUV", to] self._transform_via_path_(via, fixup = fixup) elif to == "sRGB": via = ["RGB", to] self._transform_via_path_(via, fixup = fixup) elif to == "hex": via = ["RGB", "sRGB", to] self._transform_via_path_(via, fixup = fixup) elif to in ["HLS", "HSV"]: self._ambiguous(self.__class__.__name__, to) else: self._cannot(self.__class__.__name__, to)
[docs]class RGB(colorobject): """RGB(R, G, B, alpha = None) Device independent RGB color object. Allowes conversions to: :py:class:`CIEXYZ`, :py:class:`CIELUV`, :py:class:`CIELAB`, :py:class:`sRGB`, :py:class:`HSV`, :py:class:`HLS`, :py:class:`polarLUV`, :py:class:`polarLAB`, `"hex"` (:py:class:`hexcols`). Allows additional alpha input. Note that the alpha channel will not be modified but preserved. Parameters ---------- R : numeric intensity of red ``[0.,1.]`` G : numeric intensity of green ``[0.,1.]`` B : numeric intensity of blue ``[0.,1.]`` alpha : None or numeric single value or vector of numerics in ``[0.,1.]`` for the alpha channel (``0.`` means transparent, ``1.`` opaque). If ``None`` no transparency is added Examples -------- >>> from colorspace.colorlib import sRGB >>> c = sRGB(1., 0.3, 0.5) >>> c = sRGB([1.,0.8], [0.5,0.5], [0.0,0.2]) >>> >>> from numpy import asarray >>> c = sRGB(asarray([1.,0.8]), asarray([0.5,0.5]), asarray([0.0,0.2])) .. seealso:: This object extens the :py:class:`colorlib.colorobject` which provides some methods to e.g., extract color or to modify the whitepoint. """ def __init__(self, R, G, B, alpha = None): # checking inputs, save inputs on object self._data_ = {} # Dict to store the colors/color dimensions tmp = self._colorobject_check_input_arrays_(R = R, G = G, B = B, alpha = alpha) for key,val in tmp.items(): self._data_[key] = val # White spot definition (the default) self.set_whitepoint(X = 95.047, Y = 100.000, Z = 108.883)
[docs] def to(self, to, fixup = True): """to(to, fixup = True) Transforms the colors into a new color space, if possible. No return, converts the object into a new color space and modifies the underlying object. After calling this method the original object will be of a different type. Parameters ---------- to : str name of the color space into which the colors should be converted (e.g., ``CIEXYZ``, ``HCL``, ``hex``, ``RGB``, ...) fixup : bool whether or not colors outside the defined rgb color space should be corrected if necessary. """ self._check_if_allowed_(to) from . import colorlib clib = colorlib() # Nothing to do (converted to itself) if to == self.__class__.__name__: return # Transform from RGB -> HLS elif to == "HLS": [H, L, S] = clib.RGB_to_HLS(self.get("R"), self.get("G"), self.get("B")) self._data_ = {"H" : H, "L" : L, "S" : S, "alpha" : self.get("alpha")} self.__class__ = HLS # Transform from RGB -> HSV elif to == "HSV": [H, S, V] = clib.RGB_to_HSV(self.get("R"), self.get("G"), self.get("B")) self._data_ = {"H" : H, "S" : S, "V" : V, "alpha" : self.get("alpha")} self.__class__ = HSV # Transform from RGB -> sRGB elif to == "sRGB": [R, G, B] = clib.RGB_to_DEVRGB(self.get("R"), self.get("G"), self.get("B"), self.GAMMA) self._data_ = {"R" : R, "G" : G, "B" : B, "alpha" : self.get("alpha")} self.__class__ = sRGB # Transform from RGB -> CIEXYZ elif to == "CIEXYZ": [X, Y, Z] = clib.RGB_to_XYZ(self.get("R"), self.get("G"), self.get("B"), self.WHITEX, self.WHITEY, self.WHITEZ) self._data_ = {"X" : X, "Y" : Y, "Z" : Z, "alpha" : self.get("alpha")} self.__class__ = CIEXYZ # The rest are transformations along a path elif to in ["CIELUV", "CIELAB"]: via = ["CIEXYZ", to] self._transform_via_path_(via, fixup = fixup) elif to in ["HCL","polarLUV"]: via = ["CIEXYZ", "CIELUV", to] self._transform_via_path_(via, fixup = fixup) elif to == "polarLAB": via = ["CIEXYZ", "CIELAB", to] self._transform_via_path_(via, fixup = fixup) elif to == "hex": via = ["sRGB", to] self._transform_via_path_(via, fixup = fixup) else: self._cannot(self.__class__.__name__, to)
[docs]class sRGB(colorobject): """sRGB(R, G, B, alpha = None, gamma = None) sRGB (device dependent RGB) color object. Allowes conversions to: :py:class:`CIEXYZ`, :py:class:`CIELUV`, :py:class:`CIELAB`, :py:class:`RGB`, :py:class:`HSV`, :py:class:`HLS`, :py:class:`polarLUV`, :py:class:`polarLAB`, `"hex"` (:py:class:`hexcols`). Allows additional alpha input. Note that the alpha channel will not be modified but preserved. R, G, and B have to be within ``[0., 1.]``. Parameters ---------- R : float, list of floats, numpy.ndarray intensity of red ``[0.,1.]`` G : float, list of floats, numpy.ndarray intensity of green ``[0.,1]`` B : float, list of floats, numpy.ndarray intensity of blue ``[0.,1]`` alpha : None or numeric single value or vector of numerics in ``[0.,1.]`` for the alpha channel (``0.`` means transparent, ``1.`` opaque). If ``None`` no transparency is added gamma : None, float gamma parameter. Used to convert from device dependent sRGB to RGB. If not set the default of 2.4 is used Examples -------- >>> from colorspace.colorlib import sRGB >>> c = sRGB(1., 0.3, 0.5) >>> c = sRGB([1.,0.8], [0.5,0.5], [0.0,0.2]) >>> >>> from numpy import asarray >>> c = sRGB(asarray([1.,0.8]), asarray([0.5,0.5]), asarray([0.0,0.2])) .. seealso:: This object extens the :py:class:`colorlib.colorobject` which provides some methods to e.g., extract color or to modify the whitepoint. """ def __init__(self, R, G, B, alpha = None, gamma = None): # checking inputs, save inputs on object self._data_ = {} # Dict to store the colors/color dimensions tmp = self._colorobject_check_input_arrays_(R = R, G = G, B = B, alpha = alpha) for key,val in tmp.items(): self._data_[key] = val # White spot definition (the default) self.set_whitepoint(X = 95.047, Y = 100.000, Z = 108.883) if isinstance(gamma, float): self.GAMMA = gamma
[docs] def to(self, to, fixup = True): """to(to, fixup = True) Transforms the colors into a new color space, if possible. No return, converts the object into a new color space and modifies the underlying object. After calling this method the original object will be of a different type. Parameters ---------- to : str name of the color space into which the colors should be converted (e.g., ``CIEXYZ``, ``HCL``, ``hex``, ``RGB``, ...) fixup : bool whether or not colors outside the defined rgb color space should be corrected if necessary. """ self._check_if_allowed_(to) from . import colorlib clib = colorlib() # Nothing to do (converted to itself) if to == self.__class__.__name__: return # Transformation sRGB -> RGB elif to == "RGB": [R, G, B] = clib.DEVRGB_to_RGB(self.get("R"), self.get("G"), self.get("B"), gamma = self.GAMMA) self._data_ = {"R" : R, "G" : G, "B" : B, "alpha" : self.get("alpha")} self.__class__ = RGB # Transformation sRGB -> hex elif to == "hex": hex_ = clib.sRGB_to_hex(self.get("R"), self.get("G"), self.get("B"), fixup) self._data_ = {"hex_" : hex_, "alpha" : self.get("alpha")} self.__class__ = hexcols # The rest are transformations along a path elif to in ["HSV", "HLS", "CIEXYZ"]: via = ["RGB", to] self._transform_via_path_(via, fixup = fixup) elif to in ["CIELUV", "CIELAB"]: via = ["RGB", "CIEXYZ", to] self._transform_via_path_(via, fixup = fixup) elif to in ["HCL","polarLUV"]: via = ["RGB", "CIEXYZ", "CIELUV", to] self._transform_via_path_(via, fixup = fixup) elif to == "polarLAB": via = ["RGB", "CIEXYZ", "CIELAB", to] self._transform_via_path_(via, fixup = fixup) else: self._cannot(self.__class__.__name__, to)
[docs]class CIELAB(colorobject): """CIELAB(L, A, B, alpha = None) CIELAB color object. Allowes conversions to: :py:class:`CIEXYZ`, :py:class:`CIELUV`, :py:class:`CIELAB`, :py:class:`RGB`, :py:class:`polarLUV`, :py:class:`polarLAB`, `"hex"` (:py:class:`hexcols`). Not possible are conversions to (ambiguous): :py:class:`HSV`, :py:class:`HLS`. Parameters ---------- L : numeric single value or multiple values for L dimension A : numeric single value or multiple values for A dimension B : numeric single value or multiple values for B dimension alpha : None or numeric single value or vector of numerics in ``[0.,1.]`` for the alpha channel (``0.`` means transparent, ``1.`` opaque). If ``None`` no transparency is added Examples -------- >>> from colorspace.colorlib import CIELAB >>> c = CIELAB(-30, 10, 10) >>> c = CIELAB([-30, 30], [20, 80], [40, 40]) >>> from numpy import asarray >>> c = CIELAB(asarray([-30, 30]), asarray([20, 80]), asarray([40, 40])) .. seealso:: This object extens the :py:class:`colorlib.colorobject` which provides some methods to e.g., extract color or to modify the whitepoint. """ def __init__(self, L, A, B, alpha = None): # checking inputs, save inputs on object self._data_ = {} # Dict to store the colors/color dimensions tmp = self._colorobject_check_input_arrays_(L = L, A = A, B = B, alpha = alpha) for key,val in tmp.items(): self._data_[key] = val # White spot definition (the default) self.set_whitepoint(X = 95.047, Y = 100.000, Z = 108.883)
[docs] def to(self, to, fixup = True): """to(to, fixup = True) Transforms the colors into a new color space, if possible. No return, converts the object into a new color space and modifies the underlying object. After calling this method the original object will be of a different type. Parameters ---------- to : str name of the color space into which the colors should be converted (e.g., ``CIEXYZ``, ``HCL``, ``hex``, ``RGB``, ...) fixup : bool whether or not colors outside the defined rgb color space should be corrected if necessary """ self._check_if_allowed_(to) from . import colorlib clib = colorlib() # Nothing to do (converted to itself) if to == self.__class__.__name__: return # Transformations CIELAB -> CIEXYZ elif to == "CIEXYZ": [X, Y, Z] = clib.LAB_to_XYZ(self.get("L"), self.get("A"), self.get("B"), self.WHITEX, self.WHITEY, self.WHITEZ) self._data_ = {"X" : X, "Y" : Y, "Z" : Z, "alpha" : self.get("alpha")} self.__class__ = CIEXYZ # Transformation CIELAB -> polarLAB elif to == "polarLAB": [L, A, B] = clib.LAB_to_polarLAB(self.get("L"), self.get("A"), self.get("B")) self._data_ = {"L" : L, "A" : A, "B" : B, "alpha" : self.get("alpha")} self.__class__ = polarLAB # The rest are transformations along a path elif to == "CIELUV": via = ["CIEXYZ", to] self._transform_via_path_(via, fixup = fixup) elif to in ["HCL","polarLUV"]: via = ["CIEXYZ", "CIELUV", to] self._transform_via_path_(via, fixup = fixup) elif to == "RGB": via = ["CIEXYZ", to] self._transform_via_path_(via, fixup = fixup) elif to == "sRGB": via = ["CIEXYZ", "RGB", to] self._transform_via_path_(via, fixup = fixup) elif to == "hex": via = ["CIEXYZ", "RGB", "sRGB", to] self._transform_via_path_(via, fixup = fixup) elif to in ["HLS", "HSV"]: self._ambiguous(self.__class__.__name__, to) else: self._cannot(self.__class__.__name__, to)
[docs]class polarLAB(colorobject): """polarLAB(L, A, B, alpha = None) polarLAB color object. Allowes conversions to: :py:class:`CIEXYZ`, :py:class:`CIELUV`, :py:class:`CIELAB`, :py:class:`RGB`, :py:class:`polarLUV`, :py:class:`polarLAB`, `"hex"` (:py:class:`hexcols`). Not possible are conversions to (ambiguous): :py:class:`HSV`, :py:class:`HLS`. Parameters ---------- L : numeric single value or multiple values for L dimension A : numeric single value or multiple values for A dimension B : numeric single value or multiple values for B dimension alpha : None or numeric single value or vector of numerics in ``[0.,1.]`` for the alpha channel (``0.`` means transparent, ``1.`` opaque). If ``None`` no transparency is added .. seealso:: This object extens the :py:class:`colorlib.colorobject` which provides some methods to e.g., extract color or to modify the whitepoint. """ def __init__(self, L, A, B, alpha = None): # checking inputs, save inputs on object self._data_ = {} # Dict to store the colors/color dimensions tmp = self._colorobject_check_input_arrays_(L = L, A = A, B = B, alpha = alpha) for key,val in tmp.items(): self._data_[key] = val # White spot definition (the default) self.set_whitepoint(X = 95.047, Y = 100.000, Z = 108.883)
[docs] def to(self, to, fixup = True): """to(to, fixup = True) Transforms the colors into a new color space, if possible. No return, converts the object into a new color space and modifies the underlying object. After calling this method the original object will be of a different type. Parameters ---------- to : str name of the color space into which the colors should be converted (e.g., ``CIEXYZ``, ``HCL``, ``hex``, ``RGB``, ...) fixup : bool whether or not colors outside the defined rgb color space should be corrected if necessary. """ self._check_if_allowed_(to) from . import colorlib clib = colorlib() # Nothing to do (converted to itself) if to == self.__class__.__name__: return # The only transformation we need is from polarLAB -> LAB elif to == "CIELAB": [L, A, B] = clib.polarLAB_to_LAB(self.get("L"), self.get("A"), self.get("B")) self._data_ = {"L" : L, "A" : A, "B" : B, "alpha" : self.get("alpha")} self.__class__ = CIELAB # The rest are transformationas along a path elif to == "CIEXYZ": via = ["CIELAB", to] self._transform_via_path_(via, fixup = fixup) elif to == "CIELUV": via = ["CIELAB", "CIEXYZ", to] self._transform_via_path_(via, fixup = fixup) elif to in ["HCL", "polarLUV"]: via = ["CIELAB", "CIEXYZ", "CIELUV", to] self._transform_via_path_(via, fixup = fixup) elif to == "RGB": via = ["CIELAB", "CIEXYZ", to] self._transform_via_path_(via, fixup = fixup) elif to == "sRGB": via = ["CIELAB", "CIEXYZ", "RGB", to] self._transform_via_path_(via, fixup = fixup) elif to == "hex": via = ["CIELAB", "CIEXYZ", "RGB", "sRGB", to] self._transform_via_path_(via, fixup = fixup) elif to in ["HLS", "HSV"]: self._ambiguous(self.__class__.__name__, to) else: self._cannot(self.__class__.__name__, to)
[docs]class HSV(colorobject): """HSV(H, S, V, alpha = None) HSV (Hue-Saturation-Value) color object. Allowes conversions to: :py:class:`RGB`, :py:class:`sRGB`, :py:class:`HLS`, and `"hex"` (:py:class:`hexcols`). Not possible are conversions to (ambiguous): :py:class:`CIEXYZ`, :py:class:`CIELUV`, :py:class:`CIELAB`, :py:class:`polarLUV`, :py:class:`polarLAB`, Parameters ---------- H : numeric single value or multiple values for the hue dimension S : numeric single value or multiple values for the saturation dimension V : numeric single value or multiple values for the value dimension alpha : None or numeric single value or vector of numerics in ``[0.,1.]`` for the alpha channel (``0.`` means transparent, ``1.`` opaque). If ``None`` no transparency is added Examples -------- >>> from colorspace.colorlib import HSV >>> cols = HSV([150, 150, 10], [1.5, 0, 1.5], [0.1, 0.7, 0.1]) .. seealso:: This object extens the :py:class:`colorlib.colorobject` which provides some methods to e.g., extract color or to modify the whitepoint. """ def __init__(self, H, S, V, alpha = None): # checking inputs, save inputs on object self._data_ = {} # Dict to store the colors/color dimensions tmp = self._colorobject_check_input_arrays_(H = H, S = S, V = V, alpha = alpha) for key,val in tmp.items(): self._data_[key] = val # White spot definition (the default) self.set_whitepoint(X = 95.047, Y = 100.000, Z = 108.883)
[docs] def to(self, to, fixup = True): """to(to, fixup = True) Transforms the colors into a new color space, if possible. No return, converts the object into a new color space and modifies the underlying object. After calling this method the original object will be of a different type. Parameters ---------- to : str name of the color space into which the colors should be converted (e.g., ``CIEXYZ``, ``HCL``, ``hex``, ``RGB``, ...) fixup : bool whether or not colors outside the defined rgb color space should be corrected if necessary. """ self._check_if_allowed_(to) from . import colorlib clib = colorlib() # Nothing to do (converted to itself) if to == self.__class__.__name__: return # The only transformation we need is back to RGB elif to == "RGB": [R, G, B] = clib.HSV_to_RGB(self.get("H"), self.get("S"), self.get("V")) self._data_ = {"R" : R, "G" : G, "B" : B, "alpha" : self.get("alpha")} self.__class__ = RGB # The rest are transformations along a path elif to == "sRGB": via = ["RGB", to] self._transform_via_path_(via, fixup = fixup) elif to == "hex": via = ["RGB", "sRGB", to] self._transform_via_path_(via, fixup = fixup) elif to == "HLS": via = ["RGB", to] self._transform_via_path_(via, fixup = fixup) elif to in ["CIEXYZ","CIELUV","CIELAB","polarLUV", "HCL","polarLAB"]: self._ambiguous(self.__class__.__name__, to) else: self._cannot(self.__class__.__name__, to)
[docs]class HLS(colorobject): """HLS(H, L, S, alpha = None) HLS (Hue-Lightness-Saturation) color space. Allowes conversions to: :py:class:`RGB`, :py:class:`sRGB`, :py:class:`HLS`, and `"hex"` (:py:class:`hexcols`). Not possible are conversions to (ambiguous): :py:class:`CIEXYZ`, :py:class:`CIELUV`, :py:class:`CIELAB`, :py:class:`polarLUV`, :py:class:`polarLAB`, Parameters ---------- H : numeric single value or multiple values for the hue dimension L : numeric single value or multiple values for the lightness dimension S : numeric single value or multiple values for the saturation dimension alpha : None or numeric single value or vector of numerics in ``[0.,1.]`` for the alpha channel (``0.`` means transparent, ``1.`` opaque). If ``None`` no transparency is added Examples -------- >>> from colorspace.colorlib import HLS >>> cols = HLS([150, 0, 10], [0.1, 0.7, 0.1], [3, 0, 3]) .. seealso:: This object extens the :py:class:`colorlib.colorobject` which provides some methods to e.g., extract color or to modify the whitepoint. """ def __init__(self, H, L, S, alpha = None): # checking inputs, save inputs on object self._data_ = {} # Dict to store the colors/color dimensions tmp = self._colorobject_check_input_arrays_(H = H, L = L, S = S, alpha = None) for key,val in tmp.items(): self._data_[key] = val # White spot definition (the default) self.set_whitepoint(X = 95.047, Y = 100.000, Z = 108.883)
[docs] def to(self, to, fixup = True): """to(to, fixup = True) Transforms the colors into a new color space, if possible. No return, converts the object into a new color space and modifies the underlying object. After calling this method the original object will be of a different type. Parameters ---------- to : str name of the color space into which the colors should be converted (e.g., ``CIEXYZ``, ``HCL``, ``hex``, ``RGB``, ...) fixup : bool whether or not colors outside the defined rgb color space should be corrected if necessary. """ self._check_if_allowed_(to) from . import colorlib clib = colorlib() # Nothing to do (converted to itself) if to == self.__class__.__name__: return # The only transformation we need is back to RGB elif to == "RGB": [R, G, B] = clib.HLS_to_RGB(self.get("H"), self.get("L"), self.get("S")) self._data_ = {"R" : R, "G" : G, "B" : B, "alpha" : self.get("alpha")} self.__class__ = RGB # The rest are transformations along a path elif to == "sRGB": via = ["RGB", to] self._transform_via_path_(via, fixup = fixup) elif to == "hex": via = ["RGB", "sRGB", to] self._transform_via_path_(via, fixup = fixup) elif to == "HSV": via = ["RGB", to] self._transform_via_path_(via, fixup = fixup) elif to in ["CIEXYZ","CIELUV","CIELAB","polarLUV","HCL","polarLAB"]: self._ambiguous(self.__class__.__name__, to) else: self._cannot(self.__class__.__name__, to)
[docs]class hexcols(colorobject): """hexcols(hex_) Color object for hex colors. Takes up a set of hex colors. Can be converted to all other color spaces including :py:class:`RGB`, :py:class:`sRGB`, :py:class:`HLS`, and `"hex"` (:py:class:`hexcols`), :py:class:`CIEXYZ`, :py:class:`CIELUV`, :py:class:`CIELAB`, :py:class:`polarLUV`, :py:class:`polarLAB`, Parameters ---------- hex_ : str, list of str, or numpy.ndarray of type str hex colors. Only six and eight digit hex colors are allowed (e.g., ``#000000`` or ``#00000050`` if with alpha value). If invalid hex colors are provided the object will raise a ValueError. Input can be a single string, a list of strings, or a `numpy.ndarray` containing a set of hex colors. Invalid hex colors will be handled as `numpy.nan`, alpha values can be provided but will be ignored Examples -------- >>> from colorspace.colorlib import hexcols >>> c = hexcols("#cecece") >>> c = hexcols(["#ff0000", "#00ff00"]) >>> from numpy import asarray >>> c = hexcols(asarray(["#ff000030", "#00ff0030"], dtype = "|S9")) >>> #Convert hex colors to another color space (CIEXYZ for example): >>> c.to("CIEXYZ") .. seealso:: This object extens the :py:class:`colorlib.colorobject` which provides some methods to e.g., extract color or to modify the whitepoint. """ def __init__(self, hex_): if isinstance(hex_,str): hex_ = np.asarray([hex_]) # checking inputs, save inputs on object self._data_ = {} # Dict to store the colors/color dimensions tmp = self._colorobject_check_input_arrays_(hex_ = hex_) # Checking for valid hex colors and alpha values tmp = self._check_hex_(tmp["hex_"]) for key,val in tmp.items(): self._data_[key] = val # White spot definition (the default) self.set_whitepoint(X = 95.047, Y = 100.000, Z = 108.883) def _check_hex_(self, hex_): """_check_hex_(hex_) Checking hex colors for validity. Only 6 and 8 digit hex colors with or without alpha (e.g., "#000000", "#00000050") are allowed. The method raises a ValueError if invalid hex colors are found. Parameters ---------- hex_ : list of strings a list of (hopefully) valid hex colors Returns ------- dict Returns a dict with two elements named hex_ and alpha. The hex_ element contains valid six-digit hex strings, the alpha element a list of the same length with alpha values. For all hex colors with no alpha an alpha value of ``1.0`` is set. """ from re import compile, match from numpy import sum r = compile("^(nan|#[0-9A-Fa-f]{6}([0-9]{2})?)$") check = [1 if match(r, x) else 0 for x in hex_] if not sum(check) == len(hex_): raise ValueError("invalid hex colors provided while " + \ "initializing class {:s}".format(self.__class__.__name__)) r = compile("^#[0-9A-Fa-f]{6}([0-9]{2})$") check = [1 if match(r, x) else 0 for x in hex_] # No colors with alpha if sum(check) == 0: return {"hex_": hex_} # Else extracting alpha colors else: alpha = [] for h in hex_: m = match(r, h) if not m: alpha.append(1.) else: alpha.append(int(m.group(1)) / 100.) return {"hex_": hex_, "alpha": alpha}
[docs] def to(self, to, fixup = True): """to(to, fixup = True) Transforms the colors into a new color space, if possible. No return, converts the object into a new color space and modifies the underlying object. After calling this method the original object will be of a different type. Parameters ---------- to : str name of the color space into which the colors should be converted (e.g., ``CIEXYZ``, ``HCL``, ``hex``, ``RGB``, ...) fixup : bool whether or not colors outside the defined rgb color space should be corrected if necessary """ self._check_if_allowed_(to) from . import colorlib clib = colorlib() # Nothing to do (converted to itself) if to in ["hex", self.__class__.__name__]: return # The only transformation we need is from hexcols -> sRGB elif to == "sRGB": [R, G, B] = clib.hex_to_sRGB(self.get("hex_")) self._data_ = {"R": R, "G": G, "B": B} self.__class__ = sRGB # The rest are transformations along a path elif to == "RGB": via = ["sRGB", to] self._transform_via_path_(via, fixup = fixup) elif to in ["CIEXYZ", "HLS", "HSV"]: via = ["sRGB", "RGB", to] self._transform_via_path_(via, fixup = fixup) elif to in ["CIELUV", "CIELAB"]: via = ["sRGB", "RGB", "CIEXYZ", to] self._transform_via_path_(via, fixup = fixup) elif to in ["HCL", "polarLUV"]: via = ["sRGB", "RGB", "CIEXYZ", "CIELUV", to] self._transform_via_path_(via, fixup = fixup) elif to in "polarLAB": via = ["sRGB", "RGB", "CIEXYZ", "CIELAB", to] self._transform_via_path_(via, fixup = fixup) else: self._cannot(self.__class__.__name__, to)