Coverage for characterisation/aces_it.py: 77%
211 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
1"""
2Academy Color Encoding System - Input Transform
3===============================================
5Define the *Academy Color Encoding System* (ACES) *Input Transform* utilities
6for camera RAW data processing and colour space transformations:
8- :func:`colour.sd_to_aces_relative_exposure_values`
9- :func:`colour.sd_to_ACES2065_1`
10- :func:`colour.characterisation.read_training_data_rawtoaces_v1`
11- :func:`colour.characterisation.generate_illuminants_rawtoaces_v1`
12- :func:`colour.characterisation.white_balance_multipliers`
13- :func:`colour.characterisation.best_illuminant`
14- :func:`colour.characterisation.normalise_illuminant`
15- :func:`colour.characterisation.training_data_sds_to_RGB`
16- :func:`colour.characterisation.training_data_sds_to_XYZ`
17- :func:`colour.characterisation.optimisation_factory_rawtoaces_v1`
18- :func:`colour.characterisation.optimisation_factory_Jzazbz`
19- :func:`colour.characterisation.optimisation_factory_Oklab_15`
20- :func:`colour.matrix_idt`
21- :func:`colour.camera_RGB_to_ACES2065_1`
23References
24----------
25- :cite:`Dyer2017` : Dyer, S., Forsythe, A., Irons, J., Mansencal, T., & Zhu,
26 M. (2017). RAW to ACES (Version 1.0) [Computer software].
27- :cite:`Forsythe2018` : Borer, T. (2017). Private Discussion with Mansencal,
28 T. and Shaw, N.
29- :cite:`Finlayson2015` : Finlayson, G. D., MacKiewicz, M., & Hurlbert, A.
30 (2015). Color Correction Using Root-Polynomial Regression. IEEE
31 Transactions on Image Processing, 24(5), 1460-1470.
32 doi:10.1109/TIP.2015.2405336
33- :cite:`TheAcademyofMotionPictureArtsandSciences2014q` : The Academy of
34 Motion Picture Arts and Sciences, Science and Technology Council, & Academy
35 Color Encoding System (ACES) Project Subcommittee. (2014). Technical
36 Bulletin TB-2014-004 - Informative Notes on SMPTE ST 2065-1 - Academy Color
37 Encoding Specification (ACES) (pp. 1-40). Retrieved December 19, 2014, from
38 http://j.mp/TB-2014-004
39- :cite:`TheAcademyofMotionPictureArtsandSciences2014r` : The Academy of
40 Motion Picture Arts and Sciences, Science and Technology Council, & Academy
41 Color Encoding System (ACES) Project Subcommittee. (2014). Technical
42 Bulletin TB-2014-012 - Academy Color Encoding System Version 1.0 Component
43 Names (pp. 1-8). Retrieved December 19, 2014, from http://j.mp/TB-2014-012
44- :cite:`TheAcademyofMotionPictureArtsandSciences2015c` : The Academy of
45 Motion Picture Arts and Sciences, Science and Technology Council, & Academy
46 Color Encoding System (ACES) Project Subcommittee. (2015). Procedure
47 P-2013-001 - Recommended Procedures for the Creation and Use of Digital
48 Camera System Input Device Transforms (IDTs) (pp. 1-29). Retrieved April
49 24, 2015, from http://j.mp/P-2013-001
50- :cite:`TheAcademyofMotionPictureArtsandSciencese` : The Academy of Motion
51 Picture Arts and Sciences, Science and Technology Council, & Academy Color
52 Encoding System (ACES) Project Subcommittee. (n.d.). Academy Color Encoding
53 System. Retrieved February 24, 2014, from
54 http://www.oscars.org/science-technology/council/projects/aces.html
55"""
57from __future__ import annotations
59import os
60import typing
62import numpy as np
64from colour.adaptation import matrix_chromatic_adaptation_VonKries
65from colour.algebra import euclidean_distance, vecmul
66from colour.characterisation import (
67 MSDS_ACES_RICD,
68 RGB_CameraSensitivities,
69 polynomial_expansion_Finlayson2015,
70)
71from colour.colorimetry import (
72 SDS_ILLUMINANTS,
73 MultiSpectralDistributions,
74 SpectralDistribution,
75 SpectralShape,
76 handle_spectral_arguments,
77 reshape_msds,
78 reshape_sd,
79 sd_blackbody,
80 sd_CIE_illuminant_D_series,
81 sd_to_XYZ,
82 sds_and_msds_to_msds,
83)
85if typing.TYPE_CHECKING:
86 from colour.hints import (
87 Any,
88 ArrayLike,
89 Callable,
90 DTypeFloat,
91 Literal,
92 LiteralChromaticAdaptationTransform,
93 Mapping,
94 NDArrayFloat,
95 Range1,
96 Tuple,
97 )
99from colour.hints import cast
100from colour.io import read_sds_from_csv_file
101from colour.models import XYZ_to_Jzazbz, XYZ_to_Lab, XYZ_to_Oklab, XYZ_to_xy, xy_to_XYZ
102from colour.models.rgb import (
103 RGB_COLOURSPACE_ACES2065_1,
104 RGB_Colourspace,
105 RGB_to_XYZ,
106 XYZ_to_RGB,
107)
108from colour.temperature import CCT_to_xy_CIE_D
109from colour.utilities import (
110 CanonicalMapping,
111 as_float,
112 as_float_array,
113 as_float_scalar,
114 from_range_1,
115 optional,
116 required,
117 runtime_warning,
118 tsplit,
119 zeros,
120)
121from colour.utilities.deprecation import handle_arguments_deprecation
123__author__ = "Colour Developers"
124__copyright__ = "Copyright 2013 Colour Developers"
125__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
126__maintainer__ = "Colour Developers"
127__email__ = "colour-developers@colour-science.org"
128__status__ = "Production"
130__all__ = [
131 "FLARE_PERCENTAGE",
132 "S_FLARE_FACTOR",
133 "sd_to_aces_relative_exposure_values",
134 "sd_to_ACES2065_1",
135 "SPECTRAL_SHAPE_RAWTOACES",
136 "ROOT_RESOURCES_RAWTOACES",
137 "read_training_data_rawtoaces_v1",
138 "generate_illuminants_rawtoaces_v1",
139 "white_balance_multipliers",
140 "best_illuminant",
141 "normalise_illuminant",
142 "training_data_sds_to_RGB",
143 "training_data_sds_to_XYZ",
144 "whitepoint_preserving_matrix",
145 "optimisation_factory_rawtoaces_v1",
146 "optimisation_factory_Jzazbz",
147 "optimisation_factory_Oklab_15",
148 "matrix_idt",
149 "camera_RGB_to_ACES2065_1",
150]
152FLARE_PERCENTAGE: float = 0.00500
153"""Flare percentage in the *ACES* system."""
155S_FLARE_FACTOR: float = 0.18000 / (0.18000 + FLARE_PERCENTAGE)
156"""Flare modulation factor in the *ACES* system."""
159def sd_to_aces_relative_exposure_values(
160 sd: SpectralDistribution,
161 illuminant: SpectralDistribution | None = None,
162 chromatic_adaptation_transform: (
163 LiteralChromaticAdaptationTransform | str | None
164 ) = "CAT02",
165 **kwargs: Any,
166) -> Range1:
167 """
168 Convert spectral distribution to *ACES2065-1* colourspace relative
169 exposure values.
171 Parameters
172 ----------
173 sd
174 Spectral distribution.
175 illuminant
176 *Illuminant* spectral distribution, default to
177 *CIE Standard Illuminant D65*.
178 chromatic_adaptation_transform
179 *Chromatic adaptation* transform.
181 Returns
182 -------
183 :class:`numpy.ndarray`
184 *ACES2065-1* colourspace relative exposure values array.
186 Notes
187 -----
188 +------------+-----------------------+---------------+
189 | **Range** | **Scale - Reference** | **Scale - 1** |
190 +============+=======================+===============+
191 | ``XYZ`` | 1 | 1 |
192 +------------+-----------------------+---------------+
194 - The chromatic adaptation method implemented here is a bit unusual
195 as it involves building a new colourspace based on *ACES2065-1*
196 colourspace primaries but using the whitepoint of the illuminant
197 that the spectral distribution was measured under.
199 References
200 ----------
201 :cite:`Forsythe2018`,
202 :cite:`TheAcademyofMotionPictureArtsandSciences2014q`,
203 :cite:`TheAcademyofMotionPictureArtsandSciences2014r`,
204 :cite:`TheAcademyofMotionPictureArtsandSciencese`
206 Examples
207 --------
208 >>> from colour import SDS_COLOURCHECKERS
209 >>> sd = SDS_COLOURCHECKERS["ColorChecker N Ohta"]["dark skin"]
210 >>> sd_to_aces_relative_exposure_values(
211 ... sd, chromatic_adaptation_transform=None
212 ... ) # doctest: +ELLIPSIS
213 array([ 0.1171814..., 0.0866360..., 0.0589726...])
214 >>> sd_to_aces_relative_exposure_values(sd, apply_chromatic_adaptation=True)
215 ... # doctest: +ELLIPSIS
216 array([ 0.1180779..., 0.0869031..., 0.0589125...])
217 """
219 if isinstance(chromatic_adaptation_transform, bool): # pragma: no cover
220 if chromatic_adaptation_transform is True:
221 chromatic_adaptation_transform = "CAT02"
222 elif chromatic_adaptation_transform is False:
223 chromatic_adaptation_transform = None
225 kwargs = {"apply_chromatic_adaptation": True}
227 handle_arguments_deprecation(
228 {
229 "ArgumentRemoved": ["apply_chromatic_adaptation"],
230 },
231 **kwargs,
232 )
234 illuminant = optional(illuminant, SDS_ILLUMINANTS["D65"])
236 shape = MSDS_ACES_RICD.shape
237 if sd.shape != MSDS_ACES_RICD.shape:
238 sd = reshape_sd(sd, shape, copy=False)
240 if illuminant.shape != MSDS_ACES_RICD.shape:
241 illuminant = reshape_sd(illuminant, shape, copy=False)
243 s_v = sd.values
244 i_v = illuminant.values
246 r_bar, g_bar, b_bar = tsplit(MSDS_ACES_RICD.values)
248 def k(x: NDArrayFloat, y: NDArrayFloat) -> float:
249 """Compute the :math:`K_r`, :math:`K_g` or :math:`K_b` scale factors."""
251 return as_float_scalar(1 / np.sum(x * y))
253 k_r = k(i_v, r_bar)
254 k_g = k(i_v, g_bar)
255 k_b = k(i_v, b_bar)
257 E_r = k_r * np.sum(i_v * s_v * r_bar)
258 E_g = k_g * np.sum(i_v * s_v * g_bar)
259 E_b = k_b * np.sum(i_v * s_v * b_bar)
261 E_rgb = np.array([E_r, E_g, E_b])
263 # Accounting for flare.
264 E_rgb += FLARE_PERCENTAGE
265 E_rgb *= S_FLARE_FACTOR
267 if chromatic_adaptation_transform is not None:
268 XYZ = RGB_to_XYZ(
269 E_rgb,
270 RGB_Colourspace(
271 "~ACES2065-1",
272 RGB_COLOURSPACE_ACES2065_1.primaries,
273 XYZ_to_xy(sd_to_XYZ(illuminant) / 100),
274 illuminant.name,
275 ),
276 RGB_COLOURSPACE_ACES2065_1.whitepoint,
277 chromatic_adaptation_transform,
278 )
279 E_rgb = XYZ_to_RGB(XYZ, RGB_COLOURSPACE_ACES2065_1)
281 return from_range_1(E_rgb)
284sd_to_ACES2065_1 = sd_to_aces_relative_exposure_values
286SPECTRAL_SHAPE_RAWTOACES: SpectralShape = SpectralShape(380, 780, 5)
287"""Default spectral shape according to *RAW to ACES* v1."""
289ROOT_RESOURCES_RAWTOACES: str = os.path.join(
290 os.path.dirname(__file__), "datasets", "rawtoaces"
291)
292"""
293*RAW to ACES* resources directory.
295Notes
296-----
297- *Colour* only ships a minimal dataset from *RAW to ACES*, please see
298 `Colour - Datasets <https://github.com/colour-science/colour-datasets>`_
299 for the complete *RAW to ACES* v1 dataset, i.e., *3372171*.
300"""
302_TRAINING_DATA_RAWTOACES_V1: MultiSpectralDistributions | None = None
305def read_training_data_rawtoaces_v1() -> MultiSpectralDistributions:
306 """
307 Read the *RAW to ACES* v1 training data comprising 190 reflectance
308 patches.
310 Returns
311 -------
312 :class:`colour.MultiSpectralDistributions`
313 *RAW to ACES* v1 190 patches multi-spectral distributions.
315 References
316 ----------
317 :cite:`Dyer2017`
319 Examples
320 --------
321 >>> len(read_training_data_rawtoaces_v1().labels)
322 190
323 """
325 global _TRAINING_DATA_RAWTOACES_V1 # noqa: PLW0603
327 if _TRAINING_DATA_RAWTOACES_V1 is not None:
328 training_data = _TRAINING_DATA_RAWTOACES_V1
329 else:
330 path = os.path.join(ROOT_RESOURCES_RAWTOACES, "190_Patches.csv")
331 training_data = sds_and_msds_to_msds(
332 list(read_sds_from_csv_file(path).values())
333 )
335 _TRAINING_DATA_RAWTOACES_V1 = training_data
337 return training_data
340_ILLUMINANTS_RAWTOACES_V1: CanonicalMapping | None = None
343def generate_illuminants_rawtoaces_v1() -> CanonicalMapping:
344 """
345 Generate a series of illuminants according to *RAW to ACES* v1:
347 - *CIE Illuminant D Series* in range [4000, 25000] kelvin degrees.
348 - *Blackbodies* in range [1000, 3500] kelvin degrees.
349 - A.M.P.A.S. variant of *ISO 7589 Studio Tungsten*.
351 Returns
352 -------
353 :class:`colour.utilities.CanonicalMapping`
354 Series of illuminants.
356 Notes
357 -----
358 - This definition introduces a few differences compared to
359 *RAW to ACES* v1: *CIE Illuminant D Series* are computed in range
360 [4002.15, 7003.77] kelvin degrees and the :math:`C_2` change is not
361 used in *RAW to ACES* v1.
363 References
364 ----------
365 :cite:`Dyer2017`
367 Examples
368 --------
369 >>> list(sorted(generate_illuminants_rawtoaces_v1().keys()))
370 ['1000K Blackbody', '1500K Blackbody', '2000K Blackbody', \
371'2500K Blackbody', '3000K Blackbody', '3500K Blackbody', 'D100', 'D105', \
372'D110', 'D115', 'D120', 'D125', 'D130', 'D135', 'D140', 'D145', 'D150', \
373'D155', 'D160', 'D165', 'D170', 'D175', 'D180', 'D185', 'D190', 'D195', \
374'D200', 'D205', 'D210', 'D215', 'D220', 'D225', 'D230', 'D235', 'D240', \
375'D245', 'D250', 'D40', 'D45', 'D50', 'D55', 'D60', 'D65', 'D70', 'D75', \
376'D80', 'D85', 'D90', 'D95', 'iso7589']
377 """
379 global _ILLUMINANTS_RAWTOACES_V1 # noqa: PLW0603
381 if _ILLUMINANTS_RAWTOACES_V1 is not None:
382 illuminants = _ILLUMINANTS_RAWTOACES_V1
383 else:
384 illuminants = CanonicalMapping()
386 # CIE Illuminants D Series from 4000K to 25000K.
387 for i in np.arange(4000, 25000 + 500, 500):
388 CCT = i * 1.4388 / 1.4380
389 xy = CCT_to_xy_CIE_D(CCT)
390 sd = sd_CIE_illuminant_D_series(xy)
391 sd.name = f"D{int(CCT / 100):d}"
392 illuminants[sd.name] = sd.align(SPECTRAL_SHAPE_RAWTOACES)
394 # Blackbody from 1000K to 4000K.
395 for i in np.arange(1000, 4000, 500):
396 sd = sd_blackbody(cast("float", i), SPECTRAL_SHAPE_RAWTOACES)
397 illuminants[sd.name] = sd
399 # A.M.P.A.S. variant of ISO 7589 Studio Tungsten.
400 sd = read_sds_from_csv_file(
401 os.path.join(ROOT_RESOURCES_RAWTOACES, "AMPAS_ISO_7589_Tungsten.csv")
402 )["iso7589"]
403 illuminants.update({sd.name: sd})
405 _ILLUMINANTS_RAWTOACES_V1 = illuminants
407 return illuminants
410def white_balance_multipliers(
411 sensitivities: RGB_CameraSensitivities, illuminant: SpectralDistribution
412) -> NDArrayFloat:
413 """
414 Compute *RGB* white balance multipliers for camera *RGB* spectral
415 sensitivities and the specified illuminant spectral distribution.
417 Parameters
418 ----------
419 sensitivities
420 Camera *RGB* spectral sensitivities.
421 illuminant
422 Illuminant spectral distribution.
424 Returns
425 -------
426 :class:`numpy.ndarray`
427 *RGB* white balance multipliers.
429 References
430 ----------
431 :cite:`Dyer2017`
433 Examples
434 --------
435 >>> path = os.path.join(
436 ... ROOT_RESOURCES_RAWTOACES,
437 ... "CANON_EOS_5DMark_II_RGB_Sensitivities.csv",
438 ... )
439 >>> sensitivities = sds_and_msds_to_msds(read_sds_from_csv_file(path).values())
440 >>> illuminant = SDS_ILLUMINANTS["D55"]
441 >>> white_balance_multipliers(sensitivities, illuminant)
442 ... # doctest: +ELLIPSIS
443 array([ 2.3414154..., 1. , 1.5163375...])
444 """
446 shape = sensitivities.shape
447 if illuminant.shape != shape:
448 runtime_warning(f'Aligning "{illuminant.name}" illuminant shape to "{shape}".')
449 illuminant = reshape_sd(illuminant, shape, copy=False)
451 RGB_w = 1 / np.sum(sensitivities.values * illuminant.values[..., None], axis=0)
452 RGB_w *= 1 / np.min(RGB_w)
454 return RGB_w
457def best_illuminant(
458 RGB_w: ArrayLike,
459 sensitivities: RGB_CameraSensitivities,
460 illuminants: Mapping,
461) -> SpectralDistribution:
462 """
463 Select the best illuminant for the specified *RGB* white balance
464 multipliers from a series of candidate illuminants based on camera
465 sensitivities.
467 The best illuminant is determined by finding the illuminant that produces
468 white balance multipliers closest to the specified values, minimizing the
469 sum of squared errors after normalization.
471 Parameters
472 ----------
473 RGB_w
474 *RGB* white balance multipliers.
475 sensitivities
476 Camera *RGB* spectral sensitivities.
477 illuminants
478 Illuminant spectral distributions to choose the best illuminant from.
480 Returns
481 -------
482 :class:`colour.SpectralDistribution`
483 Best illuminant spectral distribution.
485 Examples
486 --------
487 >>> path = os.path.join(
488 ... ROOT_RESOURCES_RAWTOACES,
489 ... "CANON_EOS_5DMark_II_RGB_Sensitivities.csv",
490 ... )
491 >>> sensitivities = sds_and_msds_to_msds(read_sds_from_csv_file(path).values())
492 >>> illuminants = generate_illuminants_rawtoaces_v1()
493 >>> RGB_w = white_balance_multipliers(sensitivities, SDS_ILLUMINANTS["FL2"])
494 >>> best_illuminant(RGB_w, sensitivities, illuminants).name
495 'D40'
496 """
498 RGB_w = as_float_array(RGB_w)
500 sse = np.inf
501 illuminant_b = None
502 for illuminant in illuminants.values():
503 RGB_wi = white_balance_multipliers(sensitivities, illuminant)
504 sse_c = np.sum((RGB_wi / RGB_w - 1) ** 2)
505 if sse_c < sse:
506 sse = sse_c
507 illuminant_b = illuminant
509 return cast("SpectralDistribution", illuminant_b)
512def normalise_illuminant(
513 illuminant: SpectralDistribution, sensitivities: RGB_CameraSensitivities
514) -> SpectralDistribution:
515 """
516 Normalise the specified illuminant with camera *RGB* spectral
517 sensitivities.
519 The multiplicative inverse scaling factor :math:`k` is computed by
520 multiplying the illuminant by the sensitivities channel with the maximum
521 value.
523 Parameters
524 ----------
525 illuminant
526 Illuminant spectral distribution.
527 sensitivities
528 Camera *RGB* spectral sensitivities.
530 Returns
531 -------
532 :class:`colour.SpectralDistribution`
533 Normalised illuminant.
535 Examples
536 --------
537 >>> path = os.path.join(
538 ... ROOT_RESOURCES_RAWTOACES,
539 ... "CANON_EOS_5DMark_II_RGB_Sensitivities.csv",
540 ... )
541 >>> sensitivities = sds_and_msds_to_msds(read_sds_from_csv_file(path).values())
542 >>> illuminant = SDS_ILLUMINANTS["D55"]
543 >>> np.sum(illuminant.values) # doctest: +ELLIPSIS
544 7276.1490000...
545 >>> np.sum(normalise_illuminant(illuminant, sensitivities).values)
546 ... # doctest: +ELLIPSIS
547 3.4390373...
548 """
550 shape = sensitivities.shape
551 if illuminant.shape != shape:
552 runtime_warning(f'Aligning "{illuminant.name}" illuminant shape to "{shape}".')
553 illuminant = reshape_sd(illuminant, shape)
555 c_i = np.argmax(np.max(sensitivities.values, axis=0))
556 k = 1 / np.sum(illuminant.values * sensitivities.values[..., c_i])
558 return illuminant * k
561def training_data_sds_to_RGB(
562 training_data: MultiSpectralDistributions,
563 sensitivities: RGB_CameraSensitivities,
564 illuminant: SpectralDistribution,
565) -> Tuple[NDArrayFloat, NDArrayFloat]:
566 """
567 Convert training data to *RGB* tristimulus values using the specified
568 illuminant and camera *RGB* spectral sensitivities.
570 Parameters
571 ----------
572 training_data
573 Training data multi-spectral distributions.
574 sensitivities
575 Camera *RGB* spectral sensitivities.
576 illuminant
577 Illuminant spectral distribution.
579 Returns
580 -------
581 :class:`tuple`
582 Tuple of training data *RGB* tristimulus values and white balance
583 multipliers.
585 Examples
586 --------
587 >>> path = os.path.join(
588 ... ROOT_RESOURCES_RAWTOACES,
589 ... "CANON_EOS_5DMark_II_RGB_Sensitivities.csv",
590 ... )
591 >>> sensitivities = sds_and_msds_to_msds(read_sds_from_csv_file(path).values())
592 >>> illuminant = normalise_illuminant(SDS_ILLUMINANTS["D55"], sensitivities)
593 >>> training_data = read_training_data_rawtoaces_v1()
594 >>> RGB, RGB_w = training_data_sds_to_RGB(training_data, sensitivities, illuminant)
595 >>> RGB[:5] # doctest: +ELLIPSIS
596 array([[ 0.0207582..., 0.0196857..., 0.0213935...],
597 [ 0.0895775..., 0.0891922..., 0.0891091...],
598 [ 0.7810230..., 0.7801938..., 0.7764302...],
599 [ 0.1995 ..., 0.1995 ..., 0.1995 ...],
600 [ 0.5898478..., 0.5904015..., 0.5851076...]])
601 >>> RGB_w # doctest: +ELLIPSIS
602 array([ 2.3414154..., 1. , 1.5163375...])
603 """
605 shape = sensitivities.shape
606 if illuminant.shape != shape:
607 runtime_warning(f'Aligning "{illuminant.name}" illuminant shape to "{shape}".')
608 illuminant = reshape_sd(illuminant, shape, copy=False)
610 if training_data.shape != shape:
611 runtime_warning(
612 f'Aligning "{training_data.name}" training data shape to "{shape}".'
613 )
614 training_data = reshape_msds(training_data, shape, copy=False)
616 RGB_w = white_balance_multipliers(sensitivities, illuminant)
618 RGB = np.dot(
619 np.transpose(illuminant.values[..., None] * training_data.values),
620 sensitivities.values,
621 )
623 RGB *= RGB_w
625 return RGB, RGB_w
628def training_data_sds_to_XYZ(
629 training_data: MultiSpectralDistributions,
630 cmfs: MultiSpectralDistributions,
631 illuminant: SpectralDistribution,
632 chromatic_adaptation_transform: (
633 LiteralChromaticAdaptationTransform | str | None
634 ) = "CAT02",
635) -> Range1:
636 """
637 Convert training data to *CIE XYZ* tristimulus values using the specified
638 illuminant and standard observer colour matching functions.
640 Parameters
641 ----------
642 training_data
643 Training data multi-spectral distributions.
644 cmfs
645 Standard observer colour matching functions.
646 illuminant
647 Illuminant spectral distribution.
648 chromatic_adaptation_transform
649 *Chromatic adaptation* transform, if *None* no chromatic adaptation
650 is performed.
652 Returns
653 -------
654 :class:`numpy.ndarray`
655 Training data *CIE XYZ* tristimulus values.
657 Notes
658 -----
659 +------------+-----------------------+---------------+
660 | **Range** | **Scale - Reference** | **Scale - 1** |
661 +============+=======================+===============+
662 | ``XYZ`` | 1 | 1 |
663 +------------+-----------------------+---------------+
665 Examples
666 --------
667 >>> from colour import MSDS_CMFS
668 >>> path = os.path.join(
669 ... ROOT_RESOURCES_RAWTOACES,
670 ... "CANON_EOS_5DMark_II_RGB_Sensitivities.csv",
671 ... )
672 >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]
673 >>> sensitivities = sds_and_msds_to_msds(read_sds_from_csv_file(path).values())
674 >>> illuminant = normalise_illuminant(SDS_ILLUMINANTS["D55"], sensitivities)
675 >>> training_data = read_training_data_rawtoaces_v1()
676 >>> training_data_sds_to_XYZ(training_data, cmfs, illuminant)[:5]
677 ... # doctest: +ELLIPSIS
678 array([[ 0.0174353..., 0.0179504..., 0.0196109...],
679 [ 0.0855607..., 0.0895735..., 0.0901703...],
680 [ 0.7455880..., 0.7817549..., 0.7834356...],
681 [ 0.1900528..., 0.1995 ..., 0.2012606...],
682 [ 0.5626319..., 0.5914544..., 0.5894500...]])
683 """
685 shape = cmfs.shape
686 if illuminant.shape != shape:
687 runtime_warning(f'Aligning "{illuminant.name}" illuminant shape to "{shape}".')
688 illuminant = reshape_sd(illuminant, shape, copy=False)
690 if training_data.shape != shape:
691 runtime_warning(
692 f'Aligning "{training_data.name}" training data shape to "{shape}".'
693 )
694 training_data = reshape_msds(training_data, shape, copy=False)
696 XYZ = np.dot(
697 np.transpose(illuminant.values[..., None] * training_data.values),
698 cmfs.values,
699 )
701 XYZ *= 1 / np.sum(cmfs.values[..., 1] * illuminant.values)
703 XYZ_w = np.dot(np.transpose(cmfs.values), illuminant.values)
704 XYZ_w *= 1 / XYZ_w[1]
706 if chromatic_adaptation_transform is not None:
707 M_CAT = matrix_chromatic_adaptation_VonKries(
708 XYZ_w,
709 xy_to_XYZ(RGB_COLOURSPACE_ACES2065_1.whitepoint),
710 chromatic_adaptation_transform,
711 )
713 XYZ = vecmul(M_CAT, XYZ)
715 return XYZ
718def whitepoint_preserving_matrix(
719 M: ArrayLike, RGB_w: ArrayLike = (1, 1, 1)
720) -> NDArrayFloat:
721 """
722 Normalise the specified matrix :math:`M` to preserve the white point
723 :math:`RGB_w`.
725 Parameters
726 ----------
727 M
728 Matrix :math:`M` to normalise.
729 RGB_w
730 White point :math:`RGB_w` to normalise the matrix :math:`M` with.
732 Returns
733 -------
734 :class:`numpy.ndarray`
735 Normalised matrix :math:`M`.
737 Examples
738 --------
739 >>> M = np.reshape(np.arange(9), (3, 3))
740 >>> whitepoint_preserving_matrix(M)
741 array([[ 0., 1., 0.],
742 [ 3., 4., -6.],
743 [ 6., 7., -12.]])
744 """
746 M = as_float_array(M)
747 RGB_w = as_float_array(RGB_w)
749 M[..., -1] = RGB_w - np.sum(M[..., :-1], axis=-1)
751 return M
754def optimisation_factory_rawtoaces_v1() -> Tuple[
755 NDArrayFloat, Callable, Callable, Callable
756]:
757 """
758 Generate the objective function and *CIE XYZ* colourspace to optimisation
759 colourspace/colour model function based according to *RAW to ACES* v1.
761 The objective function computes the Euclidean distance between the
762 training data *RGB* tristimulus values and the training data *CIE XYZ*
763 tristimulus values in the *CIE L\\*a\\*b\\** colourspace.
765 Implement whitepoint preservation as an optimisation constraint.
767 Returns
768 -------
769 :class:`tuple`
770 :math:`x_0` initial values, objective function, *CIE XYZ* colourspace
771 to *CIE L\\*a\\*b\\** colourspace function and finaliser function.
773 Examples
774 --------
775 >>> optimisation_factory_rawtoaces_v1() # doctest: +SKIP
776 (array([ 1., 0., 0., 1., 0., 0.]), \
777<function optimisation_factory_rawtoaces_v1.<locals> \
778.objective_function at 0x...>, \
779<function optimisation_factory_rawtoaces_v1.<locals>\
780.XYZ_to_optimization_colour_model at 0x...>, \
781<function optimisation_factory_rawtoaces_v1.<locals>\
782.finaliser_function at 0x...>)
783 """
785 x_0 = as_float_array([1, 0, 0, 1, 0, 0])
787 def objective_function(
788 M: NDArrayFloat, RGB: NDArrayFloat, Lab: NDArrayFloat
789 ) -> DTypeFloat:
790 """Objective function according to *RAW to ACES* v1."""
792 M = finaliser_function(M)
794 XYZ_t = vecmul(RGB_COLOURSPACE_ACES2065_1.matrix_RGB_to_XYZ, vecmul(M, RGB))
795 Lab_t = XYZ_to_optimization_colour_model(XYZ_t)
797 return as_float(np.linalg.norm(Lab_t - Lab))
799 def XYZ_to_optimization_colour_model(XYZ: ArrayLike) -> NDArrayFloat:
800 """*CIE XYZ* colourspace to *CIE L\\*a\\*b\\** colourspace function."""
802 return XYZ_to_Lab(XYZ, RGB_COLOURSPACE_ACES2065_1.whitepoint)
804 def finaliser_function(M: ArrayLike) -> NDArrayFloat:
805 """Finaliser function."""
807 return whitepoint_preserving_matrix(
808 np.hstack([np.reshape(M, (3, 2)), zeros((3, 1))])
809 )
811 return (
812 x_0,
813 objective_function,
814 XYZ_to_optimization_colour_model,
815 finaliser_function,
816 )
819def optimisation_factory_Jzazbz() -> Tuple[NDArrayFloat, Callable, Callable, Callable]:
820 """
821 Generate the objective function and *CIE XYZ* colourspace to optimisation
822 colourspace/colour model function based on the :math:`J_za_zb_z`
823 colourspace.
825 The objective function computes the Euclidean distance between the
826 training data *RGB* tristimulus values and the training data *CIE XYZ*
827 tristimulus values in the :math:`J_za_zb_z` colourspace.
829 Implement whitepoint preservation as a post-optimisation step.
831 Returns
832 -------
833 :class:`tuple`
834 :math:`x_0` initial values, objective function, *CIE XYZ* colourspace
835 to :math:`J_za_zb_z` colourspace function and finaliser function.
837 Examples
838 --------
839 >>> optimisation_factory_Jzazbz() # doctest: +SKIP
840 (array([ 1., 0., 0., 1., 0., 0.]), \
841<function optimisation_factory_Jzazbz.<locals>\
842.objective_function at 0x...>, \
843<function optimisation_factory_Jzazbz.<locals>\
844.XYZ_to_optimization_colour_model at 0x...>, \
845<function optimisation_factory_Jzazbz.<locals>.\
846finaliser_function at 0x...>)
847 """
849 x_0 = as_float_array([1, 0, 0, 1, 0, 0])
851 def objective_function(M: ArrayLike, RGB: ArrayLike, Jab: ArrayLike) -> DTypeFloat:
852 """:math:`J_za_zb_z` colourspace based objective function."""
854 M = finaliser_function(M)
856 XYZ_t = vecmul(RGB_COLOURSPACE_ACES2065_1.matrix_RGB_to_XYZ, vecmul(M, RGB))
857 Jab_t = XYZ_to_optimization_colour_model(XYZ_t)
859 return as_float(np.sum(euclidean_distance(Jab, Jab_t)))
861 def XYZ_to_optimization_colour_model(XYZ: ArrayLike) -> NDArrayFloat:
862 """*CIE XYZ* colourspace to :math:`J_za_zb_z` colourspace function."""
864 return XYZ_to_Jzazbz(XYZ)
866 def finaliser_function(M: ArrayLike) -> NDArrayFloat:
867 """Finaliser function."""
869 return whitepoint_preserving_matrix(
870 np.hstack([np.reshape(M, (3, 2)), zeros((3, 1))])
871 )
873 return (
874 x_0,
875 objective_function,
876 XYZ_to_optimization_colour_model,
877 finaliser_function,
878 )
881def optimisation_factory_Oklab_15() -> Tuple[
882 NDArrayFloat, Callable, Callable, Callable
883]:
884 """
885 Generate the objective function and *CIE XYZ* colourspace to optimisation
886 colourspace/colour model function based on the *Oklab* colourspace.
888 The objective function computes the Euclidean distance between the
889 training data *RGB* tristimulus values and the training data *CIE XYZ*
890 tristimulus values in the *Oklab* colourspace.
892 Implement support for *Finlayson et al. (2015)* root-polynomials of
893 degree 2 and produce 15 terms.
895 Returns
896 -------
897 :class:`tuple`
898 :math:`x_0` initial values, objective function, *CIE XYZ* colourspace
899 to *Oklab* colourspace function and finaliser function.
901 References
902 ----------
903 :cite:`Finlayson2015`
905 Examples
906 --------
907 >>> optimisation_factory_Oklab_15() # doctest: +SKIP
908 (array([ 1., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., \
9090., 1.]), \
910<function optimisation_factory_Oklab_15.<locals>\
911.objective_function at 0x...>, \
912<function optimisation_factory_Oklab_15.<locals>\
913.XYZ_to_optimization_colour_model at 0x...>, \
914<function optimisation_factory_Oklab_15.<locals>.\
915finaliser_function at 0x...>)
916 """
918 x_0 = as_float_array([1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1])
920 def objective_function(M: ArrayLike, RGB: ArrayLike, Jab: ArrayLike) -> DTypeFloat:
921 """*Oklab* colourspace based objective function."""
923 M = finaliser_function(M)
925 XYZ_t = np.transpose(
926 np.dot(
927 RGB_COLOURSPACE_ACES2065_1.matrix_RGB_to_XYZ,
928 np.dot(
929 M,
930 np.transpose(polynomial_expansion_Finlayson2015(RGB, 2, True)),
931 ),
932 )
933 )
935 Jab_t = XYZ_to_optimization_colour_model(XYZ_t)
937 return as_float(np.sum(euclidean_distance(Jab, Jab_t)))
939 def XYZ_to_optimization_colour_model(XYZ: ArrayLike) -> NDArrayFloat:
940 """*CIE XYZ* colourspace to *Oklab* colourspace function."""
942 return XYZ_to_Oklab(XYZ)
944 def finaliser_function(M: ArrayLike) -> NDArrayFloat:
945 """Finaliser function."""
947 return whitepoint_preserving_matrix(
948 np.hstack([np.reshape(M, (3, 5)), zeros((3, 1))])
949 )
951 return (
952 x_0,
953 objective_function,
954 XYZ_to_optimization_colour_model,
955 finaliser_function,
956 )
959@typing.overload
960def matrix_idt(
961 sensitivities: RGB_CameraSensitivities,
962 illuminant: SpectralDistribution,
963 training_data: MultiSpectralDistributions | None = ...,
964 cmfs: MultiSpectralDistributions | None = ...,
965 optimisation_factory: Callable = ...,
966 optimisation_kwargs: dict | None = ...,
967 chromatic_adaptation_transform: (
968 LiteralChromaticAdaptationTransform | str | None
969 ) = ...,
970 additional_data: Literal[True] = True,
971) -> Tuple[NDArrayFloat, NDArrayFloat, NDArrayFloat, NDArrayFloat]: ...
974@typing.overload
975def matrix_idt(
976 sensitivities: ...,
977 illuminant: ...,
978 training_data: MultiSpectralDistributions | None = ...,
979 cmfs: MultiSpectralDistributions | None = ...,
980 optimisation_factory: Callable = ...,
981 optimisation_kwargs: dict | None = ...,
982 chromatic_adaptation_transform: (
983 LiteralChromaticAdaptationTransform | str | None
984 ) = ...,
985 *,
986 additional_data: Literal[False],
987) -> Tuple[NDArrayFloat, NDArrayFloat]: ...
990@typing.overload
991def matrix_idt(
992 sensitivities: RGB_CameraSensitivities,
993 illuminant: SpectralDistribution,
994 training_data: MultiSpectralDistributions | None,
995 cmfs: MultiSpectralDistributions | None,
996 optimisation_factory: Callable,
997 optimisation_kwargs: dict | None,
998 chromatic_adaptation_transform: (LiteralChromaticAdaptationTransform | str | None),
999 additional_data: Literal[False],
1000) -> Tuple[NDArrayFloat, NDArrayFloat, NDArrayFloat, NDArrayFloat]: ...
1003@required("SciPy")
1004def matrix_idt(
1005 sensitivities: RGB_CameraSensitivities,
1006 illuminant: SpectralDistribution,
1007 training_data: MultiSpectralDistributions | None = None,
1008 cmfs: MultiSpectralDistributions | None = None,
1009 optimisation_factory: Callable = optimisation_factory_rawtoaces_v1,
1010 optimisation_kwargs: dict | None = None,
1011 chromatic_adaptation_transform: (
1012 LiteralChromaticAdaptationTransform | str | None
1013 ) = "CAT02",
1014 additional_data: bool = False,
1015) -> (
1016 Tuple[NDArrayFloat, NDArrayFloat, NDArrayFloat, NDArrayFloat]
1017 | Tuple[NDArrayFloat, NDArrayFloat]
1018):
1019 """
1020 Compute an *Input Device Transform* (IDT) matrix for camera *RGB*
1021 spectral sensitivities, illuminant, training data, standard observer
1022 colour matching functions and optimisation settings according to
1023 *RAW to ACES* v1 and *P-2013-001* procedures.
1025 Parameters
1026 ----------
1027 sensitivities
1028 Camera *RGB* spectral sensitivities.
1029 illuminant
1030 Illuminant spectral distribution.
1031 training_data
1032 Training data multi-spectral distributions, defaults to using the
1033 *RAW to ACES* v1 190 patches.
1034 cmfs
1035 Standard observer colour matching functions, default to the
1036 *CIE 1931 2 Degree Standard Observer*.
1037 optimisation_factory
1038 Callable producing the objective function and the *CIE XYZ* to
1039 optimisation colour model function.
1040 optimisation_kwargs
1041 Parameters for :func:`scipy.optimize.minimize` definition.
1042 chromatic_adaptation_transform
1043 *Chromatic adaptation* transform, if *None* no chromatic adaptation
1044 is performed.
1045 additional_data
1046 If *True*, the *XYZ* and *RGB* tristimulus values are also
1047 returned.
1049 Returns
1050 -------
1051 :class:`tuple`
1052 Tuple of IDT matrix and white balance multipliers or tuple of IDT
1053 matrix, white balance multipliers, *XYZ* and *RGB* tristimulus
1054 values.
1056 References
1057 ----------
1058 :cite:`Dyer2017`, :cite:`TheAcademyofMotionPictureArtsandSciences2015c`
1060 Examples
1061 --------
1062 Computing the IDT matrix for a *CANON EOS 5DMark II* and
1063 *CIE Illuminant D Series* *D55* using the method specified in *RAW to ACES* v1:
1065 >>> path = os.path.join(
1066 ... ROOT_RESOURCES_RAWTOACES,
1067 ... "CANON_EOS_5DMark_II_RGB_Sensitivities.csv",
1068 ... )
1069 >>> sensitivities = sds_and_msds_to_msds(read_sds_from_csv_file(path).values())
1070 >>> illuminant = SDS_ILLUMINANTS["D55"]
1071 >>> M, RGB_w = matrix_idt(sensitivities, illuminant)
1072 >>> np.around(M, 3)
1073 array([[ 0.865, -0.026, 0.161],
1074 [ 0.057, 1.123, -0.18 ],
1075 [ 0.024, -0.203, 1.179]])
1076 >>> RGB_w # doctest: +ELLIPSIS
1077 array([ 2.3414154..., 1. , 1.5163375...])
1079 The *RAW to ACES* v1 matrix for the same camera and optimized by
1080 `Ceres Solver <http://ceres-solver.org>`__ is as follows::
1082 0.864994 -0.026302 0.161308
1083 0.056527 1.122997 -0.179524
1084 0.023683 -0.202547 1.178864
1086 >>> M, RGB_w = matrix_idt(
1087 ... sensitivities,
1088 ... illuminant,
1089 ... optimisation_factory=optimisation_factory_Jzazbz,
1090 ... )
1091 >>> np.around(M, 3)
1092 array([[ 0.852, -0.009, 0.158],
1093 [ 0.054, 1.122, -0.176],
1094 [ 0.023, -0.224, 1.2 ]])
1095 >>> RGB_w # doctest: +ELLIPSIS
1096 array([ 2.3414154..., 1. , 1.5163375...])
1098 >>> M, RGB_w = matrix_idt(
1099 ... sensitivities,
1100 ... illuminant,
1101 ... optimisation_factory=optimisation_factory_Oklab_15,
1102 ... )
1103 >>> np.around(M, 3)
1104 array([[ 0.645, -0.611, 0.107, 0.736, 0.398, -0.275],
1105 [-0.159, 0.728, -0.091, 0.651, 0.01 , -0.139],
1106 [-0.172, -0.403, 1.394, 0.51 , -0.295, -0.034]])
1107 >>> RGB_w # doctest: +ELLIPSIS
1108 array([ 2.3414154..., 1. , 1.5163375...])
1109 """
1111 from scipy.optimize import minimize # noqa: PLC0415
1113 training_data = optional(training_data, read_training_data_rawtoaces_v1())
1115 cmfs, illuminant = handle_spectral_arguments(
1116 cmfs, illuminant, shape_default=SPECTRAL_SHAPE_RAWTOACES
1117 )
1119 shape = cmfs.shape
1120 if sensitivities.shape != shape:
1121 runtime_warning(
1122 f'Aligning "{sensitivities.name}" sensitivities shape to "{shape}".'
1123 )
1124 sensitivities = reshape_msds(sensitivities, shape, copy=False)
1126 if training_data.shape != shape:
1127 runtime_warning(
1128 f'Aligning "{training_data.name}" training data shape to "{shape}".'
1129 )
1130 training_data = reshape_msds(training_data, shape, copy=False)
1132 illuminant = normalise_illuminant(illuminant, sensitivities)
1134 RGB, RGB_w = training_data_sds_to_RGB(training_data, sensitivities, illuminant)
1136 XYZ = training_data_sds_to_XYZ(
1137 training_data, cmfs, illuminant, chromatic_adaptation_transform
1138 )
1140 (
1141 x_0,
1142 objective_function,
1143 XYZ_to_optimization_colour_model,
1144 finaliser_function,
1145 ) = optimisation_factory()
1147 optimisation_settings: dict[str, Any] = {
1148 "method": "BFGS",
1149 "jac": "2-point",
1150 }
1151 if optimisation_kwargs is not None:
1152 optimisation_settings.update(optimisation_kwargs)
1154 M = minimize(
1155 objective_function,
1156 x_0,
1157 (RGB, XYZ_to_optimization_colour_model(XYZ)),
1158 **optimisation_settings,
1159 ).x
1161 M = finaliser_function(M)
1163 if additional_data:
1164 return M, RGB_w, XYZ, RGB
1166 return M, RGB_w
1169def camera_RGB_to_ACES2065_1(
1170 RGB: ArrayLike,
1171 B: ArrayLike,
1172 b: ArrayLike,
1173 k: ArrayLike = (1, 1, 1),
1174 clip: bool = False,
1175) -> NDArrayFloat:
1176 """
1177 Convert camera *RGB* colourspace array to *ACES2065-1* colourspace using
1178 the specified *Input Device Transform* (IDT) matrix :math:`B`, white
1179 balance multipliers :math:`b`, and exposure factor :math:`k` according to
1180 the *P-2013-001* procedure.
1182 Parameters
1183 ----------
1184 RGB
1185 Camera *RGB* colourspace array.
1186 B
1187 *Input Device Transform* (IDT) matrix :math:`B`.
1188 b
1189 White balance multipliers :math:`b`.
1190 k
1191 Exposure factor :math:`k` that results in a nominally "18% gray"
1192 object in the scene producing ACES values [0.18, 0.18, 0.18].
1193 clip
1194 Whether to clip the white balanced camera *RGB* colourspace array
1195 between :math:`-\\infty` and 1. The intent is to keep sensor
1196 saturated values achromatic after white balancing.
1198 Returns
1199 -------
1200 :class:`numpy.ndarray`
1201 *ACES2065-1* colourspace relative exposure values array.
1203 References
1204 ----------
1205 :cite:`TheAcademyofMotionPictureArtsandSciences2015c`
1207 Examples
1208 --------
1209 >>> path = os.path.join(
1210 ... ROOT_RESOURCES_RAWTOACES,
1211 ... "CANON_EOS_5DMark_II_RGB_Sensitivities.csv",
1212 ... )
1213 >>> sensitivities = sds_and_msds_to_msds(read_sds_from_csv_file(path).values())
1214 >>> illuminant = SDS_ILLUMINANTS["D55"]
1215 >>> B, b = matrix_idt(sensitivities, illuminant)
1216 >>> camera_RGB_to_ACES2065_1(np.array([0.1, 0.2, 0.3]), B, b)
1217 ... # doctest: +ELLIPSIS
1218 array([ 0.270644 ..., 0.1561487..., 0.5012965...])
1219 """
1221 RGB = as_float_array(RGB)
1222 B = as_float_array(B)
1223 b = as_float_array(b)
1224 k = as_float_array(k)
1226 RGB_r = b * RGB / np.min(b)
1228 RGB_r = np.clip(RGB_r, -np.inf, 1) if clip else RGB_r
1230 return k * vecmul(B, RGB_r)