Source code for colour.characterisation.aces_it

"""
Academy Color Encoding System - Input Transform
===============================================

Define the *Academy Color Encoding System* (ACES) *Input Transform* utilities
for camera RAW data processing and colour space transformations:

-   :func:`colour.sd_to_aces_relative_exposure_values`
-   :func:`colour.sd_to_ACES2065_1`
-   :func:`colour.characterisation.read_training_data_rawtoaces_v1`
-   :func:`colour.characterisation.generate_illuminants_rawtoaces_v1`
-   :func:`colour.characterisation.white_balance_multipliers`
-   :func:`colour.characterisation.best_illuminant`
-   :func:`colour.characterisation.normalise_illuminant`
-   :func:`colour.characterisation.training_data_sds_to_RGB`
-   :func:`colour.characterisation.training_data_sds_to_XYZ`
-   :func:`colour.characterisation.optimisation_factory_rawtoaces_v1`
-   :func:`colour.characterisation.optimisation_factory_Jzazbz`
-   :func:`colour.characterisation.optimisation_factory_Oklab_15`
-   :func:`colour.matrix_idt`
-   :func:`colour.camera_RGB_to_ACES2065_1`

References
----------
-   :cite:`Dyer2017` : Dyer, S., Forsythe, A., Irons, J., Mansencal, T., & Zhu,
    M. (2017). RAW to ACES (Version 1.0) [Computer software].
-   :cite:`Forsythe2018` : Borer, T. (2017). Private Discussion with Mansencal,
    T. and Shaw, N.
-   :cite:`Finlayson2015` : Finlayson, G. D., MacKiewicz, M., & Hurlbert, A.
    (2015). Color Correction Using Root-Polynomial Regression. IEEE
    Transactions on Image Processing, 24(5), 1460-1470.
    doi:10.1109/TIP.2015.2405336
-   :cite:`TheAcademyofMotionPictureArtsandSciences2014q` : The Academy of
    Motion Picture Arts and Sciences, Science and Technology Council, & Academy
    Color Encoding System (ACES) Project Subcommittee. (2014). Technical
    Bulletin TB-2014-004 - Informative Notes on SMPTE ST 2065-1 - Academy Color
    Encoding Specification (ACES) (pp. 1-40). Retrieved December 19, 2014, from
    http://j.mp/TB-2014-004
-   :cite:`TheAcademyofMotionPictureArtsandSciences2014r` : The Academy of
    Motion Picture Arts and Sciences, Science and Technology Council, & Academy
    Color Encoding System (ACES) Project Subcommittee. (2014). Technical
    Bulletin TB-2014-012 - Academy Color Encoding System Version 1.0 Component
    Names (pp. 1-8). Retrieved December 19, 2014, from http://j.mp/TB-2014-012
-   :cite:`TheAcademyofMotionPictureArtsandSciences2015c` : The Academy of
    Motion Picture Arts and Sciences, Science and Technology Council, & Academy
    Color Encoding System (ACES) Project Subcommittee. (2015). Procedure
    P-2013-001 - Recommended Procedures for the Creation and Use of Digital
    Camera System Input Device Transforms (IDTs) (pp. 1-29). Retrieved April
    24, 2015, from http://j.mp/P-2013-001
-   :cite:`TheAcademyofMotionPictureArtsandSciencese` : The Academy of Motion
    Picture Arts and Sciences, Science and Technology Council, & Academy Color
    Encoding System (ACES) Project Subcommittee. (n.d.). Academy Color Encoding
    System. Retrieved February 24, 2014, from
    http://www.oscars.org/science-technology/council/projects/aces.html
"""

from __future__ import annotations

import os
import typing

import numpy as np

from colour.adaptation import matrix_chromatic_adaptation_VonKries
from colour.algebra import euclidean_distance, vecmul
from colour.characterisation import (
    MSDS_ACES_RICD,
    RGB_CameraSensitivities,
    polynomial_expansion_Finlayson2015,
)
from colour.colorimetry import (
    SDS_ILLUMINANTS,
    MultiSpectralDistributions,
    SpectralDistribution,
    SpectralShape,
    handle_spectral_arguments,
    reshape_msds,
    reshape_sd,
    sd_blackbody,
    sd_CIE_illuminant_D_series,
    sd_to_XYZ,
    sds_and_msds_to_msds,
)

if typing.TYPE_CHECKING:
    from colour.hints import (
        Any,
        ArrayLike,
        Callable,
        DTypeFloat,
        Literal,
        LiteralChromaticAdaptationTransform,
        Mapping,
        NDArrayFloat,
        Range1,
        Tuple,
    )

from colour.hints import cast
from colour.io import read_sds_from_csv_file
from colour.models import XYZ_to_Jzazbz, XYZ_to_Lab, XYZ_to_Oklab, XYZ_to_xy, xy_to_XYZ
from colour.models.rgb import (
    RGB_COLOURSPACE_ACES2065_1,
    RGB_Colourspace,
    RGB_to_XYZ,
    XYZ_to_RGB,
)
from colour.temperature import CCT_to_xy_CIE_D
from colour.utilities import (
    CanonicalMapping,
    as_float,
    as_float_array,
    as_float_scalar,
    from_range_1,
    optional,
    required,
    runtime_warning,
    tsplit,
    zeros,
)
from colour.utilities.deprecation import handle_arguments_deprecation

__author__ = "Colour Developers"
__copyright__ = "Copyright 2013 Colour Developers"
__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
__maintainer__ = "Colour Developers"
__email__ = "colour-developers@colour-science.org"
__status__ = "Production"

__all__ = [
    "FLARE_PERCENTAGE",
    "S_FLARE_FACTOR",
    "sd_to_aces_relative_exposure_values",
    "sd_to_ACES2065_1",
    "SPECTRAL_SHAPE_RAWTOACES",
    "ROOT_RESOURCES_RAWTOACES",
    "read_training_data_rawtoaces_v1",
    "generate_illuminants_rawtoaces_v1",
    "white_balance_multipliers",
    "best_illuminant",
    "normalise_illuminant",
    "training_data_sds_to_RGB",
    "training_data_sds_to_XYZ",
    "whitepoint_preserving_matrix",
    "optimisation_factory_rawtoaces_v1",
    "optimisation_factory_Jzazbz",
    "optimisation_factory_Oklab_15",
    "matrix_idt",
    "camera_RGB_to_ACES2065_1",
]

FLARE_PERCENTAGE: float = 0.00500
"""Flare percentage in the *ACES* system."""

S_FLARE_FACTOR: float = 0.18000 / (0.18000 + FLARE_PERCENTAGE)
"""Flare modulation factor in the *ACES* system."""


[docs] def sd_to_aces_relative_exposure_values( sd: SpectralDistribution, illuminant: SpectralDistribution | None = None, chromatic_adaptation_transform: ( LiteralChromaticAdaptationTransform | str | None ) = "CAT02", **kwargs: Any, ) -> Range1: """ Convert spectral distribution to *ACES2065-1* colourspace relative exposure values. Parameters ---------- sd Spectral distribution. illuminant *Illuminant* spectral distribution, default to *CIE Standard Illuminant D65*. chromatic_adaptation_transform *Chromatic adaptation* transform. Returns ------- :class:`numpy.ndarray` *ACES2065-1* colourspace relative exposure values array. Notes ----- +------------+-----------------------+---------------+ | **Range** | **Scale - Reference** | **Scale - 1** | +============+=======================+===============+ | ``XYZ`` | 1 | 1 | +------------+-----------------------+---------------+ - The chromatic adaptation method implemented here is a bit unusual as it involves building a new colourspace based on *ACES2065-1* colourspace primaries but using the whitepoint of the illuminant that the spectral distribution was measured under. References ---------- :cite:`Forsythe2018`, :cite:`TheAcademyofMotionPictureArtsandSciences2014q`, :cite:`TheAcademyofMotionPictureArtsandSciences2014r`, :cite:`TheAcademyofMotionPictureArtsandSciencese` Examples -------- >>> from colour import SDS_COLOURCHECKERS >>> sd = SDS_COLOURCHECKERS["ColorChecker N Ohta"]["dark skin"] >>> sd_to_aces_relative_exposure_values( ... sd, chromatic_adaptation_transform=None ... ) # doctest: +ELLIPSIS array([ 0.1171814..., 0.0866360..., 0.0589726...]) >>> sd_to_aces_relative_exposure_values(sd, apply_chromatic_adaptation=True) ... # doctest: +ELLIPSIS array([ 0.1180779..., 0.0869031..., 0.0589125...]) """ if isinstance(chromatic_adaptation_transform, bool): # pragma: no cover if chromatic_adaptation_transform is True: chromatic_adaptation_transform = "CAT02" elif chromatic_adaptation_transform is False: chromatic_adaptation_transform = None kwargs = {"apply_chromatic_adaptation": True} handle_arguments_deprecation( { "ArgumentRemoved": ["apply_chromatic_adaptation"], }, **kwargs, ) illuminant = optional(illuminant, SDS_ILLUMINANTS["D65"]) shape = MSDS_ACES_RICD.shape if sd.shape != MSDS_ACES_RICD.shape: sd = reshape_sd(sd, shape, copy=False) if illuminant.shape != MSDS_ACES_RICD.shape: illuminant = reshape_sd(illuminant, shape, copy=False) s_v = sd.values i_v = illuminant.values r_bar, g_bar, b_bar = tsplit(MSDS_ACES_RICD.values) def k(x: NDArrayFloat, y: NDArrayFloat) -> float: """Compute the :math:`K_r`, :math:`K_g` or :math:`K_b` scale factors.""" return as_float_scalar(1 / np.sum(x * y)) k_r = k(i_v, r_bar) k_g = k(i_v, g_bar) k_b = k(i_v, b_bar) E_r = k_r * np.sum(i_v * s_v * r_bar) E_g = k_g * np.sum(i_v * s_v * g_bar) E_b = k_b * np.sum(i_v * s_v * b_bar) E_rgb = np.array([E_r, E_g, E_b]) # Accounting for flare. E_rgb += FLARE_PERCENTAGE E_rgb *= S_FLARE_FACTOR if chromatic_adaptation_transform is not None: XYZ = RGB_to_XYZ( E_rgb, RGB_Colourspace( "~ACES2065-1", RGB_COLOURSPACE_ACES2065_1.primaries, XYZ_to_xy(sd_to_XYZ(illuminant) / 100), illuminant.name, ), RGB_COLOURSPACE_ACES2065_1.whitepoint, chromatic_adaptation_transform, ) E_rgb = XYZ_to_RGB(XYZ, RGB_COLOURSPACE_ACES2065_1) return from_range_1(E_rgb)
sd_to_ACES2065_1 = sd_to_aces_relative_exposure_values SPECTRAL_SHAPE_RAWTOACES: SpectralShape = SpectralShape(380, 780, 5) """Default spectral shape according to *RAW to ACES* v1.""" ROOT_RESOURCES_RAWTOACES: str = os.path.join( os.path.dirname(__file__), "datasets", "rawtoaces" ) """ *RAW to ACES* resources directory. Notes ----- - *Colour* only ships a minimal dataset from *RAW to ACES*, please see `Colour - Datasets <https://github.com/colour-science/colour-datasets>`_ for the complete *RAW to ACES* v1 dataset, i.e., *3372171*. """ _TRAINING_DATA_RAWTOACES_V1: MultiSpectralDistributions | None = None
[docs] def read_training_data_rawtoaces_v1() -> MultiSpectralDistributions: """ Read the *RAW to ACES* v1 training data comprising 190 reflectance patches. Returns ------- :class:`colour.MultiSpectralDistributions` *RAW to ACES* v1 190 patches multi-spectral distributions. References ---------- :cite:`Dyer2017` Examples -------- >>> len(read_training_data_rawtoaces_v1().labels) 190 """ global _TRAINING_DATA_RAWTOACES_V1 # noqa: PLW0603 if _TRAINING_DATA_RAWTOACES_V1 is not None: training_data = _TRAINING_DATA_RAWTOACES_V1 else: path = os.path.join(ROOT_RESOURCES_RAWTOACES, "190_Patches.csv") training_data = sds_and_msds_to_msds( list(read_sds_from_csv_file(path).values()) ) _TRAINING_DATA_RAWTOACES_V1 = training_data return training_data
_ILLUMINANTS_RAWTOACES_V1: CanonicalMapping | None = None
[docs] def generate_illuminants_rawtoaces_v1() -> CanonicalMapping: """ Generate a series of illuminants according to *RAW to ACES* v1: - *CIE Illuminant D Series* in range [4000, 25000] kelvin degrees. - *Blackbodies* in range [1000, 3500] kelvin degrees. - A.M.P.A.S. variant of *ISO 7589 Studio Tungsten*. Returns ------- :class:`colour.utilities.CanonicalMapping` Series of illuminants. Notes ----- - This definition introduces a few differences compared to *RAW to ACES* v1: *CIE Illuminant D Series* are computed in range [4002.15, 7003.77] kelvin degrees and the :math:`C_2` change is not used in *RAW to ACES* v1. References ---------- :cite:`Dyer2017` Examples -------- >>> list(sorted(generate_illuminants_rawtoaces_v1().keys())) ['1000K Blackbody', '1500K Blackbody', '2000K Blackbody', \ '2500K Blackbody', '3000K Blackbody', '3500K Blackbody', 'D100', 'D105', \ 'D110', 'D115', 'D120', 'D125', 'D130', 'D135', 'D140', 'D145', 'D150', \ 'D155', 'D160', 'D165', 'D170', 'D175', 'D180', 'D185', 'D190', 'D195', \ 'D200', 'D205', 'D210', 'D215', 'D220', 'D225', 'D230', 'D235', 'D240', \ 'D245', 'D250', 'D40', 'D45', 'D50', 'D55', 'D60', 'D65', 'D70', 'D75', \ 'D80', 'D85', 'D90', 'D95', 'iso7589'] """ global _ILLUMINANTS_RAWTOACES_V1 # noqa: PLW0603 if _ILLUMINANTS_RAWTOACES_V1 is not None: illuminants = _ILLUMINANTS_RAWTOACES_V1 else: illuminants = CanonicalMapping() # CIE Illuminants D Series from 4000K to 25000K. for i in np.arange(4000, 25000 + 500, 500): CCT = i * 1.4388 / 1.4380 xy = CCT_to_xy_CIE_D(CCT) sd = sd_CIE_illuminant_D_series(xy) sd.name = f"D{int(CCT / 100):d}" illuminants[sd.name] = sd.align(SPECTRAL_SHAPE_RAWTOACES) # Blackbody from 1000K to 4000K. for i in np.arange(1000, 4000, 500): sd = sd_blackbody(cast("float", i), SPECTRAL_SHAPE_RAWTOACES) illuminants[sd.name] = sd # A.M.P.A.S. variant of ISO 7589 Studio Tungsten. sd = read_sds_from_csv_file( os.path.join(ROOT_RESOURCES_RAWTOACES, "AMPAS_ISO_7589_Tungsten.csv") )["iso7589"] illuminants.update({sd.name: sd}) _ILLUMINANTS_RAWTOACES_V1 = illuminants return illuminants
[docs] def white_balance_multipliers( sensitivities: RGB_CameraSensitivities, illuminant: SpectralDistribution ) -> NDArrayFloat: """ Compute *RGB* white balance multipliers for camera *RGB* spectral sensitivities and the specified illuminant spectral distribution. Parameters ---------- sensitivities Camera *RGB* spectral sensitivities. illuminant Illuminant spectral distribution. Returns ------- :class:`numpy.ndarray` *RGB* white balance multipliers. References ---------- :cite:`Dyer2017` Examples -------- >>> path = os.path.join( ... ROOT_RESOURCES_RAWTOACES, ... "CANON_EOS_5DMark_II_RGB_Sensitivities.csv", ... ) >>> sensitivities = sds_and_msds_to_msds(read_sds_from_csv_file(path).values()) >>> illuminant = SDS_ILLUMINANTS["D55"] >>> white_balance_multipliers(sensitivities, illuminant) ... # doctest: +ELLIPSIS array([ 2.3414154..., 1. , 1.5163375...]) """ shape = sensitivities.shape if illuminant.shape != shape: runtime_warning(f'Aligning "{illuminant.name}" illuminant shape to "{shape}".') illuminant = reshape_sd(illuminant, shape, copy=False) RGB_w = 1 / np.sum(sensitivities.values * illuminant.values[..., None], axis=0) RGB_w *= 1 / np.min(RGB_w) return RGB_w
[docs] def best_illuminant( RGB_w: ArrayLike, sensitivities: RGB_CameraSensitivities, illuminants: Mapping, ) -> SpectralDistribution: """ Select the best illuminant for the specified *RGB* white balance multipliers from a series of candidate illuminants based on camera sensitivities. The best illuminant is determined by finding the illuminant that produces white balance multipliers closest to the specified values, minimizing the sum of squared errors after normalization. Parameters ---------- RGB_w *RGB* white balance multipliers. sensitivities Camera *RGB* spectral sensitivities. illuminants Illuminant spectral distributions to choose the best illuminant from. Returns ------- :class:`colour.SpectralDistribution` Best illuminant spectral distribution. Examples -------- >>> path = os.path.join( ... ROOT_RESOURCES_RAWTOACES, ... "CANON_EOS_5DMark_II_RGB_Sensitivities.csv", ... ) >>> sensitivities = sds_and_msds_to_msds(read_sds_from_csv_file(path).values()) >>> illuminants = generate_illuminants_rawtoaces_v1() >>> RGB_w = white_balance_multipliers(sensitivities, SDS_ILLUMINANTS["FL2"]) >>> best_illuminant(RGB_w, sensitivities, illuminants).name 'D40' """ RGB_w = as_float_array(RGB_w) sse = np.inf illuminant_b = None for illuminant in illuminants.values(): RGB_wi = white_balance_multipliers(sensitivities, illuminant) sse_c = np.sum((RGB_wi / RGB_w - 1) ** 2) if sse_c < sse: sse = sse_c illuminant_b = illuminant return cast("SpectralDistribution", illuminant_b)
[docs] def normalise_illuminant( illuminant: SpectralDistribution, sensitivities: RGB_CameraSensitivities ) -> SpectralDistribution: """ Normalise the specified illuminant with camera *RGB* spectral sensitivities. The multiplicative inverse scaling factor :math:`k` is computed by multiplying the illuminant by the sensitivities channel with the maximum value. Parameters ---------- illuminant Illuminant spectral distribution. sensitivities Camera *RGB* spectral sensitivities. Returns ------- :class:`colour.SpectralDistribution` Normalised illuminant. Examples -------- >>> path = os.path.join( ... ROOT_RESOURCES_RAWTOACES, ... "CANON_EOS_5DMark_II_RGB_Sensitivities.csv", ... ) >>> sensitivities = sds_and_msds_to_msds(read_sds_from_csv_file(path).values()) >>> illuminant = SDS_ILLUMINANTS["D55"] >>> np.sum(illuminant.values) # doctest: +ELLIPSIS 7276.1490000... >>> np.sum(normalise_illuminant(illuminant, sensitivities).values) ... # doctest: +ELLIPSIS 3.4390373... """ shape = sensitivities.shape if illuminant.shape != shape: runtime_warning(f'Aligning "{illuminant.name}" illuminant shape to "{shape}".') illuminant = reshape_sd(illuminant, shape) c_i = np.argmax(np.max(sensitivities.values, axis=0)) k = 1 / np.sum(illuminant.values * sensitivities.values[..., c_i]) return illuminant * k
[docs] def training_data_sds_to_RGB( training_data: MultiSpectralDistributions, sensitivities: RGB_CameraSensitivities, illuminant: SpectralDistribution, ) -> Tuple[NDArrayFloat, NDArrayFloat]: """ Convert training data to *RGB* tristimulus values using the specified illuminant and camera *RGB* spectral sensitivities. Parameters ---------- training_data Training data multi-spectral distributions. sensitivities Camera *RGB* spectral sensitivities. illuminant Illuminant spectral distribution. Returns ------- :class:`tuple` Tuple of training data *RGB* tristimulus values and white balance multipliers. Examples -------- >>> path = os.path.join( ... ROOT_RESOURCES_RAWTOACES, ... "CANON_EOS_5DMark_II_RGB_Sensitivities.csv", ... ) >>> sensitivities = sds_and_msds_to_msds(read_sds_from_csv_file(path).values()) >>> illuminant = normalise_illuminant(SDS_ILLUMINANTS["D55"], sensitivities) >>> training_data = read_training_data_rawtoaces_v1() >>> RGB, RGB_w = training_data_sds_to_RGB(training_data, sensitivities, illuminant) >>> RGB[:5] # doctest: +ELLIPSIS array([[ 0.0207582..., 0.0196857..., 0.0213935...], [ 0.0895775..., 0.0891922..., 0.0891091...], [ 0.7810230..., 0.7801938..., 0.7764302...], [ 0.1995 ..., 0.1995 ..., 0.1995 ...], [ 0.5898478..., 0.5904015..., 0.5851076...]]) >>> RGB_w # doctest: +ELLIPSIS array([ 2.3414154..., 1. , 1.5163375...]) """ shape = sensitivities.shape if illuminant.shape != shape: runtime_warning(f'Aligning "{illuminant.name}" illuminant shape to "{shape}".') illuminant = reshape_sd(illuminant, shape, copy=False) if training_data.shape != shape: runtime_warning( f'Aligning "{training_data.name}" training data shape to "{shape}".' ) training_data = reshape_msds(training_data, shape, copy=False) RGB_w = white_balance_multipliers(sensitivities, illuminant) RGB = np.dot( np.transpose(illuminant.values[..., None] * training_data.values), sensitivities.values, ) RGB *= RGB_w return RGB, RGB_w
[docs] def training_data_sds_to_XYZ( training_data: MultiSpectralDistributions, cmfs: MultiSpectralDistributions, illuminant: SpectralDistribution, chromatic_adaptation_transform: ( LiteralChromaticAdaptationTransform | str | None ) = "CAT02", ) -> Range1: """ Convert training data to *CIE XYZ* tristimulus values using the specified illuminant and standard observer colour matching functions. Parameters ---------- training_data Training data multi-spectral distributions. cmfs Standard observer colour matching functions. illuminant Illuminant spectral distribution. chromatic_adaptation_transform *Chromatic adaptation* transform, if *None* no chromatic adaptation is performed. Returns ------- :class:`numpy.ndarray` Training data *CIE XYZ* tristimulus values. Notes ----- +------------+-----------------------+---------------+ | **Range** | **Scale - Reference** | **Scale - 1** | +============+=======================+===============+ | ``XYZ`` | 1 | 1 | +------------+-----------------------+---------------+ Examples -------- >>> from colour import MSDS_CMFS >>> path = os.path.join( ... ROOT_RESOURCES_RAWTOACES, ... "CANON_EOS_5DMark_II_RGB_Sensitivities.csv", ... ) >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] >>> sensitivities = sds_and_msds_to_msds(read_sds_from_csv_file(path).values()) >>> illuminant = normalise_illuminant(SDS_ILLUMINANTS["D55"], sensitivities) >>> training_data = read_training_data_rawtoaces_v1() >>> training_data_sds_to_XYZ(training_data, cmfs, illuminant)[:5] ... # doctest: +ELLIPSIS array([[ 0.0174353..., 0.0179504..., 0.0196109...], [ 0.0855607..., 0.0895735..., 0.0901703...], [ 0.7455880..., 0.7817549..., 0.7834356...], [ 0.1900528..., 0.1995 ..., 0.2012606...], [ 0.5626319..., 0.5914544..., 0.5894500...]]) """ shape = cmfs.shape if illuminant.shape != shape: runtime_warning(f'Aligning "{illuminant.name}" illuminant shape to "{shape}".') illuminant = reshape_sd(illuminant, shape, copy=False) if training_data.shape != shape: runtime_warning( f'Aligning "{training_data.name}" training data shape to "{shape}".' ) training_data = reshape_msds(training_data, shape, copy=False) XYZ = np.dot( np.transpose(illuminant.values[..., None] * training_data.values), cmfs.values, ) XYZ *= 1 / np.sum(cmfs.values[..., 1] * illuminant.values) XYZ_w = np.dot(np.transpose(cmfs.values), illuminant.values) XYZ_w *= 1 / XYZ_w[1] if chromatic_adaptation_transform is not None: M_CAT = matrix_chromatic_adaptation_VonKries( XYZ_w, xy_to_XYZ(RGB_COLOURSPACE_ACES2065_1.whitepoint), chromatic_adaptation_transform, ) XYZ = vecmul(M_CAT, XYZ) return XYZ
[docs] def whitepoint_preserving_matrix( M: ArrayLike, RGB_w: ArrayLike = (1, 1, 1) ) -> NDArrayFloat: """ Normalise the specified matrix :math:`M` to preserve the white point :math:`RGB_w`. Parameters ---------- M Matrix :math:`M` to normalise. RGB_w White point :math:`RGB_w` to normalise the matrix :math:`M` with. Returns ------- :class:`numpy.ndarray` Normalised matrix :math:`M`. Examples -------- >>> M = np.reshape(np.arange(9), (3, 3)) >>> whitepoint_preserving_matrix(M) array([[ 0., 1., 0.], [ 3., 4., -6.], [ 6., 7., -12.]]) """ M = as_float_array(M) RGB_w = as_float_array(RGB_w) M[..., -1] = RGB_w - np.sum(M[..., :-1], axis=-1) return M
[docs] def optimisation_factory_rawtoaces_v1() -> Tuple[ NDArrayFloat, Callable, Callable, Callable ]: """ Generate the objective function and *CIE XYZ* colourspace to optimisation colourspace/colour model function based according to *RAW to ACES* v1. The objective function computes the Euclidean distance between the training data *RGB* tristimulus values and the training data *CIE XYZ* tristimulus values in the *CIE L\\*a\\*b\\** colourspace. Implement whitepoint preservation as an optimisation constraint. Returns ------- :class:`tuple` :math:`x_0` initial values, objective function, *CIE XYZ* colourspace to *CIE L\\*a\\*b\\** colourspace function and finaliser function. Examples -------- >>> optimisation_factory_rawtoaces_v1() # doctest: +SKIP (array([ 1., 0., 0., 1., 0., 0.]), \ <function optimisation_factory_rawtoaces_v1.<locals> \ .objective_function at 0x...>, \ <function optimisation_factory_rawtoaces_v1.<locals>\ .XYZ_to_optimization_colour_model at 0x...>, \ <function optimisation_factory_rawtoaces_v1.<locals>\ .finaliser_function at 0x...>) """ x_0 = as_float_array([1, 0, 0, 1, 0, 0]) def objective_function( M: NDArrayFloat, RGB: NDArrayFloat, Lab: NDArrayFloat ) -> DTypeFloat: """Objective function according to *RAW to ACES* v1.""" M = finaliser_function(M) XYZ_t = vecmul(RGB_COLOURSPACE_ACES2065_1.matrix_RGB_to_XYZ, vecmul(M, RGB)) Lab_t = XYZ_to_optimization_colour_model(XYZ_t) return as_float(np.linalg.norm(Lab_t - Lab)) def XYZ_to_optimization_colour_model(XYZ: ArrayLike) -> NDArrayFloat: """*CIE XYZ* colourspace to *CIE L\\*a\\*b\\** colourspace function.""" return XYZ_to_Lab(XYZ, RGB_COLOURSPACE_ACES2065_1.whitepoint) def finaliser_function(M: ArrayLike) -> NDArrayFloat: """Finaliser function.""" return whitepoint_preserving_matrix( np.hstack([np.reshape(M, (3, 2)), zeros((3, 1))]) ) return ( x_0, objective_function, XYZ_to_optimization_colour_model, finaliser_function, )
[docs] def optimisation_factory_Jzazbz() -> Tuple[NDArrayFloat, Callable, Callable, Callable]: """ Generate the objective function and *CIE XYZ* colourspace to optimisation colourspace/colour model function based on the :math:`J_za_zb_z` colourspace. The objective function computes the Euclidean distance between the training data *RGB* tristimulus values and the training data *CIE XYZ* tristimulus values in the :math:`J_za_zb_z` colourspace. Implement whitepoint preservation as a post-optimisation step. Returns ------- :class:`tuple` :math:`x_0` initial values, objective function, *CIE XYZ* colourspace to :math:`J_za_zb_z` colourspace function and finaliser function. Examples -------- >>> optimisation_factory_Jzazbz() # doctest: +SKIP (array([ 1., 0., 0., 1., 0., 0.]), \ <function optimisation_factory_Jzazbz.<locals>\ .objective_function at 0x...>, \ <function optimisation_factory_Jzazbz.<locals>\ .XYZ_to_optimization_colour_model at 0x...>, \ <function optimisation_factory_Jzazbz.<locals>.\ finaliser_function at 0x...>) """ x_0 = as_float_array([1, 0, 0, 1, 0, 0]) def objective_function(M: ArrayLike, RGB: ArrayLike, Jab: ArrayLike) -> DTypeFloat: """:math:`J_za_zb_z` colourspace based objective function.""" M = finaliser_function(M) XYZ_t = vecmul(RGB_COLOURSPACE_ACES2065_1.matrix_RGB_to_XYZ, vecmul(M, RGB)) Jab_t = XYZ_to_optimization_colour_model(XYZ_t) return as_float(np.sum(euclidean_distance(Jab, Jab_t))) def XYZ_to_optimization_colour_model(XYZ: ArrayLike) -> NDArrayFloat: """*CIE XYZ* colourspace to :math:`J_za_zb_z` colourspace function.""" return XYZ_to_Jzazbz(XYZ) def finaliser_function(M: ArrayLike) -> NDArrayFloat: """Finaliser function.""" return whitepoint_preserving_matrix( np.hstack([np.reshape(M, (3, 2)), zeros((3, 1))]) ) return ( x_0, objective_function, XYZ_to_optimization_colour_model, finaliser_function, )
[docs] def optimisation_factory_Oklab_15() -> Tuple[ NDArrayFloat, Callable, Callable, Callable ]: """ Generate the objective function and *CIE XYZ* colourspace to optimisation colourspace/colour model function based on the *Oklab* colourspace. The objective function computes the Euclidean distance between the training data *RGB* tristimulus values and the training data *CIE XYZ* tristimulus values in the *Oklab* colourspace. Implement support for *Finlayson et al. (2015)* root-polynomials of degree 2 and produce 15 terms. Returns ------- :class:`tuple` :math:`x_0` initial values, objective function, *CIE XYZ* colourspace to *Oklab* colourspace function and finaliser function. References ---------- :cite:`Finlayson2015` Examples -------- >>> optimisation_factory_Oklab_15() # doctest: +SKIP (array([ 1., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., \ 0., 1.]), \ <function optimisation_factory_Oklab_15.<locals>\ .objective_function at 0x...>, \ <function optimisation_factory_Oklab_15.<locals>\ .XYZ_to_optimization_colour_model at 0x...>, \ <function optimisation_factory_Oklab_15.<locals>.\ finaliser_function at 0x...>) """ x_0 = as_float_array([1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1]) def objective_function(M: ArrayLike, RGB: ArrayLike, Jab: ArrayLike) -> DTypeFloat: """*Oklab* colourspace based objective function.""" M = finaliser_function(M) XYZ_t = np.transpose( np.dot( RGB_COLOURSPACE_ACES2065_1.matrix_RGB_to_XYZ, np.dot( M, np.transpose(polynomial_expansion_Finlayson2015(RGB, 2, True)), ), ) ) Jab_t = XYZ_to_optimization_colour_model(XYZ_t) return as_float(np.sum(euclidean_distance(Jab, Jab_t))) def XYZ_to_optimization_colour_model(XYZ: ArrayLike) -> NDArrayFloat: """*CIE XYZ* colourspace to *Oklab* colourspace function.""" return XYZ_to_Oklab(XYZ) def finaliser_function(M: ArrayLike) -> NDArrayFloat: """Finaliser function.""" return whitepoint_preserving_matrix( np.hstack([np.reshape(M, (3, 5)), zeros((3, 1))]) ) return ( x_0, objective_function, XYZ_to_optimization_colour_model, finaliser_function, )
@typing.overload def matrix_idt( sensitivities: RGB_CameraSensitivities, illuminant: SpectralDistribution, training_data: MultiSpectralDistributions | None = ..., cmfs: MultiSpectralDistributions | None = ..., optimisation_factory: Callable = ..., optimisation_kwargs: dict | None = ..., chromatic_adaptation_transform: ( LiteralChromaticAdaptationTransform | str | None ) = ..., additional_data: Literal[True] = True, ) -> Tuple[NDArrayFloat, NDArrayFloat, NDArrayFloat, NDArrayFloat]: ... @typing.overload def matrix_idt( sensitivities: ..., illuminant: ..., training_data: MultiSpectralDistributions | None = ..., cmfs: MultiSpectralDistributions | None = ..., optimisation_factory: Callable = ..., optimisation_kwargs: dict | None = ..., chromatic_adaptation_transform: ( LiteralChromaticAdaptationTransform | str | None ) = ..., *, additional_data: Literal[False], ) -> Tuple[NDArrayFloat, NDArrayFloat]: ... @typing.overload def matrix_idt( sensitivities: RGB_CameraSensitivities, illuminant: SpectralDistribution, training_data: MultiSpectralDistributions | None, cmfs: MultiSpectralDistributions | None, optimisation_factory: Callable, optimisation_kwargs: dict | None, chromatic_adaptation_transform: (LiteralChromaticAdaptationTransform | str | None), additional_data: Literal[False], ) -> Tuple[NDArrayFloat, NDArrayFloat, NDArrayFloat, NDArrayFloat]: ...
[docs] @required("SciPy") def matrix_idt( sensitivities: RGB_CameraSensitivities, illuminant: SpectralDistribution, training_data: MultiSpectralDistributions | None = None, cmfs: MultiSpectralDistributions | None = None, optimisation_factory: Callable = optimisation_factory_rawtoaces_v1, optimisation_kwargs: dict | None = None, chromatic_adaptation_transform: ( LiteralChromaticAdaptationTransform | str | None ) = "CAT02", additional_data: bool = False, ) -> ( Tuple[NDArrayFloat, NDArrayFloat, NDArrayFloat, NDArrayFloat] | Tuple[NDArrayFloat, NDArrayFloat] ): """ Compute an *Input Device Transform* (IDT) matrix for camera *RGB* spectral sensitivities, illuminant, training data, standard observer colour matching functions and optimisation settings according to *RAW to ACES* v1 and *P-2013-001* procedures. Parameters ---------- sensitivities Camera *RGB* spectral sensitivities. illuminant Illuminant spectral distribution. training_data Training data multi-spectral distributions, defaults to using the *RAW to ACES* v1 190 patches. cmfs Standard observer colour matching functions, default to the *CIE 1931 2 Degree Standard Observer*. optimisation_factory Callable producing the objective function and the *CIE XYZ* to optimisation colour model function. optimisation_kwargs Parameters for :func:`scipy.optimize.minimize` definition. chromatic_adaptation_transform *Chromatic adaptation* transform, if *None* no chromatic adaptation is performed. additional_data If *True*, the *XYZ* and *RGB* tristimulus values are also returned. Returns ------- :class:`tuple` Tuple of IDT matrix and white balance multipliers or tuple of IDT matrix, white balance multipliers, *XYZ* and *RGB* tristimulus values. References ---------- :cite:`Dyer2017`, :cite:`TheAcademyofMotionPictureArtsandSciences2015c` Examples -------- Computing the IDT matrix for a *CANON EOS 5DMark II* and *CIE Illuminant D Series* *D55* using the method specified in *RAW to ACES* v1: >>> path = os.path.join( ... ROOT_RESOURCES_RAWTOACES, ... "CANON_EOS_5DMark_II_RGB_Sensitivities.csv", ... ) >>> sensitivities = sds_and_msds_to_msds(read_sds_from_csv_file(path).values()) >>> illuminant = SDS_ILLUMINANTS["D55"] >>> M, RGB_w = matrix_idt(sensitivities, illuminant) >>> np.around(M, 3) array([[ 0.865, -0.026, 0.161], [ 0.057, 1.123, -0.18 ], [ 0.024, -0.203, 1.179]]) >>> RGB_w # doctest: +ELLIPSIS array([ 2.3414154..., 1. , 1.5163375...]) The *RAW to ACES* v1 matrix for the same camera and optimized by `Ceres Solver <http://ceres-solver.org>`__ is as follows:: 0.864994 -0.026302 0.161308 0.056527 1.122997 -0.179524 0.023683 -0.202547 1.178864 >>> M, RGB_w = matrix_idt( ... sensitivities, ... illuminant, ... optimisation_factory=optimisation_factory_Jzazbz, ... ) >>> np.around(M, 3) array([[ 0.852, -0.009, 0.158], [ 0.054, 1.122, -0.176], [ 0.023, -0.224, 1.2 ]]) >>> RGB_w # doctest: +ELLIPSIS array([ 2.3414154..., 1. , 1.5163375...]) >>> M, RGB_w = matrix_idt( ... sensitivities, ... illuminant, ... optimisation_factory=optimisation_factory_Oklab_15, ... ) >>> np.around(M, 3) array([[ 0.645, -0.611, 0.107, 0.736, 0.398, -0.275], [-0.159, 0.728, -0.091, 0.651, 0.01 , -0.139], [-0.172, -0.403, 1.394, 0.51 , -0.295, -0.034]]) >>> RGB_w # doctest: +ELLIPSIS array([ 2.3414154..., 1. , 1.5163375...]) """ from scipy.optimize import minimize # noqa: PLC0415 training_data = optional(training_data, read_training_data_rawtoaces_v1()) cmfs, illuminant = handle_spectral_arguments( cmfs, illuminant, shape_default=SPECTRAL_SHAPE_RAWTOACES ) shape = cmfs.shape if sensitivities.shape != shape: runtime_warning( f'Aligning "{sensitivities.name}" sensitivities shape to "{shape}".' ) sensitivities = reshape_msds(sensitivities, shape, copy=False) if training_data.shape != shape: runtime_warning( f'Aligning "{training_data.name}" training data shape to "{shape}".' ) training_data = reshape_msds(training_data, shape, copy=False) illuminant = normalise_illuminant(illuminant, sensitivities) RGB, RGB_w = training_data_sds_to_RGB(training_data, sensitivities, illuminant) XYZ = training_data_sds_to_XYZ( training_data, cmfs, illuminant, chromatic_adaptation_transform ) ( x_0, objective_function, XYZ_to_optimization_colour_model, finaliser_function, ) = optimisation_factory() optimisation_settings: dict[str, Any] = { "method": "BFGS", "jac": "2-point", } if optimisation_kwargs is not None: optimisation_settings.update(optimisation_kwargs) M = minimize( objective_function, x_0, (RGB, XYZ_to_optimization_colour_model(XYZ)), **optimisation_settings, ).x M = finaliser_function(M) if additional_data: return M, RGB_w, XYZ, RGB return M, RGB_w
[docs] def camera_RGB_to_ACES2065_1( RGB: ArrayLike, B: ArrayLike, b: ArrayLike, k: ArrayLike = (1, 1, 1), clip: bool = False, ) -> NDArrayFloat: """ Convert camera *RGB* colourspace array to *ACES2065-1* colourspace using the specified *Input Device Transform* (IDT) matrix :math:`B`, white balance multipliers :math:`b`, and exposure factor :math:`k` according to the *P-2013-001* procedure. Parameters ---------- RGB Camera *RGB* colourspace array. B *Input Device Transform* (IDT) matrix :math:`B`. b White balance multipliers :math:`b`. k Exposure factor :math:`k` that results in a nominally "18% gray" object in the scene producing ACES values [0.18, 0.18, 0.18]. clip Whether to clip the white balanced camera *RGB* colourspace array between :math:`-\\infty` and 1. The intent is to keep sensor saturated values achromatic after white balancing. Returns ------- :class:`numpy.ndarray` *ACES2065-1* colourspace relative exposure values array. References ---------- :cite:`TheAcademyofMotionPictureArtsandSciences2015c` Examples -------- >>> path = os.path.join( ... ROOT_RESOURCES_RAWTOACES, ... "CANON_EOS_5DMark_II_RGB_Sensitivities.csv", ... ) >>> sensitivities = sds_and_msds_to_msds(read_sds_from_csv_file(path).values()) >>> illuminant = SDS_ILLUMINANTS["D55"] >>> B, b = matrix_idt(sensitivities, illuminant) >>> camera_RGB_to_ACES2065_1(np.array([0.1, 0.2, 0.3]), B, b) ... # doctest: +ELLIPSIS array([ 0.270644 ..., 0.1561487..., 0.5012965...]) """ RGB = as_float_array(RGB) B = as_float_array(B) b = as_float_array(b) k = as_float_array(k) RGB_r = b * RGB / np.min(b) RGB_r = np.clip(RGB_r, -np.inf, 1) if clip else RGB_r return k * vecmul(B, RGB_r)