Coverage for src/colorspace/CVD.py: 97%

238 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-08-23 19:54 +0000

1# All matrices in this file are adapted from https://github.com/njsmith/colorspacious/blob/master/colorspacious/cvd.py 

2 

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. 

12 

13def deutan(cols, severity = 1., linear = True): 

14 """Simulate Color Vision Deficiency 

15 

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). 

20 

21 See also :py:func:`protan`, :py:func:`tritan`, :py:func:`desaturate`, and 

22 :py:func:`cvd_image <colorspace.cvd_image.cvd_image>`. 

23 

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). 

33 

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). 

37 

38 Example: 

39 

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 """ 

66 

67 from .CVD import CVD 

68 from numpy import ndarray 

69 

70 CVD = CVD(cols, "deutan", severity, linear) 

71 

72 # Create return 

73 res = CVD.colors() 

74 return res.tolist() if isinstance(res, ndarray) else res 

75 

76 

77def protan(cols, severity = 1., linear = True): 

78 """Simulate Color Vision Deficiency 

79 

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. 

83 

84 See also :py:func:`deutan`, :py:func:`tritan`, :py:func:`desaturate`, and 

85 :py:func:`cvd_image <colorspace.cvd_image.cvd_image>`. 

86 

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). 

95 

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). 

100 

101 Example: 

102 

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 """ 

129 

130 from .CVD import CVD 

131 from numpy import ndarray 

132 

133 CVD = CVD(cols, "protan", severity, linear) 

134 

135 # Create return 

136 res = CVD.colors() 

137 return res.tolist() if isinstance(res, ndarray) else res 

138 

139 

140def tritan(cols, severity = 1., linear = True): 

141 """Simulate Color Vision Deficiency 

142 

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. 

146 

147 See also :py:func:`deutan`, :py:func:`protan`, :py:func:`desaturate`, and 

148 :py:func:`cvd_image <colorspace.cvd_image.cvd_image>`. 

149 

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). 

159 

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). 

163 

164 Example: 

165 

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 """ 

192 

193 from .CVD import CVD 

194 from numpy import ndarray 

195 

196 CVD = CVD(cols, "tritan", severity, linear) 

197 

198 # Create return 

199 res = CVD.colors() 

200 return res.tolist() if isinstance(res, ndarray) else res 

201 

202 

203class CVD(object): 

204 """Simulate Color Vision Defficiency 

205 

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`. 

210 

211 No return values, initializes a new CVD object providing methods 

212 to manipulate the colors according to the color deficiency (`type_`). 

213 

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). 

225 

226 

227 Example: 

228 

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)); 

245 

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 """ 

253 

254 ALLOWED = ["protan", "tritan", "deutan"] 

255 CMAP = False 

256 CMAPINPUT = None 

257 

258 def __init__(self, cols, type_, severity = 1., linear = True): 

259 

260 from colorspace import palettes 

261 

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") 

269 

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)}") 

275 

276 self._type = type_.lower() 

277 self._severity = severity 

278 self._linear = linear 

279 

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 

289 

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] 

297 

298 # Default; overwritten if input was not hex (nor cmap) 

299 self._hexinput = True 

300 

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' 

310 

311 elif isinstance(cols, (str, list)): 

312 from .utils import check_hex_colors 

313 from .colorlib import hexcols 

314 

315 # Check/convert colors 

316 cols = check_hex_colors(cols) 

317 

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") 

326 

327 # Convert 

328 from copy import deepcopy 

329 self._colors_ = deepcopy(cols) 

330 

331 def _tomat(self, x): 

332 """Transformation/Rotation Matrix 

333 

334 Helper function to convert input `x` to a proper (3 x 3) 

335 `numpy.ndarray` (matrix). 

336 

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") 

343 

344 def protan_cvd_matrizes(self, s): 

345 """Protanope Transformation Matrix 

346 

347 Returns the transformation matrix to simulate 

348 protanope color vision deficiency. 

349 

350 Args: 

351 s (int): An int in `[0, 11]` to specify which matrix to be returned. 

352 

353 Returns: 

354 numpy.ndarray: Returns a numpy float matrix of shape `3 x 3`. 

355 The color deficiency transformation or rotation matrix. 

356 

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]") 

363 

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] 

378 

379 

380 def deutan_cvd_matrizes(self, s): 

381 """Deuteranope Transformation Matrix 

382 

383 Returns the transformation matrix to simulate 

384 deuteranope color vision deficiency. 

385 

386 Args: 

387 s (int): An int in `[0, 11]` to specify which matrix to be returned. 

388 

389 Returns: 

390 numpy.ndarray: Returns a numpy float matrix of shape `3 x 3`. 

391 The color deficiency transformation or rotation matrix. 

392 

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]") 

399 

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] 

414 

415 

416 # tritanomaly CVD 

417 def tritan_cvd_matrizes(self, s): 

418 """Tritanope Transformation Matrix 

419 

420 Returns the transformation matrix to simulate 

421 tritanope color vision deficiency. 

422 

423 Args: 

424 s (int): An int in `[0, 11]` to specify which matrix to be returned. 

425 

426 Returns: 

427 numpy.ndarray: Returns a numpy float matrix of shape `3 x 3`. 

428 The color deficiency transformation or rotation matrix. 

429 

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]") 

436 

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] 

451 

452 def _interpolate_cvd_transform(self): 

453 """Interpolate Transformation Matrices 

454 

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. 

459 

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 """ 

464 

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) 

481 

482 return cvd 

483 

484 def _simulate(self): 

485 """Perform Color Transformation 

486 

487 Performs the transformation of colors to simulate color 

488 vision deficiency. 

489 

490 Returns: 

491 list: Returns a list of hex colors (str). 

492 """ 

493 

494 

495 from copy import deepcopy 

496 cols = deepcopy(self._colors_) 

497 

498 from .colorlib import colorobject 

499 

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).") 

503 

504 # Convert to linear RGB or gamma-corrected sRGB 

505 if self._linear: 

506 cols.to("RGB") 

507 else: 

508 cols.to("sRGB") 

509 

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() 

514 

515 # Apply coefficients/CVD transformation matrix 

516 RGB = RGB.transpose().dot(CVD).transpose() 

517 

518 # Save simulated data 

519 cols.set(R = RGB[0], G = RGB[1], B = RGB[2]) 

520 

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) 

527 

528 def colors(self): 

529 """Get Color Object 

530 

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`. 

534 

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 """ 

540 

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") 

550 

551 r = cols.get("R") 

552 g = cols.get("G") 

553 b = cols.get("B") 

554 

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"]] 

560 

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]) 

565 

566 from matplotlib.colors import LinearSegmentedColormap 

567 cmap = LinearSegmentedColormap(cmap.name, sd, cmap.N) 

568 

569 return cmap 

570 

571 

572# ------------------------------------------------------------------- 

573# The desaturation function 

574# ------------------------------------------------------------------- 

575def desaturate(cols, amount = 1.): 

576 """Desaturate Colors by Chroma Removal in HCL Space 

577 

578 Transform a vector of given colors to the corresponding colors 

579 with chroma reduced (by a tunable amount) in HCL space. 

580 

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. 

584 

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>`. 

587 

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.`. 

594 

595 Returns: 

596 list: Returns a list of (modified) hex colors. 

597 

598 Example: 

599 

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 """ 

624 

625 

626 from .colorlib import colorobject 

627 from .palettes import palette 

628 from .colorlib import hexcols 

629 from copy import deepcopy 

630 

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.]`") 

637 

638 # If input is str, make list out of it 

639 if isinstance(cols, str): cols = [cols] 

640 

641 # Keep class of input object for later 

642 input_cols = deepcopy(cols) 

643 

644 # Convert palette object to list of hex colors 

645 if isinstance(cols, palette): cols = cols.colors() 

646 

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 

660 

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.") 

672 

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") 

676 

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 

683 

684 # Keep original class 

685 original_class = cols.__class__.__name__ 

686 original_class = "hex" if original_class == "hexcols" else original_class 

687 

688 from copy import deepcopy 

689 cols = deepcopy(cols) 

690 cols.to("HCL") 

691 

692 # Desaturation 

693 x = (1. - amount) * cols.get("C") 

694 cols.set(C = (1. - amount) * cols.get("C")) 

695 

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) 

702 

703 cols.to(original_class) 

704 

705 # If input was no matplotlib cmap 

706 if not CMAP: 

707 if original_class == "hex": cols = cols.colors() 

708 

709 from numpy import ndarray 

710 return cols.tolist() if isinstance(cols, ndarray) else cols 

711 

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") 

718 

719 # Get input cmap and manipulate colors 

720 cmap = CMAPINPUT 

721 sd = deepcopy(cmap._segmentdata) 

722 pos = [x[0] for x in sd["red"]] 

723 

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]) 

728 

729 from matplotlib.colors import LinearSegmentedColormap 

730 cmap = LinearSegmentedColormap(cmap.name, sd, cmap.N) 

731 

732 return cmap 

733 

734 

735 

736 

737