Source code for colour.appearance.llab

"""
:math:`LLAB(l:c)` Colour Appearance Model
=========================================

Define the *:math:`LLAB(l:c)`* colour appearance model for predicting
perceptual colour attributes under varying viewing conditions.

-   :class:`colour.appearance.InductionFactors_LLAB`
-   :attr:`colour.VIEWING_CONDITIONS_LLAB`
-   :class:`colour.CAM_Specification_LLAB`
-   :func:`colour.XYZ_to_LLAB`

References
----------
-   :cite:`Fairchild2013x` : Fairchild, M. D. (2013). LLAB Model. In Color
    Appearance Models (3rd ed., pp. 6025-6178). Wiley. ISBN:B00DAYO8E2
-   :cite:`Luo1996b` : Luo, Ming Ronnier, Lo, M.-C., & Kuo, W.-G. (1996). The
    LLAB (l:c) colour model. Color Research & Application, 21(6), 412-429.
    doi:10.1002/(SICI)1520-6378(199612)21:6<412::AID-COL4>3.0.CO;2-Z
-   :cite:`Luo1996c` : Luo, Ming Ronnier, & Morovic, J. (1996). Two Unsolved
    Issues in Colour Management - Colour Appearance and Gamut Mapping.
    Conference: 5th International Conference on High Technology: Imaging
    Science and Technology - Evolution & Promise, 136-147.
    http://www.researchgate.net/publication/\
236348295_Two_Unsolved_Issues_in_Colour_Management__\
Colour_Appearance_and_Gamut_Mapping
"""

from __future__ import annotations

import typing
from dataclasses import dataclass, field

import numpy as np

from colour.algebra import polar_to_cartesian, sdiv, sdiv_mode, spow, vecmul

if typing.TYPE_CHECKING:
    from colour.hints import Annotated, ArrayLike, Domain100, NDArrayFloat

from colour.utilities import (
    CanonicalMapping,
    MixinDataclassArithmetic,
    MixinDataclassIterable,
    array_namespace,
    as_float,
    as_float_array,
    from_range_degrees,
    to_domain_100,
    tsplit,
    tstack,
    xp_as_float_array,
    xp_degrees,
    xp_radians,
)

__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__ = [
    "InductionFactors_LLAB",
    "VIEWING_CONDITIONS_LLAB",
    "MATRIX_XYZ_TO_RGB_LLAB",
    "MATRIX_RGB_TO_XYZ_LLAB",
    "CAM_ReferenceSpecification_LLAB",
    "CAM_Specification_LLAB",
    "XYZ_to_LLAB",
]


[docs] @dataclass(frozen=True) class InductionFactors_LLAB(MixinDataclassIterable): """ Define the *:math:`LLAB(l:c)`* colour appearance model induction factors. Parameters ---------- D *Discounting-the-Illuminant* factor :math:`D`. F_S Surround induction factor :math:`F_S`. F_L *Lightness* induction factor :math:`F_L`. F_C *Chroma* induction factor :math:`F_C`. References ---------- :cite:`Fairchild2013x`, :cite:`Luo1996b`, :cite:`Luo1996c` """ D: float F_S: float F_L: float F_C: float
VIEWING_CONDITIONS_LLAB: CanonicalMapping = CanonicalMapping( { "Reference Samples & Images, Average Surround, Subtending > 4": ( InductionFactors_LLAB(1, 3, 0, 1) ), "Reference Samples & Images, Average Surround, Subtending < 4": ( InductionFactors_LLAB(1, 3, 1, 1) ), "Television & VDU Displays, Dim Surround": ( InductionFactors_LLAB(0.7, 3.5, 1, 1) ), "Cut Sheet Transparency, Dim Surround": (InductionFactors_LLAB(1, 5, 1, 1.1)), "35mm Projection Transparency, Dark Surround": ( InductionFactors_LLAB(0.7, 4, 1, 1) ), } ) VIEWING_CONDITIONS_LLAB.__doc__ = """ Define the reference :math:`LLAB(l:c)` colour appearance model viewing conditions. References ---------- :cite:`Fairchild2013x`, :cite:`Luo1996b`, :cite:`Luo1996c` Aliases: - 'ref_average_4_plus': 'Reference Samples & Images, Average Surround, Subtending > 4' - 'ref_average_4_minus': 'Reference Samples & Images, Average Surround, Subtending < 4' - 'tv_dim': 'Television & VDU Displays, Dim Surround' - 'sheet_dim': 'Cut Sheet Transparency, Dim Surround' - 'projected_dark': '35mm Projection Transparency, Dark Surround' """ VIEWING_CONDITIONS_LLAB["ref_average_4_plus"] = VIEWING_CONDITIONS_LLAB[ "Reference Samples & Images, Average Surround, Subtending > 4" ] VIEWING_CONDITIONS_LLAB["ref_average_4_minus"] = VIEWING_CONDITIONS_LLAB[ "Reference Samples & Images, Average Surround, Subtending < 4" ] VIEWING_CONDITIONS_LLAB["tv_dim"] = VIEWING_CONDITIONS_LLAB[ "Television & VDU Displays, Dim Surround" ] VIEWING_CONDITIONS_LLAB["sheet_dim"] = VIEWING_CONDITIONS_LLAB[ "Cut Sheet Transparency, Dim Surround" ] VIEWING_CONDITIONS_LLAB["projected_dark"] = VIEWING_CONDITIONS_LLAB[ "35mm Projection Transparency, Dark Surround" ] MATRIX_XYZ_TO_RGB_LLAB: NDArrayFloat = np.array( [ [0.8951, 0.2664, -0.1614], [-0.7502, 1.7135, 0.0367], [0.0389, -0.0685, 1.0296], ] ) """ LLAB(l:c) colour appearance model *CIE XYZ* tristimulus values to normalised cone responses matrix. """ MATRIX_RGB_TO_XYZ_LLAB: NDArrayFloat = np.linalg.inv(MATRIX_XYZ_TO_RGB_LLAB) """ LLAB(l:c) colour appearance model normalised cone responses to *CIE XYZ* tristimulus values matrix. """ @dataclass class CAM_ReferenceSpecification_LLAB(MixinDataclassArithmetic): """ Define the *:math:`LLAB(l:c)`* colour appearance model reference specification. This specification contains field names consistent with the *Fairchild (2013)* reference. Parameters ---------- L_L Correlate of *Lightness* :math:`L_L`. Ch_L Correlate of *chroma* :math:`Ch_L`. h_L *Hue* angle :math:`h_L` in degrees. s_L Correlate of *saturation* :math:`s_L`. C_L Correlate of *colourfulness* :math:`C_L`. HC *Hue* :math:`h` composition :math:`H^C`. A_L Opponent signal :math:`A_L`. B_L Opponent signal :math:`B_L`. References ---------- :cite:`Fairchild2013x`, :cite:`Luo1996b`, :cite:`Luo1996c` """ L_L: float | NDArrayFloat | None = field(default_factory=lambda: None) Ch_L: float | NDArrayFloat | None = field(default_factory=lambda: None) h_L: float | NDArrayFloat | None = field(default_factory=lambda: None) s_L: float | NDArrayFloat | None = field(default_factory=lambda: None) C_L: float | NDArrayFloat | None = field(default_factory=lambda: None) HC: float | NDArrayFloat | None = field(default_factory=lambda: None) A_L: float | NDArrayFloat | None = field(default_factory=lambda: None) B_L: float | NDArrayFloat | None = field(default_factory=lambda: None)
[docs] @dataclass class CAM_Specification_LLAB(MixinDataclassArithmetic): """ Define the *:math:`LLAB(l:c)`* colour appearance model specification. This specification provides a standardized interface for the *LLAB(l:c)* model with field names consistent across all colour appearance models in :mod:`colour.appearance`. While the field names differ from the original *Fairchild (2013)* reference notation, they map directly to the model's perceptual correlates. Parameters ---------- J Correlate of *lightness* :math:`L_L`. C Correlate of *chroma* :math:`Ch_L`. h *Hue* angle :math:`h_L` in degrees. s Correlate of *saturation* :math:`s_L`. M Correlate of *colourfulness* :math:`C_L`. HC *Hue* :math:`h` composition :math:`H^C`. a Opponent signal :math:`A_L`. b Opponent signal :math:`B_L`. Notes ----- - This specification is the one used in the current model implementation. References ---------- :cite:`Fairchild2013x`, :cite:`Luo1996b`, :cite:`Luo1996c` """ J: float | NDArrayFloat | None = field(default_factory=lambda: None) C: float | NDArrayFloat | None = field(default_factory=lambda: None) h: float | NDArrayFloat | None = field(default_factory=lambda: None) s: float | NDArrayFloat | None = field(default_factory=lambda: None) M: float | NDArrayFloat | None = field(default_factory=lambda: None) HC: float | NDArrayFloat | None = field(default_factory=lambda: None) a: float | NDArrayFloat | None = field(default_factory=lambda: None) b: float | NDArrayFloat | None = field(default_factory=lambda: None)
[docs] def XYZ_to_LLAB( XYZ: Domain100, XYZ_0: Domain100, Y_b: ArrayLike, L: ArrayLike, surround: InductionFactors_LLAB = VIEWING_CONDITIONS_LLAB[ "Reference Samples & Images, Average Surround, Subtending < 4" ], ) -> Annotated[CAM_Specification_LLAB, 360]: """ Compute the *:math:`LLAB(l:c)`* colour appearance model correlates from the specified *CIE XYZ* tristimulus values. Parameters ---------- XYZ *CIE XYZ* tristimulus values of test sample / stimulus. XYZ_0 *CIE XYZ* tristimulus values of reference white. Y_b Luminance factor of the background in :math:`cd/m^2`. L Absolute luminance :math:`L` of reference white in :math:`cd/m^2`. surround Surround viewing conditions induction factors. Returns ------- :class:`colour.CAM_Specification_LLAB` *:math:`LLAB(l:c)`* colour appearance model specification. Notes ----- +---------------------+-----------------------+---------------+ | **Domain** | **Scale - Reference** | **Scale - 1** | +=====================+=======================+===============+ | ``XYZ`` | 100 | 1 | +---------------------+-----------------------+---------------+ | ``XYZ_0`` | 100 | 1 | +---------------------+-----------------------+---------------+ +---------------------+-----------------------+---------------+ | **Range** | **Scale - Reference** | **Scale - 1** | +=====================+=======================+===============+ | ``specification.h`` | 360 | 1 | +---------------------+-----------------------+---------------+ References ---------- :cite:`Fairchild2013x`, :cite:`Luo1996b`, :cite:`Luo1996c` Examples -------- *Fairchild (2013)* Table 14.3 Case 4 (chromatic stimulus under illuminant A reference white, mesopic luminance): >>> XYZ = np.array([19.01, 20.00, 21.78]) >>> XYZ_0 = np.array([109.85, 100.00, 35.58]) >>> Y_b = 20.0 >>> L = 31.83 >>> surround = VIEWING_CONDITIONS_LLAB["ref_average_4_minus"] >>> XYZ_to_LLAB(XYZ, XYZ_0, Y_b, L, surround) # doctest: +ELLIPSIS CAM_Specification_LLAB(J=np.float64(39.81475...), C=np.float64(29.345046...), \ h=np.float64(271.852666...), s=np.float64(0.737039...), M=np.float64(54.593098...), \ HC=None, a=np.float64(1.764967...), b=np.float64(-54.564560...)) """ XYZ = to_domain_100(XYZ) XYZ_0 = to_domain_100(XYZ_0) xp = array_namespace(XYZ, XYZ_0, Y_b, L) _X, Y, _Z = tsplit(XYZ) Y_b = xp_as_float_array(Y_b, xp=xp, like=XYZ) L = xp_as_float_array(L, xp=xp, like=XYZ) F_S = xp_as_float_array(surround.F_S, xp=xp, like=XYZ) F_L = xp_as_float_array(surround.F_L, xp=xp, like=XYZ) F_C = xp_as_float_array(surround.F_C, xp=xp, like=XYZ) D = xp_as_float_array(surround.D, xp=xp, like=XYZ) # Computing normalised cone responses for the stimulus, the reference # white and the *CIE Standard Illuminant D Series* *D65* reference. with sdiv_mode(): RGB = vecmul(MATRIX_XYZ_TO_RGB_LLAB, sdiv(XYZ, XYZ[..., 1, None])) RGB_0 = vecmul(MATRIX_XYZ_TO_RGB_LLAB, sdiv(XYZ_0, XYZ_0[..., 1, None])) XYZ_0r = xp_as_float_array([95.05, 100.00, 108.88], xp=xp, like=XYZ) RGB_0r = vecmul(MATRIX_XYZ_TO_RGB_LLAB, XYZ_0r / 100) # Computing chromatic adaptation: cone responses are adapted to the D65 # reference; the blue channel uses a nonlinear power exponent. R, G, B = tsplit(RGB) R_0, G_0, B_0 = tsplit(RGB_0) R_0r, G_0r, B_0r = tsplit(RGB_0r) beta = spow(B_0 / B_0r, 0.0834) R_a = (D * R_0r / R_0 + 1 - D) * R G_a = (D * G_0r / G_0 + 1 - D) * G B_a = (D * B_0r / spow(B_0, beta) + 1 - D) * spow(B, beta) Y_stack = tstack([Y, Y, Y]) XYZ_r = vecmul(MATRIX_RGB_TO_XYZ_LLAB, tstack([R_a, G_a, B_a]) * Y_stack) X_r, Y_r, Z_r = tsplit(XYZ_r) # Computing the nonlinear visual response :math:`f` on the three normalised # tristimulus components in a single call. one_F_s = 1 / F_S XYZ_n = tstack([X_r / 95.05, Y_r / 100, Z_r / 108.88]) f_XYZ = xp.where( XYZ_n > 0.008856, spow(XYZ_n, one_F_s), ((spow(0.008856, one_F_s) - (16 / 116)) / 0.008856) * XYZ_n + (16 / 116), ) f_X, f_Y, f_Z = tsplit(f_XYZ) # Computing opponent colour dimensions: modified *CIE L\\*a\\*b\\** with # background lightness contrast :math:`z`. z = 1 + F_L * spow(Y_b / 100, 0.5) L_L = as_float_array(116 * spow(f_Y, z) - 16) a = 500 * (f_X - f_Y) b = 200 * (f_Y - f_Z) # Computing the correlate of *chroma* :math:`Ch_L`. c = spow(a**2 + b**2, 0.5) Ch_L = 25 * xp.log1p(0.05 * c) # Computing the correlate of *colourfulness* :math:`C_L`. S_C = 1 + 0.47 * xp.log10(L) - 0.057 * xp.log10(L) ** 2 S_M = 0.7 + 0.02 * L_L - 0.0002 * L_L**2 C_L = Ch_L * S_M * S_C * F_C # Computing the correlate of *saturation* :math:`s_L`. s_L = Ch_L / L_L # Computing the *hue* angle :math:`h_L` in degrees. h_L = xp_degrees(xp.atan2(b, a)) % 360 # TODO: Implement hue composition computation. # Computing the final opponent signals :math:`A_L`, :math:`B_L` from # polar coordinates :math:`(C_L, h_L)`. A_L, B_L = tsplit(polar_to_cartesian(tstack([C_L, xp_radians(h_L)]))) return CAM_Specification_LLAB( J=as_float(L_L), C=as_float(Ch_L), h=as_float(from_range_degrees(h_L)), s=as_float(s_L), M=as_float(C_L), HC=None, a=as_float(A_L), b=as_float(B_L), )