skinoptics 0.0.1b9__py3-none-any.whl → 0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
skinoptics/colors.py CHANGED
@@ -1,1403 +1,1543 @@
1
- '''
2
- | SkinOptics
3
- | Copyright (C) 2024-2025 Victor Lima
4
-
5
- | This program is free software: you can redistribute it and/or modify
6
- | it under the terms of the GNU General Public License as published by
7
- | the Free Software Foundation, either version 3 of the License, or
8
- | (at your option) any later version.
9
-
10
- | This program is distributed in the hope that it will be useful,
11
- | but WITHOUT ANY WARRANTY; without even the implied warranty of
12
- | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
- | GNU General Public License for more details.
14
-
15
- | You should have received a copy of the GNU General Public License
16
- | along with this program. If not, see <https://www.gnu.org/licenses/>.
17
-
18
- | Victor Lima
19
- | victorporto\@ifsc.usp.br
20
- | victor.lima\@ufscar.br
21
- | victorportog.github.io
22
-
23
- | Release date:
24
- | October 2024
25
- | Last modification:
26
- | October 2024
27
-
28
- | References:
29
-
30
- | [CCH91] Chardon, Cretois & Hourseau 1991.
31
- | Skin colour typology and suntanning pathways.
32
- | https://doi.org/10.1111/j.1467-2494.1991.tb00561.x
33
-
34
- | [T*94] Takiwaki, Shirai, Kanno, Watanabe & Arase 1994.
35
- | Quantification of erythema and pigmentation using a videomicroscope and a computer.
36
- | https://doi.org/10.1111/j.1365-2133.1994.tb08462.x
37
-
38
- | [F*96] Fullerton, Fischer, Lahti, Wilhelm, Takiwaki & Serup 1996.
39
- | Guidetines for measurement of skin colour and erythema: A report from the Standardization Group of the European Society of Contact Dermatitis.
40
- | https://doi.org/10.1111/j.1600-0536.1996.tb02258.x
41
-
42
- | [S*96] Stokes, Anderson, Chandrasekar & Motta 1996.
43
- | A Standard Default Color Space for the Internet - sRGB.
44
- | https://www.w3.org/Graphics/Color/sRGB.html
45
-
46
- | [IEC99] IEC 1999.
47
- | Multimedia systems and equipment - Colour measurement and management - Part 2-1: Colour management - Default RGB colour space - sRGB.
48
- | IEC 61966-2-1:1999
49
-
50
- | [CIE04] CIE 2004.
51
- | Colorimetry, 3rd edition.
52
- | CIE 15:2004
53
-
54
- | [D*06] Del Bino, Sok, Bessac & Bernerd 2006.
55
- | Relationship between skin response to ultraviolet exposure and skin color type.
56
- | https://doi.org/10.1111/j.1600-0749.2006.00338.x
57
-
58
- | [S07] Schanda (editor) 2007.
59
- | Colorimetry: Understanding the CIE System.
60
- | http://dx.doi.org/10.1002/9780470175637
61
-
62
- | [HP11] Hunt & Pointer 2011.
63
- | Measuring Colour.
64
- | https://doi.org/10.1002/9781119975595
65
-
66
- | [DB13] Del Bino & Bernerd 2013.
67
- | Variations in skin colour and the biological consequences of ultraviolet radiation exposure.
68
- | https://doi.org/10.1111/bjd.12529
69
-
70
- | [WSS13] Wyman, Sloan & Shirley 2013.
71
- | Simple Analytic Approximations to the CIE XYZ Color Matching Functions.
72
- | https://jcgt.org/published/0002/02/01/
73
-
74
- | [CIE18a] CIE 2018.
75
- | CIE standard illuminant A - 1 nm.
76
- | https://doi.org/10.25039/CIE.DS.8jsxjrsn
77
-
78
- | [CIE18b] CIE 2018.
79
- | CIE standard illuminant D55.
80
- | https://doi.org/10.25039/CIE.DS.qewfb3kp
81
-
82
- | [CIE18c] CIE 2018.
83
- | CIE standard illuminant D75.
84
- | https://doi.org/10.25039/CIE.DS.9fvcmrk4
85
-
86
- | [CIE19a] CIE 2019.
87
- | CIE 1931 colour-matching functions, 2 degree observer.
88
- | https://doi.org/10.25039/CIE.DS.xvudnb9b
89
-
90
- | [CIE19b] CIE 2019.
91
- | CIE 1964 colour-matching functions, 10 degree observer
92
- | https://doi.org/10.25039/CIE.DS.sqksu2n5
93
-
94
- | [L*20] Ly, Dyer, Feig, Chien & Del Bino 2020.
95
- | Research Techniques Made Simple: Cutaneous Colorimetry: A Reliable Technique for Objective Skin Color Measurement.
96
- | https://doi.org/10.1016/j.jid.2019.11.003
97
-
98
- | [CIE22a] CIE 2022.
99
- | CIE standard illuminant D50.
100
- | https://doi.org/10.25039/CIE.DS.etgmuqt5
101
-
102
- | [CIE22b] CIE 2022.
103
- | CIE standard illuminant D65.
104
- | https://doi.org/10.25039/CIE.DS.hjfjmt59
105
- '''
106
-
107
- import numpy as np
108
- from scipy.interpolate import interp1d
109
- from scipy.integrate import trapezoid
110
-
111
- from skinoptics.utils import *
112
- from skinoptics.dataframes import *
113
-
114
- def rspd(lambda0, illuminant):
115
- r'''
116
- | The relative spectral power distribution S(:math:`\lambda`) of a chosen standard illuminant
117
- | as a function of wavelength.
118
- | Linear interpolation of data from CIE datasets [CIE18a] [CIE22a] [CIE18b] [CIE22b] [CIE18c].
119
-
120
- | wavelength range:
121
- | [300 nm, 830 nm] (at 1 nm intervals, for illuminant = 'A', 'D50' or 'D65')
122
- | or [300 nm, 780 nm] (at 5 nm intervals, for illuminant = 'D55' or 'D75')
123
-
124
- :param lambda0: wavelength [nm] (must be in range [300 nm, 830 nm] or [300 nm, 780 nm])
125
- :type lambda0: float or np.ndarray
126
-
127
- :param illuminant: the user can choose one of the following... 'A', 'D50', 'D55', 'D65' or 'D75'
128
- :type illuminant: str
129
-
130
- | 'A' refers to the CIE standard illuminant A
131
- | 'D50' refers to the CIE standard illuminant D50
132
- | 'D55' refers to the CIE standard illuminant D55
133
- | 'D65' refers to the CIE standard illuminant D65
134
- | 'D75' refers to the CIE standard illuminant D75
135
-
136
- :return: - **rspd** (*float or np.ndarray*) – relative spectral power distribution [-]
137
- '''
138
-
139
- if illuminant == 'A' or 'D50' or 'D65':
140
- if isinstance(lambda0, np.ndarray) == True:
141
- if np.any(lambda0 < 300) or np.any(lambda0 > 830):
142
- msg = 'At least one element in the input lambda0 is out of the range [300 nm, 830 nm].'
143
- raise Exception(msg)
144
- else:
145
- if lambda0 < 300 or lambda0 > 830:
146
- msg = 'The input lambda0 = {} nm is out of the range [300 nm, 830 nm].'.format(lambda0)
147
- raise Exception(msg)
148
- elif illuminant == 'D55' or 'D75':
149
- if isinstance(lambda0, np.ndarray) == True:
150
- if np.any(lambda0 < 300) or np.any(lambda0 > 780):
151
- msg = 'At least one element in the input lambda0 is out of the range [300 nm, 780 nm].'
152
- raise Exception(msg)
153
- else:
154
- if lambda0 < 300 or lambda0 > 780:
155
- msg = 'The input lambda0 = {} nm is out of the range [300 nm, 780 nm].'.format(lambda0)
156
- raise Exception(msg)
157
-
158
- if illuminant == 'A':
159
- rspd = interp1d(np.array(rspds_A_D50_D65_dataframe)[:,0],
160
- np.array(rspds_A_D50_D65_dataframe)[:,1])(lambda0)
161
- elif illuminant == 'D50':
162
- rspd = interp1d(np.array(rspds_A_D50_D65_dataframe)[:,0],
163
- np.array(rspds_A_D50_D65_dataframe)[:,2])(lambda0)
164
- elif illuminant == 'D55':
165
- rspd = interp1d(np.array(rspds_D55_D75_dataframe)[:,0],
166
- np.array(rspds_D55_D75_dataframe)[:,1])(lambda0)
167
- elif illuminant == 'D65':
168
- rspd = interp1d(np.array(rspds_A_D50_D65_dataframe)[:,0],
169
- np.array(rspds_A_D50_D65_dataframe)[:,3])(lambda0)
170
- elif illuminant == 'D75':
171
- rspd = interp1d(np.array(rspds_D55_D75_dataframe)[:,0],
172
- np.array(rspds_D55_D75_dataframe)[:,2])(lambda0)
173
- else:
174
- msg = 'The input illuminant = {} is not valid.'.format(illuminant)
175
- raise Exception(msg)
176
-
177
- return rspd
178
-
179
- def cmfs(lambda0, observer, cmfs_model = 'CIE'):
180
- r'''
181
- | The CIE color-matching functions :math:`\bar{x}(\lambda)`, :math:`\bar{y}(\lambda)` and :math:`\bar{z}(\lambda)` for a chosen standard observer
182
- | as a function of wavelength.
183
-
184
- | wavelength range: [360 nm, 830 nm] (at 1 nm intervals for cmfs_model = 'CIE')
185
-
186
- :param lambda0: wavelength [nm] (must be in range [360., 830.] for cmfs_model = 'CIE')
187
- :type lambda0: float or np.ndarray
188
-
189
- :param observer: the user can choose one of the following... '2o' or '10o'
190
- :type observer: str
191
-
192
- :param cmfs_model: the user can choose one of the following... 'CIE', 'Wyman_singlelobe' or 'Wyman_multilobe' (default to 'CIE')
193
- :type cmfs_model: str
194
-
195
- | '2o' refers to the CIE 1931 2 degree standard observer
196
- | '10o' refers to the CIE 1964 10 degree standard observer
197
-
198
- | 'CIE' for the linear interpolation of data from CIE datasets [CIE19a] [CIE19b]
199
- | 'Wyman_singlelobe' for the functions from Wyman, Sloan & Shirley 2013 [WSS13] (section 2.1)
200
- | 'Wyman_multilobe' for the functions from Wyman, Sloan & Shirley 2013 [WSS13] (section 2.2)
201
-
202
- :return: - **xbar** (*float or np.ndarray*) – :math:`\bar{x}(\lambda`) color-matching function [-]
203
- - **ybar** (*float or np.ndarray*) :math:`\bar{y}(\lambda`) color-matching function [-]
204
- - **zbar** (*float or np.ndarray*) :math:`\bar{z}(\lambda`) color-matching function [-]
205
- '''
206
-
207
- if cmfs_model == 'CIE':
208
- if isinstance(lambda0, np.ndarray) == True:
209
- if np.any(lambda0 < 360) or np.any(lambda0 > 830):
210
- msg = 'At least one element in the input lambda0 is out of the range [360 nm, 830 nm].'
211
- raise Exception(msg)
212
- else:
213
- if lambda0 < 360 or lambda0 > 830:
214
- msg = 'The input lambda0 = {} nm is out of the range [360 nm, 830 nm].'.format(lambda0)
215
- raise Exception(msg)
216
- if observer == '2o':
217
- xbar = interp1d(np.array(cmfs_dataframe)[:,0],
218
- np.array(cmfs_dataframe)[:,1])(lambda0)
219
- ybar = interp1d(np.array(cmfs_dataframe)[:,0],
220
- np.array(cmfs_dataframe)[:,2])(lambda0)
221
- zbar = interp1d(np.array(cmfs_dataframe)[:,0],
222
- np.array(cmfs_dataframe)[:,3])(lambda0)
223
- elif observer == '10o':
224
- xbar = interp1d(np.array(cmfs_dataframe)[:,0],
225
- np.array(cmfs_dataframe)[:,4])(lambda0)
226
- ybar = interp1d(np.array(cmfs_dataframe)[:,0],
227
- np.array(cmfs_dataframe)[:,5])(lambda0)
228
- zbar = interp1d(np.array(cmfs_dataframe)[:,0],
229
- np.array(cmfs_dataframe)[:,6])(lambda0)
230
- else:
231
- msg = 'The input observer = {} is not valid.'.format(observer)
232
- raise Exception(msg)
233
- elif cmfs_model == 'Wyman_singlelobe':
234
- if observer == '2o':
235
- xbar = gaussian(lambda0, 1.065, 595.8, 33.33) \
236
- + gaussian(lambda0, 0.366, 446.8, 19.44)
237
- ybar = gaussian(np.log(lambda0), 1.014, np.log(556.3), 0.075)
238
- zbar = gaussian(np.log(lambda0), 1.839, np.log(449.8), 0.051)
239
- elif observer == '10o':
240
- xbar = mod_gaussian_Wyman(lambda0, 0.398, -570.1, 1014, 1250) \
241
- + mod_gaussian_Wyman(-lambda0, 1.132, -1338, 743.5, 234)
242
- ybar = gaussian(lambda0, 1.011, 556.1, 46.14)
243
- zbar = mod_gaussian_Wyman(lambda0, 2.06, 265.8, 180.4,32)
244
- else:
245
- msg = 'The input observer = {} is not valid.'.format(observer)
246
- raise Exception(msg)
247
- elif cmfs_model == 'Wyman_multilobe':
248
- if observer == '2o':
249
- coeffs = [[0.362, 1.056, -0.065, 0.821, 0.286, 0., 1.217, 0.681, 0.],
250
- [442.0, 599.8, 501.1, 568.8, 530.9, 0., 437.0, 459.0, 0.],
251
- [0.0624, 0.0264, 0.0490, 0.0213, 0.0613, 0., 0.0845, 0.0385, 0.],
252
- [0.0374, 0.0323, 0.0382, 0.0247, 0.0322, 0., 0.0278, 0.0725, 0.]]
253
- if isinstance(lambda0, np.ndarray) == True:
254
- xbar, ybar, zbar = np.zeros((3,len(lambda0)))
255
- c = 0
256
- for j in range(len(lambda0)):
257
- X, Y, Z = 0., 0., 0.
258
- for i in range(3):
259
- X += piecewise_gaussian_Wyman(lambda0[j], coeffs[0][i], coeffs[1][i],
260
- coeffs[2][i], coeffs[3][i])
261
- Y += piecewise_gaussian_Wyman(lambda0[j], coeffs[0][i+3], coeffs[1][i+3],
262
- coeffs[2][i+3], coeffs[3][i+3])
263
- Z += piecewise_gaussian_Wyman(lambda0[j], coeffs[0][i+6], coeffs[1][i+6],
264
- coeffs[2][i+6], coeffs[3][i+6])
265
- xbar[j], ybar[j], zbar[j] = X, Y, Z
266
- elif isinstance(lambda0, (int, float)) == True:
267
- X, Y, Z = 0., 0., 0.
268
- for i in range(3):
269
- X += piecewise_gaussian_Wyman(lambda0, coeffs[0][i], coeffs[1][i],
270
- coeffs[2][i], coeffs[3][i])
271
- Y += piecewise_gaussian_Wyman(lambda0, coeffs[0][i+3], coeffs[1][i+3],
272
- coeffs[2][i+3], coeffs[3][i+3])
273
- Z += piecewise_gaussian_Wyman(lambda0, coeffs[0][i+6], coeffs[1][i+6],
274
- coeffs[2][i+6], coeffs[3][i+6])
275
- xbar, ybar, zbar = X, Y, Z
276
- else:
277
- msg = 'The input lambda0 must be int, float or np.ndarray.'
278
- raise Exception(msg)
279
- else:
280
- msg = 'The input observer = {} is not valid for cmfs_model = Wyman_multilobe.'.format(observer)
281
- raise Exception(msg)
282
- else:
283
- msg = 'The input cmfs_model = {} is not valid.'.format(cmfs_model)
284
- raise Exception(msg)
285
-
286
- return xbar, ybar, zbar
287
-
288
- def xy_from_XYZ(X, Y, Z):
289
- r'''
290
- | Calculate CIE xy chromaticities from CIE XYZ coordinates.
291
-
292
- | :math:`x = \frac{X}{X + Y + Z}`
293
- | :math:`y = \frac{Y}{X + Y + Z}`
294
-
295
- :param X: X coordinate [-]
296
- :type X: float or np.ndarray
297
-
298
- :param Y: Y coordinate [-]
299
- :type Y: float or np.ndarray
300
-
301
- :param Z: Z coordinate [-]
302
- :type Z: float or np.ndarray
303
-
304
- :return: - **x** (*float or np.ndarray*) – x chromaticity [-]
305
- - **y** (*float or np.ndarray*) – y chromaticity [-]
306
- '''
307
-
308
- x = X/(X + Y + Z)
309
- y = Y/(X + Y + Z)
310
-
311
- return x, y
312
-
313
- def XYZ_wp(illuminant, observer, cmfs_model = 'CIE', K = 1.):
314
- r'''
315
- The white point CIE XYZ coordinates for a chosen standard illuminant and standard observer.
316
-
317
- :param illuminant: the user can choose one of the following... 'A', 'D50', 'D55', 'D65' or 'D75'
318
- :type illuminant: str
319
-
320
- :param observer: the user can choose one of the following... '2o' or '10o'
321
- :type observer: str
322
-
323
- :param cmfs_model: the user can choose one of the following... 'CIE', 'Wyman_singlelobe' or 'Wyman_multilobe' (default to 'CIE')
324
- :type cmfs_model: str
325
-
326
- :param K: scaling factor (usually 1. or 100.) [-] (default to 1.)
327
- :type K: float
328
-
329
- | 'A' refers to the CIE standard illuminant A
330
- | 'D50' refers to the CIE standard illuminant D50
331
- | 'D55' refers to the CIE standard illuminant D55
332
- | 'D65' refers to the CIE standard illuminant D65
333
- | 'D75' refers to the CIE standard illuminant D75
334
-
335
- | '2o' refers to the CIE 1931 2 degree standard observer
336
- | '10o' refers to the CIE 1964 10 degree standard observer
337
-
338
- | 'CIE' for the linear interpolation of data from CIE datasets [CIE19a] [CIE19b]
339
- | 'Wyman_singlelobe' for the functions from Wyman, Sloan & Shirley 2013 [WSS13] (section 2.1)
340
- | 'Wyman_multilobe' for the functions from Wyman, Sloan & Shirley 2013 [WSS13] (section 2.2)
341
-
342
- | K = 1. for CIE XYZ coordinates in range [0, 1]
343
- | K = 100. for CIE XYZ coordinates in range [0, 100]
344
-
345
- :return: - **Xn** (*float*) – white point X coordinate [-]
346
- - **Yn** (*float*) white point Y coordinate [-]
347
- - **Zn** (*float*) white point Z coordinate [-]
348
- '''
349
-
350
- if illuminant == 'D55' or illuminant == 'D75':
351
- Xn, Yn, Zn = XYZ_from_spectrum(np.arange(360, 780, 1), np.ones(len(np.arange(360, 780, 1)))*100,
352
- lambda_max = 780, illuminant = illuminant, observer = observer, cmfs_model = cmfs_model, K = K)
353
- else:
354
- Xn, Yn, Zn = XYZ_from_spectrum(np.arange(360, 830, 1), np.ones(len(np.arange(360, 830, 1)))*100,
355
- illuminant = illuminant, observer = observer, cmfs_model = cmfs_model, K = K)
356
-
357
- return Xn, Yn, Zn
358
-
359
- def xy_wp(illuminant, observer):
360
- r'''
361
- | The white point CIE xy chromaticities for a chosen standard illuminant and standard observer.
362
- | Calculated from the white point CIE XYZ coordinates (see function :meth:`skinoptics.colors.XYZ_wp`).
363
-
364
- :param illuminant: the user can choose one of the following... 'A', 'D50', 'D55', 'D65' or 'D75'
365
- :type illuminant: str
366
-
367
- :param observer: the user can choose one of the following... '2o' or '10o'
368
- :type observer: str
369
-
370
- | 'A' refers to the CIE standard illuminant A
371
- | 'D50' refers to the CIE standard illuminant D50
372
- | 'D55' refers to the CIE standard illuminant D55
373
- | 'D65' refers to the CIE standard illuminant D65
374
- | 'D75' refers to the CIE standard illuminant D75
375
-
376
- | '2o' refers to the CIE 1931 2 degree standard observer
377
- | '10o' refers to the CIE 1964 10 degree standard observer
378
-
379
- :return: - **xn** (*float*) white point CIE x chromaticity [-]
380
- - **yn** (*float*) white point CIE y chromaticity [-]
381
- '''
382
-
383
- Xn, Yn, Zn = XYZ_wp(illuminant = illuminant, observer = observer)
384
- xn, yn = xy_from_XYZ(Xn, Yn, Zn)
385
-
386
- return xn, yn
387
-
388
- def transf_matrix_sRGB_linear_from_XYZ():
389
- r'''
390
- The transformation matrix employed to obtain linear sRGB coordinates from CIE XYZ coordinates.
391
-
392
- :math:`\mathcal{M} =
393
- \begin{bmatrix}
394
- 3.24062 & -1.5372 & -0.4986 \\
395
- -0.9689 & 1.8758 & 0.0415 \\
396
- 0.0557 & -0.2040 & 1.0570
397
- \end{bmatrix}`
398
-
399
- :returns: - **M** (*np.ndarray*) – transformation matrix
400
- '''
401
-
402
- return np.array([[3.24062, -1.5372, -0.4986], [-0.9689, 1.8758, 0.0415], [0.0557, -0.2040, 1.0570]])
403
-
404
- def nonlinear_corr_sRGB(u):
405
- r'''
406
- The nonlinear correction for sRGB coordinates.
407
-
408
- :math:`\gamma(u) =
409
- \left \{ \begin{matrix}
410
- 12.92 \mbox{ } u, & \mbox{if } u \le 0.0031308 \\
411
- 1.055 \mbox{ } u^{1/2.4} - 0.055, & \mbox{if } u > 0.0031308 \\
412
- \end{matrix} \right.`
413
-
414
- :param u: linear R, G or B coordinate [-]
415
- :type u: float or np.ndarray
416
-
417
- :return: - **gamma** (*float or np.ndarray*) – nonlinear R, G or B coordinate [-]
418
- '''
419
-
420
- if isinstance(u, np.ndarray) == True:
421
- gamma = np.zeros(len(u))
422
- for i in range(len(u)):
423
- if u[i] <= 0.0031308:
424
- gamma[i] = 12.92*u[i]
425
- else:
426
- gamma[i] = 1.055*u[i]**(1./2.4) - 0.055
427
- elif isinstance(u, (int, float)) == True:
428
- if u <= 0.0031308:
429
- gamma = 12.92*u
430
- else:
431
- gamma = 1.055*u**(1./2.4) - 0.055
432
- else:
433
- msg = 'u must be int, float or np.ndarray.'
434
- raise Exception(msg)
435
-
436
- return gamma
437
-
438
- def inv_nonlinear_corr_sRGB(u):
439
- r'''
440
- The inverse nonlinear correction for sRGB coordinates.
441
-
442
- :math:`\gamma^{-1}(u) =
443
- \left \{ \begin{matrix}
444
- u/12.92, & \mbox{if } u \le 0.04045 \\
445
- [(u + 0.055)/1.055]^{2.4}, & \mbox{if } u > 0.04045 \\
446
- \end{matrix} \right.`
447
-
448
- :param u: nonlinear R, G or B coordinate [-]
449
- :type u: float or np.ndarray
450
-
451
- :return: - **inv_gamma** (*float or np.ndarray*) – linear R, G or B coordinate [-]
452
- '''
453
-
454
- if isinstance(u, np.ndarray) == True:
455
- inv_gamma = np.zeros(len(u))
456
- for i in range(len(u)):
457
- if u[i] <= 0.04045:
458
- inv_gamma[i] = u[i]/12.92
459
- else:
460
- inv_gamma[i] = ((u[i] + 0.055)/1.055)**(2.4)
461
- elif isinstance(u, (int, float)) == True:
462
- if u <= 0.04045:
463
- inv_gamma = u/12.92
464
- else:
465
- inv_gamma = ((u + 0.055)/1.055)**(2.4)
466
- else:
467
- msg = 'u must be int, float or np.ndarray.'
468
- raise Exception(msg)
469
-
470
- return inv_gamma
471
-
472
- def sRGB_from_XYZ(X, Y, Z, K = 1., sRGB_scale = 'norm'):
473
- r'''
474
- | Calculate sRGB coordinates from CIE XYZ coordinates.
475
- | CIE XYZ coordinates must be for the standard illuminant D65 and the 2 degree standard observer.
476
- | For details please check Stokes et al. [S*96] and IEC [IEC99].
477
-
478
- :math:`\begin{bmatrix}
479
- R \\
480
- G \\
481
- B
482
- \end{bmatrix}
483
- =
484
- \begin{bmatrix}
485
- \gamma(R_{linear}) \\
486
- \gamma(G_{linear}) \\
487
- \gamma(B_{linear})
488
- \end{bmatrix}`
489
-
490
- in which
491
-
492
- :math:`\begin{bmatrix}
493
- R_{linear} \\
494
- G_{linear} \\
495
- B_{linear}
496
- \end{bmatrix}
497
- =
498
- \mathcal{M}
499
- \begin{bmatrix}
500
- X \\
501
- Y \\
502
- Z
503
- \end{bmatrix}`
504
-
505
- and
506
-
507
- :math:`\gamma(u) =
508
- \left \{ \begin{matrix}
509
- 12.92 \mbox{ } u, & \mbox{if } u \le 0.0031308 \\
510
- 1.055 \mbox{ } u^{1/2.4} - 0.055, & \mbox{if } u > 0.0031308 \\
511
- \end{matrix} \right.`
512
-
513
- :param X: X coordinate [-]
514
- :type X: float or np.ndarray
515
-
516
- :param Y: Y coordinate [-]
517
- :type Y: float or np.ndarray
518
-
519
- :param Z: Z coordinate [-]
520
- :type Z: float or np.ndarray
521
-
522
- :param K: scaling factor (usually 1. or 100.) [-] (default to 1.)
523
- :type K: float
524
-
525
- :param sRGB_scale: the user can choose one of the following... 'norm' or '8bit'
526
- :type sRGB_scale:: str (default to 'norm')
527
-
528
- | K = 1. for CIE XYZ coordinates in range [0, 1]
529
- | K = 100. for CIE XYZ coordinates in range [0, 100]
530
-
531
- | 'norm' for sRGB coordinates in range [0,1] (normalized scale)
532
- | '8bit' for sRGB coordinates in range [0, 255] (8-bit scale)
533
-
534
- :return: - **R** (*float or np.ndarray*) – R coordinate [-]
535
- - **G** (*float or np.ndarray*) – G coordinate [-]
536
- - **B** (*float or np.ndarray*) B coordinate [-]
537
- '''
538
-
539
- M = transf_matrix_sRGB_linear_from_XYZ()
540
-
541
- if isinstance(X, np.ndarray) == True and \
542
- isinstance(Y, np.ndarray) == True and \
543
- isinstance(Z, np.ndarray) == True:
544
- if len(X) - len(Y) != 0 or len(X) - len(Z) != 0:
545
- msg = 'X, Y and Z must have the same length.'
546
- raise Exception(msg)
547
- R, G, B = np.zeros((3, len(X)))
548
- for i in range(len(X)):
549
- R_linear, G_linear, B_linear = np.clip(np.matmul(M, np.array([X[i]/K, Y[i]/K, Z[i]/K])), 0, 1)
550
- R[i], G[i], B[i] = nonlinear_corr_sRGB(np.array([R_linear, G_linear, B_linear]))
551
- elif isinstance(X, (int, float)) == True and \
552
- isinstance(Y, (int, float)) == True and \
553
- isinstance(Z, (int, float)):
554
- R_linear, G_linear, B_linear = np.clip(np.matmul(M, np.array([X/K, Y/K, Z/K])), 0 , 1)
555
- R, G, B = nonlinear_corr_sRGB(np.array([R_linear, G_linear, B_linear]))
556
- else:
557
- msg = 'X, Y and Z must be int, float or np.ndarray.'
558
- raise Exception(msg)
559
-
560
- if sRGB_scale == 'norm':
561
- pass
562
- elif sRGB_scale == '8bit':
563
- scaling = 255
564
- R, G, B = np.round(scaling*np.array([R, G, B]))
565
- else:
566
- msg = 'The input sRGB_scale = {} is not valid.'.format(sRGB_scale)
567
- raise Exception(msg)
568
-
569
- return R, G, B
570
-
571
- def XYZ_from_sRGB(R, G, B, K = 1., sRGB_scale = 'norm'):
572
- r'''
573
- | Calculate CIE XYZ coordinates from sRGB coordinates.
574
- | The obtained CIE XYZ coordinates are respective to the standard illuminant D65 and the
575
- | 2 degree standard observer.
576
- | For details please check Stokes et al. [S*96] and IEC [IEC99].
577
-
578
- :math:`\begin{bmatrix}
579
- X \\
580
- Y \\
581
- Z
582
- \end{bmatrix}
583
- =
584
- \mathcal{M}^{-1}
585
- \begin{bmatrix}
586
- R_{linear} \\
587
- G_{linear} \\
588
- B_{linear}
589
- \end{bmatrix}`
590
-
591
- in which
592
-
593
- :math:`\begin{bmatrix}
594
- R_{linear} \\
595
- G_{linear} \\
596
- B_{linear}
597
- \end{bmatrix}
598
- =
599
- \begin{bmatrix}
600
- \gamma^{-1}(R) \\
601
- \gamma^{-1}(G) \\
602
- \gamma^{-1}(B)
603
- \end{bmatrix}`
604
-
605
- and
606
-
607
- :math:`\gamma^{-1}(u) =
608
- \left \{ \begin{matrix}
609
- u/12.92, & \mbox{if } u \le 0.04045 \\
610
- [(u + 0.055)/1.055]^{2.4}, & \mbox{if } u > 0.04045 \\
611
- \end{matrix} \right.`
612
-
613
- :param R: R coordinate [-]
614
- :type R: float or np.ndarray
615
-
616
- :param G: G coordinate [-]
617
- :type G: float or np.ndarray
618
-
619
- :param B: B coordinate [-]
620
- :type B: float or np.ndarray
621
-
622
- :param K: scaling factor (usually 1. or 100.) [-] (default to 1.)
623
- :type K: float
624
-
625
- :param sRGB_scale: the user can choose one of the following... 'norm' or '8bit'
626
- :type sRGB_scale:: str (default to 'norm')
627
-
628
- | K = 1. for CIE XYZ coordinates in range [0, 1]
629
- | K = 100. for CIE XYZ coordinates in range [0, 100]
630
-
631
- | 'norm' for sRGB coordinates in range [0,1] (normalized scale)
632
- | '8bit' for sRGB coordinates in range [0, 255] (8-bit scale)
633
-
634
- :return: - **X** (*float or np.ndarray*) – X coordinate [-]
635
- - **Y** (*float or np.ndarray*) – Y coordinate [-]
636
- - **Z** (*float or np.ndarray*) Z coordinate [-]
637
- '''
638
-
639
- if sRGB_scale == 'norm':
640
- pass
641
- elif sRGB_scale == '8bit':
642
- scaling = 255
643
- R, G, B = np.array([R, G, B])/scaling
644
- else:
645
- msg = 'The input sRGB_scale = {} is not valid.'.format(sRGB_scale)
646
- raise Exception(msg)
647
-
648
- inv_M = np.round(np.linalg.inv(transf_matrix_sRGB_linear_from_XYZ()), 4)
649
-
650
- if isinstance(R, np.ndarray) == True and \
651
- isinstance(G, np.ndarray) == True and \
652
- isinstance(B, np.ndarray) == True:
653
- if len(R) - len(G) != 0 or len(R) - len(B) != 0:
654
- msg = 'R, G and B must have the same length.'
655
- raise Exception(msg)
656
- X, Y, Z = np.zeros((3, len(R)))
657
- for i in range(len(R)):
658
- R_linear, G_linear, B_linear = inv_nonlinear_corr_sRGB(np.array([R[i], G[i], B[i]]))
659
- X[i], Y[i], Z[i] = np.matmul(inv_M, np.array([R_linear, G_linear, B_linear]))
660
- elif isinstance(R, (int, float)) == True and \
661
- isinstance(G, (int, float)) == True and \
662
- isinstance(B, (int, float)):
663
- R_linear, G_linear, B_linear = inv_nonlinear_corr_sRGB(np.array([R, G, B]))
664
- X, Y, Z = np.matmul(inv_M, np.array([R_linear, G_linear, B_linear]))
665
- else:
666
- msg = 'R, G and B must be int, float or np.ndarray.'
667
- raise Exception(msg)
668
-
669
- return X*K, Y*K, Z*K
670
-
671
- def f_Lab_from_XYZ(u):
672
- r'''
673
- | The function :math:`f(u)` used to calculate CIE L*a*b* coordinates from CIE XYZ coordinates
674
- | (see function :meth:`skinoptics.colors.Lab_from_XYZ`).
675
-
676
- :math:`f(u) = \left\{
677
- \begin{matrix}
678
- \sqrt[3]{u}, & \mbox{if } u > \left(\frac{6}{29}\right)^3 \\
679
- \frac{1}{3}\left(\frac{29}{6}\right)^2 u + \frac{4}{29}, & \mbox{if } u \le \left(\frac{6}{29}\right)^3
680
- \end{matrix}\right.`
681
-
682
- :param u: X/Xn, Y/Yn or Z/Zn ratio[-]
683
- :type u: float or np.ndarray
684
-
685
- :return: - **f** (*float or np.ndarray*) – evaluated function [-]
686
- '''
687
-
688
- delta = 6./29.
689
- if isinstance(u, np.ndarray) == True:
690
- f = np.zeros(len(u))
691
- for i in range(len(u)):
692
- if u[i] > delta**3.:
693
- f[i] = np.cbrt(u[i])
694
- else:
695
- f[i] = u[i]/3./delta**2. + 4./29.
696
- elif isinstance(u, (int, float)) == True:
697
- if u > delta**3.:
698
- f = np.cbrt(u)
699
- else:
700
- f = u/3./delta**2. + 4./29.
701
- else:
702
- msg = 'u must be int, float or np.ndarray.'
703
- raise Exception(msg)
704
-
705
- return f
706
-
707
- def inv_f_Lab_from_XYZ(u):
708
- r'''
709
- The :math:`f^{-1}(u)` function, i.e. the inverse of the :math:`f(u)` function :meth:`skinoptics.colors.f_Lab_from_XYZ`.
710
-
711
- :math:`f^{-1}(u) = \left\{
712
- \begin{matrix}
713
- u^3, & \mbox{if } u > \frac{6}{29} \\
714
- 3 \mbox{ } \left(\frac{6}{29}\right)^2\left(u - \frac{4}{29} \right), & \mbox{if } u \le \frac{6}{29}
715
- \end{matrix}\right.`
716
-
717
- :param u: function variable [-]
718
- :type u: float or np.ndarray
719
-
720
- :return: - **f** (*float or np.ndarray*) – evaluated function [-]
721
- '''
722
-
723
- delta = 6./29.
724
- if isinstance(u, np.ndarray) == True:
725
- inv_f = np.zeros(len(u))
726
- for i in range(len(u)):
727
- if u[i] > delta:
728
- inv_f[i] = u[i]**3
729
- else:
730
- inv_f[i] = 3.*delta**2.*(u[i] - 4./29.)
731
- elif isinstance(u, (int, float)) == True:
732
- if u > delta:
733
- inv_f = u**3
734
- else:
735
- inv_f = 3.*delta**2.*(u - 4./29.)
736
- else:
737
- msg = 'u must be int, float or np.ndarray.'
738
- raise Exception(msg)
739
-
740
- return inv_f
741
-
742
- def Lab_from_XYZ(X, Y, Z, illuminant = 'D65', observer = '10o', K = 1.):
743
- r'''
744
- | Calculate CIE L*a*b* coordinates from CIE XYZ coordinates.
745
- | CIE XYZ and CIE L*a*b* coordinates must be for the same standard illuminant and standard observer.
746
- | For detailts please check CIE [CIE04], Schanda 2006 [S06] and Hunt & Pointer 2011 [HP11].
747
-
748
- | :math:`L^* = 116 \mbox{ } f(Y/Y_n) - 16`
749
- | :math:`a^* = 500 \mbox{ } [f(X/X_n) - f(Y/Y_n)]`
750
- | :math:`b^* = 200 \mbox{ } [f(Y/Y_n) - f(Z/Z_n)]`
751
-
752
- in which (:math:`X_n`, :math:`Y_n`, :math:`Z_n`) is the white point and
753
-
754
- :math:`f(u) = \left\{
755
- \begin{matrix}
756
- \sqrt[3]{u}, & \mbox{if } u > \left(\frac{6}{29}\right)^3 \\
757
- \frac{1}{3}\left(\frac{29}{6}\right)^2 u + \frac{4}{29}, & \mbox{if } u \le \left(\frac{6}{29}\right)^3
758
- \end{matrix}\right.`
759
-
760
- :param X: X coordinate [-]
761
- :type X: float or np.ndarray
762
-
763
- :param Y: Y coordinate [-]
764
- :type Y: float or np.ndarray
765
-
766
- :param Z: Z coordinate [-]
767
- :type Z: float or np.ndarray
768
-
769
- :param illuminant: the user can choose one of the following... 'A', 'D50', 'D55', 'D65' or 'D75'
770
- :type illuminant: str
771
-
772
- :param observer: the user can choose one of the following... '2o' or '10o'
773
- :type observer: str
774
-
775
- :param K: scaling factor (usually 1. or 100.) [-] (default to 1.)
776
- :type K: float
777
-
778
- | 'A' refers to the CIE standard illuminant A
779
- | 'D50' refers to the CIE standard illuminant D50
780
- | 'D55' refers to the CIE standard illuminant D55
781
- | 'D65' refers to the CIE standard illuminant D65
782
- | 'D75' refers to the CIE standard illuminant D75
783
-
784
- | '2o' refers to the CIE 1931 2 degree standard observer
785
- | '10o' refers to the CIE 1964 10 degree standard observer
786
-
787
- | K = 1. for CIE XYZ coordinates in range [0, 1]
788
- | K = 100. for CIE XYZ coordinates in range [0, 100]
789
-
790
- :return: - **L** (*float or np.ndarray*) L* coordinate [-]
791
- - **a** (*float or np.ndarray*) – a* coordinate [-]
792
- - **b** (*float or np.ndarray*) b* coordinate [-]
793
- '''
794
-
795
- Xn, Yn, Zn = XYZ_wp(illuminant = illuminant, observer = observer, K = K)
796
- f = f_Lab_from_XYZ
797
-
798
- if isinstance(X, np.ndarray) == True and \
799
- isinstance(Y, np.ndarray) == True and \
800
- isinstance(Z, np.ndarray) == True:
801
- if len(X) - len(Y) != 0 or len(X) - len(Z) != 0:
802
- msg = 'X, Y and Z must have the same length.'
803
- raise Exception(msg)
804
- L, a, b = np.zeros((3, len(X)))
805
- for i in range(len(X)):
806
- L[i] = 116.*f(Y[i]/Yn) - 16.
807
- a[i] = 500.*(f(X[i]/Xn) - f(Y[i]/Yn))
808
- b[i] = 200.*(f(Y[i]/Yn) - f(Z[i]/Zn))
809
- elif isinstance(X, (int, float)) == True and \
810
- isinstance(Y, (int, float)) == True and \
811
- isinstance(Z, (int, float)):
812
- L = 116.*f(Y/Yn) - 16.
813
- a = 500.*(f(X/Xn) - f(Y/Yn))
814
- b = 200.*(f(Y/Yn) - f(Z/Zn))
815
- else:
816
- msg = 'X, Y and Z must be int, float or np.ndarray.'
817
- raise Exception(msg)
818
-
819
- return L, a, b
820
-
821
- def XYZ_from_Lab(L, a, b, illuminant = 'D65', observer = '10o', K = 1.):
822
- r'''
823
- | Calculate CIE XYZ coordinates from CIE L*a*b* coordinates.
824
- | CIE XYZ and CIE L*a*b* coordinates must be for the same standard illuminant and standard observer.
825
- | For detailts please check CIE [CIE04], Schanda 2006 [S06] and Hunt & Pointer 2011 [HP11].
826
-
827
- | :math:`X = f^{-1}[(L^* + 16)/116 + a^*/500] \mbox{ } X_n`
828
- | :math:`Y = f^{-1}[(L^* + 16)/116] \mbox{ } Y_n`
829
- | :math:`Z = f^{-1}[(L^* + 16)/116 - b^*/200] \mbox{ } Z_n`
830
-
831
- in which (:math:`X_n`, :math:`Y_n`, :math:`Z_n`) is the white point and
832
-
833
- :math:`f^{-1}(u) = \left\{
834
- \begin{matrix}
835
- u^3, & \mbox{if } u > \frac{6}{29} \\
836
- 3 \mbox{ } \left(\frac{6}{29}\right)^2\left(u - \frac{4}{29} \right), & \mbox{if } u \le \frac{6}{29}
837
- \end{matrix}\right.`
838
-
839
- :param L: L* coordinate [-] (must be in range [0, 100])
840
- :type L: float or np.ndarray
841
-
842
- :param a: a* coordinate [-]
843
- :type a: float or np.ndarray
844
-
845
- :param b: b* coordinate [-]
846
- :type b: float or np.ndarray
847
-
848
- :param illuminant: the user can choose one of the following... 'A', 'D50', 'D55', 'D65' or 'D75'
849
- :type illuminant: str
850
-
851
- :param observer: the user can choose one of the following... '2o' or '10o'
852
- :type observer: str
853
-
854
- :param K: scaling factor (usually 1. or 100.) [-] (default to 1.)
855
- :type K: float
856
-
857
- | 'A' refers to the CIE standard illuminant A
858
- | 'D50' refers to the CIE standard illuminant D50
859
- | 'D55' refers to the CIE standard illuminant D55
860
- | 'D65' refers to the CIE standard illuminant D65
861
- | 'D75' refers to the CIE standard illuminant D75
862
-
863
- | '2o' refers to the CIE 1931 2 degree standard observer
864
- | '10o' refers to the CIE 1964 10 degree standard observer
865
-
866
- | K = 1. for CIE XYZ coordinates in range [0, 1]
867
- | K = 100. for CIE XYZ coordinates in range [0, 100]
868
-
869
- :return: - **X** (*float or np.ndarray*) X* coordinate [-]
870
- - **Y** (*float or np.ndarray*) – Y* coordinate [-]
871
- - **Z** (*float or np.ndarray*) Z* coordinate [-]
872
- '''
873
-
874
- if isinstance(L, np.ndarray) == True:
875
- if np.any(L < 0) or np.any(L > 100):
876
- msg = 'At least one element in the input L is out of the range [0, 100].'
877
- raise Exception(msg)
878
- else:
879
- if L < 0 or L > 100:
880
- msg = 'The input L = {} is out of the range [0, 100].'.format(L)
881
- raise Exception(msg)
882
-
883
- Xn, Yn, Zn = XYZ_wp(illuminant = illuminant, observer = observer, K = K)
884
- inv_f = inv_f_Lab_from_XYZ
885
-
886
- if isinstance(L, np.ndarray) == True and \
887
- isinstance(a, np.ndarray) == True and \
888
- isinstance(b, np.ndarray) == True:
889
- if len(L) - len(a) != 0 or len(L) - len(b) != 0:
890
- msg = 'L, a and b must have the same length.'
891
- raise Exception(msg)
892
- X, Y, Z = np.zeros((3, len(L)))
893
- for i in range(len(L)):
894
- X[i] = Xn*inv_f((L[i] + 16.)/116. + a[i]/500.)
895
- Y[i] = Yn*inv_f((L[i] + 16.)/116.)
896
- Z[i] = Zn*inv_f((L[i] + 16.)/116. - b[i]/200.)
897
- elif isinstance(L, (int, float)) == True and \
898
- isinstance(a, (int, float)) == True and \
899
- isinstance(b, (int, float)):
900
- X = Xn*inv_f((L + 16.)/116. + a/500.)
901
- Y = Yn*inv_f((L + 16.)/116.)
902
- Z = Zn*inv_f((L + 16.)/116. - b/200.)
903
- else:
904
- msg = 'L, a and b must be float or np.ndarray.'
905
- raise Exception(msg)
906
-
907
- return X, Y, Z
908
-
909
- def chroma(a, b):
910
- r'''
911
- Calculate the chroma C* from a* and b* coordinates.
912
-
913
- :math:`C^* = \sqrt{a^{*2} + b^{*2}}`
914
-
915
- :param a: a* coordinate [-]
916
- :type a: float or np.ndarray
917
-
918
- :param b: b* coordinate [-]
919
- :type b: float or np.ndarray
920
-
921
- :return: - **chroma** (*float or np.ndarray*) – chroma [-]
922
- '''
923
-
924
- return np.sqrt(a**2. + b**2.)
925
-
926
- def hue(a, b):
927
- r'''
928
- Calculate the hue angle h* from a* and b* coordinates.
929
-
930
- :math:`h^* = \mbox{arctan2 } (b^*, a^*) \times \frac{180}{\pi}`
931
-
932
- :param a: a* coordinate [-]
933
- :type a: float or np.ndarray
934
-
935
- :param b: b* coordinate [-]
936
- :type b: float or np.ndarray
937
-
938
- :return: - **hue** (*float or np.ndarray*) hue angle [degrees] (in range [0, 360])
939
- '''
940
-
941
- hue = np.arctan2(b,a)*180./np.pi
942
-
943
- hue_shape = hue.shape
944
- hue_flatten = hue.flatten()
945
-
946
- if isinstance(hue, np.ndarray) == True:
947
- for i in hue_flatten:
948
- if i < 0:
949
- i += 360
950
- hue = hue_flatten.reshape(hue_shape)
951
- elif isinstance(hue, (int, float)) == True:
952
- if hue < 0:
953
- hue += 360
954
-
955
- return hue
956
-
957
- def ITA(L, b, L0 = 50.):
958
- r'''
959
- | Calculate the Individual Typology Angle (ITA) from L* and b* coordinates.
960
- | For details please check Chardon, Cretois & Hourseau 1991 [CCH91], Del Bino et al. 2006 [D*06],
961
- | Del Bino & Bernerd 2013 [DB13] and Ly et al. [L*20].
962
-
963
- :math:`\mbox{ITA} = \arctan\left(\frac{L^*-L_0^*}{b^*}\right) \times \frac{180}{\pi}`
964
-
965
- :param L: L* coordinate [-]
966
- :type L: float or np.ndarray
967
-
968
- :param b: b* coordinate [-]
969
- :type b: float or np.ndarray
970
-
971
- :param L0: L0 coordinate [-] (default to 50.)
972
- :type L0: float
973
-
974
- :return: - **ITA** (*float or np.ndarray*) – Individual Typology Angle [degrees]
975
- '''
976
-
977
- return np.arctan((L - L0)/b)*180./np.pi
978
-
979
- def ITA_class(ITA):
980
- r'''
981
- | Skin color classification based on the Individual Typology Angle :meth:`skinoptics.colors.ITA`.
982
- | For details please check Chardon, Cretois & Hourseau 1991 [CCH91], Del Bino et al. 2006 [D*06],
983
- | Del Bino & Bernerd 2013 [DB13] and Ly et al. [L*20].
984
-
985
- +---------------------------+-----------------------------------------------+
986
- | skin color classification | ITA range |
987
- +===========================+===============================================+
988
- | very light | ITA :math:`> 55^\circ` |
989
- +---------------------------+-----------------------------------------------+
990
- | light | :math:`41^\circ <` ITA :math:`\le 55^\circ` |
991
- +---------------------------+-----------------------------------------------+
992
- | intermediate | :math:`28^\circ <` ITA :math:`\le 41^\circ` |
993
- +---------------------------+-----------------------------------------------+
994
- | tan | :math:`10^\circ <` ITA :math:`\le 28^\circ` |
995
- +---------------------------+-----------------------------------------------+
996
- | brown | :math:`-30^\circ <` ITA :math:`\le 10^\circ` |
997
- +---------------------------+-----------------------------------------------+
998
- | dark | ITA :math:`\le -30^\circ` |
999
- +---------------------------+-----------------------------------------------+
1000
-
1001
- :param ITA: Individual Typology Angle [degrees] (must be greater than -90 and less than 90)
1002
- :type ITA: float or np.ndarray
1003
-
1004
- :return: - **ITA_class** (*str or np.ndarray*) – skin color classification based on the Individual Typology Angle
1005
- '''
1006
-
1007
- if isinstance(ITA, np.ndarray) == True:
1008
- if np.any(ITA < -90) or np.any(ITA > 90):
1009
- msg = 'At least one element in the input ITA is out of the range [-90, 90].'
1010
- raise Exception(msg)
1011
- else:
1012
- if ITA < -90 or ITA > 90:
1013
- msg = 'The input ITA = {} is out of the range [-90, 90].'.format(ITA)
1014
- raise Exception(msg)
1015
-
1016
- if isinstance(ITA, np.ndarray) == True:
1017
- ITA_class_list = ['']*len(ITA)
1018
- for i in range(len(ITA)):
1019
- if ITA[i] > 55:
1020
- ITA_class_list[i] = 'very light'
1021
- elif ITA[i] > 41 and ITA[i] <= 55:
1022
- ITA_class_list[i] = 'light'
1023
- elif ITA[i] > 28 and ITA[i] <= 41:
1024
- ITA_class_list[i] = 'intermediate'
1025
- elif ITA[i] > 10 and ITA[i] <= 28:
1026
- ITA_class_list[i] = 'tan'
1027
- elif ITA[i] > -30 and ITA[i] <= 10:
1028
- ITA_class_list[i] = 'brown'
1029
- else:
1030
- ITA_class_list[i] = 'dark'
1031
- ITA_class = np.array(ITA_class_list)
1032
- else:
1033
- if ITA > 55:
1034
- ITA_class = 'very light'
1035
- elif ITA > 41 and ITA <= 55:
1036
- ITA_class = 'light'
1037
- elif ITA > 28 and ITA <= 41:
1038
- ITA_class = 'intermediate'
1039
- elif ITA > 10 and ITA <= 28:
1040
- ITA_class = 'tan'
1041
- elif ITA > -30 and ITA <= 10:
1042
- ITA_class = 'brown'
1043
- else:
1044
- ITA_class = 'dark'
1045
-
1046
- return ITA_class
1047
-
1048
- def Delta_L(L0, L1):
1049
- r'''
1050
- Calculate the lightness difference :math:`\Delta L^*` between a reference color lightness :math:`L^*_0`
1051
- and a test color lightness :math:`L^*_1`.
1052
-
1053
- :math:`\Delta L^* = L^*_1 - L^*_0`
1054
-
1055
- :param L0: reference color L* coordinate [-]
1056
- :type L0: float or np.ndarray
1057
-
1058
- :param L1: test color L* coordinate [-]
1059
- :type L1: float or np.ndarray
1060
-
1061
- :return: - **delta_L** (*float or np.ndarray*) – lightness difference [-]
1062
- '''
1063
-
1064
- return L1 - L0
1065
-
1066
- def Delta_a(a0, a1):
1067
- r'''
1068
- Calculate the difference :math:`\Delta a^*` between a reference color :math:`a^*_0` coordinate
1069
- and a test color :math:`a^*_1` coordinate.
1070
-
1071
- :math:`\Delta a^* = a^*_1 - a^*_0`
1072
-
1073
- :param a0: reference color a* coordinate [-]
1074
- :type a0: float or np.ndarray
1075
-
1076
- :param a1: test color a* coordinate [-]
1077
- :type a1: float or np.ndarray
1078
-
1079
- :return: - **delta_a** (*float or np.ndarray*) – a* difference [-]
1080
- '''
1081
-
1082
- return a1 - a0
1083
-
1084
- def Delta_b(b0, b1):
1085
- r'''
1086
- Calculate the difference :math:`\Delta b^*` between a reference color :math:`b^*_0` coordinate
1087
- and a test color :math:`b^*_1` coordinate.
1088
-
1089
- :math:`\Delta b^* = b^*_1 - b^*_0`
1090
-
1091
- :param b0: reference color b* coordinate [-]
1092
- :type b0: float or np.ndarray
1093
-
1094
- :param b1: test color b* coordinate [-]
1095
- :type b1: float or np.ndarray
1096
-
1097
- :return: - **delta_b** (*float or np.ndarray*) – b* difference [-]
1098
- '''
1099
-
1100
- return b1 - b0
1101
-
1102
- def Delta_E(L0, a0, b0, L1, a1, b1):
1103
- r'''
1104
- Calculate the color difference :math:`\Delta E^*` between between
1105
- a reference color (:math:`L^*_0`, :math:`a^*_0`, :math:`b^*_0`) and
1106
- a test color (:math:`L^*_1`, :math:`a^*_1`, :math:`b^*_1`).
1107
-
1108
- :math:`\Delta E^* = \sqrt{(L^*_1 - L^*_0)^2 + (a^*_1 - a^*_0)^2 + (b^*_1 - b^*_0)^2}`
1109
-
1110
- :param L0: reference color L* coordinate [-]
1111
- :type L0: float or np.ndarray
1112
-
1113
- :param a0: reference color a* coordinate [-]
1114
- :type a0: float or np.ndarray
1115
-
1116
- :param b0: reference color b* coordinate [-]
1117
- :type b0: float or np.ndarray
1118
-
1119
- :param L1: test color L* coordinate [-]
1120
- :type L1: float or np.ndarray
1121
-
1122
- :param a1: test color a* coordinate [-]
1123
- :type a1: float or np.ndarray
1124
-
1125
- :param b1: test color b* coordinate [-]
1126
- :type b1: float or np.ndarray
1127
-
1128
- :return: - **delta_E** (*float or np.ndarray*) – color difference [-]
1129
- '''
1130
-
1131
- return np.sqrt(Delta_L(L0 = L0, L1 = L1)**2 + Delta_a(a0 = a0, a1 = a1)**2 + Delta_b(b0 = b0, b1 = b1)**2)
1132
-
1133
- def EI(R_green, R_red):
1134
- r'''
1135
- | Calculate the Erythema Index (EI) from the reflectances on chosen green
1136
- | (usually approx. 568 nm) and red bands (usually approx. 655 nm).
1137
- | For details please check Takiwaki et al. 1994 [T*94] and Fullerton et al. 1996 [F*96].
1138
-
1139
- :math:`\mbox{EI} = 100 \mbox{ } [\mbox{log}_{10}(R_\mbox{red}) - \mbox{log}_{10}(R_\mbox{green})]`
1140
-
1141
- :param R_green: reflectance on a chosen green band [%]
1142
- :type R_green: float or np.ndarray
1143
-
1144
- :param R_red: reflectance on a chosen red band [%]
1145
- :type R_red: float or np.ndarray
1146
-
1147
- :return: - **EI** (*float or np.ndarray*) Erythema Index [-]
1148
- '''
1149
-
1150
- return 100*(np.log10(R_red/100) - np.log10(R_green/100))
1151
-
1152
- def MI(R_red):
1153
- r'''
1154
- | Calculate the Melanin Index (MI) from the reflectance on a chosen red band
1155
- | (usually approx. 655 nm).
1156
- | For details please check Takiwaki et al. 1994 [T*94] and Fullerton et al. 1996 [F*96].
1157
-
1158
- :math:`\mbox{MI} = 100 \mbox{ } [-\mbox{log}_{10}(R_\mbox{red})]`
1159
-
1160
- :param R_red: reflectance on a chosen red band [%]
1161
- :type R_red: float or np.ndarray
1162
-
1163
- :return: - **MI** (*float or np.ndarray* – Melanin Index [-]
1164
- '''
1165
-
1166
- return 100*(-np.log10(R_red/100))
1167
-
1168
- def XYZ_from_spectrum(all_lambda, spectrum, lambda_min = 360., lambda_max = 830., lambda_step = 1.,
1169
- illuminant = 'D65', observer = '10o', cmfs_model = 'CIE', K = 1., interp1d_kind = 'cubic'):
1170
- r'''
1171
- | Calculate the CIE XYZ coordinates from the reflectance spectrum :math:`R(\lambda)` or the
1172
- | transmittance spectrum :math:`T(\lambda)` for a chosen standard illuminant and standard observer.
1173
- | Integration using the composite trapezoid rule from 360 nm to 830 nm (as default).
1174
- | If the wavelength array does not cover the whole region, a constant extrapolation is perfomed.
1175
- | For details please check CIE [CIE04] (see their section 7).
1176
-
1177
- | :math:`X = \frac{K}{N} \int_\lambda \mbox{ } R(\lambda) \mbox{ } S(\lambda) \mbox{ } \bar{x}(\lambda) \mbox{ } d\lambda`
1178
- | :math:`Y = \frac{K}{N} \int_\lambda \mbox{ } R(\lambda) \mbox{ } S(\lambda) \mbox{ } \bar{y}(\lambda) \mbox{ } d\lambda`
1179
- | :math:`Z = \frac{K}{N} \int_\lambda \mbox{ } R(\lambda) \mbox{ } S(\lambda) \mbox{ } \bar{z}(\lambda) \mbox{ } d\lambda`
1180
-
1181
- in which
1182
-
1183
- | :math:`N = \int_\lambda \mbox{ } S(\lambda) \mbox{ } \bar{y}(\lambda) \mbox{ } d\lambda`
1184
-
1185
- The reflectance spectrum :math:`R(\lambda)` is replaced by the transmittance spectrum
1186
- :math:`T(\lambda)` when dealing with color in some cases.
1187
-
1188
- :param all_lambda: wavelength array
1189
- :type all_lambda: np.ndarray
1190
-
1191
- :param spectrum: reflectance or transmittance spectrum respective to the wavelength array [%]
1192
- :type spectrum: np.ndarray
1193
-
1194
- :param lambda_min: lower limit of summation/integration (minimum wavelength to take into account) [nm] (default to 360.)
1195
- :type lambda_min: float
1196
-
1197
- :param lambda_max: upper limit of summation/integration (maximum wavelength to take into account) [nm] (default to 830.)
1198
- :type lambda_max: float
1199
-
1200
- :param lambda_step: summation interval (wavelength step) [nm] (default to 1.)
1201
- :type lambda_step: float
1202
-
1203
- :param illuminant: the user can choose one of the following... 'A', 'D50', 'D55', 'D65' or 'D75'
1204
- :type illuminant: str
1205
-
1206
- :param observer: the user can choose one of the following... '2o' or '10o'
1207
- :type observer: str
1208
-
1209
- :param cmfs_model: the user can choose one of the following... 'CIE', 'Wyman_singlelobe' or 'Wyman_multilobe' (default to 'CIE')
1210
- :type cmfs_model: str
1211
-
1212
- :param K: scaling factor (usually 1. or 100.) [-] (default to 1.)
1213
- :type K: float
1214
-
1215
- :param interp1d_kind: kind argument of scipy.interpolation.interp1d (default to 'cubic' [CIE04] (see their section 7.2.1.1))
1216
- :type interp1d_kind: str
1217
-
1218
- | 'A' refers to the CIE standard illuminant A
1219
- | 'D50' refers to the CIE standard illuminant D50
1220
- | 'D55' refers to the CIE standard illuminant D55
1221
- | 'D65' refers to the CIE standard illuminant D65
1222
- | 'D75' refers to the CIE standard illuminant D75
1223
-
1224
- | '2o' refers to the CIE 1931 2 degree standard observer
1225
- | '10o' refers to the CIE 1964 10 degree standard observer
1226
-
1227
- | 'CIE' for the linear interpolation of data from CIE datasets [CIE19a] [CIE19b]
1228
- | 'Wyman_singlelobe' for the functions from Wyman, Sloan & Shirley 2013 [WSS13] (section 2.1)
1229
- | 'Wyman_multilobe' for the functions from Wyman, Sloan & Shirley 2013 [WSS13] (section 2.2)
1230
-
1231
- | K = 1. for CIE XYZ coordinates in range [0, 1]
1232
- | K = 100. for CIE XYZ coordinates in range [0, 100]
1233
-
1234
- :return: - **X** (*float*) X coordinate [-]
1235
- - **Y** (*float*) Y coordinate [-]
1236
- - **Z** (*float*) Z coordinate [-]
1237
- '''
1238
-
1239
- x = np.arange(lambda_min, lambda_max + lambda_step, lambda_step)
1240
- R_or_T_lambda = interp1d(all_lambda, spectrum/100, kind = interp1d_kind,
1241
- bounds_error = False, fill_value = (spectrum[0]/100,
1242
- spectrum[-1]/100))(x)
1243
- S_lambda = rspd(x, illuminant = illuminant)
1244
- xbar_lambda, ybar_lambda, zbar_lambda = cmfs(x, observer = observer, cmfs_model = cmfs_model)
1245
-
1246
- y0 = S_lambda*ybar_lambda
1247
- N = trapezoid(y0, x = x, dx = lambda_step)
1248
-
1249
- R_or_T_lambda_times_S_lambda = R_or_T_lambda*S_lambda
1250
-
1251
- y1 = R_or_T_lambda_times_S_lambda*xbar_lambda
1252
- X = K/N*trapezoid(y1, x = x, dx = lambda_step)
1253
-
1254
- y2 = R_or_T_lambda_times_S_lambda*ybar_lambda
1255
- Y = K/N*trapezoid(y2, x = x, dx = lambda_step)
1256
-
1257
- y3 = R_or_T_lambda_times_S_lambda*zbar_lambda
1258
- Z = K/N*trapezoid(y3, x = x, dx = lambda_step)
1259
-
1260
- return X, Y, Z
1261
-
1262
- def sRGB_from_spectrum(all_lambda, spectrum, lambda_min = 360, lambda_max = 830, lambda_step = 1,
1263
- cmfs_model = 'CIE', interp1d_kind = 'cubic', sRGB_scale = 'norm'):
1264
- r'''
1265
- | Calculate the sRGB coordinates from the reflectance or the transmittance spectrum.
1266
- | First calculate CIE XYZ coordinates (respective to the standard illuminant D65 and
1267
- | the 2 degree standard observer) from the spectrum and then calculate sRGB coordinates
1268
- | from CIE XYZ coordinates (see functions :meth:`skinoptics.colors.sRGB_from_XYZ` and
1269
- | :meth:`skinoptics.colors.XYZ_from_spectrum`).
1270
-
1271
- :param all_lambda: wavelength array
1272
- :type all_lambda: np.ndarray
1273
-
1274
- :param spectrum: reflectance or transmittance spectrum respective to the wavelength array [%]
1275
- :type spectrum: np.ndarray
1276
-
1277
- :param lambda_min: lower limit of summation/integration (minimum wavelength to take into account) [nm] (default to 360.)
1278
- :type lambda_min: float
1279
-
1280
- :param lambda_max: upper limit of summation/integration (maximum wavelength to take into account) [nm] (default to 830.)
1281
- :type lambda_max: float
1282
-
1283
- :param lambda_step: summation interval (wavelength step) [nm] (default to 1.)
1284
- :type lambda_step: float
1285
-
1286
- :param cmfs_model: the user can choose one of the following... 'CIE', 'Wyman_singlelobe' or 'Wyman_multilobe' (default to 'CIE')
1287
- :type cmfs_model: str
1288
-
1289
- :param interp1d_kind: kind argument of scipy.interpolation.interp1d (default to 'cubic' [CIE04] (see their section 7.2.1.1))
1290
- :type interp1d_kind: str
1291
-
1292
- :param sRGB_scale: the user can choose one of the following... 'norm' or '8bit' (default to 'norm')
1293
- :type sRGB_scale: str
1294
-
1295
- | 'CIE' for the linear interpolation of data from CIE datasets [CIE19a] [CIE19b]
1296
- | 'Wyman_singlelobe' for the functions from Wyman, Sloan & Shirley 2013 [WSS13] (section 2.1)
1297
- | 'Wyman_multilobe' for the functions from Wyman, Sloan & Shirley 2013 [WSS13] (section 2.2)
1298
-
1299
- | 'norm' for sRGB coordinates in range [0,1] (normalized scale)
1300
- | '8bit' for sRGB coordinates in range [0, 255] (8-bit scale)
1301
-
1302
- :return: - **R** (*float*) – R coordinate [-]
1303
- - **G** (*float*)G coordinate [-]
1304
- - **B** (*float*) – B coordinate [-]
1305
- '''
1306
-
1307
- return sRGB_from_XYZ(*XYZ_from_spectrum(all_lambda = all_lambda, spectrum = spectrum,
1308
- lambda_min = lambda_min, lambda_max = lambda_max,
1309
- lambda_step = lambda_step,
1310
- illuminant = 'D65', observer = '2o', cmfs_model = cmfs_model,
1311
- K = 1., interp1d_kind = interp1d_kind),
1312
- K = 1., sRGB_scale = sRGB_scale)
1313
-
1314
- def Lab_from_spectrum(all_lambda, spectrum, lambda_min = 360, lambda_max = 830, lambda_step = 1,
1315
- illuminant = 'D65', observer = '10o', cmfs_model = 'CIE', interp1d_kind = 'cubic'):
1316
- r'''
1317
- | Calculate the CIE L*a*b* coordinates from the reflectance or the transmittance spectrum.
1318
- | First calculate CIE XYZ coordinates from the spectrum for a chosen standard illuminant
1319
- | and standard observer and then calculate CIE L*a*b* coordinates from CIE XYZ coordinates
1320
- | (see functions :meth:`skinoptics.colors.Lab_from_XYZ` and :meth:`skinoptics.colors.XYZ_from_spectrum`).
1321
-
1322
- :param all_lambda: wavelength array
1323
- :type all_lambda: np.ndarray
1324
-
1325
- :param spectrum: reflectance or transmittance spectrum respective to the wavelength array [%]
1326
- :type spectrum: np.ndarray
1327
-
1328
- :param lambda_min: lower limit of summation/integration (minimum wavelength to take into account) [nm] (default to 360.)
1329
- :type lambda_min: float
1330
-
1331
- :param lambda_max: upper limit of summation/integration (maximum wavelength to take into account) [nm] (default to 830.)
1332
- :type lambda_max: float
1333
-
1334
- :param lambda_step: summation interval (wavelength step) [nm] (default to 1.)
1335
- :type lambda_step: float
1336
-
1337
- :param illuminant: the user can choose one of the following... 'A', 'D50', 'D55', 'D65' or 'D75'
1338
- :type illuminant: str
1339
-
1340
- :param observer: the user can choose one of the following... '2o' or '10o'
1341
- :type observer: str
1342
-
1343
- :param cmfs_model: the user can choose one of the following... 'CIE', 'Wyman_singlelobe' or 'Wyman_multilobe' (default to 'CIE')
1344
- :type cmfs_model: str
1345
-
1346
- :param interp1d_kind: kind argument of scipy.interpolation.interp1d (default to 'cubic' [CIE04] (see their section 7.2.1.1))
1347
- :type interp1d_kind: str
1348
-
1349
- | 'A' refers to the CIE standard illuminant A
1350
- | 'D50' refers to the CIE standard illuminant D50
1351
- | 'D55' refers to the CIE standard illuminant D55
1352
- | 'D65' refers to the CIE standard illuminant D65
1353
- | 'D75' refers to the CIE standard illuminant D75
1354
-
1355
- | '2o' refers to the CIE 1931 2 degree standard observer
1356
- | '10o' refers to the CIE 1964 10 degree standard observer
1357
-
1358
- | 'CIE' for the linear interpolation of data from CIE datasets [CIE19a] [CIE19b]
1359
- | 'Wyman_singlelobe' for the functions from Wyman, Sloan & Shirley 2013 [WSS13] (section 2.1)
1360
- | 'Wyman_multilobe' for the functions from Wyman, Sloan & Shirley 2013 [WSS13] (section 2.2)
1361
-
1362
- :return: - **L** (*float*) L* coordinate [-]
1363
- - **a** (*float*) – a* coordinate [-]
1364
- - **b** (*float*) b* coordinate [-]
1365
- '''
1366
-
1367
- return Lab_from_XYZ(*XYZ_from_spectrum(all_lambda = all_lambda, spectrum = spectrum,
1368
- lambda_min = lambda_min, lambda_max = lambda_max,
1369
- lambda_step = lambda_step,
1370
- illuminant = illuminant, observer = observer, cmfs_model = cmfs_model,
1371
- K = 1., interp1d_kind = interp1d_kind),
1372
- illuminant = illuminant, observer = observer, K = 1.)
1373
-
1374
- def sRGB_from_lambda0(lambda0, cmfs_model = 'CIE', sRGB_scale = 'norm'):
1375
- r'''
1376
- | Calculate the sRGB coordinates respective to the color of a monochromatic light
1377
- | (single wavelength).
1378
-
1379
- wavelength range: [360 nm, 830 nm]
1380
-
1381
- :param lambda0: wavelength of the monochromatic light [nm]
1382
- :type lambda0: float or np.ndarray
1383
-
1384
- :param cmfs_model: the user can choose one of the following... 'CIE', 'Wyman_singlelobe' or 'Wyman_multilobe' (default to 'CIE')
1385
- :type cmfs_model: str
1386
-
1387
- :param sRGB_scale: the user can choose one of the following... 'norm' or '8bit' (default to 'norm')
1388
- :type sRGB_scale: str
1389
-
1390
- | 'CIE' for the linear interpolation of data from CIE datasets [CIE19a] [CIE19b]
1391
- | 'Wyman_singlelobe' for the functions from Wyman, Sloan & Shirley 2013 [WSS13] (section 2.1)
1392
- | 'Wyman_multilobe' for the functions from Wyman, Sloan & Shirley 2013 [WSS13] (section 2.2)
1393
-
1394
- | 'norm' for sRGB coordinates in range [0,1] (normalized scale)
1395
- | '8bit' for sRGB coordinates in range [0, 255] (8-bit scale)
1396
-
1397
- :return: - **R** (*float or np.ndarray*) – R coordinate [-]
1398
- - **G** (*float or np.ndarray*) G coordinate [-]
1399
- - **B** (*float or np.ndarray*) – B coordinate [-]
1400
- '''
1401
-
1402
- return sRGB_from_XYZ(*cmfs(lambda0, observer = '2o', cmfs_model = cmfs_model),
1403
- K = 1., sRGB_scale = sRGB_scale)
1
+ '''
2
+ | SkinOptics
3
+ | Copyright (C) 2024-2025 Victor Lima
4
+
5
+ | This program is free software: you can redistribute it and/or modify
6
+ | it under the terms of the GNU General Public License as published by
7
+ | the Free Software Foundation, either version 3 of the License, or
8
+ | (at your option) any later version.
9
+
10
+ | This program is distributed in the hope that it will be useful,
11
+ | but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ | GNU General Public License for more details.
14
+
15
+ | You should have received a copy of the GNU General Public License
16
+ | along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ | Victor Lima
19
+ | victor.lima\@ufscar.br
20
+ | victorportog.github.io
21
+
22
+ | Release date:
23
+ | October 2024
24
+ | Last modification:
25
+ | October 2025
26
+
27
+ | References:
28
+
29
+ | [CCH91] Chardon, Cretois & Hourseau 1991.
30
+ | Skin colour typology and suntanning pathways.
31
+ | https://doi.org/10.1111/j.1467-2494.1991.tb00561.x
32
+
33
+ | [T*94] Takiwaki, Shirai, Kanno, Watanabe & Arase 1994.
34
+ | Quantification of erythema and pigmentation using a videomicroscope and a computer.
35
+ | https://doi.org/10.1111/j.1365-2133.1994.tb08462.x
36
+
37
+ | [F*96] Fullerton, Fischer, Lahti, Wilhelm, Takiwaki & Serup 1996.
38
+ | Guidetines for measurement of skin colour and erythema: A report from the Standardization Group of the European Society of Contact Dermatitis.
39
+ | https://doi.org/10.1111/j.1600-0536.1996.tb02258.x
40
+
41
+ | [S*96] Stokes, Anderson, Chandrasekar & Motta 1996.
42
+ | A Standard Default Color Space for the Internet - sRGB.
43
+ | https://www.w3.org/Graphics/Color/sRGB.html
44
+
45
+ | [IEC99] IEC 1999.
46
+ | IEC 61966-2-1:1999 - Multimedia systems and equipment - Colour measurement and management - Part 2-1: Colour management - Default RGB colour space - sRGB.
47
+
48
+ | [CIE04] CIE 2004.
49
+ | Colorimetry, 3rd edition.
50
+ | CIE 15:2004
51
+
52
+ | [SWD04] Sharma, Wu & Dalal 2004.
53
+ | The CIEDE2000 Color-Difference Formula: Implementation Notes, Supplementary Test Data, and Mathematical Observations.
54
+ | https://doi.org/10.1002/col.20070
55
+
56
+ | [D*06] Del Bino, Sok, Bessac & Bernerd 2006.
57
+ | Relationship between skin response to ultraviolet exposure and skin color type.
58
+ | https://doi.org/10.1111/j.1600-0749.2006.00338.x
59
+
60
+ | [S07] Schanda (editor) 2007.
61
+ | Colorimetry: Understanding the CIE System.
62
+ | http://dx.doi.org/10.1002/9780470175637
63
+
64
+ | [IC08] ISO/CIE 2008.
65
+ | ISO 11664-4:2008 - CIE S 014-4/E:2007 - Colorimetry - Part 4: CIE 1976 L*a*b* Colour space.
66
+
67
+ | [HP11] Hunt & Pointer 2011.
68
+ | Measuring Colour.
69
+ | https://doi.org/10.1002/9781119975595
70
+
71
+ | [DB13] Del Bino & Bernerd 2013.
72
+ | Variations in skin colour and the biological consequences of ultraviolet radiation exposure.
73
+ | https://doi.org/10.1111/bjd.12529
74
+
75
+ | [WSS13] Wyman, Sloan & Shirley 2013.
76
+ | Simple Analytic Approximations to the CIE XYZ Color Matching Functions.
77
+ | https://jcgt.org/published/0002/02/01/
78
+
79
+ | [IC14] ISO/CIE 2014.
80
+ | ISO/CIE 11664-6:2014 - Colorimetry - Part 6: CIEDE2000 Colour-difference formula.
81
+
82
+ | [CIE18a] CIE 2018.
83
+ | CIE standard illuminant A - 1 nm.
84
+ | https://doi.org/10.25039/CIE.DS.8jsxjrsn
85
+
86
+ | [CIE18b] CIE 2018.
87
+ | CIE standard illuminant D55.
88
+ | https://doi.org/10.25039/CIE.DS.qewfb3kp
89
+
90
+ | [CIE18c] CIE 2018.
91
+ | CIE standard illuminant D75.
92
+ | https://doi.org/10.25039/CIE.DS.9fvcmrk4
93
+
94
+ | [CIE19a] CIE 2019.
95
+ | CIE 1931 colour-matching functions, 2 degree observer.
96
+ | https://doi.org/10.25039/CIE.DS.xvudnb9b
97
+
98
+ | [CIE19b] CIE 2019.
99
+ | CIE 1964 colour-matching functions, 10 degree observer
100
+ | https://doi.org/10.25039/CIE.DS.sqksu2n5
101
+
102
+ | [L*20] Ly, Dyer, Feig, Chien & Del Bino 2020.
103
+ | Research Techniques Made Simple: Cutaneous Colorimetry: A Reliable Technique for Objective Skin Color Measurement.
104
+ | https://doi.org/10.1016/j.jid.2019.11.003
105
+
106
+ | [CIE22a] CIE 2022.
107
+ | CIE standard illuminant D50.
108
+ | https://doi.org/10.25039/CIE.DS.etgmuqt5
109
+
110
+ | [CIE22b] CIE 2022.
111
+ | CIE standard illuminant D65.
112
+ | https://doi.org/10.25039/CIE.DS.hjfjmt59
113
+ '''
114
+
115
+ import numpy as np
116
+ from scipy.interpolate import interp1d
117
+ from scipy.integrate import trapezoid
118
+
119
+ from skinoptics.utils import *
120
+ from skinoptics.dataframes import *
121
+
122
+ def rspd(lambda0, illuminant):
123
+ r'''
124
+ | The relative spectral power distribution S(:math:`\lambda`) of a chosen standard illuminant
125
+ | as a function of wavelength.
126
+ | Linear interpolation of data from CIE datasets [CIE18a] [CIE22a] [CIE18b] [CIE22b] [CIE18c].
127
+
128
+ | wavelength range:
129
+ | [300 nm, 830 nm] (at 1 nm intervals, for illuminant = 'A', 'D50' or 'D65')
130
+ | or [300 nm, 780 nm] (at 5 nm intervals, for illuminant = 'D55' or 'D75')
131
+
132
+ :param lambda0: wavelength [nm] (must be in range [300 nm, 830 nm] or [300 nm, 780 nm])
133
+ :type lambda0: float or np.ndarray
134
+
135
+ :param illuminant: the user can choose one of the following... 'A', 'D50', 'D55', 'D65' or 'D75'
136
+ :type illuminant: str
137
+
138
+ | 'A' refers to the CIE standard illuminant A
139
+ | 'D50' refers to the CIE standard illuminant D50
140
+ | 'D55' refers to the CIE standard illuminant D55
141
+ | 'D65' refers to the CIE standard illuminant D65
142
+ | 'D75' refers to the CIE standard illuminant D75
143
+
144
+ :return: - **rspd** (*float or np.ndarray*) – relative spectral power distribution [-]
145
+ '''
146
+
147
+ if illuminant == 'A' or 'D50' or 'D65':
148
+ if isinstance(lambda0, np.ndarray) == True:
149
+ if np.any(lambda0 < 300) or np.any(lambda0 > 830):
150
+ msg = 'At least one element in the input lambda0 is out of the range [300 nm, 830 nm].'
151
+ raise Exception(msg)
152
+ else:
153
+ if lambda0 < 300 or lambda0 > 830:
154
+ msg = 'The input lambda0 = {} nm is out of the range [300 nm, 830 nm].'.format(lambda0)
155
+ raise Exception(msg)
156
+ elif illuminant == 'D55' or 'D75':
157
+ if isinstance(lambda0, np.ndarray) == True:
158
+ if np.any(lambda0 < 300) or np.any(lambda0 > 780):
159
+ msg = 'At least one element in the input lambda0 is out of the range [300 nm, 780 nm].'
160
+ raise Exception(msg)
161
+ else:
162
+ if lambda0 < 300 or lambda0 > 780:
163
+ msg = 'The input lambda0 = {} nm is out of the range [300 nm, 780 nm].'.format(lambda0)
164
+ raise Exception(msg)
165
+
166
+ if illuminant == 'A':
167
+ rspd = interp1d(np.array(rspds_A_D50_D65_dataframe)[:,0],
168
+ np.array(rspds_A_D50_D65_dataframe)[:,1])(lambda0)
169
+ elif illuminant == 'D50':
170
+ rspd = interp1d(np.array(rspds_A_D50_D65_dataframe)[:,0],
171
+ np.array(rspds_A_D50_D65_dataframe)[:,2])(lambda0)
172
+ elif illuminant == 'D55':
173
+ rspd = interp1d(np.array(rspds_D55_D75_dataframe)[:,0],
174
+ np.array(rspds_D55_D75_dataframe)[:,1])(lambda0)
175
+ elif illuminant == 'D65':
176
+ rspd = interp1d(np.array(rspds_A_D50_D65_dataframe)[:,0],
177
+ np.array(rspds_A_D50_D65_dataframe)[:,3])(lambda0)
178
+ elif illuminant == 'D75':
179
+ rspd = interp1d(np.array(rspds_D55_D75_dataframe)[:,0],
180
+ np.array(rspds_D55_D75_dataframe)[:,2])(lambda0)
181
+ else:
182
+ msg = 'The input illuminant = {} is not valid.'.format(illuminant)
183
+ raise Exception(msg)
184
+
185
+ return rspd
186
+
187
+ def cmfs(lambda0, observer, cmfs_model = 'CIE'):
188
+ r'''
189
+ | The CIE color-matching functions :math:`\bar{x}(\lambda)`, :math:`\bar{y}(\lambda)` and :math:`\bar{z}(\lambda)` for a chosen standard observer
190
+ | as a function of wavelength.
191
+
192
+ | wavelength range: [360 nm, 830 nm] (at 1 nm intervals for cmfs_model = 'CIE')
193
+
194
+ :param lambda0: wavelength [nm] (must be in range [360., 830.] for cmfs_model = 'CIE')
195
+ :type lambda0: float or np.ndarray
196
+
197
+ :param observer: the user can choose one of the following... '2o' or '10o'
198
+ :type observer: str
199
+
200
+ :param cmfs_model: the user can choose one of the following... 'CIE', 'Wyman_singlelobe' or 'Wyman_multilobe' (default to 'CIE')
201
+ :type cmfs_model: str
202
+
203
+ | '2o' refers to the CIE 1931 2 degree standard observer
204
+ | '10o' refers to the CIE 1964 10 degree standard observer
205
+
206
+ | 'CIE' for the linear interpolation of data from CIE datasets [CIE19a] [CIE19b]
207
+ | 'Wyman_singlelobe' for the functions from Wyman, Sloan & Shirley 2013 [WSS13] (section 2.1)
208
+ | 'Wyman_multilobe' for the functions from Wyman, Sloan & Shirley 2013 [WSS13] (section 2.2)
209
+
210
+ :return: - **xbar** (*float or np.ndarray*) :math:`\bar{x}(\lambda`) color-matching function [-]
211
+ - **ybar** (*float or np.ndarray*) – :math:`\bar{y}(\lambda`) color-matching function [-]
212
+ - **zbar** (*float or np.ndarray*) – :math:`\bar{z}(\lambda`) color-matching function [-]
213
+ '''
214
+
215
+ if cmfs_model == 'CIE':
216
+ if isinstance(lambda0, np.ndarray) == True:
217
+ if np.any(lambda0 < 360) or np.any(lambda0 > 830):
218
+ msg = 'At least one element in the input lambda0 is out of the range [360 nm, 830 nm].'
219
+ raise Exception(msg)
220
+ else:
221
+ if lambda0 < 360 or lambda0 > 830:
222
+ msg = 'The input lambda0 = {} nm is out of the range [360 nm, 830 nm].'.format(lambda0)
223
+ raise Exception(msg)
224
+ if observer == '2o':
225
+ xbar = interp1d(np.array(cmfs_dataframe)[:,0],
226
+ np.array(cmfs_dataframe)[:,1])(lambda0)
227
+ ybar = interp1d(np.array(cmfs_dataframe)[:,0],
228
+ np.array(cmfs_dataframe)[:,2])(lambda0)
229
+ zbar = interp1d(np.array(cmfs_dataframe)[:,0],
230
+ np.array(cmfs_dataframe)[:,3])(lambda0)
231
+ elif observer == '10o':
232
+ xbar = interp1d(np.array(cmfs_dataframe)[:,0],
233
+ np.array(cmfs_dataframe)[:,4])(lambda0)
234
+ ybar = interp1d(np.array(cmfs_dataframe)[:,0],
235
+ np.array(cmfs_dataframe)[:,5])(lambda0)
236
+ zbar = interp1d(np.array(cmfs_dataframe)[:,0],
237
+ np.array(cmfs_dataframe)[:,6])(lambda0)
238
+ else:
239
+ msg = 'The input observer = {} is not valid.'.format(observer)
240
+ raise Exception(msg)
241
+ elif cmfs_model == 'Wyman_singlelobe':
242
+ if observer == '2o':
243
+ xbar = gaussian(lambda0, 1.065, 595.8, 33.33) \
244
+ + gaussian(lambda0, 0.366, 446.8, 19.44)
245
+ ybar = gaussian(np.log(lambda0), 1.014, np.log(556.3), 0.075)
246
+ zbar = gaussian(np.log(lambda0), 1.839, np.log(449.8), 0.051)
247
+ elif observer == '10o':
248
+ xbar = mod_gaussian_Wyman(lambda0, 0.398, -570.1, 1014, 1250) \
249
+ + mod_gaussian_Wyman(-lambda0, 1.132, -1338, 743.5, 234)
250
+ ybar = gaussian(lambda0, 1.011, 556.1, 46.14)
251
+ zbar = mod_gaussian_Wyman(lambda0, 2.06, 265.8, 180.4,32)
252
+ else:
253
+ msg = 'The input observer = {} is not valid.'.format(observer)
254
+ raise Exception(msg)
255
+ elif cmfs_model == 'Wyman_multilobe':
256
+ if observer == '2o':
257
+ coeffs = [[0.362, 1.056, -0.065, 0.821, 0.286, 0., 1.217, 0.681, 0.],
258
+ [442.0, 599.8, 501.1, 568.8, 530.9, 0., 437.0, 459.0, 0.],
259
+ [0.0624, 0.0264, 0.0490, 0.0213, 0.0613, 0., 0.0845, 0.0385, 0.],
260
+ [0.0374, 0.0323, 0.0382, 0.0247, 0.0322, 0., 0.0278, 0.0725, 0.]]
261
+ if isinstance(lambda0, np.ndarray) == True:
262
+ xbar, ybar, zbar = np.zeros((3,len(lambda0)))
263
+ c = 0
264
+ for j in range(len(lambda0)):
265
+ X, Y, Z = 0., 0., 0.
266
+ for i in range(3):
267
+ X += piecewise_gaussian_Wyman(lambda0[j], coeffs[0][i], coeffs[1][i],
268
+ coeffs[2][i], coeffs[3][i])
269
+ Y += piecewise_gaussian_Wyman(lambda0[j], coeffs[0][i+3], coeffs[1][i+3],
270
+ coeffs[2][i+3], coeffs[3][i+3])
271
+ Z += piecewise_gaussian_Wyman(lambda0[j], coeffs[0][i+6], coeffs[1][i+6],
272
+ coeffs[2][i+6], coeffs[3][i+6])
273
+ xbar[j], ybar[j], zbar[j] = X, Y, Z
274
+ elif isinstance(lambda0, (int, float)) == True:
275
+ X, Y, Z = 0., 0., 0.
276
+ for i in range(3):
277
+ X += piecewise_gaussian_Wyman(lambda0, coeffs[0][i], coeffs[1][i],
278
+ coeffs[2][i], coeffs[3][i])
279
+ Y += piecewise_gaussian_Wyman(lambda0, coeffs[0][i+3], coeffs[1][i+3],
280
+ coeffs[2][i+3], coeffs[3][i+3])
281
+ Z += piecewise_gaussian_Wyman(lambda0, coeffs[0][i+6], coeffs[1][i+6],
282
+ coeffs[2][i+6], coeffs[3][i+6])
283
+ xbar, ybar, zbar = X, Y, Z
284
+ else:
285
+ msg = 'The input lambda0 must be int, float or np.ndarray.'
286
+ raise Exception(msg)
287
+ else:
288
+ msg = 'The input observer = {} is not valid for cmfs_model = Wyman_multilobe.'.format(observer)
289
+ raise Exception(msg)
290
+ else:
291
+ msg = 'The input cmfs_model = {} is not valid.'.format(cmfs_model)
292
+ raise Exception(msg)
293
+
294
+ return xbar, ybar, zbar
295
+
296
+ def xy_from_XYZ(X, Y, Z):
297
+ r'''
298
+ | Calculate CIE xy chromaticities from CIE XYZ coordinates.
299
+
300
+ | :math:`x = \frac{X}{X + Y + Z}`
301
+ | :math:`y = \frac{Y}{X + Y + Z}`
302
+
303
+ :param X: X coordinate [-]
304
+ :type X: float or np.ndarray
305
+
306
+ :param Y: Y coordinate [-]
307
+ :type Y: float or np.ndarray
308
+
309
+ :param Z: Z coordinate [-]
310
+ :type Z: float or np.ndarray
311
+
312
+ :return: - **x** (*float or np.ndarray*) – x chromaticity [-]
313
+ - **y** (*float or np.ndarray*) y chromaticity [-]
314
+ '''
315
+
316
+ x = X/(X + Y + Z)
317
+ y = Y/(X + Y + Z)
318
+
319
+ return x, y
320
+
321
+ def XYZ_wp(illuminant, observer, cmfs_model = 'CIE', K = 1.):
322
+ r'''
323
+ The white point CIE XYZ coordinates for a chosen standard illuminant and standard observer.
324
+
325
+ :param illuminant: the user can choose one of the following... 'A', 'D50', 'D55', 'D65' or 'D75'
326
+ :type illuminant: str
327
+
328
+ :param observer: the user can choose one of the following... '2o' or '10o'
329
+ :type observer: str
330
+
331
+ :param cmfs_model: the user can choose one of the following... 'CIE', 'Wyman_singlelobe' or 'Wyman_multilobe' (default to 'CIE')
332
+ :type cmfs_model: str
333
+
334
+ :param K: scaling factor (usually 1. or 100.) [-] (default to 1.)
335
+ :type K: float
336
+
337
+ | 'A' refers to the CIE standard illuminant A
338
+ | 'D50' refers to the CIE standard illuminant D50
339
+ | 'D55' refers to the CIE standard illuminant D55
340
+ | 'D65' refers to the CIE standard illuminant D65
341
+ | 'D75' refers to the CIE standard illuminant D75
342
+
343
+ | '2o' refers to the CIE 1931 2 degree standard observer
344
+ | '10o' refers to the CIE 1964 10 degree standard observer
345
+
346
+ | 'CIE' for the linear interpolation of data from CIE datasets [CIE19a] [CIE19b]
347
+ | 'Wyman_singlelobe' for the functions from Wyman, Sloan & Shirley 2013 [WSS13] (section 2.1)
348
+ | 'Wyman_multilobe' for the functions from Wyman, Sloan & Shirley 2013 [WSS13] (section 2.2)
349
+
350
+ | K = 1. for CIE XYZ coordinates in range [0, 1]
351
+ | K = 100. for CIE XYZ coordinates in range [0, 100]
352
+
353
+ :return: - **Xn** (*float*) – white point X coordinate [-]
354
+ - **Yn** (*float*) white point Y coordinate [-]
355
+ - **Zn** (*float*) white point Z coordinate [-]
356
+ '''
357
+
358
+ if illuminant == 'D55' or illuminant == 'D75':
359
+ Xn, Yn, Zn = XYZ_from_spectrum(np.arange(360, 780, 1), np.ones(len(np.arange(360, 780, 1)))*100,
360
+ lambda_max = 780, illuminant = illuminant, observer = observer, cmfs_model = cmfs_model, K = K)
361
+ else:
362
+ Xn, Yn, Zn = XYZ_from_spectrum(np.arange(360, 830, 1), np.ones(len(np.arange(360, 830, 1)))*100,
363
+ illuminant = illuminant, observer = observer, cmfs_model = cmfs_model, K = K)
364
+
365
+ return Xn, Yn, Zn
366
+
367
+ def xy_wp(illuminant, observer):
368
+ r'''
369
+ | The white point CIE xy chromaticities for a chosen standard illuminant and standard observer.
370
+ | Calculated from the white point CIE XYZ coordinates (see function :meth:`skinoptics.colors.XYZ_wp`).
371
+
372
+ :param illuminant: the user can choose one of the following... 'A', 'D50', 'D55', 'D65' or 'D75'
373
+ :type illuminant: str
374
+
375
+ :param observer: the user can choose one of the following... '2o' or '10o'
376
+ :type observer: str
377
+
378
+ | 'A' refers to the CIE standard illuminant A
379
+ | 'D50' refers to the CIE standard illuminant D50
380
+ | 'D55' refers to the CIE standard illuminant D55
381
+ | 'D65' refers to the CIE standard illuminant D65
382
+ | 'D75' refers to the CIE standard illuminant D75
383
+
384
+ | '2o' refers to the CIE 1931 2 degree standard observer
385
+ | '10o' refers to the CIE 1964 10 degree standard observer
386
+
387
+ :return: - **xn** (*float*) – white point CIE x chromaticity [-]
388
+ - **yn** (*float*) – white point CIE y chromaticity [-]
389
+ '''
390
+
391
+ Xn, Yn, Zn = XYZ_wp(illuminant = illuminant, observer = observer)
392
+ xn, yn = xy_from_XYZ(Xn, Yn, Zn)
393
+
394
+ return xn, yn
395
+
396
+ def transf_matrix_sRGB_linear_from_XYZ():
397
+ r'''
398
+ The transformation matrix employed to obtain linear sRGB coordinates from CIE XYZ coordinates.
399
+
400
+ :math:`\mathcal{M} =
401
+ \begin{bmatrix}
402
+ 3.24062 & -1.5372 & -0.4986 \\
403
+ -0.9689 & 1.8758 & 0.0415 \\
404
+ 0.0557 & -0.2040 & 1.0570
405
+ \end{bmatrix}`
406
+
407
+ :returns: - **M** (*np.ndarray*) – transformation matrix
408
+ '''
409
+
410
+ return np.array([[3.24062, -1.5372, -0.4986], [-0.9689, 1.8758, 0.0415], [0.0557, -0.2040, 1.0570]])
411
+
412
+ def nonlinear_corr_sRGB(u):
413
+ r'''
414
+ The nonlinear correction for sRGB coordinates.
415
+
416
+ :math:`\gamma(u) =
417
+ \left \{ \begin{matrix}
418
+ 12.92 \mbox{ } u, & \mbox{if } u \le 0.0031308 \\
419
+ 1.055 \mbox{ } u^{1/2.4} - 0.055, & \mbox{if } u > 0.0031308 \\
420
+ \end{matrix} \right.`
421
+
422
+ :param u: linear R, G or B coordinate [-]
423
+ :type u: float or np.ndarray
424
+
425
+ :return: - **gamma** (*float or np.ndarray*) – nonlinear R, G or B coordinate [-]
426
+ '''
427
+
428
+ if isinstance(u, np.ndarray) == True:
429
+ gamma = np.zeros(len(u))
430
+ for i in range(len(u)):
431
+ if u[i] <= 0.0031308:
432
+ gamma[i] = 12.92*u[i]
433
+ else:
434
+ gamma[i] = 1.055*u[i]**(1./2.4) - 0.055
435
+ elif isinstance(u, (int, float)) == True:
436
+ if u <= 0.0031308:
437
+ gamma = 12.92*u
438
+ else:
439
+ gamma = 1.055*u**(1./2.4) - 0.055
440
+ else:
441
+ msg = 'u must be int, float or np.ndarray.'
442
+ raise Exception(msg)
443
+
444
+ return gamma
445
+
446
+ def inv_nonlinear_corr_sRGB(u):
447
+ r'''
448
+ The inverse nonlinear correction for sRGB coordinates.
449
+
450
+ :math:`\gamma^{-1}(u) =
451
+ \left \{ \begin{matrix}
452
+ u/12.92, & \mbox{if } u \le 0.04045 \\
453
+ [(u + 0.055)/1.055]^{2.4}, & \mbox{if } u > 0.04045 \\
454
+ \end{matrix} \right.`
455
+
456
+ :param u: nonlinear R, G or B coordinate [-]
457
+ :type u: float or np.ndarray
458
+
459
+ :return: - **inv_gamma** (*float or np.ndarray*) – linear R, G or B coordinate [-]
460
+ '''
461
+
462
+ if isinstance(u, np.ndarray) == True:
463
+ inv_gamma = np.zeros(len(u))
464
+ for i in range(len(u)):
465
+ if u[i] <= 0.04045:
466
+ inv_gamma[i] = u[i]/12.92
467
+ else:
468
+ inv_gamma[i] = ((u[i] + 0.055)/1.055)**(2.4)
469
+ elif isinstance(u, (int, float)) == True:
470
+ if u <= 0.04045:
471
+ inv_gamma = u/12.92
472
+ else:
473
+ inv_gamma = ((u + 0.055)/1.055)**(2.4)
474
+ else:
475
+ msg = 'u must be int, float or np.ndarray.'
476
+ raise Exception(msg)
477
+
478
+ return inv_gamma
479
+
480
+ def sRGB_from_XYZ(X, Y, Z, K = 1., sRGB_scale = 'norm'):
481
+ r'''
482
+ | Calculate sRGB coordinates from CIE XYZ coordinates.
483
+ | CIE XYZ coordinates must be for the standard illuminant D65 and the 2 degree standard observer.
484
+ | For details please check Stokes et al. [S*96] and IEC [IEC99].
485
+
486
+ :math:`\begin{bmatrix}
487
+ R \\
488
+ G \\
489
+ B
490
+ \end{bmatrix}
491
+ =
492
+ \begin{bmatrix}
493
+ \gamma(R_{linear}) \\
494
+ \gamma(G_{linear}) \\
495
+ \gamma(B_{linear})
496
+ \end{bmatrix}`
497
+
498
+ in which
499
+
500
+ :math:`\begin{bmatrix}
501
+ R_{linear} \\
502
+ G_{linear} \\
503
+ B_{linear}
504
+ \end{bmatrix}
505
+ =
506
+ \mathcal{M}
507
+ \begin{bmatrix}
508
+ X \\
509
+ Y \\
510
+ Z
511
+ \end{bmatrix}`
512
+
513
+ and
514
+
515
+ :math:`\gamma(u) =
516
+ \left \{ \begin{matrix}
517
+ 12.92 \mbox{ } u, & \mbox{if } u \le 0.0031308 \\
518
+ 1.055 \mbox{ } u^{1/2.4} - 0.055, & \mbox{if } u > 0.0031308 \\
519
+ \end{matrix} \right.`
520
+
521
+ :param X: X coordinate [-]
522
+ :type X: float or np.ndarray
523
+
524
+ :param Y: Y coordinate [-]
525
+ :type Y: float or np.ndarray
526
+
527
+ :param Z: Z coordinate [-]
528
+ :type Z: float or np.ndarray
529
+
530
+ :param K: scaling factor (usually 1. or 100.) [-] (default to 1.)
531
+ :type K: float
532
+
533
+ :param sRGB_scale: the user can choose one of the following... 'norm' or '8bit'
534
+ :type sRGB_scale:: str (default to 'norm')
535
+
536
+ | K = 1. for CIE XYZ coordinates in range [0, 1]
537
+ | K = 100. for CIE XYZ coordinates in range [0, 100]
538
+
539
+ | 'norm' for sRGB coordinates in range [0,1] (normalized scale)
540
+ | '8bit' for sRGB coordinates in range [0, 255] (8-bit scale)
541
+
542
+ :return: - **R** (*float or np.ndarray*) R coordinate [-]
543
+ - **G** (*float or np.ndarray*) G coordinate [-]
544
+ - **B** (*float or np.ndarray*) B coordinate [-]
545
+ '''
546
+
547
+ M = transf_matrix_sRGB_linear_from_XYZ()
548
+
549
+ if isinstance(X, np.ndarray) == True and \
550
+ isinstance(Y, np.ndarray) == True and \
551
+ isinstance(Z, np.ndarray) == True:
552
+ if len(X) - len(Y) != 0 or len(X) - len(Z) != 0:
553
+ msg = 'X, Y and Z must have the same length.'
554
+ raise Exception(msg)
555
+ R, G, B = np.zeros((3, len(X)))
556
+ for i in range(len(X)):
557
+ R_linear, G_linear, B_linear = np.clip(np.matmul(M, np.array([X[i]/K, Y[i]/K, Z[i]/K])), 0, 1)
558
+ R[i], G[i], B[i] = nonlinear_corr_sRGB(np.array([R_linear, G_linear, B_linear]))
559
+ elif isinstance(X, (int, float)) == True and \
560
+ isinstance(Y, (int, float)) == True and \
561
+ isinstance(Z, (int, float)):
562
+ R_linear, G_linear, B_linear = np.clip(np.matmul(M, np.array([X/K, Y/K, Z/K])), 0 , 1)
563
+ R, G, B = nonlinear_corr_sRGB(np.array([R_linear, G_linear, B_linear]))
564
+ else:
565
+ msg = 'X, Y and Z must be int, float or np.ndarray.'
566
+ raise Exception(msg)
567
+
568
+ if sRGB_scale == 'norm':
569
+ pass
570
+ elif sRGB_scale == '8bit':
571
+ scaling = 255
572
+ R, G, B = np.round(scaling*np.array([R, G, B]))
573
+ else:
574
+ msg = 'The input sRGB_scale = {} is not valid.'.format(sRGB_scale)
575
+ raise Exception(msg)
576
+
577
+ return R, G, B
578
+
579
+ def XYZ_from_sRGB(R, G, B, K = 1., sRGB_scale = 'norm'):
580
+ r'''
581
+ | Calculate CIE XYZ coordinates from sRGB coordinates.
582
+ | The obtained CIE XYZ coordinates are respective to the standard illuminant D65 and the
583
+ | 2 degree standard observer.
584
+ | For details please check Stokes et al. [S*96] and IEC [IEC99].
585
+
586
+ :math:`\begin{bmatrix}
587
+ X \\
588
+ Y \\
589
+ Z
590
+ \end{bmatrix}
591
+ =
592
+ \mathcal{M}^{-1}
593
+ \begin{bmatrix}
594
+ R_{linear} \\
595
+ G_{linear} \\
596
+ B_{linear}
597
+ \end{bmatrix}`
598
+
599
+ in which
600
+
601
+ :math:`\begin{bmatrix}
602
+ R_{linear} \\
603
+ G_{linear} \\
604
+ B_{linear}
605
+ \end{bmatrix}
606
+ =
607
+ \begin{bmatrix}
608
+ \gamma^{-1}(R) \\
609
+ \gamma^{-1}(G) \\
610
+ \gamma^{-1}(B)
611
+ \end{bmatrix}`
612
+
613
+ and
614
+
615
+ :math:`\gamma^{-1}(u) =
616
+ \left \{ \begin{matrix}
617
+ u/12.92, & \mbox{if } u \le 0.04045 \\
618
+ [(u + 0.055)/1.055]^{2.4}, & \mbox{if } u > 0.04045 \\
619
+ \end{matrix} \right.`
620
+
621
+ :param R: R coordinate [-]
622
+ :type R: float or np.ndarray
623
+
624
+ :param G: G coordinate [-]
625
+ :type G: float or np.ndarray
626
+
627
+ :param B: B coordinate [-]
628
+ :type B: float or np.ndarray
629
+
630
+ :param K: scaling factor (usually 1. or 100.) [-] (default to 1.)
631
+ :type K: float
632
+
633
+ :param sRGB_scale: the user can choose one of the following... 'norm' or '8bit'
634
+ :type sRGB_scale:: str (default to 'norm')
635
+
636
+ | K = 1. for CIE XYZ coordinates in range [0, 1]
637
+ | K = 100. for CIE XYZ coordinates in range [0, 100]
638
+
639
+ | 'norm' for sRGB coordinates in range [0,1] (normalized scale)
640
+ | '8bit' for sRGB coordinates in range [0, 255] (8-bit scale)
641
+
642
+ :return: - **X** (*float or np.ndarray*) – X coordinate [-]
643
+ - **Y** (*float or np.ndarray*) Y coordinate [-]
644
+ - **Z** (*float or np.ndarray*) – Z coordinate [-]
645
+ '''
646
+
647
+ if sRGB_scale == 'norm':
648
+ pass
649
+ elif sRGB_scale == '8bit':
650
+ scaling = 255
651
+ R, G, B = np.array([R, G, B])/scaling
652
+ else:
653
+ msg = 'The input sRGB_scale = {} is not valid.'.format(sRGB_scale)
654
+ raise Exception(msg)
655
+
656
+ inv_M = np.round(np.linalg.inv(transf_matrix_sRGB_linear_from_XYZ()), 4)
657
+
658
+ if isinstance(R, np.ndarray) == True and \
659
+ isinstance(G, np.ndarray) == True and \
660
+ isinstance(B, np.ndarray) == True:
661
+ if len(R) - len(G) != 0 or len(R) - len(B) != 0:
662
+ msg = 'R, G and B must have the same length.'
663
+ raise Exception(msg)
664
+ X, Y, Z = np.zeros((3, len(R)))
665
+ for i in range(len(R)):
666
+ R_linear, G_linear, B_linear = inv_nonlinear_corr_sRGB(np.array([R[i], G[i], B[i]]))
667
+ X[i], Y[i], Z[i] = np.matmul(inv_M, np.array([R_linear, G_linear, B_linear]))
668
+ elif isinstance(R, (int, float)) == True and \
669
+ isinstance(G, (int, float)) == True and \
670
+ isinstance(B, (int, float)):
671
+ R_linear, G_linear, B_linear = inv_nonlinear_corr_sRGB(np.array([R, G, B]))
672
+ X, Y, Z = np.matmul(inv_M, np.array([R_linear, G_linear, B_linear]))
673
+ else:
674
+ msg = 'R, G and B must be int, float or np.ndarray.'
675
+ raise Exception(msg)
676
+
677
+ return X*K, Y*K, Z*K
678
+
679
+ def f_Lab_from_XYZ(u):
680
+ r'''
681
+ | The function :math:`f(u)` used to calculate CIE L*a*b* coordinates from CIE XYZ coordinates
682
+ | (see function :meth:`skinoptics.colors.Lab_from_XYZ`).
683
+
684
+ :math:`f(u) = \left\{
685
+ \begin{matrix}
686
+ \sqrt[3]{u}, & \mbox{if } u > \left(\frac{6}{29}\right)^3 \\
687
+ \frac{1}{3}\left(\frac{29}{6}\right)^2 u + \frac{4}{29}, & \mbox{if } u \le \left(\frac{6}{29}\right)^3
688
+ \end{matrix}\right.`
689
+
690
+ :param u: X/Xn, Y/Yn or Z/Zn ratio[-]
691
+ :type u: float or np.ndarray
692
+
693
+ :return: - **f** (*float or np.ndarray*) – evaluated function [-]
694
+ '''
695
+
696
+ delta = 6./29.
697
+ if isinstance(u, np.ndarray) == True:
698
+ f = np.zeros(len(u))
699
+ for i in range(len(u)):
700
+ if u[i] > delta**3.:
701
+ f[i] = np.cbrt(u[i])
702
+ else:
703
+ f[i] = u[i]/3./delta**2. + 4./29.
704
+ elif isinstance(u, (int, float)) == True:
705
+ if u > delta**3.:
706
+ f = np.cbrt(u)
707
+ else:
708
+ f = u/3./delta**2. + 4./29.
709
+ else:
710
+ msg = 'u must be int, float or np.ndarray.'
711
+ raise Exception(msg)
712
+
713
+ return f
714
+
715
+ def inv_f_Lab_from_XYZ(u):
716
+ r'''
717
+ The :math:`f^{-1}(u)` function, i.e. the inverse of the :math:`f(u)` function :meth:`skinoptics.colors.f_Lab_from_XYZ`.
718
+
719
+ :math:`f^{-1}(u) = \left\{
720
+ \begin{matrix}
721
+ u^3, & \mbox{if } u > \frac{6}{29} \\
722
+ 3 \mbox{ } \left(\frac{6}{29}\right)^2\left(u - \frac{4}{29} \right), & \mbox{if } u \le \frac{6}{29}
723
+ \end{matrix}\right.`
724
+
725
+ :param u: function variable [-]
726
+ :type u: float or np.ndarray
727
+
728
+ :return: - **f** (*float or np.ndarray*) – evaluated function [-]
729
+ '''
730
+
731
+ delta = 6./29.
732
+ if isinstance(u, np.ndarray) == True:
733
+ inv_f = np.zeros(len(u))
734
+ for i in range(len(u)):
735
+ if u[i] > delta:
736
+ inv_f[i] = u[i]**3
737
+ else:
738
+ inv_f[i] = 3.*delta**2.*(u[i] - 4./29.)
739
+ elif isinstance(u, (int, float)) == True:
740
+ if u > delta:
741
+ inv_f = u**3
742
+ else:
743
+ inv_f = 3.*delta**2.*(u - 4./29.)
744
+ else:
745
+ msg = 'u must be int, float or np.ndarray.'
746
+ raise Exception(msg)
747
+
748
+ return inv_f
749
+
750
+ def Lab_from_XYZ(X, Y, Z, illuminant = 'D65', observer = '10o', K = 1.):
751
+ r'''
752
+ | Calculate CIE L*a*b* coordinates from CIE XYZ coordinates.
753
+ | CIE XYZ and CIE L*a*b* coordinates must be for the same standard illuminant and standard observer.
754
+ | For detailts please check CIE [CIE04], Schanda 2006 [S06], ISO/CIE [IC08] and Hunt & Pointer 2011 [HP11].
755
+
756
+ | :math:`L^* = 116 \mbox{ } f(Y/Y_n) - 16`
757
+ | :math:`a^* = 500 \mbox{ } [f(X/X_n) - f(Y/Y_n)]`
758
+ | :math:`b^* = 200 \mbox{ } [f(Y/Y_n) - f(Z/Z_n)]`
759
+
760
+ in which (:math:`X_n`, :math:`Y_n`, :math:`Z_n`) is the white point and
761
+
762
+ :math:`f(u) = \left\{
763
+ \begin{matrix}
764
+ \sqrt[3]{u}, & \mbox{if } u > \left(\frac{6}{29}\right)^3 \\
765
+ \frac{1}{3}\left(\frac{29}{6}\right)^2 u + \frac{4}{29}, & \mbox{if } u \le \left(\frac{6}{29}\right)^3
766
+ \end{matrix}\right.`
767
+
768
+ :param X: X coordinate [-]
769
+ :type X: float or np.ndarray
770
+
771
+ :param Y: Y coordinate [-]
772
+ :type Y: float or np.ndarray
773
+
774
+ :param Z: Z coordinate [-]
775
+ :type Z: float or np.ndarray
776
+
777
+ :param illuminant: the user can choose one of the following... 'A', 'D50', 'D55', 'D65' or 'D75'
778
+ :type illuminant: str
779
+
780
+ :param observer: the user can choose one of the following... '2o' or '10o'
781
+ :type observer: str
782
+
783
+ :param K: scaling factor (usually 1. or 100.) [-] (default to 1.)
784
+ :type K: float
785
+
786
+ | 'A' refers to the CIE standard illuminant A
787
+ | 'D50' refers to the CIE standard illuminant D50
788
+ | 'D55' refers to the CIE standard illuminant D55
789
+ | 'D65' refers to the CIE standard illuminant D65
790
+ | 'D75' refers to the CIE standard illuminant D75
791
+
792
+ | '2o' refers to the CIE 1931 2 degree standard observer
793
+ | '10o' refers to the CIE 1964 10 degree standard observer
794
+
795
+ | K = 1. for CIE XYZ coordinates in range [0, 1]
796
+ | K = 100. for CIE XYZ coordinates in range [0, 100]
797
+
798
+ :return: - **L** (*float or np.ndarray*) L* coordinate [-]
799
+ - **a** (*float or np.ndarray*) a* coordinate [-]
800
+ - **b** (*float or np.ndarray*) b* coordinate [-]
801
+ '''
802
+
803
+ Xn, Yn, Zn = XYZ_wp(illuminant = illuminant, observer = observer, K = K)
804
+ f = f_Lab_from_XYZ
805
+
806
+ if isinstance(X, np.ndarray) == True and \
807
+ isinstance(Y, np.ndarray) == True and \
808
+ isinstance(Z, np.ndarray) == True:
809
+ if len(X) - len(Y) != 0 or len(X) - len(Z) != 0:
810
+ msg = 'X, Y and Z must have the same length.'
811
+ raise Exception(msg)
812
+ L, a, b = np.zeros((3, len(X)))
813
+ for i in range(len(X)):
814
+ L[i] = 116.*f(Y[i]/Yn) - 16.
815
+ a[i] = 500.*(f(X[i]/Xn) - f(Y[i]/Yn))
816
+ b[i] = 200.*(f(Y[i]/Yn) - f(Z[i]/Zn))
817
+ elif isinstance(X, (int, float)) == True and \
818
+ isinstance(Y, (int, float)) == True and \
819
+ isinstance(Z, (int, float)):
820
+ L = 116.*f(Y/Yn) - 16.
821
+ a = 500.*(f(X/Xn) - f(Y/Yn))
822
+ b = 200.*(f(Y/Yn) - f(Z/Zn))
823
+ else:
824
+ msg = 'X, Y and Z must be int, float or np.ndarray.'
825
+ raise Exception(msg)
826
+
827
+ return L, a, b
828
+
829
+ def XYZ_from_Lab(L, a, b, illuminant = 'D65', observer = '10o', K = 1.):
830
+ r'''
831
+ | Calculate CIE XYZ coordinates from CIE L*a*b* coordinates.
832
+ | CIE XYZ and CIE L*a*b* coordinates must be for the same standard illuminant and standard observer.
833
+ | For detailts please check CIE [CIE04], Schanda 2006 [S06] and Hunt & Pointer 2011 [HP11].
834
+
835
+ | :math:`X = f^{-1}[(L^* + 16)/116 + a^*/500] \mbox{ } X_n`
836
+ | :math:`Y = f^{-1}[(L^* + 16)/116] \mbox{ } Y_n`
837
+ | :math:`Z = f^{-1}[(L^* + 16)/116 - b^*/200] \mbox{ } Z_n`
838
+
839
+ in which (:math:`X_n`, :math:`Y_n`, :math:`Z_n`) is the white point and
840
+
841
+ :math:`f^{-1}(u) = \left\{
842
+ \begin{matrix}
843
+ u^3, & \mbox{if } u > \frac{6}{29} \\
844
+ 3 \mbox{ } \left(\frac{6}{29}\right)^2\left(u - \frac{4}{29} \right), & \mbox{if } u \le \frac{6}{29}
845
+ \end{matrix}\right.`
846
+
847
+ :param L: L* coordinate [-] (must be in range [0, 100])
848
+ :type L: float or np.ndarray
849
+
850
+ :param a: a* coordinate [-]
851
+ :type a: float or np.ndarray
852
+
853
+ :param b: b* coordinate [-]
854
+ :type b: float or np.ndarray
855
+
856
+ :param illuminant: the user can choose one of the following... 'A', 'D50', 'D55', 'D65' or 'D75'
857
+ :type illuminant: str
858
+
859
+ :param observer: the user can choose one of the following... '2o' or '10o'
860
+ :type observer: str
861
+
862
+ :param K: scaling factor (usually 1. or 100.) [-] (default to 1.)
863
+ :type K: float
864
+
865
+ | 'A' refers to the CIE standard illuminant A
866
+ | 'D50' refers to the CIE standard illuminant D50
867
+ | 'D55' refers to the CIE standard illuminant D55
868
+ | 'D65' refers to the CIE standard illuminant D65
869
+ | 'D75' refers to the CIE standard illuminant D75
870
+
871
+ | '2o' refers to the CIE 1931 2 degree standard observer
872
+ | '10o' refers to the CIE 1964 10 degree standard observer
873
+
874
+ | K = 1. for CIE XYZ coordinates in range [0, 1]
875
+ | K = 100. for CIE XYZ coordinates in range [0, 100]
876
+
877
+ :return: - **X** (*float or np.ndarray*) – X* coordinate [-]
878
+ - **Y** (*float or np.ndarray*) – Y* coordinate [-]
879
+ - **Z** (*float or np.ndarray*) Z* coordinate [-]
880
+ '''
881
+
882
+ if isinstance(L, np.ndarray) == True:
883
+ if np.any(L < 0) or np.any(L > 100):
884
+ msg = 'At least one element in the input L is out of the range [0, 100].'
885
+ raise Exception(msg)
886
+ else:
887
+ if L < 0 or L > 100:
888
+ msg = 'The input L = {} is out of the range [0, 100].'.format(L)
889
+ raise Exception(msg)
890
+
891
+ Xn, Yn, Zn = XYZ_wp(illuminant = illuminant, observer = observer, K = K)
892
+ inv_f = inv_f_Lab_from_XYZ
893
+
894
+ if isinstance(L, np.ndarray) == True and \
895
+ isinstance(a, np.ndarray) == True and \
896
+ isinstance(b, np.ndarray) == True:
897
+ if len(L) - len(a) != 0 or len(L) - len(b) != 0:
898
+ msg = 'L, a and b must have the same length.'
899
+ raise Exception(msg)
900
+ X, Y, Z = np.zeros((3, len(L)))
901
+ for i in range(len(L)):
902
+ X[i] = Xn*inv_f((L[i] + 16.)/116. + a[i]/500.)
903
+ Y[i] = Yn*inv_f((L[i] + 16.)/116.)
904
+ Z[i] = Zn*inv_f((L[i] + 16.)/116. - b[i]/200.)
905
+ elif isinstance(L, (int, float)) == True and \
906
+ isinstance(a, (int, float)) == True and \
907
+ isinstance(b, (int, float)):
908
+ X = Xn*inv_f((L + 16.)/116. + a/500.)
909
+ Y = Yn*inv_f((L + 16.)/116.)
910
+ Z = Zn*inv_f((L + 16.)/116. - b/200.)
911
+ else:
912
+ msg = 'L, a and b must be float or np.ndarray.'
913
+ raise Exception(msg)
914
+
915
+ return X, Y, Z
916
+
917
+ def chroma(a, b):
918
+ r'''
919
+ Calculate the chroma C* from a* and b* coordinates.
920
+
921
+ :math:`C^* = \sqrt{a^{*2} + b^{*2}}`
922
+
923
+ :param a: a* coordinate [-]
924
+ :type a: float or np.ndarray
925
+
926
+ :param b: b* coordinate [-]
927
+ :type b: float or np.ndarray
928
+
929
+ :return: - **chroma** (*float or np.ndarray*) – chroma [-]
930
+ '''
931
+
932
+ return np.sqrt(a**2. + b**2.)
933
+
934
+ def hue(a, b):
935
+ r'''
936
+ Calculate the hue angle h* from a* and b* coordinates.
937
+
938
+ :math:`h^* = \mbox{arctan2 } (b^*, a^*) \times \frac{180}{\pi}`
939
+
940
+ :param a: a* coordinate [-]
941
+ :type a: float or np.ndarray
942
+
943
+ :param b: b* coordinate [-]
944
+ :type b: float or np.ndarray
945
+
946
+ :return: - **hue** (*float or np.ndarray*) hue angle [degrees] (in range [0, 360])
947
+ '''
948
+
949
+ hue = np.arctan2(b,a)*180./np.pi
950
+
951
+ hue_shape = hue.shape
952
+ hue_flatten = hue.flatten()
953
+
954
+ if isinstance(hue, np.ndarray) == True:
955
+ for i in hue_flatten:
956
+ if i < 0:
957
+ i += 360
958
+ hue = hue_flatten.reshape(hue_shape)
959
+ elif isinstance(hue, (int, float)) == True:
960
+ if hue < 0:
961
+ hue += 360
962
+
963
+ return hue
964
+
965
+ def ITA(L, b, L0 = 50.):
966
+ r'''
967
+ | Calculate the Individual Typology Angle (ITA) from L* and b* coordinates.
968
+ | For details please check Chardon, Cretois & Hourseau 1991 [CCH91], Del Bino et al. 2006 [D*06],
969
+ | Del Bino & Bernerd 2013 [DB13] and Ly et al. 2020 [L*20].
970
+
971
+ :math:`\mbox{ITA} = \arctan\left(\frac{L^*-L_0^*}{b^*}\right) \times \frac{180}{\pi}`
972
+
973
+ :param L: L* coordinate [-]
974
+ :type L: float or np.ndarray
975
+
976
+ :param b: b* coordinate [-]
977
+ :type b: float or np.ndarray
978
+
979
+ :param L0: L0 coordinate [-] (default to 50.)
980
+ :type L0: float
981
+
982
+ :return: - **ITA** (*float or np.ndarray*) Individual Typology Angle [degrees]
983
+ '''
984
+
985
+ return np.arctan((L - L0)/b)*180./np.pi
986
+
987
+ def ITA_class(ITA):
988
+ r'''
989
+ | Skin color classification based on the Individual Typology Angle :meth:`skinoptics.colors.ITA`.
990
+ | For details please check Chardon, Cretois & Hourseau 1991 [CCH91], Del Bino et al. 2006 [D*06],
991
+ | Del Bino & Bernerd 2013 [DB13] and Ly et al. [L*20].
992
+
993
+ +---------------------------+-----------------------------------------------+
994
+ | skin color classification | ITA range |
995
+ +===========================+===============================================+
996
+ | very light | ITA :math:`> 55^\circ` |
997
+ +---------------------------+-----------------------------------------------+
998
+ | light | :math:`41^\circ <` ITA :math:`\le 55^\circ` |
999
+ +---------------------------+-----------------------------------------------+
1000
+ | intermediate | :math:`28^\circ <` ITA :math:`\le 41^\circ` |
1001
+ +---------------------------+-----------------------------------------------+
1002
+ | tan | :math:`10^\circ <` ITA :math:`\le 28^\circ` |
1003
+ +---------------------------+-----------------------------------------------+
1004
+ | brown | :math:`-30^\circ <` ITA :math:`\le 10^\circ` |
1005
+ +---------------------------+-----------------------------------------------+
1006
+ | dark | ITA :math:`\le -30^\circ` |
1007
+ +---------------------------+-----------------------------------------------+
1008
+
1009
+ :param ITA: Individual Typology Angle [degrees] (must be greater than -90 and less than 90)
1010
+ :type ITA: float or np.ndarray
1011
+
1012
+ :return: - **ITA_class** (*str or np.ndarray*) skin color classification based on the Individual Typology Angle
1013
+ '''
1014
+
1015
+ if isinstance(ITA, np.ndarray) == True:
1016
+ if np.any(ITA < -90) or np.any(ITA > 90):
1017
+ msg = 'At least one element in the input ITA is out of the range [-90, 90].'
1018
+ raise Exception(msg)
1019
+ else:
1020
+ if ITA < -90 or ITA > 90:
1021
+ msg = 'The input ITA = {} is out of the range [-90, 90].'.format(ITA)
1022
+ raise Exception(msg)
1023
+
1024
+ if isinstance(ITA, np.ndarray) == True:
1025
+ ITA_class_list = ['']*len(ITA)
1026
+ for i in range(len(ITA)):
1027
+ if ITA[i] > 55:
1028
+ ITA_class_list[i] = 'very light'
1029
+ elif ITA[i] > 41 and ITA[i] <= 55:
1030
+ ITA_class_list[i] = 'light'
1031
+ elif ITA[i] > 28 and ITA[i] <= 41:
1032
+ ITA_class_list[i] = 'intermediate'
1033
+ elif ITA[i] > 10 and ITA[i] <= 28:
1034
+ ITA_class_list[i] = 'tan'
1035
+ elif ITA[i] > -30 and ITA[i] <= 10:
1036
+ ITA_class_list[i] = 'brown'
1037
+ else:
1038
+ ITA_class_list[i] = 'dark'
1039
+ ITA_class = np.array(ITA_class_list)
1040
+ else:
1041
+ if ITA > 55:
1042
+ ITA_class = 'very light'
1043
+ elif ITA > 41 and ITA <= 55:
1044
+ ITA_class = 'light'
1045
+ elif ITA > 28 and ITA <= 41:
1046
+ ITA_class = 'intermediate'
1047
+ elif ITA > 10 and ITA <= 28:
1048
+ ITA_class = 'tan'
1049
+ elif ITA > -30 and ITA <= 10:
1050
+ ITA_class = 'brown'
1051
+ else:
1052
+ ITA_class = 'dark'
1053
+
1054
+ return ITA_class
1055
+
1056
+ def Delta_L(L0, L1):
1057
+ r'''
1058
+ Calculate the lightness difference :math:`\Delta L^*` between a reference color lightness :math:`L^*_0`
1059
+ and a test color lightness :math:`L^*_1`.
1060
+
1061
+ :math:`\Delta L^* = L^*_1 - L^*_0`
1062
+
1063
+ :param L0: reference color L* coordinate [-]
1064
+ :type L0: float or np.ndarray
1065
+
1066
+ :param L1: test color L* coordinate [-]
1067
+ :type L1: float or np.ndarray
1068
+
1069
+ :return: - **delta_L** (*float or np.ndarray*) – lightness difference [-]
1070
+ '''
1071
+
1072
+ return L1 - L0
1073
+
1074
+ def Delta_a(a0, a1):
1075
+ r'''
1076
+ Calculate the difference :math:`\Delta a^*` between a reference color :math:`a^*_0` coordinate
1077
+ and a test color :math:`a^*_1` coordinate.
1078
+
1079
+ :math:`\Delta a^* = a^*_1 - a^*_0`
1080
+
1081
+ :param a0: reference color a* coordinate [-]
1082
+ :type a0: float or np.ndarray
1083
+
1084
+ :param a1: test color a* coordinate [-]
1085
+ :type a1: float or np.ndarray
1086
+
1087
+ :return: - **delta_a** (*float or np.ndarray*) – a* difference [-]
1088
+ '''
1089
+
1090
+ return a1 - a0
1091
+
1092
+ def Delta_b(b0, b1):
1093
+ r'''
1094
+ Calculate the difference :math:`\Delta b^*` between a reference color :math:`b^*_0` coordinate
1095
+ and a test color :math:`b^*_1` coordinate.
1096
+
1097
+ :math:`\Delta b^* = b^*_1 - b^*_0`
1098
+
1099
+ :param b0: reference color b* coordinate [-]
1100
+ :type b0: float or np.ndarray
1101
+
1102
+ :param b1: test color b* coordinate [-]
1103
+ :type b1: float or np.ndarray
1104
+
1105
+ :return: - **delta_b** (*float or np.ndarray*) – b* difference [-]
1106
+ '''
1107
+
1108
+ return b1 - b0
1109
+
1110
+ def Delta_E(L0, a0, b0, L1, a1, b1):
1111
+ r'''
1112
+ Calculate the CIELAB color difference :math:`\Delta E^*_{ab}` between
1113
+ a reference color (:math:`L^*_0`, :math:`a^*_0`, :math:`b^*_0`) and
1114
+ a test color (:math:`L^*_1`, :math:`a^*_1`, :math:`b^*_1`).
1115
+
1116
+ :math:`\Delta E^*_{ab} = \sqrt{(L^*_1 - L^*_0)^2 + (a^*_1 - a^*_0)^2 + (b^*_1 - b^*_0)^2}`
1117
+
1118
+ :param L0: reference color L* coordinate [-]
1119
+ :type L0: float or np.ndarray
1120
+
1121
+ :param a0: reference color a* coordinate [-]
1122
+ :type a0: float or np.ndarray
1123
+
1124
+ :param b0: reference color b* coordinate [-]
1125
+ :type b0: float or np.ndarray
1126
+
1127
+ :param L1: test color L* coordinate [-]
1128
+ :type L1: float or np.ndarray
1129
+
1130
+ :param a1: test color a* coordinate [-]
1131
+ :type a1: float or np.ndarray
1132
+
1133
+ :param b1: test color b* coordinate [-]
1134
+ :type b1: float or np.ndarray
1135
+
1136
+ :return: - **delta_E** (*float or np.ndarray*) CIELAB color difference [-]
1137
+ '''
1138
+
1139
+ return np.sqrt(Delta_L(L0 = L0, L1 = L1)**2 + Delta_a(a0 = a0, a1 = a1)**2 + Delta_b(b0 = b0, b1 = b1)**2)
1140
+
1141
+ def Delta_E_00(L0, a0, b0, L1, a1, b1, kL = 1., kC = 1., kH = 1.):
1142
+ r'''
1143
+ Calculate the CIEDE2000 color difference :math:`\Delta E^*_{00}` between
1144
+ a reference color (:math:`L^*_0`, :math:`a^*_0`, :math:`b^*_0`) and
1145
+ a test color (:math:`L^*_1`, :math:`a^*_1`, :math:`b^*_1`).
1146
+
1147
+ :math:`\Delta E^*_{00} = \sqrt{\left(\frac{\Delta L'}{k_L S_L}\right)^2 + \left(\frac{\Delta C'}{k_C S_C}\right)^2 + \left(\frac{\Delta H'}{k_H S_H}\right)^2 + R_T \left(\frac{\Delta C'}{k_C S_C}\right) \left(\frac{\Delta H'}{k_H S_H}\right)}`
1148
+
1149
+ | For details please check Sharma, Wu & Dalal 2004 [SWD04] and ISO/CIE 2014 [IC14].
1150
+
1151
+ :param L0: reference color L* coordinate [-]
1152
+ :type L0: float or np.ndarray
1153
+
1154
+ :param a0: reference color a* coordinate [-]
1155
+ :type a0: float or np.ndarray
1156
+
1157
+ :param b0: reference color b* coordinate [-]
1158
+ :type b0: float or np.ndarray
1159
+
1160
+ :param L1: test color L* coordinate [-]
1161
+ :type L1: float or np.ndarray
1162
+
1163
+ :param a1: test color a* coordinate [-]
1164
+ :type a1: float or np.ndarray
1165
+
1166
+ :param b1: test color b* coordinate [-]
1167
+ :type b1: float or np.ndarray
1168
+
1169
+ :param kL: lightness parametric factor [-] (default to 1.)
1170
+ :type kL: float
1171
+
1172
+ :param kC: chroma parametric factor [-] (default to 1.)
1173
+ :type kC: float
1174
+
1175
+ :param kH: hue parametric factor [-] (default to 1.)
1176
+ :type kH: float
1177
+
1178
+ :return: - **delta_E_00** (*float or np.ndarray*) CIEDE2000 color difference [-]
1179
+ '''
1180
+
1181
+ C0 = chroma(a = a0, b = b0)
1182
+ C1 = chroma(a = a1, b = b1)
1183
+
1184
+ C_bar = np.mean((C0, C1))
1185
+
1186
+ G = 0.5*(1 - np.sqrt(C_bar**7/(C_bar**7 + 25**7)))
1187
+
1188
+ Ll0 = L0
1189
+ al0 = (1 + G)*a0
1190
+ bl0 = b0
1191
+ Cl0 = chroma(a = al0, b = bl0)
1192
+ hl0 = hue(a = al0, b = bl0)
1193
+
1194
+ Ll1 = L1
1195
+ al1 = (1 + G)*a1
1196
+ bl1 = b1
1197
+ Cl1 = chroma(a = al1, b = bl1)
1198
+ hl1 = hue(a = al1, b = bl1)
1199
+
1200
+ abs_diff_hl = np.abs(hl1 - hl0)
1201
+
1202
+ Delta_Ll = Ll1 - Ll0
1203
+ Delta_Cl = Cl1 - Cl0
1204
+ if isinstance(abs_diff_hl, np.ndarray) == True:
1205
+ Delta_hl = np.zeros(len(abs_diff_hl))
1206
+ for i in range(len(Delta_hl)):
1207
+ if Cl0[i]*Cl1[i] == 0:
1208
+ Delta_hl[i] = 0
1209
+ elif Cl0[i]*Cl1[i] != 0 and abs_diff_hl[i] <= 180:
1210
+ Delta_hl[i] = hl1[i] - hl0[i]
1211
+ elif Cl0[i]*Cl1[i] != 0 and hl1[i] - hl0[i] > 180:
1212
+ Delta_hl[i] = hl1[i] - hl0[i] - 360
1213
+ elif Cl0[i]*Cl1[i] != 0 and hl1[i] - hl0[i] < -180:
1214
+ Delta_hl[i] = hl1[i] - hl0[i] + 360
1215
+ elif isinstance(abs_diff_hl, (int, float)) == True:
1216
+ if Cl0*Cl1 == 0:
1217
+ Delta_hl = 0
1218
+ elif Cl0*Cl1 != 0 and abs_diff_hl <= 180:
1219
+ Delta_hl = hl1 - hl0
1220
+ elif Cl0*Cl1 != 0 and hl1 - hl0 > 180:
1221
+ Delta_hl = hl1 - hl0 - 360
1222
+ elif Cl0*Cl1 != 0 and hl1 - hl0 < -180:
1223
+ Delta_hl = hl1 - hl0 + 360
1224
+ else:
1225
+ msg = 'The input (L0, a0, b0, L1, a1, b1 = {} is not valid.'.format(np.array(L0, a0, b0, L1, a1, b1))
1226
+ raise Exception(msg)
1227
+ Delta_Hl = 2*np.sqrt(Cl0*Cl1)*np.sin(np.radians(Delta_hl/2))
1228
+
1229
+ Ll_bar = np.mean((Ll0, Ll1))
1230
+ Cl_bar = np.mean((Cl0, Cl1))
1231
+ if isinstance(abs_diff_hl, np.ndarray) == True:
1232
+ hl_bar = np.zeros(len(abs_diff_hl))
1233
+ for i in range(len(hl_bar)):
1234
+ if Cl0[i]*Cl1[i] == 0:
1235
+ hl_bar[i] = hl0[i] + hl1[i]
1236
+ elif Cl0[i]*Cl1[i] != 0 and abs_diff_hl[i] <= 180:
1237
+ hl_bar[i] = np.mean((hl0[i], hl1[i]))
1238
+ elif Cl0[i]*Cl1[i] != 0 and abs_diff_hl[i] > 180 and hl0[i] + hl1[i] < 360:
1239
+ hl_bar[i] = (hl0[i] + hl1[i] + 360)/2
1240
+ elif Cl0[i]*Cl1[i] != 0 and abs_diff_hl[i] > 180 and hl0[i] + hl1[i] >= 360:
1241
+ hl_bar[i] = (hl0[i] + hl1[i] - 360)/2
1242
+ elif isinstance(abs_diff_hl, (int, float)) == True:
1243
+ if Cl0*Cl1 == 0:
1244
+ hl_bar = hl0 + hl1
1245
+ elif Cl0*Cl1 != 0 and abs_diff_hl <= 180:
1246
+ hl_bar = np.mean((hl0, hl1))
1247
+ elif Cl0*Cl1 != 0 and abs_diff_hl > 180 and hl0 + hl1 < 360:
1248
+ hl_bar = (hl0 + hl1 + 360)/2
1249
+ elif Cl0*Cl1 != 0 and abs_diff_hl > 180 and hl0 + hl1 >= 360:
1250
+ hl_bar = (hl0 + hl1 - 360)/2
1251
+ else:
1252
+ msg = 'The input (L0, a0, b0, L1, a1, b1 = {} is not valid.'.format(np.array(L0, a0, b0, L1, a1, b1))
1253
+ raise Exception(msg)
1254
+
1255
+ T = 1 - 0.17*np.cos(np.radians(hl_bar - 30)) + 0.24*np.cos(np.radians(2*hl_bar)) \
1256
+ + 0.32*np.cos(np.radians(3*hl_bar + 6)) - 0.2*np.cos(np.radians(4*hl_bar - 63))
1257
+
1258
+ SL = 1 + (0.015*(Ll_bar - 50)**2)/np.sqrt(20 + (Ll_bar - 50)**2)
1259
+ SC = 1 + 0.045*Cl_bar
1260
+ SH = 1 + 0.015*Cl_bar*T
1261
+
1262
+ Delta_theta = 30*np.exp(-((hl_bar - 275)/25)**2)
1263
+ RC = 2*np.sqrt(Cl_bar**7/(Cl_bar**7 + 25**7))
1264
+ RT = - np.sin(np.radians(2*Delta_theta))*RC
1265
+
1266
+ Delta_E_00_1st = (Delta_Ll/(kL*SL))**2
1267
+ Delta_E_00_2nd = (Delta_Cl/(kC*SC))**2
1268
+ Delta_E_00_3rd = (Delta_Hl/(kH*SH))**2
1269
+ Delta_E_00_4th = RT*(Delta_Cl/(kC*SC))*(Delta_Hl/(kH*SH))
1270
+
1271
+ return np.sqrt(Delta_E_00_1st + Delta_E_00_2nd + Delta_E_00_3rd + Delta_E_00_4th)
1272
+
1273
+ def EI(R_green, R_red):
1274
+ r'''
1275
+ | Calculate the Erythema Index (EI) from the reflectances on chosen green
1276
+ | (usually approx. 568 nm) and red bands (usually approx. 655 nm).
1277
+ | For details please check Takiwaki et al. 1994 [T*94] and Fullerton et al. 1996 [F*96].
1278
+
1279
+ :math:`\mbox{EI} = 100 \mbox{ } [\mbox{log}_{10}(R_\mbox{red}) - \mbox{log}_{10}(R_\mbox{green})]`
1280
+
1281
+ :param R_green: reflectance on a chosen green band [%]
1282
+ :type R_green: float or np.ndarray
1283
+
1284
+ :param R_red: reflectance on a chosen red band [%]
1285
+ :type R_red: float or np.ndarray
1286
+
1287
+ :return: - **EI** (*float or np.ndarray*) – Erythema Index [-]
1288
+ '''
1289
+
1290
+ return 100*(np.log10(R_red/100) - np.log10(R_green/100))
1291
+
1292
+ def MI(R_red):
1293
+ r'''
1294
+ | Calculate the Melanin Index (MI) from the reflectance on a chosen red band
1295
+ | (usually approx. 655 nm).
1296
+ | For details please check Takiwaki et al. 1994 [T*94] and Fullerton et al. 1996 [F*96].
1297
+
1298
+ :math:`\mbox{MI} = 100 \mbox{ } [-\mbox{log}_{10}(R_\mbox{red})]`
1299
+
1300
+ :param R_red: reflectance on a chosen red band [%]
1301
+ :type R_red: float or np.ndarray
1302
+
1303
+ :return: - **MI** (*float or np.ndarray* – Melanin Index [-]
1304
+ '''
1305
+
1306
+ return 100*(-np.log10(R_red/100))
1307
+
1308
+ def XYZ_from_spectrum(all_lambda, spectrum, lambda_min = 360., lambda_max = 830., lambda_step = 1.,
1309
+ illuminant = 'D65', observer = '10o', cmfs_model = 'CIE', K = 1., interp1d_kind = 'cubic'):
1310
+ r'''
1311
+ | Calculate the CIE XYZ coordinates from the reflectance spectrum :math:`R(\lambda)` or the
1312
+ | transmittance spectrum :math:`T(\lambda)` for a chosen standard illuminant and standard observer.
1313
+ | Integration using the composite trapezoid rule from 360 nm to 830 nm (as default).
1314
+ | If the wavelength array does not cover the whole region, a constant extrapolation is perfomed.
1315
+ | For details please check CIE [CIE04] (see their section 7).
1316
+
1317
+ | :math:`X = \frac{K}{N} \int_\lambda \mbox{ } R(\lambda) \mbox{ } S(\lambda) \mbox{ } \bar{x}(\lambda) \mbox{ } d\lambda`
1318
+ | :math:`Y = \frac{K}{N} \int_\lambda \mbox{ } R(\lambda) \mbox{ } S(\lambda) \mbox{ } \bar{y}(\lambda) \mbox{ } d\lambda`
1319
+ | :math:`Z = \frac{K}{N} \int_\lambda \mbox{ } R(\lambda) \mbox{ } S(\lambda) \mbox{ } \bar{z}(\lambda) \mbox{ } d\lambda`
1320
+
1321
+ in which
1322
+
1323
+ | :math:`N = \int_\lambda \mbox{ } S(\lambda) \mbox{ } \bar{y}(\lambda) \mbox{ } d\lambda`
1324
+
1325
+ The reflectance spectrum :math:`R(\lambda)` is replaced by the transmittance spectrum
1326
+ :math:`T(\lambda)` when dealing with color in some cases.
1327
+
1328
+ :param all_lambda: wavelength array
1329
+ :type all_lambda: np.ndarray
1330
+
1331
+ :param spectrum: reflectance or transmittance spectrum respective to the wavelength array [%]
1332
+ :type spectrum: np.ndarray
1333
+
1334
+ :param lambda_min: lower limit of summation/integration (minimum wavelength to take into account) [nm] (default to 360.)
1335
+ :type lambda_min: float
1336
+
1337
+ :param lambda_max: upper limit of summation/integration (maximum wavelength to take into account) [nm] (default to 830.)
1338
+ :type lambda_max: float
1339
+
1340
+ :param lambda_step: summation interval (wavelength step) [nm] (default to 1.)
1341
+ :type lambda_step: float
1342
+
1343
+ :param illuminant: the user can choose one of the following... 'A', 'D50', 'D55', 'D65' or 'D75'
1344
+ :type illuminant: str
1345
+
1346
+ :param observer: the user can choose one of the following... '2o' or '10o'
1347
+ :type observer: str
1348
+
1349
+ :param cmfs_model: the user can choose one of the following... 'CIE', 'Wyman_singlelobe' or 'Wyman_multilobe' (default to 'CIE')
1350
+ :type cmfs_model: str
1351
+
1352
+ :param K: scaling factor (usually 1. or 100.) [-] (default to 1.)
1353
+ :type K: float
1354
+
1355
+ :param interp1d_kind: kind argument of scipy.interpolation.interp1d (default to 'cubic' [CIE04] (see their section 7.2.1.1))
1356
+ :type interp1d_kind: str
1357
+
1358
+ | 'A' refers to the CIE standard illuminant A
1359
+ | 'D50' refers to the CIE standard illuminant D50
1360
+ | 'D55' refers to the CIE standard illuminant D55
1361
+ | 'D65' refers to the CIE standard illuminant D65
1362
+ | 'D75' refers to the CIE standard illuminant D75
1363
+
1364
+ | '2o' refers to the CIE 1931 2 degree standard observer
1365
+ | '10o' refers to the CIE 1964 10 degree standard observer
1366
+
1367
+ | 'CIE' for the linear interpolation of data from CIE datasets [CIE19a] [CIE19b]
1368
+ | 'Wyman_singlelobe' for the functions from Wyman, Sloan & Shirley 2013 [WSS13] (section 2.1)
1369
+ | 'Wyman_multilobe' for the functions from Wyman, Sloan & Shirley 2013 [WSS13] (section 2.2)
1370
+
1371
+ | K = 1. for CIE XYZ coordinates in range [0, 1]
1372
+ | K = 100. for CIE XYZ coordinates in range [0, 100]
1373
+
1374
+ :return: - **X** (*float*) X coordinate [-]
1375
+ - **Y** (*float*) – Y coordinate [-]
1376
+ - **Z** (*float*) Z coordinate [-]
1377
+ '''
1378
+
1379
+ x = np.arange(lambda_min, lambda_max + lambda_step, lambda_step)
1380
+ R_or_T_lambda = interp1d(all_lambda, spectrum/100, kind = interp1d_kind,
1381
+ bounds_error = False, fill_value = (spectrum[0]/100,
1382
+ spectrum[-1]/100))(x)
1383
+ S_lambda = rspd(x, illuminant = illuminant)
1384
+ xbar_lambda, ybar_lambda, zbar_lambda = cmfs(x, observer = observer, cmfs_model = cmfs_model)
1385
+
1386
+ y0 = S_lambda*ybar_lambda
1387
+ N = trapezoid(y0, x = x, dx = lambda_step)
1388
+
1389
+ R_or_T_lambda_times_S_lambda = R_or_T_lambda*S_lambda
1390
+
1391
+ y1 = R_or_T_lambda_times_S_lambda*xbar_lambda
1392
+ X = K/N*trapezoid(y1, x = x, dx = lambda_step)
1393
+
1394
+ y2 = R_or_T_lambda_times_S_lambda*ybar_lambda
1395
+ Y = K/N*trapezoid(y2, x = x, dx = lambda_step)
1396
+
1397
+ y3 = R_or_T_lambda_times_S_lambda*zbar_lambda
1398
+ Z = K/N*trapezoid(y3, x = x, dx = lambda_step)
1399
+
1400
+ return X, Y, Z
1401
+
1402
+ def sRGB_from_spectrum(all_lambda, spectrum, lambda_min = 360, lambda_max = 830, lambda_step = 1,
1403
+ cmfs_model = 'CIE', interp1d_kind = 'cubic', sRGB_scale = 'norm'):
1404
+ r'''
1405
+ | Calculate the sRGB coordinates from the reflectance or the transmittance spectrum.
1406
+ | First calculate CIE XYZ coordinates (respective to the standard illuminant D65 and
1407
+ | the 2 degree standard observer) from the spectrum and then calculate sRGB coordinates
1408
+ | from CIE XYZ coordinates (see functions :meth:`skinoptics.colors.sRGB_from_XYZ` and
1409
+ | :meth:`skinoptics.colors.XYZ_from_spectrum`).
1410
+
1411
+ :param all_lambda: wavelength array
1412
+ :type all_lambda: np.ndarray
1413
+
1414
+ :param spectrum: reflectance or transmittance spectrum respective to the wavelength array [%]
1415
+ :type spectrum: np.ndarray
1416
+
1417
+ :param lambda_min: lower limit of summation/integration (minimum wavelength to take into account) [nm] (default to 360.)
1418
+ :type lambda_min: float
1419
+
1420
+ :param lambda_max: upper limit of summation/integration (maximum wavelength to take into account) [nm] (default to 830.)
1421
+ :type lambda_max: float
1422
+
1423
+ :param lambda_step: summation interval (wavelength step) [nm] (default to 1.)
1424
+ :type lambda_step: float
1425
+
1426
+ :param cmfs_model: the user can choose one of the following... 'CIE', 'Wyman_singlelobe' or 'Wyman_multilobe' (default to 'CIE')
1427
+ :type cmfs_model: str
1428
+
1429
+ :param interp1d_kind: kind argument of scipy.interpolation.interp1d (default to 'cubic' [CIE04] (see their section 7.2.1.1))
1430
+ :type interp1d_kind: str
1431
+
1432
+ :param sRGB_scale: the user can choose one of the following... 'norm' or '8bit' (default to 'norm')
1433
+ :type sRGB_scale: str
1434
+
1435
+ | 'CIE' for the linear interpolation of data from CIE datasets [CIE19a] [CIE19b]
1436
+ | 'Wyman_singlelobe' for the functions from Wyman, Sloan & Shirley 2013 [WSS13] (section 2.1)
1437
+ | 'Wyman_multilobe' for the functions from Wyman, Sloan & Shirley 2013 [WSS13] (section 2.2)
1438
+
1439
+ | 'norm' for sRGB coordinates in range [0,1] (normalized scale)
1440
+ | '8bit' for sRGB coordinates in range [0, 255] (8-bit scale)
1441
+
1442
+ :return: - **R** (*float*) – R coordinate [-]
1443
+ - **G** (*float*) – G coordinate [-]
1444
+ - **B** (*float*) – B coordinate [-]
1445
+ '''
1446
+
1447
+ return sRGB_from_XYZ(*XYZ_from_spectrum(all_lambda = all_lambda, spectrum = spectrum,
1448
+ lambda_min = lambda_min, lambda_max = lambda_max,
1449
+ lambda_step = lambda_step,
1450
+ illuminant = 'D65', observer = '2o', cmfs_model = cmfs_model,
1451
+ K = 1., interp1d_kind = interp1d_kind),
1452
+ K = 1., sRGB_scale = sRGB_scale)
1453
+
1454
+ def Lab_from_spectrum(all_lambda, spectrum, lambda_min = 360, lambda_max = 830, lambda_step = 1,
1455
+ illuminant = 'D65', observer = '10o', cmfs_model = 'CIE', interp1d_kind = 'cubic'):
1456
+ r'''
1457
+ | Calculate the CIE L*a*b* coordinates from the reflectance or the transmittance spectrum.
1458
+ | First calculate CIE XYZ coordinates from the spectrum for a chosen standard illuminant
1459
+ | and standard observer and then calculate CIE L*a*b* coordinates from CIE XYZ coordinates
1460
+ | (see functions :meth:`skinoptics.colors.Lab_from_XYZ` and :meth:`skinoptics.colors.XYZ_from_spectrum`).
1461
+
1462
+ :param all_lambda: wavelength array
1463
+ :type all_lambda: np.ndarray
1464
+
1465
+ :param spectrum: reflectance or transmittance spectrum respective to the wavelength array [%]
1466
+ :type spectrum: np.ndarray
1467
+
1468
+ :param lambda_min: lower limit of summation/integration (minimum wavelength to take into account) [nm] (default to 360.)
1469
+ :type lambda_min: float
1470
+
1471
+ :param lambda_max: upper limit of summation/integration (maximum wavelength to take into account) [nm] (default to 830.)
1472
+ :type lambda_max: float
1473
+
1474
+ :param lambda_step: summation interval (wavelength step) [nm] (default to 1.)
1475
+ :type lambda_step: float
1476
+
1477
+ :param illuminant: the user can choose one of the following... 'A', 'D50', 'D55', 'D65' or 'D75'
1478
+ :type illuminant: str
1479
+
1480
+ :param observer: the user can choose one of the following... '2o' or '10o'
1481
+ :type observer: str
1482
+
1483
+ :param cmfs_model: the user can choose one of the following... 'CIE', 'Wyman_singlelobe' or 'Wyman_multilobe' (default to 'CIE')
1484
+ :type cmfs_model: str
1485
+
1486
+ :param interp1d_kind: kind argument of scipy.interpolation.interp1d (default to 'cubic' [CIE04] (see their section 7.2.1.1))
1487
+ :type interp1d_kind: str
1488
+
1489
+ | 'A' refers to the CIE standard illuminant A
1490
+ | 'D50' refers to the CIE standard illuminant D50
1491
+ | 'D55' refers to the CIE standard illuminant D55
1492
+ | 'D65' refers to the CIE standard illuminant D65
1493
+ | 'D75' refers to the CIE standard illuminant D75
1494
+
1495
+ | '2o' refers to the CIE 1931 2 degree standard observer
1496
+ | '10o' refers to the CIE 1964 10 degree standard observer
1497
+
1498
+ | 'CIE' for the linear interpolation of data from CIE datasets [CIE19a] [CIE19b]
1499
+ | 'Wyman_singlelobe' for the functions from Wyman, Sloan & Shirley 2013 [WSS13] (section 2.1)
1500
+ | 'Wyman_multilobe' for the functions from Wyman, Sloan & Shirley 2013 [WSS13] (section 2.2)
1501
+
1502
+ :return: - **L** (*float*) – L* coordinate [-]
1503
+ - **a** (*float*) – a* coordinate [-]
1504
+ - **b** (*float*) – b* coordinate [-]
1505
+ '''
1506
+
1507
+ return Lab_from_XYZ(*XYZ_from_spectrum(all_lambda = all_lambda, spectrum = spectrum,
1508
+ lambda_min = lambda_min, lambda_max = lambda_max,
1509
+ lambda_step = lambda_step,
1510
+ illuminant = illuminant, observer = observer, cmfs_model = cmfs_model,
1511
+ K = 1., interp1d_kind = interp1d_kind),
1512
+ illuminant = illuminant, observer = observer, K = 1.)
1513
+
1514
+ def sRGB_from_lambda0(lambda0, cmfs_model = 'CIE', sRGB_scale = 'norm'):
1515
+ r'''
1516
+ | Calculate the sRGB coordinates respective to the color of a monochromatic light
1517
+ | (single wavelength).
1518
+
1519
+ wavelength range: [360 nm, 830 nm]
1520
+
1521
+ :param lambda0: wavelength of the monochromatic light [nm]
1522
+ :type lambda0: float or np.ndarray
1523
+
1524
+ :param cmfs_model: the user can choose one of the following... 'CIE', 'Wyman_singlelobe' or 'Wyman_multilobe' (default to 'CIE')
1525
+ :type cmfs_model: str
1526
+
1527
+ :param sRGB_scale: the user can choose one of the following... 'norm' or '8bit' (default to 'norm')
1528
+ :type sRGB_scale: str
1529
+
1530
+ | 'CIE' for the linear interpolation of data from CIE datasets [CIE19a] [CIE19b]
1531
+ | 'Wyman_singlelobe' for the functions from Wyman, Sloan & Shirley 2013 [WSS13] (section 2.1)
1532
+ | 'Wyman_multilobe' for the functions from Wyman, Sloan & Shirley 2013 [WSS13] (section 2.2)
1533
+
1534
+ | 'norm' for sRGB coordinates in range [0,1] (normalized scale)
1535
+ | '8bit' for sRGB coordinates in range [0, 255] (8-bit scale)
1536
+
1537
+ :return: - **R** (*float or np.ndarray*) – R coordinate [-]
1538
+ - **G** (*float or np.ndarray*) – G coordinate [-]
1539
+ - **B** (*float or np.ndarray*) – B coordinate [-]
1540
+ '''
1541
+
1542
+ return sRGB_from_XYZ(*cmfs(lambda0, observer = '2o', cmfs_model = cmfs_model),
1543
+ K = 1., sRGB_scale = sRGB_scale)