Coverage for colour/recovery/mallett2019.py: 100%
39 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 23:01 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 23:01 +1300
1"""
2Mallett and Yuksel (2019) - Reflectance Recovery
3================================================
5Define the objects for reflectance recovery, i.e., spectral upsampling, using
6*Mallett and Yuksel (2019)* method.
8- :func:`colour.recovery.spectral_primary_decomposition_Mallett2019`
9- :func:`colour.recovery.RGB_to_sd_Mallett2019`
11References
12----------
13- :cite:`Mallett2019` : Mallett, I., & Yuksel, C. (2019). Spectral Primary
14 Decomposition for Rendering with sRGB Reflectance. Eurographics Symposium
15 on Rendering - DL-Only and Industry Track, 7 pages. doi:10.2312/SR.20191216
16"""
18from __future__ import annotations
20import typing
22import numpy as np
24from colour.colorimetry import (
25 MultiSpectralDistributions,
26 SpectralDistribution,
27 handle_spectral_arguments,
28)
30if typing.TYPE_CHECKING:
31 from colour.models import RGB_Colourspace
32 from colour.hints import Callable, Domain1
34from colour.hints import Domain1 # noqa: TC001
35from colour.recovery import MSDS_BASIS_FUNCTIONS_sRGB_MALLETT2019
36from colour.utilities import 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_primary_decomposition_Mallett2019",
47 "RGB_to_sd_Mallett2019",
48]
51@required("SciPy")
52def spectral_primary_decomposition_Mallett2019(
53 colourspace: RGB_Colourspace,
54 cmfs: MultiSpectralDistributions | None = None,
55 illuminant: SpectralDistribution | None = None,
56 metric: Callable = np.linalg.norm,
57 metric_args: tuple = (),
58 optimisation_kwargs: dict | None = None,
59) -> MultiSpectralDistributions:
60 """
61 Perform spectral primary decomposition as described in *Mallett and
62 Yuksel (2019)* for the specified *RGB* colourspace.
64 Parameters
65 ----------
66 colourspace
67 *RGB* colourspace.
68 cmfs
69 Standard observer colour matching functions, default to the
70 *CIE 1931 2 Degree Standard Observer*.
71 illuminant
72 Illuminant spectral distribution, default to
73 *CIE Standard Illuminant D65*.
74 metric
75 Function to be minimised, i.e., the objective function.
77 ``metric(basis, *metric_args) -> float``
79 where ``basis`` is three reflectances concatenated together, each
80 with a shape matching ``shape``.
81 metric_args
82 Additional arguments passed to ``metric``.
83 optimisation_kwargs
84 Parameters for :func:`scipy.optimize.minimize` definition.
86 Returns
87 -------
88 :class:`colour.MultiSpectralDistributions`
89 Basis functions for the specified *RGB* colourspace.
91 References
92 ----------
93 :cite:`Mallett2019`
95 Notes
96 -----
97 - In addition to the *BT.709* primaries used by the *sRGB*
98 colourspace, :cite:`Mallett2019` tested *BT.2020*, *P3 D65*,
99 *Adobe RGB 1998*, *NTSC (1987)*, *Pal/Secam*, *ProPhoto RGB*, and
100 *Adobe Wide Gamut RGB* primaries, every one of which encompasses a
101 larger (albeit not always enveloping) set of *CIE L\\*a\\*b\\**
102 colours than BT.709. Of these, only *Pal/Secam* produces a
103 feasible basis, which is relatively unsurprising since it is very
104 similar to *BT.709*, whereas the others are significantly larger.
106 Examples
107 --------
108 >>> from colour import MSDS_CMFS, SDS_ILLUMINANTS, SpectralShape
109 >>> from colour.models import RGB_COLOURSPACE_PAL_SECAM
110 >>> from colour.utilities import numpy_print_options
111 >>> cmfs = (
112 ... MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]
113 ... .copy()
114 ... .align(SpectralShape(360, 780, 10))
115 ... )
116 >>> illuminant = SDS_ILLUMINANTS["D65"].copy().align(cmfs.shape)
117 >>> msds = spectral_primary_decomposition_Mallett2019(
118 ... RGB_COLOURSPACE_PAL_SECAM,
119 ... cmfs,
120 ... illuminant,
121 ... optimisation_kwargs={"options": {"ftol": 1e-5}},
122 ... )
123 >>> with numpy_print_options(suppress=True):
124 ... print(msds) # doctest: +SKIP
125 [[ 360. 0.3395134... 0.3400214... 0.3204650...]
126 [ 370. 0.3355246... 0.3338028... 0.3306724...]
127 [ 380. 0.3376707... 0.3185578... 0.3437715...]
128 [ 390. 0.3178866... 0.3351754... 0.3469378...]
129 [ 400. 0.3045154... 0.3248376... 0.3706469...]
130 [ 410. 0.2935652... 0.2919463... 0.4144884...]
131 [ 420. 0.1875740... 0.1853729... 0.6270530...]
132 [ 430. 0.0167983... 0.054483 ... 0.9287186...]
133 [ 440. 0. ... 0. ... 1. ...]
134 [ 450. 0. ... 0. ... 1. ...]
135 [ 460. 0. ... 0. ... 1. ...]
136 [ 470. 0. ... 0.0458044... 0.9541955...]
137 [ 480. 0. ... 0.2960917... 0.7039082...]
138 [ 490. 0. ... 0.5042592... 0.4957407...]
139 [ 500. 0. ... 0.6655795... 0.3344204...]
140 [ 510. 0. ... 0.8607541... 0.1392458...]
141 [ 520. 0. ... 0.9999998... 0.0000001...]
142 [ 530. 0. ... 1. ... 0. ...]
143 [ 540. 0. ... 1. ... 0. ...]
144 [ 550. 0. ... 1. ... 0. ...]
145 [ 560. 0. ... 0.9924229... 0. ...]
146 [ 570. 0. ... 0.9970703... 0.0025673...]
147 [ 580. 0.0396002... 0.9028231... 0.0575766...]
148 [ 590. 0.7058973... 0.2941026... 0. ...]
149 [ 600. 1. ... 0. ... 0. ...]
150 [ 610. 1. ... 0. ... 0. ...]
151 [ 620. 1. ... 0. ... 0. ...]
152 [ 630. 1. ... 0. ... 0. ...]
153 [ 640. 0.9835925... 0.0100166... 0.0063908...]
154 [ 650. 0.7878949... 0.1265097... 0.0855953...]
155 [ 660. 0.5987994... 0.2051062... 0.1960942...]
156 [ 670. 0.4724493... 0.2649623... 0.2625883...]
157 [ 680. 0.3989806... 0.3007488... 0.3002704...]
158 [ 690. 0.3666586... 0.3164003... 0.3169410...]
159 [ 700. 0.3497806... 0.3242863... 0.3259329...]
160 [ 710. 0.3563736... 0.3232441... 0.3203822...]
161 [ 720. 0.3362624... 0.3326209... 0.3311165...]
162 [ 730. 0.3245015... 0.3365982... 0.3389002...]
163 [ 740. 0.3335520... 0.3320670... 0.3343808...]
164 [ 750. 0.3441287... 0.3291168... 0.3267544...]
165 [ 760. 0.3343705... 0.3330132... 0.3326162...]
166 [ 770. 0.3274633... 0.3305704... 0.3419662...]
167 [ 780. 0.3475263... 0.3262331... 0.3262404...]]
168 """
170 from scipy.linalg import block_diag # noqa: PLC0415
171 from scipy.optimize import Bounds, LinearConstraint, minimize # noqa: PLC0415
173 cmfs, illuminant = handle_spectral_arguments(cmfs, illuminant)
175 N = len(cmfs.shape)
177 R_to_XYZ = np.transpose(
178 illuminant.values[..., None]
179 * cmfs.values
180 / (np.sum(cmfs.values[:, 1] * illuminant.values))
181 )
182 R_to_RGB = np.dot(colourspace.matrix_XYZ_to_RGB, R_to_XYZ)
183 basis_to_RGB = block_diag(R_to_RGB, R_to_RGB, R_to_RGB)
185 primaries = np.reshape(np.identity(3), 9)
187 # Ensure that the reflectances correspond to the correct RGB colours.
188 colour_match = LinearConstraint(basis_to_RGB, primaries, primaries)
190 # Ensure that the reflectances are bounded by [0, 1].
191 energy_conservation = Bounds(np.zeros(3 * N), np.ones(3 * N))
193 # Ensure that the sum of the three bases is bounded by [0, 1].
194 sum_matrix = np.transpose(np.tile(np.identity(N), (3, 1)))
195 sum_constraint = LinearConstraint(sum_matrix, np.zeros(N), np.ones(N))
197 optimisation_settings = {
198 "method": "SLSQP",
199 "constraints": [colour_match, sum_constraint],
200 "bounds": energy_conservation,
201 "options": {
202 "ftol": 1e-10,
203 },
204 }
206 if optimisation_kwargs is not None:
207 optimisation_settings.update(optimisation_kwargs)
209 result = minimize(
210 metric, args=metric_args, x0=np.zeros(3 * N), **optimisation_settings
211 )
213 basis_functions = np.transpose(np.reshape(result.x, (3, N)))
215 return MultiSpectralDistributions(
216 basis_functions,
217 cmfs.shape.wavelengths,
218 name=f"Basis Functions - {colourspace.name} - Mallett (2019)",
219 labels=("red", "green", "blue"),
220 )
223def RGB_to_sd_Mallett2019(
224 RGB: Domain1,
225 basis_functions: MultiSpectralDistributions = MSDS_BASIS_FUNCTIONS_sRGB_MALLETT2019,
226) -> SpectralDistribution:
227 """
228 Recover the spectral distribution of the specified *RGB* colourspace
229 array using *Mallett and Yuksel (2019)* method.
231 Parameters
232 ----------
233 RGB
234 *RGB* colourspace array.
235 basis_functions
236 Basis functions for the method. The default is to use the built-in
237 *sRGB* basis functions, i.e.,
238 :attr:`colour.recovery.MSDS_BASIS_FUNCTIONS_sRGB_MALLETT2019`.
240 Returns
241 -------
242 :class:`colour.SpectralDistribution`
243 Recovered reflectance.
245 References
246 ----------
247 :cite:`Mallett2019`
249 Notes
250 -----
251 +------------+-----------------------+---------------+
252 | **Domain** | **Scale - Reference** | **Scale - 1** |
253 +============+=======================+===============+
254 | ``RGB`` | 1 | 1 |
255 +------------+-----------------------+---------------+
257 - In addition to the *BT.709* primaries used by the *sRGB*
258 colourspace, :cite:`Mallett2019` tested *BT.2020*, *P3 D65*,
259 *Adobe RGB 1998*, *NTSC (1987)*, *Pal/Secam*, *ProPhoto RGB*, and
260 *Adobe Wide Gamut RGB* primaries, every one of which encompasses a
261 larger (albeit not always enveloping) set of *CIE L\\*a\\*b\\**
262 colours than BT.709. Of these, only *Pal/Secam* produces a
263 feasible basis, which is relatively unsurprising since it is very
264 similar to *BT.709*, whereas the others are significantly larger.
266 Examples
267 --------
268 >>> from colour import MSDS_CMFS, SDS_ILLUMINANTS, XYZ_to_sRGB
269 >>> from colour.colorimetry import sd_to_XYZ_integration
270 >>> from colour.recovery import SPECTRAL_SHAPE_sRGB_MALLETT2019
271 >>> from colour.utilities import numpy_print_options
272 >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952])
273 >>> RGB = XYZ_to_sRGB(XYZ, apply_cctf_encoding=False)
274 >>> cmfs = (
275 ... MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]
276 ... .copy()
277 ... .align(SPECTRAL_SHAPE_sRGB_MALLETT2019)
278 ... )
279 >>> illuminant = SDS_ILLUMINANTS["D65"].copy().align(cmfs.shape)
280 >>> sd = RGB_to_sd_Mallett2019(RGB)
281 >>> with numpy_print_options(suppress=True):
282 ... sd # doctest: +ELLIPSIS
283 SpectralDistribution([[ 380. , 0.1735531...],
284 [ 385. , 0.1720357...],
285 [ 390. , 0.1677721...],
286 [ 395. , 0.1576605...],
287 [ 400. , 0.1372829...],
288 [ 405. , 0.1170849...],
289 [ 410. , 0.0895694...],
290 [ 415. , 0.0706232...],
291 [ 420. , 0.0585765...],
292 [ 425. , 0.0523959...],
293 [ 430. , 0.0497598...],
294 [ 435. , 0.0476057...],
295 [ 440. , 0.0465079...],
296 [ 445. , 0.0460337...],
297 [ 450. , 0.0455839...],
298 [ 455. , 0.0452872...],
299 [ 460. , 0.0450981...],
300 [ 465. , 0.0448895...],
301 [ 470. , 0.0449257...],
302 [ 475. , 0.0448987...],
303 [ 480. , 0.0446834...],
304 [ 485. , 0.0441372...],
305 [ 490. , 0.0417137...],
306 [ 495. , 0.0373832...],
307 [ 500. , 0.0357657...],
308 [ 505. , 0.0348263...],
309 [ 510. , 0.0341953...],
310 [ 515. , 0.0337683...],
311 [ 520. , 0.0334979...],
312 [ 525. , 0.0332991...],
313 [ 530. , 0.0331909...],
314 [ 535. , 0.0332181...],
315 [ 540. , 0.0333387...],
316 [ 545. , 0.0334970...],
317 [ 550. , 0.0337381...],
318 [ 555. , 0.0341847...],
319 [ 560. , 0.0346447...],
320 [ 565. , 0.0353993...],
321 [ 570. , 0.0367367...],
322 [ 575. , 0.0392007...],
323 [ 580. , 0.0445902...],
324 [ 585. , 0.0625633...],
325 [ 590. , 0.2965381...],
326 [ 595. , 0.4215576...],
327 [ 600. , 0.4347139...],
328 [ 605. , 0.4385134...],
329 [ 610. , 0.4385184...],
330 [ 615. , 0.4385249...],
331 [ 620. , 0.4374694...],
332 [ 625. , 0.4384672...],
333 [ 630. , 0.4368251...],
334 [ 635. , 0.4340867...],
335 [ 640. , 0.4303219...],
336 [ 645. , 0.4243257...],
337 [ 650. , 0.4159482...],
338 [ 655. , 0.4057443...],
339 [ 660. , 0.3919874...],
340 [ 665. , 0.3742784...],
341 [ 670. , 0.3518421...],
342 [ 675. , 0.3240127...],
343 [ 680. , 0.2955145...],
344 [ 685. , 0.2625658...],
345 [ 690. , 0.2343423...],
346 [ 695. , 0.2174830...],
347 [ 700. , 0.2060461...],
348 [ 705. , 0.1977437...],
349 [ 710. , 0.1916846...],
350 [ 715. , 0.1861020...],
351 [ 720. , 0.1823908...],
352 [ 725. , 0.1807923...],
353 [ 730. , 0.1795571...],
354 [ 735. , 0.1785623...],
355 [ 740. , 0.1775758...],
356 [ 745. , 0.1771614...],
357 [ 750. , 0.1767431...],
358 [ 755. , 0.1764319...],
359 [ 760. , 0.1762597...],
360 [ 765. , 0.1762209...],
361 [ 770. , 0.1761803...],
362 [ 775. , 0.1761195...],
363 [ 780. , 0.1760763...]],
364 SpragueInterpolator,
365 {},
366 Extrapolator,
367 {'method': 'Constant', 'left': None, 'right': None})
368 >>> sd_to_XYZ_integration(sd, cmfs, illuminant) / 100
369 ... # doctest: +ELLIPSIS
370 array([ 0.2065436..., 0.1219996..., 0.0513764...])
371 """
373 RGB = to_domain_1(RGB)
375 sd = SpectralDistribution(
376 np.dot(RGB, np.transpose(basis_functions.values)),
377 basis_functions.wavelengths,
378 )
379 sd.name = f"{RGB} (RGB) - Mallett (2019)"
381 return sd