Source code for colour.models.common

"""
Common Colour Models Utilities
==============================

Defines various colour models common utilities:

-   :attr:`colour.COLOURSPACE_MODELS`
-   :func:`colour.models.Jab_to_JCh`
-   :func:`colour.models.JCh_to_Jab`
-   :func:`colour.models.XYZ_to_Iab`
-   :func:`colour.models.Iab_to_XYZ`

References
----------
-   :cite:`CIETC1-482004m` : CIE TC 1-48. (2004). CIE 1976 uniform colour
    spaces. In CIE 015:2004 Colorimetry, 3rd Edition (p. 24).
    ISBN:978-3-901906-33-6
"""

from __future__ import annotations

import numpy as np

from colour.algebra import cartesian_to_polar, polar_to_cartesian, vector_dot
from colour.hints import ArrayLike, Callable, NDArray, Tuple
from colour.utilities import (
    CanonicalMapping,
    attest,
    from_range_1,
    from_range_degrees,
    to_domain_1,
    to_domain_degrees,
    tsplit,
    tstack,
)
from colour.utilities.documentation import (
    DocstringTuple,
    is_documentation_building,
)

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

__all__ = [
    "COLOURSPACE_MODELS",
    "COLOURSPACE_MODELS_AXIS_LABELS",
    "COLOURSPACE_MODELS_DOMAIN_RANGE_SCALE_1_TO_REFERENCE",
    "Jab_to_JCh",
    "JCh_to_Jab",
    "XYZ_to_Iab",
    "Iab_to_XYZ",
]


COLOURSPACE_MODELS: Tuple = (
    "CAM02LCD",
    "CAM02SCD",
    "CAM02UCS",
    "CAM16LCD",
    "CAM16SCD",
    "CAM16UCS",
    "CIE XYZ",
    "CIE xyY",
    "CIE Lab",
    "CIE Luv",
    "CIE UCS",
    "CIE UVW",
    "DIN99",
    "Hunter Lab",
    "Hunter Rdab",
    "ICaCb",
    "ICtCp",
    "IPT",
    "IPT Munish 2021",
    "IgPgTg",
    "Jzazbz",
    "OSA UCS",
    "Oklab",
    "hdr-CIELAB",
    "hdr-IPT",
)
if is_documentation_building():  # pragma: no cover
    COLOURSPACE_MODELS = DocstringTuple(COLOURSPACE_MODELS)
    COLOURSPACE_MODELS.__doc__ = """
Colourspace models supporting a direct conversion to *CIE XYZ* tristimulus
values.
"""

COLOURSPACE_MODELS_AXIS_LABELS: CanonicalMapping = CanonicalMapping(
    {
        "CAM02LCD": ("$J^\\prime$", "$a^\\prime$", "$b^\\prime$"),
        "CAM02SCD": ("$J^\\prime$", "$a^\\prime$", "$b^\\prime$"),
        "CAM02UCS": ("$J^\\prime$", "$a^\\prime$", "$b^\\prime$"),
        "CAM16LCD": ("$J^\\prime$", "$a^\\prime$", "$b^\\prime$"),
        "CAM16SCD": ("$J^\\prime$", "$a^\\prime$", "$b^\\prime$"),
        "CAM16UCS": ("$J^\\prime$", "$a^\\prime$", "$b^\\prime$"),
        "CIE XYZ": ("X", "Y", "Z"),
        "CIE xyY": ("x", "y", "Y"),
        "CIE Lab": ("$L^*$", "$a^*$", "$b^*$"),
        "CIE Luv": ("$L^*$", "$u^\\prime$", "$v^\\prime$"),
        "CIE UCS": ("U", "V", "W"),
        "CIE UVW": ("U", "V", "W"),
        "DIN99": ("$L_{99}$", "$a_{99}$", "$b_{99}$"),
        "Hunter Lab": ("$L^*$", "$a^*$", "$b^*$"),
        "Hunter Rdab": ("Rd", "a", "b"),
        "ICaCb": ("$I$", "$C_a$", "$C_b$"),
        "ICtCp": ("$I$", "$C_T$", "$C_P$"),
        "IPT": ("I", "P", "T"),
        "IPT Munish 2021": ("I", "P", "T"),
        "IgPgTg": ("$I_G$", "$P_G$", "$T_G$"),
        "Jzazbz": ("$J_z$", "$a_z$", "$b_z$"),
        "OSA UCS": ("L", "j", "g"),
        "Oklab": ("$L$", "$a$", "$b$"),
        "hdr-CIELAB": ("L hdr", "a hdr", "b hdr"),
        "hdr-IPT": ("I hdr", "P hdr", "T hdr"),
    }
)
"""Colourspace models labels mapping."""

attest(COLOURSPACE_MODELS == tuple(COLOURSPACE_MODELS_AXIS_LABELS.keys()))

COLOURSPACE_MODELS_DOMAIN_RANGE_SCALE_1_TO_REFERENCE: (
    CanonicalMapping
) = CanonicalMapping(
    {
        "CAM02LCD": np.array([100, 100, 100]),
        "CAM02SCD": np.array([100, 100, 100]),
        "CAM02UCS": np.array([100, 100, 100]),
        "CAM16LCD": np.array([100, 100, 100]),
        "CAM16SCD": np.array([100, 100, 100]),
        "CAM16UCS": np.array([100, 100, 100]),
        "CIE XYZ": np.array([1, 1, 1]),
        "CIE xyY": np.array([1, 1, 1]),
        "CIE Lab": np.array([100, 100, 100]),
        "CIE Luv": np.array([100, 100, 100]),
        "CIE UCS": np.array([1, 1, 1]),
        "CIE UVW": np.array([100, 100, 100]),
        "DIN99": np.array([100, 100, 100]),
        "Hunter Lab": np.array([100, 100, 100]),
        "Hunter Rdab": np.array([100, 100, 100]),
        "ICaCb": np.array([1, 1, 1]),
        "ICtCp": np.array([1, 1, 1]),
        "IPT": np.array([1, 1, 1]),
        "IPT Munish 2021": np.array([1, 1, 1]),
        "IgPgTg": np.array([1, 1, 1]),
        "Jzazbz": np.array([1, 1, 1]),
        "OSA UCS": np.array([100, 100, 100]),
        "Oklab": np.array([1, 1, 1]),
        "hdr-CIELAB": np.array([100, 100, 100]),
        "hdr-IPT": np.array([100, 100, 100]),
    }
)
"""Colourspace models domain-range scale **'1'** to **'Reference'** mapping."""


[docs]def Jab_to_JCh(Jab: ArrayLike) -> NDArray: """ Convert from *Jab* colour representation to *JCh* colour representation. This definition is used to perform conversion from *CIE L\\*a\\*b\\** colourspace to *CIE L\\*C\\*Hab* colourspace and for other similar conversions. It implements a generic transformation from *Lightness* :math:`J`, :math:`a` and :math:`b` opponent colour dimensions to the correlates of *Lightness* :math:`J`, chroma :math:`C` and hue angle :math:`h`. Parameters ---------- Jab *Jab* colour representation array. Returns ------- :class:`numpy.ndarray` *JCh* colour representation array. Notes ----- +------------+-----------------------+-----------------+ | **Domain** | **Scale - Reference** | **Scale - 1** | +============+=======================+=================+ | ``Jab`` | ``J`` : [0, 100] | ``J`` : [0, 1] | | | | | | | ``a`` : [-100, 100] | ``a`` : [-1, 1] | | | | | | | ``b`` : [-100, 100] | ``b`` : [-1, 1] | +------------+-----------------------+-----------------+ +------------+-----------------------+-----------------+ | **Range** | **Scale - Reference** | **Scale - 1** | +============+=======================+=================+ | ``JCh`` | ``J`` : [0, 100] | ``J`` : [0, 1] | | | | | | | ``C`` : [0, 100] | ``C`` : [0, 1] | | | | | | | ``h`` : [0, 360] | ``h`` : [0, 1] | +------------+-----------------------+-----------------+ References ---------- :cite:`CIETC1-482004m` Examples -------- >>> Jab = np.array([41.52787529, 52.63858304, 26.92317922]) >>> Jab_to_JCh(Jab) # doctest: +ELLIPSIS array([ 41.5278752..., 59.1242590..., 27.0884878...]) """ L, a, b = tsplit(Jab) C, H = tsplit(cartesian_to_polar(tstack([a, b]))) JCh = tstack([L, C, from_range_degrees(np.degrees(H) % 360)]) return JCh
[docs]def JCh_to_Jab(JCh: ArrayLike) -> NDArray: """ Convert from *JCh* colour representation to *Jab* colour representation. This definition is used to perform conversion from *CIE L\\*C\\*Hab* colourspace to *CIE L\\*a\\*b\\** colourspace and for other similar conversions. It implements a generic transformation from the correlates of *Lightness* :math:`J`, chroma :math:`C` and hue angle :math:`h` to *Lightness* :math:`J`, :math:`a` and :math:`b` opponent colour dimensions. Parameters ---------- JCh *JCh* colour representation array. Returns ------- :class:`numpy.ndarray` *Jab* colour representation array. Notes ----- +-------------+-----------------------+-----------------+ | **Domain** | **Scale - Reference** | **Scale - 1** | +=============+=======================+=================+ | ``JCh`` | ``J`` : [0, 100] | ``J`` : [0, 1] | | | | | | | ``C`` : [0, 100] | ``C`` : [0, 1] | | | | | | | ``h`` : [0, 360] | ``h`` : [0, 1] | +-------------+-----------------------+-----------------+ +-------------+-----------------------+-----------------+ | **Range** | **Scale - Reference** | **Scale - 1** | +=============+=======================+=================+ | ``Jab`` | ``J`` : [0, 100] | ``J`` : [0, 1] | | | | | | | ``a`` : [-100, 100] | ``a`` : [-1, 1] | | | | | | | ``b`` : [-100, 100] | ``b`` : [-1, 1] | +-------------+-----------------------+-----------------+ References ---------- :cite:`CIETC1-482004m` Examples -------- >>> JCh = np.array([41.52787529, 59.12425901, 27.08848784]) >>> JCh_to_Jab(JCh) # doctest: +ELLIPSIS array([ 41.5278752..., 52.6385830..., 26.9231792...]) """ L, C, H = tsplit(JCh) a, b = tsplit( polar_to_cartesian(tstack([C, np.radians(to_domain_degrees(H))])) ) Jab = tstack([L, a, b]) return Jab
[docs]def XYZ_to_Iab( XYZ: ArrayLike, LMS_to_LMS_p_callable: Callable, matrix_XYZ_to_LMS: ArrayLike, matrix_LMS_p_to_Iab: ArrayLike, ) -> NDArray: """ Convert from *CIE XYZ* tristimulus values to *IPT*-like :math:`Iab` colour representation. This definition is used to perform conversion from *CIE XYZ* tristimulus values to *IPT* colourspace and for other similar conversions. It implements a generic transformation from *CIE XYZ* tristimulus values to *Lightness* :math:`I`, :math:`a` and :math:`b` representing red-green dimension, i.e. the dimension lost by protanopes and the yellow-blue dimension, i.e. the dimension lost by tritanopes, respectively. Parameters ---------- XYZ *CIE XYZ* tristimulus values. LMS_to_LMS_p_callable Callable applying the forward non-linearity to the :math:`LMS` colourspace array. matrix_XYZ_to_LMS Matrix converting from *CIE XYZ* tristimulus values to :math:`LMS` colourspace. matrix_LMS_p_to_Iab Matrix converting from non-linear :math:`LMS_p` colourspace to *IPT*-like :math:`Iab` colour representation. Returns ------- :class:`numpy.ndarray` *IPT*-like :math:`Iab` colour representation. Notes ----- +------------+-----------------------+-----------------+ | **Domain** | **Scale - Reference** | **Scale - 1** | +============+=======================+=================+ | ``XYZ`` | [0, 1] | [0, 1] | +------------+-----------------------+-----------------+ +------------+-----------------------+-----------------+ | **Range** | **Scale - Reference** | **Scale - 1** | +============+=======================+=================+ | ``Iab`` | ``I`` : [0, 1] | ``I`` : [0, 1] | | | | | | | ``a`` : [-1, 1] | ``a`` : [-1, 1] | | | | | | | ``b`` : [-1, 1] | ``b`` : [-1, 1] | +------------+-----------------------+-----------------+ Examples -------- >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) >>> LMS_to_LMS_p = lambda x: x**0.43 >>> M_XYZ_to_LMS = np.array( ... [ ... [0.4002, 0.7075, -0.0807], ... [-0.2280, 1.1500, 0.0612], ... [0.0000, 0.0000, 0.9184], ... ] ... ) >>> M_LMS_p_to_Iab = np.array( ... [ ... [0.4000, 0.4000, 0.2000], ... [4.4550, -4.8510, 0.3960], ... [0.8056, 0.3572, -1.1628], ... ] ... ) >>> XYZ_to_Iab(XYZ, LMS_to_LMS_p, M_XYZ_to_LMS, M_LMS_p_to_Iab) ... # doctest: +ELLIPSIS array([ 0.3842619..., 0.3848730..., 0.1888683...]) """ XYZ = to_domain_1(XYZ) LMS = vector_dot(matrix_XYZ_to_LMS, XYZ) LMS_p = LMS_to_LMS_p_callable(LMS) Iab = vector_dot(matrix_LMS_p_to_Iab, LMS_p) return from_range_1(Iab)
[docs]def Iab_to_XYZ( Iab: ArrayLike, LMS_p_to_LMS_callable: Callable, matrix_Iab_to_LMS_p: ArrayLike, matrix_LMS_to_XYZ: ArrayLike, ) -> NDArray: """ Convert from *IPT*-like :math:`Iab` colour representation to *CIE XYZ* tristimulus values. This definition is used to perform conversion from *IPT* colourspace to *CIE XYZ* tristimulus values and for other similar conversions. It implements a generic transformation from *Lightness* :math:`I`, :math:`a` and :math:`b` representing red-green dimension, i.e. the dimension lost by protanopes and the yellow-blue dimension, i.e. the dimension lost by tritanopes, respectively to *CIE XYZ* tristimulus values. Parameters ---------- Iab *IPT*-like :math:`Iab` colour representation. LMS_p_to_LMS_callable Callable applying the reverse non-linearity to the :math:`LMS_p` colourspace array. matrix_Iab_to_LMS_p Matrix converting from *IPT*-like :math:`Iab` colour representation to non-linear :math:`LMS_p` colourspace. matrix_LMS_to_XYZ Matrix converting from :math:`LMS` colourspace to *CIE XYZ* tristimulus values. Returns ------- :class:`numpy.ndarray` *CIE XYZ* tristimulus values. Notes ----- +------------+-----------------------+-----------------+ | **Domain** | **Scale - Reference** | **Scale - 1** | +============+=======================+=================+ | ``Iab`` | ``I`` : [0, 1] | ``I`` : [0, 1] | | | | | | | ``a`` : [-1, 1] | ``a`` : [-1, 1] | | | | | | | ``b`` : [-1, 1] | ``b`` : [-1, 1] | +------------+-----------------------+-----------------+ +------------+-----------------------+-----------------+ | **Range** | **Scale - Reference** | **Scale - 1** | +============+=======================+=================+ | ``XYZ`` | [0, 1] | [0, 1] | +------------+-----------------------+-----------------+ Examples -------- >>> Iab = np.array([0.38426191, 0.38487306, 0.18886838]) >>> LMS_p_to_LMS = lambda x: x ** (1 / 0.43) >>> M_Iab_to_LMS_p = np.linalg.inv( ... np.array( ... [ ... [0.4000, 0.4000, 0.2000], ... [4.4550, -4.8510, 0.3960], ... [0.8056, 0.3572, -1.1628], ... ] ... ) ... ) >>> M_LMS_to_XYZ = np.linalg.inv( ... np.array( ... [ ... [0.4002, 0.7075, -0.0807], ... [-0.2280, 1.1500, 0.0612], ... [0.0000, 0.0000, 0.9184], ... ] ... ) ... ) >>> Iab_to_XYZ(Iab, LMS_p_to_LMS, M_Iab_to_LMS_p, M_LMS_to_XYZ) ... # doctest: +ELLIPSIS array([ 0.2065400..., 0.1219722..., 0.0513695...]) """ Iab = to_domain_1(Iab) LMS = vector_dot(matrix_Iab_to_LMS_p, Iab) LMS_p = LMS_p_to_LMS_callable(LMS) XYZ = vector_dot(matrix_LMS_to_XYZ, LMS_p) return from_range_1(XYZ)