Source code for colour.difference.metamerism_index
"""
:math:`M_{t}` - Metamerism Index
================================
Define the :math:`M_{t}` *metamerism index* computation objects:
- :func:`colour.difference.Lab_to_metamerism_index`
- :func:`colour.difference.XYZ_to_metamerism_index`
References
----------
- :cite:`InternationalOrganizationforStandardization2024` : International
Organization for Standardization. (2024). INTERNATIONAL STANDARD ISO
18314-4 - Analytical colorimetry Part 4: Metamerism index for pairs of
samples for change of illuminant. https://www.iso.org/standard/85116.html
"""
from __future__ import annotations
import typing
import numpy as np
if typing.TYPE_CHECKING:
from colour.hints import (
Any,
Domain1,
Domain100,
Literal,
LiteralDeltaEMethod,
NDArrayFloat,
)
from colour.difference import DeltaE_Specification
import colour
from colour.colorimetry import (
MultiSpectralDistributions,
SpectralDistribution,
tristimulus_weighting_factors_integration,
)
from colour.constants import TOLERANCE_ABSOLUTE_TESTS
from colour.models import XYZ_to_Lab
from colour.utilities import (
as_array,
attest,
domain_range_scale,
filter_kwargs,
validate_method,
)
__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__ = [
"Lab_to_metamerism_index",
"XYZ_to_metamerism_index",
"sd_to_metamerism_index",
]
@typing.overload
def Lab_to_metamerism_index(
Lab_spl_t: Domain100,
Lab_std_t: Domain100,
Lab_spl_r: Domain100,
Lab_std_r: Domain100,
correction: str = ...,
method: LiteralDeltaEMethod | str = ...,
*,
additional_data: Literal[False] = False,
**kwargs: Any,
) -> NDArrayFloat: ...
@typing.overload
def Lab_to_metamerism_index(
Lab_spl_t: Domain100,
Lab_std_t: Domain100,
Lab_spl_r: Domain100,
Lab_std_r: Domain100,
correction: str = ...,
method: LiteralDeltaEMethod | str = ...,
*,
additional_data: Literal[True],
**kwargs: Any,
) -> DeltaE_Specification: ...
[docs]
def Lab_to_metamerism_index(
Lab_spl_t: Domain100,
Lab_std_t: Domain100,
Lab_spl_r: Domain100,
Lab_std_r: Domain100,
correction: Literal["Additive", "Multiplicative"] | str = "Additive",
method: LiteralDeltaEMethod | str = "CIE 2000",
additional_data: bool = False,
**kwargs: Any,
) -> NDArrayFloat | DeltaE_Specification:
"""
Compute the *metamerism index* :math:`M_{t}` between four specified
*CIE L\\*a\\*b\\** colourspace arrays.
Before computing the *metamerism index*, apply either an additive or
multiplicative correction. The correction is based on the difference
between the colour sample and colour standard under the reference
illuminant and is applied to the colour sample under the test illuminant.
The correction is applied in *CIE L\\*a\\*b\\** colourspace, which is then
used to compute the *metamerism index*.
:cite:`InternationalOrganizationforStandardization2024` recommends using
additive correction in *CIE L\\*a\\*b\\**.
Parameters
----------
Lab_spl_t
*CIE L\\*a\\*b\\** colourspace array of the colour sample under the test
illuminant.
Lab_std_t
*CIE L\\*a\\*b\\** colourspace array of the colour standard under the
test illuminant.
Lab_spl_r
*CIE L\\*a\\*b\\** colourspace array of the colour sample under the
reference illuminant.
Lab_std_r
*CIE L\\*a\\*b\\** colourspace array of the colour standard under the
reference illuminant.
correction
Correction method to apply, either ``'Additive'`` or
``'Multiplicative'``.
method
Colour-difference method.
additional_data
Whether to output additional data.
Other Parameters
----------------
c
{:func:`colour.difference.delta_E_CMC`},
*Chroma* weighting factor.
l
{:func:`colour.difference.delta_E_CMC`},
*Lightness* weighting factor.
textiles
{:func:`colour.difference.delta_E_CIE1994`,
:func:`colour.difference.delta_E_CIE2000`,
:func:`colour.difference.delta_E_DIN99`},
Textiles application specific parametric factors
:math:`k_L=2,\\ k_C=k_H=1,\\ k_1=0.048,\\ k_2=0.014,\\ k_E=2,\\ k_{CH}=0.5`
weights are used instead of
:math:`k_L=k_C=k_H=1,\\ k_1=0.045,\\ k_2=0.015,\\ k_E=k_{CH}=1.0`.
Returns
-------
:class:`numpy.ndarray` or :class:`DeltaE_Specification`
*Metamerism index* :math:`M_{t}`.
Notes
-----
+----------------+-----------------------+-------------------+
| **Domain** | **Scale - Reference** | **Scale - 1** |
+================+=======================+===================+
| ``Lab_spl_t`` | 100 | 1 |
+----------------+-----------------------+-------------------+
| ``Lab_std_t`` | 100 | 1 |
+----------------+-----------------------+-------------------+
| ``Lab_spl_r`` | 100 | 1 |
+----------------+-----------------------+-------------------+
| ``Lab_std_r`` | 100 | 1 |
+----------------+-----------------------+-------------------+
References
----------
:cite:`InternationalOrganizationforStandardization2024`
Examples
--------
>>> import numpy as np
>>> Lab_std_r = np.array([39.0908, -21.3269, 22.6657])
>>> Lab_std_t = np.array([38.17781, -17.4939, 21.0618])
>>> Lab_spl_r = np.array([38.83253, -19.8787, 20.0453])
>>> Lab_spl_t = np.array([37.9013, -19.56327, 16.9346])
>>> Lab_to_metamerism_index(
... Lab_spl_t,
... Lab_std_t,
... Lab_spl_r,
... Lab_std_r,
... correction="Additive",
... method="CIE 1976",
... ) # doctest: +ELLIPSIS
np.float64(3.8267581...)
>>> Lab_to_metamerism_index(
... Lab_spl_t,
... Lab_std_t,
... Lab_spl_r,
... Lab_std_r,
... correction="Multiplicative",
... method="CIE 1976",
... ) # doctest: +ELLIPSIS
np.float64(3.9842216...)
"""
correction = validate_method(correction, ("Additive", "Multiplicative"))
if correction == "additive":
Lab_corr_t = as_array(Lab_spl_t) - (as_array(Lab_spl_r) - as_array(Lab_std_r))
elif correction == "multiplicative":
Lab_corr_t = as_array(Lab_spl_t) * (as_array(Lab_std_r) / as_array(Lab_spl_r))
return colour.difference.delta_E(
Lab_std_t,
Lab_corr_t,
method=method,
additional_data=additional_data,
**kwargs,
)
@typing.overload
def XYZ_to_metamerism_index(
XYZ_spl_t: Domain1,
XYZ_std_t: Domain1,
XYZ_spl_r: Domain1,
XYZ_std_r: Domain1,
correction: str = ...,
method: LiteralDeltaEMethod | str = ...,
*,
additional_data: Literal[False] = False,
**kwargs: Any,
) -> NDArrayFloat: ...
@typing.overload
def XYZ_to_metamerism_index(
XYZ_spl_t: Domain1,
XYZ_std_t: Domain1,
XYZ_spl_r: Domain1,
XYZ_std_r: Domain1,
correction: str = ...,
method: LiteralDeltaEMethod | str = ...,
*,
additional_data: Literal[True],
**kwargs: Any,
) -> DeltaE_Specification: ...
[docs]
def XYZ_to_metamerism_index(
XYZ_spl_t: Domain1,
XYZ_std_t: Domain1,
XYZ_spl_r: Domain1,
XYZ_std_r: Domain1,
correction: Literal["Additive", "Multiplicative"] | str = "Multiplicative",
method: LiteralDeltaEMethod | str = "CIE 2000",
additional_data: bool = False,
**kwargs: Any,
) -> NDArrayFloat | DeltaE_Specification:
"""
Compute the *metamerism index* :math:`M_{t}` from four specified
*CIE XYZ* colourspace arrays.
Before computing the *metamerism index*, apply either an additive or
multiplicative correction. The correction is based on the difference
between the colour sample and colour standard under the reference
illuminant and is applied to the colour sample under the test illuminant.
The correction is applied in *CIE XYZ* colourspace. Afterwards, convert
to *CIE L\\*a\\*b\\** colourspace to compute the *metamerism index*.
:cite:`InternationalOrganizationforStandardization2024` recommends using
multiplicative correction in *CIE XYZ*.
Parameters
----------
XYZ_spl_t
*CIE XYZ* tristimulus array of the colour sample under the test
illuminant.
XYZ_std_t
*CIE XYZ* tristimulus array of the colour standard under the test
illuminant.
XYZ_spl_r
*CIE XYZ* tristimulus array of the colour sample under the reference
illuminant.
XYZ_std_r
*CIE XYZ* tristimulus array of the colour standard under the reference
illuminant.
correction
Correction method to apply, either ``'Additive'`` or
``'Multiplicative'``.
method
Colour-difference method.
additional_data
Whether to output additional data.
Other Parameters
----------------
illuminant
{:func:`colour.models.XYZ_to_Lab`},
Test *illuminant* *CIE xy* chromaticity coordinates or *CIE xyY*
colourspace array for conversion from *CIE XYZ* to *CIE L\\*a\\*b\\**.
c
{:func:`colour.difference.delta_E_CMC`},
*Chroma* weighting factor.
l
{:func:`colour.difference.delta_E_CMC`},
*Lightness* weighting factor.
textiles
{:func:`colour.difference.delta_E_CIE1994`,
:func:`colour.difference.delta_E_CIE2000`,
:func:`colour.difference.delta_E_DIN99`},
Textiles application specific parametric factors
:math:`k_L=2,\\ k_C=k_H=1,\\ k_1=0.048,\\ k_2=0.014,\\ k_E=2,\\ k_{CH}=0.5`
weights are used instead of
:math:`k_L=k_C=k_H=1,\\ k_1=0.045,\\ k_2=0.015,\\ k_E=k_{CH}=1.0`.
Returns
-------
:class:`numpy.ndarray` or :class:`DeltaE_Specification`
*Metamerism index* :math:`M_{t}`.
Notes
-----
+----------------+-----------------------+-------------------+
| **Domain** | **Scale - Reference** | **Scale - 1** |
+================+=======================+===================+
| ``XYZ_spl_t`` | 1 | 1 |
+----------------+-----------------------+-------------------+
| ``XYZ_std_t`` | 1 | 1 |
+----------------+-----------------------+-------------------+
| ``XYZ_spl_r`` | 1 | 1 |
+----------------+-----------------------+-------------------+
| ``XYZ_std_r`` | 1 | 1 |
+----------------+-----------------------+-------------------+
References
----------
:cite:`InternationalOrganizationforStandardization2024`
Examples
--------
>>> import numpy as np
>>> from colour import CCS_ILLUMINANTS
>>> XYZ_std_r = np.array([7.6576, 10.7116, 5.0731]) / 100
>>> XYZ_std_t = np.array([8.96442, 10.1878, 1.6663]) / 100
>>> XYZ_spl_r = np.array([7.6933, 10.5616, 5.54474]) / 100
>>> XYZ_spl_t = np.array([8.56438, 10.0324, 1.9315]) / 100
>>> XYZ_to_metamerism_index(
... XYZ_spl_t,
... XYZ_std_t,
... XYZ_spl_r,
... XYZ_std_r,
... correction="multiplicative",
... method="CIE 1976",
... illuminant=CCS_ILLUMINANTS["CIE 1964 10 Degree Standard Observer"]["A"],
... ) # doctest: +ELLIPSIS
np.float64(3.7906989...)
>>> XYZ_to_metamerism_index(
... XYZ_spl_t,
... XYZ_std_t,
... XYZ_spl_r,
... XYZ_std_r,
... correction="additive",
... method="CIE 1976",
... illuminant=CCS_ILLUMINANTS["CIE 1964 10 Degree Standard Observer"]["A"],
... ) # doctest: +ELLIPSIS
np.float64(4.6910648...)
"""
correction = validate_method(correction, ("Additive", "Multiplicative"))
if correction == "additive":
XYZ_corr_t = as_array(XYZ_spl_t) - (as_array(XYZ_spl_r) - as_array(XYZ_std_r))
elif correction == "multiplicative":
XYZ_corr_t = as_array(XYZ_spl_t) * (as_array(XYZ_std_r) / as_array(XYZ_spl_r))
Lab_std_t = XYZ_to_Lab(XYZ_std_t, **filter_kwargs(XYZ_to_Lab, **kwargs))
Lab_corr_t = XYZ_to_Lab(XYZ_corr_t, **filter_kwargs(XYZ_to_Lab, **kwargs))
return colour.difference.delta_E(
Lab_std_t,
Lab_corr_t,
method=method,
additional_data=additional_data,
**kwargs,
)
[docs]
def sd_to_metamerism_index(
sd_spl: SpectralDistribution | MultiSpectralDistributions,
sd_std: SpectralDistribution | MultiSpectralDistributions,
cmfs: MultiSpectralDistributions,
illuminant_r: SpectralDistribution,
illuminant_t: SpectralDistribution,
method: LiteralDeltaEMethod | str = "CIE 2000",
**kwargs: Any,
) -> NDArrayFloat:
"""
Compute the *metamerism index* :math:`M_t` of a sample pair for change of
illuminant using the spectral correction method as defined in
*ISO 18314-4:2024*.
The spectral correction method (*Cohen-Kappauf* matrix decomposition)
computes a corrected spectral distribution :math:`\\bar{N}_{spl,corr}` such
that the sample and standard have identical tristimulus values under the
reference illuminant. The *metamerism index* is then calculated as the
colour difference between the standard and corrected sample under the test
illuminant.
The projection matrix :math:`R` is computed from the weighting matrix
:math:`A` as:
:math:`R = A \\cdot (A^T \\cdot A)^{-1} \\cdot A^T`
The corrected spectral distribution is:
:math:`\\bar{N}_{spl,corr} = R \\cdot \\bar{N}_{std} + (I - R) \\cdot \
\\bar{N}_{spl}`
Parameters
----------
sd_spl
Spectral distribution of the sample :math:`\\bar{N}_{spl}`.
sd_std
Spectral distribution of the standard :math:`\\bar{N}_{std}`.
cmfs
Standard observer colour matching functions.
illuminant_r
Spectral distribution of the reference illuminant.
illuminant_t
Spectral distribution of the test illuminant.
method
Colour difference formula.
Other Parameters
----------------
illuminant
{:func:`colour.models.XYZ_to_Lab`},
Test *illuminant* *CIE xy* chromaticity coordinates or *CIE xyY*
colourspace array for conversion from *CIE XYZ* to *CIE L\\*a\\*b\\**.
c
{:func:`colour.difference.delta_E_CMC`},
*Chroma* weighting factor.
l
{:func:`colour.difference.delta_E_CMC`},
*Lightness* weighting factor.
textiles
{:func:`colour.difference.delta_E_CIE1994`,
:func:`colour.difference.delta_E_CIE2000`,
:func:`colour.difference.delta_E_DIN99`},
Textiles application specific parametric factors
:math:`k_L=2,\\ k_C=k_H=1,\\ k_1=0.048,\\ k_2=0.014,\\ k_E=2,\\ k_{CH}=0.5`
weights are used instead of
:math:`k_L=k_C=k_H=1,\\ k_1=0.045,\\ k_2=0.015,\\ k_E=k_{CH}=1.0`.
Returns
-------
:class:`numpy.ndarray`
*Metamerism index* :math:`M_t`.
Notes
-----
- This method implements *ISO 18314-4:2024*, Section 8.3.3,
*Spectral correction (Cohen-Kappauf matrix R)*.
- The spectral correction ensures that
:math:`\\bar{W}_{std} = \\bar{W}_{spl,corr}` under the reference
illuminant, i.e., the colour difference is zero.
References
----------
:cite:`InternationalOrganizationforStandardization2024`
Examples
--------
>>> import numpy as np
>>> from colour import MSDS_CMFS, SDS_ILLUMINANTS, CCS_ILLUMINANTS
>>> from colour.colorimetry import SpectralShape
>>> shape = SpectralShape(400, 700, 10)
>>> N_spl = np.array(
... [
... 0.0379,
... 0.0403,
... 0.0415,
... 0.0427,
... 0.045,
... 0.0483,
... 0.0521,
... 0.0572,
... 0.0624,
... 0.0673,
... 0.0777,
... 0.1026,
... 0.1307,
... 0.145,
... 0.1484,
... 0.1455,
... 0.1375,
... 0.1254,
... 0.1099,
... 0.0908,
... 0.0698,
... 0.0526,
... 0.0423,
... 0.0368,
... 0.0331,
... 0.0306,
... 0.0297,
... 0.0311,
... 0.034,
... 0.038,
... 0.0421,
... ]
... )
>>> N_std = np.array(
... [
... 0.099,
... 0.1244,
... 0.0933,
... 0.0596,
... 0.0405,
... 0.0322,
... 0.0299,
... 0.0316,
... 0.0377,
... 0.0507,
... 0.0681,
... 0.0968,
... 0.1522,
... 0.2014,
... 0.1991,
... 0.159,
... 0.1162,
... 0.0843,
... 0.0655,
... 0.057,
... 0.0553,
... 0.0582,
... 0.0638,
... 0.0716,
... 0.0818,
... 0.0959,
... 0.1131,
... 0.1317,
... 0.149,
... 0.1656,
... 0.1832,
... ]
... )
>>> N_spl = SpectralDistribution(N_spl, shape)
>>> N_std = SpectralDistribution(N_std, shape)
>>> cmfs = MSDS_CMFS["CIE 1964 10 Degree Standard Observer"]
>>> r = SDS_ILLUMINANTS["D65"]
>>> t = SDS_ILLUMINANTS["A"]
>>> sd_to_metamerism_index(
... N_spl,
... N_std,
... cmfs,
... r,
... t,
... method="CIE 1976",
... illuminant=colour.CCS_ILLUMINANTS["CIE 1964 10 Degree Standard Observer"][
... "A"
... ],
... ) # doctest: +ELLIPSIS
np.float64(3.4766679...)
"""
attest(
sd_spl.shape == sd_std.shape,
"`sd_spl` and `sd_std` spectral distributions must have the same shape!",
)
shape = sd_spl.shape
A_r = tristimulus_weighting_factors_integration(cmfs, illuminant_r, shape=shape)
A_t = tristimulus_weighting_factors_integration(cmfs, illuminant_t, shape=shape)
R = np.dot(
np.dot(A_r, np.linalg.inv(np.dot(np.transpose(A_r), A_r))), np.transpose(A_r)
)
sd_spl_corr = np.dot(R, sd_std.values) + np.dot(
np.identity(R.shape[0]) - R, sd_spl.values
)
sd_spl_corr = SpectralDistribution(sd_spl_corr, shape)
XYZ_spl_corr_t = np.dot(sd_spl_corr.values, A_t) / 100
XYZ_std_t = np.dot(sd_std.values, A_t) / 100
XYZ_spl_corr = np.dot(sd_spl_corr.values, A_r) / 100
XYZ_std = np.dot(sd_std.values, A_r) / 100
attest(
np.allclose(XYZ_std, XYZ_spl_corr, atol=TOLERANCE_ABSOLUTE_TESTS),
"The corrected sample under reference illuminant must be equal "
"to the standard under reference illuminant!",
)
with domain_range_scale("ignore"):
Lab_std_t = XYZ_to_Lab(XYZ_std_t, **filter_kwargs(XYZ_to_Lab, **kwargs))
Lab_spl_corr_t = XYZ_to_Lab(
XYZ_spl_corr_t, **filter_kwargs(XYZ_to_Lab, **kwargs)
)
return colour.difference.delta_E(
Lab_std_t,
Lab_spl_corr_t,
method=method,
**kwargs,
)