Source code for colour.temperature.ohno2013

"""
Ohno (2013) Correlated Colour Temperature
=========================================

Defines the *Ohno (2013)* correlated colour temperature :math:`T_{cp}`
computations objects:

-   :func:`colour.temperature.uv_to_CCT_Ohno2013`: Correlated colour
    temperature :math:`T_{cp}` and :math:`\\Delta_{uv}` computation of given
    *CIE UCS* colourspace *uv* chromaticity coordinates using *Ohno (2013)*
    method.
-   :func:`colour.temperature.CCT_to_uv_Ohno2013`: *CIE UCS* colourspace *uv*
    chromaticity coordinates computation of given correlated colour temperature
    :math:`T_{cp}`, :math:`\\Delta_{uv}` using *Ohno (2013)* method.

References
----------
-   :cite:`Ohno2014a` : Ohno, Yoshiro. (2014). Practical Use and Calculation of
    CCT and Duv. LEUKOS, 10(1), 47-55. doi:10.1080/15502724.2014.839020
"""

from __future__ import annotations

import numpy as np

from colour.algebra import euclidean_distance, sdiv, sdiv_mode
from colour.colorimetry import (
    MultiSpectralDistributions,
    handle_spectral_arguments,
)
from colour.hints import ArrayLike, NDArrayFloat
from colour.models import UCS_to_uv, UCS_to_XYZ, XYZ_to_UCS, uv_to_UCS
from colour.temperature import CCT_to_uv_Planck1900
from colour.utilities import (
    CACHE_REGISTRY,
    as_float_array,
    attest,
    is_caching_enabled,
    optional,
    runtime_warning,
    tsplit,
    tstack,
)

__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__ = [
    "CCT_MINIMAL_OHNO2013",
    "CCT_MAXIMAL_OHNO2013",
    "CCT_DEFAULT_SPACING_OHNO2013",
    "planckian_table",
    "uv_to_CCT_Ohno2013",
    "CCT_to_uv_Ohno2013",
    "XYZ_to_CCT_Ohno2013",
    "CCT_to_XYZ_Ohno2013",
]

CCT_MINIMAL_OHNO2013: float = 1000
CCT_MAXIMAL_OHNO2013: float = 100000
CCT_DEFAULT_SPACING_OHNO2013: float = 1.001

_CACHE_PLANCKIAN_TABLE: dict = CACHE_REGISTRY.register_cache(
    f"{__name__}._CACHE_PLANCKIAN_TABLE"
)


def planckian_table(
    cmfs: MultiSpectralDistributions,
    start: float,
    end: float,
    spacing: float,
) -> NDArrayFloat:
    """
    Return a planckian table from given *CIE UCS* colourspace *uv*
    chromaticity coordinates, colour matching functions and temperature range
    using *Ohno (2013)* method.

    Parameters
    ----------
    cmfs
        Standard observer colour matching functions.
    start
        Temperature range start in kelvin degrees.
    end
        Temperature range end in kelvin degrees.
    spacing
        The spacing between values expressed as a multiplier. Must be greater
        than 1.

    Returns
    -------
    :class:`list`
        Planckian table.

    Examples
    --------
    >>> from colour import MSDS_CMFS, SPECTRAL_SHAPE_DEFAULT
    >>> cmfs = (
    ...     MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]
    ...     .copy()
    ...     .align(SPECTRAL_SHAPE_DEFAULT)
    ... )
    >>> uv = np.array([0.1978, 0.3122])
    >>> planckian_table(cmfs, 1000, 1010, 1.005)
    ... # doctest: +ELLIPSIS
    array([[  1.00000000e+03,   4.4796...e-01,   3.5462...e-01],
           [  1.00100000e+03,   4.4772...e-01,   3.5464...e-01],
           [  1.00600500e+03,   4.4656...e-01,   3.5475...e-01],
           [  1.00900000e+03,   4.4586...e-01,   3.5481...e-01],
           [  1.01000000e+03,   4.4563...e-01,   3.5483...e-01]])
    """

    hash_key = hash((cmfs, start, end, spacing))
    if is_caching_enabled() and hash_key in _CACHE_PLANCKIAN_TABLE:
        table = _CACHE_PLANCKIAN_TABLE[hash_key].copy()
    else:
        attest(spacing > 1, "Spacing value must be greater than 1!")

        Ti = [start, start + 1]
        next_ti = start + 1
        next_spacing = spacing
        while (next_ti := next_ti * next_spacing) < end:
            Ti.append(next_ti)

            # Slightly decrease step-size for higher CCT.
            D = (next_ti - CCT_MINIMAL_OHNO2013) / (
                CCT_MAXIMAL_OHNO2013 - CCT_MINIMAL_OHNO2013
            )
            D = min(max(D, 0), 1)
            next_spacing = spacing * (1 - D) + (1 + (spacing - 1) / 10) * D
        Ti = np.concatenate([Ti, [end - 1, end]])

        table = np.concatenate(
            [np.reshape(Ti, (-1, 1)), CCT_to_uv_Planck1900(Ti, cmfs)], axis=1
        )
        _CACHE_PLANCKIAN_TABLE[hash_key] = table.copy()
    return table


[docs] def uv_to_CCT_Ohno2013( uv: ArrayLike, cmfs: MultiSpectralDistributions | None = None, start: float | None = None, end: float | None = None, spacing: float | None = None, ) -> NDArrayFloat: """ 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. 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, default to 1000. end Temperature range end in kelvin degrees, default to 100000. spacing Spacing between values of the underlying planckian table expressed as a multiplier. Default to 1.001. The closer to 1.0, the higher the precision of the returned colour temperature :math:`T_{cp}` and :math:`\\Delta_{uv}`. 1.01 provides a good balance between performance and accuracy. ``spacing`` value must be greater than 1. Returns ------- :class:`numpy.ndarray` Correlated colour temperature :math:`T_{cp}`, :math:`\\Delta_{uv}`. References ---------- :cite:`Ohno2014a` Examples -------- >>> from colour import MSDS_CMFS, SPECTRAL_SHAPE_DEFAULT >>> cmfs = ( ... MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] ... .copy() ... .align(SPECTRAL_SHAPE_DEFAULT) ... ) >>> uv = np.array([0.1978, 0.3122]) >>> uv_to_CCT_Ohno2013(uv, cmfs) # doctest: +ELLIPSIS array([ 6.50747...e+03, 3.22334...e-03]) """ uv = as_float_array(uv) cmfs, _illuminant = handle_spectral_arguments(cmfs) start = optional(start, CCT_MINIMAL_OHNO2013) end = optional(end, CCT_MAXIMAL_OHNO2013) spacing = optional(spacing, CCT_DEFAULT_SPACING_OHNO2013) shape = uv.shape uv = np.reshape(uv, (-1, 2)) # Planckian tables creation through cascade expansion. tables_data = [] for uv_i in uv: table = planckian_table(cmfs, start, end, spacing) dists = euclidean_distance(table[:, 1:], uv_i) index = np.argmin(dists) 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 tables_data.append( np.vstack( [ [*table[index - 1, ...], dists[index - 1]], [*table[index, ...], dists[index]], [*table[index + 1, ...], dists[index + 1]], ] ) ) tables = as_float_array(tables_data) Tip, uip, vip, dip = tsplit(tables[:, 0, :]) Ti, _ui, _vi, di = tsplit(tables[:, 1, :]) Tin, uin, vin, din = tsplit(tables[:, 2, :]) # Triangular solution. l = np.hypot(uin - uip, vin - vip) # noqa: E741 x = (dip**2 - din**2 + l**2) / (2 * l) T_t = Tip + (Tin - Tip) * (x / l) vtx = vip + (vin - vip) * (x / l) sign = np.sign(uv[..., 1] - vtx) D_uv_t = (dip**2 - x**2) ** (1 / 2) * sign # Parabolic solution. 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_p = -b / (2 * a) D_uv_p = (a * T_p**2 + b * T_p + c) * sign CCT_D_uv = np.where( (np.abs(D_uv_t) >= 0.002)[..., None], tstack([T_p, D_uv_p]), tstack([T_t, D_uv_t]), ) return np.reshape(CCT_D_uv, shape)
[docs] def CCT_to_uv_Ohno2013( CCT_D_uv: ArrayLike, cmfs: MultiSpectralDistributions | None = None ) -> NDArrayFloat: """ 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. References ---------- :cite:`Ohno2014a` Examples -------- >>> from colour import MSDS_CMFS, SPECTRAL_SHAPE_DEFAULT >>> cmfs = ( ... MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] ... .copy() ... .align(SPECTRAL_SHAPE_DEFAULT) ... ) >>> CCT_D_uv = np.array([6507.4342201047066, 0.003223690901513]) >>> CCT_to_uv_Ohno2013(CCT_D_uv, cmfs) # doctest: +ELLIPSIS array([ 0.1977999..., 0.3122004...]) """ CCT, D_uv = tsplit(CCT_D_uv) cmfs, _illuminant = handle_spectral_arguments(cmfs) uv_0 = CCT_to_uv_Planck1900(CCT, cmfs) uv_1 = CCT_to_uv_Planck1900(CCT + 0.01, cmfs) du, dv = tsplit(uv_0 - uv_1) h = np.hypot(du, dv) with sdiv_mode(): uv = tstack( [ uv_0[..., 0] - D_uv * sdiv(dv, h), uv_0[..., 1] + D_uv * sdiv(du, h), ] ) uv[D_uv == 0] = uv_0[D_uv == 0] return uv
[docs] def XYZ_to_CCT_Ohno2013( XYZ: ArrayLike, cmfs: MultiSpectralDistributions | None = None, start: float | None = None, end: float | None = None, spacing: float | None = None, ): """ Return the correlated colour temperature :math:`T_{cp}` and :math:`\\Delta_{uv}` from given *CIE XYZ* tristimulus values, colour matching functions and temperature range using *Ohno (2013)* method. Parameters ---------- XYZ *XYZ* 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, default to 1000. end Temperature range end in kelvin degrees, default to 100000. spacing Spacing between values of the underlying planckian table expressed as a multiplier. Default to 1.001. The closer to 1.0, the higher the precision of the returned colour temperature :math:`T_{cp}` and :math:`\\Delta_{uv}`. 1.01 provides a good balance between performance and accuracy. ``spacing`` value must be greater than 1. Returns ------- :class:`numpy.ndarray` Correlated colour temperature :math:`T_{cp}`, :math:`\\Delta_{uv}`. References ---------- :cite:`Ohno2014a` Examples -------- >>> from colour import MSDS_CMFS, SPECTRAL_SHAPE_DEFAULT >>> cmfs = ( ... MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] ... .copy() ... .align(SPECTRAL_SHAPE_DEFAULT) ... ) >>> XYZ = np.array([0.95035049, 1.0, 1.08935705]) >>> XYZ_to_CCT_Ohno2013(XYZ, cmfs) # doctest: +ELLIPSIS array([ 6.5074399...e+03, 3.2236914...e-03]) """ return uv_to_CCT_Ohno2013(UCS_to_uv(XYZ_to_UCS(XYZ)), cmfs, start, end, spacing)
[docs] def CCT_to_XYZ_Ohno2013( CCT_D_uv: ArrayLike, cmfs: MultiSpectralDistributions | None = None ): """ Return the *CIE XYZ* tristimulus values 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. Examples -------- >>> from colour import MSDS_CMFS, SPECTRAL_SHAPE_DEFAULT >>> cmfs = ( ... MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] ... .copy() ... .align(SPECTRAL_SHAPE_DEFAULT) ... ) >>> CCT_D_uv = np.array([6507.4342201047066, 0.003223690901513]) >>> CCT_to_XYZ_Ohno2013(CCT_D_uv, cmfs) # doctest: +ELLIPSIS array([ 0.9503504..., 1. , 1.0893570...]) """ return UCS_to_XYZ(uv_to_UCS(CCT_to_uv_Ohno2013(CCT_D_uv, cmfs)))