Coverage for colour/colorimetry/tristimulus_values.py: 100%

220 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-15 19:01 +1300

1""" 

2Tristimulus Values 

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

4 

5Define objects for computing CIE tristimulus values from spectral data. 

6 

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. 

11 

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` 

26 

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

39 

40from __future__ import annotations 

41 

42import typing 

43 

44import numpy as np 

45 

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) 

55 

56if typing.TYPE_CHECKING: 

57 from colour.hints import ( 

58 Any, 

59 ArrayLike, 

60 Literal, 

61 NDArrayFloat, 

62 Range1, 

63 Range100, 

64 Tuple, 

65 ) 

66 

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) 

83 

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" 

90 

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] 

108 

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). 

113 

114References 

115---------- 

116:cite:`ASTMInternational2015b` 

117""" 

118 

119_CACHE_LAGRANGE_INTERPOLATING_COEFFICIENTS: dict = CACHE_REGISTRY.register_cache( 

120 f"{__name__}._CACHE_LAGRANGE_INTERPOLATING_COEFFICIENTS" 

121) 

122 

123_CACHE_TRISTIMULUS_WEIGHTING_FACTORS: dict = CACHE_REGISTRY.register_cache( 

124 f"{__name__}._CACHE_TRISTIMULUS_WEIGHTING_FACTORS" 

125) 

126 

127_CACHE_SD_TO_XYZ: dict = CACHE_REGISTRY.register_cache(f"{__name__}._CACHE_SD_TO_XYZ") 

128 

129 

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. 

141 

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. 

150 

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. 

169 

170 Returns 

171 ------- 

172 :class:`tuple` 

173 Colour matching functions and illuminant. 

174 

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

188 

189 from colour import MSDS_CMFS, SDS_ILLUMINANTS # noqa: PLC0415 

190 

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 ) 

198 

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 ) 

204 

205 illuminant = reshape_sd(illuminant, cmfs.shape, copy=False) 

206 

207 return cmfs, illuminant 

208 

209 

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. 

217 

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. 

225 

226 Returns 

227 ------- 

228 :class:`numpy.ndarray` 

229 *Lagrange Coefficients*. 

230 

231 References 

232 ---------- 

233 :cite:`ASTMInternational2011a` 

234 

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

260 

261 global _CACHE_LAGRANGE_INTERPOLATING_COEFFICIENTS # noqa: PLW0602 

262 

263 interval_type = validate_method( 

264 interval_type, 

265 ("Boundary", "Inner"), 

266 '"{0}" interval type is invalid, it must be one of {1}!', 

267 ) 

268 

269 hash_key = hash((interval, interval_type)) 

270 

271 if is_caching_enabled() and hash_key in _CACHE_LAGRANGE_INTERPOLATING_COEFFICIENTS: 

272 return np.copy(_CACHE_LAGRANGE_INTERPOLATING_COEFFICIENTS[hash_key]) 

273 

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 

279 

280 lica = as_float_array([lagrange_coefficients(r, d) for r in r_n]) 

281 

282 _CACHE_LAGRANGE_INTERPOLATING_COEFFICIENTS[hash_key] = np.copy(lica) 

283 

284 return lica 

285 

286 

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. 

296 

297 The computed table of tristimulus weighting factors should be used with 

298 spectral data that has been corrected for spectral bandpass dependence. 

299 

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. 

325 

326 Returns 

327 ------- 

328 :class:`numpy.ndarray` 

329 Tristimulus weighting factors table. 

330 

331 Raises 

332 ------ 

333 ValueError 

334 If the colour matching functions or illuminant intervals are not 

335 equal to 1 nm. 

336 

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. 

346 

347 References 

348 ---------- 

349 :cite:`ASTMInternational2011a` 

350 

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

392 

393 if cmfs.shape.interval != 1: 

394 error = f'"{cmfs}" shape "interval" must be 1!' 

395 

396 raise ValueError(error) 

397 

398 if illuminant.shape.interval != 1: 

399 error = f'"{illuminant}" shape "interval" must be 1!' 

400 

401 raise ValueError(error) 

402 

403 global _CACHE_TRISTIMULUS_WEIGHTING_FACTORS # noqa: PLW0602 

404 

405 hash_key = hash((cmfs, illuminant, shape, k, get_domain_range_scale())) 

406 

407 if is_caching_enabled() and hash_key in _CACHE_TRISTIMULUS_WEIGHTING_FACTORS: 

408 return np.copy(_CACHE_TRISTIMULUS_WEIGHTING_FACTORS[hash_key]) 

409 

410 Y = cmfs.values 

411 S = illuminant.values 

412 

413 interval_i = int(shape.interval) 

414 W = S[::interval_i, None] * Y[::interval_i, :] 

415 

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

420 

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 

427 

428 # Intervals count. 

429 i_c = W.shape[0] 

430 i_cm = i_c - 1 

431 

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] 

437 

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 ) 

445 

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] 

454 

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] 

458 

459 with sdiv_mode(): 

460 W *= optional(k, sdiv(100, np.sum(W, axis=0)[1])) 

461 

462 _CACHE_TRISTIMULUS_WEIGHTING_FACTORS[hash_key] = np.copy(W) 

463 

464 return W 

465 

466 

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. 

474 

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. 

478 

479 Parameters 

480 ---------- 

481 W 

482 Tristimulus weighting factors table. 

483 shape_r 

484 Reference spectral shape. 

485 shape_t 

486 Test spectral shape. 

487 

488 Returns 

489 ------- 

490 :class:`numpy.ndarray` 

491 Adjusted tristimulus weighting factors. 

492 

493 References 

494 ---------- 

495 :cite:`ASTMInternational2015b` 

496 

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

533 

534 W = as_float_array(W).copy() 

535 

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] 

539 

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] 

543 

544 return W[start_index : -end_index or None, ...] 

545 

546 

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. 

558 

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. 

562 

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. 

594 

595 Returns 

596 ------- 

597 :class:`numpy.ndarray` 

598 *CIE XYZ* tristimulus values. 

599 

600 Notes 

601 ----- 

602 +-----------+-----------------------+---------------+ 

603 | **Range** | **Scale - Reference** | **Scale - 1** | 

604 +===========+=======================+===============+ 

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

606 +-----------+-----------------------+---------------+ 

607 

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. 

618 

619 References 

620 ---------- 

621 :cite:`Wyszecki2000bf` 

622 

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...]) 

656 

657 The default CMFS are the *CIE 1931 2 Degree Standard Observer*, and the 

658 default illuminant is *CIE Illuminant E*: 

659 

660 >>> sd_to_XYZ_integration(sd) 

661 ... # doctest: +ELLIPSIS 

662 array([ 11.7786939..., 9.9583972..., 5.7371816...]) 

663 """ 

664 

665 as_percentage = k is not None 

666 

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 ) 

680 

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) 

685 

686 shape = cmfs.shape 

687 

688 if sd.shape != shape: 

689 runtime_warning(f'Aligning "{sd.name}" spectral data shape to "{shape}".') 

690 

691 sd = ( 

692 reshape_sd(sd, shape, copy=False) 

693 if isinstance(sd, SpectralDistribution) 

694 else reshape_msds(sd, shape, copy=False) 

695 ) 

696 

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 ) 

705 

706 shape = cast("SpectralShape", shape) 

707 

708 R = as_float_array(sd) 

709 shape_R = R.shape 

710 wl_c_r = R.shape[-1] 

711 wl_c = len(shape.wavelengths) 

712 

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 ) 

718 

719 if cmfs.shape != shape: 

720 runtime_warning(f'Aligning "{cmfs.name}" cmfs shape to "{shape}".') 

721 cmfs = reshape_msds(cmfs, shape, copy=False) 

722 

723 if illuminant.shape != shape: 

724 runtime_warning(f'Aligning "{illuminant.name}" illuminant shape to "{shape}".') 

725 illuminant = reshape_sd(illuminant, shape, copy=False) 

726 

727 XYZ_b = cmfs.values 

728 S = illuminant.values 

729 R = np.reshape(R, (-1, wl_c_r)) 

730 

731 d_w = cmfs.shape.interval 

732 

733 with sdiv_mode(): 

734 k = cast("Real", optional(k, sdiv(100, (np.sum(XYZ_b[..., 1] * S) * d_w)))) 

735 

736 XYZ = k * np.dot(R * S, XYZ_b) * d_w 

737 

738 XYZ = from_range_100(np.reshape(XYZ, [*list(shape_R[:-1]), 3])) 

739 

740 if as_percentage: 

741 XYZ /= 100 

742 

743 return XYZ 

744 

745 

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. 

757 

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. 

784 

785 Returns 

786 ------- 

787 :class:`numpy.ndarray` 

788 *CIE XYZ* tristimulus values. 

789 

790 Notes 

791 ----- 

792 +-----------+-----------------------+---------------+ 

793 | **Range** | **Scale - Reference** | **Scale - 1** | 

794 +===========+=======================+===============+ 

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

796 +-----------+-----------------------+---------------+ 

797 

798 References 

799 ---------- 

800 :cite:`ASTMInternational2015b` 

801 

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...]) 

833 

834 The default CMFS are the *CIE 1931 2 Degree Standard Observer*, and the 

835 default illuminant is *CIE Illuminant E*: 

836 

837 >>> sd_to_XYZ_tristimulus_weighting_factors_ASTME308(sd) 

838 ... # doctest: +ELLIPSIS 

839 array([ 11.7786111..., 9.9589055..., 5.7403205...]) 

840 """ 

841 

842 cmfs, illuminant = handle_spectral_arguments( 

843 cmfs, 

844 illuminant, 

845 "CIE 1931 2 Degree Standard Observer", 

846 "E", 

847 SPECTRAL_SHAPE_ASTME308, 

848 ) 

849 

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 ) 

858 

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) 

865 

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) 

872 

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 ) 

884 

885 R = sd.values 

886 

887 XYZ = np.sum(W * R[..., None], axis=0) 

888 

889 return from_range_100(XYZ) 

890 

891 

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. 

905 

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. 

944 

945 Returns 

946 ------- 

947 :class:`numpy.ndarray` 

948 *CIE XYZ* tristimulus values. 

949 

950 Notes 

951 ----- 

952 +-----------+-----------------------+---------------+ 

953 | **Range** | **Scale - Reference** | **Scale - 1** | 

954 +===========+=======================+===============+ 

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

956 +-----------+-----------------------+---------------+ 

957 

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. 

961 

962 References 

963 ---------- 

964 :cite:`ASTMInternational2015b` 

965 

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...]) 

996 

997 The default CMFS are the *CIE 1931 2 Degree Standard Observer*, and the 

998 default illuminant is *CIE Illuminant E*: 

999 

1000 >>> sd_to_XYZ_ASTME308(sd) 

1001 ... # doctest: +ELLIPSIS 

1002 array([ 11.7781589..., 9.9585580..., 5.7408602...]) 

1003 """ 

1004 

1005 as_percentage = k is not None 

1006 

1007 cmfs, illuminant = handle_spectral_arguments( 

1008 cmfs, 

1009 illuminant, 

1010 "CIE 1931 2 Degree Standard Observer", 

1011 "E", 

1012 SPECTRAL_SHAPE_ASTME308, 

1013 ) 

1014 

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 ) 

1021 

1022 raise ValueError(error) 

1023 

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 ) 

1033 

1034 sd = reshape_sd(sd, cmfs.shape, copy=False) 

1035 

1036 if use_practice_range: 

1037 cmfs = reshape_msds(cmfs, SPECTRAL_SHAPE_ASTME308, "Trim", copy=False) 

1038 

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) 

1059 

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 ) 

1074 

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 ) 

1084 

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 ) 

1092 

1093 XYZ = method(sd, cmfs, illuminant, k=k) 

1094 

1095 if as_percentage and method is not sd_to_XYZ_integration: 

1096 XYZ /= 100 

1097 

1098 return XYZ 

1099 

1100 

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. 

1107 

1108References 

1109---------- 

1110:cite:`ASTMInternational2011a`, :cite:`ASTMInternational2015b`, 

1111:cite:`Wyszecki2000bf` 

1112 

1113Aliases: 

1114 

1115- 'astm2015': 'ASTM E308' 

1116""" 

1117SD_TO_XYZ_METHODS["astm2015"] = SD_TO_XYZ_METHODS["ASTM E308"] 

1118 

1119 

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. 

1131 

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. 

1135 

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. 

1167 

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. 

1189 

1190 Returns 

1191 ------- 

1192 :class:`numpy.ndarray` 

1193 *CIE XYZ* tristimulus values. 

1194 

1195 Notes 

1196 ----- 

1197 +-----------+-----------------------+---------------+ 

1198 | **Range** | **Scale - Reference** | **Scale - 1** | 

1199 +===========+=======================+===============+ 

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

1201 +-----------+-----------------------+---------------+ 

1202 

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. 

1213 

1214 References 

1215 ---------- 

1216 :cite:`ASTMInternational2011a`, :cite:`ASTMInternational2015b`, 

1217 :cite:`Wyszecki2000bf` 

1218 

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...]) 

1258 

1259 The default CMFS are the *CIE 1931 2 Degree Standard Observer*, and the 

1260 default illuminant is *CIE Illuminant E*: 

1261 

1262 >>> sd_to_XYZ(sd) 

1263 ... # doctest: +ELLIPSIS 

1264 array([ 11.7781589..., 9.9585580..., 5.7408602...]) 

1265 """ 

1266 

1267 cmfs, illuminant = handle_spectral_arguments( 

1268 cmfs, illuminant, illuminant_default="E" 

1269 ) 

1270 

1271 method = validate_method(method, tuple(SD_TO_XYZ_METHODS)) 

1272 

1273 global _CACHE_SD_TO_XYZ # noqa: PLW0602 

1274 

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 ) 

1290 

1291 if is_caching_enabled() and hash_key in _CACHE_SD_TO_XYZ: 

1292 return np.copy(_CACHE_SD_TO_XYZ[hash_key]) 

1293 

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] 

1301 

1302 XYZ = function(sd, cmfs, illuminant, k=k, **filter_kwargs(function, **kwargs)) 

1303 

1304 _CACHE_SD_TO_XYZ[hash_key] = np.copy(XYZ) 

1305 

1306 return XYZ 

1307 

1308 

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. 

1319 

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. 

1323 

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. 

1355 

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). 

1361 

1362 Notes 

1363 ----- 

1364 +-----------+-----------------------+---------------+ 

1365 | **Range** | **Scale - Reference** | **Scale - 1** | 

1366 +===========+=======================+===============+ 

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

1368 +-----------+-----------------------+---------------+ 

1369 

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: 

1383 

1384 .. code-block:: python 

1385 

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) 

1394 

1395 References 

1396 ---------- 

1397 :cite:`Wyszecki2000bf` 

1398 

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...]]]) 

1524 

1525 The default CMFS are the *CIE 1931 2 Degree Standard Observer*, and the 

1526 default illuminant is *CIE Illuminant E*: 

1527 

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

1543 

1544 return sd_to_XYZ_integration(msds, cmfs, illuminant, k, shape) 

1545 

1546 

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. 

1560 

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. 

1599 

1600 Returns 

1601 ------- 

1602 :class:`numpy.ndarray` 

1603 *CIE XYZ* tristimulus values. 

1604 

1605 Notes 

1606 ----- 

1607 +-----------+-----------------------+---------------+ 

1608 | **Range** | **Scale - Reference** | **Scale - 1** | 

1609 +===========+=======================+===============+ 

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

1611 +-----------+-----------------------+---------------+ 

1612 

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. 

1616 

1617 References 

1618 ---------- 

1619 :cite:`Wyszecki2000bf` 

1620 

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...]]) 

1731 

1732 The default CMFS are the *CIE 1931 2 Degree Standard Observer*, and the 

1733 default illuminant is *CIE Illuminant E*: 

1734 

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

1750 

1751 cmfs, illuminant = handle_spectral_arguments( 

1752 cmfs, 

1753 illuminant, 

1754 "CIE 1931 2 Degree Standard Observer", 

1755 "E", 

1756 SPECTRAL_SHAPE_ASTME308, 

1757 ) 

1758 

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 ) 

1774 

1775 error = ( 

1776 '"ASTM E308-15" method does not support "ArrayLike" ' 

1777 "multi-spectral distributions!" 

1778 ) 

1779 

1780 raise TypeError(error) 

1781 

1782 

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. 

1789 

1790References 

1791---------- 

1792:cite:`ASTMInternational2011a`, :cite:`ASTMInternational2015b`, 

1793:cite:`Wyszecki2000bf` 

1794 

1795Aliases: 

1796 

1797- 'astm2015': 'ASTM E308' 

1798""" 

1799MSDS_TO_XYZ_METHODS["astm2015"] = MSDS_TO_XYZ_METHODS["ASTM E308"] 

1800 

1801 

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. 

1816 

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. 

1847 

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. 

1869 

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). 

1875 

1876 Notes 

1877 ----- 

1878 +-----------+-----------------------+---------------+ 

1879 | **Range** | **Scale - Reference** | **Scale - 1** | 

1880 +===========+=======================+===============+ 

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

1882 +-----------+-----------------------+---------------+ 

1883 

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: 

1898 

1899 .. code-block:: python 

1900 

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) 

1909 

1910 References 

1911 ---------- 

1912 :cite:`ASTMInternational2011a`, :cite:`ASTMInternational2015b`, 

1913 :cite:`Wyszecki2000bf` 

1914 

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...]]]) 

2040 

2041 The default CMFS are the *CIE 1931 2 Degree Standard Observer*, and the 

2042 default illuminant is *CIE Illuminant E*: 

2043 

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

2059 

2060 method = validate_method(method, tuple(MSDS_TO_XYZ_METHODS)) 

2061 

2062 function = MSDS_TO_XYZ_METHODS[method] 

2063 

2064 return function(msds, cmfs, illuminant, k, **filter_kwargs(function, **kwargs)) 

2065 

2066 

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. 

2074 

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. 

2081 

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*. 

2089 

2090 Returns 

2091 ------- 

2092 :class:`numpy.ndarray` 

2093 *CIE XYZ* tristimulus values. 

2094 

2095 Raises 

2096 ------ 

2097 ValueError 

2098 If wavelength :math:`\\lambda` is not contained in the colour 

2099 matching functions domain. 

2100 

2101 Notes 

2102 ----- 

2103 +-----------+-----------------------+---------------+ 

2104 | **Range** | **Scale - Reference** | **Scale - 1** | 

2105 +===========+=======================+===============+ 

2106 | ``XYZ`` | 1 | 1 | 

2107 +-----------+-----------------------+---------------+ 

2108 

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

2118 

2119 wavelength = as_float_array(wavelength) 

2120 cmfs, _illuminant = handle_spectral_arguments(cmfs) 

2121 

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 ) 

2128 

2129 raise ValueError(error) 

2130 

2131 return np.reshape(cmfs[np.ravel(wavelength)], (*wavelength.shape, 3))