Coverage for colour/appearance/zcam.py: 100%
136 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 23:01 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 23:01 +1300
1"""
2ZCAM Colour Appearance Model
3============================
5Define the *ZCAM* colour appearance model for predicting perceptual colour
6attributes under varying viewing conditions.
8- :class:`colour.appearance.InductionFactors_ZCAM`
9- :attr:`colour.VIEWING_CONDITIONS_ZCAM`
10- :class:`colour.CAM_Specification_ZCAM`
11- :func:`colour.XYZ_to_ZCAM`
12- :func:`colour.ZCAM_to_XYZ`
14References
15----------
16- :cite:`Safdar2018` : Safdar, M., Hardeberg, J. Y., Kim, Y. J., & Luo, M. R.
17 (2018). A Colour Appearance Model based on J z a z b z Colour Space. Color
18 and Imaging Conference, 2018(1), 96-101.
19 doi:10.2352/ISSN.2169-2629.2018.26.96
20- :cite:`Safdar2021` : Safdar, M., Hardeberg, J. Y., & Ronnier Luo, M.
21 (2021). ZCAM, a colour appearance model based on a high dynamic range
22 uniform colour space. Optics Express, 29(4), 6036. doi:10.1364/OE.413659
23- :cite:`Zhai2018` : Zhai, Q., & Luo, M. R. (2018). Study of chromatic
24 adaptation via neutral white matches on different viewing media. Optics
25 Express, 26(6), 7724. doi:10.1364/OE.26.007724
26"""
28from __future__ import annotations
30from dataclasses import astuple, dataclass, field
32import numpy as np
34from colour.adaptation import chromatic_adaptation_Zhai2018
35from colour.algebra import sdiv, sdiv_mode, spow
36from colour.appearance.ciecam02 import (
37 VIEWING_CONDITIONS_CIECAM02,
38 degree_of_adaptation,
39 hue_angle,
40)
41from colour.colorimetry import CCS_ILLUMINANTS
42from colour.hints import ( # noqa: TC001
43 Annotated,
44 ArrayLike,
45 Domain1,
46 NDArrayFloat,
47 Range1,
48)
49from colour.models import Izazbz_to_XYZ, XYZ_to_Izazbz, xy_to_XYZ
50from colour.utilities import (
51 CanonicalMapping,
52 MixinDataclassArithmetic,
53 MixinDataclassIterable,
54 as_float,
55 as_float_array,
56 as_int_array,
57 domain_range_scale,
58 from_range_1,
59 from_range_degrees,
60 has_only_nan,
61 ones,
62 to_domain_1,
63 to_domain_degrees,
64 tsplit,
65 tstack,
66)
68__author__ = "Colour Developers"
69__copyright__ = "Copyright 2013 Colour Developers"
70__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
71__maintainer__ = "Colour Developers"
72__email__ = "colour-developers@colour-science.org"
73__status__ = "Production"
75__all__ = [
76 "InductionFactors_ZCAM",
77 "VIEWING_CONDITIONS_ZCAM",
78 "CAM_Specification_ZCAM",
79 "XYZ_to_ZCAM",
80 "ZCAM_to_XYZ",
81]
84@dataclass(frozen=True)
85class InductionFactors_ZCAM(MixinDataclassIterable):
86 """
87 Define the *ZCAM* colour appearance model induction factors.
89 Parameters
90 ----------
91 F_s
92 Surround impact :math:`F_s`.
93 F
94 Maximum degree of adaptation :math:`F`.
95 c
96 Exponential non-linearity :math:`c`.
97 N_c
98 Chromatic induction factor :math:`N_c`.
100 Notes
101 -----
102 - The *ZCAM* colour appearance model induction factors are inherited
103 from the *CIECAM02* colour appearance model.
105 References
106 ----------
107 :cite:`Safdar2021`
108 """
110 F_s: float
111 F: float
112 c: float
113 N_c: float
116VIEWING_CONDITIONS_ZCAM: CanonicalMapping = CanonicalMapping(
117 {
118 "Average": InductionFactors_ZCAM(
119 0.69, *VIEWING_CONDITIONS_CIECAM02["Average"].values
120 ),
121 "Dim": InductionFactors_ZCAM(0.59, *VIEWING_CONDITIONS_CIECAM02["Dim"].values),
122 "Dark": InductionFactors_ZCAM(
123 0.525, *VIEWING_CONDITIONS_CIECAM02["Dark"].values
124 ),
125 }
126)
127VIEWING_CONDITIONS_ZCAM.__doc__ = """
128Define the reference *ZCAM* colour appearance model
129viewing conditions.
131Provide three standard viewing conditions (*Average*, *Dim*, and *Dark*)
132with corresponding induction factors. Each condition specifies a unique
133surround impact factor (:math:`F_s`) alongside inherited *CIECAM02*
134parameters for maximum degree of adaptation (:math:`F`), exponential
135non-linearity (:math:`c`), and chromatic induction factor (:math:`N_c`).
137Notes
138-----
139- The *ZCAM* viewing conditions inherit parameters from *CIECAM02* while
140 introducing model-specific surround impact factors: 0.69 (*Average*),
141 0.59 (*Dim*), and 0.525 (*Dark*).
143References
144----------
145:cite:`Safdar2021`
146"""
148HUE_DATA_FOR_HUE_QUADRATURE: dict = {
149 "h_i": np.array([33.44, 89.29, 146.30, 238.36, 393.44]),
150 "e_i": np.array([0.68, 0.64, 1.52, 0.77, 0.68]),
151 "H_i": np.array([0.0, 100.0, 200.0, 300.0, 400.0]),
152}
155@dataclass
156class CAM_ReferenceSpecification_ZCAM(MixinDataclassArithmetic):
157 """
158 Define the *ZCAM* colour appearance model reference specification.
160 This specification contains field names consistent with the *Fairchild
161 (2013)* reference.
163 Parameters
164 ----------
165 J_z
166 Correlate of *lightness* :math:`J_z`.
167 C_z
168 Correlate of *chroma* :math:`C_z`.
169 h_z
170 *Hue* angle :math:`h_z` in degrees.
171 S_z
172 Correlate of *saturation* :math:`S_z`.
173 Q_z
174 Correlate of *brightness* :math:`Q_z`.
175 M_z
176 Correlate of *colourfulness* :math:`M_z`.
177 H
178 *Hue* :math:`h` quadrature :math:`H`.
179 H_z
180 *Hue* :math:`h` composition :math:`H_z`.
181 V_z
182 Correlate of *vividness* :math:`V_z`.
183 K_z
184 Correlate of *blackness* :math:`K_z`.
185 W_z
186 Correlate of *whiteness* :math:`W_z`.
188 References
189 ----------
190 :cite:`Safdar2021`
191 """
193 J_z: float | NDArrayFloat | None = field(default_factory=lambda: None)
194 C_z: float | NDArrayFloat | None = field(default_factory=lambda: None)
195 h_z: float | NDArrayFloat | None = field(default_factory=lambda: None)
196 S_z: float | NDArrayFloat | None = field(default_factory=lambda: None)
197 Q_z: float | NDArrayFloat | None = field(default_factory=lambda: None)
198 M_z: float | NDArrayFloat | None = field(default_factory=lambda: None)
199 H: float | NDArrayFloat | None = field(default_factory=lambda: None)
200 H_z: float | NDArrayFloat | None = field(default_factory=lambda: None)
201 V_z: float | NDArrayFloat | None = field(default_factory=lambda: None)
202 K_z: float | NDArrayFloat | None = field(default_factory=lambda: None)
203 W_z: float | NDArrayFloat | None = field(default_factory=lambda: None)
206@dataclass
207class CAM_Specification_ZCAM(MixinDataclassArithmetic):
208 """
209 Define the *ZCAM* colour appearance model specification.
211 This specification provides a standardized interface for the *ZCAM* model
212 with field names consistent across all colour appearance models in
213 :mod:`colour.appearance`. While the field names differ from the original
214 *Fairchild (2013)* reference notation, they map directly to the model's
215 perceptual correlates.
217 Parameters
218 ----------
219 J
220 *Lightness* :math:`J` is the "brightness of an area (:math:`Q`) judged
221 relative to the brightness of a similarly illuminated area that appears
222 to be white or highly transmitting (:math:`Q_w`)", i.e.,
223 :math:`J = (Q/Q_w)`. It is a visual scale with two well defined levels
224 i.e., zero and 100 for a pure black and a reference white,
225 respectively. Note that in HDR visual field, samples could have a
226 higher luminance than that of the reference white, so the lightness
227 could be over 100. Subscripts :math:`s` and :math:`w` are used to
228 annotate the sample and the reference white, respectively.
229 C
230 *Chroma* :math:`C` is "colourfulness of an area (:math:`M`) judged as
231 a proportion of the brightness of a similarly illuminated area that
232 appears white or highly transmitting (:math:`Q_w`)", i.e.,
233 :math:`C = (M/Q_w)`. It is an open-end scale with origin as a colour
234 in the neutral axis. It can be estimated as the magnitude of the
235 chromatic difference between the test colour and a neutral colour
236 having the lightness same as the test colour.
237 h
238 *Hue* angle :math:`h` is a scale ranged from :math:`0^{\\circ}` to
239 :math:`360^{\\circ}` with the hues following rainbow sequence. The same
240 distance between pairs of hues in a constant lightness and chroma shows
241 the same perceived colour difference.
242 s
243 *Saturation* :math:`s` is the "colourfulness (:math:`M`) of an area
244 judged in proportion to its brightness (:math:`Q`)", i.e.,
245 :math:`s = (M/Q)`. It can also be defined as the chroma of an area
246 judged in proportion to its lightness, i.e., :math:`s = (C/J)`. It is
247 an open-end scale with all neutral colours to have saturation of zero.
248 For example, the red bricks in a building would exhibit different
249 colours when illuminated by daylight. Those (directly) under daylight
250 will appear to be bright and colourful, and those under shadow will
251 appear darker and less colourful. However, the two areas have the same
252 saturation.
253 Q
254 *Brightness* :math:`Q` is an "attribute of a visual perception
255 according to which an area appears to emit, or reflect, more or less
256 light". It is an open-end scale with origin as pure black or complete
257 darkness. It is an absolute scale according to the illumination
258 condition i.e., an increase of brightness of an object when the
259 illuminance of light is increased. This is a visual phenomenon known as
260 Stevens effect.
261 M
262 *Colourfulness* :math:`M` is an "attribute of a visual perception
263 according to which the perceived colour of an area appears to be more
264 or less chromatic". It is an open-end scale with origin as a neutral
265 colour i.e., appearance of no hue. It is an absolute scale according to
266 the illumination condition i.e., an increase of colourfulness of an
267 object when the illuminance of light is increased. This is a visual
268 phenomenon known as Hunt effect.
269 H
270 *Hue* :math:`h` quadrature :math:`H_C` is an "attribute of a visual
271 perception according to which an area appears to be similar to one of
272 the colours: red, yellow, green, and blue, or to a combination of
273 adjacent pairs of these colours considered in a closed ring". It has
274 a 0-400 scale, i.e., hue quadrature of 0, 100, 200, 300, and 400
275 range from unitary red to, yellow, green, blue, and back to red,
276 respectively. For example, a cyan colour consists of 50% green and
277 50% blue, corresponding to a hue quadrature of 250.
278 HC
279 *Hue* :math:`h` composition :math:`H^C` used to define the hue
280 appearance of a sample. Note that hue circles formed by the equal hue
281 angle and equal hue composition appear to be quite different.
282 V
283 *Vividness* :math:`V` is an "attribute of colour used to indicate the
284 degree of departure of the colour (of stimulus) from a neutral black
285 colour", i.e., :math:`V = \\sqrt{J^2 + C^2}`. It is an open-end scale
286 with origin at pure black. This reflects the visual phenomena of an
287 object illuminated by a light to increase both the lightness and the
288 chroma.
289 K
290 *Blackness* :math:`K` is a visual attribute according to which an area
291 appears to contain more or less black content. It is a scale in the
292 Natural Colour System (NCS) and can also be defined in resemblance to a
293 pure black. It is an open-end scale with 100 as pure black (luminance
294 of 0 :math:`cd/m^2`), i.e.,
295 :math:`K = (100 - \\sqrt{J^2 + C^2} = (100 - V)`. The visual effect can
296 be illustrated by mixing a black to a colour pigment. The more black
297 pigment is added, the higher blackness will be. A blacker colour will
298 have less lightness and/or chroma than a less black colour.
299 W
300 *Whiteness* :math:`W` is a visual attribute according to which an area
301 appears to contain more or less white content. It is a scale of the NCS
302 and can also be defined in resemblance to a pure white. It is an
303 open-end scale with 100 as reference white, i.e.,
304 :math:`W = (100 - \\sqrt{(100 - J)^2 + C^2} = (100 - D)`. The visual
305 effect can be illustrated by mixing a white to a colour pigment. The
306 more white pigment is added, the higher whiteness will be. A whiter
307 colour will have a lower chroma and higher lightness than the less
308 white colour.
310 References
311 ----------
312 :cite:`Safdar2021`
313 """
315 J: float | NDArrayFloat | None = field(default_factory=lambda: None)
316 C: float | NDArrayFloat | None = field(default_factory=lambda: None)
317 h: float | NDArrayFloat | None = field(default_factory=lambda: None)
318 s: float | NDArrayFloat | None = field(default_factory=lambda: None)
319 Q: float | NDArrayFloat | None = field(default_factory=lambda: None)
320 M: float | NDArrayFloat | None = field(default_factory=lambda: None)
321 H: float | NDArrayFloat | None = field(default_factory=lambda: None)
322 HC: float | NDArrayFloat | None = field(default_factory=lambda: None)
323 V: float | NDArrayFloat | None = field(default_factory=lambda: None)
324 K: float | NDArrayFloat | None = field(default_factory=lambda: None)
325 W: float | NDArrayFloat | None = field(default_factory=lambda: None)
328TVS_D65: NDArrayFloat = xy_to_XYZ(
329 CCS_ILLUMINANTS["CIE 1931 2 Degree Standard Observer"]["D65"]
330)
333def XYZ_to_ZCAM(
334 XYZ: Domain1,
335 XYZ_w: Domain1,
336 L_A: ArrayLike,
337 Y_b: ArrayLike,
338 surround: InductionFactors_ZCAM = VIEWING_CONDITIONS_ZCAM["Average"],
339 discount_illuminant: bool = False,
340 compute_H: bool = True,
341) -> Annotated[CAM_Specification_ZCAM, (1, 1, 360, 1, 1, 1, 400, 1, 1, 1)]:
342 """
343 Compute the *ZCAM* colour appearance model correlates from the specified
344 *CIE XYZ* tristimulus values.
346 Parameters
347 ----------
348 XYZ
349 Absolute *CIE XYZ* tristimulus values of test sample / stimulus.
350 XYZ_w
351 Absolute *CIE XYZ* tristimulus values of the white under reference
352 illuminant.
353 L_A
354 Test adapting field *luminance* :math:`L_A` in :math:`cd/m^2` such as
355 :math:`L_A = L_w * Y_b / 100` (where :math:`L_w` is luminance of the
356 reference white and :math:`Y_b` is the background luminance factor).
357 Y_b
358 Luminous factor of background :math:`Y_b` such as
359 :math:`Y_b = 100 * L_b / L_w` where :math:`L_w` is the luminance of
360 the light source and :math:`L_b` is the luminance of the background.
361 For viewing images, :math:`Y_b` can be the average :math:`Y` value
362 for the pixels in the entire image, or frequently, a :math:`Y` value
363 of 20, approximating an :math:`L^*` of 50 is used.
364 surround
365 Surround viewing conditions induction factors.
366 discount_illuminant
367 Truth value indicating if the illuminant should be discounted.
368 compute_H
369 Whether to compute *Hue* :math:`h` quadrature :math:`H`. :math:`H`
370 is rarely used, and expensive to compute.
372 Returns
373 -------
374 :class:`colour.CAM_Specification_ZCAM`
375 *ZCAM* colour appearance model specification.
377 Warnings
378 --------
379 The underlying *SMPTE ST 2084:2014* transfer function is an absolute
380 transfer function.
382 Notes
383 -----
384 - *Safdar, Hardeberg and Luo (2021)* does not specify how the
385 chromatic adaptation to *CIE Standard Illuminant D65* in *Step 0*
386 should be performed. A one-step *Von Kries* chromatic adaptation
387 transform is not symmetrical or transitive when a degree of
388 adaptation is involved. *Safdar, Hardeberg and Luo (2018)* uses
389 *Zhai and Luo (2018)* two-steps chromatic adaptation transform, thus
390 it seems sensible to adopt this transform for the *ZCAM* colour
391 appearance model until more information is available. It is worth
392 noting that a one-step *Von Kries* chromatic adaptation transform
393 with support for degree of adaptation produces values closer to the
394 supplemental document compared to the *Zhai and Luo (2018)*
395 two-steps chromatic adaptation transform but then the *ZCAM* colour
396 appearance model does not round-trip properly.
397 - The underlying *SMPTE ST 2084:2014* transfer function is an absolute
398 transfer function, thus the domain and range values for the
399 *Reference* and *1* scales are only indicative that the data is not
400 affected by scale transformations.
402 +----------------------+-----------------------+---------------+
403 | **Domain** | **Scale - Reference** | **Scale - 1** |
404 +======================+=======================+===============+
405 | ``XYZ`` | UN | UN |
406 +----------------------+-----------------------+---------------+
407 | ``XYZ_w`` | UN | UN |
408 +----------------------+-----------------------+---------------+
410 +----------------------+-----------------------+---------------+
411 | **Range** | **Scale - Reference** | **Scale - 1** |
412 +======================+=======================+===============+
413 | ``specification.J`` | UN | 1 |
414 +----------------------+-----------------------+---------------+
415 | ``specification.C`` | UN | 1 |
416 +----------------------+-----------------------+---------------+
417 | ``specification.h`` | 360 | 1 |
418 +----------------------+-----------------------+---------------+
419 | ``specification.s`` | UN | 1 |
420 +----------------------+-----------------------+---------------+
421 | ``specification.Q`` | UN | 1 |
422 +----------------------+-----------------------+---------------+
423 | ``specification.M`` | UN | 1 |
424 +----------------------+-----------------------+---------------+
425 | ``specification.H`` | 400 | 1 |
426 +----------------------+-----------------------+---------------+
427 | ``specification.HC`` | UN | 1 |
428 +----------------------+-----------------------+---------------+
429 | ``specification.V`` | UN | 1 |
430 +----------------------+-----------------------+---------------+
431 | ``specification.K`` | UN | 1 |
432 +----------------------+-----------------------+---------------+
433 | ``specification.H`` | UN | 1 |
434 +----------------------+-----------------------+---------------+
436 References
437 ----------
438 :cite:`Safdar2018`, :cite:`Safdar2021`, :cite:`Zhai2018`
440 Examples
441 --------
442 >>> XYZ = np.array([185, 206, 163])
443 >>> XYZ_w = np.array([256, 264, 202])
444 >>> L_A = 264
445 >>> Y_b = 100
446 >>> surround = VIEWING_CONDITIONS_ZCAM["Average"]
447 >>> XYZ_to_ZCAM(XYZ, XYZ_w, L_A, Y_b, surround)
448 ... # doctest: +ELLIPSIS
449 CAM_Specification_ZCAM(J=92.2504437..., C=3.0216926..., h=196.3245737..., \
450s=19.1319556..., Q=321.3408463..., M=10.5256217..., H=237.6114442..., \
451HC=None, V=34.7006776..., K=25.8835968..., W=91.6821728...)
452 """
454 XYZ = to_domain_1(XYZ)
455 XYZ_w = to_domain_1(XYZ_w)
456 _X_w, Y_w, _Z_w = tsplit(XYZ_w)
457 L_A = as_float_array(L_A)
458 Y_b = as_float_array(Y_b)
460 F_s, F, _c, _N_c = surround.values
462 # Step 0 (Forward) - Chromatic adaptation from reference illuminant to
463 # "CIE Standard Illuminant D65" illuminant using "CAT02".
464 # Computing degree of adaptation :math:`D`.
465 D = degree_of_adaptation(F, L_A) if not discount_illuminant else ones(L_A.shape)
467 XYZ_D65 = chromatic_adaptation_Zhai2018(
468 XYZ, XYZ_w, TVS_D65, D, D, transform="CAT02"
469 )
471 # Step 1 (Forward) - Computing factors related with viewing conditions and
472 # independent of the test stimulus.
473 # Background factor :math:`F_b`
474 F_b = np.sqrt(Y_b / Y_w)
475 # Luminance level adaptation factor :math:`F_L`
476 F_L = 0.171 * spow(L_A, 1 / 3) * (1 - np.exp(-48 / 9 * L_A))
478 # Step 2 (Forward) - Computing achromatic response (:math:`I_z` and
479 # :math:`I_{z,w}`), redness-greenness (:math:`a_z` and :math:`a_{z,w}`),
480 # and yellowness-blueness (:math:`b_z`, :math:`b_{z,w}`).
481 with domain_range_scale("ignore"):
482 I_z, a_z, b_z = tsplit(XYZ_to_Izazbz(XYZ_D65, method="Safdar 2021"))
483 I_z_w, _a_z_w, _b_z_w = tsplit(XYZ_to_Izazbz(XYZ_w, method="Safdar 2021"))
485 # Step 3 (Forward) - Computing hue angle :math:`h_z`
486 h_z = hue_angle(a_z, b_z)
488 # Step 4 (Forward) - Computing hue quadrature :math:`H`.
489 H = hue_quadrature(h_z) if compute_H else np.full(h_z.shape, np.nan)
491 # Computing eccentricity factor :math:`e_z`.
492 e_z = 1.015 + np.cos(np.radians(89.038 + h_z % 360))
494 # Step 5 (Forward) - Computing brightness :math:`Q_z`,
495 # lightness :math:`J_z`, colourfulness :math`M_z`, and chroma :math:`C_z`
496 Q_z_p = (1.6 * F_s) / (F_b**0.12)
497 Q_z_m = F_s**2.2 * F_b**0.5 * spow(F_L, 0.2)
498 Q_z = 2700 * spow(I_z, Q_z_p) * Q_z_m
499 Q_z_w = 2700 * spow(I_z_w, Q_z_p) * Q_z_m
501 J_z = 100 * Q_z / Q_z_w
503 M_z = (
504 100
505 * (a_z**2 + b_z**2) ** 0.37
506 * ((spow(e_z, 0.068) * spow(F_L, 0.2)) / (F_b**0.1 * spow(I_z_w, 0.78)))
507 )
509 C_z = 100 * M_z / Q_z_w
511 # Step 6 (Forward) - Computing saturation :math:`S_z`,
512 # vividness :math:`V_z`, blackness :math:`K_z`, and whiteness :math:`W_z`.
513 with sdiv_mode():
514 S_z = 100 * spow(F_L, 0.6) * np.sqrt(sdiv(M_z, Q_z))
516 V_z = np.sqrt((J_z - 58) ** 2 + 3.4 * C_z**2)
518 K_z = 100 - 0.8 * np.sqrt(J_z**2 + 8 * C_z**2)
520 W_z = 100 - np.sqrt((100 - J_z) ** 2 + C_z**2)
522 return CAM_Specification_ZCAM(
523 J=as_float(from_range_1(J_z)),
524 C=as_float(from_range_1(C_z)),
525 h=as_float(from_range_degrees(h_z)),
526 s=as_float(from_range_1(S_z)),
527 Q=as_float(from_range_1(Q_z)),
528 M=as_float(from_range_1(M_z)),
529 H=as_float(from_range_degrees(H, 400)),
530 HC=None,
531 V=as_float(from_range_1(V_z)),
532 K=as_float(from_range_1(K_z)),
533 W=as_float(from_range_1(W_z)),
534 )
537def ZCAM_to_XYZ(
538 specification: Annotated[
539 CAM_Specification_ZCAM, (1, 1, 360, 1, 1, 1, 400, 1, 1, 1)
540 ],
541 XYZ_w: Domain1,
542 L_A: ArrayLike,
543 Y_b: ArrayLike,
544 surround: InductionFactors_ZCAM = VIEWING_CONDITIONS_ZCAM["Average"],
545 discount_illuminant: bool = False,
546) -> Range1:
547 """
548 Convert the *ZCAM* specification to *CIE XYZ* tristimulus values.
550 Parameters
551 ----------
552 specification
553 *ZCAM* colour appearance model specification.
554 Correlate of *lightness* :math:`J`, correlate of *chroma* :math:`C` or
555 correlate of *colourfulness* :math:`M` and *hue* angle :math:`h` in
556 degrees must be specified, e.g., :math:`JCh` or :math:`JMh`.
557 XYZ_w
558 Absolute *CIE XYZ* tristimulus values of the white under reference
559 illuminant.
560 L_A
561 Test adapting field *luminance* :math:`L_A` in :math:`cd/m^2` such as
562 :math:`L_A = L_w * Y_b / 100` (where :math:`L_w` is luminance of the
563 reference white and :math:`Y_b` is the background luminance factor).
564 Y_b
565 Luminous factor of background :math:`Y_b` such as
566 :math:`Y_b = 100 x L_b / L_w` where :math:`L_w` is the luminance of
567 the light source and :math:`L_b` is the luminance of the background.
568 For viewing images, :math:`Y_b` can be the average :math:`Y` value for
569 the pixels in the entire image, or frequently, a :math:`Y` value of
570 20, approximating an :math:`L^*` of 50 is used.
571 surround
572 Surround viewing conditions induction factors.
573 discount_illuminant
574 Truth value indicating if the illuminant should be discounted.
576 Returns
577 -------
578 :class:`numpy.ndarray`
579 *CIE XYZ* tristimulus values.
581 Raises
582 ------
583 ValueError
584 If neither :math:`C` or :math:`M` correlates have been defined in the
585 ``specification`` argument.
587 Warnings
588 --------
589 The underlying *SMPTE ST 2084:2014* transfer function is an absolute
590 transfer function.
592 Notes
593 -----
594 - *Safdar, Hardeberg and Luo (2021)* does not specify how the
595 chromatic adaptation to *CIE Standard Illuminant D65* in *Step 0*
596 should be performed. A one-step *Von Kries* chromatic adaptation
597 transform is not symmetrical or transitive when a degree of
598 adaptation is involved. *Safdar, Hardeberg and Luo (2018)* uses
599 *Zhai and Luo (2018)* two-steps chromatic adaptation transform, thus
600 it seems sensible to adopt this transform for the *ZCAM* colour
601 appearance model until more information is available. It is worth
602 noting that a one-step *Von Kries* chromatic adaptation transform
603 with support for degree of adaptation produces values closer to the
604 supplemental document compared to the *Zhai and Luo (2018)*
605 two-steps chromatic adaptation transform but then the *ZCAM* colour
606 appearance model does not round-trip properly.
607 - The underlying *SMPTE ST 2084:2014* transfer function is an absolute
608 transfer function, thus the domain and range values for the
609 *Reference* and *1* scales are only indicative that the data is not
610 affected by scale transformations.
611 - *Step 4* of the inverse model uses a rounded exponent of 1.3514
612 preventing the model to round-trip properly. Given that this
613 implementation takes some liberties with respect to the chromatic
614 adaptation transform to use, it was deemed appropriate to use an
615 exponent value that enables the *ZCAM* colour appearance model to
616 round-trip.
618 +----------------------+-----------------------+---------------+
619 | **Domain** | **Scale - Reference** | **Scale - 1** |
620 +======================+=======================+===============+
621 | ``specification.J`` | UN | UN |
622 +----------------------+-----------------------+---------------+
623 | ``specification.C`` | UN | UN |
624 +----------------------+-----------------------+---------------+
625 | ``specification.h`` | 360 | 1 |
626 +----------------------+-----------------------+---------------+
627 | ``specification.s`` | UN | UN |
628 +----------------------+-----------------------+---------------+
629 | ``specification.Q`` | UN | UN |
630 +----------------------+-----------------------+---------------+
631 | ``specification.M`` | UN | UN |
632 +----------------------+-----------------------+---------------+
633 | ``specification.H`` | 400 | 1 |
634 +----------------------+-----------------------+---------------+
635 | ``specification.HC`` | UN | UN |
636 +----------------------+-----------------------+---------------+
637 | ``specification.V`` | UN | UN |
638 +----------------------+-----------------------+---------------+
639 | ``specification.K`` | UN | UN |
640 +----------------------+-----------------------+---------------+
641 | ``specification.H`` | UN | UN |
642 +----------------------+-----------------------+---------------+
643 | ``XYZ_w`` | UN | UN |
644 +----------------------+-----------------------+---------------+
646 +----------------------+-----------------------+---------------+
647 | **Range** | **Scale - Reference** | **Scale - 1** |
648 +======================+=======================+===============+
649 | ``XYZ`` | UN | UN |
650 +----------------------+-----------------------+---------------+
652 References
653 ----------
654 :cite:`Safdar2018`, :cite:`Safdar2021`, :cite:`Zhai2018`
656 Examples
657 --------
658 >>> specification = CAM_Specification_ZCAM(
659 ... J=92.250443780723629, C=3.0216926733329013, h=196.32457375575581
660 ... )
661 >>> XYZ_w = np.array([256, 264, 202])
662 >>> L_A = 264
663 >>> Y_b = 100
664 >>> surround = VIEWING_CONDITIONS_ZCAM["Average"]
665 >>> ZCAM_to_XYZ(specification, XYZ_w, L_A, Y_b, surround)
666 ... # doctest: +ELLIPSIS
667 array([ 185., 206., 163.])
668 """
670 J_z, C_z, h_z, _S_z, _Q_z, M_z, _H, _H_Z, _V_z, _K_z, _W_z = astuple(specification)
672 J_z = to_domain_1(J_z)
673 C_z = to_domain_1(C_z)
674 h_z = to_domain_degrees(h_z)
675 M_z = to_domain_1(M_z)
677 XYZ_w = to_domain_1(XYZ_w)
678 _X_w, Y_w, _Z_w = tsplit(XYZ_w)
679 L_A = as_float_array(L_A)
680 Y_b = as_float_array(Y_b)
682 F_s, F, c, N_c = surround.values
684 # Step 0 (Forward) - Chromatic adaptation from reference illuminant to
685 # "CIE Standard Illuminant D65" illuminant using "CAT02".
686 # Computing degree of adaptation :math:`D`.
687 D = degree_of_adaptation(F, L_A) if not discount_illuminant else ones(L_A.shape)
689 # Step 1 (Forward) - Computing factors related with viewing conditions and
690 # independent of the test stimulus.
691 # Background factor :math:`F_b`
692 F_b = np.sqrt(Y_b / Y_w)
693 # Luminance level adaptation factor :math:`F_L`
694 F_L = 0.171 * spow(L_A, 1 / 3) * (1 - np.exp(-48 / 9 * L_A))
696 # Step 2 (Forward) - Computing achromatic response (:math:`I_{z,w}`),
697 # redness-greenness (:math:`a_{z,w}`), and yellowness-blueness
698 # (:math:`b_{z,w}`).
699 with domain_range_scale("ignore"):
700 I_z_w, _A_z_w, _B_z_w = tsplit(XYZ_to_Izazbz(XYZ_w, method="Safdar 2021"))
702 # Step 1 (Inverse) - Computing achromatic response (:math:`I_z`).
703 Q_z_p = (1.6 * F_s) / spow(F_b, 0.12)
704 Q_z_m = spow(F_s, 2.2) * spow(F_b, 0.5) * spow(F_L, 0.2)
705 Q_z_w = 2700 * spow(I_z_w, Q_z_p) * Q_z_m
707 I_z_p = spow(F_b, 0.12) / (1.6 * F_s)
708 I_z_d = 2700 * 100 * Q_z_m
710 I_z = spow((J_z * Q_z_w) / I_z_d, I_z_p)
712 # Step 2 (Inverse) - Computing chroma :math:`C_z`.
713 if has_only_nan(M_z) and not has_only_nan(C_z):
714 M_z = (C_z * Q_z_w) / 100
715 elif has_only_nan(M_z):
716 error = (
717 'Either "C" or "M" correlate must be defined in '
718 'the "CAM_Specification_ZCAM" argument!'
719 )
721 raise ValueError(error)
723 # Step 3 (Inverse) - Computing hue angle :math:`h_z`
724 # :math:`h_z` is currently required as an input.
726 # Computing eccentricity factor :math:`e_z`.
727 e_z = 1.015 + np.cos(np.radians(89.038 + h_z % 360))
728 h_z_r = np.radians(h_z)
730 # Step 4 (Inverse) - Computing redness-greenness (:math:`a_z`), and
731 # yellowness-blueness (:math:`b_z`).
732 # C_z_p_e = 1.3514
733 C_z_p_e = 50 / 37
734 C_z_p = spow(
735 (M_z * spow(I_z_w, 0.78) * spow(F_b, 0.1))
736 / (100 * spow(e_z, 0.068) * spow(F_L, 0.2)),
737 C_z_p_e,
738 )
739 a_z = C_z_p * np.cos(h_z_r)
740 b_z = C_z_p * np.sin(h_z_r)
742 # Step 5 (Inverse) - Computing tristimulus values :math:`XYZ_{D65}`.
743 with domain_range_scale("ignore"):
744 XYZ_D65 = Izazbz_to_XYZ(tstack([I_z, a_z, b_z]), method="Safdar 2021")
746 XYZ = chromatic_adaptation_Zhai2018(
747 XYZ_D65, TVS_D65, XYZ_w, D, D, transform="CAT02"
748 )
750 return from_range_1(XYZ)
753def hue_quadrature(h: ArrayLike) -> NDArrayFloat:
754 """
755 Compute the hue quadrature from the specified hue :math:`h` angle in
756 degrees.
758 Parameters
759 ----------
760 h
761 Hue :math:`h` angle in degrees.
763 Returns
764 -------
765 :class:`numpy.ndarray`
766 Hue quadrature.
768 Examples
769 --------
770 >>> hue_quadrature(196.3185839) # doctest: +ELLIPSIS
771 237.6052911...
772 """
774 h = as_float_array(h)
776 h_i = HUE_DATA_FOR_HUE_QUADRATURE["h_i"]
777 e_i = HUE_DATA_FOR_HUE_QUADRATURE["e_i"]
778 H_i = HUE_DATA_FOR_HUE_QUADRATURE["H_i"]
780 # :math:`h_p` = :math:`h_z` + 360 if :math:`h_z` < :math:`h_1, i.e., h_i[0]
781 h[h <= h_i[0]] += 360
782 # *np.searchsorted* returns an erroneous index if a *nan* is used as input.
783 h[np.asarray(np.isnan(h))] = 0
784 i = as_int_array(np.searchsorted(h_i, h, side="left") - 1)
786 h_ii = h_i[i]
787 e_ii = e_i[i]
788 H_ii = H_i[i]
789 h_ii1 = h_i[i + 1]
790 e_ii1 = e_i[i + 1]
792 h_h_ii = h - h_ii
794 H = H_ii + (100 * h_h_ii / e_ii) / (h_h_ii / e_ii + (h_ii1 - h) / e_ii1)
796 return as_float(H)