Coverage for recovery/meng2015.py: 53%

36 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-16 22:49 +1300

1""" 

2Meng et al. (2015) - Reflectance Recovery 

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

4 

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

6Hanika (2015)* method. 

7 

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

9 

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

16 

17from __future__ import annotations 

18 

19import typing 

20 

21import numpy as np 

22 

23from colour.colorimetry import ( 

24 MultiSpectralDistributions, 

25 SpectralDistribution, 

26 SpectralShape, 

27 handle_spectral_arguments, 

28 sd_ones, 

29 sd_to_XYZ_integration, 

30) 

31 

32if typing.TYPE_CHECKING: 

33 from colour.hints import DTypeFloat 

34 

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

36from 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 

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

55 

56 

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. 

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 )