Coverage for adaptation/zhai2018.py: 58%
36 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
1"""
2Zhai and Luo (2018) Chromatic Adaptation Model
3==============================================
5Define the *Zhai and Luo (2018)* two-step chromatic adaptation for predicting
6corresponding colours under different viewing conditions.
8- :func:`colour.adaptation.chromatic_adaptation_Zhai2018`
10References
11----------
12- :cite:`Zhai2018` : Zhai, Q., & Luo, M. R. (2018). Study of chromatic
13 adaptation via neutral white matches on different viewing media. Optics
14 Express, 26(6), 7724. doi:10.1364/OE.26.007724
15"""
17from __future__ import annotations
19import typing
21import numpy as np
23from colour.adaptation import CHROMATIC_ADAPTATION_TRANSFORMS
24from colour.algebra import vecmul
26if typing.TYPE_CHECKING:
27 from colour.hints import Literal
29from colour.hints import ( # noqa: TC001
30 ArrayLike,
31 Domain100,
32 Range100,
33)
34from colour.utilities import (
35 as_float_array,
36 from_range_100,
37 get_domain_range_scale,
38 optional,
39 to_domain_100,
40 validate_method,
41)
43__author__ = "Colour Developers"
44__copyright__ = "Copyright 2013 Colour Developers"
45__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
46__maintainer__ = "Colour Developers"
47__email__ = "colour-developers@colour-science.org"
48__status__ = "Production"
50__all__ = [
51 "chromatic_adaptation_Zhai2018",
52]
55def chromatic_adaptation_Zhai2018(
56 XYZ_b: Domain100,
57 XYZ_wb: Domain100,
58 XYZ_wd: Domain100,
59 D_b: ArrayLike = 1,
60 D_d: ArrayLike = 1,
61 XYZ_wo: ArrayLike | None = None,
62 transform: Literal["CAT02", "CAT16"] | str = "CAT02",
63) -> Range100:
64 """
65 Adapt the specified stimulus *CIE XYZ* tristimulus values from test
66 viewing conditions to reference viewing conditions using the
67 *Zhai and Luo (2018)* chromatic adaptation model.
69 According to the definition of :math:`D`, a one-step chromatic adaptation
70 transform (CAT) such as CAT02 can only transform colours from an
71 incomplete adapted field into a complete adapted field. When CAT02 is
72 used to transform from incomplete to incomplete adaptation, :math:`D` has
73 no baseline level to refer to. *Smet et al. (2017)* proposed a two-step
74 CAT concept to replace existing one-step transforms such as CAT02,
75 providing a clearer definition of :math:`D`. A two-step CAT involves a
76 baseline illuminant (BI) representing the baseline state between the test
77 and reference illuminants. In the first step, the test colour is
78 transformed from the test illuminant to the baseline illuminant
79 (:math:`BI`), then subsequently transformed to the reference illuminant.
80 Degrees of adaptation under other illuminants are calculated relative to
81 the adaptation under the :math:`BI`. As :math:`D` approaches zero, the
82 observer's adaptation point moves towards the :math:`BI`. Therefore, the
83 chromaticity of the :math:`BI` is an intrinsic property of the human
84 visual system.
86 Parameters
87 ----------
88 XYZ_b
89 Sample colour :math:`XYZ_{\\beta}` tristimulus values under input
90 illuminant :math:`\\beta`.
91 XYZ_wb
92 Input illuminant :math:`\\beta` tristimulus values.
93 XYZ_wd
94 Output illuminant :math:`\\delta` tristimulus values.
95 D_b
96 Degree of adaptation :math:`D_{\\beta}` of input illuminant
97 :math:`\\beta`.
98 D_d
99 Degree of adaptation :math:`D_{\\delta}` of output illuminant
100 :math:`\\delta`.
101 XYZ_wo
102 Baseline illuminant (:math:`BI`) :math:`o` tristimulus values.
103 transform
104 Chromatic adaptation transform matrix.
106 Returns
107 -------
108 :class:`numpy.ndarray`
109 *CIE XYZ* tristimulus values of the stimulus corresponding colour.
111 Notes
112 -----
113 +------------+-----------------------+---------------+
114 | **Domain** | **Scale - Reference** | **Scale - 1** |
115 +============+=======================+===============+
116 | ``XYZ_b`` | 100 | 1 |
117 +------------+-----------------------+---------------+
118 | ``XYZ_wb`` | 100 | 1 |
119 +------------+-----------------------+---------------+
120 | ``XYZ_wd`` | 100 | 1 |
121 +------------+-----------------------+---------------+
122 | ``XYZ_wo`` | 100 | 1 |
123 +------------+-----------------------+---------------+
125 +------------+-----------------------+---------------+
126 | **Range** | **Scale - Reference** | **Scale - 1** |
127 +============+=======================+===============+
128 | ``XYZ_d`` | 100 | 1 |
129 +------------+-----------------------+---------------+
131 References
132 ----------
133 :cite:`Zhai2018`
135 Examples
136 --------
137 >>> XYZ_b = np.array([48.900, 43.620, 6.250])
138 >>> XYZ_wb = np.array([109.850, 100, 35.585])
139 >>> XYZ_wd = np.array([95.047, 100, 108.883])
140 >>> D_b = 0.9407
141 >>> D_d = 0.9800
142 >>> XYZ_wo = np.array([100, 100, 100])
143 >>> chromatic_adaptation_Zhai2018(
144 ... XYZ_b, XYZ_wb, XYZ_wd, D_b, D_d, XYZ_wo
145 ... ) # doctest: +ELLIPSIS
146 array([ 39.1856164..., 42.1546179..., 19.2367203...])
147 >>> XYZ_d = np.array([39.18561644, 42.15461798, 19.23672036])
148 >>> chromatic_adaptation_Zhai2018(
149 ... XYZ_d, XYZ_wd, XYZ_wb, D_d, D_b, XYZ_wo
150 ... ) # doctest: +ELLIPSIS
151 array([ 48.9 , 43.62, 6.25])
152 """
154 XYZ_b = to_domain_100(XYZ_b)
155 XYZ_wb = to_domain_100(XYZ_wb)
156 XYZ_wd = to_domain_100(XYZ_wd)
157 XYZ_wo = to_domain_100(
158 optional(
159 XYZ_wo,
160 np.array([1, 1, 1])
161 if get_domain_range_scale() == "reference"
162 else np.array([0.01, 0.01, 0.01]),
163 )
164 )
165 D_b = as_float_array(D_b)
166 D_d = as_float_array(D_d)
168 Y_wb = XYZ_wb[..., 1][..., None]
169 Y_wd = XYZ_wd[..., 1][..., None]
170 Y_wo = XYZ_wo[..., 1][..., None]
172 transform = validate_method(transform, ("CAT02", "CAT16"))
173 M = CHROMATIC_ADAPTATION_TRANSFORMS[transform]
175 RGB_b = vecmul(M, XYZ_b)
176 RGB_wb = vecmul(M, XYZ_wb)
177 RGB_wd = vecmul(M, XYZ_wd)
178 RGB_wo = vecmul(M, XYZ_wo)
180 D_RGB_b = D_b * (Y_wb / Y_wo) * (RGB_wo / RGB_wb) + 1 - D_b
181 D_RGB_d = D_d * (Y_wd / Y_wo) * (RGB_wo / RGB_wd) + 1 - D_d
183 D_RGB = D_RGB_b / D_RGB_d
185 RGB_d = D_RGB * RGB_b
187 XYZ_d = vecmul(np.linalg.inv(M), RGB_d)
189 return from_range_100(XYZ_d)