Coverage for src/colorspace/colorlib.py: 98%
1069 statements
« prev ^ index » next coverage.py v7.6.4, created at 2024-10-29 15:11 +0000
« prev ^ index » next coverage.py v7.6.4, created at 2024-10-29 15:11 +0000
1# """
2# Copyright 2005, Ross Ihaka. All Rights Reserved.
3# Ported to Python by Reto Stauffer.
4#
5# Redistribution and use in source and binary forms, with or without
6# modification, are permitted provided that the following conditions
7# are met:
8#
9# 1. Redistributions of source code must retain the above copyright notice,
10# this list of conditions and the following disclaimer.
11#
12# 2. Redistributions in binary form must reproduce the above copyright
13# notice, this list of conditions and the following disclaimer in the
14# documentation and/or other materials provided with the distribution.
15#
16# 3. The name of the Ross Ihaka may not be used to endorse or promote
17# products derived from this software without specific prior written
18# permission.
19#
20# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS `AS IS''
21# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
22# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ROSS IHAKA BE LIABLE FOR
24# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
26# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
27# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
28# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
29# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30# POSSIBILITY OF SUCH DAMAGE.
31# """
34import sys
35import numpy as np
36import inspect
38class colorlib:
39 """Color Handling Superclass
41 The `colorlib` class provides a series of methods methods
42 which allow to convert colors between different color spaces
43 and is not intended to be used by end-users (not exported).
45 Users should use the dedicated classes for the available color spaces which
46 all extend this class. These are
47 :py:class:`CIELAB`, :py:class:`CIELUV`, :py:class:`CIEXYZ`,
48 :py:class:`HLS`, :py:class:`HSV`, :py:class:`RGB`, :py:class:`hexcols`,
49 :py:class:`polarLAB`, :py:class:`polarLUV`, and :py:class:`sRGB`.
50 """
52 # No initialization method, but some constants are specified here
54 _KAPPA = 24389.0 / 27.0
55 """Static constant; required for coordinate transformations.
56 Often approximated as 903.3."""
58 _EPSILON = 216.0 / 24389.0
59 """Static constant; required for coordinate transformations.
60 Often approximated as 7.787."""
62 # Default white spot
63 XN = np.asarray([ 95.047])
64 """X value for default white spot. Used for coordinate transformations."""
65 YN = np.asarray([100.000])
66 """Y value for default white spot. Used for coordinate transformations."""
67 ZN = np.asarray([108.883])
68 """Z value for default white spot. Used for coordinate transformations."""
70 # Conversion function
71 def _DEG2RAD(self, x):
72 """Convert degrees into radiant
74 Args:
75 x (float, array of floats): Value(s) in degrees.
77 Returns:
78 float, float array: Returns input `x` in radiant.
79 """
80 return np.pi / 180. * x
83 # Conversion function
84 def _RAD2DEG(self, x):
85 """Convert Radiant to Degrees
87 Converting degrees to radiants, used to convert to polar
88 color coordinates.
90 Args:
91 x (float, array of floats): Value(s) in radiant.
93 Returns:
94 float, array of floats: Returns input `x` in degrees.
95 """
96 return 180. / np.pi * x
99 def _get_white_(self, __fname__, n, XN = None, YN = None, ZN = None):
100 """Get Whitepoint
102 For some color conversion functions the "white" definition (default
103 white color) has to be specified. This function checks and prepares the
104 `XN`, `YN`, and `ZN` definition. Defaults are used if the user does not specify a
105 custom white point. If set, `XN`, `YN`, and `ZN` have to be of type `numpy.ndarray`,
106 either of length one (will be expanded to length "n"), or of length `n`.
108 Args:
109 __fname__ (str): Name of the parent method, only used if errors are dropped.
110 n (int): Number of colors to which `NX`, `NY`, and `NZ` will be expanded.
111 XN (None, float, numpy.ndarray): Either `None` (default) or an
112 `nd.array` of length one or length `n`. White point specification for
113 dimension `X`, defaults to `None`.
114 YN (None, float, numpy.ndarray): See `XN`. White point specification for
115 dimension `Y`, defaults to `None`.
116 YZ (None, numpy.ndarray): See `XN`. White point specification for
117 dimension `Z`, defaults to `None`.
119 Raises:
120 TypeError: If `XN`, `YN` and `ZN` are invalid (not `None` nor in a format
121 that can be converted into a `numpy.ndarray`).
122 ValueError: If the resulting values `XN`, `YN`, and `ZN` are not all
123 of the same length.
125 Returns:
126 list: Returns a list `[XN, YN, ZN]` with three `numpy.ndarrays`
127 of length `n`. If the inputs `XN`, `YN`, `ZN` (or some) were `None`,
128 the class defaults are used.
129 """
131 # Take defaults if not further specified
132 if not XN: XN = self.XN
133 if not YN: YN = self.YN
134 if not ZN: ZN = self.ZN
136 if isinstance(XN, float): XN = np.asarray([XN])
137 if isinstance(YN, float): YN = np.asarray([YN])
138 if isinstance(ZN, float): ZN = np.asarray([ZN])
140 # Expand if required
141 if len(XN) == 1 and not len(XN) == n: XN = np.repeat(XN, n)
142 if len(YN) == 1 and not len(YN) == n: YN = np.repeat(YN, n)
143 if len(ZN) == 1 and not len(ZN) == n: ZN = np.repeat(ZN, n)
145 # Check if all lengths match
146 if not np.all([len(x) == n for x in [XN, YN, ZN]]):
147 raise ValueError(f"arguments XN/YN/ZN to `{__fname__} have to be of the same length")
149 return [XN, YN, ZN]
152 def _check_input_arrays_(self, __fname__, **kwargs):
153 """Check Input Arrays
155 Checks if all inputs in `kwargs` are of type `numpy.ndarray` and of the
156 same length. If not, the script will throw an exception and stop.
158 Args:
159 __fname__ (str): Name of the method who called this check routine.
160 Only used to drop a useful error message if required.
161 **kwargs: Named keywords, objects to be checked.
163 Returns:
164 bool: Returns `True` if everything is OK, else an exception will be thrown.
165 """
167 # Message will be dropped if problems occur
168 msg = "Problem while checking inputs \"{:s}\" to method \"{:s}\":".format(
169 ", ".join(kwargs.keys()), __fname__)
171 from numpy import asarray
172 lengths = []
173 for key,val in kwargs.items():
175 try:
176 val = asarray(val)
177 except Exception as e:
178 raise ValueError(f"argument `{key}` to {self.__class__.__name__} " + \
179 "could not have been converted to numpy.ndarray")
180 # Else append length and proceed
181 lengths.append(len(val))
183 # Check if all do have the same length
184 if not np.all([x == lengths[0] for x in lengths]):
185 tmp = []
186 for k,v in kwargs.items(): tmp.append(f"{k} = {v}")
187 msg = f" Arguments of different lengths: {', '.join(tmp)}."
188 raise ValueError(msg)
190 # If all is fine, simply return True
191 return True
194 # -------------------------------------------------------------------
195 # -------------------------------------------------------------------
196 # -------------------------------------------------------------------
197 # -------------------------------------------------------------------
201 # ----- CIE-XYZ <-> Device dependent RGB -----
202 #
203 # Gamma Correction
204 #
205 # The following two functions provide gamma correction which
206 # can be used to switch between sRGB and linearized sRGB (RGB).
207 #
208 # The standard value of gamma for sRGB displays is approximately 2.2,
209 # but more accurately is a combination of a linear transform and
210 # a power transform with exponent 2.4
211 #
212 # gtrans maps linearized sRGB to sRGB.
213 # ftrans provides the inverse map.
214 def gtrans(self, u, gamma):
215 """Gamma Correction
217 Function `gtrans` and `ftrans` provide gamma correction which
218 can be used to switch between sRGB and linearised sRGB (RGB).
220 The standard value of gamma for sRGB displays is approximately `2.2`,
221 but more accurately is a combination of a linear transform and
222 a power transform with exponent `2.4`.
223 `gtrans` maps linearised sRGB to sRGB, `ftrans` provides the inverse mapping.
225 Args:
226 u (numpy.ndarray): Float array of length `N`.
227 gamma (float, numpy.ndarray): gamma value; if float or
228 `numpy.ndarray` of length one, `gamma` will be recycled if needed.
230 Returns:
231 numpy.ndarray: Gamma corrected values, same length as input `u`.
232 """
234 __fname__ = inspect.stack()[0][3] # Name of this method
236 # Input check
237 if isinstance(gamma, float): gamma = np.asarray([gamma])
238 if len(gamma) == 1 and not len(gamma) == len(u):
239 gamma = np.repeat(gamma, len(u))
241 # Convert input to float
242 u = np.asarray(u, dtype = "float")
244 # Checking inputs
245 self._check_input_arrays_(__fname__, u = u, gamma = gamma)
247 # Transform
248 for i,val in np.ndenumerate(u):
249 # np.fmax to avoid overflow
250 if val > 0.00304:
251 u[i] = 1.055 * np.power(val, (1. / gamma[i])) - 0.055
252 else:
253 u[i] = 12.92 * val
255 return u
257 def ftrans(self, u, gamma):
258 """Gamma Correction
260 Function `gtrans` and `ftrans` provide gamma correction which
261 can be used to switch between sRGB and linearised sRGB (RGB).
263 The standard value of gamma for sRGB displays is approximately `2.2`,
264 but more accurately is a combination of a linear transform and
265 a power transform with exponent `2.4`.
266 `gtrans` maps linearised sRGB to sRGB, `ftrans` provides the inverse mapping.
268 Args:
269 u (numpy.ndarray): Float array of length `N`.
270 gamma (float, numpy.ndarray): gamma value; if float or
271 `numpy.ndarray` of length one, `gamma` will be recycled if needed.
273 Returns:
274 numpy.ndarray: Gamma corrected values, same length as input `u`.
275 """
277 __fname__ = inspect.stack()[0][3] # Name of this method
279 # Input check
280 if isinstance(gamma, float): gamma = np.asarray([gamma])
281 if len(gamma) == 1 and not len(gamma) == len(u):
282 gamma = np.repeat(gamma, len(u))
284 # Checking inputs
285 self._check_input_arrays_(__fname__, u = u, gamma = gamma)
287 # Convert input to float
288 u = np.asarray(u, dtype = "float")
290 # Transform
291 for i,val in np.ndenumerate(u):
292 if val > 0.03928: u[i] = np.power((val + 0.055) / 1.055, gamma[i])
293 else: u[i] = val / 12.92
295 return u
297 # Support function qtrans
298 def _qtrans(self, q1, q2, hue):
299 if hue > 360.: hue = hue - 360.
300 if hue < 0: hue = hue + 360.
302 if hue < 60.: return q1 + (q2 - q1) * hue / 60.
303 elif hue < 180.: return q2
304 elif hue < 240.: return q1 + (q2 - q1) * (240. - hue) / 60.
305 else: return q1
308 def sRGB_to_RGB(self, R, G, B, gamma = 2.4):
309 """Convert Standard RGB to RGB
311 Converting colors from the Standard RGB color space to RGB.
313 Args:
314 R (numpy.ndarray): Intensities for red (`[0., 1.]`).
315 G (numpy.ndarray): Intensities for green (`[0., 1.]`).
316 B (numpy.ndarray): Intensities for blue (`[0., 1.]`).
317 gamma (float): gamma adjustment, defaults to `2.4`.
319 Returns:
320 list: Returns a list of `numpy.ndarray`s with `R`, `G`, and `B` values.
321 """
323 __fname__ = inspect.stack()[0][3] # Name of this method
325 # Input check
326 if isinstance(gamma, float): gamma = np.asarray([gamma])
327 if len(gamma) == 1 and not len(gamma) == len(R):
328 gamma = np.repeat(gamma, len(R))
330 # Checking inputs
331 self._check_input_arrays_(__fname__, R = R, G = G, B = B, gamma = gamma)
333 # Apply gamma correction
334 return [self.ftrans(x, gamma) for x in [R, G, B]]
336 def RGB_to_sRGB(self, R, G, B, gamma = 2.4):
337 """Convert RGB to Standard RGB
339 Converts one (or multiple) colors defined by their red, blue, green,
340 and blue coordinates (`[0.0, 1.0]`) to the Standard RGB color space;
341 returning a modified list of red, green, blue coordinates.
343 Args:
344 R (numpy.ndarray): Intensities for red (`[0., 1.]`).
345 G (numpy.ndarray): Intensities for green (`[0., 1.]`).
346 B (numpy.ndarray): Intensities for blue (`[0., 1.]`).
347 gamma (float): gamma adjustment, defaults to `2.4`.
349 Returns:
350 list: Returns a list of `numpy.ndarray`s with `R`, `G`, and `B` values.
351 """
353 __fname__ = inspect.stack()[0][3] # Name of this method
355 # Input check
356 if isinstance(gamma, float): gamma = np.asarray([gamma])
357 if len(gamma) == 1 and not len(gamma) == len(R):
358 gamma = np.repeat(gamma, len(R))
360 # Checking inputs
361 self._check_input_arrays_(__fname__, R = R, G = G, B = B, gamma = gamma)
363 # Apply gamma correction
364 return [self.gtrans(x, gamma) for x in [R, G, B]]
366 # -------------------------------------------------------------------
367 # -------------------------------------------------------------------
368 # -------------------------------------------------------------------
369 # -------------------------------------------------------------------
371 ## ----- CIE-XYZ <-> Device independent RGB -----
372 ## R, G, and B give the levels of red, green and blue as values
373 ## in the interval [0., 1.]. X, Y and Z give the CIE chromaticies.
374 ## XN, YN, ZN gives the chromaticity of the white point.
375 def RGB_to_XYZ(self, R, G, B, XN = None, YN = None, ZN = None):
376 """Convert RGB to CIEXYZ
378 `R`, `G`, and `B` give the levels of red, green and blue as values
379 in the interval `[0., 1.]`.
380 `XN`, `YN`, and `ZN` allow to specify additional CIE chromaticities to
381 specify a specific white point.
383 Args:
384 R (numpy.ndarray): Intensities for red (`[0., 1.]`).
385 G (numpy.ndarray): Intensities for green (`[0., 1.]`).
386 B (numpy.ndarray): Intensities for blue (`[0., 1.]`).
387 XN (None, numpy.ndarray): Chromaticity of the white point. If of
388 length `1`, the white point specification will be recycled if needed.
389 When not specified (all `None`) a default white point is used.
390 YN: See `XN`.
391 ZN: See `XN`.
393 Returns:
394 list: Returns corresponding coordinates of CIE chromaticities, a
395 list of `numpy.ndarray`s of the same length as the inputs (`[X, Y, Z]`).
396 """
398 __fname__ = inspect.stack()[0][3] # Name of this method
399 n = len(R) # Number of colors
401 # Loading definition of white
402 [XN, YN, ZN] = self._get_white_(__fname__, n, XN, YN, ZN)
404 # Checking input
405 self._check_input_arrays_(__fname__, R = R, G = G, B = B)
407 return [YN * (0.412453 * R + 0.357580 * G + 0.180423 * B), # X
408 YN * (0.212671 * R + 0.715160 * G + 0.072169 * B), # Y
409 YN * (0.019334 * R + 0.119193 * G + 0.950227 * B)] # Z
411 def XYZ_to_RGB(self, X, Y, Z, XN = None, YN = None, ZN = None):
412 """Convert CIEXYZ to RGB
414 `X`, `Y`, and `Z` specify the values in the three coordinates of the
415 CIEXYZ color space,
416 `XN`, `YN`, and `ZN` allow to specify additional CIE chromaticities to
417 specify a specific white point.
419 Args:
420 X (numpy.ndarray): Values for the `X` dimension.
421 Y (numpy.ndarray): Values for the `Y` dimension.
422 Z (numpy.ndarray): Values for the `Z` dimension.
423 XN (None, numpy.ndarray): Chromaticity of the white point. If of
424 length `1`, the white point specification will be recycled if needed.
425 When not specified (all `None`) a default white point is used.
426 YN: See `XN`.
427 ZN: See `XN`.
429 Returns:
430 list: Returns corresponding coordinates as a list of
431 `numpy.ndarray`s of the same length as the inputs (`[R, G, B]`).
432 """
434 __fname__ = inspect.stack()[0][3] # Name of this method
435 n = len(X) # Number of colors
437 # Loading definition of white
438 [XN, YN, ZN] = self._get_white_(__fname__, n, XN, YN, ZN)
440 # Checking input
441 self._check_input_arrays_(__fname__, X = X, Y = Y, Z = Z)
443 # Only YN is used
444 return [( 3.240479 * X - 1.537150 * Y - 0.498535 * Z) / YN, # R
445 (-0.969256 * X + 1.875992 * Y + 0.041556 * Z) / YN, # G
446 ( 0.055648 * X - 0.204043 * Y + 1.057311 * Z) / YN] # B
449 # -------------------------------------------------------------------
450 # -------------------------------------------------------------------
451 # -------------------------------------------------------------------
452 # -------------------------------------------------------------------
455 ## Unused as we are going CIE-XYZ <-> RGB <-> sRGB
456 ##
457 ## ## ----- CIE-XYZ <-> sRGB -----
458 ## ## R, G, and B give the levels of red, green and blue as values
459 ## ## in the interval [0., 1.]. X, Y and Z give the CIE chromaticies.
460 ## ## XN, YN, ZN gives the chromaticity of the white point.
461 ## def sRGB_to_XYZ(self, R, G, B, XN = None, YN = None, ZN = None):
462 ## """sRGB to CIEXYZ.
464 ## R, G, and B give the levels of red, green and blue as values
465 ## in the interval `[0., 1.]`. X, Y and Z give the CIE chromaticies.
467 ## Args:
468 ## R (numpy.ndarray): Indensities for red (`[0., 1.]`).
469 ## G (numpy.ndarray): Indensities for green (`[0., 1.]`).
470 ## B (numpy.ndarray): Indensities for blue (`[0., 1.]`).
471 ## XN (None or numpy.ndarray): Chromaticity of the white point. If of
472 ## length 1 the white point specification will be recycled if length of
473 ## R/G/B is larger than one. If not specified (all three `None`) default
474 ## values will be used. Defaults to None, see also YN, ZN.
475 ## YN: See `XN`.
476 ## ZN: See `XN`.
478 ## Returns:
479 ## list: Returns corresponding X/Y/Z coordinates of CIE chromaticies, a list
480 ## of `numpy.ndarray`'s of the same length as the inputs (`[X, Y,
481 ## Z]`).
482 ## """
484 ## __fname__ = inspect.stack()[0][3] # Name of this method
485 ## n = len(R) # Number of colors
487 ## # Loading definition of white
488 ## [XN, YN, ZN] = self._get_white_(__fname__, n, XN, YN, ZN)
490 ## # Checking input
491 ## self._check_input_arrays_(__fname__, R = R, G = G, B = B)
493 ## # Transform R/G/B
494 ## R = self.ftrans(R, 2.4)
495 ## G = self.ftrans(G, 2.4)
496 ## B = self.ftrans(B, 2.4)
498 ## # Convert to X/Y/Z coordinates
499 ## return[YN * (0.412453 * R + 0.357580 * G + 0.180423 * B), # X
500 ## YN * (0.212671 * R + 0.715160 * G + 0.072169 * B), # Y
501 ## YN * (0.019334 * R + 0.119193 * G + 0.950227 * B)] # Z
503 ## def XYZ_to_sRGB(self, X, Y, Z, XN = None, YN = None, ZN = None):
504 ## """CIEXYZ to sRGB.
506 ## R, G, and B give the levels of red, green and blue as values
507 ## in the interval `[0., 1.]`. X, Y and Z give the CIE chromaticies.
509 ## Args:
510 ## X (numpy.ndarray): Values for the X dimension.
511 ## Y (numpy.ndarray): Values for the Y dimension.
512 ## Z (numpy.ndarray): Values for the Z dimension.
513 ## XN (None or numpy.ndarray): Chromaticity of the white point. If of
514 ## length 1 the white point specification will be recycled if length of
515 ## R/G/B is larger than one. If not specified (all three `None`) default
516 ## values will be used. Defaults to None, see also YN, ZN.
517 ## YN: See `XN`.
518 ## ZN: See `XN`.
520 ## Returns:
521 ## list: Returns corresponding X/Y/Z coordinates of CIE chromaticies, a list
522 ## of `numpy.ndarray`'s of the same length as the inputs (`[R, G, B]`).
523 ## """
525 ## __fname__ = inspect.stack()[0][3] # Name of this method
526 ## n = len(X) # Number of colors
528 ## # Loading definition of white
529 ## [XN, YN, ZN] = self._get_white_(__fname__, n, XN, YN, ZN)
531 ## # Checking input
532 ## self._check_input_arrays_(__fname__, X = X, Y = Y, Z = Z)
534 ## # Transform and return
535 ## return [self.gtrans(( 3.240479 * X - 1.537150 * Y - 0.498535 * Z) / YN, 2.4), # R
536 ## self.gtrans((-0.969256 * X + 1.875992 * Y + 0.041556 * Z) / YN, 2.4), # G
537 ## self.gtrans(( 0.055648 * X - 0.204043 * Y + 1.057311 * Z) / YN, 2.4)] # B
540 # -------------------------------------------------------------------
541 # -------------------------------------------------------------------
542 # -------------------------------------------------------------------
543 # -------------------------------------------------------------------
545 ## ----- CIE-XYZ <-> CIE-LAB ----- */
548 def LAB_to_XYZ(self, L, A, B, XN = None, YN = None, ZN = None):
549 """Convert CIELAB to CIEXYZ
551 `L`, `A`, and `B` specify the values in the three coordinates of the
552 CIELAB color space,
553 `XN`, `YN`, and `ZN` allow to specify additional CIE chromaticities to
554 specify a specific white point.
556 Args:
557 L (numpy.ndarray): Values for the `L` dimension.
558 A (numpy.ndarray): Values for the `A` dimension.
559 B (numpy.ndarray): Values for the `B` dimension.
560 XN (None, numpy.ndarray): Chromaticity of the white point. If of
561 length `1`, the white point specification will be recycled if needed.
562 When not specified (all `None`) a default white point is used.
563 YN: See `XN`.
564 ZN: See `XN`.
566 Returns:
567 list: Returns corresponding coordinates of CIE chromaticities as a
568 list of `numpy.ndarray`s of the same length as the inputs (`[X, Y, Z]`).
569 """
571 __fname__ = inspect.stack()[0][3] # Name of this method
572 n = len(L) # Number of colors
574 # Loading definition of white
575 [XN, YN, ZN] = self._get_white_(__fname__, n, XN, YN, ZN)
577 # Checking input
578 self._check_input_arrays_(__fname__, L = L, A = A, B = B)
580 # Result arrays
581 X = np.ndarray(len(L), dtype = "float"); X[:] = 0.
582 Y = np.ndarray(len(L), dtype = "float"); Y[:] = 0.
583 Z = np.ndarray(len(L), dtype = "float"); Z[:] = 0.
585 # Calculate Y
586 for i,val in np.ndenumerate(L):
587 if val <= 0: Y[i] = 0.
588 elif val <= 8.0: Y[i] = val * YN[i] / self._KAPPA
589 elif val <= 100.: Y[i] = YN[i] * np.power((val + 16.) / 116., 3.)
590 else: Y[i] = YN[i]
592 fy = np.ndarray(len(Y), dtype = "float")
593 for i,val in np.ndenumerate(Y):
594 if val <= (self._EPSILON * YN[i]):
595 fy[i] = (self._KAPPA / 116.) * val / YN[i] + 16. / 116.
596 else:
597 fy[i] = np.power(val / YN[i], 1. / 3.)
599 # Calculate X
600 fx = fy + (A / 500.)
601 for i,val in np.ndenumerate(fx):
602 if np.power(val, 3.) <= self._EPSILON:
603 X[i] = XN[i] * (val - 16. / 116.) / (self._KAPPA / 116.)
604 else:
605 X[i] = XN[i] * np.power(val, 3.)
607 # Calculate Z
608 fz = fy - (B / 200.)
609 for i,val in np.ndenumerate(fz):
610 if np.power(val, 3.) <= self._EPSILON:
611 Z[i] = ZN[i] * (val - 16. / 116.) / (self._KAPPA / 116.)
612 else:
613 Z[i] = ZN[i] * np.power(val, 3)
615 return [X, Y, Z]
617 def XYZ_to_LAB(self, X, Y, Z, XN = None, YN = None, ZN = None):
618 """Convert CIEXYZ to CIELAB
620 `X`, `Y`, and `Z` specify the values in the three coordinates of the
621 CIELAB color space,
622 `XN`, `YN`, and `ZN` allow to specify additional CIE chromaticities to
623 specify a specific white point.
625 Args:
626 X (numpy.ndarray): Values for the `X` dimension.
627 Y (numpy.ndarray): Values for the `Y` dimension.
628 Z (numpy.ndarray): Values for the `Z` dimension.
629 XN (None, numpy.ndarray): Chromaticity of the white point. If of
630 length `1`, the white point specification will be recycled if needed.
631 When not specified (all `None`) a default white point is used.
632 YN: See `XN`.
633 ZN: See `XN`.
635 Returns:
636 list: Returns corresponding coordinates of CIE chromaticities as
637 a list of `numpy.ndarray`s of the same length as the inputs (`[L, A, B]`).
638 """
640 __fname__ = inspect.stack()[0][3] # Name of this method
641 n = len(X) # Number of colors
643 # Loading definition of white
644 [XN, YN, ZN] = self._get_white_(__fname__, n, XN, YN, ZN)
646 # Checking input
647 self._check_input_arrays_(__fname__, X = X, Y = Y, Z = Z)
649 # Support function
650 def f(t, _KAPPA, _EPSILON):
651 for i,val in np.ndenumerate(t):
652 if val > _EPSILON:
653 t[i] = np.power(val, 1./3.)
654 else:
655 t[i] = (_KAPPA / 116.) * val + 16. / 116.
656 return t
658 # Scaling
659 xr = X / XN;
660 yr = Y / YN;
661 zr = Z / ZN;
663 # Calculate L
664 L = np.ndarray(len(X), dtype = "float"); L[:] = 0.
665 for i,val in np.ndenumerate(yr):
666 if val > self._EPSILON:
667 L[i] = 116. * np.power(val, 1./3.) - 16.
668 else:
669 L[i] = self._KAPPA * val
671 xt = f(xr, self._KAPPA, self._EPSILON);
672 yt = f(yr, self._KAPPA, self._EPSILON);
673 zt = f(zr, self._KAPPA, self._EPSILON);
674 return [L, 500. * (xt - yt), 200. * (yt - zt)] # [L, A, B]
677 # -------------------------------------------------------------------
678 # -------------------------------------------------------------------
679 # -------------------------------------------------------------------
680 # -------------------------------------------------------------------
682 ## Commented as not yet used
683 ##
684 ## def XYZ_to_HLAB(self, X, Y, Z, XN = None, YN = None, ZN = None):
685 ## """CIE-XYZ to Hunter LAB.
687 ## .. note::
688 ## Note that the Hunter LAB is no longer part of the public API,
689 ## but the code is still here in case needed.
691 ## Args:
692 ## X (numpy.ndarray): Values for the X dimension.
693 ## Y (numpy.ndarray): Values for the Y dimension.
694 ## Z (numpy.ndarray): Values for the Z dimension.
695 ## XN (None or numpy.ndarray): Chromaticity of the white point. If of
696 ## length 1 the white point specification will be recycled if length of
697 ## R/G/B is larger than one. If not specified (all three `None`) default
698 ## values will be used. Defaults to None, see also YN, ZN.
699 ## YN: See `XN`.
700 ## ZN: See `XN`.
702 ## Returns:
703 ## list: Returns corresponding Hunter LAB chromaticies, a list of
704 ## `numpy.ndarray`'s of the same length as the inputs (`[L, A, B]`).
705 ## """
707 ## __fname__ = inspect.stack()[0][3] # Name of this method
708 ## n = len(X) # Number of colors
710 ## # Loading definition of white
711 ## [XN, YN, ZN] = self._get_white_(__fname__, n, XN, YN, ZN)
713 ## # Checking input
714 ## self._check_input_arrays_(__fname__, X = X, Y = Y, Z = Z)
716 ## # Transform
717 ## X = X / XN; Y = Y / YN; Z = Z / ZN;
718 ## l = np.sqrt(Y);
719 ## return [10. * l, 17.5 * (((1.02 * X) - Y) / l), 7. * ((Y - (0.847 * Z)) / l)] # [L, A, B]
722 ## def HLAB_to_XYZ(self, L, A, B, XN = None, YN = None, ZN = None):
723 ## """Hunter LAB to CIE-XYZ.
725 ## .. note::
726 ## Note that the Hunter LAB is no longer part of the public API,
727 ## but the code is still here in case needed.
729 ## Args:
730 ## L (numpy.ndarray): Values for the L dimension.
731 ## A (numpy.ndarray): Values for the A dimension.
732 ## B (numpy.ndarray): Values for the B dimension.
733 ## XN (None or numpy.ndarray): Chromaticity of the white point. If of
734 ## length 1 the white point specification will be recycled if length of
735 ## R/G/B is larger than one. If not specified (all three `None`) default
736 ## values will be used. Defaults to None, see also YN, ZN.
737 ## YN: See `XN`.
738 ## ZN: See `XN`.
740 ## Returns:
741 ## list: Returns corresponding CIE-XYZ chromaticies, a list of
742 ## `numpy.ndarray`'s of the same length as the inputs (`[X, Y, Z]`).
743 ## """
745 ## __fname__ = inspect.stack()[0][3] # Name of this method
746 ## n = len(L) # Number of colors
748 ## # Loading definition of white
749 ## [XN, YN, ZN] = self._get_white_(__fname__, n, XN, YN, ZN)
751 ## # Checking input
752 ## self._check_input_arrays_(__fname__, L = L, A = A, B = B)
754 ## # Transform
755 ## vY = L / 10.;
756 ## vX = (A / 17.5) * (L / 10);
757 ## vZ = (B / 7) * (L / 10);
758 ## vY = vY * vY;
760 ## Y = vY * XN
761 ## X = (vX + vY) / 1.02 * YN
762 ## Z = - (vZ - vY) / 0.847 * ZN
764 ## return [X, Y, Z]
767 # -------------------------------------------------------------------
768 # -------------------------------------------------------------------
769 # -------------------------------------------------------------------
770 # -------------------------------------------------------------------
771 def LAB_to_polarLAB(self, L, A, B):
772 """Convert CIELAB to the polar representation (polarLAB)
774 Converts colors from the CIELAB color space into its polar
775 representation (`polarLAB`).
776 Inverse function of :py:method:`polarLAB_to_LAB`.
778 Args:
779 L (numpy.ndarray): Values for the `L` dimension.
780 A (numpy.ndarray): Values for the `A` dimension.
781 B (numpy.ndarray): Values for the `B` dimension.
783 Returns:
784 list: Returns corresponding polar LAB chromaticities as a list of
785 `numpy.ndarray`s of the same length as the inputs (`[L, A, B]`).
786 """
788 __fname__ = inspect.stack()[0][3] # Name of this method
790 # Checking input
791 self._check_input_arrays_(__fname__, L = L, A = A, B = B)
793 # Compute H
794 H = self._RAD2DEG(np.arctan2(B, A))
795 for i,val in np.ndenumerate(H):
796 while val > 360.: val -= 360.
797 while val < 0.: val += 360.
798 H[i] = val
799 # Compute C
800 C = np.sqrt(A * A + B * B)
802 return [L, C, H]
804 def polarLAB_to_LAB(self, L, C, H):
805 """Convert polarLAB to CIELAB
807 Convert colors from the polar representation of the CIELAB
808 color space into CIELAB coordinates.
809 Inverse function of :py:method:`LAB_to_polarLAB`.
811 Args:
812 L (numpy.ndarray): Values for the polar `L` dimension.
813 C (numpy.ndarray): Values for the polar `C` dimension.
814 H (numpy.ndarray): Values for the polar `H` dimension.
816 Returns:
817 list: Returns corresponding CIELAB chromaticities as a list of
818 `numpy.ndarray`s of the same length as the inputs (`[L, A, B]`).
819 """
821 __fname__ = inspect.stack()[0][3] # Name of this method
823 # Checking input
824 self._check_input_arrays_(__fname__, L = L, H = H, C = C)
826 A = np.cos(self._DEG2RAD(H)) * C
827 B = np.sin(self._DEG2RAD(H)) * C
829 return [L, A, B]
831 # -------------------------------------------------------------------
832 # -------------------------------------------------------------------
833 # -------------------------------------------------------------------
834 # -------------------------------------------------------------------
835 def sRGB_to_HSV(self, r, g, b):
836 """Convert RGB to HSV
838 Convert one (or multiple) rgb colors given their red, blue, and
839 red coodinates (`[0.0, 1.0]`) to their corresponding hue, saturation,
840 and value (HSV) coordinates.
842 Args:
843 r (numpy.ndarray): Intensities for red (`[0., 1.]`).
844 g (numpy.ndarray): Intensities for green (`[0., 1.]`).
845 b (numpy.ndarray): Intensities for blue (`[0., 1.]`).
847 Returns:
848 list: Returns a list of `numpy.ndarray`s with the corresponding
849 coordinates in the HSV color space (`[h, s, v]`). Same length as
850 the inputs.
851 """
853 __fname__ = inspect.stack()[0][3] # Name of this method
855 # Checking input
856 self._check_input_arrays_(__fname__, r = r, g = g, b = b)
858 # Support function
859 def gethsv(r, g, b):
860 x = np.min([r, g, b])
861 y = np.max([r, g, b])
862 if y != x:
863 f = g - b if r == x else b - r if g == x else r - g
864 i = 3. if r == x else 5. if g == x else 1.
865 h = 60. * (i - f /(y - x))
866 s = (y - x)/y
867 v = y
868 else:
869 h = 0.
870 s = 0.
871 v = y
872 return [h, s, v]
874 # Result arrays
875 h = np.ndarray(len(r), dtype = "float"); h[:] = 0.
876 s = np.ndarray(len(r), dtype = "float"); s[:] = 0.
877 v = np.ndarray(len(r), dtype = "float"); v[:] = 0.
879 # Calculate h/s/v
880 for i in range(0, len(r)):
881 tmp = gethsv(r[i], g[i], b[i])
882 h[i] = tmp[0]; s[i] = tmp[1]; v[i] = tmp[2]
884 return [h, s, v]
887 def HSV_to_sRGB(self, h, s, v):
888 """Convert HSV to Standard RGB (sRGB)
890 Takes a series of HSV coordinates and converts them
891 to the sRGB color space.
893 Args:
894 h (nympy.ndarray): Hue values.
895 s (numpy.ndarray): Saturation.
896 v (numpy.ndarray): Value (the value-dimension of HSV).
898 Returns:
899 list: Returns a list of `numpy.ndarray`s with the corresponding
900 coordinates in the sRGB color space (`[r, g, b]`). Same length as
901 the inputs.
902 """
904 __fname__ = inspect.stack()[0][3] # Name of this method
906 # Checking input
907 self._check_input_arrays_(__fname__, h = h, s = s, v = v)
909 # Support function
910 def getrgb(h, s, v):
912 # If Hue is not defined:
913 if h == np.nan: return np.repeat(v, 3)
915 # Convert to [0-6]
916 h = h / 60.
917 i = np.floor(h)
918 f = h - i
920 if (i % 2) == 0: # if i is even
921 f = 1 - f
923 m = v * (1 - s)
924 n = v * (1 - s * f)
925 if i in [0, 6]: return [v, n, m]
926 elif i == 1: return [n, v, m]
927 elif i == 2: return [m, v, n]
928 elif i == 3: return [m, n, v]
929 elif i == 4: return [n, m, v]
930 elif i == 5: return [v, m, n]
931 else:
932 raise Exception(f"ended up in a non-defined ifelse with i = {i:d}")
934 # Result arrays
935 r = np.ndarray(len(h), dtype = "float"); r[:] = 0.
936 g = np.ndarray(len(h), dtype = "float"); g[:] = 0.
937 b = np.ndarray(len(h), dtype = "float"); b[:] = 0.
939 for i in range(0,len(h)):
940 tmp = getrgb(h[i], s[i], v[i])
941 r[i] = tmp[0]; g[i] = tmp[1]; b[i] = tmp[2]
943 return [r, g, b]
946 # -------------------------------------------------------------------
947 # -------------------------------------------------------------------
948 # -------------------------------------------------------------------
949 # -------------------------------------------------------------------
950 def sRGB_to_HLS(self, r, g, b):
951 """Convert Standard RGB (sRGB) to HLS
953 All r/g/b values in `[0., 1.]`, h in `[0., 360.]`, l and s in `[0., 1.]`.
954 From: <http://wiki.beyondunreal.com/wiki/RGB_To_HLS_Conversion>.
956 Args:
957 r (numpy.ndarray): Intensities for red (`[0., 1.]`)
958 g (numpy.ndarray): Intensities for green (`[0., 1.]`)
959 b (numpy.ndarray): Intensities for blue (`[0., 1.]`)
961 Returns:
962 list: Returns a list of `numpy.ndarray`s with the corresponding
963 coordinates in the HLS color space (`[h, l, s]`). Same length as
964 the inputs.
965 """
967 __fname__ = inspect.stack()[0][3] # Name of this method
969 # Checking input
970 self._check_input_arrays_(__fname__, r = r, g = g, b = b)
972 # Support function
973 def gethls(r, g, b):
974 min = np.min([r, g, b])
975 max = np.max([r, g, b])
977 l = (max + min)/2.;
979 if max != min:
980 if l < 0.5: s = (max - min) / (max + min)
981 elif l >= 0.5: s = (max - min) / (2. - max - min)
983 if r == max: h = (g - b) / (max - min);
984 if g == max: h = 2. + (b - r) / (max - min);
985 if b == max: h = 4. + (r - g) / (max - min);
987 h = h * 60.;
988 if h < 0.: h = h + 360.;
989 if h > 360.: h = h - 360.;
990 else:
991 s = 0
992 h = 0;
994 return [h, l, s]
996 # Result arrays
997 h = np.ndarray(len(r), dtype = "float"); h[:] = 0.
998 l = np.ndarray(len(r), dtype = "float"); l[:] = 0.
999 s = np.ndarray(len(r), dtype = "float"); s[:] = 0.
1001 for i in range(0,len(h)):
1002 tmp = gethls(r[i], g[i], b[i])
1003 h[i] = tmp[0]; l[i] = tmp[1]; s[i] = tmp[2]
1005 return [h, l, s]
1008 def HLS_to_sRGB(self, h, l, s):
1009 """Convert HLC to Standard RGB (sRGB)
1011 All r/g/b values in `[0., 1.]`, h in `[0., 360.]`, l and s in `[0., 1.]`.
1013 Args:
1014 h (numpy.ndarray): Hue values.
1015 l (numpy.ndarray): Lightness.
1016 s (numpy.ndarray): Saturation.
1018 Returns:
1019 list: Returns a list of `numpy.ndarray`s with the corresponding
1020 coordinates in the sRGB color space (`[r, g, b]`). Same length as
1021 the inputs.
1022 """
1024 __fname__ = inspect.stack()[0][3] # Name of this method
1026 # Checking input
1027 self._check_input_arrays_(__fname__, h = h, l = l, s = s)
1029 # Support function
1030 def getrgb(h, l, s):
1031 p2 = l * (1. + s) if l <= 0.5 else l + s - (l * s)
1032 p1 = 2 * l - p2
1034 # If saturation is zero
1035 if (s == 0): return np.repeat(l, 3)
1036 # Else
1037 return [self._qtrans(p1, p2, h + 120.), # r
1038 self._qtrans(p1, p2, h), # g
1039 self._qtrans(p1, p2, h - 120.)] # b
1041 # Result arrays
1042 r = np.ndarray(len(h), dtype = "float"); r[:] = 0.
1043 g = np.ndarray(len(h), dtype = "float"); g[:] = 0.
1044 b = np.ndarray(len(h), dtype = "float"); b[:] = 0.
1046 for i in range(0,len(r)):
1047 tmp = getrgb(h[i], l[i], s[i])
1048 r[i] = tmp[0]; g[i] = tmp[1]; b[i] = tmp[2]
1050 return [r, g, b]
1053 # -------------------------------------------------------------------
1054 # -------------------------------------------------------------------
1055 # -------------------------------------------------------------------
1056 # -------------------------------------------------------------------
1057 def XYZ_to_uv(self, X, Y, Z):
1058 """Convert CIEXYZ to u and v
1060 Converting one (or multiple) colors defined by their X, Y, and Z
1061 coordinates in the CIEXYZ color space to their corresponding
1062 u and v coordinates.
1064 Args:
1065 X (numpy.ndarray): Values for the `Z` dimension.
1066 Y (numpy.ndarray): Values for the `Y` dimension.
1067 Z (numpy.ndarray): Values for the `Z` dimension.
1069 Returns:
1070 list: Returns a list of `numpy.ndarray`s (`[u, v]`).
1071 """
1073 __fname__ = inspect.stack()[0][3] # Name of this method
1075 # Checking input
1076 self._check_input_arrays_(__fname__, X = X, Y = Y, Z = Z)
1078 # Result array
1079 x = np.ndarray(len(X), dtype = "float"); x[:] = 0.
1080 y = np.ndarray(len(X), dtype = "float"); y[:] = 0.
1082 t = X + Y + Z
1083 idx = np.where(t != 0)
1084 x[idx] = X[idx] / t[idx];
1085 y[idx] = Y[idx] / t[idx];
1087 return [2.0 * x / (6. * y - x + 1.5), # u
1088 4.5 * y / (6. * y - x + 1.5)] # v
1090 def XYZ_to_LUV(self, X, Y, Z, XN = None, YN = None, ZN = None):
1091 """Convert CIEXYZ to CIELUV.
1093 `X`, `Y`, and `Z` specify the values in the three coordinates of the
1094 CIELAB color space,
1095 `XN`, `YN`, and `ZN` allow to specify additional CIE chromaticities to
1096 specify a specific white point.
1098 Args:
1099 X (numpy.ndarray): Values for the `X` dimension.
1100 Y (numpy.ndarray): Values for the `Y` dimension.
1101 Z (numpy.ndarray): Values for the `Z` dimension.
1102 XN (None, numpy.ndarray): Chromaticity of the white point. If of
1103 length `1`, the white point specification will be recycled if needed.
1104 When not specified (all `None`) a default white point is used.
1105 YN: See `XN`.
1106 ZN: See `XN`.
1108 Returns:
1109 list: Returns corresponding coordinates of CIE chromaticities as
1110 a list of `numpy.ndarray`s of the same length as the inputs (`[L, U, V]`).
1111 """
1113 __fname__ = inspect.stack()[0][3] # Name of this method
1114 n = len(X) # Number of colors
1116 # Loading definition of white
1117 [XN, YN, ZN] = self._get_white_(__fname__, n, XN, YN, ZN)
1119 # Checking input
1120 self._check_input_arrays_(__fname__, X = X, Y = Y, Z = Z)
1122 # Convert X/Y/Z and XN/YN/ZN to uv
1123 [u, v] = self.XYZ_to_uv(X, Y, Z )
1124 [uN, vN] = self.XYZ_to_uv(XN, YN, ZN)
1126 # Calculate L
1127 L = np.ndarray(len(X), dtype = "float"); L[:] = 0.
1128 y = Y / YN
1129 for i,val in np.ndenumerate(y):
1130 L[i] = 116. * np.power(val, 1./3.) - 16. if val > self._EPSILON else self._KAPPA * val
1132 # Calculate U/V
1133 return [L, 13. * L * (u - uN), 13. * L * (v - vN)] # [L, U, V]
1135 def LUV_to_XYZ(self, L, U, V, XN = None, YN = None, ZN = None):
1136 """Convert CIELUV to CIELAB
1138 `L`, `U`, and `V` specify the values in the three coordinates of the
1139 CIELAB color space,
1140 `XN`, `YN`, and `ZN` allow to specify additional CIE chromaticities to
1141 specify a specific white point.
1143 Args:
1144 L (numpy.ndarray): Values for the `L` dimension.
1145 U (numpy.ndarray): Values for the `U` dimension.
1146 V (numpy.ndarray): Values for the `V` dimension.
1147 XN (None, numpy.ndarray): Chromaticity of the white point. If of
1148 length `1`, the white point specification will be recycled if needed.
1149 When not specified (all `None`) a default white point is used.
1150 YN: See `XN`.
1151 ZN: See `XN`.
1153 Returns:
1154 list: Returns corresponding coordinates of CIE chromaticities as
1155 a list of `numpy.ndarray`s of the same length as the inputs (`[L, A, B]`).
1156 """
1158 __fname__ = inspect.stack()[0][3] # Name of this method
1159 n = len(L) # Number of colors
1161 # Loading definition of white
1162 [XN, YN, ZN] = self._get_white_(__fname__, n, XN, YN, ZN)
1164 # Checking input
1165 self._check_input_arrays_(__fname__, L = L, U = U, V = V)
1167 # Result arrays
1168 X = np.ndarray(len(L), dtype = "float"); X[:] = 0.
1169 Y = np.ndarray(len(L), dtype = "float"); Y[:] = 0.
1170 Z = np.ndarray(len(L), dtype = "float"); Z[:] = 0.
1172 # Check for which values we do have to do the transformation
1173 def fun(L, U, V):
1174 return False if L <= 0. and U == 0. and V == 0. else True
1175 idx = np.where([fun(L[i], U[i], V[i]) for i in range(0, len(L))])[0]
1176 if len(idx) == 0: return [X, Y, Z]
1178 # Compute Y
1179 for i in idx:
1180 Y[i] = YN[i] * (np.power((L[i] + 16.)/116., 3.) if L[i] > 8. else L[i] / self._KAPPA)
1182 # Calculate X/Z
1183 from numpy import finfo, fmax
1185 # Avoiding division by zero
1186 eps = np.finfo(float).eps*10
1187 L = fmax(eps, L)
1189 [uN, vN] = self.XYZ_to_uv(XN, YN, ZN)
1190 u = U / (13. * L) + uN
1191 v = V / (13. * L) + vN
1192 X = 9.0 * Y * u / (4 * v)
1193 Z = -X / 3. - 5. * Y + 3. * Y / v
1195 return [X, Y, Z]
1198 ## ----- LUV <-> polarLUV ----- */
1199 def LUV_to_polarLUV(self, L, U, V):
1200 """Convert CIELUV to the polar representation (polarLUV; HCL)
1202 Converts colors from the CIELUV color space into its polar
1203 representation (`polarLUV`). The `polarLUV` color space
1204 is also known as the HCL (Hue-Chroma-Luminance) color space
1205 which this package uses frequently, e.g., when creating
1206 efficient color maps. Inverse function of :py:method:`polarLUV_to_LUV`.
1208 Args:
1209 L (numpy.ndarray): Values for the `L` dimension.
1210 U (numpy.ndarray): Values for the `U` dimension.
1211 V (numpy.ndarray): Values for the `V` dimension.
1213 Returns:
1214 list: Returns corresponding polar LUV chromaticities as a list of
1215 `numpy.ndarray`s of the same length as the inputs (`[L, A, B]`),
1216 also known as `[H, C, L]` coordinates.
1217 """
1219 __fname__ = inspect.stack()[0][3] # Name of this method
1221 self._check_input_arrays_(__fname__, L = L, U = U, V = V)
1223 # Calculate polarLUV coordinates
1224 C = np.sqrt(U * U + V * V)
1225 H = self._RAD2DEG(np.arctan2(V, U))
1226 for i,val in np.ndenumerate(H):
1227 while val > 360: val -= 360.
1228 while val < 0.: val += 360.
1229 H[i] = val
1231 return [L, C, H]
1233 def polarLUV_to_LUV(self, L, C, H):
1234 """Convert Polar CIELUV (HCL) to CIELUV
1236 Convert colors from the polar representation of the CIELUV color space,
1237 also known as HCL (Hue-Chroma-Luminance) color space, into CIELAB
1238 coordinates. Inverse function of :py:method:`LUV_to_polarLUV`.
1240 Args:
1241 L (numpy.ndarray): Values for the polar `L` dimension (Luminance).
1242 C (numpy.ndarray): Values for the polar `C` dimension (Chroma).
1243 H (numpy.ndarray): Values for the polar `H` dimension (Hue).
1245 Returns:
1246 list: Returns corresponding CIELAB chromaticities as a list of
1247 `numpy.ndarray`s of the same length as the inputs (`[L, U, V]`).
1248 """
1250 __fname__ = inspect.stack()[0][3] # Name of this method
1252 # Checking input
1253 self._check_input_arrays_(__fname__, L = L, C = C, H = H)
1255 H = self._DEG2RAD(H)
1256 return [L, C * np.cos(H), C * np.sin(H)] # [L, U, V]
1259 def sRGB_to_hex(self, r, g, b, fixup = True):
1260 """Convert Standard RGB (sRGB) to Hex Colors
1262 Converting one (or multiple) colors defined by their red, green, and
1263 blue coordinates from the Standard RGB color space to hex colors.
1265 Args:
1266 r (numpy.ndarray): Intensities for red (`[0., 1.,]`).
1267 g (numpy.ndarray): Intensities for green (`[0., 1.,]`).
1268 b (numpy.ndarray): Intensities for blue (`[0., 1.,]`).
1269 fixup (bool): Whether or not the `rgb` values should be corrected
1270 if they lie outside the defined RGB space (outside `[0., 1.,]`),
1271 defaults to `True`.
1273 Returns:
1274 list: A list with hex color str.
1275 """
1277 # Color fixup: limit r/g/b to [0-1]
1278 def rgbfixup(r, g, b):
1279 def fun(x):
1280 return np.asarray([np.max([0, np.min([1, e])]) \
1281 if np.isfinite(e) else np.nan for e in x])
1282 return [fun(r), fun(g), fun(b)]
1284 def rgbcleanup(r, g, b):
1285 def fun(x):
1286 tol = 1. / (2 * 255.)
1287 # Allow tiny correction close to 0. and 1.
1288 x[np.logical_and(x < 0.0, x >= -tol)] = 0.0
1289 x[np.logical_and(x > 1.0, x <= 1.0 + tol)] = 1.0
1290 return np.asarray([e if np.logical_and(e >= 0., e <= 1.)
1291 else np.nan for e in x])
1292 return [fun(r), fun(g), fun(b)]
1294 # Checking which r/g/b values are outside limits.
1295 # This only happens if fixup = FALSE.
1296 def validrgb(r, g, b):
1297 idxr = np.isfinite(r)
1298 idxg = np.isfinite(g)
1299 idxb = np.isfinite(b)
1300 return np.where(idxr * idxg * idxb)[0]
1302 # Support function to create hex coded colors
1303 def gethex(r, g, b):
1305 # Converts int to hex string
1306 def applyfun(x):
1307 x = np.asarray(x * 255. + .5, dtype = int)
1308 return f"#{x[0]:02X}{x[1]:02X}{x[2]:02X}"
1310 h = np.vstack([r,g,b]).transpose().flatten().reshape([len(r), 3])
1311 return np.apply_along_axis(applyfun, 1, h)
1313 # Let's do the conversion!
1314 if fixup: [r, g, b] = rgbfixup(r, g, b)
1315 else: [r, g, b] = rgbcleanup(r, g, b)
1317 # Create return array
1318 res = np.ndarray(len(r), dtype = "|S7"); res[:] = ""
1320 # Check valid r/g/b coordinates
1321 valid = validrgb(r, g, b)
1322 if len(valid) > 0:
1323 # Convert valid colors to hex
1324 res[valid] = gethex(r[valid], g[valid], b[valid])
1326 # Create return list with NAN's for invalid colors
1327 res = [None if len(x) == 0 else x.decode() for x in res]
1329 # Return numpy array
1330 return np.asarray(res)
1332 def hex_to_sRGB(self, hex_, gamma = 2.4):
1333 """Convert Hex Colors to Standard RGB (sRGB)
1335 Convert one (or multiple) hex colors to sRGB.
1337 Args:
1338 hex_ (str, list of str): hex color str or list of str.
1339 gamma (float): Gamma correction factor, defaults to `2.4`.
1341 Returns:
1342 list: Returns a list of `numpy.ndarray`s with the corresponding
1343 red, green, and blue intensities (`[r, g, b]`), all in `[0., 1.]`.
1344 """
1346 if isinstance(hex_,str): hex_ = [hex_]
1347 hex_ = np.asarray(hex_)
1349 # Check for valid hex colors
1350 def validhex(hex_):
1351 from re import compile
1352 pat = compile("^#[0-9A-Fa-f]{6}([0-9]{2})?$")
1353 from re import match
1354 return np.where([None if x is None else pat.match(x) is not None for x in hex_])[0]
1356 # Convert hex to rgb
1357 def getrgb(x):
1358 def applyfun(x):
1359 return np.asarray([int(x[i:i+2], 16) for i in (1, 3, 5)])
1360 rgb = [applyfun(e) for e in x]
1361 rgb = np.vstack(rgb).transpose().flatten().reshape([3,len(x)])
1362 return [rgb[0] / 255., rgb[1] / 255., rgb[2] / 255.]
1364 # Result arrays
1365 r = np.ndarray(len(hex_), dtype = "float"); r[:] = np.nan
1366 g = np.ndarray(len(hex_), dtype = "float"); g[:] = np.nan
1367 b = np.ndarray(len(hex_), dtype = "float"); b[:] = np.nan
1369 # Check valid hex colors
1370 valid = validhex(hex_)
1371 if not len(valid) == 0:
1372 # Decode valid hex strings
1373 rgb = getrgb(hex_[valid])
1374 r[valid] = rgb[0]
1375 g[valid] = rgb[1]
1376 b[valid] = rgb[2]
1378 return [r, g, b]
1381 # -------------------------------------------------------------------
1382 # Direct conversion ('shortcut') from RGB to HLS
1383 def RGB_to_HLS(self, r, g, b):
1384 """Convert RGB to HLS
1386 Shortcut from RGB to HLS (not via sRGB).
1387 All r/g/b values in `[0., 1.]`, h in `[0., 360.]`, l and s in `[0., 1.]`.
1389 Args:
1390 r (numpy.ndarray): Intensities for red (`[0., 1.]`)
1391 g (numpy.ndarray): Intensities for green (`[0., 1.]`)
1392 b (numpy.ndarray): Intensities for blue (`[0., 1.]`)
1394 Returns:
1395 list: Returns a list of `numpy.ndarray`s with the corresponding
1396 coordinates in the HLS color space (`[h, l, s]`). Same length as
1397 the inputs.
1398 """
1400 __fname__ = inspect.stack()[0][3] # Name of this method
1402 # Checking input
1403 self._check_input_arrays_(__fname__, r = r, g = g, b = b)
1405 # Create 2d numpy array where the first dimension corresponds
1406 # to specific colors, the second one to [r, g, b] of that color.
1407 tmp = np.transpose(np.stack((r, g, b)))
1409 def gethls(x):
1410 """x is expected to be a numpy array of length 3 with [r, g, b] coordinates."""
1412 mn = np.min(x)
1413 mx = np.max(x)
1415 # If minimum equals maximum we know the solution already
1416 if mn == mx: return [0., mn, 0.] # [h, l, s]
1418 # Else do the calculations
1419 l = (mn + mx) / 2.
1420 s = (mx - mn) / (mx + mn) if l < 0.5 else (mx - mn) / (2. - mx - mn)
1422 # x[0] is 'r', x[1] = 'g', x[2] = 'b'
1423 if x[0] == mx: h = 60. * (x[1] - x[2]) / (mx - mn)
1424 elif x[1] == mx: h = 60. * (2. + (x[2] - x[0]) / (mx - mn))
1425 else: h = 60. * (4. + (x[0] - x[1]) / (mx - mn))
1427 if h < 0.: h = h + 360.
1428 elif h > 360.: h = h - 360.
1430 return [h, l, s]
1433 return np.transpose([gethls(x) for x in tmp])
1436 # -------------------------------------------------------------------
1437 # Direct conversion ('shortcut') from HLS to RGB
1438 def HLS_to_RGB(self, h, l, s):
1439 """Convert HLS to RGB
1441 Shortcut from HLS to RGB (not via sRGB). Expecting h in `[0., 360.]`,
1442 l/s in `[0., 1.]`. Returns r/g/b in `[0.,1.]`.
1444 Args:
1445 h (numpy.ndarray): Hue (`[0., 360.]`)
1446 l (numpy.ndarray): Luminance (`[0., 1.]`)
1447 s (numpy.ndarray): Saturation (`[0., 1.]`)
1449 Returns:
1450 list: Returns a list of `numpy.ndarray`s with the corresponding
1451 coordinates in the RGB color space (`[r, g, b]`). Same length as
1452 the inputs.
1453 """
1455 __fname__ = inspect.stack()[0][3] # Name of this method
1457 # Checking input
1458 self._check_input_arrays_(__fname__, h = h, l = l, s = s)
1460 # Create 2d numpy array where the first dimension corresponds
1461 # to specific colors, the second one to [r, g, b] of that color.
1462 tmp = np.transpose(np.stack((h, l, s)))
1464 def getrgb(x):
1465 """x is expected to be a numpy array of length 3 with [h, l, s] coordinates."""
1467 # If saturation equals zero, return [l, l, l]
1468 if x[2] == 0.: return [x[1], x[1], x[1]]
1470 # x[0] = 'h', x[1] = 'l', x[2] = 's'
1471 p2 = x[1] * (1 + x[2]) if x[1] <= 0.5 else x[1] + x[2] - (x[1] * x[2])
1472 p1 = 2 * x[1] - p2
1474 return [self._qtrans(p1, p2, x[0] + 120.),
1475 self._qtrans(p1, p2, x[0]),
1476 self._qtrans(p1, p2, x[0] - 120.)]
1478 return np.transpose([getrgb(x) for x in tmp])
1480 # -------------------------------------------------------------------
1481 # Direct conversion ('shortcut') from RGB to HSV
1482 def RGB_to_HSV(self, r, g, b):
1483 """Convert RGB to HSV
1485 Shortcut from RGB to HSV (not via sRGB).
1486 All r/g/b values in `[0., 1.]`, h in `[0., 360.]`, l and s in `[0., 1.]`.
1488 Args:
1489 r (numpy.ndarray): Intensities for red (`[0., 1.]`)
1490 g (numpy.ndarray): Intensities for green (`[0., 1.]`)
1491 b (numpy.ndarray): Intensities for blue (`[0., 1.]`)
1493 Returns:
1494 list: Returns a list of `numpy.ndarray`s with the corresponding
1495 coordinates in the HSV color space (`[h, s, v]`). Same length as
1496 the inputs.
1497 """
1499 __fname__ = inspect.stack()[0][3] # Name of this method
1501 # Checking input
1502 self._check_input_arrays_(__fname__, r = r, g = g, b = b)
1504 # Create 2d numpy array where the first dimension corresponds
1505 # to specific colors, the second one to [r, g, b] of that color.
1506 tmp = np.transpose(np.stack((r, g, b)))
1508 def gethsv(x):
1509 """x is expected to be a numpy array of length 3 with [r, g, b] coordinates."""
1511 mn = np.min(x)
1512 mx = np.max(x)
1514 # If minimum equals maximum we know the solution already
1515 if mn == mx: return [0., 0., mx] # [h, s, v]
1517 # Else calculate new dimensions
1518 f = (x[1] - x[2]) if x[0] == mn else x[2] - x[0] if x[1] == mn else x[0] - x[1]
1519 i = 3. if x[0] == mn else 5. if x[1] == mn else 1.
1521 # Returning [h, s, v]
1522 return [60. * (i - f / (mx - mn)), (mx - mn) / mx, mx]
1524 return np.transpose([gethsv(x) for x in tmp])
1527 # -------------------------------------------------------------------
1528 # Direct conversion ('shortcut') from HSV to RGB
1529 def HSV_to_RGB(self, h, s, v):
1530 """Convert HSV to RGB
1532 Shortcut from HLS to RGB (not via sRGB). Expecting h in `[0., 360.]`,
1533 l/s in `[0., 1.]`. Returns r/g/b in `[0.,1.]`.
1535 Args:
1536 h (numpy.ndarray): Hue (`[0., 360.]`)
1537 s (numpy.ndarray): Saturation (`[0., 1.]`)
1538 v (numpy.ndarray): Value (`[0., 1.]`)
1540 Returns:
1541 list: Returns a list of `numpy.ndarray`s with the corresponding
1542 coordinates in the RGB color space (`[r, g, b]`). Same length as
1543 the inputs.
1544 """
1546 __fname__ = inspect.stack()[0][3] # Name of this method
1548 # Checking input
1549 self._check_input_arrays_(__fname__, h = h, s = s, v = v)
1551 # Create 2d numpy array where the first dimension corresponds
1552 # to specific colors, the second one to [r, g, b] of that color.
1553 tmp = np.transpose(np.stack((h, s, v)))
1555 def getrgb(x):
1556 """x is expected to be a numpy array of length 3 with [h, s, v] coordinates."""
1558 h = x[0] / 60. # Convert to [0, 6]
1559 i = np.int8(np.floor(h))
1560 f = h - i
1561 if i % 2 == 0: f = 1. - f # if i is even
1563 m = x[2] * (1. - x[1])
1564 n = x[2] * (1. - x[1] * f)
1566 if i == 0 or i == 6: return [x[2], n, m]
1567 elif i == 1: return [n, x[2], m]
1568 elif i == 2: return [m, x[2], n]
1569 elif i == 3: return [m, n, x[2]]
1570 elif i == 4: return [n, m, x[2]]
1571 elif i == 5: return [x[2], m, n]
1573 return np.transpose([getrgb(x) for x in tmp])
1576# -------------------------------------------------------------------
1577# Color object base class
1578# will be extended by the different color classes.
1579# -------------------------------------------------------------------
1580class colorobject:
1581 """Superclass for All Color Objects
1583 A series of constructors are available to construct `colorobjects` in a
1584 variety of different color spaces, all inheriting from this class. This
1585 superclass provides the general functionality to handle colors (sets of
1586 colors) and convert colors from and to different color spaces.
1588 Users should use the dedicated classes for the available color spaces which
1589 all extend this class. These are: CIELAB, CIELUV, CIEXYZ, hexcols, HLS,
1590 HSV, polarLAB, polarLUV, RGB, and sRGB.
1591 """
1593 import numpy as np
1595 # Allowed/defined color spaces
1596 ALLOWED = ["CIEXYZ", "CIELUV", "CIELAB", "polarLUV", "polarLAB",
1597 "RGB", "sRGB", "HCL", "HSV", "HLS", "hex"]
1598 """List of allowed/defined color spaces; used to check when converting
1599 colors from one color space to another."""
1601 # Used to store alpha if needed. Will only be used for some of
1602 # the colorobject objects as only few color spaces allow alpha
1603 # values.
1604 ALPHA = None
1605 """Used to store (keep) transparency when needed; will be dropped during conversion."""
1607 GAMMA = 2.4 # Used to adjust RGB (sRGB_to_RGB and back).
1608 """Gamma value used used to adjust RGB colors; currently a fixed value of 2.4."""
1610 # Standard representation of colorobject objects.
1611 def __repr__(self, digits = 2):
1612 """Color Object Standard Representation
1614 Standard representation of the color object; shows the values
1615 of all coordinates (or the hex strings if hex colors).
1617 Args:
1618 digits (int): Number of digits, defaults to `2`.
1620 Returns:
1621 str: Returns a str of the colors/coordinates of the current object.
1622 """
1623 dims = list(self._data_.keys()) # Dimensions
1625 from .colorlib import hexcols
1626 import numpy as np
1628 # Sorting the dimensions
1629 from re import match
1630 if match("^(hex_|alpha){1,2}$", "".join(dims)): dims = ["hex_"]
1631 elif match("^(R|G|B|alpha){3,4}$", "".join(dims)): dims = ["R", "G", "B"]
1632 elif match("^(L|A|B|alpha){3,4}$", "".join(dims)): dims = ["L", "A", "B"]
1633 elif match("^(L|U|V|alpha){3,4}$", "".join(dims)): dims = ["L", "U", "V"]
1634 elif match("^(H|C|L|alpha){3,4}$", "".join(dims)): dims = ["H", "C", "L"]
1635 elif match("^(X|Y|Z|alpha){3,4}$", "".join(dims)): dims = ["X", "Y", "Z"]
1636 elif match("^(H|S|V|alpha){3,4}$", "".join(dims)): dims = ["H", "S", "V"]
1637 elif match("^(H|L|S|alpha){3,4}$", "".join(dims)): dims = ["H", "L", "S"]
1639 # Number of colors
1640 ncol = max([0 if self._data_[x] is None else len(self._data_[x]) for x in dims])
1642 # Add 'alpha' to object 'dims' if we have defined alpha values
1643 # for this colorobject. Else alpha will not be printed.
1644 if "alpha" in list(self._data_.keys()):
1645 if self._data_["alpha"] is not None: dims += ["alpha"]
1647 # Start creating the string:
1648 res = ["{:s} color object ({:d} colors)".format(self.__class__.__name__, ncol)]
1650 # Show header
1651 fmt = "".join(["{:>", "{:d}".format(digits + 6), "s}"])
1652 res.append(" " + "".join([fmt.format(x) for x in dims]))
1654 # Show data
1655 # In case of a hexcols object: string formatting and
1656 # nan-replacement beforehand.
1657 if isinstance(self, hexcols):
1658 data = {}
1659 fmt = "".join(["{:", "{:d}.{:d}".format(6 + digits, 3), "f}"])
1660 data["hex_"] = np.ndarray(ncol, dtype = "|S7")
1661 for n in range(0, ncol):
1662 x = self._data_["hex_"][n]
1663 if x is None:
1664 data["hex_"][n] = None
1665 else:
1666 data["hex_"][n] = fmt.format(x) if isinstance(x, float) else x[0:7]
1667 data["alpha"] = self.get("alpha")
1668 fmt = "{:<10s}"
1669 else:
1670 fmt = "".join(["{:", "{:d}.{:d}".format(6+digits, digits), "f}"])
1671 data = self._data_
1673 # Print object content
1674 count = 0
1675 for n in range(0, ncol):
1676 if (n % 10) == 0:
1677 tmp = "{:3d}: ".format(n+1)
1678 else:
1679 tmp = " "
1680 for d in dims:
1681 # Special handling for alpha
1682 if d == "alpha":
1683 if data[d][n] is None:
1684 tmp += " ---"
1685 elif isinstance(data[d][n], float):
1686 if np.isnan(data[d][n]):
1687 tmp += " ---"
1688 elif isinstance(self, hexcols):
1689 tmp += " {:02X}".format(int(255. * data[d][n]))
1690 else:
1691 tmp += " {:4.2f}".format(data[d][n])
1692 else:
1693 if data[d] is None or data[d][n] is None:
1694 tmp += " ---"
1695 elif isinstance(data[d][n], str) or isinstance(data[d][n], np.bytes_):
1696 tmp += fmt.format(data[d][n])
1697 else:
1698 tmp += fmt.format(float(data[d][n]))
1700 count += 1
1701 res.append(tmp)
1703 if count >= 30 and ncol > 40:
1704 res.append("".join([" ......"]*len(dims)))
1705 res.append("And {:d} more [truncated]".format(ncol - count))
1706 break
1708 return "\n".join(res)
1710 def __call__(self, fixup = True, rev = False):
1711 """Magic Method
1713 Default call method of all color objects. Always returns
1714 hex colors, same as the `.colors()` method does.
1716 Args:
1717 fixup (bool): Fix colors outside defined color space, defaults to `True`.
1718 rev (bool): Revert colors, defaults to `False`.
1720 Returns:
1721 list: Returns a list of hex colors.
1722 """
1723 return self.colors(fixup = fixup, rev = rev)
1725 def __iter__(self):
1726 self.n = -1
1727 return self
1729 def __next__(self):
1730 if self.n < (self.length() - 1):
1731 self.n += 1
1732 res = self[self.n]
1733 return res
1734 else:
1735 raise StopIteration
1737 def __getitem__(self, key):
1738 if not isinstance(key, int):
1739 raise TypeError("argument `key` must be int (index)")
1741 from copy import deepcopy
1742 from numpy import array, newaxis
1743 res = deepcopy(self)
1744 for n in list(res._data_.keys()):
1745 # If None: keep it as it is, else subset
1746 if res._data_[n] is None: continue
1747 res._data_[n] = res._data_[n][newaxis, key]
1749 return res
1752 def get_whitepoint(self):
1753 """Get White Point
1755 This method returns the definition of the white point in use. If not
1756 explicitly set via the :py:method:`set_whitepoint` method, a default white
1757 point is used.
1759 Returns:
1760 dict: Returns a dict with `X`, `Y`, `Z`, the white point specification
1761 for the three dimensions.
1763 Example:
1765 >>> from colorspace import hexcols
1766 >>> c = hexcols("#ff0000")
1767 >>> c.get_whitepoint()
1768 """
1769 return {"X": self.WHITEX, "Y": self.WHITEY, "Z": self.WHITEZ}
1771 def set_whitepoint(self, **kwargs):
1772 """Set White Point
1774 A white point definition is used to adjust the colors.
1775 This method allows to set custom values. If not explicitly
1776 set a default specification is used. The :py:method:`get_whitepoint`
1777 method can be used to extract the currently used definition.
1779 Args:
1780 **kwargs: Named arguments. Allowed are `X`, `Y`, and `Z`,
1781 each of which must be float: White specification for
1782 dimension `X`/`Y`/`Z`.
1784 Example:
1786 >>> from colorspace import hexcols
1787 >>> c = hexcols("#ff0000")
1788 >>> c.set_whitepoint(X = 100., Y = 100., Z = 101.)
1789 >>> c.get_whitepoint()
1791 Raises:
1792 ValueError: If named argument is not one of `X`, `Y`, `Z`.
1793 """
1794 for key,arg in kwargs.items():
1795 if key == "X": self.WHITEX = float(arg)
1796 elif key == "Y": self.WHITEY = float(arg)
1797 elif key == "Z": self.WHITEZ = float(arg)
1798 else:
1799 raise ValueError(f"error in .set_whitepoint: " + \
1800 "argument \"{key}\" not recognized.")
1803 def _check_if_allowed_(self, x):
1804 """Check for Valid Transformation
1806 Helper function checking if the transformation of the current
1807 object into another color space is allowed or not.
1808 An exception will be thrown if the transformation is not possible.
1810 Args:
1811 x (str): Name of the target color space.
1813 Returns:
1814 No return, raises an Exception if the transformation is invalid.
1815 """
1816 if not x in self.ALLOWED:
1817 raise Exception(f"transformation from {self.__class__.__name__}" + \
1818 f" to \"{x}\" is unknown (not implemented). " + \
1819 f"The following are allowed: {', '.join(self.ALLOWED)}")
1820 return
1823 def _transform_via_path_(self, via, fixup):
1824 """Transform Colors along Path
1826 Helper function to transform a colorobject into a new color
1827 space. Calls the :py:func:`to` method one or multiple times along 'a path'
1828 as specified by `via`.
1830 Returns:
1831 No return, converts the current color space object (see method :py:func:`to`).
1833 Args:
1834 via (list of str): The path via which the current color object
1835 should be transformed. For example: A :py:class:`hexcols`
1836 object can be transformed into CIEXYZ by specifying
1837 `via = ["sRGB", "RGB", "CIEXYZ"]`.
1838 fixup (bool): Whether or not to correct invalid rgb values outside
1839 `[0., 1.]` if necessary
1840 """
1841 for v in via: self.to(v, fixup = fixup)
1843 def _colorobject_check_input_arrays_(self, **kwargs):
1844 """Colorobject Check User Input
1846 Checks if all inputs in `**kwargs` are of type `numpy.ndarray` OR lists
1847 (will be converted to `numpy.ndarray`s) and that all are of the same length.
1848 If not, the script will throw an exception.
1850 If `alpha` is given it is handled in a special way. If `alpha = None`
1851 it will simply be dropped (no alpha channel specified), else it is
1852 handled like the rest and has to fulfill the requirements all the
1853 other dimensions have to (length and type).
1855 Args:
1856 **kwargs: Named keywords, objects to be checked.
1858 Returns:
1859 bool: Returns `True` if all checks where fine, throws an exception
1860 if the inputs do not fulfil the requirements.
1861 """
1863 from numpy import asarray, float64
1865 # Message will be dropped if problems occur
1866 msg = f"Problem while checking inputs \"{', '.join(kwargs.keys())}\" " + \
1867 f"to class \"{self.__class__.__name__}\"."
1869 res = {}
1870 lengths = []
1871 keys_to_check = []
1872 for key,val in kwargs.items():
1873 # No alpha provided, simply proceed
1874 if key == "alpha" and val is None: continue
1876 keys_to_check.append(key)
1878 # If is list: convert to ndarray no matter how long the element is
1879 if isinstance(val, float) or isinstance(val, int):
1880 val = np.asarray([val])
1881 elif isinstance(val,list):
1882 try:
1883 val = np.asarray(val)
1884 except Exception as e:
1885 raise Exception(e)
1888 # For alpha, R, G, and B: check range
1889 if isinstance(self, RGB) or isinstance(self, sRGB):
1890 if np.max(val) > 1. or np.max(val) < 0.:
1891 raise ValueError("wrong values specified for " + \
1892 f"dimension {key} in {self.__class__.__name__}: " + \
1893 "values have to lie within [0., 1.]")
1895 # Check object type
1896 from numpy import asarray
1897 try:
1898 val = asarray(val)
1899 except Exception as e:
1900 raise ValueError(f"input {key} to {self.__class__.__name__}" + \
1901 f" could not have been converted to `numpy.ndarray`: {str(e)}")
1903 # Else append length and proceed
1904 lengths.append(len(val))
1906 # Append to result vector
1907 if isinstance(val, int) or isinstance(val, float): val = [val]
1908 res[key] = val if key == "hex_" else asarray(val, float64)
1910 # Check if all do have the same length
1911 if not np.all([x == lengths[0] for x in lengths]):
1912 msg += " Arguments of different lengths: {:s}".format(
1913 ", ".join(["{:s} = {:d}".format(keys_to_check[i], lengths[i]) \
1914 for i in range(0, len(keys_to_check))]))
1915 raise ValueError(msg)
1917 return res
1920 def hasalpha(self):
1921 """Check for Alpha Channel
1923 Helper method to check if the current color object has
1924 an alpha channel or not.
1926 Examples:
1928 >>> from colorspace import sRGB
1929 >>> x1 = sRGB(R = 0.5, G = 0.1, B = 0.3)
1930 >>> x1
1931 >>> #:
1932 >>> x2 = sRGB(R = 0.5, G = 0.1, B = 0.3, alpha = 0.5)
1933 >>> x2
1934 >>> #: Checking both color objects for alpha channel
1935 >>> [x1.hasalpha(), x2.hasalpha()]
1937 Returns:
1938 bool: `True` if alpha values are present, `False` if not.
1939 """
1940 if not "alpha" in self._data_.keys():
1941 return False
1942 elif self._data_["alpha"] is None:
1943 return False
1944 else:
1945 return True
1948 def dropalpha(self):
1949 """Remove Alpha Channel
1951 Remove alpha channel from the color object, if defined
1952 (see :py:method:`hasalpha`). Works for all `colorobject`s.
1954 Examples:
1956 >>> from colorspace.colorlib import HCL, sRGB, HSV
1957 >>> # Example using HCL colors
1958 >>> cols = HCL([0, 40, 80],
1959 >>> [30, 60, 80],
1960 >>> [85, 60, 35],
1961 >>> alpha = [1.0, 0.5, 0.1])
1962 >>> cols # with alpha channel
1963 >>> #:
1964 >>> cols.dropalpha()
1965 >>> cols # alpha channel removed
1966 >>>
1967 >>> #: No effect if there is no alpha channel
1968 >>> cols.dropalpha()
1969 >>> cols
1970 >>>
1971 >>> #: Example using sRGB colors
1972 >>> cols = sRGB([0.01, 0.89, 0.56],
1973 >>> [0.25, 0.89, 0.02],
1974 >>> [0.65, 0.89, 0.23],
1975 >>> alpha = [1.0, 0.5, 0.1])
1976 >>> cols # with alpha channel
1977 >>> #:
1978 >>> cols.dropalpha()
1979 >>> cols # alpha channel removed
1980 >>>
1981 >>> #: Example using HSV colors
1982 >>> cols = HSV([218, 0, 336],
1983 >>> [1, 0, 1],
1984 >>> [0.65, 0.89, 0.56],
1985 >>> alpha = [1.0, 0.5, 0.1])
1986 >>> cols # with alpha channel
1987 >>> #:
1988 >>> cols.dropalpha()
1989 >>> cols # alpha channel removed
1992 """
1993 if self.hasalpha():
1994 del self._data_["alpha"]
1997 def specplot(self, **kwargs):
1998 """Color Spectrum Plot
2000 Visualization of the spectrum of this color object.
2001 Internally calls :py:func:`specplot <colorspace.specplot.specplot>`,
2002 additional arguments to this main function can be forwarded via the
2003 `**kwargs` argument.
2005 Args:
2006 **kwargs: Additional named arguments forwarded to
2007 :py:func:`specplot <colorspace.specplot.specplot>`.
2009 Return:
2010 Returns what :py:func:`colorspace.specplot.specplot` returns.
2012 Example:
2014 >>> # Example using HCL colors
2015 >>> from colorspace import HCL, hexcols
2016 >>> cols = HCL(H = [220, 196, 172, 148, 125],
2017 >>> C = [ 44, 49, 55, 59, 50],
2018 >>> L = [ 49, 61, 72, 82, 90])
2019 >>> cols.specplot(figsize = (8, 4));
2020 >>>
2021 >>> #: Example using hex colors
2022 >>> cols = hexcols(["#0FCFC0", "#9CDED6", "#D5EAE7",
2023 >>> "#F1F1F1", "#F3E1EB", "#F6C4E1", "#F79CD4"])
2024 >>> cols.specplot(rgb = True, hcl = True, palette = True)
2026 """
2027 from copy import copy
2028 cols = copy(self)
2029 cols.to("hex")
2031 from .specplot import specplot
2032 return specplot(cols.colors(), **kwargs)
2035 def swatchplot(self, **kwargs):
2036 """Palette Swatch Plot
2038 Visualization the color palette of this color object.
2039 Internally calls :py:func:`swatchplot <colorspace.swatchplot.swatchplot>`,
2040 additional arguments to this main function can be forwarded via the
2041 `**kwargs` argument.
2043 Args:
2044 **kwargs: Additional named arguments forwarded to
2045 :py:func:`swatchplot <colorspace.swatchplot.swatchplot>`.
2047 Return:
2048 Returns what :py:func:`colorspace.swatchplot.swatchplot` returns.
2050 Example:
2052 >>> # Example using HCL colors
2053 >>> from colorspace import HCL, hexcols
2054 >>> cols = HCL(H = [220, 196, 172, 148, 125],
2055 >>> C = [ 44, 49, 55, 59, 50],
2056 >>> L = [ 49, 61, 72, 82, 90])
2057 >>> cols.swatchplot(figsize = (8, 2))
2058 >>>
2059 >>> #: Example using hex colors
2060 >>> cols = hexcols(["#0FCFC0", "#9CDED6", "#D5EAE7",
2061 >>> "#F1F1F1", "#F3E1EB", "#F6C4E1", "#F79CD4"])
2062 >>> cols.swatchplot(figsize = (8, 3.5));
2063 """
2065 from .swatchplot import swatchplot
2066 if "show_names" in kwargs.keys():
2067 del kwargs["show_names"]
2068 return swatchplot(pals = self.colors(), show_names = False, **kwargs)
2071 def hclplot(self, **kwargs):
2072 """Palette Plot in HCL Space
2074 Convenience method for calling :py:func:`hclplot <colorspace.hclplot.hclplot>`
2075 on the current color object. Additional arguments can be forwarded via `**kwargs`
2076 (see :py:func:`hclplot <colorspace.hclplot.hclplot>` for details).
2078 Args:
2079 **kwargs: Additional named arguments forwarded to
2080 :py:func:`hclplot <colorspace.hclplot.hclplot>`.
2082 Return:
2083 Returns what :py:func:`colorspace.hclplot.hclplot` returns.
2085 Example:
2087 >>> # Example using HCL colors
2088 >>> from colorspace import HCL, hexcols
2089 >>> cols = HCL(H = [220, 196, 172, 148, 125],
2090 >>> C = [ 44, 49, 55, 59, 50],
2091 >>> L = [ 49, 61, 72, 82, 90])
2092 >>> cols.hclplot();
2093 >>>
2094 >>> #: Example using hex colors
2095 >>> cols = hexcols(["#0FCFC0", "#9CDED6", "#D5EAE7",
2096 >>> "#F1F1F1", "#F3E1EB", "#F6C4E1", "#F79CD4"])
2097 >>> cols.hclplot(figsize = (8, 3.5));
2098 """
2100 from .hclplot import hclplot
2101 return hclplot(x = self.colors(), **kwargs)
2103 def colors(self, fixup = True, rev = False):
2104 """Extract Hex Colors
2106 Convers the current object into an object of class :py:class:`hexcols`
2107 and extracts the hex colors as list of str.
2109 If the object contains alpha values, the alpha level is added to the
2110 hex string if and only if alpha is not equal to `1.0`.
2112 Args:
2113 fixup (bool): Whether or not to correct rgb values outside the
2114 defined range of `[0., 1.]`, defaults to `True`.
2115 rev (bool): Should the color palette be reversed? Defaults to `False`.
2117 Returns:
2118 list: Returns a list of hex color strings.
2120 Example:
2122 >>> from colorspace import HCL, sRGB, HSV
2123 >>> # Example using HCL colors
2124 >>> cols = HCL([0, 40, 80],
2125 >>> [30, 60, 80],
2126 >>> [85, 60, 35])
2127 >>> cols.colors()
2128 >>>
2129 >>> #: Example using sRGB colors
2130 >>> cols = sRGB([0.01, 0.89, 0.56],
2131 >>> [0.25, 0.89, 0.02],
2132 >>> [0.65, 0.89, 0.23])
2133 >>> cols.colors()
2134 >>>
2135 >>> #: Example using HSV colors
2136 >>> cols = HSV([218, 0, 336],
2137 >>> [1, 0, 1],
2138 >>> [0.65, 0.89, 0.56])
2139 >>> cols.colors()
2141 """
2143 from copy import copy
2144 from numpy import ndarray, round
2146 x = copy(self)
2147 x.to("hex", fixup = fixup)
2148 if x.hasalpha():
2149 res = x.get("hex_").tolist()
2150 # Appending alpha if alpha < 1.0
2151 for i in range(0, len(res)):
2152 if self._data_["alpha"][i] < 1.0:
2153 tmp = int(round(self._data_["alpha"][i] * 255. + 0.0001))
2154 res[i] += f"{tmp:02X}"
2155 # Return hex with alpha
2156 colors = res
2157 else:
2158 colors = x.get("hex_")
2160 if rev:
2161 from numpy import flip
2162 colors = flip(colors)
2164 return colors.tolist() if isinstance(colors, ndarray) else colors
2167 def get(self, dimname = None):
2168 """Extracting Color Coordinates
2170 Allows to extract the current values of one or multiple dimensions
2171 for all colors of this color object. The names of the coordinates varies
2172 between different color spaces.
2174 Args:
2175 dimname (None, str): If `None` (default) values of all coordinates
2176 of the current color object are returned. A specific coordinate
2177 can be specified if needed.
2179 Returns:
2180 Returns a `numpy.ndarray` if coordinates of one specific dimension are
2181 requested, else a `dict` of arrays.
2183 Example:
2185 >>> from colorspace import HCL, sRGB, hexcols
2186 >>> # Example using HCL color object with alpha channel
2187 >>> cols = HCL([260, 80, 30], [80, 0, 80], [30, 90, 30], [1, 0.6, 0.2])
2188 >>> cols.get("H") # Specific dimension
2189 >>> #:
2190 >>> cols.get("alpha") # Alpha (if existing)
2191 >>> #:
2192 >>> cols.get() # All dimensions
2193 >>>
2194 >>> #: Convert colors to sRGB
2195 >>> cols.to("sRGB")
2196 >>> cols.get("R") # Specific dimension
2197 >>> #:
2198 >>> cols.get() # All dimensions
2199 >>>
2200 >>> #: Convert to hexcols
2201 >>> cols.to("hex")
2202 >>> cols.get("hex_")
2204 Raises:
2205 TypeError: If argument `dimname` is neither None or str.
2206 ValueError: If the dimension specified on `dimnames` does not exist.
2207 """
2209 # Return all coordinates
2210 from copy import copy
2211 if dimname is None:
2212 return copy(self._data_)
2213 # No string?
2214 elif not isinstance(dimname, str):
2215 raise TypeError("argument `dimname` must be None or str")
2216 # Else only the requested dimension
2217 elif not dimname in self._data_.keys():
2218 # Alpha channel never defined, return None (which
2219 # is a valid value for "no alpha")
2220 if dimname == "alpha":
2221 return None
2222 else:
2223 raise ValueError(f"{self.__class__.__name__} has no dimension {dimname}")
2225 return copy(self._data_[dimname])
2228 def set(self, **kwargs):
2229 """Set Coordinates/Manipulate Colors
2231 Allows to manipulate current colors. The named input arguments
2232 have to fulfil a specific set or requirements. If not, the function
2233 raises exceptions. The requirements:
2235 * Dimension has to exist
2236 * New data/values must be of same length and type as the existing ones
2238 Args:
2239 **kwargs: Named arguments. The key is the name of the dimension to
2240 be changed, the value an object which fulfills the requirements
2241 (see description of this method)
2243 Raises:
2244 ValueError: If the dimension does not exist.
2245 ValueError: If the new data can't be converted into
2246 `numpy.array` (is done automatically if needed).
2247 ValueError: If new data has wrong length (does not match the
2248 number of colors/length of current values).
2250 Example:
2252 >>> # Example shown for HCL colors, works the same
2253 >>> # for all other color objects (sRGB, hexcols, ...)
2254 >>> from colorspace import HCL
2255 >>> cols = HCL([260, 80, 30], [80, 0, 80], [30, 90, 30])
2256 >>> cols
2257 >>> #:
2258 >>> cols.set(H = [150, 150, 30])
2259 >>> cols
2260 """
2261 # Looping over inputs
2262 from numpy import asarray, ndarray
2263 for key,vals in kwargs.items():
2264 key.upper()
2266 # Check if the key provided by the user is a valid dimension
2267 # of the current object.
2268 if not key in self._data_.keys():
2269 raise ValueError(f"{self.__class__.__name__} has no dimension {key}")
2271 # In case the input is a single int/float or a list; try
2272 # to convert the input into a numpy.array using the same
2273 # dtype as the existing dimension (loaded via self.get(key)).
2274 if isinstance(vals, (list, int, float)):
2275 if isinstance(vals, (int, float)): vals = [vals]
2276 t = type(self.get(key)[0]) # Current type (get current dimension)
2277 try:
2278 vals = np.asarray(vals, dtype = t)
2279 except Exception as e:
2280 raise ValueError(f"problems converting new data to {t} " + \
2281 f" in {self.__class__.__name__}: {str(e)}")
2283 # New values do have to have the same length as the old ones,
2284 n = len(self.get(key))
2285 t = type(self.get(key)[0])
2286 try:
2287 vals = np.asarray(vals, dtype = t)
2288 except Exception as e:
2289 raise ValueError(f"problems converting new data to {t} " + \
2290 f" in {self.__class__.__name__}: {str(e)}")
2291 if not vals.size == n:
2292 raise ValueError("number of values to be stored on the object " + \
2293 f"{self.__class__.__name__} have to match the current dimension")
2295 self._data_[key] = vals
2297 def length(self):
2298 """Get Number of Colors
2300 Returns the number of colors defined in this color object.
2301 Note that `len(<object>)` works as well.
2303 Returns:
2304 int: Number of colors.
2306 Examples:
2308 >>> from colorspace import sRGB, hexcols, HCL
2309 >>> # Examples for three different color objects
2310 >>> x1 = sRGB([1, 0], [1, 1], [0, 0])
2311 >>> [x1.length(), len(x1)]
2312 >>> #:
2313 >>> x2 = hexcols(["#ff0000", "#00ff00", "#0000ff"])
2314 >>> [x2.length(), len(x2)]
2315 >>> #:
2316 >>> x3 = HCL([275, 314, 353, 31, 70],
2317 >>> [70, 85, 102, 86, 45],
2318 >>> [25, 40, 55, 70, 85])
2319 >>> [x3.length(), len(x3)]
2321 """
2322 return max([0 if self._data_[x] is None else len(self._data_[x]) for x in self._data_.keys()])
2324 def __len__(self):
2325 return self.length()
2328 # Currently not used but implemented as fallback for the future
2329 def _cannot(self, from_, to):
2330 """Error: Conversion not Possible
2332 Helper function used to raise an exception as a specific
2333 transformation is not possible by definition.
2335 Args:
2336 from_ (str): Name of the current color space.
2337 to (str): Name of the target color space.
2339 Raises:
2340 Exception: Always, that is the intent of this method.
2341 """
2342 raise Exception(f"cannot convert class \"{from_}\" to \"{to}\"")
2344 def _ambiguous(self, from_, to):
2345 """Error: Conversion Ambiguous
2347 Helper function used to raise an exception as a specific
2348 transformation is ambiguous and therefore not possible by definition.
2350 Args:
2351 from_ (str): Name of the current color space.
2352 to (str): Name of the target color space.
2354 Raises:
2355 Exception: Always, that is the intent of this method.
2356 """
2357 raise Exception(f"conversion not possible, ambiguous conversion from \"{from_}\" to \"{to}\"")
2360# -------------------------------------------------------------------
2361# PolarLUV or HCL object
2362# -------------------------------------------------------------------
2363class polarLUV(colorobject):
2364 """Create polarLUV (HCL) Color Object
2366 Creates a color object in the polar representation of the :py:class:`CIELUV`
2367 color space, also known as the Hue-Chroma-Luminance (HCL) color space.
2368 Can be converted to: :py:class:`CIEXYZ`, :py:class:`CIELUV`,
2369 :py:class:`CIELAB`, :py:class:`RGB`, :py:class:`sRGB`,
2370 :py:class:`polarLAB`, and :py:class:`hexcols`.
2371 Not allowed (ambiguous) are transformations to :py:class:`HSV` and :py:class:`HLS`.
2373 Args:
2374 H (int, float, list, numpy.array):
2375 Numeric value(s) for hue dimension (`[-360., 360.]`).
2376 C (int, float, list, numpy.array):
2377 Numeric value(s) for chroma dimension (`[0., 100.+]`).
2378 L (int, float, list, numpy.array):
2379 Numeric value(s) for luminance dimension (`[0., 100.]`).
2380 alpha (None, float, list, numpy.array): Numeric value(s) for the alpha
2381 channel (`[0., 1.]`) where `0.` equals full transparency, `1.` full
2382 opacity. If `None` (default) no transparency is added.
2384 Example:
2386 >>> from colorspace import polarLUV, HCL
2387 >>> # Constructing color object with one single color via float
2388 >>> polarLUV(100., 30, 50.)
2389 >>> #: polarLUV is the HCL color space, this
2390 >>> # is equivalent to the command above.
2391 >>> HCL(100., 30, 50.)
2392 >>> #: Constructing object via lists
2393 >>> HCL([100, 80], [30, 50], [30, 80])
2394 >>> #: Constructing object via numpy arrays
2395 >>> from numpy import asarray
2396 >>> HCL(asarray([100, 80]), asarray([30, 50]), asarray([30, 80]))
2397 """
2399 def __init__(self, H, C, L, alpha = None):
2401 # Checking inputs, save inputs on object
2402 self._data_ = {} # Dict to store the colors/color dimensions
2403 tmp = self._colorobject_check_input_arrays_(H = H, C = C, L = L, alpha = alpha)
2404 for key,val in tmp.items(): self._data_[key] = val
2405 # White spot definition (the default)
2406 self.set_whitepoint(X = 95.047, Y = 100.000, Z = 108.883)
2408 def to(self, to, fixup = True):
2409 """Transform Color Space
2411 Allows to transform the current object into a different color space,
2412 if possible. Converting the colors of the current object into
2413 another color space. After calling this method, the object
2414 will be of a different class.
2416 Args:
2417 to (str): Name of the color space into which the colors should be
2418 converted (e.g., `"CIEXYZ"`, `"HCL"`, `"hex"`, `"sRGB"`, ...).
2419 fixup (bool): Whether or not colors outside the defined rgb color space
2420 should be corrected if necessary, defaults to `True`.
2422 Examples:
2424 >>> # HCL() identical to polarLUV()
2425 >>> from colorspace import HCL
2426 >>> x = HCL([275, 314, 353, 31, 70],
2427 >>> [ 70, 85, 102, 86, 45],
2428 >>> [ 25, 40, 55, 70, 85])
2429 >>> x
2430 >>> #:
2431 >>> type(x)
2432 >>> #: Convert colors to sRGB
2433 >>> x.to("sRGB")
2434 >>> x
2435 >>> #:
2436 >>> type(x)
2437 >>> #: Convert from sRGB to hex
2438 >>> x.to("hex")
2439 >>> x
2440 >>> #: Convert back to HCL colors.
2441 >>> # Round-off errors due to conversion to 'hex'.
2442 >>> x.to("HCL")
2443 >>> x
2444 >>> #: Extracting hex colors (returns list of str)
2445 >>> x.colors()
2447 """
2448 self._check_if_allowed_(to)
2449 from . import colorlib
2450 clib = colorlib()
2452 # Nothing to do (converted to itself)
2453 if to in ["HCL", self.__class__.__name__]:
2454 return
2456 # This is the only transformation from polarLUV -> LUV
2457 elif to == "CIELUV":
2458 [L, U, V] = clib.polarLUV_to_LUV(self.get("L"), self.get("C"), self.get("H"))
2459 self._data_ = {"L" : L, "U" : U, "V" : V, "alpha" : self.get("alpha")}
2460 self.__class__ = CIELUV
2462 # The rest are transformations along a path
2463 elif to == "CIEXYZ":
2464 via = ["CIELUV", to]
2465 self._transform_via_path_(via, fixup = fixup)
2467 elif to == "CIELAB":
2468 via = ["CIELUV", "CIEXYZ", to]
2469 self._transform_via_path_(via, fixup = fixup)
2471 elif to == "RGB":
2472 via = ["CIELUV", "CIEXYZ", to]
2473 self._transform_via_path_(via, fixup = fixup)
2475 elif to == "sRGB":
2476 via = ["CIELUV", "CIEXYZ", to]
2477 self._transform_via_path_(via, fixup = fixup)
2479 elif to == "polarLAB":
2480 via = ["CIELUV", "CIEXYZ", "CIELAB", to]
2481 self._transform_via_path_(via, fixup = fixup)
2483 elif to == "hex":
2484 via = ["CIELUV", "CIEXYZ", "sRGB", to]
2485 self._transform_via_path_(via, fixup = fixup)
2487 elif to in ["HLS", "HSV"]:
2488 self._ambiguous(self.__class__.__name__, to)
2490 # Currently not used but implemented as fallback for the future
2491 else: self._cannot(self.__class__.__name__, to)
2493# polarLUV is HCL, make copy
2494HCL = polarLUV
2497# -------------------------------------------------------------------
2498# CIELUV color object
2499# -------------------------------------------------------------------
2500class CIELUV(colorobject):
2501 """Create CIELUV Color Object
2503 Creates a color object in the CIELUV color space.
2504 Can be converted to: :py:class:`CIEXYZ`, :py:class:`CIELUV`,
2505 :py:class:`CIELAB`, :py:class:`RGB`, :py:class:`sRGB`,
2506 :py:class:`polarLAB`, and :py:class:`hexcols`.
2507 Not allowed (ambiguous) are transformations to :py:class:`HSV` and :py:class:`HLS`.
2509 Args:
2510 L (int, float, list, numpy.array):
2511 Numeric value(s) for L dimension.
2512 U (int, float, list, numpy.array):
2513 Numeric value(s) for U dimension.
2514 V (int, float, list, numpy.array):
2515 Numeric value(s) for L dimension.
2516 alpha (None, float, list, numpy.array): Numeric value(s) for the alpha
2517 channel (`[0., 1.]`) where `0.` equals full transparency, `1.` full
2518 opacity. If `None` (default) no transparency is added.
2520 Example:
2522 >>> from colorspace import CIELUV
2523 >>> # Constructing color object with one single color via float
2524 >>> CIELUV(0, 10, 10)
2525 >>> #: Constructing object via lists
2526 >>> CIELUV([10, 30], [20, 80], [100, 40])
2527 >>> #: Constructing object via numpy arrays
2528 >>> from numpy import asarray
2529 >>> CIELUV(asarray([10, 30]), asarray([20, 80]), asarray([100, 40]))
2531 """
2532 def __init__(self, L, U, V, alpha = None):
2534 # checking inputs, save inputs on object
2535 self._data_ = {} # Dict to store the colors/color dimensions
2536 tmp = self._colorobject_check_input_arrays_(L = L, U = U, V = V, alpha = alpha)
2537 for key,val in tmp.items(): self._data_[key] = val
2538 # White spot definition (the default)
2539 self.set_whitepoint(X = 95.047, Y = 100.000, Z = 108.883)
2542 def to(self, to, fixup = True):
2543 """Transform Color Space
2545 Allows to transform the current object into a different color space,
2546 if possible. Converting the colors of the current object into
2547 another color space. After calling this method, the object
2548 will be of a different class.
2550 Args:
2551 to (str): Name of the color space into which the colors should be
2552 converted (e.g., `"CIEXYZ"`, `"HCL"`, `"hex"`, `"sRGB"`, ...).
2553 fixup (bool): Whether or not colors outside the defined rgb color space
2554 should be corrected if necessary, defaults to `True`.
2556 Examples:
2558 >>> from colorspace import CIELUV
2559 >>> x = CIELUV([ 25, 45, 65, 85],
2560 >>> [ 6, 75, 90, 16],
2561 >>> [-70, -50, 30, 42])
2562 >>> x
2563 >>> #:
2564 >>> type(x)
2565 >>> #: Convert colors to sRGB
2566 >>> x.to("sRGB")
2567 >>> x
2568 >>> #:
2569 >>> type(x)
2570 >>> #: Convert from sRGB to hex
2571 >>> x.to("hex")
2572 >>> x
2573 >>> #: Convert back to CIELUV colors.
2574 >>> # Round-off errors due to conversion to 'hex'.
2575 >>> x.to("CIELUV")
2576 >>> x
2577 >>> #: Extracting hex colors (returns list of str)
2578 >>> x.colors()
2580 """
2581 self._check_if_allowed_(to)
2582 from . import colorlib
2583 clib = colorlib()
2585 # Nothing to do (converted to itself)
2586 if to == self.__class__.__name__:
2587 return
2588 # Transformation from CIELUV -> CIEXYZ
2589 elif to == "CIEXYZ":
2590 [X, Y, Z] = clib.LUV_to_XYZ(self.get("L"), self.get("U"), self.get("V"),
2591 self.WHITEX, self.WHITEY, self.WHITEZ)
2592 self._data_ = {"X" : X, "Y" : Y, "Z" : Z, "alpha" : self.get("alpha")}
2593 self.__class__ = CIEXYZ
2595 # Transformation from CIELUV -> polarLUV (HCL)
2596 elif to in ["HCL","polarLUV"]:
2597 [L, C, H] = clib.LUV_to_polarLUV(self.get("L"), self.get("U"), self.get("V"))
2598 self._data_ = {"L" : L, "C" : C, "H" : H, "alpha" : self.get("alpha")}
2599 self.__class__ = polarLUV
2601 # The rest are transformations along a path
2602 elif to == "CIELAB":
2603 via = ["CIEXYZ", to]
2604 self._transform_via_path_(via, fixup = fixup)
2606 elif to == "RGB":
2607 via = ["CIEXYZ", to]
2608 self._transform_via_path_(via, fixup = fixup)
2610 elif to == "sRGB":
2611 via = ["CIEXYZ", "RGB", to]
2612 self._transform_via_path_(via, fixup = fixup)
2614 elif to == "polarLAB":
2615 via = ["CIEXYZ", "CIELAB", to]
2616 self._transform_via_path_(via, fixup = fixup)
2618 elif to == "hex":
2619 via = ["CIEXYZ", "RGB", "sRGB", to]
2620 self._transform_via_path_(via, fixup = fixup)
2622 elif to in ["HLS", "HSV"]:
2623 self._ambiguous(self.__class__.__name__, to)
2625 else: self._cannot(self.__class__.__name__, to)
2627# -------------------------------------------------------------------
2628# CIEXYZ color object
2629# -------------------------------------------------------------------
2630class CIEXYZ(colorobject):
2631 """Create CIEXYZ Color Object
2633 Creates a color object in the CIEXYZ color space.
2634 Can be converted to: :py:class:`CIEXYZ`, :py:class:`CIELUV`,
2635 :py:class:`CIELAB`, :py:class:`RGB`, :py:class:`sRGB`,
2636 :py:class:`polarLAB`, and :py:class:`hexcols`.
2637 Not allowed (ambiguous) are transformations to :py:class:`HSV` and :py:class:`HLS`.
2639 Args:
2640 X (int, float, list, numpy.array):
2641 Numeric value(s) for X dimension.
2642 Y (int, float, list, numpy.array):
2643 Numeric value(s) for Y dimension.
2644 Z (int, float, list, numpy.array):
2645 Numeric value(s) for Z dimension.
2646 alpha (None, float, list, numpy.array): Numeric value(s) for the alpha
2647 channel (`[0., 1.]`) where `0.` equals full transparency, `1.` full
2648 opacity. If `None` (default) no transparency is added.
2650 Example:
2652 >>> from colorspace import CIEXYZ
2653 >>> # Constructing color object with one single color via float
2654 >>> CIEXYZ(80, 30, 10)
2655 >>> #: Constructing object via lists
2656 >>> CIEXYZ([10, 0], [20, 80], [40, 40])
2657 >>> #: Constructing object via numpy arrays
2658 >>> from numpy import asarray
2659 >>> CIEXYZ(asarray([10, 0]), asarray([20, 80]), asarray([40, 40]))
2661 """
2662 def __init__(self, X, Y, Z, alpha = None):
2664 # checking inputs, save inputs on object
2665 self._data_ = {} # Dict to store the colors/color dimensions
2666 tmp = self._colorobject_check_input_arrays_(X = X, Y = Y, Z = Z, alpha = alpha)
2667 for key,val in tmp.items(): self._data_[key] = val
2668 # White spot definition (the default)
2669 self.set_whitepoint(X = 95.047, Y = 100.000, Z = 108.883)
2672 def to(self, to, fixup = True):
2673 """Transform Color Space
2675 Allows to transform the current object into a different color space,
2676 if possible. Converting the colors of the current object into
2677 another color space. After calling this method, the object
2678 will be of a different class.
2680 Args:
2681 to (str): Name of the color space into which the colors should be
2682 converted (e.g., `"CIELUV"`, `"HCL"`, `"hex"`, `"sRGB"`, ...).
2683 fixup (bool): Whether or not colors outside the defined rgb color space
2684 should be corrected if necessary, defaults to `True`.
2686 Examples:
2688 >>> from colorspace import CIEXYZ
2689 >>> x = CIEXYZ([ 8.5, 27.8, 46.2, 62.1],
2690 >>> [ 4.4, 14.5, 34.1, 65.9],
2691 >>> [27.2, 31.9, 17.2, 40.0])
2692 >>> x
2693 >>> #:
2694 >>> type(x)
2695 >>> #: Convert colors to sRGB
2696 >>> x.to("sRGB")
2697 >>> x
2698 >>> #:
2699 >>> type(x)
2700 >>> #: Convert from sRGB to hex
2701 >>> x.to("hex")
2702 >>> x
2703 >>> #: Convert back to CIEXYZ colors.
2704 >>> # Round-off errors due to conversion to 'hex'.
2705 >>> x.to("CIEXYZ")
2706 >>> x
2707 >>> #: Extracting hex colors (returns list of str)
2708 >>> x.colors()
2710 """
2711 self._check_if_allowed_(to)
2712 from . import colorlib
2713 clib = colorlib()
2715 # Nothing to do (converted to itself)
2716 if to == self.__class__.__name__:
2717 return
2719 # Transformation from CIEXYZ -> CIELUV
2720 elif to == "CIELUV":
2721 [L, U, V] = clib.XYZ_to_LUV(self.get("X"), self.get("Y"), self.get("Z"),
2722 self.WHITEX, self.WHITEY, self.WHITEZ)
2723 self._data_ = {"L" : L, "U" : U, "V" : V, "alpha" : self.get("alpha")}
2724 self.__class__ = CIELUV
2726 # Transformation from CIEXYZ -> CIELAB
2727 elif to == "CIELAB":
2728 [L, A, B] = clib.XYZ_to_LAB(self.get("X"), self.get("Y"), self.get("Z"),
2729 self.WHITEX, self.WHITEY, self.WHITEZ)
2730 self._data_ = {"L" : L, "A" : A, "B" : B, "alpha" : self.get("alpha")}
2731 self.__class__ = CIELAB
2733 # Transformation from CIEXYZ -> RGB
2734 elif to == "RGB":
2735 [R, G, B] = clib.XYZ_to_RGB(self.get("X"), self.get("Y"), self.get("Z"),
2736 self.WHITEX, self.WHITEY, self.WHITEZ)
2737 self._data_ = {"R" : R, "G" : G, "B" : B, "alpha" : self.get("alpha")}
2738 self.__class__ = RGB
2740 # The rest are transformations along a path
2741 elif to == "polarLAB":
2742 via = ["CIELAB", to]
2743 self._transform_via_path_(via, fixup = fixup)
2745 elif to in ["HCL", "polarLUV"]:
2746 via = ["CIELUV", to]
2747 self._transform_via_path_(via, fixup = fixup)
2749 elif to == "sRGB":
2750 via = ["RGB", to]
2751 self._transform_via_path_(via, fixup = fixup)
2753 elif to == "hex":
2754 via = ["RGB", "sRGB", to]
2755 self._transform_via_path_(via, fixup = fixup)
2757 elif to in ["HLS", "HSV"]:
2758 self._ambiguous(self.__class__.__name__, to)
2760 else: self._cannot(self.__class__.__name__, to)
2763class RGB(colorobject):
2764 """Create RGB Color Object
2766 Allows conversions to: :py:class:`CIELAB`, :py:class:`CIELUV`,
2767 :py:class:`CIEXYZ`, :py:class:`HLS`, :py:class:`HSV`, :py:class:`hexcols`.
2768 :py:class:`polarLAB`, :py:class:`polarLUV` and :py:class:`sRGB`.
2770 Args:
2771 R (int, float, list, numpy.array):
2772 Numeric value(s) for red intensity (`[0., 1.]`).
2773 G (int, float, list, numpy.array):
2774 Numeric value(s) for green intensity (`[0., 1.]`).
2775 B (int, float, list, numpy.array):
2776 Numeric value(s) for blue intensity (`[0., 1.]`).
2777 alpha (None, float, list, numpy.array): Numeric value(s) for the alpha
2778 channel (`[0., 1.]`) where `0.` equals full transparency, `1.` full
2779 opacity. If `None` (default) no transparency is added.
2781 Example:
2783 >>> from colorspace import RGB
2784 >>> # Constructing color object with one single color via float
2785 >>> RGB(1., 0.3, 0.5)
2786 >>> #: Constructing object via lists
2787 >>> RGB([1., 0.8], [0.5, 0.5], [0.0, 0.2])
2788 >>> #: Constructing object via numpy arrays
2789 >>> from numpy import asarray
2790 >>> RGB(asarray([1., 0.8]), asarray([0.5, 0.5]), asarray([0.0, 0.2]))
2792 """
2794 def __init__(self, R, G, B, alpha = None):
2796 # checking inputs, save inputs on object
2797 self._data_ = {} # Dict to store the colors/color dimensions
2799 tmp = self._colorobject_check_input_arrays_(R = R, G = G, B = B, alpha = alpha)
2800 for key,val in tmp.items(): self._data_[key] = val
2801 # White spot definition (the default)
2802 self.set_whitepoint(X = 95.047, Y = 100.000, Z = 108.883)
2805 def to(self, to, fixup = True):
2806 """Transform Color Space
2808 Allows to transform the current object into a different color space,
2809 if possible. Converting the colors of the current object into
2810 another color space. After calling this method, the object
2811 will be of a different class.
2813 Args:
2814 to (str): Name of the color space into which the colors should be
2815 converted (e.g., `"CIEXYZ"`, `"HCL"`, `"hex"`, `"sRGB"`, ...).
2816 fixup (bool): Whether or not colors outside the defined rgb color space
2817 should be corrected if necessary, defaults to `True`.
2819 Examples:
2821 >>> from colorspace import RGB
2822 >>> x = RGB([0.070, 0.520, 0.887, 0.799],
2823 >>> [0.012, 0.015, 0.198, 0.651],
2824 >>> [0.283, 0.323, 0.138, 0.323])
2825 >>> x
2826 >>> #:
2827 >>> type(x)
2828 >>> #: Convert colors to CIEXYZ
2829 >>> x.to("CIELUV")
2830 >>> x
2831 >>> #:
2832 >>> type(x)
2833 >>> #: Convert from CIELUV to HCL
2834 >>> x.to("HCL")
2835 >>> x
2836 >>> # Convert back to RGB
2837 >>> x.to("RGB")
2838 >>> x
2839 >>> #: Extracting hex colors (returns list of str)
2840 >>> x.colors()
2842 """
2843 self._check_if_allowed_(to)
2844 from . import colorlib
2845 clib = colorlib()
2847 # Nothing to do (converted to itself)
2848 if to == self.__class__.__name__:
2849 return
2851 # Transform from RGB -> sRGB
2852 elif to == "sRGB":
2853 [R, G, B] = clib.RGB_to_sRGB(self.get("R"), self.get("G"), self.get("B"),
2854 self.GAMMA)
2855 self._data_ = {"R" : R, "G" : G, "B" : B, "alpha" : self.get("alpha")}
2856 self.__class__ = sRGB
2858 # Transform from RGB -> CIEXYZ
2859 elif to == "CIEXYZ":
2860 [X, Y, Z] = clib.RGB_to_XYZ(self.get("R"), self.get("G"), self.get("B"),
2861 self.WHITEX, self.WHITEY, self.WHITEZ)
2862 self._data_ = {"X" : X, "Y" : Y, "Z" : Z, "alpha" : self.get("alpha")}
2863 self.__class__ = CIEXYZ
2865 # From RGB to HLS: take direct path (not via sRGB)
2866 elif to in ["HLS"]:
2867 [H, L, S] = clib.RGB_to_HLS(self.get("R"), self.get("G"), self.get("B"))
2868 self._data_ = {"H" : H, "L" : L, "S" : S, "alpha" : self.get("alpha")}
2869 self.__class__ = HLS
2871 # From RGB to HSV: take direct path (not via sRGB)
2872 elif to in ["HSV"]:
2873 [H, S, V] = clib.RGB_to_HSV(self.get("R"), self.get("G"), self.get("B"))
2874 self._data_ = {"H" : H, "S" : S, "V" : V, "alpha" : self.get("alpha")}
2875 self.__class__ = HSV
2877 # The rest are transformations along a path
2878 elif to in ["hex"]:
2879 via = ["sRGB", to]
2880 self._transform_via_path_(via, fixup = fixup)
2882 elif to in ["CIELUV", "CIELAB"]:
2883 via = ["CIEXYZ", to]
2884 self._transform_via_path_(via, fixup = fixup)
2886 elif to in ["HCL","polarLUV"]:
2887 via = ["CIEXYZ", "CIELUV", to]
2888 self._transform_via_path_(via, fixup = fixup)
2890 elif to == "polarLAB":
2891 via = ["CIEXYZ", "CIELAB", to]
2892 self._transform_via_path_(via, fixup = fixup)
2894 else: self._cannot(self.__class__.__name__, to)
2897class sRGB(colorobject):
2898 """Create Standard RGB (sRGB) Color Object
2900 Allows conversions to: :py:class:`CIELAB`, :py:class:`CIELUV`,
2901 :py:class:`CIEXYZ`, :py:class:`HLS`, :py:class:`HSV`, :py:class:`RGB`,
2902 :py:class:`hexcols`. :py:class:`polarLAB` and :py:class:`polarLUV`.
2904 Args:
2905 R (int, float, list, numpy.array):
2906 Numeric value(s) for red intensity (`[0., 1.]`).
2907 G (int, float, list, numpy.array):
2908 Numeric value(s) for green intensity (`[0., 1.]`).
2909 B (int, float, list, numpy.array):
2910 Numeric value(s) for blue intensity (`[0., 1.]`).
2911 alpha (None, float, list, numpy.array): Numeric value(s) for the alpha
2912 channel (`[0., 1.]`) where `0.` equals full transparency, `1.` full
2913 opacity. If `None` (default) no transparency is added.
2914 gamma (None, float): If `None` (default) the default gamma value is used.
2915 Can be specified to overwrite the default.
2917 Example:
2919 >>> from colorspace import sRGB
2920 >>> # Constructing color object with one single color via float
2921 >>> sRGB(1., 0.3, 0.5)
2922 >>> #: Constructing object via lists
2923 >>> sRGB([1., 0.8], [0.5, 0.5], [0.0, 0.2])
2924 >>> #: Constructing object via numpy arrays
2925 >>> from numpy import asarray
2926 >>> sRGB(asarray([1., 0.8]), asarray([0.5, 0.5]), asarray([0.0, 0.2]))
2928 """
2930 def __init__(self, R, G, B, alpha = None, gamma = None):
2932 # checking inputs, save inputs on object
2933 self._data_ = {} # Dict to store the colors/color dimensions
2934 tmp = self._colorobject_check_input_arrays_(R = R, G = G, B = B, alpha = alpha)
2935 for key,val in tmp.items(): self._data_[key] = val
2937 # White spot definition (the default)
2938 self.set_whitepoint(X = 95.047, Y = 100.000, Z = 108.883)
2940 if isinstance(gamma, float): self.GAMMA = gamma
2943 def to(self, to, fixup = True):
2944 """Transform Color Space
2946 Allows to transform the current object into a different color space,
2947 if possible. Converting the colors of the current object into
2948 another color space. After calling this method, the object
2949 will be of a different class.
2951 Args:
2952 to (str): Name of the color space into which the colors should be
2953 converted (e.g., `"CIEXYZ"`, `"HCL"`, `"hex"`, `"sRGB"`, ...).
2954 fixup (bool): Whether or not colors outside the defined rgb color space
2955 should be corrected if necessary, defaults to `True`.
2957 Examples:
2959 >>> from colorspace import sRGB
2960 >>> x = sRGB([0.294, 0.749, 0.949, 0.905],
2961 >>> [0.113, 0.129, 0.482, 0.827],
2962 >>> [0.568, 0.603, 0.407, 0.603])
2963 >>> x
2964 >>> #:
2965 >>> type(x)
2966 >>> #: Convert colors to CIEXYZ
2967 >>> x.to("CIELUV")
2968 >>> x
2969 >>> #:
2970 >>> type(x)
2971 >>> #: Convert from CIELUV to HCL
2972 >>> x.to("HCL")
2973 >>> x
2974 >>> #: Convert back to Standard RGB colors.
2975 >>> x.to("sRGB")
2976 >>> x
2977 >>> #: Extracting hex colors (returns list of str)
2978 >>> x.colors()
2980 """
2981 self._check_if_allowed_(to)
2982 from . import colorlib
2983 clib = colorlib()
2985 # Nothing to do (converted to itself)
2986 if to == self.__class__.__name__:
2987 return
2989 # Transformation sRGB -> RGB
2990 elif to == "RGB":
2991 [R, G, B] = clib.sRGB_to_RGB(self.get("R"), self.get("G"), self.get("B"),
2992 gamma = self.GAMMA)
2993 self._data_ = {"R" : R, "G" : G, "B" : B, "alpha" : self.get("alpha")}
2994 self.__class__ = RGB
2996 # Transformation sRGB -> hex
2997 elif to == "hex":
2998 hex_ = clib.sRGB_to_hex(self.get("R"), self.get("G"), self.get("B"), fixup)
2999 self._data_ = {"hex_" : hex_, "alpha" : self.get("alpha")}
3000 self.__class__ = hexcols
3002 # Transform from RGB -> HLS
3003 elif to == "HLS":
3004 [H, L, S] = clib.sRGB_to_HLS(self.get("R"), self.get("G"), self.get("B"))
3005 self._data_ = {"H" : H, "L" : L, "S" : S, "alpha" : self.get("alpha")}
3006 self.__class__ = HLS
3008 # Transform from RGB -> HSV
3009 elif to == "HSV":
3010 [H, S, V] = clib.sRGB_to_HSV(self.get("R"), self.get("G"), self.get("B"))
3011 self._data_ = {"H" : H, "S" : S, "V" : V, "alpha" : self.get("alpha")}
3012 self.__class__ = HSV
3014 # The rest are transformations along a path
3015 elif to in ["CIEXYZ"]:
3016 via = ["RGB", to]
3017 self._transform_via_path_(via, fixup = fixup)
3019 elif to in ["CIELUV", "CIELAB"]:
3020 via = ["RGB", "CIEXYZ", to]
3021 self._transform_via_path_(via, fixup = fixup)
3023 elif to in ["HCL","polarLUV"]:
3024 via = ["RGB", "CIEXYZ", "CIELUV", to]
3025 self._transform_via_path_(via, fixup = fixup)
3027 elif to == "polarLAB":
3028 via = ["RGB", "CIEXYZ", "CIELAB", to]
3029 self._transform_via_path_(via, fixup = fixup)
3031 else: self._cannot(self.__class__.__name__, to)
3034class CIELAB(colorobject):
3035 """Create CIELAB Color Object
3037 Creates a color object in the CIELAB color space.
3038 Can be converted to: :py:class:`CIEXYZ`, :py:class:`CIELUV`,
3039 :py:class:`CIELAB`, :py:class:`RGB`, :py:class:`sRGB`,
3040 :py:class:`polarLAB`, and :py:class:`hexcols`.
3041 Not allowed (ambiguous) are transformations to :py:class:`HSV` and :py:class:`HLS`.
3043 Args:
3044 L (int, float, list, numpy.array):
3045 Numeric value(s) for L dimension.
3046 A (int, float, list, numpy.array):
3047 Numeric value(s) for A dimension.
3048 B (int, float, list, numpy.array):
3049 Numeric value(s) for B dimension.
3050 alpha (None, float, list, numpy.array): Numeric value(s) for the alpha
3051 channel (`[0., 1.]`) where `0.` equals full transparency, `1.` full
3052 opacity. If `None` (default) no transparency is added.
3054 Example:
3056 >>> from colorspace import CIELAB
3057 >>> # Constructing color object with one single color via float
3058 >>> CIELAB(-30, 10, 10)
3059 >>> #: Constructing object via lists
3060 >>> CIELAB([-30, 30], [20, 80], [40, 40])
3061 >>> #: Constructing object via numpy arrays
3062 >>> from numpy import asarray
3063 >>> CIELAB(asarray([-30, 30]), asarray([20, 80]), asarray([40, 40]))
3065 """
3067 def __init__(self, L, A, B, alpha = None):
3069 # checking inputs, save inputs on object
3070 self._data_ = {} # Dict to store the colors/color dimensions
3071 tmp = self._colorobject_check_input_arrays_(L = L, A = A, B = B, alpha = alpha)
3072 for key,val in tmp.items(): self._data_[key] = val
3073 # White spot definition (the default)
3074 self.set_whitepoint(X = 95.047, Y = 100.000, Z = 108.883)
3077 def to(self, to, fixup = True):
3078 """Transform Color Space
3080 Allows to transform the current object into a different color space,
3081 if possible.
3083 Args:
3084 to (str): Name of the color space into which the colors should be
3085 converted (e.g., `CIEXYZ`, `HCL`, `hex`, `RGB`, ...)
3086 fixup (bool): Whether or not colors outside the defined rgb color space
3087 should be corrected if necessary, defaults to True.
3089 Returns:
3090 No return, converts the object into a new color space and modifies
3091 the underlying object. After calling this method the object will
3092 be of a different class.
3093 """
3094 self._check_if_allowed_(to)
3095 from . import colorlib
3096 clib = colorlib()
3098 # Nothing to do (converted to itself)
3099 if to == self.__class__.__name__:
3100 return
3102 # Transformations CIELAB -> CIEXYZ
3103 elif to == "CIEXYZ":
3104 [X, Y, Z] = clib.LAB_to_XYZ(self.get("L"), self.get("A"), self.get("B"),
3105 self.WHITEX, self.WHITEY, self.WHITEZ)
3106 self._data_ = {"X" : X, "Y" : Y, "Z" : Z, "alpha" : self.get("alpha")}
3107 self.__class__ = CIEXYZ
3109 # Transformation CIELAB -> polarLAB
3110 elif to == "polarLAB":
3111 [L, A, B] = clib.LAB_to_polarLAB(self.get("L"), self.get("A"), self.get("B"))
3112 self._data_ = {"L" : L, "A" : A, "B" : B, "alpha" : self.get("alpha")}
3113 self.__class__ = polarLAB
3115 # The rest are transformations along a path
3116 elif to == "CIELUV":
3117 via = ["CIEXYZ", to]
3118 self._transform_via_path_(via, fixup = fixup)
3120 elif to in ["HCL","polarLUV"]:
3121 via = ["CIEXYZ", "CIELUV", to]
3122 self._transform_via_path_(via, fixup = fixup)
3124 elif to == "RGB":
3125 via = ["CIEXYZ", to]
3126 self._transform_via_path_(via, fixup = fixup)
3128 elif to == "sRGB":
3129 via = ["CIEXYZ", "RGB", to]
3130 self._transform_via_path_(via, fixup = fixup)
3132 elif to == "hex":
3133 via = ["CIEXYZ", "RGB", "sRGB", to]
3134 self._transform_via_path_(via, fixup = fixup)
3136 elif to in ["HLS", "HSV"]:
3137 self._ambiguous(self.__class__.__name__, to)
3139 else: self._cannot(self.__class__.__name__, to)
3142class polarLAB(colorobject):
3143 """Create Polar LAB Color Object
3145 Creates a color object in the polar representation of the
3146 :py:class:`CIELAB` color space.
3147 Can be converted to: :py:class:`CIEXYZ`, :py:class:`CIELUV`,
3148 :py:class:`CIELAB`, :py:class:`RGB`, :py:class:`sRGB`,
3149 :py:class:`polarLAB`, and :py:class:`hexcols`.
3150 Not allowed (ambiguous) are transformations to :py:class:`HSV` and :py:class:`HLS`.
3152 Args:
3153 L (int, float, list, numpy.array):
3154 Numeric value(s) for L dimension.
3155 A (int, float, list, numpy.array):
3156 Numeric value(s) for A dimension.
3157 B (int, float, list, numpy.array):
3158 Numeric value(s) for B dimension.
3159 alpha (None, float, list, numpy.array): Numeric value(s) for the alpha
3160 channel (`[0., 1.]`) where `0.` equals full transparency, `1.` full
3161 opacity. If `None` (default) no transparency is added.
3163 Examples:
3165 >>> from colorspace import polarLAB
3166 >>> cols = polarLAB([50, 80, 30], [100, 120, 140], [40, 130, 300])
3167 >>> cols
3168 >>> #: Convert to hex colors
3169 >>> cols.to("hex")
3170 >>> cols
3172 """
3174 def __init__(self, L, A, B, alpha = None):
3176 # checking inputs, save inputs on object
3177 self._data_ = {} # Dict to store the colors/color dimensions
3178 tmp = self._colorobject_check_input_arrays_(L = L, A = A, B = B, alpha = alpha)
3179 for key,val in tmp.items(): self._data_[key] = val
3180 # White spot definition (the default)
3181 self.set_whitepoint(X = 95.047, Y = 100.000, Z = 108.883)
3184 def to(self, to, fixup = True):
3185 """Transform Color Space
3187 Allows to transform the current object into a different color space,
3188 if possible. Converting the colors of the current object into
3189 another color space. After calling this method, the object
3190 will be of a different class.
3192 Args:
3193 to (str): Name of the color space into which the colors should be
3194 converted (e.g., `"CIEXYZ"`, `"HCL"`, `"hex"`, `"sRGB"`, ...).
3195 fixup (bool): Whether or not colors outside the defined rgb color space
3196 should be corrected if necessary, defaults to `True`.
3198 Examples:
3200 >>> from colorspace import polarLAB
3201 >>> x = polarLAB([ 25, 45, 65, 85],
3202 >>> [ 72, 75, 54, 31],
3203 >>> [310, 338, 36, 92])
3204 >>> x
3205 >>> #:
3206 >>> type(x)
3207 >>> #: Convert colors to sRGB
3208 >>> x.to("sRGB")
3209 >>> x
3210 >>> #:
3211 >>> type(x)
3212 >>> #: Convert from sRGB to hex
3213 >>> x.to("hex")
3214 >>> x
3215 >>> #: Convert back to polarLAB colors.
3216 >>> # Round-off errors due to conversion to 'hex'.
3217 >>> x.to("polarLAB")
3218 >>> x
3219 >>> #: Extracting hex colors (returns list of str)
3220 >>> x.colors()
3222 """
3223 self._check_if_allowed_(to)
3224 from . import colorlib
3225 clib = colorlib()
3227 # Nothing to do (converted to itself)
3228 if to == self.__class__.__name__:
3229 return
3231 # The only transformation we need is from polarLAB -> LAB
3232 elif to == "CIELAB":
3233 [L, A, B] = clib.polarLAB_to_LAB(self.get("L"), self.get("A"), self.get("B"))
3234 self._data_ = {"L" : L, "A" : A, "B" : B, "alpha" : self.get("alpha")}
3235 self.__class__ = CIELAB
3237 # The rest are transformationas along a path
3238 elif to == "CIEXYZ":
3239 via = ["CIELAB", to]
3240 self._transform_via_path_(via, fixup = fixup)
3242 elif to == "CIELUV":
3243 via = ["CIELAB", "CIEXYZ", to]
3244 self._transform_via_path_(via, fixup = fixup)
3246 elif to in ["HCL", "polarLUV"]:
3247 via = ["CIELAB", "CIEXYZ", "CIELUV", to]
3248 self._transform_via_path_(via, fixup = fixup)
3250 elif to == "RGB":
3251 via = ["CIELAB", "CIEXYZ", to]
3252 self._transform_via_path_(via, fixup = fixup)
3254 elif to == "sRGB":
3255 via = ["CIELAB", "CIEXYZ", "RGB", to]
3256 self._transform_via_path_(via, fixup = fixup)
3258 elif to == "hex":
3259 via = ["CIELAB", "CIEXYZ", "RGB", "sRGB", to]
3260 self._transform_via_path_(via, fixup = fixup)
3262 elif to in ["HLS", "HSV"]:
3263 self._ambiguous(self.__class__.__name__, to)
3265 else: self._cannot(self.__class__.__name__, to)
3268class HSV(colorobject):
3269 """Create HSV Color Object
3271 Creates a color object in the Hue-Saturation-Value (HSV) color space.
3272 Can be converted to: :py:class:`RGB`, :py:class:`sRGB`, :py:class:`HLS`,
3273 and :py:class:`hexcols`.
3274 Not allowed (ambiguous) are transformations to :py:class:`CIEXYZ`,
3275 :py:class:`CIELUV`, :py:class:`CIELAB`, :py:class:`polarLUV`, and
3276 :py:class:`polarLAB`.
3278 Args:
3279 H (int, float, list, numpy.array):
3280 Numeric value(s) for Hue dimension.
3281 S (int, float, list, numpy.array):
3282 Numeric value(s) for Saturation dimension.
3283 V (int, float, list, numpy.array):
3284 Numeric value(s) for Value dimension.
3285 alpha (None, float, list, numpy.array): Numeric value(s) for the alpha
3286 channel (`[0., 1.]`) where `0.` equals full transparency, `1.` full
3287 opacity. If `None` (default) no transparency is added.
3289 Examples:
3291 >>> #: Constructing object via numpy arrays
3292 >>> from colorspace import HSV
3293 >>> # Constructing color object with one single color via float
3294 >>> HSV(150, 150, 10)
3295 >>> #: Constructing object via lists
3296 >>> HSV([150, 150, 10], [1.5, 0, 1.5], [0.1, 0.7, 0.1])
3297 >>> #: Constructing object via numpy arrays
3298 >>> from numpy import asarray
3299 >>> cols = HSV(asarray([150, 150, 150]),
3300 >>> asarray([1.5, 0, 1.5]),
3301 >>> asarray([0.1, 0.7, 0.1]))
3302 >>> cols
3303 >>> #: Converting to RGB
3304 >>> cols.to("RGB")
3305 >>> cols
3306 """
3308 def __init__(self, H, S, V, alpha = None):
3310 # checking inputs, save inputs on object
3311 self._data_ = {} # Dict to store the colors/color dimensions
3312 tmp = self._colorobject_check_input_arrays_(H = H, S = S, V = V, alpha = alpha)
3313 for key,val in tmp.items(): self._data_[key] = val
3314 # White spot definition (the default)
3315 self.set_whitepoint(X = 95.047, Y = 100.000, Z = 108.883)
3318 def to(self, to, fixup = True):
3319 """Transform Color Space
3321 Allows to transform the current object into a different color space,
3322 if possible. Converting the colors of the current object into
3323 another color space. After calling this method, the object
3324 will be of a different class.
3326 Args:
3327 to (str): Name of the color space into which the colors should be
3328 converted (e.g., `"CIEXYZ"`, `"HCL"`, `"hex"`, `"sRGB"`, ...).
3329 fixup (bool): Whether or not colors outside the defined rgb color space
3330 should be corrected if necessary, defaults to `True`.
3332 Examples:
3334 >>> from colorspace import HSV
3335 >>> x = HSV([ 264, 314, 8, 44],
3336 >>> [0.80, 0.83, 0.57, 0.33],
3337 >>> [0.57, 0.75, 0.95, 0.91])
3338 >>> x
3339 >>> #:
3340 >>> type(x)
3341 >>> #: Convert colors to HLS
3342 >>> x.to("HLS")
3343 >>> x
3344 >>> #:
3345 >>> type(x)
3346 >>> #: Convert colors to HSV
3347 >>> x.to("HSV")
3348 >>> x
3349 >>> #: Extracting hex colors (returns list of str)
3350 >>> x.colors()
3352 """
3353 self._check_if_allowed_(to)
3354 from . import colorlib
3355 clib = colorlib()
3357 # Nothing to do (converted to itself)
3358 if to == self.__class__.__name__:
3359 return
3361 # The only transformation we need is back to RGB
3362 elif to == "sRGB":
3363 [R, G, B] = clib.HSV_to_sRGB(self.get("H"), self.get("S"), self.get("V"))
3364 self._data_ = {"R" : R, "G" : G, "B" : B, "alpha" : self.get("alpha")}
3365 self.__class__ = sRGB
3367 # From HLS to RGB: take direct path (not via sRGB)
3368 elif to in ["RGB"]:
3369 [R, G, B] = clib.HSV_to_RGB(self.get("H"), self.get("S"), self.get("V"))
3370 self._data_ = {"R" : R, "G" : G, "B" : B, "alpha" : self.get("alpha")}
3371 self.__class__ = RGB
3373 elif to == "hex":
3374 via = ["sRGB", to]
3375 self._transform_via_path_(via, fixup = fixup)
3377 elif to == "HLS":
3378 via = ["sRGB", to]
3379 self._transform_via_path_(via, fixup = fixup)
3381 elif to in ["CIEXYZ", "CIELUV", "CIELAB", "polarLUV", "HCL", "polarLAB"]:
3382 self._ambiguous(self.__class__.__name__, to)
3384 else: self._cannot(self.__class__.__name__, to)
3387class HLS(colorobject):
3388 """Create HLS Color Object
3390 Creates a color object in the Hue-Lightness-Saturation (HLS) color space.
3391 Can be converted to: :py:class:`RGB`, :py:class:`sRGB`, :py:class:`HSV`,
3392 and :py:class:`hexcols`.
3393 Not allowed (ambiguous) are transformations to :py:class:`CIEXYZ`,
3394 :py:class:`CIELUV`, :py:class:`CIELAB`, :py:class:`polarLUV`, and
3395 :py:class:`polarLAB`.
3397 Args:
3398 H (int, float, list, numpy.array):
3399 Numeric value(s) for Hue dimension.
3400 L (int, float, list, numpy.array):
3401 Numeric value(s) for Lightness dimension.
3402 S (int, float, list, numpy.array):
3403 Numeric value(s) for Saturation dimension.
3404 alpha (None, float, list, numpy.array): Numeric value(s) for the alpha
3405 channel (`[0., 1.]`) where `0.` equals full transparency, `1.` full
3406 opacity. If `None` (default) no transparency is added.
3408 Examples:
3410 >>> from colorspace import HLS
3411 >>> # Constructing color object with one single color via float
3412 >>> HLS(150, 0.1, 3)
3413 >>> #: Constructing object via lists
3414 >>> HLS([150, 0, 10], [0.1, 0.7, 0.1], [3, 0, 3])
3415 >>> #: Constructing object via numpy arrays
3416 >>> from numpy import asarray
3417 >>> cols = HLS(asarray([150, 0, 10]),
3418 >>> asarray([0.1, 0.7, 0.1]),
3419 >>> asarray([3, 0, 3]))
3420 >>> cols
3421 >>> #: Converting to RGB
3422 >>> cols.to("RGB")
3423 >>> cols
3424 """
3426 def __init__(self, H, L, S, alpha = None):
3428 # checking inputs, save inputs on object
3429 self._data_ = {} # Dict to store the colors/color dimensions
3430 tmp = self._colorobject_check_input_arrays_(H = H, L = L, S = S, alpha = None)
3431 for key,val in tmp.items(): self._data_[key] = val
3432 # White spot definition (the default)
3433 self.set_whitepoint(X = 95.047, Y = 100.000, Z = 108.883)
3436 def to(self, to, fixup = True):
3437 """Transform Color Space
3439 Allows to transform the current object into a different color space,
3440 if possible. Converting the colors of the current object into
3441 another color space. After calling this method, the object
3442 will be of a different class.
3444 Args:
3445 to (str): Name of the color space into which the colors should be
3446 converted (e.g., `"CIEXYZ"`, `"HCL"`, `"hex"`, `"sRGB"`, ...).
3447 fixup (bool): Whether or not colors outside the defined rgb color space
3448 should be corrected if necessary, defaults to `True`.
3450 Examples:
3452 >>> from colorspace import HLS
3453 >>> x = HLS([264, 314, 8, 44],
3454 >>> [0.34, 0.44, 0.68, 0.75],
3455 >>> [0.67, 0.71, 0.84, 0.62])
3456 >>> x
3457 >>> #:
3458 >>> type(x)
3459 >>> #: Convert colors to HSV
3460 >>> x.to("HSV")
3461 >>> x
3462 >>> #:
3463 >>> type(x)
3464 >>> #: Convert colors to HLS
3465 >>> x.to("HLS")
3466 >>> x
3467 >>> #: Extracting hex colors (returns list of str)
3468 >>> x.colors()
3470 """
3471 self._check_if_allowed_(to)
3472 from . import colorlib
3473 clib = colorlib()
3475 # Nothing to do (converted to itself)
3476 if to == self.__class__.__name__:
3477 return
3479 # The only transformation we need is back to RGB
3480 elif to == "sRGB":
3481 [R, G, B] = clib.HLS_to_sRGB(self.get("H"), self.get("L"), self.get("S"))
3482 self._data_ = {"R" : R, "G" : G, "B" : B, "alpha" : self.get("alpha")}
3483 self.__class__ = sRGB
3485 # From HSV to RGB: take direct path (not via sRGB)
3486 elif to in ["RGB"]:
3487 [R, G, B] = clib.HLS_to_RGB(self.get("H"), self.get("L"), self.get("S"))
3488 self._data_ = {"R" : R, "G" : G, "B" : B, "alpha" : self.get("alpha")}
3489 self.__class__ = RGB
3491 elif to == "hex":
3492 via = ["sRGB", to]
3493 self._transform_via_path_(via, fixup = fixup)
3495 elif to == "HSV":
3496 via = ["sRGB", to]
3497 self._transform_via_path_(via, fixup = fixup)
3499 elif to in ["CIEXYZ", "CIELUV", "CIELAB", "polarLUV", "HCL", "polarLAB"]:
3500 self._ambiguous(self.__class__.__name__, to)
3502 else: self._cannot(self.__class__.__name__, to)
3505class hexcols(colorobject):
3506 """Create Hex Color Object
3508 Creates a color object using hex colors (str).
3509 Can be converted to all other color spaces: :py:class:`CIELAB`,
3510 :py:class:`CIELUV`, :py:class:`CIEXYZ`, :py:class:`HLS`, :py:class:`HSV`,
3511 :py:class:`RGB`, :py:class:`polarLAB`, :py:class:`polarLUV`, and
3512 :py:class:`sRGB`.
3514 Args:
3515 hex_ (str, list of str, numpy.ndarray of type str):
3516 Hex colors. Only six and eight digit hex colors are allowed (e.g.,
3517 `#000000` or `#00000050` if with alpha channel). If invalid hex
3518 colors are provided the object will raise an exception. Invalid hex
3519 colors will be handled as `numpy.nan`.
3521 Examples:
3523 >>> from colorspace import hexcols
3524 >>> # Creating hex color object from string
3525 >>> hexcols("#cecece")
3526 >>> #: Creating hex color object from list of strings
3527 >>> hexcols(["#ff0000", "#00ff00"])
3528 >>> #: Creating hex colors via numpy array
3529 >>> from numpy import asarray
3530 >>> cols = hexcols(asarray(["#ff000030", "#00ff0030",
3531 >>> "#FFFFFF", "#000"]))
3532 >>> cols
3533 >>> #: Convert hex colors to another color space (CIEXYZ)
3534 >>> cols.to("CIEXYZ")
3535 >>> cols
3536 >>> #: Picking 7 hex colors from the Green-Orange
3537 >>> # diverging palette for demonstrating standard representation
3538 >>> # in jupyter engine and standard print.
3539 >>> from colorspace import diverging_hcl
3540 >>> cols2 = hexcols(diverging_hcl("Green-Orange")(7))
3541 >>> cols2 # jupyter HTML representation
3542 >>> #:
3543 >>> print(cols2) # default representation
3544 """
3546 def __init__(self, hex_):
3548 from colorspace import check_hex_colors
3549 import numpy as np
3551 # If hex_ is str, convert to list
3552 if isinstance(hex_, str): hex_ = [hex_]
3553 hex_ = check_hex_colors(hex_)
3555 self._data_ = {} # Dict to store the colors/color dimensions
3557 # This is the one step where we extract transparency from
3558 # hex colors once we enter the world of colorobjects.
3559 def get_alpha(hex_):
3560 # Trying to extract char 7:9, leave None if color is None
3561 hex_ = [None if (x is None or len(x) < 9) else x[7:9] for x in hex_]
3562 return [None if x is None else int(x, 16) / 255 for x in hex_]
3564 # Remove apha if any
3565 def remove_alpha(hex_):
3566 return [None if x is None else x[:7] if len(x) > 7 else x for x in hex_]
3568 # Forwarding input 'hex_' to check_hex_colors which will throw
3569 # an error if we do not understand this input type.
3570 tmp = np.asarray(get_alpha(hex_), dtype = "float")
3571 # Remove alpha from 9-digit hex if any, convert to ndarray
3572 self._data_["hex_"] = np.asarray(remove_alpha(hex_), dtype = object)
3573 # Store alpha (if any)
3574 if not np.all(np.isnan(tmp)): self._data_["alpha"] = tmp
3576 # White spot definition (the default)
3577 self.set_whitepoint(X = 95.047, Y = 100.000, Z = 108.883)
3580 def to(self, to, fixup = True):
3581 """Transform Color Space
3583 Allows to transform the current object into a different color space,
3584 if possible.
3586 Allows to transform the current object into a different color space,
3587 if possible. Converting the colors of the current object into
3588 another color space. After calling this method, the object
3589 will be of a different class.
3591 Args:
3592 to (str): Name of the color space into which the colors should be
3593 converted (e.g., `"CIEXYZ"`, `"HCL"`, `"HSL"`, `"sRGB"`, ...).
3594 fixup (bool): Whether or not colors outside the defined rgb color space
3595 should be corrected if necessary, defaults to `True`.
3597 Examples:
3599 >>> from colorspace import hexcols
3600 >>> x = hexcols(["#4B1D91", "#BF219A", "#F27B68", "#E7D39A"])
3601 >>> x
3602 >>> #:
3603 >>> type(x)
3604 >>> #: Convert colors to sRGB
3605 >>> x.to("sRGB")
3606 >>> x
3607 >>> #:
3608 >>> type(x)
3609 >>> #: Convert from sRGB to HCL
3610 >>> x.to("HCL")
3611 >>> x
3612 >>> #: Convert back to hex colors.
3613 >>> # Round-off errors due to conversion to 'hex'.
3614 >>> x.to("hex")
3615 >>> x
3617 """
3618 self._check_if_allowed_(to)
3619 from . import colorlib
3620 clib = colorlib()
3622 # Nothing to do (converted to itself)
3623 if to in ["hex", self.__class__.__name__]:
3624 return
3626 # The only transformation we need is from hexcols -> sRGB
3627 elif to == "sRGB":
3628 [R, G, B] = clib.hex_to_sRGB([None if x is None else x[0:7] for x in self.get("hex_")])
3629 alpha = self.get("alpha")
3630 self._data_ = {"R": R, "G": G, "B": B}
3631 if alpha is not None: self._data_["alpha"] = alpha
3632 self.__class__ = sRGB
3634 # The rest are transformations along a path
3635 elif to == "RGB":
3636 via = ["sRGB", to]
3637 self._transform_via_path_(via, fixup = fixup)
3639 elif to in ["HLS", "HSV"]:
3640 via = ["sRGB", to]
3641 self._transform_via_path_(via, fixup = fixup)
3643 elif to in ["CIEXYZ"]:
3644 via = ["sRGB", "RGB", to]
3645 self._transform_via_path_(via, fixup = fixup)
3647 elif to in ["CIELUV", "CIELAB"]:
3648 via = ["sRGB", "RGB", "CIEXYZ", to]
3649 self._transform_via_path_(via, fixup = fixup)
3651 elif to in ["HCL", "polarLUV"]:
3652 via = ["sRGB", "RGB", "CIEXYZ", "CIELUV", to]
3653 self._transform_via_path_(via, fixup = fixup)
3655 elif to in "polarLAB":
3656 via = ["sRGB", "RGB", "CIEXYZ", "CIELAB", to]
3657 self._transform_via_path_(via, fixup = fixup)
3659 else: self._cannot(self.__class__.__name__, to)
3661 def _repr_html_(self):
3662 """_repr_html_()
3664 Standard HTML representation of the object when using
3665 the jupyter engine. Will display the colors as html list,
3666 thanks to @matteoferla (github) for the idea and contribution.
3667 """
3668 from colorspace import contrast_ratio
3670 # ul style
3671 su = {"font-size": "0.5em", "list-style": "none", "display": "flex",
3672 "padding": "0 0 0.5em 0", "text-align": "center"}
3673 # li style
3674 sl = {"width": "5.75em", "height": "5.75em", "padding": "0.25em",
3675 "display": "inline-block", "margin": "0 0.25em 0 0",
3676 "border": "0.5px solid gray"}
3678 # Getting list of hex colors
3679 cols = self.colors()
3681 dict2style = lambda d: ';'.join(map(':'.join, d.items()))
3683 res = f"<ul class=\"colorspace-hexcols\" style=\"{dict2style(su)}\">\n"
3684 for i in range(len(self)):
3685 # Calculating contrast ratio to decide text color
3686 cw = contrast_ratio("#FFF", bg = cols[i])[0]
3687 cb = contrast_ratio("#000", bg = cols[i])[0]
3688 sl["color"] = "white" if cw > cb else "black"
3689 sl["background-color"] = cols[i]
3690 res += f"<li style=\"{dict2style(sl)}\">{cols[i]}</li>\n"
3692 res += "</ul>\n"
3693 return res
3695def compare_colors(a, b, exact = False, _all = True, atol = None):
3696 """Compare Sets of Colors
3698 Compares two sets of colors based on two color objects. The objects
3699 provided on argument `a` and `b` must inherit from `colorobject`.
3700 This can be any of the following classes: :py:class:`CIELAB`,
3701 :py:class:`CIELUV`, :py:class:`CIEXYZ`, :py:class:`HLS`, :py:class:`HSV`,
3702 :py:class:`RGB`, :py:class:`hexcols`, :py:class:`polarLAB`,
3703 :py:class:`polarLUV`, or :py:class:`sRGB`.
3705 Args:
3706 a (colorobject): Object which inherits from `colorobject`.
3707 b (colorobject): Object which inherits from `colorobject`.
3708 exact (bool): Default `False`, check for colors being nearly equal
3709 (see `atol`). If set to `True` the coordinates must be identical.
3710 Note: in case `a` and `b` are hex colors
3711 (colorspace.colorlib.hexcols) strings will always be matched exactly.
3712 _all (bool): Default `True`; the function will return `True` if
3713 all colors are identical/nearly equal. If set to `False` the return
3714 will be a list of bool containing `True` and `False` for each
3715 pair of colors.
3716 atol (None or float): Absolute tolerance for the distance measure
3717 between two colors to be considered as nearly equal (must be > 0 if set).
3718 Only used if `exact = False`, else `atol = 1e-6` is used. If set
3719 to `None` the tolerance will automatically be set depending on the
3720 type of the objects. Defaults to None.
3723 Returns:
3724 bool, list: Returns `True` if all colors of `a` are exactly equal or
3725 nearly equal (see arguments) to the colors in object `b`. If `_all =
3726 False`, a list of bool is returned indicating pair-wise comparison
3727 of all colors in `a` and `b`.
3729 Example:
3731 >>> from colorspace import RGB, hexcols, compare_colors
3732 >>>
3733 >>> # Three RGB colors
3734 >>> a = RGB([0.5, 0.5], [0.1, 0.1], [0.9, 0.9])
3735 >>> b = RGB([0.5, 0.5], [0.1, 0.1], [0.9, 0.91])
3736 >>>
3737 >>> compare_colors(a, b)
3738 >>> #:
3739 >>> compare_colors(a, b, atol = 0.1)
3740 >>> #:
3741 >>> compare_colors(a, b, exact = True)
3742 >>> #:
3743 >>> compare_colors(a, b, exact = True, _all = False)
3744 >>>
3745 >>> #: Same example using two sets of hexcolor objects
3746 >>> x = hexcols(["#ff00ff", "#003300"])
3747 >>> y = hexcols(["#ff00ff", "#003301"])
3748 >>> compare_colors(x, y)
3749 >>> #:
3750 >>> compare_colors(x, y, _all = False)
3751 >>>
3752 >>> #: Convert HEX to HCL (polarLUV) and back, compare the
3753 >>> # resulting colors to the original ones; should be identical
3754 >>> from copy import deepcopy
3755 >>> z = hexcols(["#ff00ff", "#003301"])
3756 >>> zz = deepcopy(z)
3757 >>> zz.to("HCL")
3758 >>> zz
3759 >>> #:
3760 >>> zz.to("hex")
3761 >>> zz
3762 >>> #:
3763 >>> compare_colors(z, zz)
3765 Raises:
3766 TypeError: If `a` or `b` are not objects of a class which inherits from
3767 `colorobject`.
3768 TypeError: If `a` and `b` are not of the same class.
3769 ValueError: If `a` and `b` are not of the same length, i.e., do not contain
3770 the same number of colors.
3771 TypeError: If `exact` or `_all` are not bool.
3772 TypeError: If `atol` is neither `None` nor float.
3773 ValueError: If `atol` is not larger than 0.
3774 """
3776 from numpy import sqrt, isclose
3778 if not isinstance(a, colorobject):
3779 raise TypeError("argument `a` must be an object based on colorspace.colorlib.colorobject")
3780 if not isinstance(b, colorobject):
3781 raise TypeError("argument `b` must be an object based on colorspace.colorlib.colorobject")
3782 if not type(a) == type(b):
3783 raise TypeError("Input `a` and `b` not of same type")
3784 if not a.length() == b.length():
3785 raise ValueError("Objects do not contain the same number of colors")
3786 if not isinstance(exact, bool):
3787 raise TypeError("argument `exact` must be bool")
3788 if not isinstance(_all, bool):
3789 raise TypeError("argument `_all` must be bool")
3790 if not isinstance(atol, float) and not isinstance(atol, type(None)):
3791 raise TypeError("argument `atol` must be float or None")
3792 if atol is not None and atol <= 0:
3793 raise ValueError("argument `atol` must be > 0.")
3795 if exact: atol = 1e-6
3797 def distance(a, b):
3798 dist = 0. # Start with zero distance
3799 for n in list(a._data_.keys()):
3800 tmpa = a.get(n)
3801 tmpb = b.get(n)
3802 # Both None and that is our alpha channel, no judgement
3803 if tmpa is None and tmpb is None and n == "alpha":
3804 continue
3805 # Bot not none, calc Eudlidean distance
3806 elif not tmpa is None and not tmpb is None:
3807 dist += (tmpa[0] - tmpb[0])**2.0
3808 # One missing? Penalize by + 100
3809 else:
3810 dist += 100.
3812 return sqrt(dist)
3814 # Compare hex colors; always on string level
3815 if isinstance(a, hexcols):
3816 # Getting coordinates
3817 res = [a[i].get("hex_")[0].upper() == b[i].get("hex_")[0].upper() for i in range(0, a.length())]
3818 # Calculate absolute difference between coordinates R/G/B[/alpha].
3819 # Threading alpha like another coordinates as all coordinates are scaled [0-1].
3820 elif isinstance(a, RGB) or isinstance(a, sRGB) or \
3821 isinstance(a, HLS) or isinstance(a, HSV):
3822 # HEX precision in RGB coordinates is about sqrt((1./255.)**2 * 3) / 2 = 0.003396178
3823 if not atol: atol = 0.005
3824 res = [distance(a[i], b[i]) for i in range(0, a.length())]
3825 res = isclose(res, 0, atol = atol)
3826 # HCL or polarLUV (both return instance polarLUV)
3827 # TODO(enhancement): Calculating the Euclidean distance on HCL and (if
3828 # available) alpha which itself is in [0, 1]. Should be weighted
3829 # differently (scaled distance)?
3830 elif isinstance(a, polarLUV) or isinstance(a, CIELUV) or isinstance(a, CIELAB) or \
3831 isinstance(a, polarLAB) or isinstance(a, CIEXYZ):
3833 if not atol: atol = 1
3834 res = [distance(a[i], b[i]) for i in range(0, a.length())]
3835 res = isclose(res, 0, atol = atol)
3838 # If _all is True: check if all elements are True
3839 if _all:
3840 from numpy import all
3841 res = all(res)
3842 return res