Coverage for recovery/meng2015.py: 53%

1"""

2 Meng et al. (2015) - Reflectance Recovery

3=========================================

4

5 Define the objects for reflectance recovery using the *Meng, Simon and

6 Hanika (2015)* method.

7

8- :func:`colour.recovery.XYZ_to_sd_Meng2015`

9

10 References

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"""

16

17 from __future__ import annotations

18

19 import typing

20

21 import numpy as np

22

23 from colour.colorimetry import (

24 MultiSpectralDistributions,

25 SpectralDistribution,

26 SpectralShape,

27 handle_spectral_arguments,

28 sd_ones,

29 sd_to_XYZ_integration,

30)

31

32 if typing.TYPE_CHECKING:

33 from colour.hints import DTypeFloat

34

35 from colour.hints import Domain1, NDArrayFloat # noqa: TC001

36 from colour.utilities import from_range_100, required, to_domain_1

37

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"

44

45 __all__ = [

46 "SPECTRAL_SHAPE_MENG2015",

47 "XYZ_to_sd_Meng2015",

48]

49

50 SPECTRAL_SHAPE_MENG2015: SpectralShape = SpectralShape(360, 780, 5)

51"""

52 Spectral shape according to *ASTM E308-15* practise shape but using an interval

53 of 5.

54"""

55

56

57@required("SciPy")

58 def 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.

67

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.

84

85 Returns

86 -------

87 :class:`colour.SpectralDistribution`

88 Recovered spectral distribution.

89

90 Raises

91 ------

92 RuntimeError

93 If the optimisation fails.

94

95 Notes

96 -----

97 +------------+-----------------------+---------------+

98 | **Domain** | **Scale - Reference** | **Scale - 1** |

99 +============+=======================+===============+

100 | ``XYZ`` | 1 | 1 |

101 +------------+-----------------------+---------------+

102

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.

108

109 References

110 ----------

111 :cite:`Meng2015c`

112

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 """

177

178 from scipy.optimize import minimize # noqa: PLC0415

179

180 XYZ = to_domain_1(XYZ)

181

182 cmfs, illuminant = handle_spectral_arguments(

183 cmfs, illuminant, shape_default=SPECTRAL_SHAPE_MENG2015

184 )

185

186 sd = sd_ones(cmfs.shape)

187

188 def objective_function(a: NDArrayFloat) -> DTypeFloat:

189 """Define the objective function."""

190

191 return np.sum(np.square(np.diff(a)))

192

193 def constraint_function(a: NDArrayFloat) -> NDArrayFloat:

194 """Define the constraint function."""

195

196 sd[:] = a

197

198 return sd_to_XYZ_integration(sd, cmfs=cmfs, illuminant=illuminant) - XYZ

199

200 wavelengths = sd.wavelengths

201 bins = wavelengths.size

202

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)

213

214 result = minimize(objective_function, sd.values, **optimisation_settings)

215

216 if not result.success:

217 error = (

218 f"Optimisation failed for {XYZ} after {result.nit} iterations: "

219 f'"{result.message}".'

220 )

221

222 raise RuntimeError(error)

223

224 return SpectralDistribution(

225 from_range_100(result.x * 100),

226 wavelengths,

227 name=f"{XYZ} (XYZ) - Meng (2015)",

228 )