Example #1
0
    def test_handle_spectral_arguments(self):
        """
        Test :func:`colour.colorimetry.tristimulus_values.\
handle_spectral_arguments` definition.
        """

        cmfs, illuminant = handle_spectral_arguments()
        # pylint: disable=E1102
        self.assertEqual(
            cmfs,
            reshape_msds(MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]),
        )
        self.assertEqual(illuminant, reshape_sd(SDS_ILLUMINANTS["D65"]))

        shape = SpectralShape(400, 700, 20)
        cmfs, illuminant = handle_spectral_arguments(shape_default=shape)
        self.assertEqual(cmfs.shape, shape)
        self.assertEqual(illuminant.shape, shape)

        cmfs, illuminant = handle_spectral_arguments(
            cmfs_default="CIE 2012 2 Degree Standard Observer",
            illuminant_default="E",
            shape_default=shape,
        )
        self.assertEqual(
            cmfs,
            reshape_msds(MSDS_CMFS["CIE 2012 2 Degree Standard Observer"],
                         shape=shape),
        )
        self.assertEqual(illuminant,
                         sd_ones(shape, interpolator=LinearInterpolator) * 100)
Example #2
0
    def setUp(self):
        """Initialise the common tests attributes."""

        self._shape = SPECTRAL_SHAPE_OTSU2018
        self._cmfs, self._sd_D65 = handle_spectral_arguments(
            shape_default=self._shape
        )

        self._reflectances = sds_and_msds_to_msds(
            list(SDS_COLOURCHECKERS["ColorChecker N Ohta"].values())
            + list(SDS_COLOURCHECKERS["BabelColor Average"].values())
        )

        self._tree = Tree_Otsu2018(
            self._reflectances, self._cmfs, self._sd_D65
        )

        self._XYZ_D65 = sd_to_XYZ(self._sd_D65)
        self._xy_D65 = XYZ_to_xy(self._XYZ_D65)

        self._temporary_directory = tempfile.mkdtemp()

        self._path = os.path.join(
            self._temporary_directory, "Test_Otsu2018.npz"
        )
Example #3
0
    def setUp(self):
        """Initialise the common tests attributes."""

        self._shape = SPECTRAL_SHAPE_OTSU2018
        self._cmfs, self._sd_D65 = handle_spectral_arguments(
            shape_default=self._shape
        )

        self._reflectances = sds_and_msds_to_msds(
            SDS_COLOURCHECKERS["ColorChecker N Ohta"].values()
        )

        self._tree = Tree_Otsu2018(self._reflectances)
        self._tree.optimise()
        for leaf in self._tree.leaves:
            if len(leaf.parent.children) == 2:
                self._node_a = leaf.parent
                self._node_b, self._node_c = self._node_a.children
                break

        self._data_a = Data_Otsu2018(
            np.transpose(reshape_msds(self._reflectances, self._shape).values),
            self._cmfs,
            self._sd_D65,
        )
        self._data_b = self._node_b.data

        self._partition_axis = self._node_a.partition_axis
Example #4
0
    def setUp(self):
        """Initialise the common tests attributes."""

        self._shape = SPECTRAL_SHAPE_OTSU2018
        self._cmfs, self._sd_D65 = handle_spectral_arguments(
            shape_default=self._shape
        )
        self._XYZ_D65 = sd_to_XYZ(self._sd_D65)
        self._xy_D65 = XYZ_to_xy(self._XYZ_D65)
Example #5
0
def _CCT_to_uv_Ohno2013(
        CCT_D_uv: ArrayLike,
        cmfs: Optional[MultiSpectralDistributions] = None) -> NDArray:
    """
    Return the *CIE UCS* colourspace *uv* chromaticity coordinates from given
    correlated colour temperature :math:`T_{cp}`, :math:`\\Delta_{uv}` and
    colour matching functions using *Ohno (2013)* method.

    Parameters
    ----------
    CCT_D_uv
        Correlated colour temperature :math:`T_{cp}`, :math:`\\Delta_{uv}`.
    cmfs
        Standard observer colour matching functions, default to the
        *CIE 1931 2 Degree Standard Observer*.

    Returns
    -------
    :class:`numpy.ndarray`
        *CIE UCS* colourspace *uv* chromaticity coordinates.
    """

    CCT, D_uv = tsplit(CCT_D_uv)

    cmfs, _illuminant = handle_spectral_arguments(cmfs)

    delta = 0.01

    sd = sd_blackbody(CCT, cmfs.shape)
    XYZ = sd_to_XYZ(sd, cmfs)
    XYZ *= 1 / np.max(XYZ)
    UVW = XYZ_to_UCS(XYZ)
    u0, v0 = UCS_to_uv(UVW)

    if D_uv == 0:
        return np.array([u0, v0])
    else:
        sd = sd_blackbody(CCT + delta, cmfs.shape)
        XYZ = sd_to_XYZ(sd, cmfs)
        XYZ *= 1 / np.max(XYZ)
        UVW = XYZ_to_UCS(XYZ)
        u1, v1 = UCS_to_uv(UVW)

        du = u0 - u1
        dv = v0 - v1

        u = u0 - D_uv * (dv / np.hypot(du, dv))
        v = v0 + D_uv * (du / np.hypot(du, dv))

        return np.array([u, v])
Example #6
0
    def setUp(self):
        """Initialise the common tests attributes."""

        self._shape = SPECTRAL_SHAPE_OTSU2018
        self._cmfs, self._sd_D65 = handle_spectral_arguments(
            shape_default=self._shape
        )

        self._reflectances = np.transpose(
            reshape_msds(
                sds_and_msds_to_msds(
                    SDS_COLOURCHECKERS["ColorChecker N Ohta"].values()
                ),
                self._shape,
            ).values
        )

        self._data = Data_Otsu2018(
            self._reflectances, self._cmfs, self._sd_D65
        )
Example #7
0
    def generate(
        self,
        colourspace: RGB_Colourspace,
        cmfs: Optional[MultiSpectralDistributions] = None,
        illuminant: Optional[SpectralDistribution] = None,
        size: Integer = 64,
        print_callable: Callable = print,
    ):
        """
        Generate the lookup table data for given *RGB* colourspace, colour
        matching functions, illuminant and given size.

        Parameters
        ----------
        colourspace
            The *RGB* colourspace to create a lookup table for.
        cmfs
            Standard observer colour matching functions, default to the
            *CIE 1931 2 Degree Standard Observer*.
        illuminant
            Illuminant spectral distribution, default to
            *CIE Standard Illuminant D65*.
        size
            The resolution of the lookup table. Higher values will decrease
            errors but at the cost of a much longer run time. The published
            *\\*.coeff* files have a resolution of 64.
        print_callable
            Callable used to print progress and diagnostic information.

        Examples
        --------
        >>> from colour import MSDS_CMFS, SDS_ILLUMINANTS
        >>> from colour.models import RGB_COLOURSPACE_sRGB
        >>> from colour.utilities import numpy_print_options
        >>> cmfs = (
        ...     MSDS_CMFS['CIE 1931 2 Degree Standard Observer'].
        ...     copy().align(SpectralShape(360, 780, 10))
        ... )
        >>> illuminant = SDS_ILLUMINANTS['D65'].copy().align(cmfs.shape)
        >>> LUT = LUT3D_Jakob2019()
        >>> print(LUT.interpolator)
        None
        >>> LUT.generate(RGB_COLOURSPACE_sRGB, cmfs, illuminant, 3)
        ======================================================================\
=========
        *                                                                     \
        *
        *   "Jakob et al. (2018)" LUT Optimisation                            \
        *
        *                                                                     \
        *
        ======================================================================\
=========
        <BLANKLINE>
        Optimising 27 coefficients...
        <BLANKLINE>
        >>> print(LUT.interpolator)
        ... # doctest: +ELLIPSIS
        <scipy.interpolate...RegularGridInterpolator object at 0x...>
        """

        cmfs, illuminant = handle_spectral_arguments(
            cmfs, illuminant, shape_default=SPECTRAL_SHAPE_JAKOB2019
        )
        shape = cmfs.shape

        xy_n = XYZ_to_xy(sd_to_XYZ_integration(illuminant, cmfs))

        # It could be interesting to have different resolutions for lightness
        # and chromaticity, but the current file format doesn't allow it.
        lightness_steps = size
        chroma_steps = size

        self._lightness_scale = lightness_scale(lightness_steps)
        self._coefficients = np.empty(
            [3, chroma_steps, chroma_steps, lightness_steps, 3]
        )

        cube_indexes = np.ndindex(3, chroma_steps, chroma_steps)
        total_coefficients = chroma_steps**2 * 3

        # First, create a list of all the fully bright colours with the order
        # matching cube_indexes.
        samples = np.linspace(0, 1, chroma_steps)
        ij = np.reshape(
            np.transpose(np.meshgrid([1], samples, samples, indexing="ij")),
            (-1, 3),
        )
        chromas = np.concatenate(
            [
                as_float_array(ij),
                np.roll(ij, 1, axis=1),
                np.roll(ij, 2, axis=1),
            ]
        )

        message_box(
            '"Jakob et al. (2018)" LUT Optimisation',
            print_callable=print_callable,
        )

        print_callable(f"\nOptimising {total_coefficients} coefficients...\n")

        def optimize(ijkL: ArrayLike, coefficients_0: ArrayLike) -> NDArray:
            """
            Solve for a specific lightness and stores the result in the
            appropriate cell.
            """

            i, j, k, L = tsplit(ijkL, dtype=DEFAULT_INT_DTYPE)

            RGB = self._lightness_scale[L] * chroma

            XYZ = RGB_to_XYZ(
                RGB,
                colourspace.whitepoint,
                xy_n,
                colourspace.matrix_RGB_to_XYZ,
            )

            coefficients, _error = find_coefficients_Jakob2019(
                XYZ, cmfs, illuminant, coefficients_0, dimensionalise=False
            )

            self._coefficients[i, L, j, k, :] = dimensionalise_coefficients(
                coefficients, shape
            )

            return coefficients

        with tqdm(total=total_coefficients) as progress:
            for ijk, chroma in zip(cube_indexes, chromas):
                progress.update()

                # Starts from somewhere in the middle, similarly to how
                # feedback works in "colour.recovery.\
                # find_coefficients_Jakob2019" definition.
                L_middle = lightness_steps // 3
                coefficients_middle = optimize(
                    np.hstack([ijk, L_middle]), zeros(3)
                )

                # Down the lightness scale.
                coefficients_0 = coefficients_middle
                for L in reversed(range(0, L_middle)):
                    coefficients_0 = optimize(
                        np.hstack([ijk, L]), coefficients_0
                    )

                # Up the lightness scale.
                coefficients_0 = coefficients_middle
                for L in range(L_middle + 1, lightness_steps):
                    coefficients_0 = optimize(
                        np.hstack([ijk, L]), coefficients_0
                    )

        self._size = size
        self._create_interpolator()
Example #8
0
def XYZ_to_sd_Jakob2019(
    XYZ: ArrayLike,
    cmfs: Optional[MultiSpectralDistributions] = None,
    illuminant: Optional[SpectralDistribution] = None,
    optimisation_kwargs: Optional[Dict] = None,
    additional_data: Boolean = False,
) -> Union[Tuple[SpectralDistribution, Floating], SpectralDistribution]:
    """
    Recover the spectral distribution of given RGB colourspace array
    using *Jakob and Hanika (2019)* method.

    Parameters
    ----------
    XYZ
        *CIE XYZ* tristimulus values to recover the spectral distribution from.
    cmfs
        Standard observer colour matching functions, default to the
        *CIE 1931 2 Degree Standard Observer*.
    illuminant
        Illuminant spectral distribution, default to
        *CIE Standard Illuminant D65*.
    optimisation_kwargs
        Parameters for :func:`colour.recovery.find_coefficients_Jakob2019`
        definition.
    additional_data
        If *True*, ``error`` will be returned alongside the recovered spectral
        distribution.

    Returns
    -------
    :class:`tuple` or :class:`colour.SpectralDistribution`
        Tuple of recovered spectral distribution and :math:`\\Delta E_{76}`
        between the target colour and the colour corresponding to the computed
        coefficients or recovered spectral distribution.

    References
    ----------
    :cite:`Jakob2019`

    Examples
    --------
    >>> from colour import (
    ...     CCS_ILLUMINANTS, MSDS_CMFS, SDS_ILLUMINANTS, XYZ_to_sRGB)
    >>> from colour.colorimetry import sd_to_XYZ_integration
    >>> from colour.utilities import numpy_print_options
    >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952])
    >>> cmfs = (
    ...     MSDS_CMFS['CIE 1931 2 Degree Standard Observer'].
    ...     copy().align(SpectralShape(360, 780, 10))
    ... )
    >>> illuminant = SDS_ILLUMINANTS['D65'].copy().align(cmfs.shape)
    >>> sd = XYZ_to_sd_Jakob2019(XYZ, cmfs, illuminant)
    >>> with numpy_print_options(suppress=True):
    ...     sd  # doctest: +ELLIPSIS
    SpectralDistribution([[ 360.        ,    0.4893773...],
                          [ 370.        ,    0.3258214...],
                          [ 380.        ,    0.2147792...],
                          [ 390.        ,    0.1482413...],
                          [ 400.        ,    0.1086169...],
                          [ 410.        ,    0.0841255...],
                          [ 420.        ,    0.0683114...],
                          [ 430.        ,    0.0577144...],
                          [ 440.        ,    0.0504267...],
                          [ 450.        ,    0.0453552...],
                          [ 460.        ,    0.0418520...],
                          [ 470.        ,    0.0395259...],
                          [ 480.        ,    0.0381430...],
                          [ 490.        ,    0.0375741...],
                          [ 500.        ,    0.0377685...],
                          [ 510.        ,    0.0387432...],
                          [ 520.        ,    0.0405871...],
                          [ 530.        ,    0.0434783...],
                          [ 540.        ,    0.0477225...],
                          [ 550.        ,    0.0538256...],
                          [ 560.        ,    0.0626314...],
                          [ 570.        ,    0.0755869...],
                          [ 580.        ,    0.0952675...],
                          [ 590.        ,    0.1264265...],
                          [ 600.        ,    0.1779272...],
                          [ 610.        ,    0.2649393...],
                          [ 620.        ,    0.4039779...],
                          [ 630.        ,    0.5832105...],
                          [ 640.        ,    0.7445440...],
                          [ 650.        ,    0.8499970...],
                          [ 660.        ,    0.9094792...],
                          [ 670.        ,    0.9425378...],
                          [ 680.        ,    0.9616376...],
                          [ 690.        ,    0.9732481...],
                          [ 700.        ,    0.9806562...],
                          [ 710.        ,    0.9855873...],
                          [ 720.        ,    0.9889903...],
                          [ 730.        ,    0.9914117...],
                          [ 740.        ,    0.9931801...],
                          [ 750.        ,    0.9945009...],
                          [ 760.        ,    0.9955066...],
                          [ 770.        ,    0.9962855...],
                          [ 780.        ,    0.9968976...]],
                         interpolator=SpragueInterpolator,
                         interpolator_kwargs={},
                         extrapolator=Extrapolator,
                         extrapolator_kwargs={...})
    >>> sd_to_XYZ_integration(sd, cmfs, illuminant) / 100  # doctest: +ELLIPSIS
    array([ 0.2066217...,  0.1220128...,  0.0513958...])
    """

    XYZ = to_domain_1(XYZ)

    cmfs, illuminant = handle_spectral_arguments(
        cmfs, illuminant, shape_default=SPECTRAL_SHAPE_JAKOB2019
    )

    optimisation_kwargs = optional(optimisation_kwargs, {})

    with domain_range_scale("ignore"):
        coefficients, error = find_coefficients_Jakob2019(
            XYZ, cmfs, illuminant, **optimisation_kwargs
        )

    sd = sd_Jakob2019(coefficients, cmfs.shape)
    sd.name = f"{XYZ} (XYZ) - Jakob (2019)"

    if additional_data:
        return sd, error
    else:
        return sd
Example #9
0
def find_coefficients_Jakob2019(
    XYZ: ArrayLike,
    cmfs: Optional[MultiSpectralDistributions] = None,
    illuminant: Optional[SpectralDistribution] = None,
    coefficients_0: ArrayLike = zeros(3),
    max_error: Floating = JND_CIE1976 / 100,
    dimensionalise: Boolean = True,
) -> Tuple[NDArray, Floating]:
    """
    Compute the coefficients for *Jakob and Hanika (2019)* reflectance
    spectral model.

    Parameters
    ----------
    XYZ
        *CIE XYZ* tristimulus values to find the coefficients for.
    cmfs
        Standard observer colour matching functions, default to the
        *CIE 1931 2 Degree Standard Observer*.
    illuminant
        Illuminant spectral distribution, default to
        *CIE Standard Illuminant D65*.
    coefficients_0
        Starting coefficients for the solver.
    max_error
        Maximal acceptable error. Set higher to save computational time.
        If *None*, the solver will keep going until it is very close to the
        minimum. The default is ``ACCEPTABLE_DELTA_E``.
    dimensionalise
        If *True*, returned coefficients are dimensionful and will not work
        correctly if fed back as ``coefficients_0``. The default is *True*.

    Returns
    -------
    :class:`tuple`
        Tuple of computed coefficients that best fit the given colour and
        :math:`\\Delta E_{76}` between the target colour and the colour
        corresponding to the computed coefficients.

    References
    ----------
    :cite:`Jakob2019`

    Examples
    --------
    >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952])
    >>> find_coefficients_Jakob2019(XYZ)  # doctest: +ELLIPSIS
    (array([  1.3723791...e-04,  -1.3514399...e-01,   3.0838973...e+01]), \
0.0141941...)
    """

    coefficients_0 = as_float_array(coefficients_0)

    cmfs, illuminant = handle_spectral_arguments(
        cmfs, illuminant, shape_default=SPECTRAL_SHAPE_JAKOB2019
    )

    def optimize(
        target_o: NDArray, coefficients_0_o: NDArray
    ) -> Tuple[NDArray, Floating]:
        """Minimise the error function using *L-BFGS-B* method."""

        try:
            result = minimize(
                error_function,
                coefficients_0_o,
                (target_o, cmfs, illuminant, max_error),
                method="L-BFGS-B",
                jac=True,
            )

            return result.x, result.fun
        except StopMinimizationEarly as error:
            return error.coefficients, error.error

    xy_n = XYZ_to_xy(sd_to_XYZ_integration(illuminant, cmfs))

    XYZ_good = full(3, 0.5)
    coefficients_good = zeros(3)

    divisions = 3
    while divisions < 10:
        XYZ_r = XYZ_good
        coefficient_r = coefficients_good
        keep_divisions = False

        coefficients_0 = coefficient_r
        for i in range(1, divisions):
            XYZ_i = (XYZ - XYZ_r) * i / (divisions - 1) + XYZ_r
            Lab_i = XYZ_to_Lab(XYZ_i)

            coefficients_0, error = optimize(Lab_i, coefficients_0)

            if error > max_error:
                break
            else:
                XYZ_good = XYZ_i
                coefficients_good = coefficients_0
                keep_divisions = True
        else:
            break

        if not keep_divisions:
            divisions += 2

    target = XYZ_to_Lab(XYZ, xy_n)
    coefficients, error = optimize(target, coefficients_0)

    if dimensionalise:
        coefficients = dimensionalise_coefficients(coefficients, cmfs.shape)

    return coefficients, error
Example #10
0
def XYZ_to_sd_Meng2015(
    XYZ: ArrayLike,
    cmfs: Optional[MultiSpectralDistributions] = None,
    illuminant: Optional[SpectralDistribution] = None,
    optimisation_kwargs: Optional[Dict] = None,
) -> SpectralDistribution:
    """
    Recover the spectral distribution of given *CIE XYZ* tristimulus values
    using *Meng et al. (2015)* method.

    Parameters
    ----------
    XYZ
        *CIE XYZ* tristimulus values to recover the spectral distribution from.
    cmfs
        Standard observer colour matching functions. The wavelength
        :math:`\\lambda_{i}` range interval of the colour matching functions
        affects directly the time the computations take. The current default
        interval of 5 is a good compromise between precision and time spent,
        default to the *CIE 1931 2 Degree Standard Observer*.
    illuminant
        Illuminant spectral distribution, default to
        *CIE Standard Illuminant D65*.
    optimisation_kwargs
        Parameters for :func:`scipy.optimize.minimize` definition.

    Returns
    -------
    :class:`colour.SpectralDistribution`
        Recovered spectral distribution.

    Notes
    -----
    +------------+-----------------------+---------------+
    | **Domain** | **Scale - Reference** | **Scale - 1** |
    +============+=======================+===============+
    | ``XYZ``    | [0, 1]                | [0, 1]        |
    +------------+-----------------------+---------------+

    -   The definition used to convert spectrum to *CIE XYZ* tristimulus
        values is :func:`colour.colorimetry.spectral_to_XYZ_integration`
        definition because it processes any measurement interval opposed to
        :func:`colour.colorimetry.sd_to_XYZ_ASTME308` definition that
        handles only measurement interval of 1, 5, 10 or 20nm.

    References
    ----------
    :cite:`Meng2015c`

    Examples
    --------
    >>> from colour import MSDS_CMFS, SDS_ILLUMINANTS
    >>> from colour.utilities import numpy_print_options
    >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952])
    >>> cmfs = (
    ...     MSDS_CMFS['CIE 1931 2 Degree Standard Observer'].
    ...     copy().align(SpectralShape(360, 780, 10))
    ... )
    >>> illuminant = SDS_ILLUMINANTS['D65'].copy().align(cmfs.shape)
    >>> sd = XYZ_to_sd_Meng2015(XYZ, cmfs, illuminant)
    >>> with numpy_print_options(suppress=True):
    ...     sd  # doctest: +SKIP
    SpectralDistribution([[ 360.        ,    0.0762005...],
                          [ 370.        ,    0.0761792...],
                          [ 380.        ,    0.0761363...],
                          [ 390.        ,    0.0761194...],
                          [ 400.        ,    0.0762539...],
                          [ 410.        ,    0.0761671...],
                          [ 420.        ,    0.0754649...],
                          [ 430.        ,    0.0731519...],
                          [ 440.        ,    0.0676701...],
                          [ 450.        ,    0.0577800...],
                          [ 460.        ,    0.0441993...],
                          [ 470.        ,    0.0285064...],
                          [ 480.        ,    0.0138728...],
                          [ 490.        ,    0.0033585...],
                          [ 500.        ,    0.       ...],
                          [ 510.        ,    0.       ...],
                          [ 520.        ,    0.       ...],
                          [ 530.        ,    0.       ...],
                          [ 540.        ,    0.0055767...],
                          [ 550.        ,    0.0317581...],
                          [ 560.        ,    0.0754491...],
                          [ 570.        ,    0.1314115...],
                          [ 580.        ,    0.1937649...],
                          [ 590.        ,    0.2559311...],
                          [ 600.        ,    0.3123173...],
                          [ 610.        ,    0.3584966...],
                          [ 620.        ,    0.3927335...],
                          [ 630.        ,    0.4159458...],
                          [ 640.        ,    0.4306660...],
                          [ 650.        ,    0.4391040...],
                          [ 660.        ,    0.4439497...],
                          [ 670.        ,    0.4463618...],
                          [ 680.        ,    0.4474625...],
                          [ 690.        ,    0.4479868...],
                          [ 700.        ,    0.4482116...],
                          [ 710.        ,    0.4482800...],
                          [ 720.        ,    0.4483472...],
                          [ 730.        ,    0.4484251...],
                          [ 740.        ,    0.4484633...],
                          [ 750.        ,    0.4485071...],
                          [ 760.        ,    0.4484969...],
                          [ 770.        ,    0.4484853...],
                          [ 780.        ,    0.4485134...]],
                         interpolator=SpragueInterpolator,
                         interpolator_kwargs={},
                         extrapolator=Extrapolator,
                         extrapolator_kwargs={...})
    >>> sd_to_XYZ_integration(sd, cmfs, illuminant) / 100  # doctest: +ELLIPSIS
    array([ 0.2065400...,  0.1219722...,  0.0513695...])
    """

    XYZ = to_domain_1(XYZ)

    cmfs, illuminant = handle_spectral_arguments(
        cmfs, illuminant, shape_default=SPECTRAL_SHAPE_MENG2015
    )

    sd = sd_ones(cmfs.shape)

    def objective_function(a: ArrayLike) -> FloatingOrNDArray:
        """Define the objective function."""

        return np.sum(np.diff(a) ** 2)

    def constraint_function(a: ArrayLike) -> NDArray:
        """Define the constraint function."""

        sd[:] = a
        return (
            sd_to_XYZ_integration(sd, cmfs=cmfs, illuminant=illuminant) - XYZ
        )

    wavelengths = sd.wavelengths
    bins = wavelengths.size

    optimisation_settings = {
        "method": "SLSQP",
        "constraints": {"type": "eq", "fun": constraint_function},
        "bounds": np.tile(np.array([0, 1000]), (bins, 1)),
        "options": {
            "ftol": 1e-10,
        },
    }
    if optimisation_kwargs is not None:
        optimisation_settings.update(optimisation_kwargs)

    result = minimize(objective_function, sd.values, **optimisation_settings)

    if not result.success:
        raise RuntimeError(
            f"Optimization failed for {XYZ} after {result.nit} iterations: "
            f'"{result.message}".'
        )

    return SpectralDistribution(
        from_range_100(result.x * 100),
        wavelengths,
        name=f"{XYZ} (XYZ) - Meng (2015)",
    )
Example #11
0
def dominant_wavelength(
    xy: ArrayLike,
    xy_n: ArrayLike,
    cmfs: Optional[MultiSpectralDistributions] = None,
    inverse: bool = False,
) -> Tuple[NDArray, NDArray, NDArray]:
    """
    Return the *dominant wavelength* :math:`\\lambda_d` for given colour
    stimulus :math:`xy` and the related :math:`xy_wl` first and :math:`xy_{cw}`
    second intersection coordinates with the spectral locus.

    In the eventuality where the :math:`xy_wl` first intersection coordinates
    are on the line of purples, the *complementary wavelength* will be computed
    in lieu.

    The *complementary wavelength* is indicated by a negative sign and the
    :math:`xy_{cw}` second intersection coordinates which are set by default to
    the same value than :math:`xy_wl` first intersection coordinates will be
    set to the *complementary dominant wavelength* intersection coordinates
    with the spectral locus.

    Parameters
    ----------
    xy
        Colour stimulus *CIE xy* chromaticity coordinates.
    xy_n
        Achromatic stimulus *CIE xy* chromaticity coordinates.
    cmfs
        Standard observer colour matching functions, default to the
        *CIE 1931 2 Degree Standard Observer*.
    inverse
        Inverse the computation direction to retrieve the
        *complementary wavelength*.

    Returns
    -------
    :class:`tuple`
        *Dominant wavelength*, first intersection point *CIE xy* chromaticity
        coordinates, second intersection point *CIE xy* chromaticity
        coordinates.

    References
    ----------
    :cite:`CIETC1-482004o`, :cite:`Erdogana`

    Examples
    --------
    *Dominant wavelength* computation:

    >>> from colour.colorimetry import MSDS_CMFS
    >>> from pprint import pprint
    >>> cmfs = MSDS_CMFS['CIE 1931 2 Degree Standard Observer']
    >>> xy = np.array([0.54369557, 0.32107944])
    >>> xy_n = np.array([0.31270000, 0.32900000])
    >>> pprint(dominant_wavelength(xy, xy_n, cmfs))  # doctest: +ELLIPSIS
    (array(616...),
     array([ 0.6835474...,  0.3162840...]),
     array([ 0.6835474...,  0.3162840...]))

    *Complementary dominant wavelength* is returned if the first intersection
    is located on the line of purples:

    >>> xy = np.array([0.37605506, 0.24452225])
    >>> pprint(dominant_wavelength(xy, xy_n))  # doctest: +ELLIPSIS
    (array(-509.0),
     array([ 0.4572314...,  0.1362814...]),
     array([ 0.0104096...,  0.7320745...]))
    """

    cmfs, _illuminant = handle_spectral_arguments(cmfs)

    xy = as_float_array(xy)
    xy_n = np.resize(xy_n, xy.shape)

    xy_s = XYZ_to_xy(cmfs.values)

    i_wl, xy_wl = closest_spectral_locus_wavelength(xy, xy_n, xy_s, inverse)
    xy_cwl = xy_wl
    wl = cmfs.wavelengths[i_wl]

    xy_e = (extend_line_segment(xy, xy_n) if inverse else extend_line_segment(
        xy_n, xy))
    intersect = intersect_line_segments(np.concatenate((xy_n, xy_e), -1),
                                        np.hstack([xy_s[0],
                                                   xy_s[-1]])).intersect
    intersect = np.reshape(intersect, wl.shape)

    i_wl_r, xy_cwl_r = closest_spectral_locus_wavelength(
        xy, xy_n, xy_s, not inverse)
    wl_r = -cmfs.wavelengths[i_wl_r]

    wl = np.where(intersect, wl_r, wl)
    xy_cwl = np.where(intersect[..., np.newaxis], xy_cwl_r, xy_cwl)

    return wl, np.squeeze(xy_wl), np.squeeze(xy_cwl)
Example #12
0
def _uv_to_CCT_Ohno2013(
    uv: ArrayLike,
    cmfs: Optional[MultiSpectralDistributions] = None,
    start: Floating = CCT_MINIMAL,
    end: Floating = CCT_MAXIMAL,
    count: Integer = CCT_SAMPLES,
    iterations: Integer = CCT_CALCULATION_ITERATIONS,
) -> NDArray:
    """
    Return the correlated colour temperature :math:`T_{cp}` and
    :math:`\\Delta_{uv}` from given *CIE UCS* colourspace *uv* chromaticity
    coordinates, colour matching functions and temperature range using
    *Ohno (2013)* method.

    The ``iterations`` parameter defines the calculations' precision: The
    higher its value, the more planckian tables will be generated through
    cascade expansion in order to converge to the exact solution.

    Parameters
    ----------
    uv
        *CIE UCS* colourspace *uv* chromaticity coordinates.
    cmfs
        Standard observer colour matching functions, default to the
        *CIE 1931 2 Degree Standard Observer*.
    start
        Temperature range start in kelvin degrees.
    end
        Temperature range end in kelvin degrees.
    count
        Temperatures count in the planckian tables.
    iterations
        Number of planckian tables to generate.

    Returns
    -------
    :class:`numpy.ndarray`
        Correlated colour temperature :math:`T_{cp}`, :math:`\\Delta_{uv}`.
    """

    cmfs, _illuminant = handle_spectral_arguments(cmfs)

    # Ensuring that we do at least one iteration to initialise the variables.
    iterations = max(int(iterations), 1)

    # Planckian table creation through cascade expansion.
    for _i in range(iterations):
        table = planckian_table(uv, cmfs, start, end, count)
        index = planckian_table_minimal_distance_index(table)
        if index == 0:
            runtime_warning(
                "Minimal distance index is on lowest planckian table bound, "
                "unpredictable results may occur!")
            index += 1
        elif index == len(table) - 1:
            runtime_warning(
                "Minimal distance index is on highest planckian table bound, "
                "unpredictable results may occur!")
            index -= 1

        start = table[index - 1].Ti
        end = table[index + 1].Ti

    _ux, vx = tsplit(uv)

    Tuvdip, Tuvdi, Tuvdin = (table[index - 1], table[index], table[index + 1])
    Tip, uip, vip, dip = Tuvdip.Ti, Tuvdip.ui, Tuvdip.vi, Tuvdip.di
    Ti, di = Tuvdi.Ti, Tuvdi.di
    Tin, uin, vin, din = Tuvdin.Ti, Tuvdin.ui, Tuvdin.vi, Tuvdin.di

    # Triangular solution.
    l = np.hypot(uin - uip, vin - vip)  # noqa
    x = (dip**2 - din**2 + l**2) / (2 * l)
    T = Tip + (Tin - Tip) * (x / l)

    vtx = vip + (vin - vip) * (x / l)
    sign = 1 if vx - vtx >= 0 else -1
    D_uv = (dip**2 - x**2)**(1 / 2) * sign

    # Parabolic solution.
    if np.abs(D_uv) >= 0.002:
        X = (Tin - Ti) * (Tip - Tin) * (Ti - Tip)
        a = (Tip * (din - di) + Ti * (dip - din) + Tin * (di - dip)) * X**-1
        b = (-(Tip**2 * (din - di) + Ti**2 * (dip - din) + Tin**2 *
               (di - dip)) * X**-1)
        c = (-(dip * (Tin - Ti) * Ti * Tin + di *
               (Tip - Tin) * Tip * Tin + din * (Ti - Tip) * Tip * Ti) * X**-1)

        T = -b / (2 * a)

        D_uv = sign * (a * T**2 + b * T + c)

    return np.array([T, D_uv])
Example #13
0
def spectral_primary_decomposition_Mallett2019(
    colourspace: RGB_Colourspace,
    cmfs: Optional[MultiSpectralDistributions] = None,
    illuminant: Optional[SpectralDistribution] = None,
    metric: Callable = np.linalg.norm,
    metric_args: Tuple = tuple(),
    optimisation_kwargs: Optional[Dict] = None,
) -> MultiSpectralDistributions:
    """
    Perform the spectral primary decomposition as described in *Mallett and
    Yuksel (2019)* for given *RGB* colourspace.

    Parameters
    ----------
    colourspace
        *RGB* colourspace.
    cmfs
        Standard observer colour matching functions, default to the
        *CIE 1931 2 Degree Standard Observer*.
    illuminant
        Illuminant spectral distribution, default to
        *CIE Standard Illuminant D65*.
    metric
        Function to be minimised, i.e. the objective function.

            ``metric(basis, *metric_args) -> float``

        where ``basis`` is three reflectances concatenated together, each
        with a shape matching ``shape``.
    metric_args
        Additional arguments passed to ``metric``.
    optimisation_kwargs
        Parameters for :func:`scipy.optimize.minimize` definition.

    Returns
    -------
    :class:`colour.MultiSpectralDistributions`
        Basis functions for given *RGB* colourspace.

    References
    ----------
    :cite:`Mallett2019`

    Notes
    -----
    -   In-addition to the *BT.709* primaries used by the *sRGB* colourspace,
        :cite:`Mallett2019` tried *BT.2020*, *P3 D65*, *Adobe RGB 1998*,
        *NTSC (1987)*, *Pal/Secam*, *ProPhoto RGB*,
        and *Adobe Wide Gamut RGB* primaries, every one of which encompasses a
        larger (albeit not-always-enveloping) set of *CIE L\\*a\\*b\\** colours
        than BT.709. Of these, only *Pal/Secam* produces a feasible basis,
        which is relatively unsurprising since it is very similar to *BT.709*,
        whereas the others are significantly larger.

    Examples
    --------
    >>> from colour import MSDS_CMFS, SDS_ILLUMINANTS, SpectralShape
    >>> from colour.models import RGB_COLOURSPACE_PAL_SECAM
    >>> from colour.utilities import numpy_print_options
    >>> cmfs = (
    ...     MSDS_CMFS['CIE 1931 2 Degree Standard Observer'].
    ...     copy().align(SpectralShape(360, 780, 10))
    ... )
    >>> illuminant = SDS_ILLUMINANTS['D65'].copy().align(cmfs.shape)
    >>> msds = spectral_primary_decomposition_Mallett2019(
    ...     RGB_COLOURSPACE_PAL_SECAM, cmfs, illuminant, optimisation_kwargs={
    ...         'options': {'ftol': 1e-5}
    ...     }
    ... )
    >>> with numpy_print_options(suppress=True):
    ...     print(msds)  # doctest: +SKIP
    [[ 360.            0.3395134...    0.3400214...    0.3204650...]
     [ 370.            0.3355246...    0.3338028...    0.3306724...]
     [ 380.            0.3376707...    0.3185578...    0.3437715...]
     [ 390.            0.3178866...    0.3351754...    0.3469378...]
     [ 400.            0.3045154...    0.3248376...    0.3706469...]
     [ 410.            0.2935652...    0.2919463...    0.4144884...]
     [ 420.            0.1875740...    0.1853729...    0.6270530...]
     [ 430.            0.0167983...    0.054483 ...    0.9287186...]
     [ 440.            0.       ...    0.       ...    1.       ...]
     [ 450.            0.       ...    0.       ...    1.       ...]
     [ 460.            0.       ...    0.       ...    1.       ...]
     [ 470.            0.       ...    0.0458044...    0.9541955...]
     [ 480.            0.       ...    0.2960917...    0.7039082...]
     [ 490.            0.       ...    0.5042592...    0.4957407...]
     [ 500.            0.       ...    0.6655795...    0.3344204...]
     [ 510.            0.       ...    0.8607541...    0.1392458...]
     [ 520.            0.       ...    0.9999998...    0.0000001...]
     [ 530.            0.       ...    1.       ...    0.       ...]
     [ 540.            0.       ...    1.       ...    0.       ...]
     [ 550.            0.       ...    1.       ...    0.       ...]
     [ 560.            0.       ...    0.9924229...    0.       ...]
     [ 570.            0.       ...    0.9970703...    0.0025673...]
     [ 580.            0.0396002...    0.9028231...    0.0575766...]
     [ 590.            0.7058973...    0.2941026...    0.       ...]
     [ 600.            1.       ...    0.       ...    0.       ...]
     [ 610.            1.       ...    0.       ...    0.       ...]
     [ 620.            1.       ...    0.       ...    0.       ...]
     [ 630.            1.       ...    0.       ...    0.       ...]
     [ 640.            0.9835925...    0.0100166...    0.0063908...]
     [ 650.            0.7878949...    0.1265097...    0.0855953...]
     [ 660.            0.5987994...    0.2051062...    0.1960942...]
     [ 670.            0.4724493...    0.2649623...    0.2625883...]
     [ 680.            0.3989806...    0.3007488...    0.3002704...]
     [ 690.            0.3666586...    0.3164003...    0.3169410...]
     [ 700.            0.3497806...    0.3242863...    0.3259329...]
     [ 710.            0.3563736...    0.3232441...    0.3203822...]
     [ 720.            0.3362624...    0.3326209...    0.3311165...]
     [ 730.            0.3245015...    0.3365982...    0.3389002...]
     [ 740.            0.3335520...    0.3320670...    0.3343808...]
     [ 750.            0.3441287...    0.3291168...    0.3267544...]
     [ 760.            0.3343705...    0.3330132...    0.3326162...]
     [ 770.            0.3274633...    0.3305704...    0.3419662...]
     [ 780.            0.3475263...    0.3262331...    0.3262404...]]
    """

    cmfs, illuminant = handle_spectral_arguments(cmfs, illuminant)

    N = len(cmfs.shape)

    R_to_XYZ = np.transpose(illuminant.values[..., np.newaxis] * cmfs.values /
                            (np.sum(cmfs.values[:, 1] * illuminant.values)))
    R_to_RGB = np.dot(colourspace.matrix_XYZ_to_RGB, R_to_XYZ)
    basis_to_RGB = block_diag(R_to_RGB, R_to_RGB, R_to_RGB)

    primaries = np.reshape(np.identity(3), 9)

    # Ensure that the reflectances correspond to the correct RGB colours.
    colour_match = LinearConstraint(basis_to_RGB, primaries, primaries)

    # Ensure that the reflectances are bounded by [0, 1].
    energy_conservation = Bounds(np.zeros(3 * N), np.ones(3 * N))

    # Ensure that the sum of the three bases is bounded by [0, 1].
    sum_matrix = np.transpose(np.tile(np.identity(N), (3, 1)))
    sum_constraint = LinearConstraint(sum_matrix, np.zeros(N), np.ones(N))

    optimisation_settings = {
        "method": "SLSQP",
        "constraints": [colour_match, sum_constraint],
        "bounds": energy_conservation,
        "options": {
            "ftol": 1e-10,
        },
    }

    if optimisation_kwargs is not None:
        optimisation_settings.update(optimisation_kwargs)

    result = minimize(metric,
                      args=metric_args,
                      x0=np.zeros(3 * N),
                      **optimisation_settings)

    basis_functions = np.transpose(np.reshape(result.x, (3, N)))

    return MultiSpectralDistributions(
        basis_functions,
        cmfs.shape.range(),
        name=f"Basis Functions - {colourspace.name} - Mallett (2019)",
        labels=("red", "green", "blue"),
    )
Example #14
0
def is_within_visible_spectrum(
    XYZ: ArrayLike,
    cmfs: Optional[MultiSpectralDistributions] = None,
    illuminant: Optional[SpectralDistribution] = None,
    tolerance: Optional[Floating] = None,
    **kwargs: Any,
) -> NDArray:
    """
    Return whether given *CIE XYZ* tristimulus values are within the visible
    spectrum volume, i.e. *Rösch-MacAdam* colour solid, for given colour
    matching functions and illuminant.

    Parameters
    ----------
    XYZ
        *CIE XYZ* tristimulus values.
    cmfs
        Standard observer colour matching functions, default to the
        *CIE 1931 2 Degree Standard Observer*.
    illuminant
        Illuminant spectral distribution, default to *CIE Illuminant E*.
    tolerance
        Tolerance allowed in the inside-triangle check.

    Other Parameters
    ----------------
    kwargs
        {:func:`colour.msds_to_XYZ`},
        See the documentation of the previously listed definition.

    Returns
    -------
    :class:`numpy.ndarray`
        Are *CIE XYZ* tristimulus values within the visible spectrum volume,
        i.e. *Rösch-MacAdam* colour solid.

    Notes
    -----
    +------------+-----------------------+---------------+
    | **Domain** | **Scale - Reference** | **Scale - 1** |
    +============+=======================+===============+
    | ``XYZ``    | [0, 1]                | [0, 1]        |
    +------------+-----------------------+---------------+

    Examples
    --------
    >>> import numpy as np
    >>> is_within_visible_spectrum(np.array([0.3205, 0.4131, 0.51]))
    array(True, dtype=bool)
    >>> a = np.array([[0.3205, 0.4131, 0.51],
    ...               [-0.0005, 0.0031, 0.001]])
    >>> is_within_visible_spectrum(a)
    array([ True, False], dtype=bool)
    """

    cmfs, illuminant = handle_spectral_arguments(
        cmfs,
        illuminant,
        "CIE 1931 2 Degree Standard Observer",
        "E",
        SPECTRAL_SHAPE_OUTER_SURFACE_XYZ,
    )

    key = (hash(cmfs), hash(illuminant), str(kwargs))
    vertices = _CACHE_OUTER_SURFACE_XYZ_POINTS.get(key)

    if vertices is None:
        _CACHE_OUTER_SURFACE_XYZ_POINTS[key] = vertices = solid_RoschMacAdam(
            cmfs, illuminant, **kwargs)

    return is_within_mesh_volume(XYZ, vertices, tolerance)
Example #15
0
def XYZ_outer_surface(
    cmfs: Optional[MultiSpectralDistributions] = None,
    illuminant: Optional[SpectralDistribution] = None,
    point_order: Union[Literal["Bins", "Pulse Wave Width"], str] = "Bins",
    filter_jagged_points: Boolean = False,
    **kwargs: Any,
) -> NDArray:
    """
    Generate the *Rösch-MacAdam* colour solid, i.e. *CIE XYZ* colourspace
    outer surface, for given colour matching functions using multi-spectral
    conversion of pulse waves to *CIE XYZ* tristimulus values.

    Parameters
    ----------
    cmfs
        Standard observer colour matching functions, default to the
        *CIE 1931 2 Degree Standard Observer*.
    illuminant
        Illuminant spectral distribution, default to *CIE Illuminant E*.
    point_order
        Method for ordering the underlying pulse waves used to generate the
        *Rösch-MacAdam* colour solid. *Bins* is the default order, with
        *Pulse Wave Width* ordering, instead of iterating over the pulse wave
        widths first, iteration occurs over the bins, producing blocks of pulse
        waves with increasing width.
    filter_jagged_points
        Whether to filter the underlying jagged pulses. When ``point_order`` is
        set to *Pulse Wave Width*, the pulses are ordered by increasing width.
        Because of the discrete nature of the underlying signal, the resulting
        pulses will be jagged. For example assuming 5 bins, the center block
        with the two extreme values added would be as follows::

            0 0 0 0 0
            0 0 1 0 0
            0 0 1 1 0 <--
            0 1 1 1 0
            0 1 1 1 1 <--
            1 1 1 1 1

        Setting the ``filter_jagged_points`` parameter to `True` will result
        in the removal of the two marked pulse waves above thus avoiding jagged
        lines when plotting and having to resort to excessive ``bins`` values.

    Other Parameters
    ----------------
    kwargs
        {:func:`colour.msds_to_XYZ`},
        See the documentation of the previously listed definition.

    Returns
    -------
    :class:`numpy.ndarray`
        *Rösch-MacAdam* colour solid, *CIE XYZ* outer surface tristimulus
        values.

    References
    ----------
    :cite:`Lindbloom2015`, :cite:`Mansencal2018`, :cite:`Martinez-Verdu2007`

    Examples
    --------
    >>> from colour import MSDS_CMFS, SPECTRAL_SHAPE_DEFAULT
    >>> shape = SpectralShape(
    ...     SPECTRAL_SHAPE_DEFAULT.start, SPECTRAL_SHAPE_DEFAULT.end, 84
    ... )
    >>> cmfs = MSDS_CMFS['CIE 1931 2 Degree Standard Observer']
    >>> XYZ_outer_surface(cmfs.copy().align(shape))  # doctest: +ELLIPSIS
    array([[  0.0000000...e+00,   0.0000000...e+00,   0.0000000...e+00],
           [  9.6361381...e-05,   2.9056776...e-06,   4.4961226...e-04],
           [  2.5910529...e-01,   2.1031298...e-02,   1.3207468...e+00],
           [  1.0561021...e-01,   6.2038243...e-01,   3.5423571...e-02],
           [  7.2647980...e-01,   3.5460869...e-01,   2.1005149...e-04],
           [  1.0971874...e-02,   3.9635453...e-03,   0.0000000...e+00],
           [  3.0792572...e-05,   1.1119762...e-05,   0.0000000...e+00],
           [  2.5920165...e-01,   2.1034203...e-02,   1.3211965...e+00],
           [  3.6471551...e-01,   6.4141373...e-01,   1.3561704...e+00],
           [  8.3209002...e-01,   9.7499113...e-01,   3.5633622...e-02],
           [  7.3745167...e-01,   3.5857224...e-01,   2.1005149...e-04],
           [  1.1002667...e-02,   3.9746651...e-03,   0.0000000...e+00],
           [  1.2715395...e-04,   1.4025439...e-05,   4.4961226...e-04],
           [  3.6481187...e-01,   6.4141663...e-01,   1.3566200...e+00],
           [  1.0911953...e+00,   9.9602242...e-01,   1.3563805...e+00],
           [  8.4306189...e-01,   9.7895467...e-01,   3.5633622...e-02],
           [  7.3748247...e-01,   3.5858336...e-01,   2.1005149...e-04],
           [  1.1099028...e-02,   3.9775708...e-03,   4.4961226...e-04],
           [  2.5923244...e-01,   2.1045323...e-02,   1.3211965...e+00],
           [  1.0912916...e+00,   9.9602533...e-01,   1.3568301...e+00],
           [  1.1021671...e+00,   9.9998597...e-01,   1.3563805...e+00],
           [  8.4309268...e-01,   9.7896579...e-01,   3.5633622...e-02],
           [  7.3757883...e-01,   3.5858626...e-01,   6.5966375...e-04],
           [  2.7020432...e-01,   2.5008868...e-02,   1.3211965...e+00],
           [  3.6484266...e-01,   6.4142775...e-01,   1.3566200...e+00],
           [  1.1022635...e+00,   9.9998888...e-01,   1.3568301...e+00],
           [  1.1021979...e+00,   9.9999709...e-01,   1.3563805...e+00],
           [  8.4318905...e-01,   9.7896870...e-01,   3.6083235...e-02],
           [  9.9668412...e-01,   3.7961756...e-01,   1.3214065...e+00],
           [  3.7581454...e-01,   6.4539130...e-01,   1.3566200...e+00],
           [  1.0913224...e+00,   9.9603645...e-01,   1.3568301...e+00],
           [  1.1022943...e+00,   1.0000000...e+00,   1.3568301...e+00]])
    """

    cmfs, illuminant = handle_spectral_arguments(
        cmfs,
        illuminant,
        "CIE 1931 2 Degree Standard Observer",
        "E",
        SPECTRAL_SHAPE_OUTER_SURFACE_XYZ,
    )

    settings = dict(kwargs)
    settings.update({"shape": cmfs.shape})

    key = (
        hash(cmfs),
        hash(illuminant),
        point_order,
        filter_jagged_points,
        str(settings),
    )
    XYZ = _CACHE_OUTER_SURFACE_XYZ.get(key)

    if XYZ is None:
        pulse_waves = generate_pulse_waves(len(cmfs.wavelengths), point_order,
                                           filter_jagged_points)
        XYZ = (msds_to_XYZ(
            pulse_waves, cmfs, illuminant, method="Integration", **settings) /
               100)

        _CACHE_OUTER_SURFACE_XYZ[key] = XYZ

    return XYZ
Example #16
0
def matrix_idt(
    sensitivities: RGB_CameraSensitivities,
    illuminant: SpectralDistribution,
    training_data: Optional[MultiSpectralDistributions] = None,
    cmfs: Optional[MultiSpectralDistributions] = None,
    optimisation_factory: Callable = optimisation_factory_rawtoaces_v1,
    optimisation_kwargs: Optional[Dict] = None,
    chromatic_adaptation_transform: Union[Literal["Bianco 2010",
                                                  "Bianco PC 2010", "Bradford",
                                                  "CAT02 Brill 2008", "CAT02",
                                                  "CAT16", "CMCCAT2000",
                                                  "CMCCAT97", "Fairchild",
                                                  "Sharp", "Von Kries",
                                                  "XYZ Scaling", ],
                                          str, ] = "CAT02",
    additional_data: Boolean = False,
) -> Union[Tuple[NDArray, NDArray, NDArray, NDArray], Tuple[NDArray, NDArray]]:
    """
    Compute an *Input Device Transform* (IDT) matrix for given camera *RGB*
    spectral sensitivities, illuminant, training data, standard observer colour
    matching functions and optimization 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 *Input Device Transform* (IDT) matrix and white balance
        multipliers or tuple of *Input Device Transform* (IDT) matrix, white
        balance multipliers, *XYZ* and *RGB* tristimulus values.

    References
    ----------
    :cite:`Dyer2017`, :cite:`TheAcademyofMotionPictureArtsandSciences2015c`

    Examples
    --------
    Computing the *Input Device Transform* (IDT) matrix for a
    *CANON EOS 5DMark II* and *CIE Illuminant D Series* *D55* using
    the method given in *RAW to ACES* v1:

    >>> path = os.path.join(
    ...     RESOURCES_DIRECTORY_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.85 , -0.016,  0.151],
           [ 0.051,  1.126, -0.185],
           [ 0.02 , -0.194,  1.162]])
    >>> 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.848, -0.016,  0.158],
           [ 0.053,  1.114, -0.175],
           [ 0.023, -0.225,  1.196]])
    >>> RGB_w  # doctest: +ELLIPSIS
    array([ 2.3414154...,  1.        ,  1.5163375...])
    """

    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}".'
        )
        # pylint: disable=E1102
        sensitivities = reshape_msds(sensitivities,
                                     shape)  # type: ignore[assignment]

    if training_data.shape != shape:
        runtime_warning(
            f'Aligning "{training_data.name}" training data shape to "{shape}".'
        )
        # pylint: disable=E1102
        training_data = reshape_msds(training_data, shape)

    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)

    (
        objective_function,
        XYZ_to_optimization_colour_model,
    ) = optimisation_factory()
    optimisation_settings = {
        "method": "BFGS",
        "jac": "2-point",
    }
    if optimisation_kwargs is not None:
        optimisation_settings.update(optimisation_kwargs)

    M = minimize(
        objective_function,
        np.ravel(np.identity(3)),
        (RGB, XYZ_to_optimization_colour_model(XYZ)),
        **optimisation_settings,
    ).x.reshape([3, 3])

    if additional_data:
        return M, RGB_w, XYZ, RGB
    else:
        return M, RGB_w