Coverage for colour/recovery/meng2015.py: 100%
36 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"""
2Meng et al. (2015) - Reflectance Recovery
3=========================================
5Define the objects for reflectance recovery using the *Meng, Simon and
6Hanika (2015)* method.
8- :func:`colour.recovery.XYZ_to_sd_Meng2015`
10References
11----------
12- :cite:`Meng2015c` : Meng, J., Simon, F., Hanika, J., & Dachsbacher, C.
13 (2015). Physically Meaningful Rendering using Tristimulus Colours. Computer
14 Graphics Forum, 34(4), 31-40. doi:10.1111/cgf.12676
15"""
17from __future__ import annotations
19import typing
21import numpy as np
23from colour.colorimetry import (
24 MultiSpectralDistributions,
25 SpectralDistribution,
26 SpectralShape,
27 handle_spectral_arguments,
28 sd_ones,
29 sd_to_XYZ_integration,
30)
32if typing.TYPE_CHECKING:
33 from colour.hints import DTypeFloat
35from colour.hints import Domain1, NDArrayFloat # noqa: TC001
36from colour.utilities import from_range_100, required, to_domain_1
38__author__ = "Colour Developers"
39__copyright__ = "Copyright 2013 Colour Developers"
40__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
41__maintainer__ = "Colour Developers"
42__email__ = "colour-developers@colour-science.org"
43__status__ = "Production"
45__all__ = [
46 "SPECTRAL_SHAPE_MENG2015",
47 "XYZ_to_sd_Meng2015",
48]
50SPECTRAL_SHAPE_MENG2015: SpectralShape = SpectralShape(360, 780, 5)
51"""
52Spectral shape according to *ASTM E308-15* practise shape but using an interval
53of 5.
54"""
57@required("SciPy")
58def XYZ_to_sd_Meng2015(
59 XYZ: Domain1,
60 cmfs: MultiSpectralDistributions | None = None,
61 illuminant: SpectralDistribution | None = None,
62 optimisation_kwargs: dict | None = None,
63) -> SpectralDistribution:
64 """
65 Recover the spectral distribution from the specified *CIE XYZ* tristimulus
66 values using the *Meng et al. (2015)* method.
68 Parameters
69 ----------
70 XYZ
71 *CIE XYZ* tristimulus values from which to recover the spectral
72 distribution.
73 cmfs
74 Standard observer colour matching functions. The wavelength
75 :math:`\\lambda_{i}` range interval of the colour matching functions
76 directly affects the computation time. The current default interval
77 of 5 provides a good compromise between precision and computation
78 time. Defaults to the *CIE 1931 2 Degree Standard Observer*.
79 illuminant
80 Illuminant spectral distribution. Defaults to
81 *CIE Standard Illuminant D65*.
82 optimisation_kwargs
83 Parameters for the :func:`scipy.optimize.minimize` definition.
85 Returns
86 -------
87 :class:`colour.SpectralDistribution`
88 Recovered spectral distribution.
90 Raises
91 ------
92 RuntimeError
93 If the optimisation fails.
95 Notes
96 -----
97 +------------+-----------------------+---------------+
98 | **Domain** | **Scale - Reference** | **Scale - 1** |
99 +============+=======================+===============+
100 | ``XYZ`` | 1 | 1 |
101 +------------+-----------------------+---------------+
103 - The definition used to convert spectrum to *CIE XYZ* tristimulus
104 values is :func:`colour.colorimetry.spectral_to_XYZ_integration`
105 because it processes any measurement interval, as opposed to
106 :func:`colour.colorimetry.sd_to_XYZ_ASTME308` which handles only
107 measurement intervals of 1, 5, 10 or 20nm.
109 References
110 ----------
111 :cite:`Meng2015c`
113 Examples
114 --------
115 >>> from colour import MSDS_CMFS, SDS_ILLUMINANTS
116 >>> from colour.utilities import numpy_print_options
117 >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952])
118 >>> cmfs = (
119 ... MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]
120 ... .copy()
121 ... .align(SpectralShape(360, 780, 10))
122 ... )
123 >>> illuminant = SDS_ILLUMINANTS["D65"].copy().align(cmfs.shape)
124 >>> sd = XYZ_to_sd_Meng2015(XYZ, cmfs, illuminant)
125 >>> with numpy_print_options(suppress=True):
126 ... sd # doctest: +SKIP
127 SpectralDistribution([[ 360. , 0.0762005...],
128 [ 370. , 0.0761792...],
129 [ 380. , 0.0761363...],
130 [ 390. , 0.0761194...],
131 [ 400. , 0.0762539...],
132 [ 410. , 0.0761671...],
133 [ 420. , 0.0754649...],
134 [ 430. , 0.0731519...],
135 [ 440. , 0.0676701...],
136 [ 450. , 0.0577800...],
137 [ 460. , 0.0441993...],
138 [ 470. , 0.0285064...],
139 [ 480. , 0.0138728...],
140 [ 490. , 0.0033585...],
141 [ 500. , 0. ...],
142 [ 510. , 0. ...],
143 [ 520. , 0. ...],
144 [ 530. , 0. ...],
145 [ 540. , 0.0055767...],
146 [ 550. , 0.0317581...],
147 [ 560. , 0.0754491...],
148 [ 570. , 0.1314115...],
149 [ 580. , 0.1937649...],
150 [ 590. , 0.2559311...],
151 [ 600. , 0.3123173...],
152 [ 610. , 0.3584966...],
153 [ 620. , 0.3927335...],
154 [ 630. , 0.4159458...],
155 [ 640. , 0.4306660...],
156 [ 650. , 0.4391040...],
157 [ 660. , 0.4439497...],
158 [ 670. , 0.4463618...],
159 [ 680. , 0.4474625...],
160 [ 690. , 0.4479868...],
161 [ 700. , 0.4482116...],
162 [ 710. , 0.4482800...],
163 [ 720. , 0.4483472...],
164 [ 730. , 0.4484251...],
165 [ 740. , 0.4484633...],
166 [ 750. , 0.4485071...],
167 [ 760. , 0.4484969...],
168 [ 770. , 0.4484853...],
169 [ 780. , 0.4485134...]],
170 interpolator=SpragueInterpolator,
171 interpolator_kwargs={},
172 extrapolator=Extrapolator,
173 extrapolator_kwargs={...})
174 >>> sd_to_XYZ_integration(sd, cmfs, illuminant) / 100 # doctest: +ELLIPSIS
175 array([ 0.2065400..., 0.1219722..., 0.0513695...])
176 """
178 from scipy.optimize import minimize # noqa: PLC0415
180 XYZ = to_domain_1(XYZ)
182 cmfs, illuminant = handle_spectral_arguments(
183 cmfs, illuminant, shape_default=SPECTRAL_SHAPE_MENG2015
184 )
186 sd = sd_ones(cmfs.shape)
188 def objective_function(a: NDArrayFloat) -> DTypeFloat:
189 """Define the objective function."""
191 return np.sum(np.square(np.diff(a)))
193 def constraint_function(a: NDArrayFloat) -> NDArrayFloat:
194 """Define the constraint function."""
196 sd[:] = a
198 return sd_to_XYZ_integration(sd, cmfs=cmfs, illuminant=illuminant) - XYZ
200 wavelengths = sd.wavelengths
201 bins = wavelengths.size
203 optimisation_settings = {
204 "method": "SLSQP",
205 "constraints": {"type": "eq", "fun": constraint_function},
206 "bounds": np.tile(np.array([0, 1000]), (bins, 1)),
207 "options": {
208 "ftol": 1e-10,
209 },
210 }
211 if optimisation_kwargs is not None:
212 optimisation_settings.update(optimisation_kwargs)
214 result = minimize(objective_function, sd.values, **optimisation_settings)
216 if not result.success:
217 error = (
218 f"Optimisation failed for {XYZ} after {result.nit} iterations: "
219 f'"{result.message}".'
220 )
222 raise RuntimeError(error)
224 return SpectralDistribution(
225 from_range_100(result.x * 100),
226 wavelengths,
227 name=f"{XYZ} (XYZ) - Meng (2015)",
228 )