Coverage for colorimetry/tristimulus_values.py: 83%
222 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
1"""
2Tristimulus Values
3==================
5Define objects for computing CIE tristimulus values from spectral data.
7This module provides comprehensive functionality for converting spectral
8distributions to CIE XYZ tristimulus values using various integration and
9summation methods. The default implementation follows the *ASTM E308-15*
10standard practice.
12- :attr:`colour.SPECTRAL_SHAPE_ASTME308`
13- :func:`colour.colorimetry.handle_spectral_arguments`
14- :func:`colour.colorimetry.tristimulus_weighting_factors_ASTME2022`
15- :func:`colour.colorimetry.sd_to_XYZ_integration`
16- :func:`colour.colorimetry.\
17sd_to_XYZ_tristimulus_weighting_factors_ASTME308`
18- :func:`colour.colorimetry.sd_to_XYZ_ASTME308`
19- :attr:`colour.SD_TO_XYZ_METHODS`
20- :func:`colour.sd_to_XYZ`
21- :func:`colour.colorimetry.msds_to_XYZ_integration`
22- :func:`colour.colorimetry.msds_to_XYZ_ASTME308`
23- :attr:`colour.MSDS_TO_XYZ_METHODS`
24- :func:`colour.msds_to_XYZ`
25- :func:`colour.wavelength_to_XYZ`
27References
28----------
29- :cite:`ASTMInternational2011a` : ASTM International. (2011). ASTM E2022-11
30 - Standard Practice for Calculation of Weighting Factors for Tristimulus
31 Integration (pp. 1-10). doi:10.1520/E2022-11
32- :cite:`ASTMInternational2015b` : ASTM International. (2015). ASTM E308-15 -
33 Standard Practice for Computing the Colors of Objects by Using the CIE
34 System (pp. 1-47). doi:10.1520/E0308-15
35- :cite:`Wyszecki2000bf` : Wyszecki, Günther, & Stiles, W. S. (2000).
36 Integration Replaced by Summation. In Color Science: Concepts and Methods,
37 Quantitative Data and Formulae (pp. 158-163). Wiley. ISBN:978-0-471-39918-6
38"""
40from __future__ import annotations
42import typing
44import numpy as np
46from colour.algebra import lagrange_coefficients, sdiv, sdiv_mode
47from colour.colorimetry import (
48 SPECTRAL_SHAPE_DEFAULT,
49 MultiSpectralDistributions,
50 SpectralDistribution,
51 SpectralShape,
52 reshape_msds,
53 reshape_sd,
54)
56if typing.TYPE_CHECKING:
57 from colour.hints import (
58 Any,
59 ArrayLike,
60 Literal,
61 NDArrayFloat,
62 Range1,
63 Range100,
64 Tuple,
65 )
67from colour.hints import Real, cast
68from colour.utilities import (
69 CACHE_REGISTRY,
70 CanonicalMapping,
71 as_float_array,
72 as_int_scalar,
73 attest,
74 filter_kwargs,
75 from_range_100,
76 get_domain_range_scale,
77 int_digest,
78 is_caching_enabled,
79 optional,
80 runtime_warning,
81 validate_method,
82)
84__author__ = "Colour Developers"
85__copyright__ = "Copyright 2013 Colour Developers"
86__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
87__maintainer__ = "Colour Developers"
88__email__ = "colour-developers@colour-science.org"
89__status__ = "Production"
91__all__ = [
92 "SPECTRAL_SHAPE_ASTME308",
93 "handle_spectral_arguments",
94 "lagrange_coefficients_ASTME2022",
95 "tristimulus_weighting_factors_ASTME2022",
96 "adjust_tristimulus_weighting_factors_ASTME308",
97 "sd_to_XYZ_integration",
98 "sd_to_XYZ_tristimulus_weighting_factors_ASTME308",
99 "sd_to_XYZ_ASTME308",
100 "SD_TO_XYZ_METHODS",
101 "sd_to_XYZ",
102 "msds_to_XYZ_integration",
103 "msds_to_XYZ_ASTME308",
104 "MSDS_TO_XYZ_METHODS",
105 "msds_to_XYZ",
106 "wavelength_to_XYZ",
107]
109SPECTRAL_SHAPE_ASTME308: SpectralShape = SPECTRAL_SHAPE_DEFAULT
110SPECTRAL_SHAPE_ASTME308.__doc__ = """
111Define the spectral shape for *ASTM E308-15* practice with wavelength range
112from 360 to 780 nm at 1 nm intervals: (360, 780, 1).
114References
115----------
116:cite:`ASTMInternational2015b`
117"""
119_CACHE_LAGRANGE_INTERPOLATING_COEFFICIENTS: dict = CACHE_REGISTRY.register_cache(
120 f"{__name__}._CACHE_LAGRANGE_INTERPOLATING_COEFFICIENTS"
121)
123_CACHE_TRISTIMULUS_WEIGHTING_FACTORS: dict = CACHE_REGISTRY.register_cache(
124 f"{__name__}._CACHE_TRISTIMULUS_WEIGHTING_FACTORS"
125)
127_CACHE_SD_TO_XYZ: dict = CACHE_REGISTRY.register_cache(f"{__name__}._CACHE_SD_TO_XYZ")
130def handle_spectral_arguments(
131 cmfs: MultiSpectralDistributions | None = None,
132 illuminant: SpectralDistribution | None = None,
133 cmfs_default: str = "CIE 1931 2 Degree Standard Observer",
134 illuminant_default: str = "D65",
135 shape_default: SpectralShape = SPECTRAL_SHAPE_DEFAULT,
136 issue_runtime_warnings: bool = True,
137) -> Tuple[MultiSpectralDistributions, SpectralDistribution]:
138 """
139 Handle spectral arguments for various *Colour* definitions that perform
140 spectral computations.
142 - If ``cmfs`` is not specified, select one according to
143 ``cmfs_default``. The returned colour matching functions adopt the
144 spectral shape specified by ``shape_default``.
145 - If ``illuminant`` is not specified, select one according to
146 ``illuminant_default``. The returned illuminant adopts the spectral
147 shape of the returned colour matching functions.
148 - If ``illuminant`` is specified, align the returned illuminant's
149 spectral shape to that of the returned colour matching functions.
151 Parameters
152 ----------
153 cmfs
154 Standard observer colour matching functions, default to the
155 *CIE 1931 2 Degree Standard Observer*.
156 illuminant
157 Illuminant spectral distribution, default to
158 *CIE Standard Illuminant D65*.
159 cmfs_default
160 Default colour matching functions to use if ``cmfs`` is not
161 specified.
162 illuminant_default
163 Default illuminant to use if ``illuminant`` is not specified.
164 shape_default
165 Default spectral shape to align the final colour matching functions
166 and illuminant.
167 issue_runtime_warnings
168 Whether to issue runtime warnings.
170 Returns
171 -------
172 :class:`tuple`
173 Colour matching functions and illuminant.
175 Examples
176 --------
177 >>> cmfs, illuminant = handle_spectral_arguments()
178 >>> cmfs.name, cmfs.shape, illuminant.name, illuminant.shape
179 ('CIE 1931 2 Degree Standard Observer', SpectralShape(360.0, 780.0, 1.0), \
180'D65', SpectralShape(360.0, 780.0, 1.0))
181 >>> cmfs, illuminant = handle_spectral_arguments(
182 ... shape_default=SpectralShape(400, 700, 20)
183 ... )
184 >>> cmfs.name, cmfs.shape, illuminant.name, illuminant.shape
185 ('CIE 1931 2 Degree Standard Observer', \
186SpectralShape(400.0, 700.0, 20.0), 'D65', SpectralShape(400.0, 700.0, 20.0))
187 """
189 from colour import MSDS_CMFS, SDS_ILLUMINANTS # noqa: PLC0415
191 cmfs = optional(
192 cmfs, reshape_msds(MSDS_CMFS[cmfs_default], shape_default, copy=False)
193 )
194 illuminant = optional(
195 illuminant,
196 reshape_sd(SDS_ILLUMINANTS[illuminant_default], cmfs.shape, copy=False),
197 )
199 if illuminant.shape != cmfs.shape:
200 issue_runtime_warnings and runtime_warning(
201 f'Aligning "{illuminant.name}" illuminant shape to "{cmfs.name}" '
202 f"colour matching functions shape."
203 )
205 illuminant = reshape_sd(illuminant, cmfs.shape, copy=False)
207 return cmfs, illuminant
210def lagrange_coefficients_ASTME2022(
211 interval: int = 10,
212 interval_type: Literal["Boundary", "Inner"] | str = "Inner",
213) -> NDArrayFloat:
214 """
215 Compute *Lagrange Coefficients* for the specified interval size using
216 practice *ASTM E2022-11* method.
218 Parameters
219 ----------
220 interval
221 Interval size in nm.
222 interval_type
223 If the interval is an *inner* interval, *Lagrange Coefficients* are
224 computed for degree 4. Degree 3 is used for a *boundary* interval.
226 Returns
227 -------
228 :class:`numpy.ndarray`
229 *Lagrange Coefficients*.
231 References
232 ----------
233 :cite:`ASTMInternational2011a`
235 Examples
236 --------
237 >>> lagrange_coefficients_ASTME2022(10, "inner")
238 ... # doctest: +ELLIPSIS
239 array([[-0.028..., 0.940..., 0.104..., -0.016...],
240 [-0.048..., 0.864..., 0.216..., -0.032...],
241 [-0.059..., 0.773..., 0.331..., -0.045...],
242 [-0.064..., 0.672..., 0.448..., -0.056...],
243 [-0.062..., 0.562..., 0.562..., -0.062...],
244 [-0.056..., 0.448..., 0.672..., -0.064...],
245 [-0.045..., 0.331..., 0.773..., -0.059...],
246 [-0.032..., 0.216..., 0.864..., -0.048...],
247 [-0.016..., 0.104..., 0.940..., -0.028...]])
248 >>> lagrange_coefficients_ASTME2022(10, "boundary")
249 ... # doctest: +ELLIPSIS
250 array([[ 0.85..., 0.19..., -0.04...],
251 [ 0.72..., 0.36..., -0.08...],
252 [ 0.59..., 0.51..., -0.10...],
253 [ 0.48..., 0.64..., -0.12...],
254 [ 0.37..., 0.75..., -0.12...],
255 [ 0.28..., 0.84..., -0.12...],
256 [ 0.19..., 0.91..., -0.10...],
257 [ 0.12..., 0.96..., -0.08...],
258 [ 0.05..., 0.99..., -0.04...]])
259 """
261 global _CACHE_LAGRANGE_INTERPOLATING_COEFFICIENTS # noqa: PLW0602
263 interval_type = validate_method(
264 interval_type,
265 ("Boundary", "Inner"),
266 '"{0}" interval type is invalid, it must be one of {1}!',
267 )
269 hash_key = hash((interval, interval_type))
271 if is_caching_enabled() and hash_key in _CACHE_LAGRANGE_INTERPOLATING_COEFFICIENTS:
272 return np.copy(_CACHE_LAGRANGE_INTERPOLATING_COEFFICIENTS[hash_key])
274 r_n = np.linspace(1 / interval, 1 - (1 / interval), interval - 1)
275 d = 3
276 if interval_type == "inner":
277 r_n += 1
278 d = 4
280 lica = as_float_array([lagrange_coefficients(r, d) for r in r_n])
282 _CACHE_LAGRANGE_INTERPOLATING_COEFFICIENTS[hash_key] = np.copy(lica)
284 return lica
287def tristimulus_weighting_factors_ASTME2022(
288 cmfs: MultiSpectralDistributions,
289 illuminant: SpectralDistribution,
290 shape: SpectralShape,
291 k: Real | None = None,
292) -> NDArrayFloat:
293 """
294 Compute a table of tristimulus weighting factors for the specified colour
295 matching functions and illuminant using practise *ASTM E2022-11* method.
297 The computed table of tristimulus weighting factors should be used with
298 spectral data that has been corrected for spectral bandpass dependence.
300 Parameters
301 ----------
302 cmfs
303 Standard observer colour matching functions.
304 illuminant
305 Illuminant spectral distribution.
306 shape
307 Shape used to build the table, only the interval is needed.
308 k
309 Normalisation constant :math:`k`. For reflecting or transmitting
310 object colours, :math:`k` is chosen so that :math:`Y = 100` for
311 objects for which the spectral reflectance factor
312 :math:`R(\\lambda)` of the object colour or the spectral
313 transmittance factor :math:`\\tau(\\lambda)` of the object is equal
314 to unity for all wavelengths. For self-luminous objects and
315 illuminants, the constants :math:`k` is usually chosen on the
316 grounds of convenience. If, however, in the CIE 1931 standard
317 colorimetric system, the :math:`Y` value is required to be
318 numerically equal to the absolute value of a photometric quantity,
319 the constant, :math:`k`, must be put equal to the numerical value
320 of :math:`K_m`, the maximum spectral luminous efficacy (which is
321 equal to 683 :math:`lm\\cdot W^{-1}`) and
322 :math:`\\Phi_\\lambda(\\lambda)` must be the spectral concentration
323 of the radiometric quantity corresponding to the photometric
324 quantity required.
326 Returns
327 -------
328 :class:`numpy.ndarray`
329 Tristimulus weighting factors table.
331 Raises
332 ------
333 ValueError
334 If the colour matching functions or illuminant intervals are not
335 equal to 1 nm.
337 Notes
338 -----
339 - Input colour matching functions and illuminant intervals are
340 expected to be equal to 1 nm. If the illuminant data is not
341 available at 1 nm interval, it needs to be interpolated using *CIE*
342 recommendations: The method developed by *Sprague (1880)* should be
343 used for interpolating functions having a uniformly spaced
344 independent variable and a *Cubic Spline* method for non-uniformly
345 spaced independent variable.
347 References
348 ----------
349 :cite:`ASTMInternational2011a`
351 Examples
352 --------
353 >>> from colour import (
354 ... MSDS_CMFS,
355 ... SpectralDistribution,
356 ... SpectralShape,
357 ... sd_CIE_standard_illuminant_A,
358 ... )
359 >>> from colour.utilities import numpy_print_options
360 >>> cmfs = MSDS_CMFS["CIE 1964 10 Degree Standard Observer"]
361 >>> A = sd_CIE_standard_illuminant_A(cmfs.shape)
362 >>> with numpy_print_options(suppress=True):
363 ... tristimulus_weighting_factors_ASTME2022(
364 ... cmfs, A, SpectralShape(360, 830, 20)
365 ... )
366 ... # doctest: +ELLIPSIS
367 array([[ -0.0002981..., -0.0000317..., -0.0013301...],
368 [ -0.0087155..., -0.0008915..., -0.0407436...],
369 [ 0.0599679..., 0.0050203..., 0.2565018...],
370 [ 0.7734225..., 0.0779839..., 3.6965732...],
371 [ 1.9000905..., 0.3037005..., 9.7554195...],
372 [ 1.9707727..., 0.8552809..., 11.4867325...],
373 [ 0.7183623..., 2.1457000..., 6.7845806...],
374 [ 0.0426667..., 4.8985328..., 2.3208000...],
375 [ 1.5223302..., 9.6471138..., 0.7430671...],
376 [ 5.6770329..., 14.4609708..., 0.1958194...],
377 [ 12.4451744..., 17.4742541..., 0.0051827...],
378 [ 20.5535772..., 17.5838219..., -0.0026512...],
379 [ 25.3315384..., 14.8957035..., 0. ...],
380 [ 21.5711570..., 10.0796619..., 0. ...],
381 [ 12.1785817..., 5.0680655..., 0. ...],
382 [ 4.6675746..., 1.8303239..., 0. ...],
383 [ 1.3236117..., 0.5129694..., 0. ...],
384 [ 0.3175325..., 0.1230084..., 0. ...],
385 [ 0.0746341..., 0.0290243..., 0. ...],
386 [ 0.0182990..., 0.0071606..., 0. ...],
387 [ 0.0047942..., 0.0018888..., 0. ...],
388 [ 0.0013293..., 0.0005277..., 0. ...],
389 [ 0.0004254..., 0.0001704..., 0. ...],
390 [ 0.0000962..., 0.0000389..., 0. ...]])
391 """
393 if cmfs.shape.interval != 1:
394 error = f'"{cmfs}" shape "interval" must be 1!'
396 raise ValueError(error)
398 if illuminant.shape.interval != 1:
399 error = f'"{illuminant}" shape "interval" must be 1!'
401 raise ValueError(error)
403 global _CACHE_TRISTIMULUS_WEIGHTING_FACTORS # noqa: PLW0602
405 hash_key = hash((cmfs, illuminant, shape, k, get_domain_range_scale()))
407 if is_caching_enabled() and hash_key in _CACHE_TRISTIMULUS_WEIGHTING_FACTORS:
408 return np.copy(_CACHE_TRISTIMULUS_WEIGHTING_FACTORS[hash_key])
410 Y = cmfs.values
411 S = illuminant.values
413 interval_i = int(shape.interval)
414 W = S[::interval_i, None] * Y[::interval_i, :]
416 # First and last measurement intervals *Lagrange Coefficients*.
417 c_c = lagrange_coefficients_ASTME2022(interval_i, "boundary")
418 # Intermediate measurement intervals *Lagrange Coefficients*.
419 c_b = lagrange_coefficients_ASTME2022(interval_i, "inner")
421 # Total wavelengths count.
422 w_c = len(Y)
423 # Measurement interval interpolated values count.
424 r_c = c_b.shape[0]
425 # Last interval first interpolated wavelength.
426 w_lif = w_c - (w_c - 1) % interval_i - 1 - r_c
428 # Intervals count.
429 i_c = W.shape[0]
430 i_cm = i_c - 1
432 for i in range(3):
433 # First interval.
434 for h in range(r_c):
435 for g in range(3):
436 W[g, i] = W[g, i] + c_c[h, g] * S[h + 1] * Y[h + 1, i]
438 # Last interval.
439 for h in range(r_c):
440 for g in range(i_cm, i_cm - 3, -1):
441 W[g, i] = (
442 W[g, i]
443 + c_c[r_c - h - 1, i_cm - g] * S[h + w_lif] * Y[h + w_lif, i]
444 )
446 # Intermediate intervals.
447 for h in range(i_c - 3):
448 for g in range(r_c):
449 w_i = (r_c + 1) * (h + 1) + 1 + g
450 W[h, i] = W[h, i] + c_b[g, 0] * S[w_i] * Y[w_i, i]
451 W[h + 1, i] = W[h + 1, i] + c_b[g, 1] * S[w_i] * Y[w_i, i]
452 W[h + 2, i] = W[h + 2, i] + c_b[g, 2] * S[w_i] * Y[w_i, i]
453 W[h + 3, i] = W[h + 3, i] + c_b[g, 3] * S[w_i] * Y[w_i, i]
455 # Extrapolation of potential incomplete interval.
456 for h in range(as_int_scalar(w_c - ((w_c - 1) % interval_i)), w_c, 1):
457 W[i_cm, i] = W[i_cm, i] + S[h] * Y[h, i]
459 with sdiv_mode():
460 W *= optional(k, sdiv(100, np.sum(W, axis=0)[1]))
462 _CACHE_TRISTIMULUS_WEIGHTING_FACTORS[hash_key] = np.copy(W)
464 return W
467def adjust_tristimulus_weighting_factors_ASTME308(
468 W: ArrayLike, shape_r: SpectralShape, shape_t: SpectralShape
469) -> NDArrayFloat:
470 """
471 Adjust the specified table of tristimulus weighting factors to account for a
472 shorter wavelength range of the test spectral shape compared to the
473 reference spectral shape using practice *ASTM E308-15* method.
475 The adjustment redistributes weights at wavelengths for which data are
476 not available by adding them to the weights at the shortest and longest
477 wavelengths for which spectral data are available.
479 Parameters
480 ----------
481 W
482 Tristimulus weighting factors table.
483 shape_r
484 Reference spectral shape.
485 shape_t
486 Test spectral shape.
488 Returns
489 -------
490 :class:`numpy.ndarray`
491 Adjusted tristimulus weighting factors.
493 References
494 ----------
495 :cite:`ASTMInternational2015b`
497 Examples
498 --------
499 >>> from colour import (
500 ... MSDS_CMFS,
501 ... SpectralDistribution,
502 ... SpectralShape,
503 ... sd_CIE_standard_illuminant_A,
504 ... )
505 >>> from colour.utilities import numpy_print_options
506 >>> cmfs = MSDS_CMFS["CIE 1964 10 Degree Standard Observer"]
507 >>> A = sd_CIE_standard_illuminant_A(cmfs.shape)
508 >>> W = tristimulus_weighting_factors_ASTME2022(
509 ... cmfs, A, SpectralShape(360, 830, 20)
510 ... )
511 >>> with numpy_print_options(suppress=True):
512 ... adjust_tristimulus_weighting_factors_ASTME308(
513 ... W, SpectralShape(360, 830, 20), SpectralShape(400, 700, 20)
514 ... )
515 ... # doctest: +ELLIPSIS
516 array([[ 0.0509543..., 0.0040971..., 0.2144280...],
517 [ 0.7734225..., 0.0779839..., 3.6965732...],
518 [ 1.9000905..., 0.3037005..., 9.7554195...],
519 [ 1.9707727..., 0.8552809..., 11.4867325...],
520 [ 0.7183623..., 2.1457000..., 6.7845806...],
521 [ 0.0426667..., 4.8985328..., 2.3208000...],
522 [ 1.5223302..., 9.6471138..., 0.7430671...],
523 [ 5.6770329..., 14.4609708..., 0.1958194...],
524 [ 12.4451744..., 17.4742541..., 0.0051827...],
525 [ 20.5535772..., 17.5838219..., -0.0026512...],
526 [ 25.3315384..., 14.8957035..., 0. ...],
527 [ 21.5711570..., 10.0796619..., 0. ...],
528 [ 12.1785817..., 5.0680655..., 0. ...],
529 [ 4.6675746..., 1.8303239..., 0. ...],
530 [ 1.3236117..., 0.5129694..., 0. ...],
531 [ 0.4171109..., 0.1618194..., 0. ...]])
532 """
534 W = as_float_array(W).copy()
536 start_index = int((shape_t.start - shape_r.start) / shape_r.interval)
537 for i in range(start_index):
538 W[start_index] += W[i]
540 end_index = int((shape_r.end - shape_t.end) / shape_r.interval)
541 for i in range(end_index):
542 W[-end_index - 1] += W[-i - 1]
544 return W[start_index : -end_index or None, ...]
547def sd_to_XYZ_integration(
548 sd: ArrayLike | SpectralDistribution | MultiSpectralDistributions,
549 cmfs: MultiSpectralDistributions | None = None,
550 illuminant: SpectralDistribution | None = None,
551 k: Real | None = None,
552 shape: SpectralShape | None = None,
553) -> Range100:
554 """
555 Convert the specified spectral distribution to *CIE XYZ* tristimulus
556 values using the specified colour matching functions and illuminant
557 using the classical integration method.
559 The spectral distribution can be either a
560 :class:`colour.SpectralDistribution` class instance or an `ArrayLike` in
561 which case the ``shape`` must be passed.
563 Parameters
564 ----------
565 sd
566 Spectral distribution, if an `ArrayLike` the wavelengths are
567 expected to be in the last axis, e.g., for a spectral array with
568 77 bins, ``sd`` shape could be (77, ) or (1, 77).
569 cmfs
570 Standard observer colour matching functions, default to the
571 *CIE 1931 2 Degree Standard Observer*.
572 illuminant
573 Illuminant spectral distribution, default to *CIE Illuminant E*.
574 k
575 Normalisation constant :math:`k`. For reflecting or transmitting
576 object colours, :math:`k` is chosen so that :math:`Y = 100` for
577 objects for which the spectral reflectance factor
578 :math:`R(\\lambda)` of the object colour or the spectral
579 transmittance factor :math:`\\tau(\\lambda)` of the object is equal
580 to unity for all wavelengths. For self-luminous objects and
581 illuminants, the constants :math:`k` is usually chosen on the
582 grounds of convenience. If, however, in the CIE 1931 standard
583 colorimetric system, the :math:`Y` value is required to be
584 numerically equal to the absolute value of a photometric quantity,
585 the constant, :math:`k`, must be put equal to the numerical value
586 of :math:`K_m`, the maximum spectral luminous efficacy (which is
587 equal to 683 :math:`lm\\cdot W^{-1}`) and
588 :math:`\\Phi_\\lambda(\\lambda)` must be the spectral concentration
589 of the radiometric quantity corresponding to the photometric
590 quantity required.
591 shape
592 Spectral shape that ``sd``, ``cmfs`` and ``illuminant`` will be
593 aligned to if passed.
595 Returns
596 -------
597 :class:`numpy.ndarray`
598 *CIE XYZ* tristimulus values.
600 Notes
601 -----
602 +-----------+-----------------------+---------------+
603 | **Range** | **Scale - Reference** | **Scale - 1** |
604 +===========+=======================+===============+
605 | ``XYZ`` | 100 | 1 |
606 +-----------+-----------------------+---------------+
608 - When :math:`k` is set to a value other than *None*, the computed
609 *CIE XYZ* tristimulus values are assumed to be absolute and are thus
610 converted from percentages by a final division by 100.
611 - The code path using the `ArrayLike` spectral distribution produces
612 results different to the code path using a
613 :class:`colour.SpectralDistribution` class instance: the former
614 favours execution speed by aligning the colour matching functions
615 and illuminant to the specified spectral shape while the latter
616 favours precision by aligning the spectral distribution to the
617 colour matching functions.
619 References
620 ----------
621 :cite:`Wyszecki2000bf`
623 Examples
624 --------
625 >>> from colour import MSDS_CMFS, SDS_ILLUMINANTS
626 >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]
627 >>> illuminant = SDS_ILLUMINANTS["D65"]
628 >>> shape = SpectralShape(400, 700, 20)
629 >>> data = np.array(
630 ... [
631 ... 0.0641,
632 ... 0.0645,
633 ... 0.0562,
634 ... 0.0537,
635 ... 0.0559,
636 ... 0.0651,
637 ... 0.0705,
638 ... 0.0772,
639 ... 0.0870,
640 ... 0.1128,
641 ... 0.1360,
642 ... 0.1511,
643 ... 0.1688,
644 ... 0.1996,
645 ... 0.2397,
646 ... 0.2852,
647 ... ]
648 ... )
649 >>> sd = SpectralDistribution(data, shape)
650 >>> sd_to_XYZ_integration(sd, cmfs, illuminant)
651 ... # doctest: +ELLIPSIS
652 array([ 10.8404805..., 9.6838697..., 6.2115722...])
653 >>> sd_to_XYZ_integration(data, cmfs, illuminant, shape=shape)
654 ... # doctest: +ELLIPSIS
655 array([ 10.8993917..., 9.6986145..., 6.2540301...])
657 The default CMFS are the *CIE 1931 2 Degree Standard Observer*, and the
658 default illuminant is *CIE Illuminant E*:
660 >>> sd_to_XYZ_integration(sd)
661 ... # doctest: +ELLIPSIS
662 array([ 11.7786939..., 9.9583972..., 5.7371816...])
663 """
665 as_percentage = k is not None
667 # NOTE: The "illuminant" argument is reshaped by the
668 # `handle_spectral_arguments` definition, but, in this case, it is not
669 # desirable as we want to reshape it according to the final "shape" which,
670 # if not directly passed, is only available after the subsequent if/else
671 # block thus we are carefully avoiding to unpack over it.
672 if illuminant is None:
673 cmfs, illuminant = handle_spectral_arguments(
674 cmfs, illuminant, illuminant_default="E"
675 )
676 else:
677 cmfs, _illuminant = handle_spectral_arguments(
678 cmfs, illuminant, illuminant_default="E"
679 )
681 if isinstance(sd, (SpectralDistribution, MultiSpectralDistributions)):
682 if shape is not None:
683 cmfs = reshape_msds(cmfs, shape, copy=False)
684 illuminant = reshape_sd(illuminant, shape, copy=False)
686 shape = cmfs.shape
688 if sd.shape != shape:
689 runtime_warning(f'Aligning "{sd.name}" spectral data shape to "{shape}".')
691 sd = (
692 reshape_sd(sd, shape, copy=False)
693 if isinstance(sd, SpectralDistribution)
694 else reshape_msds(sd, shape, copy=False)
695 )
697 R = np.transpose(sd.values)
698 shape_R = R.shape
699 wl_c_r = R.shape[-1]
700 else:
701 attest(
702 shape is not None,
703 "A spectral shape must be explicitly passed with a spectral data array!",
704 )
706 shape = cast("SpectralShape", shape)
708 R = as_float_array(sd)
709 shape_R = R.shape
710 wl_c_r = R.shape[-1]
711 wl_c = len(shape.wavelengths)
713 attest(
714 wl_c_r == wl_c,
715 f"Spectral data array with {wl_c_r} wavelengths is not compatible "
716 f"with spectral shape with {wl_c} wavelengths!",
717 )
719 if cmfs.shape != shape:
720 runtime_warning(f'Aligning "{cmfs.name}" cmfs shape to "{shape}".')
721 cmfs = reshape_msds(cmfs, shape, copy=False)
723 if illuminant.shape != shape:
724 runtime_warning(f'Aligning "{illuminant.name}" illuminant shape to "{shape}".')
725 illuminant = reshape_sd(illuminant, shape, copy=False)
727 XYZ_b = cmfs.values
728 S = illuminant.values
729 R = np.reshape(R, (-1, wl_c_r))
731 d_w = cmfs.shape.interval
733 with sdiv_mode():
734 k = cast("Real", optional(k, sdiv(100, (np.sum(XYZ_b[..., 1] * S) * d_w))))
736 XYZ = k * np.dot(R * S, XYZ_b) * d_w
738 XYZ = from_range_100(np.reshape(XYZ, [*list(shape_R[:-1]), 3]))
740 if as_percentage:
741 XYZ /= 100
743 return XYZ
746def sd_to_XYZ_tristimulus_weighting_factors_ASTME308(
747 sd: SpectralDistribution,
748 cmfs: MultiSpectralDistributions | None = None,
749 illuminant: SpectralDistribution | None = None,
750 k: Real | None = None,
751) -> Range100:
752 """
753 Convert the specified spectral distribution to *CIE XYZ* tristimulus
754 values using the specified colour matching functions and illuminant
755 with a table of tristimulus weighting factors according to the
756 *ASTM E308-15* method.
758 Parameters
759 ----------
760 sd
761 Spectral distribution.
762 cmfs
763 Standard observer colour matching functions, default to the
764 *CIE 1931 2 Degree Standard Observer*.
765 illuminant
766 Illuminant spectral distribution, default to *CIE Illuminant E*.
767 k
768 Normalisation constant :math:`k`. For reflecting or transmitting
769 object colours, :math:`k` is chosen so that :math:`Y = 100` for
770 objects for which the spectral reflectance factor
771 :math:`R(\\lambda)` of the object colour or the spectral
772 transmittance factor :math:`\\tau(\\lambda)` of the object is equal
773 to unity for all wavelengths. For self-luminous objects and
774 illuminants, the constants :math:`k` is usually chosen on the
775 grounds of convenience. If, however, in the CIE 1931 standard
776 colorimetric system, the :math:`Y` value is required to be
777 numerically equal to the absolute value of a photometric quantity,
778 the constant, :math:`k`, must be put equal to the numerical value
779 of :math:`K_m`, the maximum spectral luminous efficacy (which is
780 equal to 683 :math:`lm\\cdot W^{-1}`) and
781 :math:`\\Phi_\\lambda(\\lambda)` must be the spectral concentration
782 of the radiometric quantity corresponding to the photometric
783 quantity required.
785 Returns
786 -------
787 :class:`numpy.ndarray`
788 *CIE XYZ* tristimulus values.
790 Notes
791 -----
792 +-----------+-----------------------+---------------+
793 | **Range** | **Scale - Reference** | **Scale - 1** |
794 +===========+=======================+===============+
795 | ``XYZ`` | 100 | 1 |
796 +-----------+-----------------------+---------------+
798 References
799 ----------
800 :cite:`ASTMInternational2015b`
802 Examples
803 --------
804 >>> from colour import MSDS_CMFS, SDS_ILLUMINANTS
805 >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]
806 >>> illuminant = SDS_ILLUMINANTS["D65"]
807 >>> shape = SpectralShape(400, 700, 20)
808 >>> data = np.array(
809 ... [
810 ... 0.0641,
811 ... 0.0645,
812 ... 0.0562,
813 ... 0.0537,
814 ... 0.0559,
815 ... 0.0651,
816 ... 0.0705,
817 ... 0.0772,
818 ... 0.0870,
819 ... 0.1128,
820 ... 0.1360,
821 ... 0.1511,
822 ... 0.1688,
823 ... 0.1996,
824 ... 0.2397,
825 ... 0.2852,
826 ... ]
827 ... )
828 >>> sd = SpectralDistribution(data, shape)
829 >>> sd_to_XYZ_tristimulus_weighting_factors_ASTME308(
830 ... sd, cmfs, illuminant
831 ... ) # doctest: +ELLIPSIS
832 array([ 10.8405832..., 9.6844909..., 6.2155622...])
834 The default CMFS are the *CIE 1931 2 Degree Standard Observer*, and the
835 default illuminant is *CIE Illuminant E*:
837 >>> sd_to_XYZ_tristimulus_weighting_factors_ASTME308(sd)
838 ... # doctest: +ELLIPSIS
839 array([ 11.7786111..., 9.9589055..., 5.7403205...])
840 """
842 cmfs, illuminant = handle_spectral_arguments(
843 cmfs,
844 illuminant,
845 "CIE 1931 2 Degree Standard Observer",
846 "E",
847 SPECTRAL_SHAPE_ASTME308,
848 )
850 if cmfs.shape.interval != 1:
851 runtime_warning(f'Interpolating "{cmfs.name}" cmfs to 1nm interval.')
852 cmfs = reshape_msds(
853 cmfs,
854 SpectralShape(cmfs.shape.start, cmfs.shape.end, 1),
855 "Interpolate",
856 copy=False,
857 )
859 if illuminant.shape != cmfs.shape:
860 runtime_warning(
861 f'Aligning "{illuminant.name}" illuminant shape to "{cmfs.name}" '
862 f"colour matching functions shape."
863 )
864 illuminant = reshape_sd(illuminant, cmfs.shape, copy=False)
866 if sd.shape.boundaries != cmfs.shape.boundaries:
867 runtime_warning(
868 f'Trimming "{sd.name}" spectral distribution boundaries using '
869 f'"{cmfs.name}" colour matching functions shape.'
870 )
871 sd = reshape_sd(sd, cmfs.shape, "Trim", copy=False)
873 W = tristimulus_weighting_factors_ASTME2022(
874 cmfs,
875 illuminant,
876 SpectralShape(cmfs.shape.start, cmfs.shape.end, sd.shape.interval),
877 k,
878 )
879 start_w = cmfs.shape.start
880 end_w = cmfs.shape.start + sd.shape.interval * (W.shape[0] - 1)
881 W = adjust_tristimulus_weighting_factors_ASTME308(
882 W, SpectralShape(start_w, end_w, sd.shape.interval), sd.shape
883 )
885 R = sd.values
887 XYZ = np.sum(W * R[..., None], axis=0)
889 return from_range_100(XYZ)
892def sd_to_XYZ_ASTME308(
893 sd: SpectralDistribution,
894 cmfs: MultiSpectralDistributions | None = None,
895 illuminant: SpectralDistribution | None = None,
896 use_practice_range: bool = True,
897 mi_5nm_omission_method: bool = True,
898 mi_20nm_interpolation_method: bool = True,
899 k: Real | None = None,
900) -> Range100:
901 """
902 Convert the specified spectral distribution to *CIE XYZ* tristimulus values
903 using the specified colour matching functions and illuminant according to
904 practice *ASTM E308-15* method.
906 Parameters
907 ----------
908 sd
909 Spectral distribution.
910 cmfs
911 Standard observer colour matching functions, default to the
912 *CIE 1931 2 Degree Standard Observer*.
913 illuminant
914 Illuminant spectral distribution, default to *CIE Illuminant E*.
915 use_practice_range
916 Practice *ASTM E308-15* working wavelengths range is [360, 780],
917 if *True* this argument will trim the colour matching functions
918 appropriately.
919 mi_5nm_omission_method
920 5 nm measurement intervals spectral distribution conversion to
921 tristimulus values will use a 5 nm version of the colour matching
922 functions instead of a table of tristimulus weighting factors.
923 mi_20nm_interpolation_method
924 20 nm measurement intervals spectral distribution conversion to
925 tristimulus values will use a dedicated interpolation method instead
926 of a table of tristimulus weighting factors.
927 k
928 Normalisation constant :math:`k`. For reflecting or transmitting
929 object colours, :math:`k` is chosen so that :math:`Y = 100` for
930 objects for which the spectral reflectance factor
931 :math:`R(\\lambda)` of the object colour or the spectral
932 transmittance factor :math:`\\tau(\\lambda)` of the object is equal
933 to unity for all wavelengths. For self-luminous objects and
934 illuminants, the constants :math:`k` is usually chosen on the
935 grounds of convenience. If, however, in the CIE 1931 standard
936 colorimetric system, the :math:`Y` value is required to be
937 numerically equal to the absolute value of a photometric quantity,
938 the constant, :math:`k`, must be put equal to the numerical value
939 of :math:`K_m`, the maximum spectral luminous efficacy (which is
940 equal to 683 :math:`lm\\cdot W^{-1}`) and
941 :math:`\\Phi_\\lambda(\\lambda)` must be the spectral concentration
942 of the radiometric quantity corresponding to the photometric
943 quantity required.
945 Returns
946 -------
947 :class:`numpy.ndarray`
948 *CIE XYZ* tristimulus values.
950 Notes
951 -----
952 +-----------+-----------------------+---------------+
953 | **Range** | **Scale - Reference** | **Scale - 1** |
954 +===========+=======================+===============+
955 | ``XYZ`` | 100 | 1 |
956 +-----------+-----------------------+---------------+
958 - When :math:`k` is set to a value other than *None*, the computed
959 *CIE XYZ* tristimulus values are assumed to be absolute and are thus
960 converted from percentages by a final division by 100.
962 References
963 ----------
964 :cite:`ASTMInternational2015b`
966 Examples
967 --------
968 >>> from colour import MSDS_CMFS, SDS_ILLUMINANTS
969 >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]
970 >>> illuminant = SDS_ILLUMINANTS["D65"]
971 >>> shape = SpectralShape(400, 700, 20)
972 >>> data = np.array(
973 ... [
974 ... 0.0641,
975 ... 0.0645,
976 ... 0.0562,
977 ... 0.0537,
978 ... 0.0559,
979 ... 0.0651,
980 ... 0.0705,
981 ... 0.0772,
982 ... 0.0870,
983 ... 0.1128,
984 ... 0.1360,
985 ... 0.1511,
986 ... 0.1688,
987 ... 0.1996,
988 ... 0.2397,
989 ... 0.2852,
990 ... ]
991 ... )
992 >>> sd = SpectralDistribution(data, shape)
993 >>> sd_to_XYZ_ASTME308(sd, cmfs, illuminant)
994 ... # doctest: +ELLIPSIS
995 array([ 10.8401953..., 9.6841740..., 6.2158913...])
997 The default CMFS are the *CIE 1931 2 Degree Standard Observer*, and the
998 default illuminant is *CIE Illuminant E*:
1000 >>> sd_to_XYZ_ASTME308(sd)
1001 ... # doctest: +ELLIPSIS
1002 array([ 11.7781589..., 9.9585580..., 5.7408602...])
1003 """
1005 as_percentage = k is not None
1007 cmfs, illuminant = handle_spectral_arguments(
1008 cmfs,
1009 illuminant,
1010 "CIE 1931 2 Degree Standard Observer",
1011 "E",
1012 SPECTRAL_SHAPE_ASTME308,
1013 )
1015 if sd.shape.interval not in (1, 5, 10, 20):
1016 error = (
1017 "Tristimulus values conversion from spectral data according to "
1018 'practise "ASTM E308-15" should be performed on spectral data '
1019 "with measurement interval of 1, 5, 10 or 20nm!"
1020 )
1022 raise ValueError(error)
1024 if sd.shape.interval in (10, 20) and (
1025 sd.shape.start % 10 != 0 or sd.shape.end % 10 != 0
1026 ):
1027 runtime_warning(
1028 f'"{sd.name}" spectral distribution shape does not start at a '
1029 f'tenth and will be aligned to "{cmfs.name}" colour matching '
1030 'functions shape! Note that practise "ASTM E308-15" does not '
1031 "define a behaviour in this case."
1032 )
1034 sd = reshape_sd(sd, cmfs.shape, copy=False)
1036 if use_practice_range:
1037 cmfs = reshape_msds(cmfs, SPECTRAL_SHAPE_ASTME308, "Trim", copy=False)
1039 method = sd_to_XYZ_tristimulus_weighting_factors_ASTME308
1040 if sd.shape.interval == 1:
1041 method = sd_to_XYZ_integration
1042 elif sd.shape.interval == 5 and mi_5nm_omission_method:
1043 if cmfs.shape.interval != 5:
1044 cmfs = reshape_msds(
1045 cmfs,
1046 SpectralShape(cmfs.shape.start, cmfs.shape.end, 5),
1047 "Interpolate",
1048 copy=False,
1049 )
1050 method = sd_to_XYZ_integration
1051 elif sd.shape.interval == 20 and mi_20nm_interpolation_method:
1052 sd = sd.copy()
1053 if sd.shape.boundaries != cmfs.shape.boundaries:
1054 runtime_warning(
1055 f'Trimming "{sd.name}" spectral distribution shape to '
1056 f'"{cmfs.name}" colour matching functions shape.'
1057 )
1058 sd = reshape_sd(sd, cmfs.shape, "Trim", copy=False)
1060 # Extrapolation of additional 20nm padding intervals.
1061 sd = reshape_sd(
1062 sd,
1063 SpectralShape(sd.shape.start - 20, sd.shape.end + 20, 10),
1064 copy=False,
1065 )
1066 for i in range(2):
1067 sd[sd.wavelengths[i]] = (
1068 3 * sd.values[i + 2] - 3 * sd.values[i + 4] + sd.values[i + 6]
1069 )
1070 i_e = len(sd.domain) - 1 - i
1071 sd[sd.wavelengths[i_e]] = (
1072 sd.values[i_e - 6] - 3 * sd.values[i_e - 4] + 3 * sd.values[i_e - 2]
1073 )
1075 # Interpolating every odd numbered values.
1076 # TODO: Investigate code vectorisation.
1077 for i in range(3, len(sd.domain) - 3, 2):
1078 sd[sd.wavelengths[i]] = (
1079 -0.0625 * sd.values[i - 3]
1080 + 0.5625 * sd.values[i - 1]
1081 + 0.5625 * sd.values[i + 1]
1082 - 0.0625 * sd.values[i + 3]
1083 )
1085 # Discarding the additional 20nm padding intervals.
1086 sd = reshape_sd(
1087 sd,
1088 SpectralShape(sd.shape.start + 20, sd.shape.end - 20, 10),
1089 "Trim",
1090 copy=False,
1091 )
1093 XYZ = method(sd, cmfs, illuminant, k=k)
1095 if as_percentage and method is not sd_to_XYZ_integration:
1096 XYZ /= 100
1098 return XYZ
1101SD_TO_XYZ_METHODS = CanonicalMapping(
1102 {"ASTM E308": sd_to_XYZ_ASTME308, "Integration": sd_to_XYZ_integration}
1103)
1104SD_TO_XYZ_METHODS.__doc__ = """
1105Supported spectral distribution to *CIE XYZ* tristimulus values conversion
1106methods.
1108References
1109----------
1110:cite:`ASTMInternational2011a`, :cite:`ASTMInternational2015b`,
1111:cite:`Wyszecki2000bf`
1113Aliases:
1115- 'astm2015': 'ASTM E308'
1116"""
1117SD_TO_XYZ_METHODS["astm2015"] = SD_TO_XYZ_METHODS["ASTM E308"]
1120def sd_to_XYZ(
1121 sd: ArrayLike | SpectralDistribution | MultiSpectralDistributions,
1122 cmfs: MultiSpectralDistributions | None = None,
1123 illuminant: SpectralDistribution | None = None,
1124 k: Real | None = None,
1125 method: Literal["ASTM E308", "Integration"] | str = "ASTM E308",
1126 **kwargs: Any,
1127) -> Range100:
1128 """
1129 Convert specified spectral distribution to *CIE XYZ* tristimulus values using
1130 specified colour matching functions, illuminant and method.
1132 If ``method`` is *Integration*, the spectral distribution can be either a
1133 :class:`colour.SpectralDistribution` class instance or an `ArrayLike` in
1134 which case the ``shape`` must be passed.
1136 Parameters
1137 ----------
1138 sd
1139 Spectral distribution, if an `ArrayLike` and ``method`` is
1140 *Integration* the wavelengths are expected to be in the last axis, e.g.,
1141 for a spectral array with 77 bins, ``sd`` shape could be (77, ) or
1142 (1, 77).
1143 cmfs
1144 Standard observer colour matching functions, default to the
1145 *CIE 1931 2 Degree Standard Observer*.
1146 illuminant
1147 Illuminant spectral distribution, default to *CIE Illuminant E*.
1148 k
1149 Normalisation constant :math:`k`. For reflecting or transmitting
1150 object colours, :math:`k` is chosen so that :math:`Y = 100` for
1151 objects for which the spectral reflectance factor
1152 :math:`R(\\lambda)` of the object colour or the spectral
1153 transmittance factor :math:`\\tau(\\lambda)` of the object is equal
1154 to unity for all wavelengths. For self-luminous objects and
1155 illuminants, the constants :math:`k` is usually chosen on the
1156 grounds of convenience. If, however, in the CIE 1931 standard
1157 colorimetric system, the :math:`Y` value is required to be
1158 numerically equal to the absolute value of a photometric quantity,
1159 the constant, :math:`k`, must be put equal to the numerical value
1160 of :math:`K_m`, the maximum spectral luminous efficacy (which is
1161 equal to 683 :math:`lm\\cdot W^{-1}`) and
1162 :math:`\\Phi_\\lambda(\\lambda)` must be the spectral concentration
1163 of the radiometric quantity corresponding to the photometric
1164 quantity required.
1165 method
1166 Computation method.
1168 Other Parameters
1169 ----------------
1170 mi_5nm_omission_method
1171 {:func:`colour.colorimetry.sd_to_XYZ_ASTME308`},
1172 5 nm measurement intervals spectral distribution conversion to
1173 tristimulus values will use a 5 nm version of the colour matching
1174 functions instead of a table of tristimulus weighting factors.
1175 mi_20nm_interpolation_method
1176 {:func:`colour.colorimetry.sd_to_XYZ_ASTME308`},
1177 20 nm measurement intervals spectral distribution conversion to
1178 tristimulus values will use a dedicated interpolation method instead
1179 of a table of tristimulus weighting factors.
1180 shape
1181 {:func:`colour.colorimetry.sd_to_XYZ_integration`},
1182 Spectral shape that ``sd``, ``cmfs`` and ``illuminant`` will be
1183 aligned to if passed.
1184 use_practice_range
1185 {:func:`colour.colorimetry.sd_to_XYZ_ASTME308`},
1186 Practise *ASTM E308-15* working wavelengths range is [360, 780],
1187 if *True* this argument will trim the colour matching functions
1188 appropriately.
1190 Returns
1191 -------
1192 :class:`numpy.ndarray`
1193 *CIE XYZ* tristimulus values.
1195 Notes
1196 -----
1197 +-----------+-----------------------+---------------+
1198 | **Range** | **Scale - Reference** | **Scale - 1** |
1199 +===========+=======================+===============+
1200 | ``XYZ`` | 100 | 1 |
1201 +-----------+-----------------------+---------------+
1203 - When :math:`k` is set to a value other than *None*, the computed
1204 *CIE XYZ* tristimulus values are assumed to be absolute and are thus
1205 converted from percentages by a final division by 100.
1206 - The code path using the `ArrayLike` spectral distribution produces
1207 results different to the code path using a
1208 :class:`colour.SpectralDistribution` class instance: the former
1209 favours execution speed by aligning the colour matching functions and
1210 illuminant to the specified spectral shape while the latter favours
1211 precision by aligning the spectral distribution to the colour matching
1212 functions.
1214 References
1215 ----------
1216 :cite:`ASTMInternational2011a`, :cite:`ASTMInternational2015b`,
1217 :cite:`Wyszecki2000bf`
1219 Examples
1220 --------
1221 >>> from colour import MSDS_CMFS, SDS_ILLUMINANTS
1222 >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]
1223 >>> illuminant = SDS_ILLUMINANTS["D65"]
1224 >>> shape = SpectralShape(400, 700, 20)
1225 >>> data = np.array(
1226 ... [
1227 ... 0.0641,
1228 ... 0.0645,
1229 ... 0.0562,
1230 ... 0.0537,
1231 ... 0.0559,
1232 ... 0.0651,
1233 ... 0.0705,
1234 ... 0.0772,
1235 ... 0.0870,
1236 ... 0.1128,
1237 ... 0.1360,
1238 ... 0.1511,
1239 ... 0.1688,
1240 ... 0.1996,
1241 ... 0.2397,
1242 ... 0.2852,
1243 ... ]
1244 ... )
1245 >>> sd = SpectralDistribution(data, shape)
1246 >>> sd_to_XYZ(sd, cmfs, illuminant)
1247 ... # doctest: +ELLIPSIS
1248 array([ 10.8401953..., 9.6841740..., 6.2158913...])
1249 >>> sd_to_XYZ(sd, cmfs, illuminant, use_practice_range=False)
1250 ... # doctest: +ELLIPSIS
1251 array([ 10.8402774..., 9.6841967..., 6.2158838...])
1252 >>> sd_to_XYZ(sd, cmfs, illuminant, method="Integration")
1253 ... # doctest: +ELLIPSIS
1254 array([ 10.8404805..., 9.6838697..., 6.2115722...])
1255 >>> sd_to_XYZ(data, cmfs, illuminant, method="Integration", shape=shape)
1256 ... # doctest: +ELLIPSIS
1257 array([ 10.8993917..., 9.6986145..., 6.2540301...])
1259 The default CMFS are the *CIE 1931 2 Degree Standard Observer*, and the
1260 default illuminant is *CIE Illuminant E*:
1262 >>> sd_to_XYZ(sd)
1263 ... # doctest: +ELLIPSIS
1264 array([ 11.7781589..., 9.9585580..., 5.7408602...])
1265 """
1267 cmfs, illuminant = handle_spectral_arguments(
1268 cmfs, illuminant, illuminant_default="E"
1269 )
1271 method = validate_method(method, tuple(SD_TO_XYZ_METHODS))
1273 global _CACHE_SD_TO_XYZ # noqa: PLW0602
1275 hash_key = hash(
1276 (
1277 (
1278 sd
1279 if isinstance(sd, (SpectralDistribution, MultiSpectralDistributions))
1280 else int_digest(np.asarray(sd).tobytes())
1281 ),
1282 cmfs,
1283 illuminant,
1284 k,
1285 method,
1286 tuple(kwargs.items()),
1287 get_domain_range_scale(),
1288 )
1289 )
1291 if is_caching_enabled() and hash_key in _CACHE_SD_TO_XYZ:
1292 return np.copy(_CACHE_SD_TO_XYZ[hash_key])
1294 if isinstance(sd, MultiSpectralDistributions):
1295 runtime_warning(
1296 "A multi-spectral distributions was passed, enforcing integration method!"
1297 )
1298 function = sd_to_XYZ_integration
1299 else:
1300 function = SD_TO_XYZ_METHODS[method]
1302 XYZ = function(sd, cmfs, illuminant, k=k, **filter_kwargs(function, **kwargs))
1304 _CACHE_SD_TO_XYZ[hash_key] = np.copy(XYZ)
1306 return XYZ
1309def msds_to_XYZ_integration(
1310 msds: ArrayLike | SpectralDistribution | MultiSpectralDistributions,
1311 cmfs: MultiSpectralDistributions | None = None,
1312 illuminant: SpectralDistribution | None = None,
1313 k: Real | None = None,
1314 shape: SpectralShape | None = None,
1315) -> Range100:
1316 """
1317 Convert the specified multi-spectral distributions to *CIE XYZ* tristimulus
1318 values using the specified colour matching functions and illuminant.
1320 The multi-spectral distributions can be either a
1321 :class:`colour.MultiSpectralDistributions` class instance or an
1322 `ArrayLike` in which case the ``shape`` must be passed.
1324 Parameters
1325 ----------
1326 msds
1327 Multi-spectral distributions, if an `ArrayLike` the wavelengths are
1328 expected to be in the last axis, e.g., for a 512x384 multi-spectral
1329 image with 77 bins, ``msds`` shape should be (384, 512, 77).
1330 cmfs
1331 Standard observer colour matching functions, default to the
1332 *CIE 1931 2 Degree Standard Observer*.
1333 illuminant
1334 Illuminant spectral distribution, default to *CIE Illuminant E*.
1335 k
1336 Normalisation constant :math:`k`. For reflecting or transmitting
1337 object colours, :math:`k` is chosen so that :math:`Y = 100` for
1338 objects for which the spectral reflectance factor
1339 :math:`R(\\lambda)` of the object colour or the spectral
1340 transmittance factor :math:`\\tau(\\lambda)` of the object is equal
1341 to unity for all wavelengths. For self-luminous objects and
1342 illuminants, the constants :math:`k` is usually chosen on the
1343 grounds of convenience. If, however, in the CIE 1931 standard
1344 colorimetric system, the :math:`Y` value is required to be
1345 numerically equal to the absolute value of a photometric quantity,
1346 the constant, :math:`k`, must be put equal to the numerical value
1347 of :math:`K_m`, the maximum spectral luminous efficacy (which is
1348 equal to 683 :math:`lm\\cdot W^{-1}`) and
1349 :math:`\\Phi_\\lambda(\\lambda)` must be the spectral concentration
1350 of the radiometric quantity corresponding to the photometric
1351 quantity required.
1352 shape
1353 Spectral shape that ``sd``, ``cmfs`` and ``illuminant`` will be
1354 aligned to if passed.
1356 Returns
1357 -------
1358 :class:`numpy.ndarray`
1359 *CIE XYZ* tristimulus values, for a 512x384 multi-spectral image with
1360 77 bins, the output shape will be (384, 512, 3).
1362 Notes
1363 -----
1364 +-----------+-----------------------+---------------+
1365 | **Range** | **Scale - Reference** | **Scale - 1** |
1366 +===========+=======================+===============+
1367 | ``XYZ`` | 100 | 1 |
1368 +-----------+-----------------------+---------------+
1370 - When :math:`k` is set to a value other than *None*, the computed
1371 *CIE XYZ* tristimulus values are assumed to be absolute and are thus
1372 converted from percentages by a final division by 100.
1373 - The code path using the `ArrayLike` multi-spectral distributions
1374 produces results different to the code path using a
1375 :class:`colour.MultiSpectralDistributions` class instance: the
1376 former favours execution speed by aligning the colour matching
1377 functions and illuminant to the specified spectral shape while the
1378 latter favours precision by aligning the multi-spectral distributions
1379 to the colour matching functions.
1380 - If precision is required, it is possible to interpolate the
1381 multi-spectral distributions with :py:class:`scipy.interpolate.interp1d`
1382 class on the last / tail axis as follows:
1384 .. code-block:: python
1386 interpolator = scipy.interpolate.interp1d(
1387 wavelengths,
1388 values,
1389 axis=-1,
1390 kind="linear",
1391 fill_value="extrapolate",
1392 )
1393 values_i = interpolator(wavelengths_i)
1395 References
1396 ----------
1397 :cite:`Wyszecki2000bf`
1399 Examples
1400 --------
1401 >>> from colour import MSDS_CMFS, SDS_ILLUMINANTS
1402 >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]
1403 >>> illuminant = SDS_ILLUMINANTS["D65"]
1404 >>> shape = SpectralShape(400, 700, 60)
1405 >>> data = np.array(
1406 ... [
1407 ... [
1408 ... 0.0137,
1409 ... 0.0159,
1410 ... 0.0096,
1411 ... 0.0111,
1412 ... 0.0179,
1413 ... 0.1057,
1414 ... 0.0433,
1415 ... 0.0258,
1416 ... 0.0248,
1417 ... 0.0186,
1418 ... 0.0310,
1419 ... 0.0473,
1420 ... ],
1421 ... [
1422 ... 0.0913,
1423 ... 0.3145,
1424 ... 0.2582,
1425 ... 0.0709,
1426 ... 0.2971,
1427 ... 0.4620,
1428 ... 0.2683,
1429 ... 0.0831,
1430 ... 0.1203,
1431 ... 0.1292,
1432 ... 0.1682,
1433 ... 0.3221,
1434 ... ],
1435 ... [
1436 ... 0.0152,
1437 ... 0.0842,
1438 ... 0.4139,
1439 ... 0.0220,
1440 ... 0.5630,
1441 ... 0.1918,
1442 ... 0.2373,
1443 ... 0.0430,
1444 ... 0.0054,
1445 ... 0.0079,
1446 ... 0.3719,
1447 ... 0.2268,
1448 ... ],
1449 ... [
1450 ... 0.0281,
1451 ... 0.0907,
1452 ... 0.2228,
1453 ... 0.1249,
1454 ... 0.2375,
1455 ... 0.5625,
1456 ... 0.0518,
1457 ... 0.3230,
1458 ... 0.0065,
1459 ... 0.4006,
1460 ... 0.0861,
1461 ... 0.3161,
1462 ... ],
1463 ... [
1464 ... 0.1918,
1465 ... 0.7103,
1466 ... 0.0041,
1467 ... 0.1817,
1468 ... 0.0024,
1469 ... 0.4209,
1470 ... 0.0118,
1471 ... 0.2302,
1472 ... 0.1860,
1473 ... 0.9404,
1474 ... 0.0041,
1475 ... 0.1124,
1476 ... ],
1477 ... [
1478 ... 0.0430,
1479 ... 0.0437,
1480 ... 0.3744,
1481 ... 0.0020,
1482 ... 0.5819,
1483 ... 0.0027,
1484 ... 0.0823,
1485 ... 0.0081,
1486 ... 0.3625,
1487 ... 0.3213,
1488 ... 0.7849,
1489 ... 0.0024,
1490 ... ],
1491 ... ]
1492 ... )
1493 >>> msds = MultiSpectralDistributions(data, shape)
1494 >>> msds_to_XYZ_integration(msds, cmfs, illuminant)
1495 ... # doctest: +ELLIPSIS
1496 array([[ 7.5029704..., 3.9487844..., 8.4034669...],
1497 [ 26.9259681..., 15.0724609..., 28.7057807...],
1498 [ 16.7032188..., 28.2172346..., 25.6455984...],
1499 [ 11.5767013..., 8.6400993..., 6.5768406...],
1500 [ 18.7314793..., 35.0750364..., 30.1457266...],
1501 [ 45.1656756..., 39.6136917..., 43.6783499...],
1502 [ 8.1755696..., 13.0934177..., 25.9420944...],
1503 [ 22.4676286..., 19.3099080..., 7.9637549...],
1504 [ 6.5781241..., 2.5255349..., 11.0930768...],
1505 [ 43.9147364..., 27.9803924..., 11.7292655...],
1506 [ 8.5365923..., 19.7030166..., 17.7050933...],
1507 [ 23.9088250..., 26.2129529..., 30.6763148...]])
1508 >>> data = np.reshape(data, (2, 6, 6))
1509 >>> msds_to_XYZ_integration(data, cmfs, illuminant, shape=shape)
1510 ... # doctest: +ELLIPSIS
1511 array([[[ 1.3104332..., 1.1377026..., 1.8267926...],
1512 [ 2.1875548..., 2.2510619..., 3.0721540...],
1513 [ 16.8714661..., 17.7063715..., 35.8709902...],
1514 [ 12.1648722..., 12.7222194..., 10.4880888...],
1515 [ 16.0419431..., 23.0985768..., 11.1479902...],
1516 [ 9.2391014..., 3.8301575..., 5.4703803...]],
1517 <BLANKLINE>
1518 [[ 13.8734231..., 17.3942194..., 11.0364103...],
1519 [ 27.7096381..., 20.8626722..., 35.5581690...],
1520 [ 22.7886687..., 11.4769218..., 78.3300659...],
1521 [ 51.1284864..., 52.2463568..., 26.1483754...],
1522 [ 14.4749229..., 20.5011495..., 6.6228107...],
1523 [ 33.6001365..., 36.3242617..., 2.8254217...]]])
1525 The default CMFS are the *CIE 1931 2 Degree Standard Observer*, and the
1526 default illuminant is *CIE Illuminant E*:
1528 >>> msds_to_XYZ_integration(msds)
1529 ... # doctest: +ELLIPSIS
1530 array([[ 8.2415862..., 4.2543993..., 7.6100842...],
1531 [ 29.6144619..., 16.1158465..., 25.9015472...],
1532 [ 16.6799560..., 27.2350547..., 22.9413337...],
1533 [ 12.5597688..., 9.0667136..., 5.9670327...],
1534 [ 18.5804689..., 33.6618109..., 26.9249733...],
1535 [ 47.7113308..., 40.4573249..., 39.6439145...],
1536 [ 7.830207 ..., 12.3689624..., 23.3742655...],
1537 [ 24.1695370..., 20.0629815..., 7.2718670...],
1538 [ 7.2333751..., 2.7982097..., 10.0688374...],
1539 [ 48.7358074..., 30.2417164..., 10.6753233...],
1540 [ 8.3231013..., 18.6791507..., 15.8228184...],
1541 [ 24.6452277..., 26.0809382..., 27.7106399...]])
1542 """
1544 return sd_to_XYZ_integration(msds, cmfs, illuminant, k, shape)
1547def msds_to_XYZ_ASTME308(
1548 msds: MultiSpectralDistributions,
1549 cmfs: MultiSpectralDistributions | None = None,
1550 illuminant: SpectralDistribution | None = None,
1551 k: Real | None = None,
1552 use_practice_range: bool = True,
1553 mi_5nm_omission_method: bool = True,
1554 mi_20nm_interpolation_method: bool = True,
1555) -> Range100:
1556 """
1557 Convert specified multi-spectral distributions to *CIE XYZ* tristimulus
1558 values using the specified colour matching functions and illuminant according
1559 to practise *ASTM E308-15* method.
1561 Parameters
1562 ----------
1563 msds
1564 Multi-spectral distributions.
1565 cmfs
1566 Standard observer colour matching functions, default to the
1567 *CIE 1931 2 Degree Standard Observer*.
1568 illuminant
1569 Illuminant spectral distribution, default to *CIE Illuminant E*.
1570 k
1571 Normalisation constant :math:`k`. For reflecting or transmitting
1572 object colours, :math:`k` is chosen so that :math:`Y = 100` for
1573 objects for which the spectral reflectance factor
1574 :math:`R(\\lambda)` of the object colour or the spectral
1575 transmittance factor :math:`\\tau(\\lambda)` of the object is equal
1576 to unity for all wavelengths. For self-luminous objects and
1577 illuminants, the constants :math:`k` is usually chosen on the
1578 grounds of convenience. If, however, in the CIE 1931 standard
1579 colorimetric system, the :math:`Y` value is required to be
1580 numerically equal to the absolute value of a photometric quantity,
1581 the constant, :math:`k`, must be put equal to the numerical value
1582 of :math:`K_m`, the maximum spectral luminous efficacy (which is
1583 equal to 683 :math:`lm\\cdot W^{-1}`) and
1584 :math:`\\Phi_\\lambda(\\lambda)` must be the spectral concentration
1585 of the radiometric quantity corresponding to the photometric
1586 quantity required.
1587 use_practice_range
1588 Practise *ASTM E308-15* working wavelengths range is [360, 780],
1589 if *True* this argument will trim the colour matching functions
1590 appropriately.
1591 mi_5nm_omission_method
1592 5 nm measurement intervals multi-spectral distributions conversion to
1593 tristimulus values will use a 5 nm version of the colour matching
1594 functions instead of a table of tristimulus weighting factors.
1595 mi_20nm_interpolation_method
1596 20 nm measurement intervals multi-spectral distributions conversion to
1597 tristimulus values will use a dedicated interpolation method instead
1598 of a table of tristimulus weighting factors.
1600 Returns
1601 -------
1602 :class:`numpy.ndarray`
1603 *CIE XYZ* tristimulus values.
1605 Notes
1606 -----
1607 +-----------+-----------------------+---------------+
1608 | **Range** | **Scale - Reference** | **Scale - 1** |
1609 +===========+=======================+===============+
1610 | ``XYZ`` | 100 | 1 |
1611 +-----------+-----------------------+---------------+
1613 - When :math:`k` is set to a value other than *None*, the computed
1614 *CIE XYZ* tristimulus values are assumed to be absolute and are thus
1615 converted from percentages by a final division by 100.
1617 References
1618 ----------
1619 :cite:`Wyszecki2000bf`
1621 Examples
1622 --------
1623 >>> from colour import MSDS_CMFS, SDS_ILLUMINANTS
1624 >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]
1625 >>> illuminant = SDS_ILLUMINANTS["D65"]
1626 >>> shape = SpectralShape(400, 700, 60)
1627 >>> data = np.array(
1628 ... [
1629 ... [
1630 ... 0.0137,
1631 ... 0.0159,
1632 ... 0.0096,
1633 ... 0.0111,
1634 ... 0.0179,
1635 ... 0.1057,
1636 ... 0.0433,
1637 ... 0.0258,
1638 ... 0.0248,
1639 ... 0.0186,
1640 ... 0.0310,
1641 ... 0.0473,
1642 ... ],
1643 ... [
1644 ... 0.0913,
1645 ... 0.3145,
1646 ... 0.2582,
1647 ... 0.0709,
1648 ... 0.2971,
1649 ... 0.4620,
1650 ... 0.2683,
1651 ... 0.0831,
1652 ... 0.1203,
1653 ... 0.1292,
1654 ... 0.1682,
1655 ... 0.3221,
1656 ... ],
1657 ... [
1658 ... 0.0152,
1659 ... 0.0842,
1660 ... 0.4139,
1661 ... 0.0220,
1662 ... 0.5630,
1663 ... 0.1918,
1664 ... 0.2373,
1665 ... 0.0430,
1666 ... 0.0054,
1667 ... 0.0079,
1668 ... 0.3719,
1669 ... 0.2268,
1670 ... ],
1671 ... [
1672 ... 0.0281,
1673 ... 0.0907,
1674 ... 0.2228,
1675 ... 0.1249,
1676 ... 0.2375,
1677 ... 0.5625,
1678 ... 0.0518,
1679 ... 0.3230,
1680 ... 0.0065,
1681 ... 0.4006,
1682 ... 0.0861,
1683 ... 0.3161,
1684 ... ],
1685 ... [
1686 ... 0.1918,
1687 ... 0.7103,
1688 ... 0.0041,
1689 ... 0.1817,
1690 ... 0.0024,
1691 ... 0.4209,
1692 ... 0.0118,
1693 ... 0.2302,
1694 ... 0.1860,
1695 ... 0.9404,
1696 ... 0.0041,
1697 ... 0.1124,
1698 ... ],
1699 ... [
1700 ... 0.0430,
1701 ... 0.0437,
1702 ... 0.3744,
1703 ... 0.0020,
1704 ... 0.5819,
1705 ... 0.0027,
1706 ... 0.0823,
1707 ... 0.0081,
1708 ... 0.3625,
1709 ... 0.3213,
1710 ... 0.7849,
1711 ... 0.0024,
1712 ... ],
1713 ... ]
1714 ... )
1715 >>> msds = MultiSpectralDistributions(data, shape)
1716 >>> msds = msds.align(SpectralShape(400, 700, 20))
1717 >>> msds_to_XYZ_ASTME308(msds, cmfs, illuminant)
1718 ... # doctest: +ELLIPSIS
1719 array([[ 7.5052758..., 3.9557516..., 8.38929 ...],
1720 [ 26.9408494..., 15.0987746..., 28.6631260...],
1721 [ 16.7047370..., 28.2089815..., 25.6556751...],
1722 [ 11.5711808..., 8.6445071..., 6.5587827...],
1723 [ 18.7428858..., 35.0626352..., 30.1778517...],
1724 [ 45.1224886..., 39.6238997..., 43.5813345...],
1725 [ 8.1786985..., 13.0950215..., 25.9326459...],
1726 [ 22.4462888..., 19.3115133..., 7.9304333...],
1727 [ 6.5764361..., 2.5305945..., 11.07253 ...],
1728 [ 43.9113380..., 28.0003541..., 11.6852531...],
1729 [ 8.5496209..., 19.6913570..., 17.7400079...],
1730 [ 23.8866733..., 26.2147704..., 30.6297684...]])
1732 The default CMFS are the *CIE 1931 2 Degree Standard Observer*, and the
1733 default illuminant is *CIE Illuminant E*:
1735 >>> msds_to_XYZ_ASTME308(msds)
1736 ... # doctest: +ELLIPSIS
1737 array([[ 8.2439318..., 4.2617641..., 7.5977409...],
1738 [ 29.6290771..., 16.1443076..., 25.8640484...],
1739 [ 16.6819067..., 27.2271403..., 22.9490590...],
1740 [ 12.5543694..., 9.0705685..., 5.9516323...],
1741 [ 18.5921357..., 33.6508573..., 26.9511144...],
1742 [ 47.6698072..., 40.4630866..., 39.5612904...],
1743 [ 7.8336896..., 12.3711768..., 23.3654245...],
1744 [ 24.1486630..., 20.0621956..., 7.2438655...],
1745 [ 7.2323703..., 2.8033217..., 10.0510790...],
1746 [ 48.7322793..., 30.2614779..., 10.6377135...],
1747 [ 8.3365770..., 18.6690888..., 15.8517212...],
1748 [ 24.6240657..., 26.0805317..., 27.6706915...]])
1749 """
1751 cmfs, illuminant = handle_spectral_arguments(
1752 cmfs,
1753 illuminant,
1754 "CIE 1931 2 Degree Standard Observer",
1755 "E",
1756 SPECTRAL_SHAPE_ASTME308,
1757 )
1759 if isinstance(msds, MultiSpectralDistributions):
1760 return as_float_array(
1761 [
1762 sd_to_XYZ_ASTME308(
1763 sd,
1764 cmfs,
1765 illuminant,
1766 use_practice_range,
1767 mi_5nm_omission_method,
1768 mi_20nm_interpolation_method,
1769 k,
1770 )
1771 for sd in msds.to_sds()
1772 ]
1773 )
1775 error = (
1776 '"ASTM E308-15" method does not support "ArrayLike" '
1777 "multi-spectral distributions!"
1778 )
1780 raise TypeError(error)
1783MSDS_TO_XYZ_METHODS = CanonicalMapping(
1784 {"ASTM E308": msds_to_XYZ_ASTME308, "Integration": msds_to_XYZ_integration}
1785)
1786MSDS_TO_XYZ_METHODS.__doc__ = """
1787Supported multi-spectral distributions to *CIE XYZ* tristimulus values
1788conversion methods.
1790References
1791----------
1792:cite:`ASTMInternational2011a`, :cite:`ASTMInternational2015b`,
1793:cite:`Wyszecki2000bf`
1795Aliases:
1797- 'astm2015': 'ASTM E308'
1798"""
1799MSDS_TO_XYZ_METHODS["astm2015"] = MSDS_TO_XYZ_METHODS["ASTM E308"]
1802def msds_to_XYZ(
1803 msds: ArrayLike | SpectralDistribution | MultiSpectralDistributions,
1804 cmfs: MultiSpectralDistributions | None = None,
1805 illuminant: SpectralDistribution | None = None,
1806 k: Real | None = None,
1807 method: Literal["ASTM E308", "Integration"] | str = "ASTM E308",
1808 **kwargs: Any,
1809) -> Range100:
1810 """
1811 Convert specified multi-spectral distributions to *CIE XYZ* tristimulus
1812 values using the specified colour matching functions and illuminant. For the
1813 *Integration* method, the multi-spectral distributions can be either a
1814 :class:`colour.MultiSpectralDistributions` class instance or an
1815 `ArrayLike` in which case the ``shape`` must be passed.
1817 Parameters
1818 ----------
1819 msds
1820 Multi-spectral distributions, if an `ArrayLike` the wavelengths are
1821 expected to be in the last axis, e.g., for a 512x384 multi-spectral
1822 image with 77 bins, ``msds`` shape should be (384, 512, 77).
1823 cmfs
1824 Standard observer colour matching functions, default to the
1825 *CIE 1931 2 Degree Standard Observer*.
1826 illuminant
1827 Illuminant spectral distribution, default to *CIE Illuminant E*.
1828 k
1829 Normalisation constant :math:`k`. For reflecting or transmitting
1830 object colours, :math:`k` is chosen so that :math:`Y = 100` for
1831 objects for which the spectral reflectance factor
1832 :math:`R(\\lambda)` of the object colour or the spectral
1833 transmittance factor :math:`\\tau(\\lambda)` of the object is equal
1834 to unity for all wavelengths. For self-luminous objects and
1835 illuminants, the constants :math:`k` is usually chosen on the
1836 grounds of convenience. If, however, in the CIE 1931 standard
1837 colorimetric system, the :math:`Y` value is required to be
1838 numerically equal to the absolute value of a photometric quantity,
1839 the constant, :math:`k`, must be put equal to the numerical value
1840 of :math:`K_m`, the maximum spectral luminous efficacy (which is
1841 equal to 683 :math:`lm\\cdot W^{-1}`) and
1842 :math:`\\Phi_\\lambda(\\lambda)` must be the spectral concentration
1843 of the radiometric quantity corresponding to the photometric
1844 quantity required.
1845 method
1846 Computation method.
1848 Other Parameters
1849 ----------------
1850 mi_5nm_omission_method
1851 {:func:`colour.colorimetry.msds_to_XYZ_ASTME308`},
1852 5 nm measurement intervals multi-spectral distributions conversion to
1853 tristimulus values will use a 5 nm version of the colour matching
1854 functions instead of a table of tristimulus weighting factors.
1855 mi_20nm_interpolation_method
1856 {:func:`colour.colorimetry.msds_to_XYZ_ASTME308`},
1857 20 nm measurement intervals multi-spectral distributions conversion to
1858 tristimulus values will use a dedicated interpolation method instead
1859 of a table of tristimulus weighting factors.
1860 shape
1861 {:func:`colour.colorimetry.msds_to_XYZ_integration`},
1862 Spectral shape that ``msds``, ``cmfs`` and ``illuminant`` will be
1863 aligned to if passed.
1864 use_practice_range
1865 {:func:`colour.colorimetry.msds_to_XYZ_ASTME308`},
1866 Practise *ASTM E308-15* working wavelengths range is [360, 780],
1867 if *True* this argument will trim the colour matching functions
1868 appropriately.
1870 Returns
1871 -------
1872 :class:`numpy.ndarray`
1873 *CIE XYZ* tristimulus values, for a 512x384 multi-spectral image
1874 with 77 wavelengths, the output shape will be (384, 512, 3).
1876 Notes
1877 -----
1878 +-----------+-----------------------+---------------+
1879 | **Range** | **Scale - Reference** | **Scale - 1** |
1880 +===========+=======================+===============+
1881 | ``XYZ`` | 100 | 1 |
1882 +-----------+-----------------------+---------------+
1884 - When :math:`k` is set to a value other than *None*, the computed
1885 *CIE XYZ* tristimulus values are assumed to be absolute and are thus
1886 converted from percentages by a final division by 100.
1887 - The code path using the `ArrayLike` multi-spectral distributions
1888 produces results different to the code path using a
1889 :class:`colour.MultiSpectralDistributions` class instance: the
1890 former favours execution speed by aligning the colour matching
1891 functions and illuminant to the specified spectral shape while the
1892 latter favours precision by aligning the multi-spectral distributions
1893 to the colour matching functions.
1894 - If precision is required, it is possible to interpolate the
1895 multi-spectral distributions with
1896 :py:class:`scipy.interpolate.interp1d` class on the last / tail axis
1897 as follows:
1899 .. code-block:: python
1901 interpolator = scipy.interpolate.interp1d(
1902 wavelengths,
1903 values,
1904 axis=-1,
1905 kind="linear",
1906 fill_value="extrapolate",
1907 )
1908 values_i = interpolator(wavelengths_i)
1910 References
1911 ----------
1912 :cite:`ASTMInternational2011a`, :cite:`ASTMInternational2015b`,
1913 :cite:`Wyszecki2000bf`
1915 Examples
1916 --------
1917 >>> from colour import MSDS_CMFS, SDS_ILLUMINANTS
1918 >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]
1919 >>> illuminant = SDS_ILLUMINANTS["D65"]
1920 >>> shape = SpectralShape(400, 700, 60)
1921 >>> data = np.array(
1922 ... [
1923 ... [
1924 ... 0.0137,
1925 ... 0.0159,
1926 ... 0.0096,
1927 ... 0.0111,
1928 ... 0.0179,
1929 ... 0.1057,
1930 ... 0.0433,
1931 ... 0.0258,
1932 ... 0.0248,
1933 ... 0.0186,
1934 ... 0.0310,
1935 ... 0.0473,
1936 ... ],
1937 ... [
1938 ... 0.0913,
1939 ... 0.3145,
1940 ... 0.2582,
1941 ... 0.0709,
1942 ... 0.2971,
1943 ... 0.4620,
1944 ... 0.2683,
1945 ... 0.0831,
1946 ... 0.1203,
1947 ... 0.1292,
1948 ... 0.1682,
1949 ... 0.3221,
1950 ... ],
1951 ... [
1952 ... 0.0152,
1953 ... 0.0842,
1954 ... 0.4139,
1955 ... 0.0220,
1956 ... 0.5630,
1957 ... 0.1918,
1958 ... 0.2373,
1959 ... 0.0430,
1960 ... 0.0054,
1961 ... 0.0079,
1962 ... 0.3719,
1963 ... 0.2268,
1964 ... ],
1965 ... [
1966 ... 0.0281,
1967 ... 0.0907,
1968 ... 0.2228,
1969 ... 0.1249,
1970 ... 0.2375,
1971 ... 0.5625,
1972 ... 0.0518,
1973 ... 0.3230,
1974 ... 0.0065,
1975 ... 0.4006,
1976 ... 0.0861,
1977 ... 0.3161,
1978 ... ],
1979 ... [
1980 ... 0.1918,
1981 ... 0.7103,
1982 ... 0.0041,
1983 ... 0.1817,
1984 ... 0.0024,
1985 ... 0.4209,
1986 ... 0.0118,
1987 ... 0.2302,
1988 ... 0.1860,
1989 ... 0.9404,
1990 ... 0.0041,
1991 ... 0.1124,
1992 ... ],
1993 ... [
1994 ... 0.0430,
1995 ... 0.0437,
1996 ... 0.3744,
1997 ... 0.0020,
1998 ... 0.5819,
1999 ... 0.0027,
2000 ... 0.0823,
2001 ... 0.0081,
2002 ... 0.3625,
2003 ... 0.3213,
2004 ... 0.7849,
2005 ... 0.0024,
2006 ... ],
2007 ... ]
2008 ... )
2009 >>> msds = MultiSpectralDistributions(data, shape)
2010 >>> msds_to_XYZ(msds, cmfs, illuminant, method="Integration")
2011 ... # doctest: +ELLIPSIS
2012 array([[ 7.5029704..., 3.9487844..., 8.4034669...],
2013 [ 26.9259681..., 15.0724609..., 28.7057807...],
2014 [ 16.7032188..., 28.2172346..., 25.6455984...],
2015 [ 11.5767013..., 8.6400993..., 6.5768406...],
2016 [ 18.7314793..., 35.0750364..., 30.1457266...],
2017 [ 45.1656756..., 39.6136917..., 43.6783499...],
2018 [ 8.1755696..., 13.0934177..., 25.9420944...],
2019 [ 22.4676286..., 19.3099080..., 7.9637549...],
2020 [ 6.5781241..., 2.5255349..., 11.0930768...],
2021 [ 43.9147364..., 27.9803924..., 11.7292655...],
2022 [ 8.5365923..., 19.7030166..., 17.7050933...],
2023 [ 23.9088250..., 26.2129529..., 30.6763148...]])
2024 >>> data = np.reshape(data, (2, 6, 6))
2025 >>> msds_to_XYZ(data, cmfs, illuminant, method="Integration", shape=shape)
2026 ... # doctest: +ELLIPSIS
2027 array([[[ 1.3104332..., 1.1377026..., 1.8267926...],
2028 [ 2.1875548..., 2.2510619..., 3.0721540...],
2029 [ 16.8714661..., 17.7063715..., 35.8709902...],
2030 [ 12.1648722..., 12.7222194..., 10.4880888...],
2031 [ 16.0419431..., 23.0985768..., 11.1479902...],
2032 [ 9.2391014..., 3.8301575..., 5.4703803...]],
2033 <BLANKLINE>
2034 [[ 13.8734231..., 17.3942194..., 11.0364103...],
2035 [ 27.7096381..., 20.8626722..., 35.5581690...],
2036 [ 22.7886687..., 11.4769218..., 78.3300659...],
2037 [ 51.1284864..., 52.2463568..., 26.1483754...],
2038 [ 14.4749229..., 20.5011495..., 6.6228107...],
2039 [ 33.6001365..., 36.3242617..., 2.8254217...]]])
2041 The default CMFS are the *CIE 1931 2 Degree Standard Observer*, and the
2042 default illuminant is *CIE Illuminant E*:
2044 >>> msds_to_XYZ(msds, method="Integration")
2045 ... # doctest: +ELLIPSIS
2046 array([[ 8.2415862..., 4.2543993..., 7.6100842...],
2047 [ 29.6144619..., 16.1158465..., 25.9015472...],
2048 [ 16.6799560..., 27.2350547..., 22.9413337...],
2049 [ 12.5597688..., 9.0667136..., 5.9670327...],
2050 [ 18.5804689..., 33.6618109..., 26.9249733...],
2051 [ 47.7113308..., 40.4573249..., 39.6439145...],
2052 [ 7.830207 ..., 12.3689624..., 23.3742655...],
2053 [ 24.1695370..., 20.0629815..., 7.2718670...],
2054 [ 7.2333751..., 2.7982097..., 10.0688374...],
2055 [ 48.7358074..., 30.2417164..., 10.6753233...],
2056 [ 8.3231013..., 18.6791507..., 15.8228184...],
2057 [ 24.6452277..., 26.0809382..., 27.7106399...]])
2058 """
2060 method = validate_method(method, tuple(MSDS_TO_XYZ_METHODS))
2062 function = MSDS_TO_XYZ_METHODS[method]
2064 return function(msds, cmfs, illuminant, k, **filter_kwargs(function, **kwargs))
2067def wavelength_to_XYZ(
2068 wavelength: ArrayLike,
2069 cmfs: MultiSpectralDistributions | None = None,
2070) -> Range1:
2071 """
2072 Convert the specified wavelength :math:`\\lambda` to *CIE XYZ* tristimulus
2073 values using the specified colour matching functions.
2075 If the wavelength :math:`\\lambda` is not available in the colour
2076 matching function, its value will be calculated according to
2077 *CIE 15:2004* recommendation: the method developed by *Sprague (1880)*
2078 will be used for interpolating functions having a uniformly spaced
2079 independent variable and the *Cubic Spline* method for non-uniformly
2080 spaced independent variable.
2082 Parameters
2083 ----------
2084 wavelength
2085 Wavelength :math:`\\lambda` in nm.
2086 cmfs
2087 Standard observer colour matching functions, default to the
2088 *CIE 1931 2 Degree Standard Observer*.
2090 Returns
2091 -------
2092 :class:`numpy.ndarray`
2093 *CIE XYZ* tristimulus values.
2095 Raises
2096 ------
2097 ValueError
2098 If wavelength :math:`\\lambda` is not contained in the colour
2099 matching functions domain.
2101 Notes
2102 -----
2103 +-----------+-----------------------+---------------+
2104 | **Range** | **Scale - Reference** | **Scale - 1** |
2105 +===========+=======================+===============+
2106 | ``XYZ`` | 1 | 1 |
2107 +-----------+-----------------------+---------------+
2109 Examples
2110 --------
2111 >>> from colour import MSDS_CMFS
2112 >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]
2113 >>> wavelength_to_XYZ(480, cmfs) # doctest: +ELLIPSIS
2114 array([ 0.09564 , 0.13902 , 0.8129501...])
2115 >>> wavelength_to_XYZ(480.5, cmfs) # doctest: +ELLIPSIS
2116 array([ 0.0914287..., 0.1418350..., 0.7915726...])
2117 """
2119 wavelength = as_float_array(wavelength)
2120 cmfs, _illuminant = handle_spectral_arguments(cmfs)
2122 shape = cmfs.shape
2123 if np.min(wavelength) < shape.start or np.max(wavelength) > shape.end:
2124 error = (
2125 f'"{wavelength}nm" wavelength is not in '
2126 f'"[{shape.start}, {shape.end}]" domain!'
2127 )
2129 raise ValueError(error)
2131 return np.reshape(cmfs[np.ravel(wavelength)], (*wavelength.shape, 3))