Coverage for src/colorspace/colorlib.py: 98%
1066 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-08-23 19:54 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2024-08-23 19:54 +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 # Checking inputs
242 self._check_input_arrays_(__fname__, u = u, gamma = gamma)
244 # Transform
245 for i,val in np.ndenumerate(u):
246 if val > 0.00304: u[i] = 1.055 * np.power(val, (1. / gamma[i])) - 0.055
247 else: u[i] = 12.92 * val
249 return u
251 def ftrans(self, u, gamma):
252 """Gamma Correction
254 Function `gtrans` and `ftrans` provide gamma correction which
255 can be used to switch between sRGB and linearised sRGB (RGB).
257 The standard value of gamma for sRGB displays is approximately `2.2`,
258 but more accurately is a combination of a linear transform and
259 a power transform with exponent `2.4`.
260 `gtrans` maps linearised sRGB to sRGB, `ftrans` provides the inverse mapping.
262 Args:
263 u (numpy.ndarray): Float array of length `N`.
264 gamma (float, numpy.ndarray): gamma value; if float or
265 `numpy.ndarray` of length one, `gamma` will be recycled if needed.
267 Returns:
268 numpy.ndarray: Gamma corrected values, same length as input `u`.
269 """
271 __fname__ = inspect.stack()[0][3] # Name of this method
273 # Input check
274 if isinstance(gamma, float): gamma = np.asarray([gamma])
275 if len(gamma) == 1 and not len(gamma) == len(u):
276 gamma = np.repeat(gamma, len(u))
278 # Checking inputs
279 self._check_input_arrays_(__fname__, u = u, gamma = gamma)
281 # Transform
282 for i,val in np.ndenumerate(u):
283 if val > 0.03928: u[i] = np.power((val + 0.055) / 1.055, gamma[i])
284 else: u[i] = val / 12.92
286 return u
288 # Support function qtrans
289 def _qtrans(self, q1, q2, hue):
290 if hue > 360.: hue = hue - 360.
291 if hue < 0: hue = hue + 360.
293 if hue < 60.: return q1 + (q2 - q1) * hue / 60.
294 elif hue < 180.: return q2
295 elif hue < 240.: return q1 + (q2 - q1) * (240. - hue) / 60.
296 else: return q1
299 def sRGB_to_RGB(self, R, G, B, gamma = 2.4):
300 """Convert Standard RGB to RGB
302 Converting colors from the Standard RGB color space to RGB.
304 Args:
305 R (numpy.ndarray): Intensities for red (`[0., 1.]`).
306 G (numpy.ndarray): Intensities for green (`[0., 1.]`).
307 B (numpy.ndarray): Intensities for blue (`[0., 1.]`).
308 gamma (float): gamma adjustment, defaults to `2.4`.
310 Returns:
311 list: Returns a list of `numpy.ndarray`s with `R`, `G`, and `B` values.
312 """
314 __fname__ = inspect.stack()[0][3] # Name of this method
316 # Input check
317 if isinstance(gamma, float): gamma = np.asarray([gamma])
318 if len(gamma) == 1 and not len(gamma) == len(R):
319 gamma = np.repeat(gamma, len(R))
321 # Checking inputs
322 self._check_input_arrays_(__fname__, R = R, G = G, B = B, gamma = gamma)
324 # Apply gamma correction
325 return [self.ftrans(x, gamma) for x in [R, G, B]]
327 def RGB_to_sRGB(self, R, G, B, gamma = 2.4):
328 """Convert RGB to Standard RGB
330 Converts one (or multiple) colors defined by their red, blue, green,
331 and blue coordinates (`[0.0, 1.0]`) to the Standard RGB color space;
332 returning a modified list of red, green, blue coordinates.
334 Args:
335 R (numpy.ndarray): Intensities for red (`[0., 1.]`).
336 G (numpy.ndarray): Intensities for green (`[0., 1.]`).
337 B (numpy.ndarray): Intensities for blue (`[0., 1.]`).
338 gamma (float): gamma adjustment, defaults to `2.4`.
340 Returns:
341 list: Returns a list of `numpy.ndarray`s with `R`, `G`, and `B` values.
342 """
344 __fname__ = inspect.stack()[0][3] # Name of this method
346 # Input check
347 if isinstance(gamma, float): gamma = np.asarray([gamma])
348 if len(gamma) == 1 and not len(gamma) == len(R):
349 gamma = np.repeat(gamma, len(R))
351 # Checking inputs
352 self._check_input_arrays_(__fname__, R = R, G = G, B = B, gamma = gamma)
354 # Apply gamma correction
355 return [self.gtrans(x, gamma) for x in [R, G, B]]
357 # -------------------------------------------------------------------
358 # -------------------------------------------------------------------
359 # -------------------------------------------------------------------
360 # -------------------------------------------------------------------
362 ## ----- CIE-XYZ <-> Device independent RGB -----
363 ## R, G, and B give the levels of red, green and blue as values
364 ## in the interval [0., 1.]. X, Y and Z give the CIE chromaticies.
365 ## XN, YN, ZN gives the chromaticity of the white point.
366 def RGB_to_XYZ(self, R, G, B, XN = None, YN = None, ZN = None):
367 """Convert RGB to CIEXYZ
369 `R`, `G`, and `B` give the levels of red, green and blue as values
370 in the interval `[0., 1.]`.
371 `XN`, `YN`, and `ZN` allow to specify additional CIE chromaticities to
372 specify a specific white point.
374 Args:
375 R (numpy.ndarray): Intensities for red (`[0., 1.]`).
376 G (numpy.ndarray): Intensities for green (`[0., 1.]`).
377 B (numpy.ndarray): Intensities for blue (`[0., 1.]`).
378 XN (None, numpy.ndarray): Chromaticity of the white point. If of
379 length `1`, the white point specification will be recycled if needed.
380 When not specified (all `None`) a default white point is used.
381 YN: See `XN`.
382 ZN: See `XN`.
384 Returns:
385 list: Returns corresponding coordinates of CIE chromaticities, a
386 list of `numpy.ndarray`s of the same length as the inputs (`[X, Y, Z]`).
387 """
389 __fname__ = inspect.stack()[0][3] # Name of this method
390 n = len(R) # Number of colors
392 # Loading definition of white
393 [XN, YN, ZN] = self._get_white_(__fname__, n, XN, YN, ZN)
395 # Checking input
396 self._check_input_arrays_(__fname__, R = R, G = G, B = B)
398 return [YN * (0.412453 * R + 0.357580 * G + 0.180423 * B), # X
399 YN * (0.212671 * R + 0.715160 * G + 0.072169 * B), # Y
400 YN * (0.019334 * R + 0.119193 * G + 0.950227 * B)] # Z
402 def XYZ_to_RGB(self, X, Y, Z, XN = None, YN = None, ZN = None):
403 """Convert CIEXYZ to RGB
405 `X`, `Y`, and `Z` specify the values in the three coordinates of the
406 CIEXYZ color space,
407 `XN`, `YN`, and `ZN` allow to specify additional CIE chromaticities to
408 specify a specific white point.
410 Args:
411 X (numpy.ndarray): Values for the `X` dimension.
412 Y (numpy.ndarray): Values for the `Y` dimension.
413 Z (numpy.ndarray): Values for the `Z` dimension.
414 XN (None, numpy.ndarray): Chromaticity of the white point. If of
415 length `1`, the white point specification will be recycled if needed.
416 When not specified (all `None`) a default white point is used.
417 YN: See `XN`.
418 ZN: See `XN`.
420 Returns:
421 list: Returns corresponding coordinates as a list of
422 `numpy.ndarray`s of the same length as the inputs (`[R, G, B]`).
423 """
425 __fname__ = inspect.stack()[0][3] # Name of this method
426 n = len(X) # Number of colors
428 # Loading definition of white
429 [XN, YN, ZN] = self._get_white_(__fname__, n, XN, YN, ZN)
431 # Checking input
432 self._check_input_arrays_(__fname__, X = X, Y = Y, Z = Z)
434 # Only YN is used
435 return [( 3.240479 * X - 1.537150 * Y - 0.498535 * Z) / YN, # R
436 (-0.969256 * X + 1.875992 * Y + 0.041556 * Z) / YN, # G
437 ( 0.055648 * X - 0.204043 * Y + 1.057311 * Z) / YN] # B
440 # -------------------------------------------------------------------
441 # -------------------------------------------------------------------
442 # -------------------------------------------------------------------
443 # -------------------------------------------------------------------
446 ## Unused as we are going CIE-XYZ <-> RGB <-> sRGB
447 ##
448 ## ## ----- CIE-XYZ <-> sRGB -----
449 ## ## R, G, and B give the levels of red, green and blue as values
450 ## ## in the interval [0., 1.]. X, Y and Z give the CIE chromaticies.
451 ## ## XN, YN, ZN gives the chromaticity of the white point.
452 ## def sRGB_to_XYZ(self, R, G, B, XN = None, YN = None, ZN = None):
453 ## """sRGB to CIEXYZ.
455 ## R, G, and B give the levels of red, green and blue as values
456 ## in the interval `[0., 1.]`. X, Y and Z give the CIE chromaticies.
458 ## Args:
459 ## R (numpy.ndarray): Indensities for red (`[0., 1.]`).
460 ## G (numpy.ndarray): Indensities for green (`[0., 1.]`).
461 ## B (numpy.ndarray): Indensities for blue (`[0., 1.]`).
462 ## XN (None or numpy.ndarray): Chromaticity of the white point. If of
463 ## length 1 the white point specification will be recycled if length of
464 ## R/G/B is larger than one. If not specified (all three `None`) default
465 ## values will be used. Defaults to None, see also YN, ZN.
466 ## YN: See `XN`.
467 ## ZN: See `XN`.
469 ## Returns:
470 ## list: Returns corresponding X/Y/Z coordinates of CIE chromaticies, a list
471 ## of `numpy.ndarray`'s of the same length as the inputs (`[X, Y,
472 ## Z]`).
473 ## """
475 ## __fname__ = inspect.stack()[0][3] # Name of this method
476 ## n = len(R) # Number of colors
478 ## # Loading definition of white
479 ## [XN, YN, ZN] = self._get_white_(__fname__, n, XN, YN, ZN)
481 ## # Checking input
482 ## self._check_input_arrays_(__fname__, R = R, G = G, B = B)
484 ## # Transform R/G/B
485 ## R = self.ftrans(R, 2.4)
486 ## G = self.ftrans(G, 2.4)
487 ## B = self.ftrans(B, 2.4)
489 ## # Convert to X/Y/Z coordinates
490 ## return[YN * (0.412453 * R + 0.357580 * G + 0.180423 * B), # X
491 ## YN * (0.212671 * R + 0.715160 * G + 0.072169 * B), # Y
492 ## YN * (0.019334 * R + 0.119193 * G + 0.950227 * B)] # Z
494 ## def XYZ_to_sRGB(self, X, Y, Z, XN = None, YN = None, ZN = None):
495 ## """CIEXYZ to sRGB.
497 ## R, G, and B give the levels of red, green and blue as values
498 ## in the interval `[0., 1.]`. X, Y and Z give the CIE chromaticies.
500 ## Args:
501 ## X (numpy.ndarray): Values for the X dimension.
502 ## Y (numpy.ndarray): Values for the Y dimension.
503 ## Z (numpy.ndarray): Values for the Z dimension.
504 ## XN (None or numpy.ndarray): Chromaticity of the white point. If of
505 ## length 1 the white point specification will be recycled if length of
506 ## R/G/B is larger than one. If not specified (all three `None`) default
507 ## values will be used. Defaults to None, see also YN, ZN.
508 ## YN: See `XN`.
509 ## ZN: See `XN`.
511 ## Returns:
512 ## list: Returns corresponding X/Y/Z coordinates of CIE chromaticies, a list
513 ## of `numpy.ndarray`'s of the same length as the inputs (`[R, G, B]`).
514 ## """
516 ## __fname__ = inspect.stack()[0][3] # Name of this method
517 ## n = len(X) # Number of colors
519 ## # Loading definition of white
520 ## [XN, YN, ZN] = self._get_white_(__fname__, n, XN, YN, ZN)
522 ## # Checking input
523 ## self._check_input_arrays_(__fname__, X = X, Y = Y, Z = Z)
525 ## # Transform and return
526 ## return [self.gtrans(( 3.240479 * X - 1.537150 * Y - 0.498535 * Z) / YN, 2.4), # R
527 ## self.gtrans((-0.969256 * X + 1.875992 * Y + 0.041556 * Z) / YN, 2.4), # G
528 ## self.gtrans(( 0.055648 * X - 0.204043 * Y + 1.057311 * Z) / YN, 2.4)] # B
531 # -------------------------------------------------------------------
532 # -------------------------------------------------------------------
533 # -------------------------------------------------------------------
534 # -------------------------------------------------------------------
536 ## ----- CIE-XYZ <-> CIE-LAB ----- */
539 def LAB_to_XYZ(self, L, A, B, XN = None, YN = None, ZN = None):
540 """Convert CIELAB to CIEXYZ
542 `L`, `A`, and `B` specify the values in the three coordinates of the
543 CIELAB color space,
544 `XN`, `YN`, and `ZN` allow to specify additional CIE chromaticities to
545 specify a specific white point.
547 Args:
548 L (numpy.ndarray): Values for the `L` dimension.
549 A (numpy.ndarray): Values for the `A` dimension.
550 B (numpy.ndarray): Values for the `B` dimension.
551 XN (None, numpy.ndarray): Chromaticity of the white point. If of
552 length `1`, the white point specification will be recycled if needed.
553 When not specified (all `None`) a default white point is used.
554 YN: See `XN`.
555 ZN: See `XN`.
557 Returns:
558 list: Returns corresponding coordinates of CIE chromaticities as a
559 list of `numpy.ndarray`s of the same length as the inputs (`[X, Y, Z]`).
560 """
562 __fname__ = inspect.stack()[0][3] # Name of this method
563 n = len(L) # Number of colors
565 # Loading definition of white
566 [XN, YN, ZN] = self._get_white_(__fname__, n, XN, YN, ZN)
568 # Checking input
569 self._check_input_arrays_(__fname__, L = L, A = A, B = B)
571 # Result arrays
572 X = np.ndarray(len(L), dtype = "float"); X[:] = 0.
573 Y = np.ndarray(len(L), dtype = "float"); Y[:] = 0.
574 Z = np.ndarray(len(L), dtype = "float"); Z[:] = 0.
576 # Calculate Y
577 for i,val in np.ndenumerate(L):
578 if val <= 0: Y[i] = 0.
579 elif val <= 8.0: Y[i] = val * YN[i] / self._KAPPA
580 elif val <= 100.: Y[i] = YN[i] * np.power((val + 16.) / 116., 3.)
581 else: Y[i] = YN[i]
583 fy = np.ndarray(len(Y), dtype = "float")
584 for i,val in np.ndenumerate(Y):
585 if val <= (self._EPSILON * YN[i]):
586 fy[i] = (self._KAPPA / 116.) * val / YN[i] + 16. / 116.
587 else:
588 fy[i] = np.power(val / YN[i], 1. / 3.)
590 # Calculate X
591 fx = fy + (A / 500.)
592 for i,val in np.ndenumerate(fx):
593 if np.power(val, 3.) <= self._EPSILON:
594 X[i] = XN[i] * (val - 16. / 116.) / (self._KAPPA / 116.)
595 else:
596 X[i] = XN[i] * np.power(val, 3.)
598 # Calculate Z
599 fz = fy - (B / 200.)
600 for i,val in np.ndenumerate(fz):
601 if np.power(val, 3.) <= self._EPSILON:
602 Z[i] = ZN[i] * (val - 16. / 116.) / (self._KAPPA / 116.)
603 else:
604 Z[i] = ZN[i] * np.power(val, 3)
606 return [X, Y, Z]
608 def XYZ_to_LAB(self, X, Y, Z, XN = None, YN = None, ZN = None):
609 """Convert CIEXYZ to CIELAB
611 `X`, `Y`, and `Z` specify the values in the three coordinates of the
612 CIELAB color space,
613 `XN`, `YN`, and `ZN` allow to specify additional CIE chromaticities to
614 specify a specific white point.
616 Args:
617 X (numpy.ndarray): Values for the `X` dimension.
618 Y (numpy.ndarray): Values for the `Y` dimension.
619 Z (numpy.ndarray): Values for the `Z` dimension.
620 XN (None, numpy.ndarray): Chromaticity of the white point. If of
621 length `1`, the white point specification will be recycled if needed.
622 When not specified (all `None`) a default white point is used.
623 YN: See `XN`.
624 ZN: See `XN`.
626 Returns:
627 list: Returns corresponding coordinates of CIE chromaticities as
628 a list of `numpy.ndarray`s of the same length as the inputs (`[L, A, B]`).
629 """
631 __fname__ = inspect.stack()[0][3] # Name of this method
632 n = len(X) # Number of colors
634 # Loading definition of white
635 [XN, YN, ZN] = self._get_white_(__fname__, n, XN, YN, ZN)
637 # Checking input
638 self._check_input_arrays_(__fname__, X = X, Y = Y, Z = Z)
640 # Support function
641 def f(t, _KAPPA, _EPSILON):
642 for i,val in np.ndenumerate(t):
643 if val > _EPSILON:
644 t[i] = np.power(val, 1./3.)
645 else:
646 t[i] = (_KAPPA / 116.) * val + 16. / 116.
647 return t
649 # Scaling
650 xr = X / XN;
651 yr = Y / YN;
652 zr = Z / ZN;
654 # Calculate L
655 L = np.ndarray(len(X), dtype = "float"); L[:] = 0.
656 for i,val in np.ndenumerate(yr):
657 if val > self._EPSILON:
658 L[i] = 116. * np.power(val, 1./3.) - 16.
659 else:
660 L[i] = self._KAPPA * val
662 xt = f(xr, self._KAPPA, self._EPSILON);
663 yt = f(yr, self._KAPPA, self._EPSILON);
664 zt = f(zr, self._KAPPA, self._EPSILON);
665 return [L, 500. * (xt - yt), 200. * (yt - zt)] # [L, A, B]
668 # -------------------------------------------------------------------
669 # -------------------------------------------------------------------
670 # -------------------------------------------------------------------
671 # -------------------------------------------------------------------
673 ## Commented as not yet used
674 ##
675 ## def XYZ_to_HLAB(self, X, Y, Z, XN = None, YN = None, ZN = None):
676 ## """CIE-XYZ to Hunter LAB.
678 ## .. note::
679 ## Note that the Hunter LAB is no longer part of the public API,
680 ## but the code is still here in case needed.
682 ## Args:
683 ## X (numpy.ndarray): Values for the X dimension.
684 ## Y (numpy.ndarray): Values for the Y dimension.
685 ## Z (numpy.ndarray): Values for the Z dimension.
686 ## XN (None or numpy.ndarray): Chromaticity of the white point. If of
687 ## length 1 the white point specification will be recycled if length of
688 ## R/G/B is larger than one. If not specified (all three `None`) default
689 ## values will be used. Defaults to None, see also YN, ZN.
690 ## YN: See `XN`.
691 ## ZN: See `XN`.
693 ## Returns:
694 ## list: Returns corresponding Hunter LAB chromaticies, a list of
695 ## `numpy.ndarray`'s of the same length as the inputs (`[L, A, B]`).
696 ## """
698 ## __fname__ = inspect.stack()[0][3] # Name of this method
699 ## n = len(X) # Number of colors
701 ## # Loading definition of white
702 ## [XN, YN, ZN] = self._get_white_(__fname__, n, XN, YN, ZN)
704 ## # Checking input
705 ## self._check_input_arrays_(__fname__, X = X, Y = Y, Z = Z)
707 ## # Transform
708 ## X = X / XN; Y = Y / YN; Z = Z / ZN;
709 ## l = np.sqrt(Y);
710 ## return [10. * l, 17.5 * (((1.02 * X) - Y) / l), 7. * ((Y - (0.847 * Z)) / l)] # [L, A, B]
713 ## def HLAB_to_XYZ(self, L, A, B, XN = None, YN = None, ZN = None):
714 ## """Hunter LAB to CIE-XYZ.
716 ## .. note::
717 ## Note that the Hunter LAB is no longer part of the public API,
718 ## but the code is still here in case needed.
720 ## Args:
721 ## L (numpy.ndarray): Values for the L dimension.
722 ## A (numpy.ndarray): Values for the A dimension.
723 ## B (numpy.ndarray): Values for the B dimension.
724 ## XN (None or numpy.ndarray): Chromaticity of the white point. If of
725 ## length 1 the white point specification will be recycled if length of
726 ## R/G/B is larger than one. If not specified (all three `None`) default
727 ## values will be used. Defaults to None, see also YN, ZN.
728 ## YN: See `XN`.
729 ## ZN: See `XN`.
731 ## Returns:
732 ## list: Returns corresponding CIE-XYZ chromaticies, a list of
733 ## `numpy.ndarray`'s of the same length as the inputs (`[X, Y, Z]`).
734 ## """
736 ## __fname__ = inspect.stack()[0][3] # Name of this method
737 ## n = len(L) # Number of colors
739 ## # Loading definition of white
740 ## [XN, YN, ZN] = self._get_white_(__fname__, n, XN, YN, ZN)
742 ## # Checking input
743 ## self._check_input_arrays_(__fname__, L = L, A = A, B = B)
745 ## # Transform
746 ## vY = L / 10.;
747 ## vX = (A / 17.5) * (L / 10);
748 ## vZ = (B / 7) * (L / 10);
749 ## vY = vY * vY;
751 ## Y = vY * XN
752 ## X = (vX + vY) / 1.02 * YN
753 ## Z = - (vZ - vY) / 0.847 * ZN
755 ## return [X, Y, Z]
758 # -------------------------------------------------------------------
759 # -------------------------------------------------------------------
760 # -------------------------------------------------------------------
761 # -------------------------------------------------------------------
762 def LAB_to_polarLAB(self, L, A, B):
763 """Convert CIELAB to the polar representation (polarLAB)
765 Converts colors from the CIELAB color space into its polar
766 representation (`polarLAB`).
767 Inverse function of :py:method:`polarLAB_to_LAB`.
769 Args:
770 L (numpy.ndarray): Values for the `L` dimension.
771 A (numpy.ndarray): Values for the `A` dimension.
772 B (numpy.ndarray): Values for the `B` dimension.
774 Returns:
775 list: Returns corresponding polar LAB chromaticities as a list of
776 `numpy.ndarray`s of the same length as the inputs (`[L, A, B]`).
777 """
779 __fname__ = inspect.stack()[0][3] # Name of this method
781 # Checking input
782 self._check_input_arrays_(__fname__, L = L, A = A, B = B)
784 # Compute H
785 H = self._RAD2DEG(np.arctan2(B, A))
786 for i,val in np.ndenumerate(H):
787 while val > 360.: val -= 360.
788 while val < 0.: val += 360.
789 H[i] = val
790 # Compute C
791 C = np.sqrt(A * A + B * B)
793 return [L, C, H]
795 def polarLAB_to_LAB(self, L, C, H):
796 """Convert polarLAB to CIELAB
798 Convert colors from the polar representation of the CIELAB
799 color space into CIELAB coordinates.
800 Inverse function of :py:method:`LAB_to_polarLAB`.
802 Args:
803 L (numpy.ndarray): Values for the polar `L` dimension.
804 C (numpy.ndarray): Values for the polar `C` dimension.
805 H (numpy.ndarray): Values for the polar `H` dimension.
807 Returns:
808 list: Returns corresponding CIELAB chromaticities as a list of
809 `numpy.ndarray`s of the same length as the inputs (`[L, A, B]`).
810 """
812 __fname__ = inspect.stack()[0][3] # Name of this method
814 # Checking input
815 self._check_input_arrays_(__fname__, L = L, H = H, C = C)
817 A = np.cos(self._DEG2RAD(H)) * C
818 B = np.sin(self._DEG2RAD(H)) * C
820 return [L, A, B]
822 # -------------------------------------------------------------------
823 # -------------------------------------------------------------------
824 # -------------------------------------------------------------------
825 # -------------------------------------------------------------------
826 def sRGB_to_HSV(self, r, g, b):
827 """Convert RGB to HSV
829 Convert one (or multiple) rgb colors given their red, blue, and
830 red coodinates (`[0.0, 1.0]`) to their corresponding hue, saturation,
831 and value (HSV) coordinates.
833 Args:
834 r (numpy.ndarray): Intensities for red (`[0., 1.]`).
835 g (numpy.ndarray): Intensities for green (`[0., 1.]`).
836 b (numpy.ndarray): Intensities for blue (`[0., 1.]`).
838 Returns:
839 list: Returns a list of `numpy.ndarray`s with the corresponding
840 coordinates in the HSV color space (`[h, s, v]`). Same length as
841 the inputs.
842 """
844 __fname__ = inspect.stack()[0][3] # Name of this method
846 # Checking input
847 self._check_input_arrays_(__fname__, r = r, g = g, b = b)
849 # Support function
850 def gethsv(r, g, b):
851 x = np.min([r, g, b])
852 y = np.max([r, g, b])
853 if y != x:
854 f = g - b if r == x else b - r if g == x else r - g
855 i = 3. if r == x else 5. if g == x else 1.
856 h = 60. * (i - f /(y - x))
857 s = (y - x)/y
858 v = y
859 else:
860 h = 0.
861 s = 0.
862 v = y
863 return [h, s, v]
865 # Result arrays
866 h = np.ndarray(len(r), dtype = "float"); h[:] = 0.
867 s = np.ndarray(len(r), dtype = "float"); s[:] = 0.
868 v = np.ndarray(len(r), dtype = "float"); v[:] = 0.
870 # Calculate h/s/v
871 for i in range(0, len(r)):
872 tmp = gethsv(r[i], g[i], b[i])
873 h[i] = tmp[0]; s[i] = tmp[1]; v[i] = tmp[2]
875 return [h, s, v]
878 def HSV_to_sRGB(self, h, s, v):
879 """Convert HSV to Standard RGB (sRGB)
881 Takes a series of HSV coordinates and converts them
882 to the sRGB color space.
884 Args:
885 h (nympy.ndarray): Hue values.
886 s (numpy.ndarray): Saturation.
887 v (numpy.ndarray): Value (the value-dimension of HSV).
889 Returns:
890 list: Returns a list of `numpy.ndarray`s with the corresponding
891 coordinates in the sRGB color space (`[r, g, b]`). Same length as
892 the inputs.
893 """
895 __fname__ = inspect.stack()[0][3] # Name of this method
897 # Checking input
898 self._check_input_arrays_(__fname__, h = h, s = s, v = v)
900 # Support function
901 def getrgb(h, s, v):
903 # If Hue is not defined:
904 if h == np.nan: return np.repeat(v, 3)
906 # Convert to [0-6]
907 h = h / 60.
908 i = np.floor(h)
909 f = h - i
911 if (i % 2) == 0: # if i is even
912 f = 1 - f
914 m = v * (1 - s)
915 n = v * (1 - s * f)
916 if i in [0, 6]: return [v, n, m]
917 elif i == 1: return [n, v, m]
918 elif i == 2: return [m, v, n]
919 elif i == 3: return [m, n, v]
920 elif i == 4: return [n, m, v]
921 elif i == 5: return [v, m, n]
922 else:
923 raise Exception(f"ended up in a non-defined ifelse with i = {i:d}")
925 # Result arrays
926 r = np.ndarray(len(h), dtype = "float"); r[:] = 0.
927 g = np.ndarray(len(h), dtype = "float"); g[:] = 0.
928 b = np.ndarray(len(h), dtype = "float"); b[:] = 0.
930 for i in range(0,len(h)):
931 tmp = getrgb(h[i], s[i], v[i])
932 r[i] = tmp[0]; g[i] = tmp[1]; b[i] = tmp[2]
934 return [r, g, b]
937 # -------------------------------------------------------------------
938 # -------------------------------------------------------------------
939 # -------------------------------------------------------------------
940 # -------------------------------------------------------------------
941 def sRGB_to_HLS(self, r, g, b):
942 """Convert Standard RGB (sRGB) to HLS
944 All r/g/b values in `[0., 1.]`, h in `[0., 360.]`, l and s in `[0., 1.]`.
945 From: <http://wiki.beyondunreal.com/wiki/RGB_To_HLS_Conversion>.
947 Args:
948 r (numpy.ndarray): Intensities for red (`[0., 1.]`)
949 g (numpy.ndarray): Intensities for green (`[0., 1.]`)
950 b (numpy.ndarray): Intensities for blue (`[0., 1.]`)
952 Returns:
953 list: Returns a list of `numpy.ndarray`s with the corresponding
954 coordinates in the HLS color space (`[h, l, s]`). Same length as
955 the inputs.
956 """
958 __fname__ = inspect.stack()[0][3] # Name of this method
960 # Checking input
961 self._check_input_arrays_(__fname__, r = r, g = g, b = b)
963 # Support function
964 def gethls(r, g, b):
965 min = np.min([r, g, b])
966 max = np.max([r, g, b])
968 l = (max + min)/2.;
970 if max != min:
971 if l < 0.5: s = (max - min) / (max + min)
972 elif l >= 0.5: s = (max - min) / (2. - max - min)
974 if r == max: h = (g - b) / (max - min);
975 if g == max: h = 2. + (b - r) / (max - min);
976 if b == max: h = 4. + (r - g) / (max - min);
978 h = h * 60.;
979 if h < 0.: h = h + 360.;
980 if h > 360.: h = h - 360.;
981 else:
982 s = 0
983 h = 0;
985 return [h, l, s]
987 # Result arrays
988 h = np.ndarray(len(r), dtype = "float"); h[:] = 0.
989 l = np.ndarray(len(r), dtype = "float"); l[:] = 0.
990 s = np.ndarray(len(r), dtype = "float"); s[:] = 0.
992 for i in range(0,len(h)):
993 tmp = gethls(r[i], g[i], b[i])
994 h[i] = tmp[0]; l[i] = tmp[1]; s[i] = tmp[2]
996 return [h, l, s]
999 def HLS_to_sRGB(self, h, l, s):
1000 """Convert HLC to Standard RGB (sRGB)
1002 All r/g/b values in `[0., 1.]`, h in `[0., 360.]`, l and s in `[0., 1.]`.
1004 Args:
1005 h (numpy.ndarray): Hue values.
1006 l (numpy.ndarray): Lightness.
1007 s (numpy.ndarray): Saturation.
1009 Returns:
1010 list: Returns a list of `numpy.ndarray`s with the corresponding
1011 coordinates in the sRGB color space (`[r, g, b]`). Same length as
1012 the inputs.
1013 """
1015 __fname__ = inspect.stack()[0][3] # Name of this method
1017 # Checking input
1018 self._check_input_arrays_(__fname__, h = h, l = l, s = s)
1020 # Support function
1021 def getrgb(h, l, s):
1022 p2 = l * (1. + s) if l <= 0.5 else l + s - (l * s)
1023 p1 = 2 * l - p2
1025 # If saturation is zero
1026 if (s == 0): return np.repeat(l, 3)
1027 # Else
1028 return [self._qtrans(p1, p2, h + 120.), # r
1029 self._qtrans(p1, p2, h), # g
1030 self._qtrans(p1, p2, h - 120.)] # b
1032 # Result arrays
1033 r = np.ndarray(len(h), dtype = "float"); r[:] = 0.
1034 g = np.ndarray(len(h), dtype = "float"); g[:] = 0.
1035 b = np.ndarray(len(h), dtype = "float"); b[:] = 0.
1037 for i in range(0,len(r)):
1038 tmp = getrgb(h[i], l[i], s[i])
1039 r[i] = tmp[0]; g[i] = tmp[1]; b[i] = tmp[2]
1041 return [r, g, b]
1044 # -------------------------------------------------------------------
1045 # -------------------------------------------------------------------
1046 # -------------------------------------------------------------------
1047 # -------------------------------------------------------------------
1048 def XYZ_to_uv(self, X, Y, Z):
1049 """Convert CIEXYZ to u and v
1051 Converting one (or multiple) colors defined by their X, Y, and Z
1052 coordinates in the CIEXYZ color space to their corresponding
1053 u and v coordinates.
1055 Args:
1056 X (numpy.ndarray): Values for the `Z` dimension.
1057 Y (numpy.ndarray): Values for the `Y` dimension.
1058 Z (numpy.ndarray): Values for the `Z` dimension.
1060 Returns:
1061 list: Returns a list of `numpy.ndarray`s (`[u, v]`).
1062 """
1064 __fname__ = inspect.stack()[0][3] # Name of this method
1066 # Checking input
1067 self._check_input_arrays_(__fname__, X = X, Y = Y, Z = Z)
1069 # Result array
1070 x = np.ndarray(len(X), dtype = "float"); x[:] = 0.
1071 y = np.ndarray(len(X), dtype = "float"); y[:] = 0.
1073 t = X + Y + Z
1074 idx = np.where(t != 0)
1075 x[idx] = X[idx] / t[idx];
1076 y[idx] = Y[idx] / t[idx];
1078 return [2.0 * x / (6. * y - x + 1.5), # u
1079 4.5 * y / (6. * y - x + 1.5)] # v
1081 def XYZ_to_LUV(self, X, Y, Z, XN = None, YN = None, ZN = None):
1082 """Convert CIEXYZ to CIELUV.
1084 `X`, `Y`, and `Z` specify the values in the three coordinates of the
1085 CIELAB color space,
1086 `XN`, `YN`, and `ZN` allow to specify additional CIE chromaticities to
1087 specify a specific white point.
1089 Args:
1090 X (numpy.ndarray): Values for the `X` dimension.
1091 Y (numpy.ndarray): Values for the `Y` dimension.
1092 Z (numpy.ndarray): Values for the `Z` dimension.
1093 XN (None, numpy.ndarray): Chromaticity of the white point. If of
1094 length `1`, the white point specification will be recycled if needed.
1095 When not specified (all `None`) a default white point is used.
1096 YN: See `XN`.
1097 ZN: See `XN`.
1099 Returns:
1100 list: Returns corresponding coordinates of CIE chromaticities as
1101 a list of `numpy.ndarray`s of the same length as the inputs (`[L, U, V]`).
1102 """
1104 __fname__ = inspect.stack()[0][3] # Name of this method
1105 n = len(X) # Number of colors
1107 # Loading definition of white
1108 [XN, YN, ZN] = self._get_white_(__fname__, n, XN, YN, ZN)
1110 # Checking input
1111 self._check_input_arrays_(__fname__, X = X, Y = Y, Z = Z)
1113 # Convert X/Y/Z and XN/YN/ZN to uv
1114 [u, v] = self.XYZ_to_uv(X, Y, Z )
1115 [uN, vN] = self.XYZ_to_uv(XN, YN, ZN)
1117 # Calculate L
1118 L = np.ndarray(len(X), dtype = "float"); L[:] = 0.
1119 y = Y / YN
1120 for i,val in np.ndenumerate(y):
1121 L[i] = 116. * np.power(val, 1./3.) - 16. if val > self._EPSILON else self._KAPPA * val
1123 # Calculate U/V
1124 return [L, 13. * L * (u - uN), 13. * L * (v - vN)] # [L, U, V]
1126 def LUV_to_XYZ(self, L, U, V, XN = None, YN = None, ZN = None):
1127 """Convert CIELUV to CIELAB
1129 `L`, `U`, and `V` specify the values in the three coordinates of the
1130 CIELAB color space,
1131 `XN`, `YN`, and `ZN` allow to specify additional CIE chromaticities to
1132 specify a specific white point.
1134 Args:
1135 L (numpy.ndarray): Values for the `L` dimension.
1136 U (numpy.ndarray): Values for the `U` dimension.
1137 V (numpy.ndarray): Values for the `V` dimension.
1138 XN (None, numpy.ndarray): Chromaticity of the white point. If of
1139 length `1`, the white point specification will be recycled if needed.
1140 When not specified (all `None`) a default white point is used.
1141 YN: See `XN`.
1142 ZN: See `XN`.
1144 Returns:
1145 list: Returns corresponding coordinates of CIE chromaticities as
1146 a list of `numpy.ndarray`s of the same length as the inputs (`[L, A, B]`).
1147 """
1149 __fname__ = inspect.stack()[0][3] # Name of this method
1150 n = len(L) # Number of colors
1152 # Loading definition of white
1153 [XN, YN, ZN] = self._get_white_(__fname__, n, XN, YN, ZN)
1155 # Checking input
1156 self._check_input_arrays_(__fname__, L = L, U = U, V = V)
1158 # Result arrays
1159 X = np.ndarray(len(L), dtype = "float"); X[:] = 0.
1160 Y = np.ndarray(len(L), dtype = "float"); Y[:] = 0.
1161 Z = np.ndarray(len(L), dtype = "float"); Z[:] = 0.
1163 # Check for which values we do have to do the transformation
1164 def fun(L, U, V):
1165 return False if L <= 0. and U == 0. and V == 0. else True
1166 idx = np.where([fun(L[i], U[i], V[i]) for i in range(0, len(L))])[0]
1167 if len(idx) == 0: return [X, Y, Z]
1169 # Compute Y
1170 for i in idx:
1171 Y[i] = YN[i] * (np.power((L[i] + 16.)/116., 3.) if L[i] > 8. else L[i] / self._KAPPA)
1173 # Calculate X/Z
1174 from numpy import finfo, fmax
1176 # Avoiding division by zero
1177 eps = np.finfo(float).eps*10
1178 L = fmax(eps, L)
1180 [uN, vN] = self.XYZ_to_uv(XN, YN, ZN)
1181 u = U / (13. * L) + uN
1182 v = V / (13. * L) + vN
1183 X = 9.0 * Y * u / (4 * v)
1184 Z = -X / 3. - 5. * Y + 3. * Y / v
1186 return [X, Y, Z]
1189 ## ----- LUV <-> polarLUV ----- */
1190 def LUV_to_polarLUV(self, L, U, V):
1191 """Convert CIELUV to the polar representation (polarLUV; HCL)
1193 Converts colors from the CIELUV color space into its polar
1194 representation (`polarLUV`). The `polarLUV` color space
1195 is also known as the HCL (Hue-Chroma-Luminance) color space
1196 which this package uses frequently, e.g., when creating
1197 efficient color maps. Inverse function of :py:method:`polarLUV_to_LUV`.
1199 Args:
1200 L (numpy.ndarray): Values for the `L` dimension.
1201 U (numpy.ndarray): Values for the `U` dimension.
1202 V (numpy.ndarray): Values for the `V` dimension.
1204 Returns:
1205 list: Returns corresponding polar LUV chromaticities as a list of
1206 `numpy.ndarray`s of the same length as the inputs (`[L, A, B]`),
1207 also known as `[H, C, L]` coordinates.
1208 """
1210 __fname__ = inspect.stack()[0][3] # Name of this method
1212 self._check_input_arrays_(__fname__, L = L, U = U, V = V)
1214 # Calculate polarLUV coordinates
1215 C = np.sqrt(U * U + V * V)
1216 H = self._RAD2DEG(np.arctan2(V, U))
1217 for i,val in np.ndenumerate(H):
1218 while val > 360: val -= 360.
1219 while val < 0.: val += 360.
1220 H[i] = val
1222 return [L, C, H]
1224 def polarLUV_to_LUV(self, L, C, H):
1225 """Convert Polar CIELUV (HCL) to CIELUV
1227 Convert colors from the polar representation of the CIELUV color space,
1228 also known as HCL (Hue-Chroma-Luminance) color space, into CIELAB
1229 coordinates. Inverse function of :py:method:`LUV_to_polarLUV`.
1231 Args:
1232 L (numpy.ndarray): Values for the polar `L` dimension (Luminance).
1233 C (numpy.ndarray): Values for the polar `C` dimension (Chroma).
1234 H (numpy.ndarray): Values for the polar `H` dimension (Hue).
1236 Returns:
1237 list: Returns corresponding CIELAB chromaticities as a list of
1238 `numpy.ndarray`s of the same length as the inputs (`[L, U, V]`).
1239 """
1241 __fname__ = inspect.stack()[0][3] # Name of this method
1243 # Checking input
1244 self._check_input_arrays_(__fname__, L = L, C = C, H = H)
1246 H = self._DEG2RAD(H)
1247 return [L, C * np.cos(H), C * np.sin(H)] # [L, U, V]
1250 def sRGB_to_hex(self, r, g, b, fixup = True):
1251 """Convert Standard RGB (sRGB) to Hex Colors
1253 Converting one (or multiple) colors defined by their red, green, and
1254 blue coordinates from the Standard RGB color space to hex colors.
1256 Args:
1257 r (numpy.ndarray): Intensities for red (`[0., 1.,]`).
1258 g (numpy.ndarray): Intensities for green (`[0., 1.,]`).
1259 b (numpy.ndarray): Intensities for blue (`[0., 1.,]`).
1260 fixup (bool): Whether or not the `rgb` values should be corrected
1261 if they lie outside the defined RGB space (outside `[0., 1.,]`),
1262 defaults to `True`.
1264 Returns:
1265 list: A list with hex color str.
1266 """
1268 # Color fixup: limit r/g/b to [0-1]
1269 def rgbfixup(r, g, b):
1270 def fun(x):
1271 return np.asarray([np.max([0, np.min([1, e])]) \
1272 if np.isfinite(e) else np.nan for e in x])
1273 return [fun(r), fun(g), fun(b)]
1275 def rgbcleanup(r, g, b):
1276 def fun(x):
1277 tol = 1. / (2 * 255.)
1278 # Allow tiny correction close to 0. and 1.
1279 x[np.logical_and(x < 0.0, x >= -tol)] = 0.0
1280 x[np.logical_and(x > 1.0, x <= 1.0 + tol)] = 1.0
1281 return np.asarray([e if np.logical_and(e >= 0., e <= 1.)
1282 else np.nan for e in x])
1283 return [fun(r), fun(g), fun(b)]
1285 # Checking which r/g/b values are outside limits.
1286 # This only happens if fixup = FALSE.
1287 def validrgb(r, g, b):
1288 idxr = np.isfinite(r)
1289 idxg = np.isfinite(g)
1290 idxb = np.isfinite(b)
1291 return np.where(idxr * idxg * idxb)[0]
1293 # Support function to create hex coded colors
1294 def gethex(r, g, b):
1296 # Converts int to hex string
1297 def applyfun(x):
1298 x = np.asarray(x * 255. + .5, dtype = int)
1299 return f"#{x[0]:02X}{x[1]:02X}{x[2]:02X}"
1301 h = np.vstack([r,g,b]).transpose().flatten().reshape([len(r), 3])
1302 return np.apply_along_axis(applyfun, 1, h)
1304 # Let's do the conversion!
1305 if fixup: [r, g, b] = rgbfixup(r, g, b)
1306 else: [r, g, b] = rgbcleanup(r, g, b)
1308 # Create return array
1309 res = np.ndarray(len(r), dtype = "|S7"); res[:] = ""
1311 # Check valid r/g/b coordinates
1312 valid = validrgb(r, g, b)
1313 if len(valid) > 0:
1314 # Convert valid colors to hex
1315 res[valid] = gethex(r[valid], g[valid], b[valid])
1317 # Create return list with NAN's for invalid colors
1318 res = [None if len(x) == 0 else x.decode() for x in res]
1320 # Return numpy array
1321 return np.asarray(res)
1323 def hex_to_sRGB(self, hex_, gamma = 2.4):
1324 """Convert Hex Colors to Standard RGB (sRGB)
1326 Convert one (or multiple) hex colors to sRGB.
1328 Args:
1329 hex_ (str, list of str): hex color str or list of str.
1330 gamma (float): Gamma correction factor, defaults to `2.4`.
1332 Returns:
1333 list: Returns a list of `numpy.ndarray`s with the corresponding
1334 red, green, and blue intensities (`[r, g, b]`), all in `[0., 1.]`.
1335 """
1337 if isinstance(hex_,str): hex_ = [hex_]
1338 hex_ = np.asarray(hex_)
1340 # Check for valid hex colors
1341 def validhex(hex_):
1342 from re import compile
1343 pat = compile("^#[0-9A-Fa-f]{6}([0-9]{2})?$")
1344 from re import match
1345 return np.where([None if x is None else pat.match(x) is not None for x in hex_])[0]
1347 # Convert hex to rgb
1348 def getrgb(x):
1349 def applyfun(x):
1350 return np.asarray([int(x[i:i+2], 16) for i in (1, 3, 5)])
1351 rgb = [applyfun(e) for e in x]
1352 rgb = np.vstack(rgb).transpose().flatten().reshape([3,len(x)])
1353 return [rgb[0] / 255., rgb[1] / 255., rgb[2] / 255.]
1355 # Result arrays
1356 r = np.ndarray(len(hex_), dtype = "float"); r[:] = np.nan
1357 g = np.ndarray(len(hex_), dtype = "float"); g[:] = np.nan
1358 b = np.ndarray(len(hex_), dtype = "float"); b[:] = np.nan
1360 # Check valid hex colors
1361 valid = validhex(hex_)
1362 if not len(valid) == 0:
1363 # Decode valid hex strings
1364 rgb = getrgb(hex_[valid])
1365 r[valid] = rgb[0]
1366 g[valid] = rgb[1]
1367 b[valid] = rgb[2]
1369 return [r, g, b]
1372 # -------------------------------------------------------------------
1373 # Direct conversion ('shortcut') from RGB to HLS
1374 def RGB_to_HLS(self, r, g, b):
1375 """Convert RGB to HLS
1377 Shortcut from RGB to HLS (not via sRGB).
1378 All r/g/b values in `[0., 1.]`, h in `[0., 360.]`, l and s in `[0., 1.]`.
1380 Args:
1381 r (numpy.ndarray): Intensities for red (`[0., 1.]`)
1382 g (numpy.ndarray): Intensities for green (`[0., 1.]`)
1383 b (numpy.ndarray): Intensities for blue (`[0., 1.]`)
1385 Returns:
1386 list: Returns a list of `numpy.ndarray`s with the corresponding
1387 coordinates in the HLS color space (`[h, l, s]`). Same length as
1388 the inputs.
1389 """
1391 __fname__ = inspect.stack()[0][3] # Name of this method
1393 # Checking input
1394 self._check_input_arrays_(__fname__, r = r, g = g, b = b)
1396 # Create 2d numpy array where the first dimension corresponds
1397 # to specific colors, the second one to [r, g, b] of that color.
1398 tmp = np.transpose(np.stack((r, g, b)))
1400 def gethls(x):
1401 """x is expected to be a numpy array of length 3 with [r, g, b] coordinates."""
1403 mn = np.min(x)
1404 mx = np.max(x)
1406 # If minimum equals maximum we know the solution already
1407 if mn == mx: return [0., mn, 0.] # [h, l, s]
1409 # Else do the calculations
1410 l = (mn + mx) / 2.
1411 s = (mx - mn) / (mx + mn) if l < 0.5 else (mx - mn) / (2. - mx - mn)
1413 # x[0] is 'r', x[1] = 'g', x[2] = 'b'
1414 if x[0] == mx: h = 60. * (x[1] - x[2]) / (mx - mn)
1415 elif x[1] == mx: h = 60. * (2. + (x[2] - x[0]) / (mx - mn))
1416 else: h = 60. * (4. + (x[0] - x[1]) / (mx - mn))
1418 if h < 0.: h = h + 360.
1419 elif h > 360.: h = h - 360.
1421 return [h, l, s]
1424 return np.transpose([gethls(x) for x in tmp])
1427 # -------------------------------------------------------------------
1428 # Direct conversion ('shortcut') from HLS to RGB
1429 def HLS_to_RGB(self, h, l, s):
1430 """Convert HLS to RGB
1432 Shortcut from HLS to RGB (not via sRGB). Expecting h in `[0., 360.]`,
1433 l/s in `[0., 1.]`. Returns r/g/b in `[0.,1.]`.
1435 Args:
1436 h (numpy.ndarray): Hue (`[0., 360.]`)
1437 l (numpy.ndarray): Luminance (`[0., 1.]`)
1438 s (numpy.ndarray): Saturation (`[0., 1.]`)
1440 Returns:
1441 list: Returns a list of `numpy.ndarray`s with the corresponding
1442 coordinates in the RGB color space (`[r, g, b]`). Same length as
1443 the inputs.
1444 """
1446 __fname__ = inspect.stack()[0][3] # Name of this method
1448 # Checking input
1449 self._check_input_arrays_(__fname__, h = h, l = l, s = s)
1451 # Create 2d numpy array where the first dimension corresponds
1452 # to specific colors, the second one to [r, g, b] of that color.
1453 tmp = np.transpose(np.stack((h, l, s)))
1455 def getrgb(x):
1456 """x is expected to be a numpy array of length 3 with [h, l, s] coordinates."""
1458 # If saturation equals zero, return [l, l, l]
1459 if x[2] == 0.: return [x[1], x[1], x[1]]
1461 # x[0] = 'h', x[1] = 'l', x[2] = 's'
1462 p2 = x[1] * (1 + x[2]) if x[1] <= 0.5 else x[1] + x[2] - (x[1] * x[2])
1463 p1 = 2 * x[1] - p2
1465 return [self._qtrans(p1, p2, x[0] + 120.),
1466 self._qtrans(p1, p2, x[0]),
1467 self._qtrans(p1, p2, x[0] - 120.)]
1469 return np.transpose([getrgb(x) for x in tmp])
1471 # -------------------------------------------------------------------
1472 # Direct conversion ('shortcut') from RGB to HSV
1473 def RGB_to_HSV(self, r, g, b):
1474 """Convert RGB to HSV
1476 Shortcut from RGB to HSV (not via sRGB).
1477 All r/g/b values in `[0., 1.]`, h in `[0., 360.]`, l and s in `[0., 1.]`.
1479 Args:
1480 r (numpy.ndarray): Intensities for red (`[0., 1.]`)
1481 g (numpy.ndarray): Intensities for green (`[0., 1.]`)
1482 b (numpy.ndarray): Intensities for blue (`[0., 1.]`)
1484 Returns:
1485 list: Returns a list of `numpy.ndarray`s with the corresponding
1486 coordinates in the HSV color space (`[h, s, v]`). Same length as
1487 the inputs.
1488 """
1490 __fname__ = inspect.stack()[0][3] # Name of this method
1492 # Checking input
1493 self._check_input_arrays_(__fname__, r = r, g = g, b = b)
1495 # Create 2d numpy array where the first dimension corresponds
1496 # to specific colors, the second one to [r, g, b] of that color.
1497 tmp = np.transpose(np.stack((r, g, b)))
1499 def gethsv(x):
1500 """x is expected to be a numpy array of length 3 with [r, g, b] coordinates."""
1502 mn = np.min(x)
1503 mx = np.max(x)
1505 # If minimum equals maximum we know the solution already
1506 if mn == mx: return [0., 0., mx] # [h, s, v]
1508 # Else calculate new dimensions
1509 f = (x[1] - x[2]) if x[0] == mn else x[2] - x[0] if x[1] == mn else x[0] - x[1]
1510 i = 3. if x[0] == mn else 5. if x[1] == mn else 1.
1512 # Returning [h, s, v]
1513 return [60. * (i - f / (mx - mn)), (mx - mn) / mx, mx]
1515 return np.transpose([gethsv(x) for x in tmp])
1518 # -------------------------------------------------------------------
1519 # Direct conversion ('shortcut') from HSV to RGB
1520 def HSV_to_RGB(self, h, s, v):
1521 """Convert HSV to RGB
1523 Shortcut from HLS to RGB (not via sRGB). Expecting h in `[0., 360.]`,
1524 l/s in `[0., 1.]`. Returns r/g/b in `[0.,1.]`.
1526 Args:
1527 h (numpy.ndarray): Hue (`[0., 360.]`)
1528 s (numpy.ndarray): Saturation (`[0., 1.]`)
1529 v (numpy.ndarray): Value (`[0., 1.]`)
1531 Returns:
1532 list: Returns a list of `numpy.ndarray`s with the corresponding
1533 coordinates in the RGB color space (`[r, g, b]`). Same length as
1534 the inputs.
1535 """
1537 __fname__ = inspect.stack()[0][3] # Name of this method
1539 # Checking input
1540 self._check_input_arrays_(__fname__, h = h, s = s, v = v)
1542 # Create 2d numpy array where the first dimension corresponds
1543 # to specific colors, the second one to [r, g, b] of that color.
1544 tmp = np.transpose(np.stack((h, s, v)))
1546 def getrgb(x):
1547 """x is expected to be a numpy array of length 3 with [h, s, v] coordinates."""
1549 h = x[0] / 60. # Convert to [0, 6]
1550 i = np.int8(np.floor(h))
1551 f = h - i
1552 if i % 2 == 0: f = 1. - f # if i is even
1554 m = x[2] * (1. - x[1])
1555 n = x[2] * (1. - x[1] * f)
1557 if i == 0 or i == 6: return [x[2], n, m]
1558 elif i == 1: return [n, x[2], m]
1559 elif i == 2: return [m, x[2], n]
1560 elif i == 3: return [m, n, x[2]]
1561 elif i == 4: return [n, m, x[2]]
1562 elif i == 5: return [x[2], m, n]
1564 return np.transpose([getrgb(x) for x in tmp])
1567# -------------------------------------------------------------------
1568# Color object base class
1569# will be extended by the different color classes.
1570# -------------------------------------------------------------------
1571class colorobject:
1572 """Superclass for All Color Objects
1574 A series of constructors are available to construct `colorobjects` in a
1575 variety of different color spaces, all inheriting from this class. This
1576 superclass provides the general functionality to handle colors (sets of
1577 colors) and convert colors from and to different color spaces.
1579 Users should use the dedicated classes for the available color spaces which
1580 all extend this class. These are: CIELAB, CIELUV, CIEXYZ, hexcols, HLS,
1581 HSV, polarLAB, polarLUV, RGB, and sRGB.
1582 """
1584 import numpy as np
1586 # Allowed/defined color spaces
1587 ALLOWED = ["CIEXYZ", "CIELUV", "CIELAB", "polarLUV", "polarLAB",
1588 "RGB", "sRGB", "HCL", "HSV", "HLS", "hex"]
1589 """List of allowed/defined color spaces; used to check when converting
1590 colors from one color space to another."""
1592 # Used to store alpha if needed. Will only be used for some of
1593 # the colorobject objects as only few color spaces allow alpha
1594 # values.
1595 ALPHA = None
1596 """Used to store (keep) transparency when needed; will be dropped during conversion."""
1598 GAMMA = 2.4 # Used to adjust RGB (sRGB_to_RGB and back).
1599 """Gamma value used used to adjust RGB colors; currently a fixed value of 2.4."""
1601 # Standard representation of colorobject objects.
1602 def __repr__(self, digits = 2):
1603 """Color Object Standard Representation
1605 Standard representation of the color object; shows the values
1606 of all coordinates (or the hex strings if hex colors).
1608 Args:
1609 digits (int): Number of digits, defaults to `2`.
1611 Returns:
1612 str: Returns a str of the colors/coordinates of the current object.
1613 """
1614 dims = list(self._data_.keys()) # Dimensions
1616 from .colorlib import hexcols
1617 import numpy as np
1619 # Sorting the dimensions
1620 from re import match
1621 if match("^(hex_|alpha){1,2}$", "".join(dims)): dims = ["hex_"]
1622 elif match("^(R|G|B|alpha){3,4}$", "".join(dims)): dims = ["R", "G", "B"]
1623 elif match("^(L|A|B|alpha){3,4}$", "".join(dims)): dims = ["L", "A", "B"]
1624 elif match("^(L|U|V|alpha){3,4}$", "".join(dims)): dims = ["L", "U", "V"]
1625 elif match("^(H|C|L|alpha){3,4}$", "".join(dims)): dims = ["H", "C", "L"]
1626 elif match("^(X|Y|Z|alpha){3,4}$", "".join(dims)): dims = ["X", "Y", "Z"]
1627 elif match("^(H|S|V|alpha){3,4}$", "".join(dims)): dims = ["H", "S", "V"]
1628 elif match("^(H|L|S|alpha){3,4}$", "".join(dims)): dims = ["H", "L", "S"]
1630 # Number of colors
1631 ncol = max([0 if self._data_[x] is None else len(self._data_[x]) for x in dims])
1633 # Add 'alpha' to object 'dims' if we have defined alpha values
1634 # for this colorobject. Else alpha will not be printed.
1635 if "alpha" in list(self._data_.keys()):
1636 if self._data_["alpha"] is not None: dims += ["alpha"]
1638 # Start creating the string:
1639 res = ["{:s} color object ({:d} colors)".format(self.__class__.__name__, ncol)]
1641 # Show header
1642 fmt = "".join(["{:>", "{:d}".format(digits + 6), "s}"])
1643 res.append(" " + "".join([fmt.format(x) for x in dims]))
1645 # Show data
1646 # In case of a hexcols object: string formatting and
1647 # nan-replacement beforehand.
1648 if isinstance(self, hexcols):
1649 data = {}
1650 fmt = "".join(["{:", "{:d}.{:d}".format(6 + digits, 3), "f}"])
1651 data["hex_"] = np.ndarray(ncol, dtype = "|S7")
1652 for n in range(0, ncol):
1653 x = self._data_["hex_"][n]
1654 if x is None:
1655 data["hex_"][n] = None
1656 else:
1657 data["hex_"][n] = fmt.format(x) if isinstance(x, float) else x[0:7]
1658 data["alpha"] = self.get("alpha")
1659 fmt = "{:<10s}"
1660 else:
1661 fmt = "".join(["{:", "{:d}.{:d}".format(6+digits, digits), "f}"])
1662 data = self._data_
1664 # Print object content
1665 count = 0
1666 for n in range(0, ncol):
1667 if (n % 10) == 0:
1668 tmp = "{:3d}: ".format(n+1)
1669 else:
1670 tmp = " "
1671 for d in dims:
1672 # Special handling for alpha
1673 if d == "alpha":
1674 if data[d][n] is None:
1675 tmp += " ---"
1676 elif isinstance(data[d][n], float):
1677 if np.isnan(data[d][n]):
1678 tmp += " ---"
1679 elif isinstance(self, hexcols):
1680 tmp += " {:02X}".format(int(255. * data[d][n]))
1681 else:
1682 tmp += " {:4.2f}".format(data[d][n])
1683 else:
1684 if data[d] is None or data[d][n] is None:
1685 tmp += " ---"
1686 elif isinstance(data[d][n], str) or isinstance(data[d][n], np.bytes_):
1687 tmp += fmt.format(data[d][n])
1688 else:
1689 tmp += fmt.format(float(data[d][n]))
1691 count += 1
1692 res.append(tmp)
1694 if count >= 30 and ncol > 40:
1695 res.append("".join([" ......"]*len(dims)))
1696 res.append("And {:d} more [truncated]".format(ncol - count))
1697 break
1699 return "\n".join(res)
1701 def __call__(self, fixup = True, rev = False):
1702 """Magic Method
1704 Default call method of all color objects. Always returns
1705 hex colors, same as the `.colors()` method does.
1707 Args:
1708 fixup (bool): Fix colors outside defined color space, defaults to `True`.
1709 rev (bool): Revert colors, defaults to `False`.
1711 Returns:
1712 list: Returns a list of hex colors.
1713 """
1714 return self.colors(fixup = fixup, rev = rev)
1716 def __iter__(self):
1717 self.n = -1
1718 return self
1720 def __next__(self):
1721 if self.n < (self.length() - 1):
1722 self.n += 1
1723 res = self[self.n]
1724 return res
1725 else:
1726 raise StopIteration
1728 def __getitem__(self, key):
1729 if not isinstance(key, int):
1730 raise TypeError("argument `key` must be int (index)")
1732 from copy import deepcopy
1733 from numpy import array, newaxis
1734 res = deepcopy(self)
1735 for n in list(res._data_.keys()):
1736 # If None: keep it as it is, else subset
1737 if res._data_[n] is None: continue
1738 res._data_[n] = res._data_[n][newaxis, key]
1740 return res
1743 def get_whitepoint(self):
1744 """Get White Point
1746 This method returns the definition of the white point in use. If not
1747 explicitly set via the :py:method:`set_whitepoint` method, a default white
1748 point is used.
1750 Returns:
1751 dict: Returns a dict with `X`, `Y`, `Z`, the white point specification
1752 for the three dimensions.
1754 Example:
1756 >>> from colorspace import hexcols
1757 >>> c = hexcols("#ff0000")
1758 >>> c.get_whitepoint()
1759 """
1760 return {"X": self.WHITEX, "Y": self.WHITEY, "Z": self.WHITEZ}
1762 def set_whitepoint(self, **kwargs):
1763 """Set White Point
1765 A white point definition is used to adjust the colors.
1766 This method allows to set custom values. If not explicitly
1767 set a default specification is used. The :py:method:`get_whitepoint`
1768 method can be used to extract the currently used definition.
1770 Args:
1771 **kwargs: Named arguments. Allowed are `X`, `Y`, and `Z`,
1772 each of which must be float: White specification for
1773 dimension `X`/`Y`/`Z`.
1775 Example:
1777 >>> from colorspace import hexcols
1778 >>> c = hexcols("#ff0000")
1779 >>> c.set_whitepoint(X = 100., Y = 100., Z = 101.)
1780 >>> c.get_whitepoint()
1782 Raises:
1783 ValueError: If named argument is not one of `X`, `Y`, `Z`.
1784 """
1785 for key,arg in kwargs.items():
1786 if key == "X": self.WHITEX = float(arg)
1787 elif key == "Y": self.WHITEY = float(arg)
1788 elif key == "Z": self.WHITEZ = float(arg)
1789 else:
1790 raise ValueError(f"error in .set_whitepoint: " + \
1791 "argument \"{key}\" not recognized.")
1794 def _check_if_allowed_(self, x):
1795 """Check for Valid Transformation
1797 Helper function checking if the transformation of the current
1798 object into another color space is allowed or not.
1799 An exception will be thrown if the transformation is not possible.
1801 Args:
1802 x (str): Name of the target color space.
1804 Returns:
1805 No return, raises an Exception if the transformation is invalid.
1806 """
1807 if not x in self.ALLOWED:
1808 raise Exception(f"transformation from {self.__class__.__name__}" + \
1809 f" to \"{x}\" is unknown (not implemented). " + \
1810 f"The following are allowed: {', '.join(self.ALLOWED)}")
1811 return
1814 def _transform_via_path_(self, via, fixup):
1815 """Transform Colors along Path
1817 Helper function to transform a colorobject into a new color
1818 space. Calls the :py:func:`to` method one or multiple times along 'a path'
1819 as specified by `via`.
1821 Returns:
1822 No return, converts the current color space object (see method :py:func:`to`).
1824 Args:
1825 via (list of str): The path via which the current color object
1826 should be transformed. For example: A :py:class:`hexcols`
1827 object can be transformed into CIEXYZ by specifying
1828 `via = ["sRGB", "RGB", "CIEXYZ"]`.
1829 fixup (bool): Whether or not to correct invalid rgb values outside
1830 `[0., 1.]` if necessary
1831 """
1832 for v in via: self.to(v, fixup = fixup)
1834 def _colorobject_check_input_arrays_(self, **kwargs):
1835 """Colorobject Check User Input
1837 Checks if all inputs in `**kwargs` are of type `numpy.ndarray` OR lists
1838 (will be converted to `numpy.ndarray`s) and that all are of the same length.
1839 If not, the script will throw an exception.
1841 If `alpha` is given it is handled in a special way. If `alpha = None`
1842 it will simply be dropped (no alpha channel specified), else it is
1843 handled like the rest and has to fulfill the requirements all the
1844 other dimensions have to (length and type).
1846 Args:
1847 **kwargs: Named keywords, objects to be checked.
1849 Returns:
1850 bool: Returns `True` if all checks where fine, throws an exception
1851 if the inputs do not fulfil the requirements.
1852 """
1854 from numpy import asarray, float64
1856 # Message will be dropped if problems occur
1857 msg = f"Problem while checking inputs \"{', '.join(kwargs.keys())}\" " + \
1858 f"to class \"{self.__class__.__name__}\"."
1860 res = {}
1861 lengths = []
1862 keys_to_check = []
1863 for key,val in kwargs.items():
1864 # No alpha provided, simply proceed
1865 if key == "alpha" and val is None: continue
1867 keys_to_check.append(key)
1869 # If is list: convert to ndarray no matter how long the element is
1870 if isinstance(val, float) or isinstance(val, int):
1871 val = np.asarray([val])
1872 elif isinstance(val,list):
1873 try:
1874 val = np.asarray(val)
1875 except Exception as e:
1876 raise Exception(e)
1879 # For alpha, R, G, and B: check range
1880 if isinstance(self, RGB) or isinstance(self, sRGB):
1881 if np.max(val) > 1. or np.max(val) < 0.:
1882 raise ValueError("wrong values specified for " + \
1883 f"dimension {key} in {self.__class__.__name__}: " + \
1884 "values have to lie within [0., 1.]")
1886 # Check object type
1887 from numpy import asarray
1888 try:
1889 val = asarray(val)
1890 except Exception as e:
1891 raise ValueError(f"input {key} to {self.__class__.__name__}" + \
1892 f" could not have been converted to `numpy.ndarray`: {str(e)}")
1894 # Else append length and proceed
1895 lengths.append(len(val))
1897 # Append to result vector
1898 if isinstance(val, int) or isinstance(val, float): val = [val]
1899 res[key] = val if key == "hex_" else asarray(val, float64)
1901 # Check if all do have the same length
1902 if not np.all([x == lengths[0] for x in lengths]):
1903 msg += " Arguments of different lengths: {:s}".format(
1904 ", ".join(["{:s} = {:d}".format(keys_to_check[i], lengths[i]) \
1905 for i in range(0, len(keys_to_check))]))
1906 raise ValueError(msg)
1908 return res
1911 def hasalpha(self):
1912 """Check for Alpha Channel
1914 Helper method to check if the current color object has
1915 an alpha channel or not.
1917 Examples:
1919 >>> from colorspace import sRGB
1920 >>> x1 = sRGB(R = 0.5, G = 0.1, B = 0.3)
1921 >>> x1
1922 >>> #:
1923 >>> x2 = sRGB(R = 0.5, G = 0.1, B = 0.3, alpha = 0.5)
1924 >>> x2
1925 >>> #: Checking both color objects for alpha channel
1926 >>> [x1.hasalpha(), x2.hasalpha()]
1928 Returns:
1929 bool: `True` if alpha values are present, `False` if not.
1930 """
1931 if not "alpha" in self._data_.keys():
1932 return False
1933 elif self._data_["alpha"] is None:
1934 return False
1935 else:
1936 return True
1939 def dropalpha(self):
1940 """Remove Alpha Channel
1942 Remove alpha channel from the color object, if defined
1943 (see :py:method:`hasalpha`). Works for all `colorobject`s.
1945 Examples:
1947 >>> from colorspace.colorlib import HCL, sRGB, HSV
1948 >>> # Example using HCL colors
1949 >>> cols = HCL([0, 40, 80],
1950 >>> [30, 60, 80],
1951 >>> [85, 60, 35],
1952 >>> alpha = [1.0, 0.5, 0.1])
1953 >>> cols # with alpha channel
1954 >>> #:
1955 >>> cols.dropalpha()
1956 >>> cols # alpha channel removed
1957 >>>
1958 >>> #: No effect if there is no alpha channel
1959 >>> cols.dropalpha()
1960 >>> cols
1961 >>>
1962 >>> #: Example using sRGB colors
1963 >>> cols = sRGB([0.01, 0.89, 0.56],
1964 >>> [0.25, 0.89, 0.02],
1965 >>> [0.65, 0.89, 0.23],
1966 >>> alpha = [1.0, 0.5, 0.1])
1967 >>> cols # with alpha channel
1968 >>> #:
1969 >>> cols.dropalpha()
1970 >>> cols # alpha channel removed
1971 >>>
1972 >>> #: Example using HSV colors
1973 >>> cols = HSV([218, 0, 336],
1974 >>> [1, 0, 1],
1975 >>> [0.65, 0.89, 0.56],
1976 >>> alpha = [1.0, 0.5, 0.1])
1977 >>> cols # with alpha channel
1978 >>> #:
1979 >>> cols.dropalpha()
1980 >>> cols # alpha channel removed
1983 """
1984 if self.hasalpha():
1985 del self._data_["alpha"]
1988 def specplot(self, **kwargs):
1989 """Color Spectrum Plot
1991 Visualization of the spectrum of this color object.
1992 Internally calls :py:func:`specplot <colorspace.specplot.specplot>`,
1993 additional arguments to this main function can be forwarded via the
1994 `**kwargs` argument.
1996 Args:
1997 **kwargs: Additional named arguments forwarded to
1998 :py:func:`specplot <colorspace.specplot.specplot>`.
2000 Return:
2001 Returns what :py:func:`colorspace.specplot.specplot` returns.
2003 Example:
2005 >>> # Example using HCL colors
2006 >>> from colorspace import HCL, hexcols
2007 >>> cols = HCL(H = [220, 196, 172, 148, 125],
2008 >>> C = [ 44, 49, 55, 59, 50],
2009 >>> L = [ 49, 61, 72, 82, 90])
2010 >>> cols.specplot(figsize = (8, 4));
2011 >>>
2012 >>> #: Example using hex colors
2013 >>> cols = hexcols(["#0FCFC0", "#9CDED6", "#D5EAE7",
2014 >>> "#F1F1F1", "#F3E1EB", "#F6C4E1", "#F79CD4"])
2015 >>> cols.specplot(rgb = True, hcl = True, palette = True)
2017 """
2018 from copy import copy
2019 cols = copy(self)
2020 cols.to("hex")
2022 from .specplot import specplot
2023 return specplot(cols.colors(), **kwargs)
2026 def swatchplot(self, **kwargs):
2027 """Palette Swatch Plot
2029 Visualization the color palette of this color object.
2030 Internally calls :py:func:`swatchplot <colorspace.swatchplot.swatchplot>`,
2031 additional arguments to this main function can be forwarded via the
2032 `**kwargs` argument.
2034 Args:
2035 **kwargs: Additional named arguments forwarded to
2036 :py:func:`swatchplot <colorspace.swatchplot.swatchplot>`.
2038 Return:
2039 Returns what :py:func:`colorspace.swatchplot.swatchplot` returns.
2041 Example:
2043 >>> # Example using HCL colors
2044 >>> from colorspace import HCL, hexcols
2045 >>> cols = HCL(H = [220, 196, 172, 148, 125],
2046 >>> C = [ 44, 49, 55, 59, 50],
2047 >>> L = [ 49, 61, 72, 82, 90])
2048 >>> cols.swatchplot(figsize = (8, 2))
2049 >>>
2050 >>> #: Example using hex colors
2051 >>> cols = hexcols(["#0FCFC0", "#9CDED6", "#D5EAE7",
2052 >>> "#F1F1F1", "#F3E1EB", "#F6C4E1", "#F79CD4"])
2053 >>> cols.swatchplot(figsize = (8, 3.5));
2054 """
2056 from .swatchplot import swatchplot
2057 if "show_names" in kwargs.keys():
2058 del kwargs["show_names"]
2059 return swatchplot(pals = self.colors(), show_names = False, **kwargs)
2062 def hclplot(self, **kwargs):
2063 """Palette Plot in HCL Space
2065 Convenience method for calling :py:func:`hclplot <colorspace.hclplot.hclplot>`
2066 on the current color object. Additional arguments can be forwarded via `**kwargs`
2067 (see :py:func:`hclplot <colorspace.hclplot.hclplot>` for details).
2069 Args:
2070 **kwargs: Additional named arguments forwarded to
2071 :py:func:`hclplot <colorspace.hclplot.hclplot>`.
2073 Return:
2074 Returns what :py:func:`colorspace.hclplot.hclplot` returns.
2076 Example:
2078 >>> # Example using HCL colors
2079 >>> from colorspace import HCL, hexcols
2080 >>> cols = HCL(H = [220, 196, 172, 148, 125],
2081 >>> C = [ 44, 49, 55, 59, 50],
2082 >>> L = [ 49, 61, 72, 82, 90])
2083 >>> cols.hclplot();
2084 >>>
2085 >>> #: Example using hex colors
2086 >>> cols = hexcols(["#0FCFC0", "#9CDED6", "#D5EAE7",
2087 >>> "#F1F1F1", "#F3E1EB", "#F6C4E1", "#F79CD4"])
2088 >>> cols.hclplot(figsize = (8, 3.5));
2089 """
2091 from .hclplot import hclplot
2092 return hclplot(x = self.colors(), **kwargs)
2094 def colors(self, fixup = True, rev = False):
2095 """Extract Hex Colors
2097 Convers the current object into an object of class :py:class:`hexcols`
2098 and extracts the hex colors as list of str.
2100 If the object contains alpha values, the alpha level is added to the
2101 hex string if and only if alpha is not equal to `1.0`.
2103 Args:
2104 fixup (bool): Whether or not to correct rgb values outside the
2105 defined range of `[0., 1.]`, defaults to `True`.
2106 rev (bool): Should the color palette be reversed? Defaults to `False`.
2108 Returns:
2109 list: Returns a list of hex color strings.
2111 Example:
2113 >>> from colorspace import HCL, sRGB, HSV
2114 >>> # Example using HCL colors
2115 >>> cols = HCL([0, 40, 80],
2116 >>> [30, 60, 80],
2117 >>> [85, 60, 35])
2118 >>> cols.colors()
2119 >>>
2120 >>> #: Example using sRGB colors
2121 >>> cols = sRGB([0.01, 0.89, 0.56],
2122 >>> [0.25, 0.89, 0.02],
2123 >>> [0.65, 0.89, 0.23])
2124 >>> cols.colors()
2125 >>>
2126 >>> #: Example using HSV colors
2127 >>> cols = HSV([218, 0, 336],
2128 >>> [1, 0, 1],
2129 >>> [0.65, 0.89, 0.56])
2130 >>> cols.colors()
2132 """
2134 from copy import copy
2135 from numpy import ndarray, round
2137 x = copy(self)
2138 x.to("hex", fixup = fixup)
2139 if x.hasalpha():
2140 res = x.get("hex_").tolist()
2141 # Appending alpha if alpha < 1.0
2142 for i in range(0, len(res)):
2143 if self._data_["alpha"][i] < 1.0:
2144 tmp = int(round(self._data_["alpha"][i] * 255. + 0.0001))
2145 res[i] += f"{tmp:02X}"
2146 # Return hex with alpha
2147 colors = res
2148 else:
2149 colors = x.get("hex_")
2151 if rev:
2152 from numpy import flip
2153 colors = flip(colors)
2155 return colors.tolist() if isinstance(colors, ndarray) else colors
2158 def get(self, dimname = None):
2159 """Extracting Color Coordinates
2161 Allows to extract the current values of one or multiple dimensions
2162 for all colors of this color object. The names of the coordinates varies
2163 between different color spaces.
2165 Args:
2166 dimname (None, str): If `None` (default) values of all coordinates
2167 of the current color object are returned. A specific coordinate
2168 can be specified if needed.
2170 Returns:
2171 Returns a `numpy.ndarray` if coordinates of one specific dimension are
2172 requested, else a `dict` of arrays.
2174 Example:
2176 >>> from colorspace import HCL, sRGB, hexcols
2177 >>> # Example using HCL color object with alpha channel
2178 >>> cols = HCL([260, 80, 30], [80, 0, 80], [30, 90, 30], [1, 0.6, 0.2])
2179 >>> cols.get("H") # Specific dimension
2180 >>> #:
2181 >>> cols.get("alpha") # Alpha (if existing)
2182 >>> #:
2183 >>> cols.get() # All dimensions
2184 >>>
2185 >>> #: Convert colors to sRGB
2186 >>> cols.to("sRGB")
2187 >>> cols.get("R") # Specific dimension
2188 >>> #:
2189 >>> cols.get() # All dimensions
2190 >>>
2191 >>> #: Convert to hexcols
2192 >>> cols.to("hex")
2193 >>> cols.get("hex_")
2195 Raises:
2196 TypeError: If argument `dimname` is neither None or str.
2197 ValueError: If the dimension specified on `dimnames` does not exist.
2198 """
2200 # Return all coordinates
2201 from copy import copy
2202 if dimname is None:
2203 return copy(self._data_)
2204 # No string?
2205 elif not isinstance(dimname, str):
2206 raise TypeError("argument `dimname` must be None or str")
2207 # Else only the requested dimension
2208 elif not dimname in self._data_.keys():
2209 # Alpha channel never defined, return None (which
2210 # is a valid value for "no alpha")
2211 if dimname == "alpha":
2212 return None
2213 else:
2214 raise ValueError(f"{self.__class__.__name__} has no dimension {dimname}")
2216 return copy(self._data_[dimname])
2219 def set(self, **kwargs):
2220 """Set Coordinates/Manipulate Colors
2222 Allows to manipulate current colors. The named input arguments
2223 have to fulfil a specific set or requirements. If not, the function
2224 raises exceptions. The requirements:
2226 * Dimension has to exist
2227 * New data/values must be of same length and type as the existing ones
2229 Args:
2230 **kwargs: Named arguments. The key is the name of the dimension to
2231 be changed, the value an object which fulfills the requirements
2232 (see description of this method)
2234 Raises:
2235 ValueError: If the dimension does not exist.
2236 ValueError: If the new data can't be converted into
2237 `numpy.array` (is done automatically if needed).
2238 ValueError: If new data has wrong length (does not match the
2239 number of colors/length of current values).
2241 Example:
2243 >>> # Example shown for HCL colors, works the same
2244 >>> # for all other color objects (sRGB, hexcols, ...)
2245 >>> from colorspace import HCL
2246 >>> cols = HCL([260, 80, 30], [80, 0, 80], [30, 90, 30])
2247 >>> cols
2248 >>> #:
2249 >>> cols.set(H = [150, 150, 30])
2250 >>> cols
2251 """
2252 # Looping over inputs
2253 from numpy import asarray, ndarray
2254 for key,vals in kwargs.items():
2255 key.upper()
2257 # Check if the key provided by the user is a valid dimension
2258 # of the current object.
2259 if not key in self._data_.keys():
2260 raise ValueError(f"{self.__class__.__name__} has no dimension {key}")
2262 # In case the input is a single int/float or a list; try
2263 # to convert the input into a numpy.array using the same
2264 # dtype as the existing dimension (loaded via self.get(key)).
2265 if isinstance(vals, (list, int, float)):
2266 if isinstance(vals, (int, float)): vals = [vals]
2267 t = type(self.get(key)[0]) # Current type (get current dimension)
2268 try:
2269 vals = np.asarray(vals, dtype = t)
2270 except Exception as e:
2271 raise ValueError(f"problems converting new data to {t} " + \
2272 f" in {self.__class__.__name__}: {str(e)}")
2274 # New values do have to have the same length as the old ones,
2275 n = len(self.get(key))
2276 t = type(self.get(key)[0])
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)}")
2282 if not vals.size == n:
2283 raise ValueError("number of values to be stored on the object " + \
2284 f"{self.__class__.__name__} have to match the current dimension")
2286 self._data_[key] = vals
2288 def length(self):
2289 """Get Number of Colors
2291 Returns the number of colors defined in this color object.
2292 Note that `len(<object>)` works as well.
2294 Returns:
2295 int: Number of colors.
2297 Examples:
2299 >>> from colorspace import sRGB, hexcols, HCL
2300 >>> # Examples for three different color objects
2301 >>> x1 = sRGB([1, 0], [1, 1], [0, 0])
2302 >>> [x1.length(), len(x1)]
2303 >>> #:
2304 >>> x2 = hexcols(["#ff0000", "#00ff00", "#0000ff"])
2305 >>> [x2.length(), len(x2)]
2306 >>> #:
2307 >>> x3 = HCL([275, 314, 353, 31, 70],
2308 >>> [70, 85, 102, 86, 45],
2309 >>> [25, 40, 55, 70, 85])
2310 >>> [x3.length(), len(x3)]
2312 """
2313 return max([0 if self._data_[x] is None else len(self._data_[x]) for x in self._data_.keys()])
2315 def __len__(self):
2316 return self.length()
2319 # Currently not used but implemented as fallback for the future
2320 def _cannot(self, from_, to):
2321 """Error: Conversion not Possible
2323 Helper function used to raise an exception as a specific
2324 transformation is not possible by definition.
2326 Args:
2327 from_ (str): Name of the current color space.
2328 to (str): Name of the target color space.
2330 Raises:
2331 Exception: Always, that is the intent of this method.
2332 """
2333 raise Exception(f"cannot convert class \"{from_}\" to \"{to}\"")
2335 def _ambiguous(self, from_, to):
2336 """Error: Conversion Ambiguous
2338 Helper function used to raise an exception as a specific
2339 transformation is ambiguous and therefore not possible by definition.
2341 Args:
2342 from_ (str): Name of the current color space.
2343 to (str): Name of the target color space.
2345 Raises:
2346 Exception: Always, that is the intent of this method.
2347 """
2348 raise Exception(f"conversion not possible, ambiguous conversion from \"{from_}\" to \"{to}\"")
2351# -------------------------------------------------------------------
2352# PolarLUV or HCL object
2353# -------------------------------------------------------------------
2354class polarLUV(colorobject):
2355 """Create polarLUV (HCL) Color Object
2357 Creates a color object in the polar representation of the :py:class:`CIELUV`
2358 color space, also known as the Hue-Chroma-Luminance (HCL) color space.
2359 Can be converted to: :py:class:`CIEXYZ`, :py:class:`CIELUV`,
2360 :py:class:`CIELAB`, :py:class:`RGB`, :py:class:`sRGB`,
2361 :py:class:`polarLAB`, and :py:class:`hexcols`.
2362 Not allowed (ambiguous) are transformations to :py:class:`HSV` and :py:class:`HLS`.
2364 Args:
2365 H (int, float, list, numpy.array):
2366 Numeric value(s) for hue dimension (`[-360., 360.]`).
2367 C (int, float, list, numpy.array):
2368 Numeric value(s) for chroma dimension (`[0., 100.+]`).
2369 L (int, float, list, numpy.array):
2370 Numeric value(s) for luminance dimension (`[0., 100.]`).
2371 alpha (None, float, list, numpy.array): Numeric value(s) for the alpha
2372 channel (`[0., 1.]`) where `0.` equals full transparency, `1.` full
2373 opacity. If `None` (default) no transparency is added.
2375 Example:
2377 >>> from colorspace import polarLUV, HCL
2378 >>> # Constructing color object with one single color via float
2379 >>> polarLUV(100., 30, 50.)
2380 >>> #: polarLUV is the HCL color space, this
2381 >>> # is equivalent to the command above.
2382 >>> HCL(100., 30, 50.)
2383 >>> #: Constructing object via lists
2384 >>> HCL([100, 80], [30, 50], [30, 80])
2385 >>> #: Constructing object via numpy arrays
2386 >>> from numpy import asarray
2387 >>> HCL(asarray([100, 80]), asarray([30, 50]), asarray([30, 80]))
2388 """
2390 def __init__(self, H, C, L, alpha = None):
2392 # Checking inputs, save inputs on object
2393 self._data_ = {} # Dict to store the colors/color dimensions
2394 tmp = self._colorobject_check_input_arrays_(H = H, C = C, L = L, alpha = alpha)
2395 for key,val in tmp.items(): self._data_[key] = val
2396 # White spot definition (the default)
2397 self.set_whitepoint(X = 95.047, Y = 100.000, Z = 108.883)
2399 def to(self, to, fixup = True):
2400 """Transform Color Space
2402 Allows to transform the current object into a different color space,
2403 if possible. Converting the colors of the current object into
2404 another color space. After calling this method, the object
2405 will be of a different class.
2407 Args:
2408 to (str): Name of the color space into which the colors should be
2409 converted (e.g., `"CIEXYZ"`, `"HCL"`, `"hex"`, `"sRGB"`, ...).
2410 fixup (bool): Whether or not colors outside the defined rgb color space
2411 should be corrected if necessary, defaults to `True`.
2413 Examples:
2415 >>> # HCL() identical to polarLUV()
2416 >>> from colorspace import HCL
2417 >>> x = HCL([275, 314, 353, 31, 70],
2418 >>> [ 70, 85, 102, 86, 45],
2419 >>> [ 25, 40, 55, 70, 85])
2420 >>> x
2421 >>> #:
2422 >>> type(x)
2423 >>> #: Convert colors to sRGB
2424 >>> x.to("sRGB")
2425 >>> x
2426 >>> #:
2427 >>> type(x)
2428 >>> #: Convert from sRGB to hex
2429 >>> x.to("hex")
2430 >>> x
2431 >>> #: Convert back to HCL colors.
2432 >>> # Round-off errors due to conversion to 'hex'.
2433 >>> x.to("HCL")
2434 >>> x
2435 >>> #: Extracting hex colors (returns list of str)
2436 >>> x.colors()
2438 """
2439 self._check_if_allowed_(to)
2440 from . import colorlib
2441 clib = colorlib()
2443 # Nothing to do (converted to itself)
2444 if to in ["HCL", self.__class__.__name__]:
2445 return
2447 # This is the only transformation from polarLUV -> LUV
2448 elif to == "CIELUV":
2449 [L, U, V] = clib.polarLUV_to_LUV(self.get("L"), self.get("C"), self.get("H"))
2450 self._data_ = {"L" : L, "U" : U, "V" : V, "alpha" : self.get("alpha")}
2451 self.__class__ = CIELUV
2453 # The rest are transformations along a path
2454 elif to == "CIEXYZ":
2455 via = ["CIELUV", to]
2456 self._transform_via_path_(via, fixup = fixup)
2458 elif to == "CIELAB":
2459 via = ["CIELUV", "CIEXYZ", to]
2460 self._transform_via_path_(via, fixup = fixup)
2462 elif to == "RGB":
2463 via = ["CIELUV", "CIEXYZ", to]
2464 self._transform_via_path_(via, fixup = fixup)
2466 elif to == "sRGB":
2467 via = ["CIELUV", "CIEXYZ", to]
2468 self._transform_via_path_(via, fixup = fixup)
2470 elif to == "polarLAB":
2471 via = ["CIELUV", "CIEXYZ", "CIELAB", to]
2472 self._transform_via_path_(via, fixup = fixup)
2474 elif to == "hex":
2475 via = ["CIELUV", "CIEXYZ", "sRGB", to]
2476 self._transform_via_path_(via, fixup = fixup)
2478 elif to in ["HLS", "HSV"]:
2479 self._ambiguous(self.__class__.__name__, to)
2481 # Currently not used but implemented as fallback for the future
2482 else: self._cannot(self.__class__.__name__, to)
2484# polarLUV is HCL, make copy
2485HCL = polarLUV
2488# -------------------------------------------------------------------
2489# CIELUV color object
2490# -------------------------------------------------------------------
2491class CIELUV(colorobject):
2492 """Create CIELUV Color Object
2494 Creates a color object in the CIELUV color space.
2495 Can be converted to: :py:class:`CIEXYZ`, :py:class:`CIELUV`,
2496 :py:class:`CIELAB`, :py:class:`RGB`, :py:class:`sRGB`,
2497 :py:class:`polarLAB`, and :py:class:`hexcols`.
2498 Not allowed (ambiguous) are transformations to :py:class:`HSV` and :py:class:`HLS`.
2500 Args:
2501 L (int, float, list, numpy.array):
2502 Numeric value(s) for L dimension.
2503 U (int, float, list, numpy.array):
2504 Numeric value(s) for U dimension.
2505 V (int, float, list, numpy.array):
2506 Numeric value(s) for L dimension.
2507 alpha (None, float, list, numpy.array): Numeric value(s) for the alpha
2508 channel (`[0., 1.]`) where `0.` equals full transparency, `1.` full
2509 opacity. If `None` (default) no transparency is added.
2511 Example:
2513 >>> from colorspace import CIELUV
2514 >>> # Constructing color object with one single color via float
2515 >>> CIELUV(0, 10, 10)
2516 >>> #: Constructing object via lists
2517 >>> CIELUV([10, 30], [20, 80], [100, 40])
2518 >>> #: Constructing object via numpy arrays
2519 >>> from numpy import asarray
2520 >>> CIELUV(asarray([10, 30]), asarray([20, 80]), asarray([100, 40]))
2522 """
2523 def __init__(self, L, U, V, alpha = None):
2525 # checking inputs, save inputs on object
2526 self._data_ = {} # Dict to store the colors/color dimensions
2527 tmp = self._colorobject_check_input_arrays_(L = L, U = U, V = V, alpha = alpha)
2528 for key,val in tmp.items(): self._data_[key] = val
2529 # White spot definition (the default)
2530 self.set_whitepoint(X = 95.047, Y = 100.000, Z = 108.883)
2533 def to(self, to, fixup = True):
2534 """Transform Color Space
2536 Allows to transform the current object into a different color space,
2537 if possible. Converting the colors of the current object into
2538 another color space. After calling this method, the object
2539 will be of a different class.
2541 Args:
2542 to (str): Name of the color space into which the colors should be
2543 converted (e.g., `"CIEXYZ"`, `"HCL"`, `"hex"`, `"sRGB"`, ...).
2544 fixup (bool): Whether or not colors outside the defined rgb color space
2545 should be corrected if necessary, defaults to `True`.
2547 Examples:
2549 >>> from colorspace import CIELUV
2550 >>> x = CIELUV([ 25, 45, 65, 85],
2551 >>> [ 6, 75, 90, 16],
2552 >>> [-70, -50, 30, 42])
2553 >>> x
2554 >>> #:
2555 >>> type(x)
2556 >>> #: Convert colors to sRGB
2557 >>> x.to("sRGB")
2558 >>> x
2559 >>> #:
2560 >>> type(x)
2561 >>> #: Convert from sRGB to hex
2562 >>> x.to("hex")
2563 >>> x
2564 >>> #: Convert back to CIELUV colors.
2565 >>> # Round-off errors due to conversion to 'hex'.
2566 >>> x.to("CIELUV")
2567 >>> x
2568 >>> #: Extracting hex colors (returns list of str)
2569 >>> x.colors()
2571 """
2572 self._check_if_allowed_(to)
2573 from . import colorlib
2574 clib = colorlib()
2576 # Nothing to do (converted to itself)
2577 if to == self.__class__.__name__:
2578 return
2579 # Transformation from CIELUV -> CIEXYZ
2580 elif to == "CIEXYZ":
2581 [X, Y, Z] = clib.LUV_to_XYZ(self.get("L"), self.get("U"), self.get("V"),
2582 self.WHITEX, self.WHITEY, self.WHITEZ)
2583 self._data_ = {"X" : X, "Y" : Y, "Z" : Z, "alpha" : self.get("alpha")}
2584 self.__class__ = CIEXYZ
2586 # Transformation from CIELUV -> polarLUV (HCL)
2587 elif to in ["HCL","polarLUV"]:
2588 [L, C, H] = clib.LUV_to_polarLUV(self.get("L"), self.get("U"), self.get("V"))
2589 self._data_ = {"L" : L, "C" : C, "H" : H, "alpha" : self.get("alpha")}
2590 self.__class__ = polarLUV
2592 # The rest are transformations along a path
2593 elif to == "CIELAB":
2594 via = ["CIEXYZ", to]
2595 self._transform_via_path_(via, fixup = fixup)
2597 elif to == "RGB":
2598 via = ["CIEXYZ", to]
2599 self._transform_via_path_(via, fixup = fixup)
2601 elif to == "sRGB":
2602 via = ["CIEXYZ", "RGB", to]
2603 self._transform_via_path_(via, fixup = fixup)
2605 elif to == "polarLAB":
2606 via = ["CIEXYZ", "CIELAB", to]
2607 self._transform_via_path_(via, fixup = fixup)
2609 elif to == "hex":
2610 via = ["CIEXYZ", "RGB", "sRGB", to]
2611 self._transform_via_path_(via, fixup = fixup)
2613 elif to in ["HLS", "HSV"]:
2614 self._ambiguous(self.__class__.__name__, to)
2616 else: self._cannot(self.__class__.__name__, to)
2618# -------------------------------------------------------------------
2619# CIEXYZ color object
2620# -------------------------------------------------------------------
2621class CIEXYZ(colorobject):
2622 """Create CIEXYZ Color Object
2624 Creates a color object in the CIEXYZ color space.
2625 Can be converted to: :py:class:`CIEXYZ`, :py:class:`CIELUV`,
2626 :py:class:`CIELAB`, :py:class:`RGB`, :py:class:`sRGB`,
2627 :py:class:`polarLAB`, and :py:class:`hexcols`.
2628 Not allowed (ambiguous) are transformations to :py:class:`HSV` and :py:class:`HLS`.
2630 Args:
2631 X (int, float, list, numpy.array):
2632 Numeric value(s) for X dimension.
2633 Y (int, float, list, numpy.array):
2634 Numeric value(s) for Y dimension.
2635 Z (int, float, list, numpy.array):
2636 Numeric value(s) for Z dimension.
2637 alpha (None, float, list, numpy.array): Numeric value(s) for the alpha
2638 channel (`[0., 1.]`) where `0.` equals full transparency, `1.` full
2639 opacity. If `None` (default) no transparency is added.
2641 Example:
2643 >>> from colorspace import CIEXYZ
2644 >>> # Constructing color object with one single color via float
2645 >>> CIEXYZ(80, 30, 10)
2646 >>> #: Constructing object via lists
2647 >>> CIEXYZ([10, 0], [20, 80], [40, 40])
2648 >>> #: Constructing object via numpy arrays
2649 >>> from numpy import asarray
2650 >>> CIEXYZ(asarray([10, 0]), asarray([20, 80]), asarray([40, 40]))
2652 """
2653 def __init__(self, X, Y, Z, alpha = None):
2655 # checking inputs, save inputs on object
2656 self._data_ = {} # Dict to store the colors/color dimensions
2657 tmp = self._colorobject_check_input_arrays_(X = X, Y = Y, Z = Z, alpha = alpha)
2658 for key,val in tmp.items(): self._data_[key] = val
2659 # White spot definition (the default)
2660 self.set_whitepoint(X = 95.047, Y = 100.000, Z = 108.883)
2663 def to(self, to, fixup = True):
2664 """Transform Color Space
2666 Allows to transform the current object into a different color space,
2667 if possible. Converting the colors of the current object into
2668 another color space. After calling this method, the object
2669 will be of a different class.
2671 Args:
2672 to (str): Name of the color space into which the colors should be
2673 converted (e.g., `"CIELUV"`, `"HCL"`, `"hex"`, `"sRGB"`, ...).
2674 fixup (bool): Whether or not colors outside the defined rgb color space
2675 should be corrected if necessary, defaults to `True`.
2677 Examples:
2679 >>> from colorspace import CIEXYZ
2680 >>> x = CIEXYZ([ 8.5, 27.8, 46.2, 62.1],
2681 >>> [ 4.4, 14.5, 34.1, 65.9],
2682 >>> [27.2, 31.9, 17.2, 40.0])
2683 >>> x
2684 >>> #:
2685 >>> type(x)
2686 >>> #: Convert colors to sRGB
2687 >>> x.to("sRGB")
2688 >>> x
2689 >>> #:
2690 >>> type(x)
2691 >>> #: Convert from sRGB to hex
2692 >>> x.to("hex")
2693 >>> x
2694 >>> #: Convert back to CIEXYZ colors.
2695 >>> # Round-off errors due to conversion to 'hex'.
2696 >>> x.to("CIEXYZ")
2697 >>> x
2698 >>> #: Extracting hex colors (returns list of str)
2699 >>> x.colors()
2701 """
2702 self._check_if_allowed_(to)
2703 from . import colorlib
2704 clib = colorlib()
2706 # Nothing to do (converted to itself)
2707 if to == self.__class__.__name__:
2708 return
2710 # Transformation from CIEXYZ -> CIELUV
2711 elif to == "CIELUV":
2712 [L, U, V] = clib.XYZ_to_LUV(self.get("X"), self.get("Y"), self.get("Z"),
2713 self.WHITEX, self.WHITEY, self.WHITEZ)
2714 self._data_ = {"L" : L, "U" : U, "V" : V, "alpha" : self.get("alpha")}
2715 self.__class__ = CIELUV
2717 # Transformation from CIEXYZ -> CIELAB
2718 elif to == "CIELAB":
2719 [L, A, B] = clib.XYZ_to_LAB(self.get("X"), self.get("Y"), self.get("Z"),
2720 self.WHITEX, self.WHITEY, self.WHITEZ)
2721 self._data_ = {"L" : L, "A" : A, "B" : B, "alpha" : self.get("alpha")}
2722 self.__class__ = CIELAB
2724 # Transformation from CIEXYZ -> RGB
2725 elif to == "RGB":
2726 [R, G, B] = clib.XYZ_to_RGB(self.get("X"), self.get("Y"), self.get("Z"),
2727 self.WHITEX, self.WHITEY, self.WHITEZ)
2728 self._data_ = {"R" : R, "G" : G, "B" : B, "alpha" : self.get("alpha")}
2729 self.__class__ = RGB
2731 # The rest are transformations along a path
2732 elif to == "polarLAB":
2733 via = ["CIELAB", to]
2734 self._transform_via_path_(via, fixup = fixup)
2736 elif to in ["HCL", "polarLUV"]:
2737 via = ["CIELUV", to]
2738 self._transform_via_path_(via, fixup = fixup)
2740 elif to == "sRGB":
2741 via = ["RGB", to]
2742 self._transform_via_path_(via, fixup = fixup)
2744 elif to == "hex":
2745 via = ["RGB", "sRGB", to]
2746 self._transform_via_path_(via, fixup = fixup)
2748 elif to in ["HLS", "HSV"]:
2749 self._ambiguous(self.__class__.__name__, to)
2751 else: self._cannot(self.__class__.__name__, to)
2754class RGB(colorobject):
2755 """Create RGB Color Object
2757 Allows conversions to: :py:class:`CIELAB`, :py:class:`CIELUV`,
2758 :py:class:`CIEXYZ`, :py:class:`HLS`, :py:class:`HSV`, :py:class:`hexcols`.
2759 :py:class:`polarLAB`, :py:class:`polarLUV` and :py:class:`sRGB`.
2761 Args:
2762 R (int, float, list, numpy.array):
2763 Numeric value(s) for red intensity (`[0., 1.]`).
2764 G (int, float, list, numpy.array):
2765 Numeric value(s) for green intensity (`[0., 1.]`).
2766 B (int, float, list, numpy.array):
2767 Numeric value(s) for blue intensity (`[0., 1.]`).
2768 alpha (None, float, list, numpy.array): Numeric value(s) for the alpha
2769 channel (`[0., 1.]`) where `0.` equals full transparency, `1.` full
2770 opacity. If `None` (default) no transparency is added.
2772 Example:
2774 >>> from colorspace import RGB
2775 >>> # Constructing color object with one single color via float
2776 >>> RGB(1., 0.3, 0.5)
2777 >>> #: Constructing object via lists
2778 >>> RGB([1., 0.8], [0.5, 0.5], [0.0, 0.2])
2779 >>> #: Constructing object via numpy arrays
2780 >>> from numpy import asarray
2781 >>> RGB(asarray([1., 0.8]), asarray([0.5, 0.5]), asarray([0.0, 0.2]))
2783 """
2785 def __init__(self, R, G, B, alpha = None):
2787 # checking inputs, save inputs on object
2788 self._data_ = {} # Dict to store the colors/color dimensions
2790 tmp = self._colorobject_check_input_arrays_(R = R, G = G, B = B, alpha = alpha)
2791 for key,val in tmp.items(): self._data_[key] = val
2792 # White spot definition (the default)
2793 self.set_whitepoint(X = 95.047, Y = 100.000, Z = 108.883)
2796 def to(self, to, fixup = True):
2797 """Transform Color Space
2799 Allows to transform the current object into a different color space,
2800 if possible. Converting the colors of the current object into
2801 another color space. After calling this method, the object
2802 will be of a different class.
2804 Args:
2805 to (str): Name of the color space into which the colors should be
2806 converted (e.g., `"CIEXYZ"`, `"HCL"`, `"hex"`, `"sRGB"`, ...).
2807 fixup (bool): Whether or not colors outside the defined rgb color space
2808 should be corrected if necessary, defaults to `True`.
2810 Examples:
2812 >>> from colorspace import RGB
2813 >>> x = RGB([0.070, 0.520, 0.887, 0.799],
2814 >>> [0.012, 0.015, 0.198, 0.651],
2815 >>> [0.283, 0.323, 0.138, 0.323])
2816 >>> x
2817 >>> #:
2818 >>> type(x)
2819 >>> #: Convert colors to CIEXYZ
2820 >>> x.to("CIELUV")
2821 >>> x
2822 >>> #:
2823 >>> type(x)
2824 >>> #: Convert from CIELUV to HCL
2825 >>> x.to("HCL")
2826 >>> x
2827 >>> # Convert back to RGB
2828 >>> x.to("RGB")
2829 >>> x
2830 >>> #: Extracting hex colors (returns list of str)
2831 >>> x.colors()
2833 """
2834 self._check_if_allowed_(to)
2835 from . import colorlib
2836 clib = colorlib()
2838 # Nothing to do (converted to itself)
2839 if to == self.__class__.__name__:
2840 return
2842 # Transform from RGB -> sRGB
2843 elif to == "sRGB":
2844 [R, G, B] = clib.RGB_to_sRGB(self.get("R"), self.get("G"), self.get("B"),
2845 self.GAMMA)
2846 self._data_ = {"R" : R, "G" : G, "B" : B, "alpha" : self.get("alpha")}
2847 self.__class__ = sRGB
2849 # Transform from RGB -> CIEXYZ
2850 elif to == "CIEXYZ":
2851 [X, Y, Z] = clib.RGB_to_XYZ(self.get("R"), self.get("G"), self.get("B"),
2852 self.WHITEX, self.WHITEY, self.WHITEZ)
2853 self._data_ = {"X" : X, "Y" : Y, "Z" : Z, "alpha" : self.get("alpha")}
2854 self.__class__ = CIEXYZ
2856 # From RGB to HLS: take direct path (not via sRGB)
2857 elif to in ["HLS"]:
2858 [H, L, S] = clib.RGB_to_HLS(self.get("R"), self.get("G"), self.get("B"))
2859 self._data_ = {"H" : H, "L" : L, "S" : S, "alpha" : self.get("alpha")}
2860 self.__class__ = HLS
2862 # From RGB to HSV: take direct path (not via sRGB)
2863 elif to in ["HSV"]:
2864 [H, S, V] = clib.RGB_to_HSV(self.get("R"), self.get("G"), self.get("B"))
2865 self._data_ = {"H" : H, "S" : S, "V" : V, "alpha" : self.get("alpha")}
2866 self.__class__ = HSV
2868 # The rest are transformations along a path
2869 elif to in ["hex"]:
2870 via = ["sRGB", to]
2871 self._transform_via_path_(via, fixup = fixup)
2873 elif to in ["CIELUV", "CIELAB"]:
2874 via = ["CIEXYZ", to]
2875 self._transform_via_path_(via, fixup = fixup)
2877 elif to in ["HCL","polarLUV"]:
2878 via = ["CIEXYZ", "CIELUV", to]
2879 self._transform_via_path_(via, fixup = fixup)
2881 elif to == "polarLAB":
2882 via = ["CIEXYZ", "CIELAB", to]
2883 self._transform_via_path_(via, fixup = fixup)
2885 else: self._cannot(self.__class__.__name__, to)
2888class sRGB(colorobject):
2889 """Create Standard RGB (sRGB) Color Object
2891 Allows conversions to: :py:class:`CIELAB`, :py:class:`CIELUV`,
2892 :py:class:`CIEXYZ`, :py:class:`HLS`, :py:class:`HSV`, :py:class:`RGB`,
2893 :py:class:`hexcols`. :py:class:`polarLAB` and :py:class:`polarLUV`.
2895 Args:
2896 R (int, float, list, numpy.array):
2897 Numeric value(s) for red intensity (`[0., 1.]`).
2898 G (int, float, list, numpy.array):
2899 Numeric value(s) for green intensity (`[0., 1.]`).
2900 B (int, float, list, numpy.array):
2901 Numeric value(s) for blue intensity (`[0., 1.]`).
2902 alpha (None, float, list, numpy.array): Numeric value(s) for the alpha
2903 channel (`[0., 1.]`) where `0.` equals full transparency, `1.` full
2904 opacity. If `None` (default) no transparency is added.
2905 gamma (None, float): If `None` (default) the default gamma value is used.
2906 Can be specified to overwrite the default.
2908 Example:
2910 >>> from colorspace import sRGB
2911 >>> # Constructing color object with one single color via float
2912 >>> sRGB(1., 0.3, 0.5)
2913 >>> #: Constructing object via lists
2914 >>> sRGB([1., 0.8], [0.5, 0.5], [0.0, 0.2])
2915 >>> #: Constructing object via numpy arrays
2916 >>> from numpy import asarray
2917 >>> sRGB(asarray([1., 0.8]), asarray([0.5, 0.5]), asarray([0.0, 0.2]))
2919 """
2921 def __init__(self, R, G, B, alpha = None, gamma = None):
2923 # checking inputs, save inputs on object
2924 self._data_ = {} # Dict to store the colors/color dimensions
2925 tmp = self._colorobject_check_input_arrays_(R = R, G = G, B = B, alpha = alpha)
2926 for key,val in tmp.items(): self._data_[key] = val
2928 # White spot definition (the default)
2929 self.set_whitepoint(X = 95.047, Y = 100.000, Z = 108.883)
2931 if isinstance(gamma, float): self.GAMMA = gamma
2934 def to(self, to, fixup = True):
2935 """Transform Color Space
2937 Allows to transform the current object into a different color space,
2938 if possible. Converting the colors of the current object into
2939 another color space. After calling this method, the object
2940 will be of a different class.
2942 Args:
2943 to (str): Name of the color space into which the colors should be
2944 converted (e.g., `"CIEXYZ"`, `"HCL"`, `"hex"`, `"sRGB"`, ...).
2945 fixup (bool): Whether or not colors outside the defined rgb color space
2946 should be corrected if necessary, defaults to `True`.
2948 Examples:
2950 >>> from colorspace import sRGB
2951 >>> x = sRGB([0.294, 0.749, 0.949, 0.905],
2952 >>> [0.113, 0.129, 0.482, 0.827],
2953 >>> [0.568, 0.603, 0.407, 0.603])
2954 >>> x
2955 >>> #:
2956 >>> type(x)
2957 >>> #: Convert colors to CIEXYZ
2958 >>> x.to("CIELUV")
2959 >>> x
2960 >>> #:
2961 >>> type(x)
2962 >>> #: Convert from CIELUV to HCL
2963 >>> x.to("HCL")
2964 >>> x
2965 >>> #: Convert back to Standard RGB colors.
2966 >>> x.to("sRGB")
2967 >>> x
2968 >>> #: Extracting hex colors (returns list of str)
2969 >>> x.colors()
2971 """
2972 self._check_if_allowed_(to)
2973 from . import colorlib
2974 clib = colorlib()
2976 # Nothing to do (converted to itself)
2977 if to == self.__class__.__name__:
2978 return
2980 # Transformation sRGB -> RGB
2981 elif to == "RGB":
2982 [R, G, B] = clib.sRGB_to_RGB(self.get("R"), self.get("G"), self.get("B"),
2983 gamma = self.GAMMA)
2984 self._data_ = {"R" : R, "G" : G, "B" : B, "alpha" : self.get("alpha")}
2985 self.__class__ = RGB
2987 # Transformation sRGB -> hex
2988 elif to == "hex":
2989 hex_ = clib.sRGB_to_hex(self.get("R"), self.get("G"), self.get("B"), fixup)
2990 self._data_ = {"hex_" : hex_, "alpha" : self.get("alpha")}
2991 self.__class__ = hexcols
2993 # Transform from RGB -> HLS
2994 elif to == "HLS":
2995 [H, L, S] = clib.sRGB_to_HLS(self.get("R"), self.get("G"), self.get("B"))
2996 self._data_ = {"H" : H, "L" : L, "S" : S, "alpha" : self.get("alpha")}
2997 self.__class__ = HLS
2999 # Transform from RGB -> HSV
3000 elif to == "HSV":
3001 [H, S, V] = clib.sRGB_to_HSV(self.get("R"), self.get("G"), self.get("B"))
3002 self._data_ = {"H" : H, "S" : S, "V" : V, "alpha" : self.get("alpha")}
3003 self.__class__ = HSV
3005 # The rest are transformations along a path
3006 elif to in ["CIEXYZ"]:
3007 via = ["RGB", to]
3008 self._transform_via_path_(via, fixup = fixup)
3010 elif to in ["CIELUV", "CIELAB"]:
3011 via = ["RGB", "CIEXYZ", to]
3012 self._transform_via_path_(via, fixup = fixup)
3014 elif to in ["HCL","polarLUV"]:
3015 via = ["RGB", "CIEXYZ", "CIELUV", to]
3016 self._transform_via_path_(via, fixup = fixup)
3018 elif to == "polarLAB":
3019 via = ["RGB", "CIEXYZ", "CIELAB", to]
3020 self._transform_via_path_(via, fixup = fixup)
3022 else: self._cannot(self.__class__.__name__, to)
3025class CIELAB(colorobject):
3026 """Create CIELAB Color Object
3028 Creates a color object in the CIELAB color space.
3029 Can be converted to: :py:class:`CIEXYZ`, :py:class:`CIELUV`,
3030 :py:class:`CIELAB`, :py:class:`RGB`, :py:class:`sRGB`,
3031 :py:class:`polarLAB`, and :py:class:`hexcols`.
3032 Not allowed (ambiguous) are transformations to :py:class:`HSV` and :py:class:`HLS`.
3034 Args:
3035 L (int, float, list, numpy.array):
3036 Numeric value(s) for L dimension.
3037 A (int, float, list, numpy.array):
3038 Numeric value(s) for A dimension.
3039 B (int, float, list, numpy.array):
3040 Numeric value(s) for B dimension.
3041 alpha (None, float, list, numpy.array): Numeric value(s) for the alpha
3042 channel (`[0., 1.]`) where `0.` equals full transparency, `1.` full
3043 opacity. If `None` (default) no transparency is added.
3045 Example:
3047 >>> from colorspace import CIELAB
3048 >>> # Constructing color object with one single color via float
3049 >>> CIELAB(-30, 10, 10)
3050 >>> #: Constructing object via lists
3051 >>> CIELAB([-30, 30], [20, 80], [40, 40])
3052 >>> #: Constructing object via numpy arrays
3053 >>> from numpy import asarray
3054 >>> CIELAB(asarray([-30, 30]), asarray([20, 80]), asarray([40, 40]))
3056 """
3058 def __init__(self, L, A, B, alpha = None):
3060 # checking inputs, save inputs on object
3061 self._data_ = {} # Dict to store the colors/color dimensions
3062 tmp = self._colorobject_check_input_arrays_(L = L, A = A, B = B, alpha = alpha)
3063 for key,val in tmp.items(): self._data_[key] = val
3064 # White spot definition (the default)
3065 self.set_whitepoint(X = 95.047, Y = 100.000, Z = 108.883)
3068 def to(self, to, fixup = True):
3069 """Transform Color Space
3071 Allows to transform the current object into a different color space,
3072 if possible.
3074 Args:
3075 to (str): Name of the color space into which the colors should be
3076 converted (e.g., `CIEXYZ`, `HCL`, `hex`, `RGB`, ...)
3077 fixup (bool): Whether or not colors outside the defined rgb color space
3078 should be corrected if necessary, defaults to True.
3080 Returns:
3081 No return, converts the object into a new color space and modifies
3082 the underlying object. After calling this method the object will
3083 be of a different class.
3084 """
3085 self._check_if_allowed_(to)
3086 from . import colorlib
3087 clib = colorlib()
3089 # Nothing to do (converted to itself)
3090 if to == self.__class__.__name__:
3091 return
3093 # Transformations CIELAB -> CIEXYZ
3094 elif to == "CIEXYZ":
3095 [X, Y, Z] = clib.LAB_to_XYZ(self.get("L"), self.get("A"), self.get("B"),
3096 self.WHITEX, self.WHITEY, self.WHITEZ)
3097 self._data_ = {"X" : X, "Y" : Y, "Z" : Z, "alpha" : self.get("alpha")}
3098 self.__class__ = CIEXYZ
3100 # Transformation CIELAB -> polarLAB
3101 elif to == "polarLAB":
3102 [L, A, B] = clib.LAB_to_polarLAB(self.get("L"), self.get("A"), self.get("B"))
3103 self._data_ = {"L" : L, "A" : A, "B" : B, "alpha" : self.get("alpha")}
3104 self.__class__ = polarLAB
3106 # The rest are transformations along a path
3107 elif to == "CIELUV":
3108 via = ["CIEXYZ", to]
3109 self._transform_via_path_(via, fixup = fixup)
3111 elif to in ["HCL","polarLUV"]:
3112 via = ["CIEXYZ", "CIELUV", to]
3113 self._transform_via_path_(via, fixup = fixup)
3115 elif to == "RGB":
3116 via = ["CIEXYZ", to]
3117 self._transform_via_path_(via, fixup = fixup)
3119 elif to == "sRGB":
3120 via = ["CIEXYZ", "RGB", to]
3121 self._transform_via_path_(via, fixup = fixup)
3123 elif to == "hex":
3124 via = ["CIEXYZ", "RGB", "sRGB", to]
3125 self._transform_via_path_(via, fixup = fixup)
3127 elif to in ["HLS", "HSV"]:
3128 self._ambiguous(self.__class__.__name__, to)
3130 else: self._cannot(self.__class__.__name__, to)
3133class polarLAB(colorobject):
3134 """Create Polar LAB Color Object
3136 Creates a color object in the polar representation of the
3137 :py:class:`CIELAB` color space.
3138 Can be converted to: :py:class:`CIEXYZ`, :py:class:`CIELUV`,
3139 :py:class:`CIELAB`, :py:class:`RGB`, :py:class:`sRGB`,
3140 :py:class:`polarLAB`, and :py:class:`hexcols`.
3141 Not allowed (ambiguous) are transformations to :py:class:`HSV` and :py:class:`HLS`.
3143 Args:
3144 L (int, float, list, numpy.array):
3145 Numeric value(s) for L dimension.
3146 A (int, float, list, numpy.array):
3147 Numeric value(s) for A dimension.
3148 B (int, float, list, numpy.array):
3149 Numeric value(s) for B dimension.
3150 alpha (None, float, list, numpy.array): Numeric value(s) for the alpha
3151 channel (`[0., 1.]`) where `0.` equals full transparency, `1.` full
3152 opacity. If `None` (default) no transparency is added.
3154 Examples:
3156 >>> from colorspace import polarLAB
3157 >>> cols = polarLAB([50, 80, 30], [100, 120, 140], [40, 130, 300])
3158 >>> cols
3159 >>> #: Convert to hex colors
3160 >>> cols.to("hex")
3161 >>> cols
3163 """
3165 def __init__(self, L, A, B, alpha = None):
3167 # checking inputs, save inputs on object
3168 self._data_ = {} # Dict to store the colors/color dimensions
3169 tmp = self._colorobject_check_input_arrays_(L = L, A = A, B = B, alpha = alpha)
3170 for key,val in tmp.items(): self._data_[key] = val
3171 # White spot definition (the default)
3172 self.set_whitepoint(X = 95.047, Y = 100.000, Z = 108.883)
3175 def to(self, to, fixup = True):
3176 """Transform Color Space
3178 Allows to transform the current object into a different color space,
3179 if possible. Converting the colors of the current object into
3180 another color space. After calling this method, the object
3181 will be of a different class.
3183 Args:
3184 to (str): Name of the color space into which the colors should be
3185 converted (e.g., `"CIEXYZ"`, `"HCL"`, `"hex"`, `"sRGB"`, ...).
3186 fixup (bool): Whether or not colors outside the defined rgb color space
3187 should be corrected if necessary, defaults to `True`.
3189 Examples:
3191 >>> from colorspace import polarLAB
3192 >>> x = polarLAB([ 25, 45, 65, 85],
3193 >>> [ 72, 75, 54, 31],
3194 >>> [310, 338, 36, 92])
3195 >>> x
3196 >>> #:
3197 >>> type(x)
3198 >>> #: Convert colors to sRGB
3199 >>> x.to("sRGB")
3200 >>> x
3201 >>> #:
3202 >>> type(x)
3203 >>> #: Convert from sRGB to hex
3204 >>> x.to("hex")
3205 >>> x
3206 >>> #: Convert back to polarLAB colors.
3207 >>> # Round-off errors due to conversion to 'hex'.
3208 >>> x.to("polarLAB")
3209 >>> x
3210 >>> #: Extracting hex colors (returns list of str)
3211 >>> x.colors()
3213 """
3214 self._check_if_allowed_(to)
3215 from . import colorlib
3216 clib = colorlib()
3218 # Nothing to do (converted to itself)
3219 if to == self.__class__.__name__:
3220 return
3222 # The only transformation we need is from polarLAB -> LAB
3223 elif to == "CIELAB":
3224 [L, A, B] = clib.polarLAB_to_LAB(self.get("L"), self.get("A"), self.get("B"))
3225 self._data_ = {"L" : L, "A" : A, "B" : B, "alpha" : self.get("alpha")}
3226 self.__class__ = CIELAB
3228 # The rest are transformationas along a path
3229 elif to == "CIEXYZ":
3230 via = ["CIELAB", to]
3231 self._transform_via_path_(via, fixup = fixup)
3233 elif to == "CIELUV":
3234 via = ["CIELAB", "CIEXYZ", to]
3235 self._transform_via_path_(via, fixup = fixup)
3237 elif to in ["HCL", "polarLUV"]:
3238 via = ["CIELAB", "CIEXYZ", "CIELUV", to]
3239 self._transform_via_path_(via, fixup = fixup)
3241 elif to == "RGB":
3242 via = ["CIELAB", "CIEXYZ", to]
3243 self._transform_via_path_(via, fixup = fixup)
3245 elif to == "sRGB":
3246 via = ["CIELAB", "CIEXYZ", "RGB", to]
3247 self._transform_via_path_(via, fixup = fixup)
3249 elif to == "hex":
3250 via = ["CIELAB", "CIEXYZ", "RGB", "sRGB", to]
3251 self._transform_via_path_(via, fixup = fixup)
3253 elif to in ["HLS", "HSV"]:
3254 self._ambiguous(self.__class__.__name__, to)
3256 else: self._cannot(self.__class__.__name__, to)
3259class HSV(colorobject):
3260 """Create HSV Color Object
3262 Creates a color object in the Hue-Saturation-Value (HSV) color space.
3263 Can be converted to: :py:class:`RGB`, :py:class:`sRGB`, :py:class:`HLS`,
3264 and :py:class:`hexcols`.
3265 Not allowed (ambiguous) are transformations to :py:class:`CIEXYZ`,
3266 :py:class:`CIELUV`, :py:class:`CIELAB`, :py:class:`polarLUV`, and
3267 :py:class:`polarLAB`.
3269 Args:
3270 H (int, float, list, numpy.array):
3271 Numeric value(s) for Hue dimension.
3272 S (int, float, list, numpy.array):
3273 Numeric value(s) for Saturation dimension.
3274 V (int, float, list, numpy.array):
3275 Numeric value(s) for Value dimension.
3276 alpha (None, float, list, numpy.array): Numeric value(s) for the alpha
3277 channel (`[0., 1.]`) where `0.` equals full transparency, `1.` full
3278 opacity. If `None` (default) no transparency is added.
3280 Examples:
3282 >>> #: Constructing object via numpy arrays
3283 >>> from colorspace import HSV
3284 >>> # Constructing color object with one single color via float
3285 >>> HSV(150, 150, 10)
3286 >>> #: Constructing object via lists
3287 >>> HSV([150, 150, 10], [1.5, 0, 1.5], [0.1, 0.7, 0.1])
3288 >>> #: Constructing object via numpy arrays
3289 >>> from numpy import asarray
3290 >>> cols = HSV(asarray([150, 150, 150]),
3291 >>> asarray([1.5, 0, 1.5]),
3292 >>> asarray([0.1, 0.7, 0.1]))
3293 >>> cols
3294 >>> #: Converting to RGB
3295 >>> cols.to("RGB")
3296 >>> cols
3297 """
3299 def __init__(self, H, S, V, alpha = None):
3301 # checking inputs, save inputs on object
3302 self._data_ = {} # Dict to store the colors/color dimensions
3303 tmp = self._colorobject_check_input_arrays_(H = H, S = S, V = V, alpha = alpha)
3304 for key,val in tmp.items(): self._data_[key] = val
3305 # White spot definition (the default)
3306 self.set_whitepoint(X = 95.047, Y = 100.000, Z = 108.883)
3309 def to(self, to, fixup = True):
3310 """Transform Color Space
3312 Allows to transform the current object into a different color space,
3313 if possible. Converting the colors of the current object into
3314 another color space. After calling this method, the object
3315 will be of a different class.
3317 Args:
3318 to (str): Name of the color space into which the colors should be
3319 converted (e.g., `"CIEXYZ"`, `"HCL"`, `"hex"`, `"sRGB"`, ...).
3320 fixup (bool): Whether or not colors outside the defined rgb color space
3321 should be corrected if necessary, defaults to `True`.
3323 Examples:
3325 >>> from colorspace import HSV
3326 >>> x = HSV([ 264, 314, 8, 44],
3327 >>> [0.80, 0.83, 0.57, 0.33],
3328 >>> [0.57, 0.75, 0.95, 0.91])
3329 >>> x
3330 >>> #:
3331 >>> type(x)
3332 >>> #: Convert colors to HLS
3333 >>> x.to("HLS")
3334 >>> x
3335 >>> #:
3336 >>> type(x)
3337 >>> #: Convert colors to HSV
3338 >>> x.to("HSV")
3339 >>> x
3340 >>> #: Extracting hex colors (returns list of str)
3341 >>> x.colors()
3343 """
3344 self._check_if_allowed_(to)
3345 from . import colorlib
3346 clib = colorlib()
3348 # Nothing to do (converted to itself)
3349 if to == self.__class__.__name__:
3350 return
3352 # The only transformation we need is back to RGB
3353 elif to == "sRGB":
3354 [R, G, B] = clib.HSV_to_sRGB(self.get("H"), self.get("S"), self.get("V"))
3355 self._data_ = {"R" : R, "G" : G, "B" : B, "alpha" : self.get("alpha")}
3356 self.__class__ = sRGB
3358 # From HLS to RGB: take direct path (not via sRGB)
3359 elif to in ["RGB"]:
3360 [R, G, B] = clib.HSV_to_RGB(self.get("H"), self.get("S"), self.get("V"))
3361 self._data_ = {"R" : R, "G" : G, "B" : B, "alpha" : self.get("alpha")}
3362 self.__class__ = RGB
3364 elif to == "hex":
3365 via = ["sRGB", to]
3366 self._transform_via_path_(via, fixup = fixup)
3368 elif to == "HLS":
3369 via = ["sRGB", to]
3370 self._transform_via_path_(via, fixup = fixup)
3372 elif to in ["CIEXYZ", "CIELUV", "CIELAB", "polarLUV", "HCL", "polarLAB"]:
3373 self._ambiguous(self.__class__.__name__, to)
3375 else: self._cannot(self.__class__.__name__, to)
3378class HLS(colorobject):
3379 """Create HLS Color Object
3381 Creates a color object in the Hue-Lightness-Saturation (HLS) color space.
3382 Can be converted to: :py:class:`RGB`, :py:class:`sRGB`, :py:class:`HSV`,
3383 and :py:class:`hexcols`.
3384 Not allowed (ambiguous) are transformations to :py:class:`CIEXYZ`,
3385 :py:class:`CIELUV`, :py:class:`CIELAB`, :py:class:`polarLUV`, and
3386 :py:class:`polarLAB`.
3388 Args:
3389 H (int, float, list, numpy.array):
3390 Numeric value(s) for Hue dimension.
3391 L (int, float, list, numpy.array):
3392 Numeric value(s) for Lightness dimension.
3393 S (int, float, list, numpy.array):
3394 Numeric value(s) for Saturation dimension.
3395 alpha (None, float, list, numpy.array): Numeric value(s) for the alpha
3396 channel (`[0., 1.]`) where `0.` equals full transparency, `1.` full
3397 opacity. If `None` (default) no transparency is added.
3399 Examples:
3401 >>> from colorspace import HLS
3402 >>> # Constructing color object with one single color via float
3403 >>> HLS(150, 0.1, 3)
3404 >>> #: Constructing object via lists
3405 >>> HLS([150, 0, 10], [0.1, 0.7, 0.1], [3, 0, 3])
3406 >>> #: Constructing object via numpy arrays
3407 >>> from numpy import asarray
3408 >>> cols = HLS(asarray([150, 0, 10]),
3409 >>> asarray([0.1, 0.7, 0.1]),
3410 >>> asarray([3, 0, 3]))
3411 >>> cols
3412 >>> #: Converting to RGB
3413 >>> cols.to("RGB")
3414 >>> cols
3415 """
3417 def __init__(self, H, L, S, alpha = None):
3419 # checking inputs, save inputs on object
3420 self._data_ = {} # Dict to store the colors/color dimensions
3421 tmp = self._colorobject_check_input_arrays_(H = H, L = L, S = S, alpha = None)
3422 for key,val in tmp.items(): self._data_[key] = val
3423 # White spot definition (the default)
3424 self.set_whitepoint(X = 95.047, Y = 100.000, Z = 108.883)
3427 def to(self, to, fixup = True):
3428 """Transform Color Space
3430 Allows to transform the current object into a different color space,
3431 if possible. Converting the colors of the current object into
3432 another color space. After calling this method, the object
3433 will be of a different class.
3435 Args:
3436 to (str): Name of the color space into which the colors should be
3437 converted (e.g., `"CIEXYZ"`, `"HCL"`, `"hex"`, `"sRGB"`, ...).
3438 fixup (bool): Whether or not colors outside the defined rgb color space
3439 should be corrected if necessary, defaults to `True`.
3441 Examples:
3443 >>> from colorspace import HLS
3444 >>> x = HLS([264, 314, 8, 44],
3445 >>> [0.34, 0.44, 0.68, 0.75],
3446 >>> [0.67, 0.71, 0.84, 0.62])
3447 >>> x
3448 >>> #:
3449 >>> type(x)
3450 >>> #: Convert colors to HSV
3451 >>> x.to("HSV")
3452 >>> x
3453 >>> #:
3454 >>> type(x)
3455 >>> #: Convert colors to HLS
3456 >>> x.to("HLS")
3457 >>> x
3458 >>> #: Extracting hex colors (returns list of str)
3459 >>> x.colors()
3461 """
3462 self._check_if_allowed_(to)
3463 from . import colorlib
3464 clib = colorlib()
3466 # Nothing to do (converted to itself)
3467 if to == self.__class__.__name__:
3468 return
3470 # The only transformation we need is back to RGB
3471 elif to == "sRGB":
3472 [R, G, B] = clib.HLS_to_sRGB(self.get("H"), self.get("L"), self.get("S"))
3473 self._data_ = {"R" : R, "G" : G, "B" : B, "alpha" : self.get("alpha")}
3474 self.__class__ = sRGB
3476 # From HSV to RGB: take direct path (not via sRGB)
3477 elif to in ["RGB"]:
3478 [R, G, B] = clib.HLS_to_RGB(self.get("H"), self.get("L"), self.get("S"))
3479 self._data_ = {"R" : R, "G" : G, "B" : B, "alpha" : self.get("alpha")}
3480 self.__class__ = RGB
3482 elif to == "hex":
3483 via = ["sRGB", to]
3484 self._transform_via_path_(via, fixup = fixup)
3486 elif to == "HSV":
3487 via = ["sRGB", to]
3488 self._transform_via_path_(via, fixup = fixup)
3490 elif to in ["CIEXYZ", "CIELUV", "CIELAB", "polarLUV", "HCL", "polarLAB"]:
3491 self._ambiguous(self.__class__.__name__, to)
3493 else: self._cannot(self.__class__.__name__, to)
3496class hexcols(colorobject):
3497 """Create Hex Color Object
3499 Creates a color object using hex colors (str).
3500 Can be converted to all other color spaces: :py:class:`CIELAB`,
3501 :py:class:`CIELUV`, :py:class:`CIEXYZ`, :py:class:`HLS`, :py:class:`HSV`,
3502 :py:class:`RGB`, :py:class:`polarLAB`, :py:class:`polarLUV`, and
3503 :py:class:`sRGB`.
3505 Args:
3506 hex_ (str, list of str, numpy.ndarray of type str):
3507 Hex colors. Only six and eight digit hex colors are allowed (e.g.,
3508 `#000000` or `#00000050` if with alpha channel). If invalid hex
3509 colors are provided the object will raise an exception. Invalid hex
3510 colors will be handled as `numpy.nan`.
3512 Examples:
3514 >>> from colorspace import hexcols
3515 >>> # Creating hex color object from string
3516 >>> hexcols("#cecece")
3517 >>> #: Creating hex color object from list of strings
3518 >>> hexcols(["#ff0000", "#00ff00"])
3519 >>> #: Creating hex colors via numpy array
3520 >>> from numpy import asarray
3521 >>> cols = hexcols(asarray(["#ff000030", "#00ff0030",
3522 >>> "#FFFFFF", "#000"]))
3523 >>> cols
3524 >>> #: Convert hex colors to another color space (CIEXYZ)
3525 >>> cols.to("CIEXYZ")
3526 >>> cols
3527 >>> #: Picking 7 hex colors from the Green-Orange
3528 >>> # diverging palette for demonstrating standard representation
3529 >>> # in jupyter engine and standard print.
3530 >>> from colorspace import diverging_hcl
3531 >>> cols2 = hexcols(diverging_hcl("Green-Orange")(7))
3532 >>> cols2 # jupyter HTML representation
3533 >>> #:
3534 >>> print(cols2) # default representation
3535 """
3537 def __init__(self, hex_):
3539 from colorspace import check_hex_colors
3540 import numpy as np
3542 # If hex_ is str, convert to list
3543 if isinstance(hex_, str): hex_ = [hex_]
3544 hex_ = check_hex_colors(hex_)
3546 self._data_ = {} # Dict to store the colors/color dimensions
3548 # This is the one step where we extract transparency from
3549 # hex colors once we enter the world of colorobjects.
3550 def get_alpha(hex_):
3551 # Trying to extract char 7:9, leave None if color is None
3552 hex_ = [None if (x is None or len(x) < 9) else x[7:9] for x in hex_]
3553 return [None if x is None else int(x, 16) / 255 for x in hex_]
3555 # Remove apha if any
3556 def remove_alpha(hex_):
3557 return [None if x is None else x[:7] if len(x) > 7 else x for x in hex_]
3559 # Forwarding input 'hex_' to check_hex_colors which will throw
3560 # an error if we do not understand this input type.
3561 tmp = np.asarray(get_alpha(hex_), dtype = "float")
3562 # Remove alpha from 9-digit hex if any, convert to ndarray
3563 self._data_["hex_"] = np.asarray(remove_alpha(hex_), dtype = object)
3564 # Store alpha (if any)
3565 if not np.all(np.isnan(tmp)): self._data_["alpha"] = tmp
3567 # White spot definition (the default)
3568 self.set_whitepoint(X = 95.047, Y = 100.000, Z = 108.883)
3571 def to(self, to, fixup = True):
3572 """Transform Color Space
3574 Allows to transform the current object into a different color space,
3575 if possible.
3577 Allows to transform the current object into a different color space,
3578 if possible. Converting the colors of the current object into
3579 another color space. After calling this method, the object
3580 will be of a different class.
3582 Args:
3583 to (str): Name of the color space into which the colors should be
3584 converted (e.g., `"CIEXYZ"`, `"HCL"`, `"HSL"`, `"sRGB"`, ...).
3585 fixup (bool): Whether or not colors outside the defined rgb color space
3586 should be corrected if necessary, defaults to `True`.
3588 Examples:
3590 >>> from colorspace import hexcols
3591 >>> x = hexcols(["#4B1D91", "#BF219A", "#F27B68", "#E7D39A"])
3592 >>> x
3593 >>> #:
3594 >>> type(x)
3595 >>> #: Convert colors to sRGB
3596 >>> x.to("sRGB")
3597 >>> x
3598 >>> #:
3599 >>> type(x)
3600 >>> #: Convert from sRGB to HCL
3601 >>> x.to("HCL")
3602 >>> x
3603 >>> #: Convert back to hex colors.
3604 >>> # Round-off errors due to conversion to 'hex'.
3605 >>> x.to("hex")
3606 >>> x
3608 """
3609 self._check_if_allowed_(to)
3610 from . import colorlib
3611 clib = colorlib()
3613 # Nothing to do (converted to itself)
3614 if to in ["hex", self.__class__.__name__]:
3615 return
3617 # The only transformation we need is from hexcols -> sRGB
3618 elif to == "sRGB":
3619 [R, G, B] = clib.hex_to_sRGB([None if x is None else x[0:7] for x in self.get("hex_")])
3620 alpha = self.get("alpha")
3621 self._data_ = {"R": R, "G": G, "B": B}
3622 if alpha is not None: self._data_["alpha"] = alpha
3623 self.__class__ = sRGB
3625 # The rest are transformations along a path
3626 elif to == "RGB":
3627 via = ["sRGB", to]
3628 self._transform_via_path_(via, fixup = fixup)
3630 elif to in ["HLS", "HSV"]:
3631 via = ["sRGB", to]
3632 self._transform_via_path_(via, fixup = fixup)
3634 elif to in ["CIEXYZ"]:
3635 via = ["sRGB", "RGB", to]
3636 self._transform_via_path_(via, fixup = fixup)
3638 elif to in ["CIELUV", "CIELAB"]:
3639 via = ["sRGB", "RGB", "CIEXYZ", to]
3640 self._transform_via_path_(via, fixup = fixup)
3642 elif to in ["HCL", "polarLUV"]:
3643 via = ["sRGB", "RGB", "CIEXYZ", "CIELUV", to]
3644 self._transform_via_path_(via, fixup = fixup)
3646 elif to in "polarLAB":
3647 via = ["sRGB", "RGB", "CIEXYZ", "CIELAB", to]
3648 self._transform_via_path_(via, fixup = fixup)
3650 else: self._cannot(self.__class__.__name__, to)
3652 def _repr_html_(self):
3653 """_repr_html_()
3655 Standard HTML representation of the object when using
3656 the jupyter engine. Will display the colors as html list,
3657 thanks to @matteoferla (github) for the idea and contribution.
3658 """
3659 from colorspace import contrast_ratio
3661 # ul style
3662 su = {"font-size": "0.5em", "list-style": "none", "display": "flex",
3663 "padding": "0 0 0.5em 0", "text-align": "center"}
3664 # li style
3665 sl = {"width": "5.75em", "height": "5.75em", "padding": "0.25em",
3666 "display": "inline-block", "margin": "0 0.25em 0 0",
3667 "border": "0.5px solid gray"}
3669 # Getting list of hex colors
3670 cols = self.colors()
3672 dict2style = lambda d: ';'.join(map(':'.join, d.items()))
3674 res = f"<ul class=\"colorspace-hexcols\" style=\"{dict2style(su)}\">\n"
3675 for i in range(len(self)):
3676 # Calculating contrast ratio to decide text color
3677 cw = contrast_ratio("#FFF", bg = cols[i])[0]
3678 cb = contrast_ratio("#000", bg = cols[i])[0]
3679 sl["color"] = "white" if cw > cb else "black"
3680 sl["background-color"] = cols[i]
3681 res += f"<li style=\"{dict2style(sl)}\">{cols[i]}</li>\n"
3683 res += "</ul>\n"
3684 return res
3686def compare_colors(a, b, exact = False, _all = True, atol = None):
3687 """Compare Sets of Colors
3689 Compares two sets of colors based on two color objects. The objects
3690 provided on argument `a` and `b` must inherit from `colorobject`.
3691 This can be any of the following classes: :py:class:`CIELAB`,
3692 :py:class:`CIELUV`, :py:class:`CIEXYZ`, :py:class:`HLS`, :py:class:`HSV`,
3693 :py:class:`RGB`, :py:class:`hexcols`, :py:class:`polarLAB`,
3694 :py:class:`polarLUV`, or :py:class:`sRGB`.
3696 Args:
3697 a (colorobject): Object which inherits from `colorobject`.
3698 b (colorobject): Object which inherits from `colorobject`.
3699 exact (bool): Default `False`, check for colors being nearly equal
3700 (see `atol`). If set to `True` the coordinates must be identical.
3701 Note: in case `a` and `b` are hex colors
3702 (colorspace.colorlib.hexcols) strings will always be matched exactly.
3703 _all (bool): Default `True`; the function will return `True` if
3704 all colors are identical/nearly equal. If set to `False` the return
3705 will be a list of bool containing `True` and `False` for each
3706 pair of colors.
3707 atol (None or float): Absolute tolerance for the distance measure
3708 between two colors to be considered as nearly equal (must be > 0 if set).
3709 Only used if `exact = False`, else `atol = 1e-6` is used. If set
3710 to `None` the tolerance will automatically be set depending on the
3711 type of the objects. Defaults to None.
3714 Returns:
3715 bool, list: Returns `True` if all colors of `a` are exactly equal or
3716 nearly equal (see arguments) to the colors in object `b`. If `_all =
3717 False`, a list of bool is returned indicating pair-wise comparison
3718 of all colors in `a` and `b`.
3720 Example:
3722 >>> from colorspace import RGB, hexcols, compare_colors
3723 >>>
3724 >>> # Three RGB colors
3725 >>> a = RGB([0.5, 0.5], [0.1, 0.1], [0.9, 0.9])
3726 >>> b = RGB([0.5, 0.5], [0.1, 0.1], [0.9, 0.91])
3727 >>>
3728 >>> compare_colors(a, b)
3729 >>> #:
3730 >>> compare_colors(a, b, atol = 0.1)
3731 >>> #:
3732 >>> compare_colors(a, b, exact = True)
3733 >>> #:
3734 >>> compare_colors(a, b, exact = True, _all = False)
3735 >>>
3736 >>> #: Same example using two sets of hexcolor objects
3737 >>> x = hexcols(["#ff00ff", "#003300"])
3738 >>> y = hexcols(["#ff00ff", "#003301"])
3739 >>> compare_colors(x, y)
3740 >>> #:
3741 >>> compare_colors(x, y, _all = False)
3742 >>>
3743 >>> #: Convert HEX to HCL (polarLUV) and back, compare the
3744 >>> # resulting colors to the original ones; should be identical
3745 >>> from copy import deepcopy
3746 >>> z = hexcols(["#ff00ff", "#003301"])
3747 >>> zz = deepcopy(z)
3748 >>> zz.to("HCL")
3749 >>> zz
3750 >>> #:
3751 >>> zz.to("hex")
3752 >>> zz
3753 >>> #:
3754 >>> compare_colors(z, zz)
3756 Raises:
3757 TypeError: If `a` or `b` are not objects of a class which inherits from
3758 `colorobject`.
3759 TypeError: If `a` and `b` are not of the same class.
3760 ValueError: If `a` and `b` are not of the same length, i.e., do not contain
3761 the same number of colors.
3762 TypeError: If `exact` or `_all` are not bool.
3763 TypeError: If `atol` is neither `None` nor float.
3764 ValueError: If `atol` is not larger than 0.
3765 """
3767 from numpy import sqrt, isclose
3769 if not isinstance(a, colorobject):
3770 raise TypeError("argument `a` must be an object based on colorspace.colorlib.colorobject")
3771 if not isinstance(b, colorobject):
3772 raise TypeError("argument `b` must be an object based on colorspace.colorlib.colorobject")
3773 if not type(a) == type(b):
3774 raise TypeError("Input `a` and `b` not of same type")
3775 if not a.length() == b.length():
3776 raise ValueError("Objects do not contain the same number of colors")
3777 if not isinstance(exact, bool):
3778 raise TypeError("argument `exact` must be bool")
3779 if not isinstance(_all, bool):
3780 raise TypeError("argument `_all` must be bool")
3781 if not isinstance(atol, float) and not isinstance(atol, type(None)):
3782 raise TypeError("argument `atol` must be float or None")
3783 if atol is not None and atol <= 0:
3784 raise ValueError("argument `atol` must be > 0.")
3786 if exact: atol = 1e-6
3788 def distance(a, b):
3789 dist = 0. # Start with zero distance
3790 for n in list(a._data_.keys()):
3791 tmpa = a.get(n)
3792 tmpb = b.get(n)
3793 # Both None and that is our alpha channel, no judgement
3794 if tmpa is None and tmpb is None and n == "alpha":
3795 continue
3796 # Bot not none, calc Eudlidean distance
3797 elif not tmpa is None and not tmpb is None:
3798 dist += (tmpa[0] - tmpb[0])**2.0
3799 # One missing? Penalize by + 100
3800 else:
3801 dist += 100.
3803 return sqrt(dist)
3805 # Compare hex colors; always on string level
3806 if isinstance(a, hexcols):
3807 # Getting coordinates
3808 res = [a[i].get("hex_")[0].upper() == b[i].get("hex_")[0].upper() for i in range(0, a.length())]
3809 # Calculate absolute difference between coordinates R/G/B[/alpha].
3810 # Threading alpha like another coordinates as all coordinates are scaled [0-1].
3811 elif isinstance(a, RGB) or isinstance(a, sRGB) or \
3812 isinstance(a, HLS) or isinstance(a, HSV):
3813 # HEX precision in RGB coordinates is about sqrt((1./255.)**2 * 3) / 2 = 0.003396178
3814 if not atol: atol = 0.005
3815 res = [distance(a[i], b[i]) for i in range(0, a.length())]
3816 res = isclose(res, 0, atol = atol)
3817 # HCL or polarLUV (both return instance polarLUV)
3818 # TODO(enhancement): Calculating the Euclidean distance on HCL and (if
3819 # available) alpha which itself is in [0, 1]. Should be weighted
3820 # differently (scaled distance)?
3821 elif isinstance(a, polarLUV) or isinstance(a, CIELUV) or isinstance(a, CIELAB) or \
3822 isinstance(a, polarLAB) or isinstance(a, CIEXYZ):
3824 if not atol: atol = 1
3825 res = [distance(a[i], b[i]) for i in range(0, a.length())]
3826 res = isclose(res, 0, atol = atol)
3829 # If _all is True: check if all elements are True
3830 if _all:
3831 from numpy import all
3832 res = all(res)
3833 return res