"""
:math:`\\Delta E^*_{ab}` - Delta E Colour Difference
====================================================
Define the :math:`\\Delta E^*_{ab}` colour difference computation objects.
- :attr:`colour.difference.JND_CIE1976`
- :func:`colour.difference.delta_E_CIE1976`
- :func:`colour.difference.delta_E_CIE1994`
- :func:`colour.difference.delta_E_CIE2000`
- :func:`colour.difference.delta_E_CMC`
- :func:`colour.difference.delta_E_ITP`
- :func:`colour.difference.delta_E_HyAB`
- :func:`colour.difference.delta_E_HyCH`
References
----------
- :cite:`Abasi2020a` : Abasi, S., Amani Tehran, M., & Fairchild, M. D. (2020).
Distance metrics for very large color differences. Color Research &
Application, 45(2), 208-223. doi:10.1002/col.22451
- :cite:`InternationalTelecommunicationUnion2019` : International
Telecommunication Union. (2019). Recommendation ITU-R BT.2124-0 -
Objective metric for the assessment of the potential visibility of colour
differences in television (pp. 1-36).
https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.2124-0-201901-I!!PDF-E.pdf
- :cite:`Lindbloom2003c` : Lindbloom, B. (2003). Delta E (CIE 1976).
Retrieved February 24, 2014, from
http://brucelindbloom.com/Eqn_DeltaE_CIE76.html
- :cite:`Lindbloom2009f` : Lindbloom, B. (2009). Delta E (CMC). Retrieved
February 24, 2014, from http://brucelindbloom.com/Eqn_DeltaE_CMC.html
- :cite:`Lindbloom2011a` : Lindbloom, B. (2011). Delta E (CIE 1994).
Retrieved February 24, 2014, from
http://brucelindbloom.com/Eqn_DeltaE_CIE94.html
- :cite:`Melgosa2013b` : Melgosa, M. (2013). CIE / ISO new standard:
CIEDE2000. http://www.color.org/events/colorimetry/\
Melgosa_CIEDE2000_Workshop-July4.pdf
- :cite:`Sharma2005b` : Sharma, G., Wu, W., & Dalal, E. N. (2005). The
CIEDE2000 color-difference formula: Implementation notes, supplementary
test data, and mathematical observations. Color Research & Application,
30(1), 21-30. doi:10.1002/col.20070
- :cite:`Mokrzycki2011` : Mokrzycki, W., & Tatol, M. (2011). Color difference
Delta E - A survey. Machine Graphics and Vision, 20, 383-411.
"""
from __future__ import annotations
import typing
from dataclasses import astuple, dataclass, field
import numpy as np
if typing.TYPE_CHECKING:
from colour.hints import (
Domain1,
Domain100,
Literal,
NDArrayFloat,
)
from colour.algebra import euclidean_distance
from colour.utilities import (
MixinDataclassArithmetic,
as_float,
as_float_array,
to_domain_100,
tsplit,
)
from colour.utilities.documentation import DocstringFloat, is_documentation_building
__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__ = [
"JND_CIE1976",
"DeltaE_Specification_CIE1976",
"delta_E_CIE1976",
"DeltaE_Specification_CIE1994",
"delta_E_CIE1994",
"intermediate_attributes_CIE2000",
"DeltaE_Specification_CIE2000",
"delta_E_CIE2000",
"DeltaE_Specification_CMC",
"delta_E_CMC",
"DeltaE_Specification_ITP",
"delta_E_ITP",
"DeltaE_Specification_HyAB",
"delta_E_HyAB",
"DeltaE_Specification_HyCH",
"delta_E_HyCH",
]
JND_CIE1976 = 2.3
if is_documentation_building(): # pragma: no cover
JND_CIE1976 = DocstringFloat(JND_CIE1976)
JND_CIE1976.__doc__ = """
Just Noticeable Difference (JND) according to *CIE 1976* colour difference
formula, i.e., Euclidean distance in *CIE L\\*a\\*b\\** colourspace.
Notes
-----
A standard observer sees the difference in colour as follows:
- 0 < :math:`\\Delta E^*_{ab}` < 1 : Observer does not notice the difference.
- 1 < :math:`\\Delta E^*_{ab}` < 2 : Only experienced observer can notice the
difference.
- 2 < :math:`\\Delta E^*_{ab}` < 3:5 : Unexperienced observer also notices
the difference.
- 3:5 < :math:`\\Delta E^*_{ab}` < 5 : Clear difference in colour is noticed.
- 5 < :math:`\\Delta E^*_{ab}` : Observer notices two different colours.
References
----------
:cite:`Mokrzycki2011`
"""
@dataclass
class DeltaE_Specification_CIE1976(MixinDataclassArithmetic):
"""
Define the *CIE 1976* colour difference specification.
This data structure is returned by
:func:`colour.difference.delta_E_CIE1976` when ``additional_data=True``.
Parameters
----------
dE
Colour difference :math:`\\Delta E_{76}`.
dL
Raw *lightness* difference :math:`\\Delta L^*`.
da
Raw :math:`\\Delta a^*` difference.
db
Raw :math:`\\Delta b^*` difference.
"""
dE: NDArrayFloat | None = field(default_factory=lambda: None)
dL: NDArrayFloat | None = field(default_factory=lambda: None)
da: NDArrayFloat | None = field(default_factory=lambda: None)
db: NDArrayFloat | None = field(default_factory=lambda: None)
@typing.overload
def delta_E_CIE1976(
Lab_1: Domain100,
Lab_2: Domain100,
*,
additional_data: Literal[False] = False,
) -> NDArrayFloat: ...
@typing.overload
def delta_E_CIE1976(
Lab_1: Domain100,
Lab_2: Domain100,
*,
additional_data: Literal[True],
) -> DeltaE_Specification_CIE1976: ...
[docs]
def delta_E_CIE1976(
Lab_1: Domain100,
Lab_2: Domain100,
additional_data: bool = False,
) -> NDArrayFloat | DeltaE_Specification_CIE1976:
"""
Compute the colour difference :math:`\\Delta E_{76}` between two
specified *CIE L\\*a\\*b\\** colourspace arrays using the *CIE 1976*
recommendation.
Parameters
----------
Lab_1
*CIE L\\*a\\*b\\** colourspace array 1.
Lab_2
*CIE L\\*a\\*b\\** colourspace array 2.
additional_data
Whether to output additional data.
Returns
-------
:class:`numpy.ndarray` or :class:`DeltaE_Specification_CIE1976`
Colour difference :math:`\\Delta E_{76}`.
Notes
-----
+------------+-----------------------+-------------------+
| **Domain** | **Scale - Reference** | **Scale - 1** |
+============+=======================+===================+
| ``Lab_1`` | 100 | 1 |
+------------+-----------------------+-------------------+
| ``Lab_2`` | 100 | 1 |
+------------+-----------------------+-------------------+
References
----------
:cite:`Lindbloom2003c`
Examples
--------
>>> Lab_1 = np.array([48.99183622, -0.10561667, 400.65619925])
>>> Lab_2 = np.array([50.65907324, -0.11671910, 402.82235718])
>>> delta_E_CIE1976(Lab_1, Lab_2) # doctest: +ELLIPSIS
np.float64(2.7335037...)
>>> delta_E_CIE1976(
... Lab_1,
... Lab_2,
... additional_data=True,
... ) # doctest: +ELLIPSIS
DeltaE_Specification_CIE1976(dE=np.float64(2.7335037...), \
dL=np.float64(-1.6672370...), da=np.float64(0.0111024...), \
db=np.float64(-2.1661579...))
"""
Lab_1 = to_domain_100(Lab_1)
Lab_2 = to_domain_100(Lab_2)
dE = euclidean_distance(Lab_1, Lab_2)
if not additional_data:
return dE
dLab = as_float_array(Lab_1) - as_float_array(Lab_2)
return DeltaE_Specification_CIE1976(
dE,
as_float(dLab[..., 0]),
as_float(dLab[..., 1]),
as_float(dLab[..., 2]),
)
@dataclass
class DeltaE_Specification_CIE1994(MixinDataclassArithmetic):
"""
Define the *CIE 1994* colour difference specification.
This data structure is returned by
:func:`colour.difference.delta_E_CIE1994` when ``additional_data=True``.
Parameters
----------
dE
Colour difference :math:`\\Delta E_{94}`.
dL
Weighted *lightness* difference :math:`\\Delta L^* / (k_L S_L)`.
dC
Weighted *chroma* difference :math:`\\Delta C^*_{ab} / (k_C S_C)`.
dH
Weighted *hue* difference :math:`\\Delta H^*_{ab} / (k_H S_H)`.
Notes
-----
- Enabling the ``textiles`` parameter modifies the parametric weighting
factors and therefore directly affects the returned component values.
"""
dE: NDArrayFloat | None = field(default_factory=lambda: None)
dL: NDArrayFloat | None = field(default_factory=lambda: None)
dC: NDArrayFloat | None = field(default_factory=lambda: None)
dH: NDArrayFloat | None = field(default_factory=lambda: None)
@typing.overload
def delta_E_CIE1994(
Lab_1: Domain100,
Lab_2: Domain100,
textiles: bool = ...,
*,
additional_data: Literal[False] = False,
) -> NDArrayFloat: ...
@typing.overload
def delta_E_CIE1994(
Lab_1: Domain100,
Lab_2: Domain100,
textiles: bool = ...,
*,
additional_data: Literal[True],
) -> DeltaE_Specification_CIE1994: ...
[docs]
def delta_E_CIE1994(
Lab_1: Domain100,
Lab_2: Domain100,
textiles: bool = False,
additional_data: bool = False,
) -> NDArrayFloat | DeltaE_Specification_CIE1994:
"""
Compute the colour difference :math:`\\Delta E_{94}` between two specified
*CIE L\\*a\\*b\\** colourspace arrays using the *CIE 1994* recommendation.
Parameters
----------
Lab_1
*CIE L\\*a\\*b\\** colourspace array 1.
Lab_2
*CIE L\\*a\\*b\\** colourspace array 2.
textiles
Textiles application specific parametric factors,
:math:`k_L=2,\\ k_C=k_H=1,\\ k_1=0.048,\\ k_2=0.014` weights are used
instead of :math:`k_L=k_C=k_H=1,\\ k_1=0.045,\\ k_2=0.015`.
additional_data
Whether to output additional data.
Returns
-------
:class:`numpy.ndarray` or :class:`DeltaE_Specification_CIE1994`
Colour difference :math:`\\Delta E_{94}`.
Notes
-----
+------------+-----------------------+-------------------+
| **Domain** | **Scale - Reference** | **Scale - 1** |
+============+=======================+===================+
| ``Lab_1`` | 100 | 1 |
+------------+-----------------------+-------------------+
| ``Lab_2`` | 100 | 1 |
+------------+-----------------------+-------------------+
- *CIE 1994* colour differences are not symmetrical: difference between
``Lab_1`` and ``Lab_2`` may not be the same as difference between
``Lab_2`` and ``Lab_1`` thus one colour must be understood to be the
reference against which a sample colour is compared.
References
----------
:cite:`Lindbloom2011a`
Examples
--------
>>> Lab_1 = np.array([48.99183622, -0.10561667, 400.65619925])
>>> Lab_2 = np.array([50.65907324, -0.11671910, 402.82235718])
>>> delta_E_CIE1994(Lab_1, Lab_2) # doctest: +ELLIPSIS
np.float64(1.6711191...)
>>> delta_E_CIE1994(
... Lab_1,
... Lab_2,
... additional_data=True,
... ) # doctest: +ELLIPSIS
DeltaE_Specification_CIE1994(dE=np.float64(1.6711191...), \
dL=np.float64(-1.6672370...), dC=np.float64(-0.1138315...), \
dH=np.float64(0.0014983...))
>>> delta_E_CIE1994(Lab_1, Lab_2, textiles=True) # doctest: +ELLIPSIS
np.float64(0.8404677...)
>>> delta_E_CIE1994(
... Lab_1,
... Lab_2,
... textiles=True,
... additional_data=True,
... ) # doctest: +ELLIPSIS
DeltaE_Specification_CIE1994(dE=np.float64(0.8404677...), \
dL=np.float64(-0.8336185...), dC=np.float64(-0.1070687...), \
dH=np.float64(0.0015891...))
"""
L_1, a_1, b_1 = tsplit(to_domain_100(Lab_1))
L_2, a_2, b_2 = tsplit(to_domain_100(Lab_2))
k_1 = 0.048 if textiles else 0.045
k_2 = 0.014 if textiles else 0.015
k_L = 2 if textiles else 1
k_C = 1
k_H = 1
C_1 = np.hypot(a_1, b_1)
C_2 = np.hypot(a_2, b_2)
s_L = 1
s_C = 1 + k_1 * C_1
s_H = 1 + k_2 * C_1
delta_L = L_1 - L_2
delta_C = C_1 - C_2
delta_A = a_1 - a_2
delta_B = b_1 - b_2
radical = delta_A**2 + delta_B**2 - delta_C**2
delta_H = np.where(radical > 0, np.sqrt(np.maximum(radical, 0)), 0)
L = delta_L / (k_L * s_L)
C = delta_C / (k_C * s_C)
H = delta_H / (k_H * s_H)
d_E = as_float(np.sqrt(L**2 + C**2 + H**2))
if not additional_data:
return d_E
return DeltaE_Specification_CIE1994(
d_E,
L,
C,
H,
)
@dataclass
class Attributes_Specification_CIE2000(MixinDataclassArithmetic):
"""
Define the *CIE 2000* colour-difference formula intermediate attributes.
Parameters
----------
S_L
*Lightness* weighting function :math:`S_L`.
S_C
*Chroma* weighting function :math:`S_C`.
S_H
*Hue* weighting function :math:`S_H`.
delta_L_p
Adjusted *lightness* difference :math:`\\Delta L'`.
delta_C_p
Adjusted *chroma* difference :math:`\\Delta C'`.
delta_H_p
Adjusted *hue* difference :math:`\\Delta H'`.
R_T
Rotation term :math:`R_T`.
"""
S_L: float | NDArrayFloat | None = field(default_factory=lambda: None)
S_C: float | NDArrayFloat | None = field(default_factory=lambda: None)
S_H: float | NDArrayFloat | None = field(default_factory=lambda: None)
delta_L_p: float | NDArrayFloat | None = field(default_factory=lambda: None)
delta_C_p: float | NDArrayFloat | None = field(default_factory=lambda: None)
delta_H_p: float | NDArrayFloat | None = field(default_factory=lambda: None)
R_T: float | NDArrayFloat | None = field(default_factory=lambda: None)
def intermediate_attributes_CIE2000(
Lab_1: Domain100, Lab_2: Domain100
) -> Attributes_Specification_CIE2000:
"""
Compute intermediate attributes for CIE 2000 colour difference calculation
between two specified *CIE L\\*a\\*b\\** colourspace arrays.
The intermediate attributes include the lightness, chroma, and hue
weighting functions (S_L, S_C, S_H), as well as the adjusted colour
differences (delta_L_p, delta_C_p, delta_H_p) and the rotation term (R_T)
required for computing :math:`\\Delta E_{00}`.
Parameters
----------
Lab_1
*CIE L\\*a\\*b\\** colourspace array 1.
Lab_2
*CIE L\\*a\\*b\\** colourspace array 2.
Returns
-------
:class:`numpy.ndarray`
Intermediate attributes to compute the difference :math:`\\Delta E_{00}`.
Notes
-----
+------------+-----------------------+-------------------+
| **Domain** | **Scale - Reference** | **Scale - 1** |
+============+=======================+===================+
| ``Lab_1`` | 100 | 1 |
+------------+-----------------------+-------------------+
| ``Lab_2`` | 100 | 1 |
+------------+-----------------------+-------------------+
References
----------
:cite:`Melgosa2013b`, :cite:`Sharma2005b`
Examples
--------
>>> Lab_1 = np.array([48.99183622, -0.10561667, 400.65619925])
>>> Lab_2 = np.array([50.65907324, -0.11671910, 402.82235718])
>>> intermediate_attributes_CIE2000(Lab_1, Lab_2) # doctest: +ELLIPSIS
Attributes_Specification_CIE2000(S_L=np.float64(1.0001021...), \
S_C=np.float64(19.0782682...), S_H=np.float64(4.7226695...), \
delta_L_p=np.float64(1.6672370...), delta_C_p=np.float64(2.1661609...), \
delta_H_p=np.float64(0.0105030...), R_T=np.float64(-3...))
"""
L_1, a_1, b_1 = tsplit(to_domain_100(Lab_1))
L_2, a_2, b_2 = tsplit(to_domain_100(Lab_2))
C_1_ab = np.hypot(a_1, b_1)
C_2_ab = np.hypot(a_2, b_2)
C_bar_ab = (C_1_ab + C_2_ab) / 2
C_bar_ab_7 = C_bar_ab**7
G = 0.5 * (1 - np.sqrt(C_bar_ab_7 / (C_bar_ab_7 + 25**7)))
a_p_1 = (1 + G) * a_1
a_p_2 = (1 + G) * a_2
C_p_1 = np.hypot(a_p_1, b_1)
C_p_2 = np.hypot(a_p_2, b_2)
h_p_1 = np.where(
np.logical_and(b_1 == 0, a_p_1 == 0),
0,
np.degrees(np.arctan2(b_1, a_p_1)) % 360,
)
h_p_2 = np.where(
np.logical_and(b_2 == 0, a_p_2 == 0),
0,
np.degrees(np.arctan2(b_2, a_p_2)) % 360,
)
delta_L_p = L_2 - L_1
delta_C_p = C_p_2 - C_p_1
h_p_2_s_1 = h_p_2 - h_p_1
C_p_1_m_2 = C_p_1 * C_p_2
delta_h_p = np.select(
[
C_p_1_m_2 == 0,
np.fabs(h_p_2_s_1) <= 180,
h_p_2_s_1 > 180,
h_p_2_s_1 < -180,
],
[
0,
h_p_2_s_1,
h_p_2_s_1 - 360,
h_p_2_s_1 + 360,
],
)
delta_H_p = 2 * np.sqrt(C_p_1_m_2) * np.sin(np.deg2rad(delta_h_p / 2))
L_bar_p = (L_1 + L_2) / 2
C_bar_p = (C_p_1 + C_p_2) / 2
a_h_p_1_s_2 = np.fabs(h_p_1 - h_p_2)
h_p_1_a_2 = h_p_1 + h_p_2
h_bar_p = np.select(
[
C_p_1_m_2 == 0,
a_h_p_1_s_2 <= 180,
np.logical_and(a_h_p_1_s_2 > 180, h_p_1_a_2 < 360),
np.logical_and(a_h_p_1_s_2 > 180, h_p_1_a_2 >= 360),
],
[
h_p_1_a_2,
h_p_1_a_2 / 2,
(h_p_1_a_2 + 360) / 2,
(h_p_1_a_2 - 360) / 2,
],
)
T = (
1
- 0.17 * np.cos(np.deg2rad(h_bar_p - 30))
+ 0.24 * np.cos(np.deg2rad(2 * h_bar_p))
+ 0.32 * np.cos(np.deg2rad(3 * h_bar_p + 6))
- 0.20 * np.cos(np.deg2rad(4 * h_bar_p - 63))
)
delta_theta = 30 * np.exp(-(((h_bar_p - 275) / 25) ** 2))
C_bar_p_7 = C_bar_p**7
R_C = 2 * np.sqrt(C_bar_p_7 / (C_bar_p_7 + 25**7))
L_bar_p_2 = (L_bar_p - 50) ** 2
S_L = 1 + ((0.015 * L_bar_p_2) / np.sqrt(20 + L_bar_p_2))
S_C = 1 + 0.045 * C_bar_p
S_H = 1 + 0.015 * C_bar_p * T
R_T = -np.sin(np.deg2rad(2 * delta_theta)) * R_C
return Attributes_Specification_CIE2000(
S_L,
S_C,
S_H,
delta_L_p,
delta_C_p,
delta_H_p,
R_T,
)
@dataclass
class DeltaE_Specification_CIE2000(MixinDataclassArithmetic):
"""
Define the *CIE 2000* colour difference specification.
This data structure is returned by
:func:`colour.difference.delta_E_CIE2000` when ``additional_data=True``.
Parameters
----------
dE
Colour difference :math:`\\Delta E_{00}`.
dL
Weighted *lightness* difference :math:`\\Delta L' / (k_L S_L)`.
dC
Weighted *chroma* difference :math:`\\Delta C' / (k_C S_C)`.
dH
Weighted *hue* difference :math:`\\Delta H' / (k_H S_H)`.
Notes
-----
- Enabling the ``textiles`` parameter modifies the parametric weighting
factors and therefore directly affects the returned component values.
"""
dE: NDArrayFloat | None = field(default_factory=lambda: None)
dL: NDArrayFloat | None = field(default_factory=lambda: None)
dC: NDArrayFloat | None = field(default_factory=lambda: None)
dH: NDArrayFloat | None = field(default_factory=lambda: None)
@typing.overload
def delta_E_CIE2000(
Lab_1: Domain100,
Lab_2: Domain100,
textiles: bool = ...,
*,
additional_data: Literal[False] = False,
) -> NDArrayFloat: ...
@typing.overload
def delta_E_CIE2000(
Lab_1: Domain100,
Lab_2: Domain100,
textiles: bool = ...,
*,
additional_data: Literal[True],
) -> DeltaE_Specification_CIE2000: ...
[docs]
def delta_E_CIE2000(
Lab_1: Domain100,
Lab_2: Domain100,
textiles: bool = False,
additional_data: bool = False,
) -> NDArrayFloat | DeltaE_Specification_CIE2000:
"""
Compute the colour difference :math:`\\Delta E_{00}` between two specified
*CIE L\\*a\\*b\\** colourspace arrays using the *CIE 2000* recommendation.
Parameters
----------
Lab_1
*CIE L\\*a\\*b\\** colourspace array 1.
Lab_2
*CIE L\\*a\\*b\\** colourspace array 2.
textiles
Textiles application specific parametric factors.
:math:`k_L=2,\\ k_C=k_H=1` weights are used instead of
:math:`k_L=k_C=k_H=1`.
additional_data
Whether to output additional data.
Returns
-------
:class:`numpy.ndarray` or :class:`DeltaE_Specification_CIE2000`
Colour difference :math:`\\Delta E_{00}`.
Notes
-----
+------------+-----------------------+-------------------+
| **Domain** | **Scale - Reference** | **Scale - 1** |
+============+=======================+===================+
| ``Lab_1`` | 100 | 1 |
+------------+-----------------------+-------------------+
| ``Lab_2`` | 100 | 1 |
+------------+-----------------------+-------------------+
- Parametric factors :math:`k_L=k_C=k_H=1` weights under
*reference conditions*:
- Illumination: D65 source
- Illuminance: 1000 lx
- Observer: Normal colour vision
- Background field: Uniform, neutral gray with :math:`L^*=50`
- Viewing mode: Object
- Sample size: Greater than 4 degrees
- Sample separation: Direct edge contact
- Sample colour-difference magnitude: Lower than 5.0
:math:`\\Delta E_{00}`
- Sample structure: Homogeneous (without texture)
References
----------
:cite:`Melgosa2013b`, :cite:`Sharma2005b`
Examples
--------
>>> Lab_1 = np.array([48.99183622, -0.10561667, 400.65619925])
>>> Lab_2 = np.array([50.65907324, -0.11671910, 402.82235718])
>>> delta_E_CIE2000(Lab_1, Lab_2) # doctest: +ELLIPSIS
np.float64(1.6709303...)
>>> delta_E_CIE2000(
... Lab_1,
... Lab_2,
... additional_data=True,
... ) # doctest: +ELLIPSIS
DeltaE_Specification_CIE2000(dE=np.float64(1.6709303...), \
dL=np.float64(1.6670667...), dC=np.float64(0.1135407...), \
dH=np.float64(0.0022239...))
>>> delta_E_CIE2000(Lab_1, Lab_2, textiles=True) # doctest: +ELLIPSIS
np.float64(0.8412338...)
>>> delta_E_CIE2000(
... Lab_1,
... Lab_2,
... textiles=True,
... additional_data=True,
... ) # doctest: +ELLIPSIS
DeltaE_Specification_CIE2000(dE=np.float64(0.8412338...), \
dL=np.float64(0.8335333...), dC=np.float64(0.1135407...), \
dH=np.float64(0.0022239...))
"""
S_L, S_C, S_H, delta_L_p, delta_C_p, delta_H_p, R_T = astuple(
intermediate_attributes_CIE2000(Lab_1, Lab_2)
)
k_L = 2 if textiles else 1
k_C = 1
k_H = 1
L = delta_L_p / (k_L * S_L)
C = delta_C_p / (k_C * S_C)
H = delta_H_p / (k_H * S_H)
d_E = as_float(np.sqrt(L**2 + C**2 + H**2 + R_T * C * H))
if not additional_data:
return d_E
return DeltaE_Specification_CIE2000(
d_E,
L,
C,
H,
)
@dataclass
class DeltaE_Specification_CMC(MixinDataclassArithmetic):
"""
Define the *CMC* colour difference specification.
This data structure is returned by
:func:`colour.difference.delta_E_CMC` when ``additional_data=True``.
Parameters
----------
dE
Colour difference :math:`\\Delta E_{CMC}`.
dL
Weighted *lightness* difference :math:`\\Delta L^* / (l S_L)`.
dC
Weighted *chroma* difference :math:`\\Delta C^*_{ab} / (c S_C)`.
dH
Weighted *hue* difference :math:`\\Delta H^*_{ab} / S_H`.
"""
dE: NDArrayFloat | None = field(default_factory=lambda: None)
dL: NDArrayFloat | None = field(default_factory=lambda: None)
dC: NDArrayFloat | None = field(default_factory=lambda: None)
dH: NDArrayFloat | None = field(default_factory=lambda: None)
@typing.overload
def delta_E_CMC(
Lab_1: Domain100,
Lab_2: Domain100,
l: float = ..., # noqa: E741
c: float = ...,
*,
additional_data: Literal[False] = False,
) -> NDArrayFloat: ...
@typing.overload
def delta_E_CMC(
Lab_1: Domain100,
Lab_2: Domain100,
l: float = ..., # noqa: E741
c: float = ...,
*,
additional_data: Literal[True],
) -> DeltaE_Specification_CMC: ...
[docs]
def delta_E_CMC(
Lab_1: Domain100,
Lab_2: Domain100,
l: float = 2, # noqa: E741
c: float = 1,
additional_data: bool = False,
) -> NDArrayFloat | DeltaE_Specification_CMC:
"""
Compute the colour difference :math:`\\Delta E_{CMC}` between two
specified *CIE L\\*a\\*b\\** colourspace arrays using the *Colour
Measurement Committee* recommendation.
The quasimetric has two parameters: *lightness* (l) and *chroma* (c),
allowing users to weight the difference based on the ratio of l:c.
Commonly used values are 2:1 for acceptability and 1:1 for the
threshold of imperceptibility.
Parameters
----------
Lab_1
*CIE L\\*a\\*b\\** colourspace array 1.
Lab_2
*CIE L\\*a\\*b\\** colourspace array 2.
l
*Lightness* weighting factor.
c
*Chroma* weighting factor.
additional_data
Whether to output additional data.
Returns
-------
:class:`numpy.ndarray` or :class:`DeltaE_Specification_CMC`
Colour difference :math:`\\Delta E_{CMC}`.
Notes
-----
+------------+-----------------------+-------------------+
| **Domain** | **Scale - Reference** | **Scale - 1** |
+============+=======================+===================+
| ``Lab_1`` | 100 | 1 |
+------------+-----------------------+-------------------+
| ``Lab_2`` | 100 | 1 |
+------------+-----------------------+-------------------+
References
----------
:cite:`Lindbloom2009f`
Examples
--------
>>> Lab_1 = np.array([48.99183622, -0.10561667, 400.65619925])
>>> Lab_2 = np.array([50.65907324, -0.11671910, 402.82235718])
>>> delta_E_CMC(Lab_1, Lab_2) # doctest: +ELLIPSIS
np.float64(0.8996999...)
>>> delta_E_CMC(
... Lab_1,
... Lab_2,
... additional_data=True,
... ) # doctest: +ELLIPSIS
DeltaE_Specification_CMC(dE=np.float64(0.8996999...), \
dL=np.float64(-0.7743459...), dC=np.float64(-0.4580766...), \
dH=np.float64(0.0037676...))
"""
L_1, a_1, b_1 = tsplit(to_domain_100(Lab_1))
L_2, a_2, b_2 = tsplit(to_domain_100(Lab_2))
C_1 = np.hypot(a_1, b_1)
C_2 = np.hypot(a_2, b_2)
s_L = np.where(L_1 < 16, 0.511, (0.040975 * L_1) / (1 + 0.01765 * L_1))
s_C = 0.0638 * C_1 / (1 + 0.0131 * C_1) + 0.638
h_1 = np.degrees(np.arctan2(b_1, a_1)) % 360
t = np.where(
np.logical_and(h_1 >= 164, h_1 <= 345),
0.56 + np.fabs(0.2 * np.cos(np.deg2rad(h_1 + 168))),
0.36 + np.fabs(0.4 * np.cos(np.deg2rad(h_1 + 35))),
)
C_4 = C_1 * C_1 * C_1 * C_1
f = np.sqrt(C_4 / (C_4 + 1900))
s_h = s_C * (f * t + 1 - f)
delta_L = L_1 - L_2
delta_C = C_1 - C_2
delta_A = a_1 - a_2
delta_B = b_1 - b_2
radical = delta_A**2 + delta_B**2 - delta_C**2
delta_H = np.where(radical > 0, np.sqrt(np.maximum(radical, 0)), 0)
L = delta_L / (l * s_L)
C = delta_C / (c * s_C)
H = delta_H / s_h
d_E = as_float(np.sqrt(L**2 + C**2 + H**2))
if not additional_data:
return d_E
return DeltaE_Specification_CMC(
d_E,
L,
C,
H,
)
@dataclass
class DeltaE_Specification_ITP(MixinDataclassArithmetic):
"""
Define the *ITP* colour difference specification.
This data structure is returned by
:func:`colour.difference.delta_E_ITP` when ``additional_data=True``.
Parameters
----------
dE
Colour difference :math:`\\Delta E_{ITP}`.
dI
*Intensity* difference :math:`\\Delta I`.
dT
Half-scaled *Tritan* difference :math:`\\Delta T / 2`.
dP
*Protan* difference :math:`\\Delta P`.
Notes
-----
- :math:`\\Delta E_{ITP} = 720 \\sqrt{dI^2 + dT^2 + dP^2}`.
- ``dT`` is **half-scaled prior to differencing** as specified by
*Recommendation ITU-R BT.2124*.
"""
dE: NDArrayFloat | None = field(default_factory=lambda: None)
dI: NDArrayFloat | None = field(default_factory=lambda: None)
dT: NDArrayFloat | None = field(default_factory=lambda: None)
dP: NDArrayFloat | None = field(default_factory=lambda: None)
@typing.overload
def delta_E_ITP(
ICtCp_1: Domain1,
ICtCp_2: Domain1,
*,
additional_data: Literal[False] = False,
) -> NDArrayFloat: ...
@typing.overload
def delta_E_ITP(
ICtCp_1: Domain1,
ICtCp_2: Domain1,
*,
additional_data: Literal[True],
) -> DeltaE_Specification_ITP: ...
[docs]
def delta_E_ITP(
ICtCp_1: Domain1,
ICtCp_2: Domain1,
additional_data: bool = False,
) -> NDArrayFloat | DeltaE_Specification_ITP:
"""
Compute the colour difference :math:`\\Delta E_{ITP}` between two specified
:math:`IC_TC_P` colour encoding arrays using the
*Recommendation ITU-R BT.2124*.
Parameters
----------
ICtCp_1
:math:`IC_TC_P` colour encoding array 1.
ICtCp_2
:math:`IC_TC_P` colour encoding array 2.
additional_data
Whether to output additional data.
Returns
-------
:class:`numpy.ndarray` or :class:`DeltaE_Specification_ITP`
Colour difference :math:`\\Delta E_{ITP}`.
Notes
-----
- A value of 1 is equivalent to a just noticeable difference when viewed
in the most critical adaptation state.
+--------------+-----------------------+--------------------+
| **Domain** | **Scale - Reference** | **Scale - 1** |
+==============+=======================+====================+
| ``ICtCp_1`` | 1 | 1 |
+--------------+-----------------------+--------------------+
| ``ICtCp_2`` | 1 | 1 |
+--------------+-----------------------+--------------------+
References
----------
:cite:`InternationalTelecommunicationUnion2019`
Examples
--------
>>> ICtCp_1 = np.array([0.4885468072, -0.04739350675, 0.07475401302])
>>> ICtCp_2 = np.array([0.4899203231, -0.04567508203, 0.07361341775])
>>> delta_E_ITP(ICtCp_1, ICtCp_2) # doctest: +ELLIPSIS
np.float64(1.4265722...)
>>> delta_E_ITP(
... ICtCp_1,
... ICtCp_2,
... additional_data=True,
... ) # doctest: +ELLIPSIS
DeltaE_Specification_ITP(dE=np.float64(1.4265722...), \
dI=np.float64(0.0013735...), dT=np.float64(0.0008592...), \
dP=np.float64(-0.0011405...))
"""
I_1, T_1, P_1 = tsplit(ICtCp_1)
T_1 *= 0.5
I_2, T_2, P_2 = tsplit(ICtCp_2)
T_2 *= 0.5
I = I_2 - I_1 # noqa: E741
T = T_2 - T_1
P = P_2 - P_1
d_E_ITP = as_float(720 * np.sqrt(I**2 + T**2 + P**2))
if not additional_data:
return d_E_ITP
return DeltaE_Specification_ITP(
d_E_ITP,
I,
T,
P,
)
@dataclass
class DeltaE_Specification_HyAB(MixinDataclassArithmetic):
"""
Define the *HyAB* colour difference specification.
This data structure is returned by
:func:`colour.difference.delta_E_HyAB` when ``additional_data=True``.
Parameters
----------
dE
Colour difference :math:`\\Delta E_{HyAB}`.
dL
Raw *lightness* difference :math:`\\Delta L^*`.
da
Raw :math:`\\Delta a^*` difference.
db
Raw :math:`\\Delta b^*` difference.
"""
dE: NDArrayFloat | None = field(default_factory=lambda: None)
dL: NDArrayFloat | None = field(default_factory=lambda: None)
da: NDArrayFloat | None = field(default_factory=lambda: None)
db: NDArrayFloat | None = field(default_factory=lambda: None)
@typing.overload
def delta_E_HyAB(
Lab_1: Domain100,
Lab_2: Domain100,
*,
additional_data: Literal[False] = False,
) -> NDArrayFloat: ...
@typing.overload
def delta_E_HyAB(
Lab_1: Domain100,
Lab_2: Domain100,
*,
additional_data: Literal[True],
) -> DeltaE_Specification_HyAB: ...
[docs]
def delta_E_HyAB(
Lab_1: Domain100,
Lab_2: Domain100,
additional_data: bool = False,
) -> NDArrayFloat | DeltaE_Specification_HyAB:
"""
Compute the colour difference between two *CIE L\\*a\\*b\\** colourspace arrays
using a combination of a Euclidean metric in hue and chroma with a
city-block metric to incorporate lightness differences.
This metric is intended for large colour differences, on the order of 10
*CIE L\\*a\\*b\\** units or greater.
Parameters
----------
Lab_1
*CIE L\\*a\\*b\\** colourspace array 1.
Lab_2
*CIE L\\*a\\*b\\** colourspace array 2.
additional_data
Whether to output additional data.
Returns
-------
:class:`numpy.ndarray` or :class:`DeltaE_Specification_HyAB`
Colour difference :math:`\\Delta E_{HyAB}`.
Notes
-----
+------------+-----------------------+-------------------+
| **Domain** | **Scale - Reference** | **Scale - 1** |
+============+=======================+===================+
| ``Lab_1`` | 100 | 1 |
+------------+-----------------------+-------------------+
| ``Lab_2`` | 100 | 1 |
+------------+-----------------------+-------------------+
References
----------
:cite:`Abasi2020a`
Examples
--------
>>> Lab_1 = np.array([39.91531343, 51.16658481, 146.12933781])
>>> Lab_2 = np.array([53.12207516, -39.92365056, 249.54831278])
>>> delta_E_HyAB(Lab_1, Lab_2) # doctest: +ELLIPSIS
np.float64(151.0215481...)
>>> delta_E_HyAB(
... Lab_1,
... Lab_2,
... additional_data=True,
... ) # doctest: +ELLIPSIS
DeltaE_Specification_HyAB(dE=np.float64(151.0215481...), \
dL=np.float64(-13.2067617...), da=np.float64(91.0902353...), \
db=np.float64(-103.4189749...))
"""
dLab = to_domain_100(Lab_1) - to_domain_100(Lab_2)
dL, da, db = tsplit(dLab)
HyAB = as_float(np.abs(dL) + np.hypot(da, db))
if not additional_data:
return HyAB
return DeltaE_Specification_HyAB(
HyAB,
dL,
da,
db,
)
@dataclass
class DeltaE_Specification_HyCH(MixinDataclassArithmetic):
"""
Define the *HyCH* colour difference specification.
This data structure is returned by
:func:`colour.difference.delta_E_HyCH` when ``additional_data=True``.
Parameters
----------
dE
Colour difference :math:`\\Delta E_{HyCH}`.
dL
Weighted *lightness* difference :math:`\\Delta L' / (k_L S_L)`.
dC
Weighted *chroma* difference :math:`\\Delta C' / (k_C S_C)`.
dH
Weighted *hue* difference :math:`\\Delta H' / (k_H S_H)`.
Notes
-----
- Components are weighted based on *CIE 2000* intermediate attributes.
- Enabling the ``textiles`` parameter modifies the parametric weighting
factors and therefore directly affects the returned component values.
"""
dE: NDArrayFloat | None = field(default_factory=lambda: None)
dL: NDArrayFloat | None = field(default_factory=lambda: None)
dC: NDArrayFloat | None = field(default_factory=lambda: None)
dH: NDArrayFloat | None = field(default_factory=lambda: None)
@typing.overload
def delta_E_HyCH(
Lab_1: Domain100,
Lab_2: Domain100,
textiles: bool = ...,
*,
additional_data: Literal[False] = False,
) -> NDArrayFloat: ...
@typing.overload
def delta_E_HyCH(
Lab_1: Domain100,
Lab_2: Domain100,
textiles: bool = ...,
*,
additional_data: Literal[True],
) -> DeltaE_Specification_HyCH: ...
[docs]
def delta_E_HyCH(
Lab_1: Domain100,
Lab_2: Domain100,
textiles: bool = False,
additional_data: bool = False,
) -> NDArrayFloat | DeltaE_Specification_HyCH:
"""
Compute the colour difference between two *CIE L\\*a\\*b\\** colourspace
arrays using a combination of Euclidean metric in hue and chroma with a
city-block metric to incorporate lightness differences based on
*CIE 2000* recommendation attributes.
This metric is intended for large colour differences, on the order of 10
*CIE L\\*a\\*b\\** units or greater.
Parameters
----------
Lab_1
*CIE L\\*a\\*b\\** colourspace array 1.
Lab_2
*CIE L\\*a\\*b\\** colourspace array 2.
textiles
Whether to use the textile-specific parametrization.
additional_data
Whether to output additional data.
Returns
-------
:class:`numpy.ndarray` or :class:`DeltaE_Specification_HyCH`
Colour difference :math:`\\Delta E_{HyCH}`.
Notes
-----
+------------+-----------------------+-------------------+
| **Domain** | **Scale - Reference** | **Scale - 1** |
+============+=======================+===================+
| ``Lab_1`` | 100 | 1 |
+------------+-----------------------+-------------------+
| ``Lab_2`` | 100 | 1 |
+------------+-----------------------+-------------------+
References
----------
:cite:`Abasi2020a`
Examples
--------
>>> Lab_1 = np.array([39.91531343, 51.16658481, 146.12933781])
>>> Lab_2 = np.array([53.12207516, -39.92365056, 249.54831278])
>>> delta_E_HyCH(Lab_1, Lab_2) # doctest: +ELLIPSIS
np.float64(48.6642794...)
>>> delta_E_HyCH(
... Lab_1,
... Lab_2,
... additional_data=True,
... ) # doctest: +ELLIPSIS
DeltaE_Specification_HyCH(dE=np.float64(48.6642794...), \
dL=np.float64(12.7962972...), dC=np.float64(9.6258211...), \
dH=np.float64(34.5522171...))
"""
S_L, S_C, S_H, delta_L_p, delta_C_p, delta_H_p, R_T = astuple(
intermediate_attributes_CIE2000(Lab_1, Lab_2)
)
k_L = 2 if textiles else 1
k_C = 1
k_H = 1
L = delta_L_p / (k_L * S_L)
C = delta_C_p / (k_C * S_C)
H = delta_H_p / (k_H * S_H)
HyCH = as_float(np.abs(L) + np.sqrt(C**2 + H**2))
if not additional_data:
return HyCH
return DeltaE_Specification_HyCH(
HyCH,
L,
C,
H,
)