Coverage for src/colorspace/CVD.py: 97%
238 statements
« prev ^ index » next coverage.py v7.6.4, created at 2024-10-29 15:11 +0000
« prev ^ index » next coverage.py v7.6.4, created at 2024-10-29 15:11 +0000
1# All matrices in this file are adapted from https://github.com/njsmith/colorspacious/blob/master/colorspacious/cvd.py
3# Color Vision Deficiency (CVD) Conversion Functions.
4#
5# Conversion tables for simulating different types of color vision deficiency (CVD):
6# Protanomaly, deutanomaly, tritanomaly.
7#
8# Machado et al. (2009) have established a novel model, that allows to handle normal color
9# vision, anomalous trichromacy, and dichromacy in a unified way. They also provide conversion
10# formulas along with tables of certain constants that allow to simulate various types of
11# CVD. See \code{\link{simulate_cvd}} for the corresponding simulation functions.
13def deutan(cols, severity = 1., linear = True):
14 """Simulate Color Vision Deficiency
16 Transformation of colors by simulating color vision deficiencies, based on
17 a CVD transform matrix. This function is an interface to the CVD object and
18 returns simulated colors for deuteranope vision (green-yellow-red
19 weakness).
21 See also :py:func:`protan`, :py:func:`tritan`, :py:func:`desaturate`, and
22 :py:func:`cvd_image <colorspace.cvd_image.cvd_image>`.
24 Args:
25 cols (list, colorobject, matplotlib.colors.LinearSegmentedColormap):
26 Single hex color, list of hex colors (str), a matoplotlib cmap, or
27 a color color object (such as RGB, hexcols, CIELUV).
28 severity (float): Severity in `[0., 1.]`. Zero means no deficiency, one
29 maximum deficiency, defaults to `1.`.
30 linear (bool): Should the color vision deficiency transformation be applied to the
31 linearised RGB coordinates (default)? If `False`, the transformation is applied to the
32 gamma-corrected sRGB coordinates (as in the Machado et al. 2009 supplementary materials).
34 Returns:
35 colorobject: Returns an object of the same type as the input object `cols` with
36 modified colors as people with deuteranomaly see these colors (simulated).
38 Example:
40 >>> from colorspace import rainbow_hcl, deutan, palette
41 >>> from colorspace import specplot, swatchplot
42 >>>
43 >>> # Drawing 100 colors along the HCL rainbow color palette
44 >>> cols = rainbow_hcl()(100)
45 >>> specplot(cols);
46 >>> #:
47 >>> specplot(deutan(cols));
48 >>> #:
49 >>> specplot(deutan(cols, 0.5));
50 >>>
51 >>> #: List of (hex) colors
52 >>> cols = ["magenta", "red", "orange", "#F2F204", "#6BF204", "#4DA00D"]
53 >>> deutan(cols);
54 >>>
55 >>> #: Visualize original and simulated color swatches
56 >>> swatchplot([cols, deutan(cols)],
57 >>> show_names = False, figsize = (5, 1.5));
58 >>>
59 >>> #: From palette object
60 >>> pal = palette(cols, name = "custom palette")
61 >>> deutan(pal)
62 >>>
63 >>> #: From cmap (returns cmap)
64 >>> deutan(pal.cmap())
65 """
67 from .CVD import CVD
68 from numpy import ndarray
70 CVD = CVD(cols, "deutan", severity, linear)
72 # Create return
73 res = CVD.colors()
74 return res.tolist() if isinstance(res, ndarray) else res
77def protan(cols, severity = 1., linear = True):
78 """Simulate Color Vision Deficiency
80 Transformation of colors by simulating color vision deficiencies, based on
81 a CVD transform matrix. This function is an interface to the CVD object and
82 returns simulated colors for protanope vision.
84 See also :py:func:`deutan`, :py:func:`tritan`, :py:func:`desaturate`, and
85 :py:func:`cvd_image <colorspace.cvd_image.cvd_image>`.
87 Args:
88 cols (list, colorobject, matplotlib.colors.LinearSegmentedColormap): A list of valid hex colors (str)
89 or a colorobject (such as RGB, HCL, CIEXYZ).
90 severity (float): Severity in `[0., 1.]`. Zero means no deficiency, one
91 maximum deficiency, defaults to `1.`.
92 linear (bool): Should the color vision deficiency transformation be applied to the
93 linearised RGB coordinates (default)? If `False`, the transformation is applied to the
94 gamma-corrected sRGB coordinates (as in the Machado et al. 2009 supplementary materials).
96 Returns:
97 colorobject: Returns an object of the same type as the input object
98 `cols` with modified colors as people with protanope color vision
99 might see the colors (simulated).
101 Example:
103 >>> from colorspace import rainbow_hcl, protan, palette
104 >>> from colorspace import specplot, swatchplot
105 >>>
106 >>> # Drawing 100 colors along the HCL rainbow color palette
107 >>> cols = rainbow_hcl()(100)
108 >>> specplot(cols);
109 >>> #:
110 >>> specplot(protan(cols));
111 >>> #:
112 >>> specplot(protan(cols, 0.5));
113 >>>
114 >>> #: List of (hex) colors
115 >>> cols = ["magenta", "red", "orange", "#F2F204", "#6BF204", "#4DA00D"]
116 >>> protan(cols);
117 >>>
118 >>> #: Visualize original and simulated color swatches
119 >>> swatchplot([cols, protan(cols)],
120 >>> show_names = False, figsize = (5, 1.5));
121 >>>
122 >>> #: From palette object
123 >>> pal = palette(cols, name = "custom palette")
124 >>> protan(pal)
125 >>>
126 >>> #: From cmap (returns cmap)
127 >>> protan(pal.cmap())
128 """
130 from .CVD import CVD
131 from numpy import ndarray
133 CVD = CVD(cols, "protan", severity, linear)
135 # Create return
136 res = CVD.colors()
137 return res.tolist() if isinstance(res, ndarray) else res
140def tritan(cols, severity = 1., linear = True):
141 """Simulate Color Vision Deficiency
143 Transformation of R colors by simulating color vision deficiencies, based
144 on a CVD transform matrix. This function is an interface to the CVD object
145 and returns simulated colors for tritanope vision.
147 See also :py:func:`deutan`, :py:func:`protan`, :py:func:`desaturate`, and
148 :py:func:`cvd_image <colorspace.cvd_image.cvd_image>`.
150 Args:
151 cols (list, colorobject, matplotlib.colors.LinearSegmentedColormap):
152 Single hex color, list of hex colors (str), a matoplotlib cmap, or
153 a color color object (such as RGB, hexcols, CIELUV).
154 severity (float): Severity in `[0., 1.]`. Zero means no deficiency,
155 one maximum deficiency, defaults to `1.`.
156 linear (bool): Should the color vision deficiency transformation be applied to the
157 linearised RGB coordinates (default)? If `False`, the transformation is applied to the
158 gamma-corrected sRGB coordinates (as in the Machado et al. 2009 supplementary materials).
160 Returns:
161 colorobject: Returns an object of the same type as the input object `cols` with
162 modified colors as people with tritanomaly see these colors (simulated).
164 Example:
166 >>> from colorspace import rainbow_hcl, tritan, palette
167 >>> from colorspace import specplot, swatchplot
168 >>>
169 >>> # Drawing 100 colors along the HCL rainbow color palette
170 >>> cols = rainbow_hcl()(100)
171 >>> specplot(cols);
172 >>> #:
173 >>> specplot(tritan(cols));
174 >>> #:
175 >>> specplot(tritan(cols, 0.5));
176 >>>
177 >>> #: List of (hex) colors
178 >>> cols = ["magenta", "red", "orange", "#F2F204", "#6BF204", "#4DA00D"]
179 >>> tritan(cols);
180 >>>
181 >>> #: Visualize original and simulated color swatches
182 >>> swatchplot([cols, tritan(cols)],
183 >>> show_names = False, figsize = (5, 1.5));
184 >>>
185 >>> #: From palette object
186 >>> pal = palette(cols, name = "custom palette")
187 >>> tritan(pal)
188 >>>
189 >>> #: From cmap (returns cmap)
190 >>> tritan(pal.cmap())
191 """
193 from .CVD import CVD
194 from numpy import ndarray
196 CVD = CVD(cols, "tritan", severity, linear)
198 # Create return
199 res = CVD.colors()
200 return res.tolist() if isinstance(res, ndarray) else res
203class CVD(object):
204 """Simulate Color Vision Defficiency
206 Class simulating color vision deficiencies (CVD)
207 for protanope, deteranope, and tritanope visual constraints.
208 End-users are advised to use the convenience functions
209 :py:func:`deutan`, :py:func:`protan`, and :py:func:`tritan`.
211 No return values, initializes a new CVD object providing methods
212 to manipulate the colors according to the color deficiency (`type_`).
214 Args:
215 cols (list, colorobject, matplotlib.colors.LinearSegmentedColormap):
216 Single hex color, list of hex colors (str), a matoplotlib cmap, or
217 a color color object (such as RGB, hexcols, CIELUV).
218 type_ (str): Type of the deficiency which should be simulated; one
219 of `"deutan"`, `"protan"`, and `"tritan"`
220 severity (float): Severity in `[0., 1.]`. Zero means no deficiency,
221 one maximum deficiency, defaults to 1.0.
222 linear (bool): Should the color vision deficiency transformation be applied to the
223 linearised RGB coordinates (default)? If `False`, the transformation is applied to the
224 gamma-corrected sRGB coordinates (as in the Machado et al. 2009 supplementary materials).
227 Example:
229 >>> from colorspace import rainbow_hcl
230 >>> cols = rainbow_hcl()(10)
231 >>>
232 >>> # Modify colors by emulating color vision deficiency
233 >>> from colorspace.CVD import CVD
234 >>> deut = CVD(cols, "deutan")
235 >>> prot = CVD(cols, "protan")
236 >>> trit = CVD(cols, "tritan")
237 >>>
238 >>> # Spectrum plots of modified colors
239 >>> from colorspace import specplot
240 >>> specplot(deut.colors(), figsize = (7, 0.5));
241 >>> #:
242 >>> specplot(prot.colors(), figsize = (7, 0.5));
243 >>> #:
244 >>> specplot(trit.colors(), figsize = (7, 0.5));
246 Raises:
247 TypeError: If argument `type_` not str.
248 ValueError: If argument `type_` not among the allowed types. Not case sensitive.
249 TypeError: If argument `severity` is no float or int.
250 ValueError: If argument `severity` not in `[0., 1.]`.
251 TypeError: If argument `linear` is no bool.
252 """
254 ALLOWED = ["protan", "tritan", "deutan"]
255 CMAP = False
256 CMAPINPUT = None
258 def __init__(self, cols, type_, severity = 1., linear = True):
260 from colorspace import palettes
262 if not isinstance(severity, (int, float)):
263 raise TypeError("argument `severity` must be float (`[0., 1.]`) or int (`[0, 1]`)")
264 elif isinstance(severity, int): severity = float(severity)
265 if severity < 0. or severity > 1.:
266 raise ValueError("argument `severity` must be in `[0., 1.]`")
267 if not isinstance(linear, bool):
268 raise TypeError("argument `linear` must be bool")
270 # Checking type
271 if not isinstance(type_, str):
272 raise TypeError("argument `type_` must be str.")
273 if not type_.lower() in self.ALLOWED:
274 raise ValueError(f"argument `type_` wrong, has to be one of {', '.join(self.ALLOWED)}")
276 self._type = type_.lower()
277 self._severity = severity
278 self._linear = linear
280 # Check if we have a matplotlib.cmap
281 try:
282 from matplotlib.colors import LinearSegmentedColormap
283 if isinstance(cols, LinearSegmentedColormap):
284 from copy import copy
285 self.CMAP = True
286 self.CMAPINPUT = copy(cols)
287 except:
288 pass
290 # If the input is a palettes.palette: extract colors and
291 # store it in a list. Will then be handled further down as 'list' object.
292 if isinstance(cols, palettes.palette):
293 cols = cols.colors()
294 # Single hex string to list
295 elif isinstance(cols, str):
296 cols = [cols]
298 # Default; overwritten if input was not hex (nor cmap)
299 self._hexinput = True
301 # Checking input `cols`:
302 # If cmap (matplotlib LinearSegmentedColormap: Convert to sRGB
303 if self.CMAP:
304 # Create an sRGB object
305 from .colorlib import sRGB
306 cols = sRGB(R = [x[1] for x in cols._segmentdata["red"]],
307 G = [x[1] for x in cols._segmentdata["green"]],
308 B = [x[1] for x in cols._segmentdata["blue"]])
309 cols.to("hex") # Faking 'hex input'
311 elif isinstance(cols, (str, list)):
312 from .utils import check_hex_colors
313 from .colorlib import hexcols
315 # Check/convert colors
316 cols = check_hex_colors(cols)
318 # Internally: create a hexcols object; will return hex colors
319 # when calling .colors() method
320 cols = hexcols(cols)
321 else:
322 self._hexinput = False
323 from .colorlib import colorobject
324 if not isinstance(cols, colorobject):
325 raise TypeError("argument `cols` does not match any of the allowed types")
327 # Convert
328 from copy import deepcopy
329 self._colors_ = deepcopy(cols)
331 def _tomat(self, x):
332 """Transformation/Rotation Matrix
334 Helper function to convert input `x` to a proper (3 x 3)
335 `numpy.ndarray` (matrix).
337 Returns:
338 numpy.ndarray: Returns a numpy float matrix of shape `3 x 3`.
339 The color deficiency transformation or rotation matrix.
340 """
341 from numpy import reshape, asarray
342 return asarray(x, dtype = float).reshape((3,3), order = "F")
344 def protan_cvd_matrizes(self, s):
345 """Protanope Transformation Matrix
347 Returns the transformation matrix to simulate
348 protanope color vision deficiency.
350 Args:
351 s (int): An int in `[0, 11]` to specify which matrix to be returned.
353 Returns:
354 numpy.ndarray: Returns a numpy float matrix of shape `3 x 3`.
355 The color deficiency transformation or rotation matrix.
357 Raises:
358 TypeError: If argument `s` is no int.
359 ValueError: If argument `s` is not in `[0, 11]`.
360 """
361 if not isinstance(s, int): raise TypeError("argument `s` must be int")
362 elif s < 0 or s > 11: raise ValueError("argument `s` must be in [0, 11]")
364 # Protanope CDV transformation matrix definition
365 x = []
366 x.append(self._tomat(( 1.000000, 0.000000, -0.000000, 0.000000, 1.000000, 0.000000, -0.000000, -0.000000, 1.000000)))
367 x.append(self._tomat(( 0.856167, 0.182038, -0.038205, 0.029342, 0.955115, 0.015544, -0.002880, -0.001563, 1.004443)))
368 x.append(self._tomat(( 0.734766, 0.334872, -0.069637, 0.051840, 0.919198, 0.028963, -0.004928, -0.004209, 1.009137)))
369 x.append(self._tomat(( 0.630323, 0.465641, -0.095964, 0.069181, 0.890046, 0.040773, -0.006308, -0.007724, 1.014032)))
370 x.append(self._tomat(( 0.539009, 0.579343, -0.118352, 0.082546, 0.866121, 0.051332, -0.007136, -0.011959, 1.019095)))
371 x.append(self._tomat(( 0.458064, 0.679578, -0.137642, 0.092785, 0.846313, 0.060902, -0.007494, -0.016807, 1.024301)))
372 x.append(self._tomat(( 0.385450, 0.769005, -0.154455, 0.100526, 0.829802, 0.069673, -0.007442, -0.022190, 1.029632)))
373 x.append(self._tomat(( 0.319627, 0.849633, -0.169261, 0.106241, 0.815969, 0.077790, -0.007025, -0.028051, 1.035076)))
374 x.append(self._tomat(( 0.259411, 0.923008, -0.182420, 0.110296, 0.804340, 0.085364, -0.006276, -0.034346, 1.040622)))
375 x.append(self._tomat(( 0.203876, 0.990338, -0.194214, 0.112975, 0.794542, 0.092483, -0.005222, -0.041043, 1.046265)))
376 x.append(self._tomat(( 0.152286, 1.052583, -0.204868, 0.114503, 0.786281, 0.099216, -0.003882, -0.048116, 1.051998)))
377 return x[s]
380 def deutan_cvd_matrizes(self, s):
381 """Deuteranope Transformation Matrix
383 Returns the transformation matrix to simulate
384 deuteranope color vision deficiency.
386 Args:
387 s (int): An int in `[0, 11]` to specify which matrix to be returned.
389 Returns:
390 numpy.ndarray: Returns a numpy float matrix of shape `3 x 3`.
391 The color deficiency transformation or rotation matrix.
393 Raises:
394 TypeError: If argument `s` is no int.
395 ValueError: If argument `s` is not in `[0, 11]`.
396 """
397 if not isinstance(s, int): raise TypeError("argument `s` must be int")
398 elif s < 0 or s > 11: raise ValueError("argument `s` must be in [0, 11]")
400 # Deuteranope CDV transformation matrix definition
401 x = []
402 x.append(self._tomat(( 1.000000, 0.000000, -0.000000, 0.000000, 1.000000, 0.000000, -0.000000, -0.000000, 1.000000)))
403 x.append(self._tomat(( 0.866435, 0.177704, -0.044139, 0.049567, 0.939063, 0.011370, -0.003453, 0.007233, 0.996220)))
404 x.append(self._tomat(( 0.760729, 0.319078, -0.079807, 0.090568, 0.889315, 0.020117, -0.006027, 0.013325, 0.992702)))
405 x.append(self._tomat(( 0.675425, 0.433850, -0.109275, 0.125303, 0.847755, 0.026942, -0.007950, 0.018572, 0.989378)))
406 x.append(self._tomat(( 0.605511, 0.528560, -0.134071, 0.155318, 0.812366, 0.032316, -0.009376, 0.023176, 0.986200)))
407 x.append(self._tomat(( 0.547494, 0.607765, -0.155259, 0.181692, 0.781742, 0.036566, -0.010410, 0.027275, 0.983136)))
408 x.append(self._tomat(( 0.498864, 0.674741, -0.173604, 0.205199, 0.754872, 0.039929, -0.011131, 0.030969, 0.980162)))
409 x.append(self._tomat(( 0.457771, 0.731899, -0.189670, 0.226409, 0.731012, 0.042579, -0.011595, 0.034333, 0.977261)))
410 x.append(self._tomat(( 0.422823, 0.781057, -0.203881, 0.245752, 0.709602, 0.044646, -0.011843, 0.037423, 0.974421)))
411 x.append(self._tomat(( 0.392952, 0.823610, -0.216562, 0.263559, 0.690210, 0.046232, -0.011910, 0.040281, 0.971630)))
412 x.append(self._tomat(( 0.367322, 0.860646, -0.227968, 0.280085, 0.672501, 0.047413, -0.011820, 0.042940, 0.968881)))
413 return x[s]
416 # tritanomaly CVD
417 def tritan_cvd_matrizes(self, s):
418 """Tritanope Transformation Matrix
420 Returns the transformation matrix to simulate
421 tritanope color vision deficiency.
423 Args:
424 s (int): An int in `[0, 11]` to specify which matrix to be returned.
426 Returns:
427 numpy.ndarray: Returns a numpy float matrix of shape `3 x 3`.
428 The color deficiency transformation or rotation matrix.
430 Raises:
431 TypeError: If argument `s` is no int.
432 ValueError: If argument `s` is not in `[0, 11]`.
433 """
434 if not isinstance(s, int): raise TypeError("argument `s` must be int")
435 elif s < 0 or s > 11: raise ValueError("argument `s` must be in [0, 11]")
437 # Tritanope CDV transformation matrix definition
438 x = []
439 x.append(self._tomat(( 1.000000, 0.000000, -0.000000, 0.000000, 1.000000, 0.000000, -0.000000, -0.000000, 1.000000)))
440 x.append(self._tomat(( 0.926670, 0.092514, -0.019184, 0.021191, 0.964503, 0.014306, 0.008437, 0.054813, 0.936750)))
441 x.append(self._tomat(( 0.895720, 0.133330, -0.029050, 0.029997, 0.945400, 0.024603, 0.013027, 0.104707, 0.882266)))
442 x.append(self._tomat(( 0.905871, 0.127791, -0.033662, 0.026856, 0.941251, 0.031893, 0.013410, 0.148296, 0.838294)))
443 x.append(self._tomat(( 0.948035, 0.089490, -0.037526, 0.014364, 0.946792, 0.038844, 0.010853, 0.193991, 0.795156)))
444 x.append(self._tomat(( 1.017277, 0.027029, -0.044306, -0.006113, 0.958479, 0.047634, 0.006379, 0.248708, 0.744913)))
445 x.append(self._tomat(( 1.104996, -0.046633, -0.058363, -0.032137, 0.971635, 0.060503, 0.001336, 0.317922, 0.680742)))
446 x.append(self._tomat(( 1.193214, -0.109812, -0.083402, -0.058496, 0.979410, 0.079086, -0.002346, 0.403492, 0.598854)))
447 x.append(self._tomat(( 1.257728, -0.139648, -0.118081, -0.078003, 0.975409, 0.102594, -0.003316, 0.501214, 0.502102)))
448 x.append(self._tomat(( 1.278864, -0.125333, -0.153531, -0.084748, 0.957674, 0.127074, -0.000989, 0.601151, 0.399838)))
449 x.append(self._tomat(( 1.255528, -0.076749, -0.178779, -0.078411, 0.930809, 0.147602, 0.004733, 0.691367, 0.303900)))
450 return x[s]
452 def _interpolate_cvd_transform(self):
453 """Interpolate Transformation Matrices
455 The package provides 12 transformation matrices for deuteranope,
456 protanope, and tritanope color vision deficiencies. To allow for
457 more gradual changes, these matrices are linearly interpolated
458 depending on the severity requested, performed by this method.
460 Returns:
461 numpy.ndarray: Returns a numpy float matrix of shape `3 x 3`.
462 The interpolated color deficiency transformation or rotation matrix.
463 """
465 # Getting severity
466 fun = getattr(self, f"{self._type.lower()}_cvd_matrizes")
467 severity = self._severity
468 if severity <= 0.:
469 cvd = fun(0)
470 elif severity >= 1.:
471 cvd = fun(10)
472 else:
473 from numpy import floor, ceil
474 lo = int(floor(severity * 10.))
475 hi = int(ceil(severity * 10.))
476 if lo == hi:
477 cvd = fun(lo)
478 else:
479 cvd = (hi - severity * 10.) * fun(lo) + \
480 (severity * 10. - lo) * fun(hi)
482 return cvd
484 def _simulate(self):
485 """Perform Color Transformation
487 Performs the transformation of colors to simulate color
488 vision deficiency.
490 Returns:
491 list: Returns a list of hex colors (str).
492 """
495 from copy import deepcopy
496 cols = deepcopy(self._colors_)
498 from .colorlib import colorobject
500 if not isinstance(cols, colorobject):
501 raise ValueError("input cols to {:s}".format(self.__class__.__name__) + \
502 "has to be a colorobject (e.g., CIELAB, RGB, hexcols).")
504 # Convert to linear RGB or gamma-corrected sRGB
505 if self._linear:
506 cols.to("RGB")
507 else:
508 cols.to("sRGB")
510 # Transform color
511 from numpy import dot, vstack
512 RGB = vstack([cols.get("R"), cols.get("G"), cols.get("B")])
513 CVD = self._interpolate_cvd_transform()
515 # Apply coefficients/CVD transformation matrix
516 RGB = RGB.transpose().dot(CVD).transpose()
518 # Save simulated data
519 cols.set(R = RGB[0], G = RGB[1], B = RGB[2])
521 # User provided hex colors?
522 from copy import copy
523 if self._hexinput:
524 return copy(cols.colors())
525 else:
526 return copy(cols)
528 def colors(self):
529 """Get Color Object
531 Allows to extract the modified colors (simulated color vision deficiency)
532 to be used otherwise. The return is a color object of the same class
533 as the original input to `CVD`.
535 Returns:
536 colorobject, matplotlib.colors.LinearSegmentedColormap: Returns
537 the colors of the object with simulated colors for the color vision
538 deficiency as specified when initializing the object.
539 """
541 # If input was no matplotlib cmap
542 if not self.CMAP:
543 return self._simulate()
544 # Else simulate and re-create the colormap
545 else:
546 # We converted the cmap rgbs to hex, now revert this
547 from .colorlib import hexcols
548 cols = hexcols(self._simulate())
549 cols.to("sRGB")
551 r = cols.get("R")
552 g = cols.get("G")
553 b = cols.get("B")
555 # Get input cmap and manipulate colors
556 from copy import deepcopy
557 cmap = self.CMAPINPUT
558 sd = deepcopy(cmap._segmentdata)
559 pos = [x[0] for x in sd["red"]]
561 for i in range(len(sd["red"])):
562 sd["red"][i] = (pos[i], r[i], r[i])
563 sd["green"][i] = (pos[i], g[i], g[i])
564 sd["blue"][i] = (pos[i], b[i], b[i])
566 from matplotlib.colors import LinearSegmentedColormap
567 cmap = LinearSegmentedColormap(cmap.name, sd, cmap.N)
569 return cmap
572# -------------------------------------------------------------------
573# The desaturation function
574# -------------------------------------------------------------------
575def desaturate(cols, amount = 1.):
576 """Desaturate Colors by Chroma Removal in HCL Space
578 Transform a vector of given colors to the corresponding colors
579 with chroma reduced (by a tunable amount) in HCL space.
581 The color object (`col`) is transformed to the HCL color
582 space where the chroma is reduced, before converted back to the original
583 color space.
585 See also: :py:func:`deutan`, :py:func:`protan`, :py:func:`tritan`,
586 :py:func:`desaturate`, and :py:func:`cvd_image <colorspace.cvd_image.cvd_image>`.
588 Args:
589 cols (str, list, matplotlib.colors.LinearSegmentedColormap, colorobject):
590 Single hex color, list of hex colors (str), a matoplotlib cmap, or
591 a color color object (such as RGB, hexcols, CIELUV).
592 amount (float): A value in `[0.,1.]` defining the degree of desaturation.
593 `amount = 1.` removes all color, `amount = 0.` none, defaults to `1.`.
595 Returns:
596 list: Returns a list of (modified) hex colors.
598 Example:
600 >>> from colorspace import palette, diverging_hcl, desaturate
601 >>> from colorspace import specplot, swatchplot
602 >>> from colorspace.colorlib import hexcols
603 >>>
604 >>> cols = hexcols(diverging_hcl()(10))
605 >>> specplot(desaturate(cols));
606 >>> #:
607 >>> specplot(desaturate(cols, 0.5));
608 >>>
609 >>> #: Take a list of colors which can be interpreted/translated to hex
610 >>> # colors and desaturate them via the HCL color space
611 >>> cols = ["magenta", "red", "orange", "#F2F204", "#6BF204", "#4DA00D"]
612 >>> desaturate(cols)
613 >>> #:
614 >>> swatchplot([cols, desaturate(cols)],
615 >>> show_names = False, figsize = (5, 1.5));
616 >>>
617 >>> #: Desaturate palette object (same colors as above)
618 >>> pal = palette(cols, name = "custom palette")
619 >>> desaturate(pal)
620 >>>
621 >>> #: Desaturate a matplotlib cmap object
622 >>> desaturate(pal.cmap())
623 """
626 from .colorlib import colorobject
627 from .palettes import palette
628 from .colorlib import hexcols
629 from copy import deepcopy
631 # Sanity checks
632 if not isinstance(amount, (float, int)):
633 raise TypeError("argument `amount` must be float or int")
634 elif isinstance(amount, int): amount = float(amount)
635 if amount < 0. or amount > 1.:
636 raise ValueError("argument `amount` must be in `[0., 1.]`")
638 # If input is str, make list out of it
639 if isinstance(cols, str): cols = [cols]
641 # Keep class of input object for later
642 input_cols = deepcopy(cols)
644 # Convert palette object to list of hex colors
645 if isinstance(cols, palette): cols = cols.colors()
647 # Check if we have a matplotlib.cmap
648 try:
649 from matplotlib.colors import LinearSegmentedColormap, ListedColormap
650 if isinstance(cols, (LinearSegmentedColormap, ListedColormap)):
651 from copy import copy
652 CMAP = True
653 CMAPINPUT = copy(cols)
654 else:
655 CMAP = False
656 CMAPINPUT = copy(cols)
657 except:
658 CMAP = False
659 CMAPINPUT = None
661 # If input is a matploblib cmap: convert to sRGB
662 if CMAP:
663 # Create an sRGB object
664 from .cmap import cmap_to_sRGB
665 cols = cmap_to_sRGB(cols)
666 # If we have hex color input: convert to colorspace.colorlib.hexcols
667 elif isinstance(cols, list) or isinstance(cols, str):
668 cols = hexcols(cols)
669 elif not isinstance(cols, colorobject):
670 import inspect
671 raise TypeError(f"argument `cols` to {inspect.stack()[0][3]} not among the allowed types.")
673 # From here on "col" needs to be a colorspace.colorlib.colorobject
674 if not isinstance(cols, colorobject):
675 raise Exception("internal error; `cols` should be a colorobject by now but is not")
677 # Checking amount
678 if amount == 0.:
679 if not CMAP:
680 return input_cols if isinstance(input_cols, (str, list)) else input_cols.colors()
681 else:
682 return input_cols # CMAP
684 # Keep original class
685 original_class = cols.__class__.__name__
686 original_class = "hex" if original_class == "hexcols" else original_class
688 from copy import deepcopy
689 cols = deepcopy(cols)
690 cols.to("HCL")
692 # Desaturation
693 x = (1. - amount) * cols.get("C")
694 cols.set(C = (1. - amount) * cols.get("C"))
696 from numpy import where, logical_or
697 idx = where(logical_or(cols.get("L") <= 0, cols.get("L") >= 100))[0]
698 if len(idx) > 0:
699 C = cols.get("C"); C[idx] = 0
700 H = cols.get("H"); H[idx] = 0
701 cols.set(C = C, H = H)
703 cols.to(original_class)
705 # If input was no matplotlib cmap
706 if not CMAP:
707 if original_class == "hex": cols = cols.colors()
709 from numpy import ndarray
710 return cols.tolist() if isinstance(cols, ndarray) else cols
712 # Else manipulate the original cmap object and return
713 # a new cmap object with adjusted colors
714 else:
715 r = cols.get("R")
716 g = cols.get("G")
717 b = cols.get("B")
719 # Get input cmap and manipulate colors
720 cmap = CMAPINPUT
721 sd = deepcopy(cmap._segmentdata)
722 pos = [x[0] for x in sd["red"]]
724 for i in range(len(sd["red"])):
725 sd["red"][i] = (pos[i], r[i], r[i])
726 sd["green"][i] = (pos[i], g[i], g[i])
727 sd["blue"][i] = (pos[i], b[i], b[i])
729 from matplotlib.colors import LinearSegmentedColormap
730 cmap = LinearSegmentedColormap(cmap.name, sd, cmap.N)
732 return cmap