Coverage for colour/recovery/__init__.py: 100%
32 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-15 19:01 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-15 19:01 +1300
1"""
2References
3----------
4- :cite:`Jakob2019` : Jakob, W., & Hanika, J. (2019). A Low-Dimensional
5 Function Space for Efficient Spectral Upsampling. Computer Graphics Forum,
6 38(2), 147-155. doi:10.1111/cgf.13626
7- :cite:`Mallett2019` : Mallett, I., & Yuksel, C. (2019). Spectral Primary
8 Decomposition for Rendering with sRGB Reflectance. Eurographics Symposium
9 on Rendering - DL-Only and Industry Track, 7 pages. doi:10.2312/SR.20191216
10- :cite:`Meng2015c` : Meng, J., Simon, F., Hanika, J., & Dachsbacher, C.
11 (2015). Physically Meaningful Rendering using Tristimulus Colours. Computer
12 Graphics Forum, 34(4), 31-40. doi:10.1111/cgf.12676
13- :cite:`Otsu2018` : Otsu, H., Yamamoto, M., & Hachisuka, T. (2018).
14 Reproducing Spectral Reflectances From Tristimulus Colours. Computer
15 Graphics Forum, 37(6), 370-381. doi:10.1111/cgf.13332
16- :cite:`Smits1999a` : Smits, B. (1999). An RGB-to-Spectrum Conversion for
17 Reflectances. Journal of Graphics Tools, 4(4), 11-22.
18 doi:10.1080/10867651.1999.10487511
19"""
21from __future__ import annotations
23import typing
25if typing.TYPE_CHECKING:
26 from colour.colorimetry import SpectralDistribution
27 from colour.hints import Any, ArrayLike, Literal
29from colour.utilities import (
30 CanonicalMapping,
31 as_float_array,
32 filter_kwargs,
33 validate_method,
34)
36from . import datasets
37from .datasets import * # noqa: F403
38from .jakob2019 import (
39 LUT3D_Jakob2019,
40 XYZ_to_sd_Jakob2019,
41 find_coefficients_Jakob2019,
42 sd_Jakob2019,
43)
44from .jiang2013 import (
45 PCA_Jiang2013,
46 RGB_to_msds_camera_sensitivities_Jiang2013,
47 RGB_to_sd_camera_sensitivity_Jiang2013,
48)
49from .mallett2019 import (
50 RGB_to_sd_Mallett2019,
51 spectral_primary_decomposition_Mallett2019,
52)
53from .meng2015 import XYZ_to_sd_Meng2015
54from .otsu2018 import (
55 Dataset_Otsu2018,
56 Tree_Otsu2018,
57 XYZ_to_sd_Otsu2018,
58)
59from .smits1999 import RGB_to_sd_Smits1999
61__all__ = datasets.__all__
62__all__ += [
63 "LUT3D_Jakob2019",
64 "XYZ_to_sd_Jakob2019",
65 "find_coefficients_Jakob2019",
66 "sd_Jakob2019",
67]
68__all__ += [
69 "PCA_Jiang2013",
70 "RGB_to_msds_camera_sensitivities_Jiang2013",
71 "RGB_to_sd_camera_sensitivity_Jiang2013",
72]
73__all__ += [
74 "RGB_to_sd_Mallett2019",
75 "spectral_primary_decomposition_Mallett2019",
76]
77__all__ += [
78 "XYZ_to_sd_Meng2015",
79]
80__all__ += [
81 "Dataset_Otsu2018",
82 "Tree_Otsu2018",
83 "XYZ_to_sd_Otsu2018",
84]
85__all__ += [
86 "RGB_to_sd_Smits1999",
87]
89XYZ_TO_SD_METHODS: CanonicalMapping = CanonicalMapping(
90 {
91 "Jakob 2019": XYZ_to_sd_Jakob2019,
92 "Mallett 2019": RGB_to_sd_Mallett2019,
93 "Meng 2015": XYZ_to_sd_Meng2015,
94 "Otsu 2018": XYZ_to_sd_Otsu2018,
95 "Smits 1999": RGB_to_sd_Smits1999,
96 }
97)
98XYZ_TO_SD_METHODS.__doc__ = """
99Supported spectral distribution recovery methods.
101References
102----------
103:cite:`Jakob2019`, :cite:`Mallett2019`, :cite:`Meng2015c`,
104:cite:`Smits1999a`
105"""
108def XYZ_to_sd(
109 XYZ: ArrayLike,
110 method: (
111 Literal[
112 "Jakob 2019",
113 "Mallett 2019",
114 "Meng 2015",
115 "Otsu 2018",
116 "Smits 1999",
117 ]
118 | str
119 ) = "Meng 2015",
120 **kwargs: Any,
121) -> SpectralDistribution:
122 """
123 Recover the spectral distribution of the specified *CIE XYZ* tristimulus
124 values using the specified method.
126 Parameters
127 ----------
128 XYZ
129 *CIE XYZ* tristimulus values to recover the spectral distribution
130 from.
131 method
132 Computation method.
134 Other Parameters
135 ----------------
136 additional_data
137 {:func:`colour.recovery.XYZ_to_sd_Jakob2019`},
138 If *True*, ``error`` will be returned alongside the recovered
139 spectral distribution.
140 basis_functions
141 {:func:`colour.recovery.RGB_to_sd_Mallett2019`},
142 Basis functions for the method. The default is to use the built-in
143 *sRGB* basis functions, i.e.,
144 :attr:`colour.recovery.MSDS_BASIS_FUNCTIONS_sRGB_MALLETT2019`.
145 clip
146 {:func:`colour.recovery.XYZ_to_sd_Otsu2018`},
147 If *True*, the default, values below zero and above unity in the
148 recovered spectral distributions will be clipped. This ensures that
149 the returned reflectance is physical and conserves energy, but will
150 cause noticeable colour differences in case of very saturated
151 colours.
152 cmfs
153 {:func:`colour.recovery.XYZ_to_sd_Meng2015`},
154 Standard observer colour matching functions.
155 dataset
156 {:func:`colour.recovery.XYZ_to_sd_Otsu2018`},
157 Dataset to use for reconstruction. The default is to use the
158 published data.
159 illuminant
160 {:func:`colour.recovery.XYZ_to_sd_Jakob2019`,
161 :func:`colour.recovery.XYZ_to_sd_Meng2015`},
162 Illuminant spectral distribution, default to
163 *CIE Standard Illuminant D65*.
164 optimisation_kwargs
165 {:func:`colour.recovery.XYZ_to_sd_Jakob2019`,
166 :func:`colour.recovery.XYZ_to_sd_Meng2015`},
167 Parameters for :func:`scipy.optimize.minimize` and
168 :func:`colour.recovery.find_coefficients_Jakob2019` definitions.
170 Returns
171 -------
172 :class:`colour.SpectralDistribution`
173 Recovered spectral distribution.
175 Notes
176 -----
177 +------------+-----------------------+---------------+
178 | **Domain** | **Scale - Reference** | **Scale - 1** |
179 +============+=======================+===============+
180 | ``XYZ`` | 1 | 1 |
181 +------------+-----------------------+---------------+
183 - *Smits (1999)* method will internally convert specified *CIE XYZ*
184 tristimulus values to *sRGB* colourspace array assuming equal
185 energy illuminant *E*.
187 References
188 ----------
189 :cite:`Jakob2019`, :cite:`Mallett2019`, :cite:`Meng2015c`,
190 :cite:`Otsu2018`, :cite:`Smits1999a`
192 Examples
193 --------
194 *Jakob and Hanika (2019)* reflectance recovery:
196 >>> import numpy as np
197 >>> from colour import MSDS_CMFS, SDS_ILLUMINANTS, SpectralShape
198 >>> from colour.colorimetry import sd_to_XYZ_integration
199 >>> from colour.utilities import numpy_print_options
200 >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952])
201 >>> cmfs = (
202 ... MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]
203 ... .copy()
204 ... .align(SpectralShape(360, 780, 10))
205 ... )
206 >>> illuminant = SDS_ILLUMINANTS["D65"].copy().align(cmfs.shape)
207 >>> sd = XYZ_to_sd(XYZ, method="Jakob 2019", cmfs=cmfs, illuminant=illuminant)
208 >>> with numpy_print_options(suppress=True):
209 ... sd # doctest: +ELLIPSIS
210 SpectralDistribution([[ 360. , 0.4893773...],
211 [ 370. , 0.3258214...],
212 [ 380. , 0.2147792...],
213 [ 390. , 0.1482413...],
214 [ 400. , 0.1086169...],
215 [ 410. , 0.0841255...],
216 [ 420. , 0.0683114...],
217 [ 430. , 0.0577144...],
218 [ 440. , 0.0504267...],
219 [ 450. , 0.0453552...],
220 [ 460. , 0.0418520...],
221 [ 470. , 0.0395259...],
222 [ 480. , 0.0381430...],
223 [ 490. , 0.0375741...],
224 [ 500. , 0.0377685...],
225 [ 510. , 0.0387432...],
226 [ 520. , 0.0405871...],
227 [ 530. , 0.0434783...],
228 [ 540. , 0.0477225...],
229 [ 550. , 0.0538256...],
230 [ 560. , 0.0626314...],
231 [ 570. , 0.0755869...],
232 [ 580. , 0.0952675...],
233 [ 590. , 0.1264265...],
234 [ 600. , 0.1779272...],
235 [ 610. , 0.2649393...],
236 [ 620. , 0.4039779...],
237 [ 630. , 0.5832105...],
238 [ 640. , 0.7445440...],
239 [ 650. , 0.8499970...],
240 [ 660. , 0.9094792...],
241 [ 670. , 0.9425378...],
242 [ 680. , 0.9616376...],
243 [ 690. , 0.9732481...],
244 [ 700. , 0.9806562...],
245 [ 710. , 0.9855873...],
246 [ 720. , 0.9889903...],
247 [ 730. , 0.9914117...],
248 [ 740. , 0.9931801...],
249 [ 750. , 0.9945009...],
250 [ 760. , 0.9955066...],
251 [ 770. , 0.9962855...],
252 [ 780. , 0.9968976...]],
253 SpragueInterpolator,
254 {},
255 Extrapolator,
256 {'method': 'Constant', 'left': None, 'right': None})
257 >>> sd_to_XYZ_integration(sd, cmfs, illuminant) / 100 # doctest: +ELLIPSIS
258 array([ 0.2066217..., 0.1220128..., 0.0513958...])
260 *Mallett and Yuksel (2019)* reflectance recovery:
262 >>> cmfs = (
263 ... MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]
264 ... .copy()
265 ... .align(SPECTRAL_SHAPE_sRGB_MALLETT2019)
266 ... )
267 >>> illuminant = SDS_ILLUMINANTS["D65"].copy().align(cmfs.shape)
268 >>> sd = XYZ_to_sd(XYZ, method="Mallett 2019")
269 >>> with numpy_print_options(suppress=True):
270 ... sd # doctest: +ELLIPSIS
271 SpectralDistribution([[ 380. , 0.1735531...],
272 [ 385. , 0.1720357...],
273 [ 390. , 0.1677721...],
274 [ 395. , 0.1576605...],
275 [ 400. , 0.1372829...],
276 [ 405. , 0.1170849...],
277 [ 410. , 0.0895694...],
278 [ 415. , 0.0706232...],
279 [ 420. , 0.0585765...],
280 [ 425. , 0.0523959...],
281 [ 430. , 0.0497598...],
282 [ 435. , 0.0476057...],
283 [ 440. , 0.0465079...],
284 [ 445. , 0.0460337...],
285 [ 450. , 0.0455839...],
286 [ 455. , 0.0452872...],
287 [ 460. , 0.0450981...],
288 [ 465. , 0.0448895...],
289 [ 470. , 0.0449257...],
290 [ 475. , 0.0448987...],
291 [ 480. , 0.0446834...],
292 [ 485. , 0.0441372...],
293 [ 490. , 0.0417137...],
294 [ 495. , 0.0373832...],
295 [ 500. , 0.0357657...],
296 [ 505. , 0.0348263...],
297 [ 510. , 0.0341953...],
298 [ 515. , 0.0337683...],
299 [ 520. , 0.0334979...],
300 [ 525. , 0.0332991...],
301 [ 530. , 0.0331909...],
302 [ 535. , 0.0332181...],
303 [ 540. , 0.0333387...],
304 [ 545. , 0.0334970...],
305 [ 550. , 0.0337381...],
306 [ 555. , 0.0341847...],
307 [ 560. , 0.0346447...],
308 [ 565. , 0.0353993...],
309 [ 570. , 0.0367367...],
310 [ 575. , 0.0392007...],
311 [ 580. , 0.0445902...],
312 [ 585. , 0.0625633...],
313 [ 590. , 0.2965381...],
314 [ 595. , 0.4215576...],
315 [ 600. , 0.4347139...],
316 [ 605. , 0.4385134...],
317 [ 610. , 0.4385184...],
318 [ 615. , 0.4385249...],
319 [ 620. , 0.4374694...],
320 [ 625. , 0.4384672...],
321 [ 630. , 0.4368251...],
322 [ 635. , 0.4340867...],
323 [ 640. , 0.4303219...],
324 [ 645. , 0.4243257...],
325 [ 650. , 0.4159482...],
326 [ 655. , 0.4057443...],
327 [ 660. , 0.3919874...],
328 [ 665. , 0.3742784...],
329 [ 670. , 0.3518421...],
330 [ 675. , 0.3240127...],
331 [ 680. , 0.2955145...],
332 [ 685. , 0.2625658...],
333 [ 690. , 0.2343423...],
334 [ 695. , 0.2174830...],
335 [ 700. , 0.2060461...],
336 [ 705. , 0.1977437...],
337 [ 710. , 0.1916846...],
338 [ 715. , 0.1861020...],
339 [ 720. , 0.1823908...],
340 [ 725. , 0.1807923...],
341 [ 730. , 0.1795571...],
342 [ 735. , 0.1785623...],
343 [ 740. , 0.1775758...],
344 [ 745. , 0.1771614...],
345 [ 750. , 0.1767431...],
346 [ 755. , 0.1764319...],
347 [ 760. , 0.1762597...],
348 [ 765. , 0.1762209...],
349 [ 770. , 0.1761803...],
350 [ 775. , 0.1761195...],
351 [ 780. , 0.1760763...]],
352 SpragueInterpolator,
353 {},
354 Extrapolator,
355 {'method': 'Constant', 'left': None, 'right': None})
356 >>> sd_to_XYZ_integration(sd, cmfs, illuminant) / 100
357 ... # doctest: +ELLIPSIS
358 array([ 0.2065436..., 0.1219996..., 0.0513764...])
360 *Meng (2015)* reflectance recovery:
362 >>> cmfs = (
363 ... MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]
364 ... .copy()
365 ... .align(SpectralShape(360, 780, 10))
366 ... )
367 >>> illuminant = SDS_ILLUMINANTS["D65"].copy().align(cmfs.shape)
368 >>> sd = XYZ_to_sd(XYZ, method="Meng 2015", cmfs=cmfs, illuminant=illuminant)
369 >>> with numpy_print_options(suppress=True):
370 ... sd # doctest: +SKIP
371 SpectralDistribution([[ 360. , 0.0762005...],
372 [ 370. , 0.0761792...],
373 [ 380. , 0.0761363...],
374 [ 390. , 0.0761194...],
375 [ 400. , 0.0762539...],
376 [ 410. , 0.0761671...],
377 [ 420. , 0.0754649...],
378 [ 430. , 0.0731519...],
379 [ 440. , 0.0676701...],
380 [ 450. , 0.0577800...],
381 [ 460. , 0.0441993...],
382 [ 470. , 0.0285064...],
383 [ 480. , 0.0138728...],
384 [ 490. , 0.0033585...],
385 [ 500. , 0. ...],
386 [ 510. , 0. ...],
387 [ 520. , 0. ...],
388 [ 530. , 0. ...],
389 [ 540. , 0.0055767...],
390 [ 550. , 0.0317581...],
391 [ 560. , 0.0754491...],
392 [ 570. , 0.1314115...],
393 [ 580. , 0.1937649...],
394 [ 590. , 0.2559311...],
395 [ 600. , 0.3123173...],
396 [ 610. , 0.3584966...],
397 [ 620. , 0.3927335...],
398 [ 630. , 0.4159458...],
399 [ 640. , 0.4306660...],
400 [ 650. , 0.4391040...],
401 [ 660. , 0.4439497...],
402 [ 670. , 0.4463618...],
403 [ 680. , 0.4474625...],
404 [ 690. , 0.4479868...],
405 [ 700. , 0.4482116...],
406 [ 710. , 0.4482800...],
407 [ 720. , 0.4483472...],
408 [ 730. , 0.4484251...],
409 [ 740. , 0.4484633...],
410 [ 750. , 0.4485071...],
411 [ 760. , 0.4484969...],
412 [ 770. , 0.4484853...],
413 [ 780. , 0.4485134...]],
414 SpragueInterpolator,
415 {},
416 Extrapolator,
417 {'method': 'Constant', 'left': None, 'right': None})
418 >>> sd_to_XYZ_integration(sd, cmfs, illuminant) / 100 # doctest: +ELLIPSIS
419 array([ 0.2065400..., 0.1219722..., 0.0513695...])
421 *Otsu, Yamamoto and Hachisuka (2018)* reflectance recovery:
423 >>> cmfs = (
424 ... MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]
425 ... .copy()
426 ... .align(SPECTRAL_SHAPE_OTSU2018)
427 ... )
428 >>> illuminant = SDS_ILLUMINANTS["D65"].copy().align(cmfs.shape)
429 >>> sd = XYZ_to_sd(XYZ, method="Otsu 2018", cmfs=cmfs, illuminant=illuminant)
430 >>> with numpy_print_options(suppress=True):
431 ... sd # doctest: +ELLIPSIS
432 SpectralDistribution([[ 380. , 0.0601939...],
433 [ 390. , 0.0568063...],
434 [ 400. , 0.0517429...],
435 [ 410. , 0.0495841...],
436 [ 420. , 0.0502007...],
437 [ 430. , 0.0506489...],
438 [ 440. , 0.0510020...],
439 [ 450. , 0.0493782...],
440 [ 460. , 0.0468046...],
441 [ 470. , 0.0437132...],
442 [ 480. , 0.0416957...],
443 [ 490. , 0.0403783...],
444 [ 500. , 0.0405197...],
445 [ 510. , 0.0406031...],
446 [ 520. , 0.0416912...],
447 [ 530. , 0.0430956...],
448 [ 540. , 0.0444474...],
449 [ 550. , 0.0459336...],
450 [ 560. , 0.0507631...],
451 [ 570. , 0.0628967...],
452 [ 580. , 0.0844661...],
453 [ 590. , 0.1334277...],
454 [ 600. , 0.2262428...],
455 [ 610. , 0.3599330...],
456 [ 620. , 0.4885571...],
457 [ 630. , 0.5752546...],
458 [ 640. , 0.6193023...],
459 [ 650. , 0.6450744...],
460 [ 660. , 0.6610548...],
461 [ 670. , 0.6688673...],
462 [ 680. , 0.6795426...],
463 [ 690. , 0.6887933...],
464 [ 700. , 0.7003469...],
465 [ 710. , 0.7084128...],
466 [ 720. , 0.7154674...],
467 [ 730. , 0.7234334...]],
468 SpragueInterpolator,
469 {},
470 Extrapolator,
471 {'method': 'Constant', 'left': None, 'right': None})
472 >>> sd_to_XYZ_integration(sd, cmfs, illuminant) / 100 # doctest: +ELLIPSIS
473 array([ 0.2065494..., 0.1219712..., 0.0514002...])
475 *Smits (1999)* reflectance recovery:
477 >>> cmfs = (
478 ... MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]
479 ... .copy()
480 ... .align(SpectralShape(360, 780, 10))
481 ... )
482 >>> illuminant = SDS_ILLUMINANTS["E"].copy().align(cmfs.shape)
483 >>> sd = XYZ_to_sd(XYZ, method="Smits 1999")
484 >>> with numpy_print_options(suppress=True):
485 ... sd # doctest: +ELLIPSIS
486 SpectralDistribution([[ 380. , 0.0787830...],
487 [ 417.7778 , 0.0622018...],
488 [ 455.5556 , 0.0446206...],
489 [ 493.3333 , 0.0352220...],
490 [ 531.1111 , 0.0324149...],
491 [ 568.8889 , 0.0330105...],
492 [ 606.6667 , 0.3207115...],
493 [ 644.4444 , 0.3836164...],
494 [ 682.2222 , 0.3836164...],
495 [ 720. , 0.3835649...]],
496 LinearInterpolator,
497 {},
498 Extrapolator,
499 {'method': 'Constant', 'left': None, 'right': None})
500 >>> sd_to_XYZ_integration(sd, cmfs, illuminant) / 100 # doctest: +ELLIPSIS
501 array([ 0.1894770..., 0.1126470..., 0.0474420...])
502 """
504 a = as_float_array(XYZ)
505 method = validate_method(method, tuple(XYZ_TO_SD_METHODS))
507 function = XYZ_TO_SD_METHODS[method]
509 if function is RGB_to_sd_Smits1999:
510 from colour.recovery.smits1999 import XYZ_to_RGB_Smits1999 # noqa: PLC0415
512 a = XYZ_to_RGB_Smits1999(XYZ)
513 elif function is RGB_to_sd_Mallett2019:
514 from colour.models import XYZ_to_sRGB # noqa: PLC0415
516 a = XYZ_to_sRGB(XYZ, apply_cctf_encoding=False)
518 return function(a, **filter_kwargs(function, **kwargs))
521__all__ += [
522 "XYZ_TO_SD_METHODS",
523 "XYZ_to_sd",
524]