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