Coverage for colorimetry/tristimulus_values.py: 83%

1"""

2 Tristimulus Values

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

4

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

6

7 This module provides comprehensive functionality for converting spectral

8 distributions to CIE XYZ tristimulus values using various integration and

9 summation methods. The default implementation follows the *ASTM E308-15*

10 standard 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.\

17 sd_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

27 References

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, Gü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

40 from __future__ import annotations

41

42 import typing

43

44 import numpy as np

45

46 from colour.algebra import lagrange_coefficients, sdiv, sdiv_mode

47 from colour.colorimetry import (

48 SPECTRAL_SHAPE_DEFAULT,

49 MultiSpectralDistributions,

50 SpectralDistribution,

51 SpectralShape,

52 reshape_msds,

53 reshape_sd,

54)

55

56 if typing.TYPE_CHECKING:

57 from colour.hints import (

58 Any,

59 ArrayLike,

60 Literal,

61 NDArrayFloat,

62 Range1,

63 Range100,

64 Tuple,

65 )

66

67 from colour.hints import Real, cast

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

109 SPECTRAL_SHAPE_ASTME308: SpectralShape = SPECTRAL_SHAPE_DEFAULT

110 SPECTRAL_SHAPE_ASTME308.__doc__ = """

111 Define the spectral shape for *ASTM E308-15* practice with wavelength range

112 from 360 to 780 nm at 1 nm intervals: (360, 780, 1).

113

114 References

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

130 def 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', \

186 SpectralShape(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

210 def 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

287 def 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

467 def 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

547 def 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

746 def 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

892 def 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

1101 SD_TO_XYZ_METHODS = CanonicalMapping(

1102 {"ASTM E308": sd_to_XYZ_ASTME308, "Integration": sd_to_XYZ_integration}

1103)

1104 SD_TO_XYZ_METHODS.__doc__ = """

1105 Supported spectral distribution to *CIE XYZ* tristimulus values conversion

1106 methods.

1107

1108 References

1109----------

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

1111:cite:`Wyszecki2000bf`

1112

1113 Aliases:

1114

1115- 'astm2015': 'ASTM E308'

1116"""

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

1118

1119

1120 def 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

1309 def 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

1547 def 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

1783 MSDS_TO_XYZ_METHODS = CanonicalMapping(

1784 {"ASTM E308": msds_to_XYZ_ASTME308, "Integration": msds_to_XYZ_integration}

1785)

1786 MSDS_TO_XYZ_METHODS.__doc__ = """

1787 Supported multi-spectral distributions to *CIE XYZ* tristimulus values

1788 conversion methods.

1789

1790 References

1791----------

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

1793:cite:`Wyszecki2000bf`

1794

1795 Aliases:

1796

1797- 'astm2015': 'ASTM E308'

1798"""

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

1800

1801

1802 def 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

2067 def 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))