#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
CIECAM02 Colour Appearance Model
================================
Defines *CIECAM02* colour appearance model objects:
- :class:`CIECAM02_InductionFactors`
- :attr:`CIECAM02_VIEWING_CONDITIONS`
- :class:`CIECAM02_Specification`
- :func:`XYZ_to_CIECAM02`
- :func:`CIECAM02_to_XYZ`
See Also
--------
`CIECAM02 Colour Appearance Model IPython Notebook
<http://nbviewer.ipython.org/github/colour-science/colour-ipython/blob/master/notebooks/appearance/ciecam02.ipynb>`_ # noqa
References
----------
.. [1] http://en.wikipedia.org/wiki/CIECAM02
(Last accessed 14 August 2014)
.. [2] **Mark D. Fairchild**, *Color Appearance Models, 2nd Edition*,
The Wiley-IS&T Series in Imaging Science and Technology,
published 19 November 2004, ISBN-13: 978-0470012161,
pages 265-277.
.. [3] **Stephen Westland, Caterina Ripamonti, Vien Cheung**,
*Computational Colour Science Using MATLAB, 2nd Edition*,
The Wiley-IS&T Series in Imaging Science and Technology,
published July 2012, ISBN-13: 978-0-470-66569-5, page 38.
.. [4] `The CIECAM02 Color Appearance Model
<http://rit-mcsl.org/fairchild/PDFs/PRO19.pdf>`_
(Last accessed 30 July 2014)
"""
from __future__ import division, unicode_literals
import bisect
try:
from functools import lru_cache
except ImportError:
from backports.functools_lru_cache import lru_cache
import math
import numpy as np
from collections import namedtuple
from colour.adaptation.cat import CAT02_CAT, CAT02_INVERSE_CAT
from colour.appearance.hunt import (XYZ_TO_HPE_MATRIX,
HPE_TO_XYZ_MATRIX,
luminance_level_adaptation_factor)
from colour.utilities import CaseInsensitiveMapping
__author__ = 'Colour Developers'
__copyright__ = 'Copyright (C) 2013 - 2014 - Colour Developers'
__license__ = 'New BSD License - http://opensource.org/licenses/BSD-3-Clause'
__maintainer__ = 'Colour Developers'
__email__ = 'colour-science@googlegroups.com'
__status__ = 'Production'
__all__ = ['CIECAM02_InductionFactors',
'CIECAM02_VIEWING_CONDITIONS',
'HUE_DATA_FOR_HUE_QUADRATURE',
'CIECAM02_Specification',
'XYZ_to_CIECAM02',
'CIECAM02_to_XYZ',
'chromatic_induction_factors',
'base_exponential_non_linearity',
'viewing_condition_dependent_parameters',
'degree_of_adaptation',
'full_chromatic_adaptation_forward',
'full_chromatic_adaptation_reverse',
'RGB_to_rgb',
'rgb_to_RGB',
'post_adaptation_non_linear_response_compression_forward',
'post_adaptation_non_linear_response_compression_reverse',
'opponent_colour_dimensions_forward',
'opponent_colour_dimensions_reverse',
'hue_angle',
'hue_quadrature',
'eccentricity_factor',
'achromatic_response_forward',
'achromatic_response_reverse',
'lightness_correlate',
'brightness_correlate',
'temporary_magnitude_quantity_forward',
'temporary_magnitude_quantity_reverse',
'chroma_correlate',
'colourfulness_correlate',
'saturation_correlate',
'P',
'post_adaptation_non_linear_response_compression_matrix']
[docs]class CIECAM02_InductionFactors(
namedtuple('CIECAM02_InductionFactors',
('F', 'c', 'N_c'))):
"""
*CIECAM02* colour appearance model induction factors.
Parameters
----------
F : numeric
Maximum degree of adaptation :math:`F`.
c : numeric
Exponential non linearity :math:`c`.
N_c : numeric
Chromatic induction factor :math:`N_c`.
"""
CIECAM02_VIEWING_CONDITIONS = CaseInsensitiveMapping(
{'Average': CIECAM02_InductionFactors(1, 0.69, 1),
'Dim': CIECAM02_InductionFactors(0.9, 0.59, 0.95),
'Dark': CIECAM02_InductionFactors(0.8, 0.525, 0.8)})
"""
Reference *CIECAM02* colour appearance model viewing conditions.
CIECAM02_VIEWING_CONDITIONS : dict
('Average', 'Dim', 'Dark')
"""
HUE_DATA_FOR_HUE_QUADRATURE = {
'h_i': np.array([20.14, 90.00, 164.25, 237.53, 380.14]),
'e_i': np.array([0.8, 0.7, 1.0, 1.2, 0.8]),
'H_i': np.array([0.0, 100.0, 200.0, 300.0, 400.0])}
[docs]class CIECAM02_Specification(
namedtuple('CIECAM02_Specification',
('J', 'C', 'h', 's', 'Q', 'M', 'H', 'HC'))):
"""
Defines the *CIECAM02* colour appearance model specification.
Parameters
----------
J : numeric
Correlate of *Lightness* :math:`J`.
C : numeric
Correlate of *chroma* :math:`C`.
h : numeric
*Hue* angle :math:`h` in degrees.
s : numeric
Correlate of *saturation* :math:`s`.
Q : numeric
Correlate of *brightness* :math:`Q`.
M : numeric
Correlate of *colourfulness* :math:`M`.
H : numeric
*Hue* :math:`h` quadrature :math:`H`.
HC : numeric
*Hue* :math:`h` composition :math:`H^C`.
"""
[docs]def XYZ_to_CIECAM02(XYZ,
XYZ_w,
L_A,
Y_b,
surround=CIECAM02_VIEWING_CONDITIONS.get('Average'),
discount_illuminant=False):
"""
Computes the *CIECAM02* colour appearance model correlates from given
*CIE XYZ* colourspace matrix.
This is the *forward* implementation.
Parameters
----------
XYZ : array_like, (3,)
*CIE XYZ* colourspace matrix of test sample / stimulus in domain
[0, 100].
XYZ_w : array_like, (3,)
*CIE XYZ* colourspace matrix of reference white in domain [0, 100].
L_A : numeric
Adapting field *luminance* :math:`L_A` in :math:`cd/m^2`.
Y_b : numeric
Adapting field *Y* tristimulus value :math:`Y_b`.
surround : CIECAM02_InductionFactors, optional
Surround viewing conditions induction factors.
discount_illuminant : bool, optional
Truth value indicating if the illuminant should be discounted.
Returns
-------
CIECAM02_Specification
*CIECAM02* colour appearance model specification.
Warning
-------
The input domain of that definition is non standard!
Notes
-----
- Input *CIE XYZ* colourspace matrix is in domain [0, 100].
- Input *CIE XYZ_w* colourspace matrix is in domain [0, 100].
Examples
--------
>>> XYZ = np.array([19.01, 20.00, 21.78])
>>> XYZ_w = np.array([95.05, 100.00, 108.88])
>>> L_A = 318.31
>>> Y_b = 20.0
>>> surround = CIECAM02_VIEWING_CONDITIONS['Average']
>>> XYZ_to_CIECAM02(XYZ, XYZ_w, L_A, Y_b, surround) # doctest: +ELLIPSIS
CIECAM02_Specification(J=41.7310911..., C=0.1047077..., h=219.0484326..., s=2.3603053..., Q=195.3713259..., M=0.1088421..., H=278.0607358..., HC=None)
"""
XYZ = np.array(XYZ).reshape((3, 1))
XYZ_w = np.array(XYZ_w).reshape((3, 1))
X_w, Y_w, Z_w = np.ravel(XYZ_w)
n, F_L, N_bb, N_cb, z = viewing_condition_dependent_parameters(Y_b,
Y_w,
L_A)
# Converting *CIE XYZ* colourspace matrices to *CMCCAT2000* transform
# sharpened *RGB* values.
RGB = np.dot(CAT02_CAT, XYZ)
RGB_w = np.dot(CAT02_CAT, XYZ_w)
# Computing degree of adaptation :math:`D`.
D = degree_of_adaptation(surround.F,
L_A) if not discount_illuminant else 1
# Computing full chromatic adaptation.
RGB_c = full_chromatic_adaptation_forward(RGB, RGB_w, Y_w, D)
RGB_wc = full_chromatic_adaptation_forward(RGB_w, RGB_w, Y_w, D)
# Converting to *Hunt-Pointer-Estevez* colourspace.
RGB_p = RGB_to_rgb(RGB_c)
RGB_pw = RGB_to_rgb(RGB_wc)
# Applying forward post-adaptation non linear response compression.
RGB_a = post_adaptation_non_linear_response_compression_forward(
RGB_p, F_L)
RGB_aw = post_adaptation_non_linear_response_compression_forward(
RGB_pw, F_L)
# Converting to preliminary cartesian coordinates.
a, b = opponent_colour_dimensions_forward(RGB_a)
# -------------------------------------------------------------------------
# Computing the *hue* angle :math:`h`.
h = hue_angle(a, b)
# -------------------------------------------------------------------------
# Computing hue :math:`h` quadrature :math:`H`.
H = hue_quadrature(h)
# TODO: Compute hue composition.
# Computing eccentricity factor *e_t*.
e_t = eccentricity_factor(h)
# Computing achromatic responses for the stimulus and the whitepoint.
A = achromatic_response_forward(RGB_a, N_bb)
A_w = achromatic_response_forward(RGB_aw, N_bb)
# -------------------------------------------------------------------------
# Computing the correlate of *Lightness* :math:`J`.
# -------------------------------------------------------------------------
J = lightness_correlate(A, A_w, surround.c, z)
# -------------------------------------------------------------------------
# Computing the correlate of *brightness* :math:`Q`.
# -------------------------------------------------------------------------
Q = brightness_correlate(surround.c, J, A_w, F_L)
# -------------------------------------------------------------------------
# Computing the correlate of *chroma* :math:`C`.
# -------------------------------------------------------------------------
C = chroma_correlate(J, n, surround.N_c, N_cb, e_t, a, b, RGB_a)
# -------------------------------------------------------------------------
# Computing the correlate of *colourfulness* :math:`M`.
# -------------------------------------------------------------------------
M = colourfulness_correlate(C, F_L)
# -------------------------------------------------------------------------
# Computing the correlate of *saturation* :math:`s`.
# -------------------------------------------------------------------------
s = saturation_correlate(M, Q)
return CIECAM02_Specification(J, C, h, s, Q, M, H, None)
[docs]def CIECAM02_to_XYZ(J, C, h,
XYZ_w,
L_A,
Y_b,
surround=CIECAM02_VIEWING_CONDITIONS.get(
'Average'),
discount_illuminant=False):
"""
Converts *CIECAM02* specification to *CIE XYZ* colourspace matrix.
This is the *reverse* implementation.
Parameters
----------
CIECAM02_Specification : CIECAM02_Specification
*CIECAM02* specification.
XYZ_w : array_like
*CIE XYZ* colourspace matrix of reference white.
L_A : numeric
Adapting field *luminance* :math:`L_A` in :math:`cd/m^2`.
Y_b : numeric
Adapting field *Y* tristimulus value :math:`Y_b`.
surround : CIECAM02_Surround, optional
Surround viewing conditions.
discount_illuminant : bool, optional
Discount the illuminant.
Returns
-------
XYZ : ndarray
*CIE XYZ* colourspace matrix.
Warning
-------
The output domain of that definition is non standard!
Notes
-----
- Input *CIE XYZ_w* colourspace matrix is in domain [0, 100].
- Output *CIE XYZ* colourspace matrix is in domain [0, 100].
Examples
--------
>>> J = 41.731091132513917
>>> C = 0.1047077571711053
>>> h = 219.0484326582719
>>> XYZ_w = np.array([95.05, 100.00, 108.88])
>>> L_A = 318.31
>>> Y_b = 20.0
>>> CIECAM02_to_XYZ(J, C, h, XYZ_w, L_A, Y_b) # doctest: +ELLIPSIS
array([ 19.01..., 20... , 21.78...])
"""
XYZ_w = np.array(XYZ_w).reshape((3, 1))
X_w, Y_w, Zw = np.ravel(XYZ_w)
n, F_L, N_bb, N_cb, z = viewing_condition_dependent_parameters(Y_b,
Y_w,
L_A)
# Converting *CIE XYZ* colourspace matrices to *CMCCAT2000* transform
# sharpened *RGB* values.
RGB_w = np.dot(CAT02_CAT, XYZ_w)
# Computing degree of adaptation :math:`D`.
D = degree_of_adaptation(surround.F,
L_A) if not discount_illuminant else 1
# Computation full chromatic adaptation.
RGB_wc = full_chromatic_adaptation_forward(RGB_w, RGB_w, Y_w, D)
# Converting to *Hunt-Pointer-Estevez* colourspace.
RGB_pw = RGB_to_rgb(RGB_wc)
# Applying post-adaptation non linear response compression.
RGB_aw = post_adaptation_non_linear_response_compression_forward(
RGB_pw, F_L)
# Computing achromatic responses for the stimulus and the whitepoint.
A_w = achromatic_response_forward(RGB_aw, N_bb)
# Computing temporary magnitude quantity :math:`t`.
t = temporary_magnitude_quantity_reverse(C, J, n)
# Computing eccentricity factor *e_t*.
e_t = eccentricity_factor(h)
# Computing achromatic response :math:`A` for the stimulus.
A = achromatic_response_reverse(A_w, J, surround.c, z)
# Computing *P_1* to *P_3*.
P_1, P_2, P_3 = P(surround.N_c, N_cb, e_t, t, A, N_bb)
# Computing opponent colour dimensions :math:`a` and :math:`b`.
a, b = opponent_colour_dimensions_reverse((P_1, P_2, P_3), h)
# Computing post-adaptation non linear response compression matrix.
RGB_a = post_adaptation_non_linear_response_compression_matrix(P_2, a,
b)
# Applying reverse post-adaptation non linear response compression.
RGB_p = post_adaptation_non_linear_response_compression_reverse(RGB_a,
F_L)
# Converting to *Hunt-Pointer-Estevez* colourspace.
RGB_c = rgb_to_RGB(RGB_p)
# Applying reverse full chromatic adaptation.
RGB = full_chromatic_adaptation_reverse(RGB_c, RGB_w, Y_w, D)
# Converting *CMCCAT2000* transform sharpened *RGB* values to *CIE XYZ*
# colourspace matrices.
XYZ = np.dot(CAT02_INVERSE_CAT, RGB)
return XYZ
[docs]def chromatic_induction_factors(n):
"""
Returns the chromatic induction factors :math:`N_{bb}` and :math:`N_{cb}`.
Parameters
----------
n : numeric
Function of the luminance factor of the background :math:`n`.
Returns
-------
tuple
Chromatic induction factors :math:`N_{bb}` and :math:`N_{cb}`.
Examples
--------
>>> chromatic_induction_factors(0.2) # doctest: +ELLIPSIS
(1.0003040..., 1.0003040...)
"""
N_bb = N_cb = 0.725 * (1 / n) ** 0.2
return N_bb, N_cb
[docs]def base_exponential_non_linearity(n):
"""
Returns the base exponential non linearity :math:`n`.
Parameters
----------
n : numeric
Function of the luminance factor of the background :math:`n`.
Returns
-------
numeric
Base exponential non linearity :math:`z`.
Examples
--------
>>> base_exponential_non_linearity(0.2) # doctest: +ELLIPSIS
1.9272135...
"""
z = 1.48 + math.sqrt(n)
return z
@lru_cache(maxsize=8192)
[docs]def viewing_condition_dependent_parameters(Y_b, Y_w, L_A):
"""
Returns the viewing condition dependent parameters.
Parameters
----------
Y_b : numeric
Adapting field *Y* tristimulus value :math:`Y_b`.
Y_w : numeric
Whitepoint *Y* tristimulus value :math:`Y_w`.
L_A : numeric
Adapting field *luminance* :math:`L_A` in :math:`cd/m^2`.
Returns
-------
tuple
Viewing condition dependent parameters.
Examples
--------
>>> viewing_condition_dependent_parameters(20.0, 100.0, 318.31) # noqa # doctest: +ELLIPSIS
(0.2000000..., 1.1675444..., 1.0003040..., 1.0003040..., 1.9272135...)
"""
n = Y_b / Y_w
F_L = luminance_level_adaptation_factor(L_A)
N_bb, N_cb = chromatic_induction_factors(n)
z = base_exponential_non_linearity(n)
return n, F_L, N_bb, N_cb, z
[docs]def degree_of_adaptation(F, L_A):
"""
Returns the degree of adaptation :math:`D` from given surround maximum
degree of adaptation :math:`F` and Adapting field *luminance* :math:`L_A`
in :math:`cd/m^2`.
Parameters
----------
F : numeric
Surround maximum degree of adaptation :math:`F`.
L_A : numeric
Adapting field *luminance* :math:`L_A` in :math:`cd/m^2`.
Returns
-------
numeric
Degree of adaptation :math:`D`.
Examples
--------
>>> degree_of_adaptation(1.0, 318.31) # doctest: +ELLIPSIS
0.9944687...
"""
D = F * (1 - (1 / 3.6) * np.exp((-L_A - 42) / 92))
return D
[docs]def full_chromatic_adaptation_forward(RGB, RGB_w, Y_w, D):
"""
Applies full chromatic adaptation to given *CMCCAT2000* transform sharpened
*RGB* matrix using given *CMCCAT2000* transform sharpened whitepoint
*RGB_w* matrix.
Parameters
----------
RGB : array_like
*CMCCAT2000* transform sharpened *RGB* matrix.
RGB_w : array_like
*CMCCAT2000* transform sharpened whitepoint *RGB_w* matrix.
Y_w : numeric
Whitepoint *Y* tristimulus value :math:`Y_w`.
D : numeric
Degree of adaptation :math:`D`.
Returns
-------
ndarray, (3,)
Adapted *RGB* matrix.
Examples
--------
>>> RGB = np.array([18.985456, 20.707422, 21.747482])
>>> RGB_w = np.array([94.930528, 103.536988, 108.717742])
>>> Y_w = 100.0
>>> D = 0.994468780088
>>> full_chromatic_adaptation_forward(RGB, RGB_w, Y_w, D) # noqa # doctest: +ELLIPSIS
array([ 19.9937078..., 20.0039363..., 20.0132638...])
"""
R, G, B = np.ravel(RGB)
R_w, G_w, B_w = np.ravel(RGB_w)
equation = lambda x, y: ((Y_w * D / y) + 1 - D) * x
R_c = equation(R, R_w)
G_c = equation(G, G_w)
B_c = equation(B, B_w)
return np.array([R_c, G_c, B_c])
[docs]def full_chromatic_adaptation_reverse(RGB, RGB_w, Y_w, D):
"""
Reverts full chromatic adaptation of given *CMCCAT2000* transform sharpened
*RGB* matrix using given *CMCCAT2000* transform sharpened whitepoint
*RGB_w* matrix.
Parameters
----------
RGB : array_like
*CMCCAT2000* transform sharpened *RGB* matrix.
RGB_w : array_like
*CMCCAT2000* transform sharpened whitepoint *RGB_w* matrix.
Y_w : numeric
Whitepoint *Y* tristimulus value :math:`Y_w`.
D : numeric
Degree of adaptation :math:`D`.
Returns
-------
ndarray, (3,)
Adapted *RGB* matrix.
Examples
--------
>>> RGB = np.array([19.99370783, 20.00393634, 20.01326387])
>>> RGB_w = np.array([94.930528, 103.536988, 108.717742])
>>> Y_w = 100.0
>>> D = 0.994468780088
>>> full_chromatic_adaptation_reverse(RGB, RGB_w, Y_w, D)
array([ 18.985456, 20.707422, 21.747482])
"""
R, G, B = np.ravel(RGB)
R_w, G_w, B_w = np.ravel(RGB_w)
equation = lambda x, y: x / (Y_w * (D / y) + 1 - D)
R_c = equation(R, R_w)
G_c = equation(G, G_w)
B_c = equation(B, B_w)
return np.array([R_c, G_c, B_c])
[docs]def RGB_to_rgb(RGB):
"""
Converts given *RGB* matrix to *Hunt-Pointer-Estevez*
:math:`\\rho\gamma\\beta` colourspace.
Parameters
----------
RGB : array_like, (3,)
*RGB* matrix.
Returns
-------
ndarray, (3,)
*Hunt-Pointer-Estevez* :math:`\\rho\gamma\\beta` colourspace matrix.
Examples
--------
>>> RGB = np.array([19.99370783, 20.00393634, 20.01326387])
>>> RGB_to_rgb(RGB) # doctest: +ELLIPSIS
array([ 19.9969397..., 20.0018612..., 20.0135053...])
"""
rgb = np.dot(np.dot(XYZ_TO_HPE_MATRIX, CAT02_INVERSE_CAT), RGB)
return rgb
[docs]def rgb_to_RGB(rgb):
"""
Converts given *Hunt-Pointer-Estevez* :math:`\\rho\gamma\\beta` colourspace
matrix to *RGB* matrix.
Parameters
----------
rgb : array_like, (3,)
*Hunt-Pointer-Estevez* :math:`\\rho\gamma\\beta` colourspace matrix.
Returns
-------
ndarray, (3,)
*RGB* matrix.
Examples
--------
>>> rgb = np.array([19.99693975, 20.00186123, 20.0135053])
>>> rgb_to_RGB(rgb) # doctest: +ELLIPSIS
array([ 19.9937078..., 20.0039363..., 20.0132638...])
"""
RGB = np.dot(np.dot(CAT02_CAT, HPE_TO_XYZ_MATRIX), rgb)
return RGB
[docs]def post_adaptation_non_linear_response_compression_forward(RGB, F_L):
"""
Returns given *CMCCAT2000* transform sharpened *RGB* matrix with post
adaptation non linear response compression.
Parameters
----------
RGB : array_like
*CMCCAT2000* transform sharpened *RGB* matrix.
Returns
-------
ndarray, (3,)
Compressed *CMCCAT2000* transform sharpened *RGB* matrix.
Examples
--------
>>> RGB = np.array([19.99693975, 20.00186123, 20.0135053])
>>> F_L = 1.16754446415
>>> post_adaptation_non_linear_response_compression_forward(RGB, F_L) # noqa # doctest: +ELLIPSIS
array([ 7.9463202..., 7.9471152..., 7.9489959...])
"""
# TODO: Check for negative values and their handling.
RGB_c = ((((400 * (F_L * RGB / 100) ** 0.42) /
(27.13 + (F_L * RGB / 100) ** 0.42))) + 0.1)
return RGB_c
[docs]def post_adaptation_non_linear_response_compression_reverse(RGB, F_L):
"""
Returns given *CMCCAT2000* transform sharpened *RGB* matrix without post
adaptation non linear response compression.
Parameters
----------
RGB : array_like
*CMCCAT2000* transform sharpened *RGB* matrix.
Returns
-------
ndarray, (3,)
Uncompressed *CMCCAT2000* transform sharpened *RGB* matrix.
Examples
--------
>>> RGB = np.array([7.9463202, 7.94711528, 7.94899595])
>>> F_L = 1.16754446415
>>> post_adaptation_non_linear_response_compression_reverse(RGB, F_L) # noqa # doctest: +ELLIPSIS
array([ 19.9969397..., 20.0018612..., 20.0135052...])
"""
RGB_p = ((np.sign(RGB - 0.1) *
(100 / F_L) * ((27.13 * np.abs(RGB - 0.1)) /
(400 - np.abs(RGB - 0.1))) ** (1 / 0.42)))
return RGB_p
[docs]def opponent_colour_dimensions_forward(RGB):
"""
Returns opponent colour dimensions from given compressed *CMCCAT2000*
transform sharpened *RGB* matrix for forward *CIECAM02* implementation
Parameters
----------
RGB : array_like
Compressed *CMCCAT2000* transform sharpened *RGB* matrix.
Returns
-------
tuple
Opponent colour dimensions.
Examples
--------
>>> RGB = np.array([7.9463202, 7.94711528, 7.94899595])
>>> opponent_colour_dimensions_forward(RGB) # doctest: +ELLIPSIS
(-0.0006241..., -0.0005062...)
"""
R, G, B = np.ravel(RGB)
a = R - 12 * G / 11 + B / 11
b = (R + G - 2 * B) / 9
return a, b
[docs]def opponent_colour_dimensions_reverse(P, h):
"""
Returns opponent colour dimensions from given points :math:`P` and hue
:math:`h` in degrees for reverse *CIECAM02* implementation.
Parameters
----------
p : array_like
Points :math:`P`.
h : numeric
Hue :math:`h` in degrees.
Returns
-------
tuple
Opponent colour dimensions.
Examples
--------
>>> p = (30162.890815335879, 24.237205467134817, 1.05)
>>> h = -140.9515673417281
>>> opponent_colour_dimensions_reverse(p, h) # doctest: +ELLIPSIS
(-0.0006241..., -0.0005062...)
"""
P_1, P_2, P_3 = P
hr = math.radians(h)
sin_hr, cos_hr = math.sin(hr), math.cos(hr)
P_4 = P_1 / sin_hr
P_5 = P_1 / cos_hr
n = P_2 * (2 + P_3) * (460 / 1403)
if abs(sin_hr) >= abs(cos_hr):
b = n / (P_4 + (2 + P_3) * (220 / 1403) * (cos_hr / sin_hr) - (
27 / 1403) + P_3 * (6300 / 1403))
a = b * (cos_hr / sin_hr)
else:
a = n / (P_5 + (2 + P_3) * (220 / 1403) - (
(27 / 1403) - P_3 * (6300 / 1403)) * (sin_hr / cos_hr))
b = a * (sin_hr / cos_hr)
return a, b
[docs]def hue_angle(a, b):
"""
Returns the *hue* angle :math:`h` in degrees.
Parameters
----------
a : numeric
Opponent colour dimension :math:`a`.
b : numeric
Opponent colour dimension :math:`b`.
Returns
-------
numeric
*Hue* angle :math:`h` in degrees.
Examples
--------
>>> a = -0.0006241120682426434
>>> b = -0.0005062701067729668
>>> hue_angle(a, b) # doctest: +ELLIPSIS
219.0484326...
"""
h = math.degrees(np.arctan2(b, a)) % 360
return h
[docs]def hue_quadrature(h):
"""
Returns the hue quadrature from given hue :math:`h` angle in degrees.
Parameters
----------
h : numeric
Hue :math:`h` angle in degrees.
Returns
-------
numeric
Hue quadrature.
Examples
--------
>>> hue_quadrature(219.0484326582719) # doctest: +ELLIPSIS
278.0607358...
"""
h_i = HUE_DATA_FOR_HUE_QUADRATURE.get('h_i')
e_i = HUE_DATA_FOR_HUE_QUADRATURE.get('e_i')
H_i = HUE_DATA_FOR_HUE_QUADRATURE.get('H_i')
i = bisect.bisect_left(h_i, h) - 1
h_ii = h_i[i]
e_ii = e_i[i]
H_ii = H_i[i]
h_ii1 = h_i[i + 1]
e_ii1 = e_i[i + 1]
if h < 20.14:
H = 385.9
H += (14.1 * h / 0.856) / (h / 0.856 + (20.14 - h) / 0.8)
elif h >= 237.53:
H = H_ii
H += ((85.9 * (h - h_ii) / e_ii) /
((h - h_ii) / e_ii + (360 - h) / 0.856))
else:
H = H_ii
H += ((100 * (h - h_ii) / e_ii) /
((h - h_ii) / e_ii + (h_ii1 - h) / e_ii1))
return H
[docs]def eccentricity_factor(h):
"""
Returns the eccentricity factor :math:`e_t` from given hue :math:`h` angle
for forward *CIECAM02* implementation.
Parameters
----------
h : numeric
Hue :math:`h` angle in degrees.
Returns
-------
numeric
Eccentricity factor :math:`e_t`.
Examples
--------
>>> eccentricity_factor(-140.951567342) # doctest: +ELLIPSIS
1.1740054...
"""
e_t = 1 / 4 * (math.cos(2 + h * math.pi / 180) + 3.8)
return e_t
[docs]def achromatic_response_forward(RGB, N_bb):
"""
Returns the achromatic response :math:`A` from given compressed
*CMCCAT2000* transform sharpened *RGB* matrix and :math:`N_{bb}` chromatic
induction factor for forward *CIECAM02* implementation.
Parameters
----------
RGB : array_like
Compressed *CMCCAT2000* transform sharpened *RGB* matrix.
N_bb : numeric
Chromatic induction factor :math:`N_{bb}`.
Returns
-------
numeric
Achromatic response :math:`A`.
Examples
--------
>>> RGB = np.array([7.9463202, 7.94711528, 7.94899595])
>>> N_bb = 1.0003040045593807
>>> achromatic_response_forward(RGB, N_bb) # doctest: +ELLIPSIS
23.9394809...
"""
R, G, B = np.ravel(RGB)
A = (2 * R + G + (1 / 20) * B - 0.305) * N_bb
return A
[docs]def achromatic_response_reverse(A_w, J, c, z):
"""
Returns the achromatic response :math:`A` from given achromatic response
:math:`A_w` for the whitepoint, *Lightness* correlate :math:`J`, surround
exponential non linearity :math:`c` and base exponential non linearity
:math:`z` for reverse *CIECAM02* implementation.
Parameters
----------
A_w : numeric
Achromatic response :math:`A_w` for the whitepoint.
J : numeric
*Lightness* correlate :math:`J`.
c : numeric
Surround exponential non linearity :math:`c`.
z : numeric
Base exponential non linearity :math:`z`.
Returns
-------
numeric
Achromatic response :math:`A`.
Examples
--------
>>> A_w = 46.1882087914
>>> J = 41.73109113251392
>>> c = 0.69
>>> z = 1.9272135954999579
>>> achromatic_response_reverse(A_w, J, c, z) # doctest: +ELLIPSIS
23.9394809...
"""
A = A_w * (J / 100) ** (1 / (c * z))
return A
[docs]def lightness_correlate(A, A_w, c, z):
"""
Returns the *Lightness* correlate :math:`J`.
Parameters
----------
A : numeric
Achromatic response :math:`A` for the stimulus.
A_w : numeric
Achromatic response :math:`A_w` for the whitepoint.
c : numeric
Surround exponential non linearity :math:`c`.
z : numeric
Base exponential non linearity :math:`z`.
Returns
-------
numeric
*Lightness* correlate :math:`J`.
Examples
--------
>>> A = 23.9394809667
>>> A_w = 46.1882087914
>>> c = 0.69
>>> z = 1.9272135955
>>> lightness_correlate(A, A_w, c, z) # doctest: +ELLIPSIS
41.7310911...
"""
J = 100 * (A / A_w) ** (c * z)
return J
[docs]def brightness_correlate(c, J, A_w, F_L):
"""
Returns the *brightness* correlate :math:`Q`.
Parameters
----------
c : numeric
Surround exponential non linearity :math:`c`.
J : numeric
*Lightness* correlate :math:`J`.
A_w : numeric
Achromatic response :math:`A_w` for the whitepoint.
F_L : numeric
*Luminance* level adaptation factor :math:`F_L`.
Returns
-------
numeric
*Brightness* correlate :math:`Q`.
Examples
--------
>>> c = 0.69
>>> J = 41.7310911325
>>> A_w = 46.1882087914
>>> F_L = 1.16754446415
>>> brightness_correlate(c, J, A_w, F_L) # doctest: +ELLIPSIS
195.3713259...
"""
Q = (4 / c) * math.sqrt(J / 100) * (A_w + 4) * F_L ** 0.25
return Q
[docs]def temporary_magnitude_quantity_forward(N_c, N_cb, e_t, a, b, RGB_a):
"""
Returns the temporary magnitude quantity :math:`t`. for forward *CIECAM02*
implementation.
Parameters
----------
N_c : numeric
Surround chromatic induction factor :math:`N_{c}`.
N_cb : numeric
Chromatic induction factor :math:`N_{cb}`.
e_t : numeric
Eccentricity factor :math:`e_t`.
a : numeric
Opponent colour dimension :math:`a`.
b : numeric
Opponent colour dimension :math:`b`.
RGB_a : array_like
Compressed stimulus *CMCCAT2000* transform sharpened *RGB* matrix.
Returns
-------
numeric
Temporary magnitude quantity :math:`t`.
Examples
--------
>>> N_c = 1.0
>>> N_cb = 1.00030400456
>>> e_t = 1.1740054728519145
>>> a = -0.000624112068243
>>> b = -0.000506270106773
>>> RGB_a = np.array([7.9463202, 7.94711528, 7.94899595])
>>> temporary_magnitude_quantity_forward(N_c, N_cb, e_t, a, b, RGB_a) # noqa # doctest: +ELLIPSIS
0.1497462...
"""
Ra, Ga, Ba = np.ravel(RGB_a)
t = ((50000 / 13) * N_c * N_cb) * (e_t * (a ** 2 + b ** 2) ** 0.5) / (
Ra + Ga + 21 * Ba / 20)
return t
[docs]def temporary_magnitude_quantity_reverse(C, J, n):
"""
Returns the temporary magnitude quantity :math:`t`. for reverse *CIECAM02*
implementation.
Parameters
----------
C : numeric
*Chroma* correlate :math:`C`.
J : numeric
*Lightness* correlate :math:`J`.
n : numeric
Function of the luminance factor of the background :math:`n`.
Returns
-------
numeric
Temporary magnitude quantity :math:`t`.
Examples
--------
>>> C = 68.8364136888275
>>> J = 41.749268505999
>>> n = 0.2
>>> temporary_magnitude_quantity_reverse(C, J, n) # doctest: +ELLIPSIS
202.3873619...
"""
t = (C / (math.sqrt(J / 100) * (1.64 - 0.29 ** n) ** 0.73)) ** (1 / 0.9)
return t
[docs]def chroma_correlate(J, n, N_c, N_cb, e_t, a, b, RGB_a):
"""
Returns the *chroma* correlate :math:`C`.
Parameters
----------
J : numeric
*Lightness* correlate :math:`J`.
n : numeric
Function of the luminance factor of the background :math:`n`.
N_c : numeric
Surround chromatic induction factor :math:`N_{c}`.
N_cb : numeric
Chromatic induction factor :math:`N_{cb}`.
e_t : numeric
Eccentricity factor :math:`e_t`.
a : numeric
Opponent colour dimension :math:`a`.
b : numeric
Opponent colour dimension :math:`b`.
RGB_a : array_like
Compressed stimulus *CMCCAT2000* transform sharpened *RGB* matrix.
Returns
-------
numeric
*Chroma* correlate :math:`C`.
Examples
--------
>>> J = 41.7310911325
>>> n = 0.2
>>> N_c = 1.0
>>> N_cb = 1.00030400456
>>> e_t = 1.17400547285
>>> a = -0.000624112068243
>>> b = -0.000506270106773
>>> RGB_a = np.array([7.9463202, 7.94711528, 7.94899595])
>>> chroma_correlate(J, n, N_c, N_cb, e_t, a, b, RGB_a) # noqa # doctest: +ELLIPSIS
0.1047077...
"""
t = temporary_magnitude_quantity_forward(N_c, N_cb, e_t, a, b, RGB_a)
C = t ** 0.9 * (J / 100) ** 0.5 * (1.64 - 0.29 ** n) ** 0.73
return C
[docs]def colourfulness_correlate(C, F_L):
"""
Returns the *colourfulness* correlate :math:`M`.
Parameters
----------
C : numeric
*Chroma* correlate :math:`C`.
F_L : numeric
*Luminance* level adaptation factor :math:`F_L`.
Returns
-------
numeric
*Colourfulness* correlate :math:`M`.
Examples
--------
>>> C = 0.104707757171
>>> F_L = 1.16754446415
>>> colourfulness_correlate(C, F_L) # doctest: +ELLIPSIS
0.1088421...
"""
M = C * F_L ** 0.25
return M
[docs]def saturation_correlate(M, Q):
"""
Returns the *saturation* correlate :math:`s`.
Parameters
----------
M : numeric
*Colourfulness* correlate :math:`M`.
Q : numeric
*Brightness* correlate :math:`C`.
Returns
-------
numeric
*Saturation* correlate :math:`s`.
Examples
--------
>>> M = 0.108842175669
>>> Q = 195.371325966
>>> saturation_correlate(M, Q) # doctest: +ELLIPSIS
2.3603053...
"""
s = 100 * (M / Q) ** 0.5
return s
[docs]def P(N_c, N_cb, e_t, t, A, N_bb):
"""
Returns the points :math:`P_1`, :math:`P_2` and :math:`P_3`.
Parameters
----------
N_c : numeric
Surround chromatic induction factor :math:`N_{c}`.
N_cb : numeric
Chromatic induction factor :math:`N_{cb}`.
e_t : numeric
Eccentricity factor :math:`e_t`.
t : numeric
Temporary magnitude quantity :math:`t`.
A : numeric
Achromatic response :math:`A` for the stimulus.
N_bb : numeric
Chromatic induction factor :math:`N_{bb}`.
Returns
-------
tuple
Points :math:`P`.
Examples
--------
>>> N_c = 1.0
>>> N_cb = 1.00030400456
>>> e_t = 1.1740054728519145
>>> t = 0.149746202921
>>> A = 23.9394809667
>>> N_bb = 1.00030400456
>>> P(N_c, N_cb, e_t, t, A, N_bb) # doctest: +ELLIPSIS
(30162.8908154..., 24.2372054..., 1.05)
"""
P_1 = ((50000 / 13) * N_c * N_cb * e_t) / t
P_2 = A / N_bb + 0.305
P_3 = 21 / 20
return P_1, P_2, P_3
[docs]def post_adaptation_non_linear_response_compression_matrix(P_2, a, b):
"""
Returns the post adaptation non linear response compression matrix.
Parameters
----------
P_2 : numeric
Point :math:`P_2`.
a : numeric
Opponent colour dimension :math:`a`.
b : numeric
Opponent colour dimension :math:`b`.
Returns
-------
ndarray, (3,)
Points :math:`P`.
Examples
--------
>>> P_2 = 24.2372054671
>>> a = -0.000624112068243
>>> b = -0.000506270106773
>>> post_adaptation_non_linear_response_compression_matrix(P_2, a, b) # noqa # doctest: +ELLIPSIS
array([ 7.9463202..., 7.9471152..., 7.9489959...])
"""
R_a = (460 * P_2 + 451 * a + 288 * b) / 1403
G_a = (460 * P_2 - 891 * a - 261 * b) / 1403
B_a = (460 * P_2 - 220 * a - 6300 * b) / 1403
return np.array([R_a, G_a, B_a])