Source code for colour.blindness.machado2009

"""
Simulation of CVD - Machado, Oliveira and Fernandes (2009)
==========================================================

Define the *Machado et al. (2009)* objects for simulation of colour vision
deficiency:

-   :func:`colour.msds_cmfs_anomalous_trichromacy_Machado2009`
-   :func:`colour.matrix_anomalous_trichromacy_Machado2009`
-   :func:`colour.matrix_cvd_Machado2009`

References
----------
-   :cite:`Colblindora` : Colblindor. (n.d.). Deuteranopia - Red-Green Color
    Blindness. Retrieved July 4, 2015, from
    http://www.color-blindness.com/deuteranopia-red-green-color-blindness/
-   :cite:`Colblindorb` : Colblindor. (n.d.). Protanopia - Red-Green Color
    Blindness. Retrieved July 4, 2015, from
    http://www.color-blindness.com/protanopia-red-green-color-blindness/
-   :cite:`Colblindorc` : Colblindor. (n.d.). Tritanopia - Blue-Yellow Color
    Blindness. Retrieved July 4, 2015, from
    http://www.color-blindness.com/tritanopia-blue-yellow-color-blindness/
-   :cite:`Machado2009` : Machado, G.M., Oliveira, M. M., & Fernandes, L.
    (2009). A Physiologically-based Model for Simulation of Color Vision
    Deficiency. IEEE Transactions on Visualization and Computer Graphics,
    15(6), 1291-1298. doi:10.1109/TVCG.2009.113
"""

from __future__ import annotations

import numpy as np

from colour.algebra import vecmul
from colour.blindness import CVD_MATRICES_MACHADO2010
from colour.characterisation import RGB_DisplayPrimaries
from colour.colorimetry import (
    LMS_ConeFundamentals,
    SpectralShape,
    reshape_msds,
)
from colour.hints import ArrayLike, Literal, NDArrayFloat
from colour.utilities import (
    as_float_array,
    as_int_scalar,
    tsplit,
    tstack,
    usage_warning,
)

__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__ = [
    "MATRIX_LMS_TO_WSYBRG",
    "matrix_RGB_to_WSYBRG",
    "msds_cmfs_anomalous_trichromacy_Machado2009",
    "matrix_anomalous_trichromacy_Machado2009",
    "matrix_cvd_Machado2009",
]

MATRIX_LMS_TO_WSYBRG: NDArrayFloat = np.array(
    [
        [0.600, 0.400, 0.000],
        [0.240, 0.105, -0.700],
        [1.200, -1.600, 0.400],
    ]
)
"""
Ingling and Tsou (1977) matrix converting from cones responses to
opponent-colour space.
"""


def matrix_RGB_to_WSYBRG(
    cmfs: LMS_ConeFundamentals, primaries: RGB_DisplayPrimaries
) -> NDArrayFloat:
    """
    Compute the matrix transforming from *RGB* colourspace to opponent-colour
    space using *Machado et al. (2009)* method.

    Parameters
    ----------
    cmfs
        *LMS* cone fundamentals colour matching functions.
    primaries
        *RGB* display primaries tri-spectral distributions.

    Returns
    -------
    :class:`numpy.ndarray`
        Matrix transforming from *RGB* colourspace to opponent-colour space.

    Examples
    --------
    >>> from colour.characterisation import MSDS_DISPLAY_PRIMARIES
    >>> from colour.colorimetry import MSDS_CMFS_LMS
    >>> cmfs = MSDS_CMFS_LMS["Stockman & Sharpe 2 Degree Cone Fundamentals"]
    >>> d_LMS = np.array([15, 0, 0])
    >>> primaries = MSDS_DISPLAY_PRIMARIES["Apple Studio Display"]
    >>> matrix_RGB_to_WSYBRG(cmfs, primaries)  # doctest: +ELLIPSIS
    array([[  0.2126535...,   0.6704626...,   0.1168838...],
           [  4.7095295...,  12.4862869..., -16.1958165...],
           [-11.1518474...,  15.2534789...,  -3.1016315...]])
    """

    wavelengths = cmfs.wavelengths
    WSYBRG = vecmul(MATRIX_LMS_TO_WSYBRG, cmfs.values)
    WS, YB, RG = tsplit(WSYBRG)

    primaries = reshape_msds(
        primaries,
        cmfs.shape,
        copy=False,
        extrapolator_kwargs={"method": "Constant", "left": 0, "right": 0},
    )

    R, G, B = tsplit(primaries.values)

    WS_R = np.trapezoid(R * WS, wavelengths)  # pyright: ignore
    WS_G = np.trapezoid(G * WS, wavelengths)  # pyright: ignore
    WS_B = np.trapezoid(B * WS, wavelengths)  # pyright: ignore

    YB_R = np.trapezoid(R * YB, wavelengths)  # pyright: ignore
    YB_G = np.trapezoid(G * YB, wavelengths)  # pyright: ignore
    YB_B = np.trapezoid(B * YB, wavelengths)  # pyright: ignore

    RG_R = np.trapezoid(R * RG, wavelengths)  # pyright: ignore
    RG_G = np.trapezoid(G * RG, wavelengths)  # pyright: ignore
    RG_B = np.trapezoid(B * RG, wavelengths)  # pyright: ignore

    M_G = as_float_array(
        [
            [WS_R, WS_G, WS_B],
            [YB_R, YB_G, YB_B],
            [RG_R, RG_G, RG_B],
        ]
    )

    M_G /= np.sum(M_G, axis=-1)[:, None]

    return M_G


[docs] def msds_cmfs_anomalous_trichromacy_Machado2009( cmfs: LMS_ConeFundamentals, d_LMS: ArrayLike ) -> LMS_ConeFundamentals: """ Shift given *LMS* cone fundamentals colour matching functions with given :math:`\\Delta_{LMS}` shift amount in nanometers to simulate anomalous trichromacy using *Machado et al. (2009)* method. Parameters ---------- cmfs *LMS* cone fundamentals colour matching functions. d_LMS :math:`\\Delta_{LMS}` shift amount in nanometers. Notes ----- - Input *LMS* cone fundamentals colour matching functions interval is expected to be 1 nanometer, incompatible input will be interpolated at 1 nanometer interval. - Input :math:`\\Delta_{LMS}` shift amount is in domain [0, 20]. Returns ------- :class:`colour.LMS_ConeFundamentals` Anomalous trichromacy *LMS* cone fundamentals colour matching functions. Warnings -------- *Machado et al. (2009)* simulation of tritanomaly is based on the shift paradigm as an approximation to the actual phenomenon and restrain the model from trying to model tritanopia. The pre-generated matrices are using a shift value in domain [5, 59] contrary to the domain [0, 20] used for protanomaly and deuteranomaly simulation. References ---------- :cite:`Colblindorb`, :cite:`Colblindora`, :cite:`Colblindorc`, :cite:`Machado2009` Examples -------- >>> from colour.colorimetry import MSDS_CMFS_LMS >>> cmfs = MSDS_CMFS_LMS["Stockman & Sharpe 2 Degree Cone Fundamentals"] >>> cmfs[450] array([ 0.0498639, 0.0870524, 0.955393 ]) >>> msds_cmfs_anomalous_trichromacy_Machado2009(cmfs, np.array([15, 0, 0]))[ ... 450 ... ] # doctest: +ELLIPSIS array([ 0.0891288..., 0.0870524 , 0.955393 ]) """ cmfs = cmfs.copy() if cmfs.shape.interval != 1: cmfs.interpolate(SpectralShape(cmfs.shape.start, cmfs.shape.end, 1)) cmfs.extrapolator_kwargs = {"method": "Constant", "left": 0, "right": 0} L, M, _S = tsplit(cmfs.values) d_L, d_M, d_S = tsplit(d_LMS) if d_S != 0: usage_warning( '"Machado et al. (2009)" simulation of tritanomaly is based on ' "the shift paradigm as an approximation to the actual phenomenon " "and restrain the model from trying to model tritanopia.\n" "The pre-generated matrices are using a shift value in domain " "[5, 59] contrary to the domain [0, 20] used for protanomaly and " "deuteranomaly simulation." ) area_L = np.trapezoid(L, cmfs.wavelengths) # pyright: ignore area_M = np.trapezoid(M, cmfs.wavelengths) # pyright: ignore def alpha(x: NDArrayFloat) -> NDArrayFloat: """Compute :math:`alpha` factor.""" return (20 - x) / 20 # Corrected equations as per: # http://www.inf.ufrgs.br/~oliveira/pubs_files/ # CVD_Simulation/CVD_Simulation.html#Errata L_a = alpha(d_L) * L + 0.96 * area_L / area_M * (1 - alpha(d_L)) * M M_a = alpha(d_M) * M + 1 / 0.96 * area_M / area_L * (1 - alpha(d_M)) * L S_a = cmfs[cmfs.wavelengths - d_S][:, 2] LMS_a = tstack([L_a, M_a, S_a]) cmfs[cmfs.wavelengths] = LMS_a severity = f"{d_L}, {d_M}, {d_S}" template = "{0} - Anomalous Trichromacy ({1})" cmfs.name = template.format(cmfs.name, severity) cmfs.display_name = template.format(cmfs.display_name, severity) return cmfs
[docs] def matrix_anomalous_trichromacy_Machado2009( cmfs: LMS_ConeFundamentals, primaries: RGB_DisplayPrimaries, d_LMS: ArrayLike, ) -> NDArrayFloat: """ Compute the *Machado et al. (2009)* *CVD* matrix for given *LMS* cone fundamentals colour matching functions and display primaries tri-spectral distributions with given :math:`\\Delta_{LMS}` shift amount in nanometers to simulate anomalous trichromacy. Parameters ---------- cmfs *LMS* cone fundamentals colour matching functions. primaries *RGB* display primaries tri-spectral distributions. d_LMS :math:`\\Delta_{LMS}` shift amount in nanometers. Notes ----- - Input *LMS* cone fundamentals colour matching functions interval is expected to be 1 nanometer, incompatible input will be interpolated at 1 nanometer interval. - Input :math:`\\Delta_{LMS}` shift amount is in domain [0, 20]. Returns ------- :class:`numpy.ndarray` Anomalous trichromacy matrix. References ---------- :cite:`Colblindorb`, :cite:`Colblindora`, :cite:`Colblindorc`, :cite:`Machado2009` Examples -------- >>> from colour.characterisation import MSDS_DISPLAY_PRIMARIES >>> from colour.colorimetry import MSDS_CMFS_LMS >>> cmfs = MSDS_CMFS_LMS["Stockman & Sharpe 2 Degree Cone Fundamentals"] >>> d_LMS = np.array([15, 0, 0]) >>> primaries = MSDS_DISPLAY_PRIMARIES["Apple Studio Display"] >>> matrix_anomalous_trichromacy_Machado2009(cmfs, primaries, d_LMS) ... # doctest: +ELLIPSIS array([[-0.2777465..., 2.6515008..., -1.3737543...], [ 0.2718936..., 0.2004786..., 0.5276276...], [ 0.0064404..., 0.2592157..., 0.7343437...]]) """ if cmfs.shape.interval != 1: cmfs = reshape_msds( cmfs, SpectralShape(cmfs.shape.start, cmfs.shape.end, 1), "Interpolate", copy=False, ) M_n = matrix_RGB_to_WSYBRG(cmfs, primaries) cmfs_a = msds_cmfs_anomalous_trichromacy_Machado2009(cmfs, d_LMS) M_a = matrix_RGB_to_WSYBRG(cmfs_a, primaries) return np.matmul(np.linalg.inv(M_n), M_a)
[docs] def matrix_cvd_Machado2009( deficiency: Literal["Deuteranomaly", "Protanomaly", "Tritanomaly"] | str, severity: float, ) -> NDArrayFloat: """ Compute *Machado et al. (2009)* *CVD* matrix for given deficiency and severity using the pre-computed matrices dataset. Parameters ---------- deficiency Colour blindness / vision deficiency types : - *Protanomaly* : defective long-wavelength cones (L-cones). The complete absence of L-cones is known as *Protanopia* or *red-dichromacy*. - *Deuteranomaly* : defective medium-wavelength cones (M-cones) with peak of sensitivity moved towards the red sensitive cones. The complete absence of M-cones is known as *Deuteranopia*. - *Tritanomaly* : defective short-wavelength cones (S-cones), an alleviated form of blue-yellow color blindness. The complete absence of S-cones is known as *Tritanopia*. severity Severity of the colour vision deficiency in domain [0, 1]. Returns ------- :class:`numpy.ndarray` *CVD* matrix. References ---------- :cite:`Colblindorb`, :cite:`Colblindora`, :cite:`Colblindorc`, :cite:`Machado2009` Examples -------- >>> matrix_cvd_Machado2009("Protanomaly", 0.15) # doctest: +ELLIPSIS array([[ 0.7869875..., 0.2694875..., -0.0564735...], [ 0.0431695..., 0.933774 ..., 0.023058 ...], [-0.004238 ..., -0.0024515..., 1.0066895...]]) """ if deficiency.lower() == "tritanomaly": usage_warning( '"Machado et al. (2009)" simulation of tritanomaly is based on ' "the shift paradigm as an approximation to the actual phenomenon " "and restrain the model from trying to model tritanopia.\n" "The pre-generated matrices are using a shift value in domain " "[5, 59] contrary to the domain [0, 20] used for protanomaly and " "deuteranomaly simulation." ) matrices = CVD_MATRICES_MACHADO2010[deficiency] samples = np.array(sorted(matrices.keys())) index = as_int_scalar( np.clip(np.searchsorted(samples, severity), 0, len(samples) - 1) ) a = samples[index] b = samples[min(index + 1, len(samples) - 1)] m1, m2 = matrices[a], matrices[b] if a == b: # 1.0 severity CVD matrix, returning directly. return m1 else: return m1 + (severity - a) * ((m2 - m1) / (b - a))