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