Coverage for colour/models/jzazbz.py: 100%
75 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-15 19:01 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-15 19:01 +1300
1"""
2:math:`J_za_zb_z` Colourspace
3=============================
5Define the :math:`J_za_zb_z` colourspace transformations.
7- :func:`colour.models.IZAZBZ_METHODS`
8- :func:`colour.models.XYZ_to_Izazbz`
9- :func:`colour.models.Izazbz_to_XYZ`
10- :func:`colour.XYZ_to_Jzazbz`
11- :func:`colour.Jzazbz_to_XYZ`
13References
14----------
15- :cite:`Safdar2017` : Safdar, M., Cui, G., Kim, Y. J., & Luo, M. R. (2017).
16 Perceptually uniform color space for image signals including high dynamic
17 range and wide gamut. Optics Express, 25(13), 15131.
18 doi:10.1364/OE.25.015131
19- :cite:`Safdar2021` : Safdar, M., Hardeberg, J. Y., & Ronnier Luo, M.
20 (2021). ZCAM, a colour appearance model based on a high dynamic range
21 uniform colour space. Optics Express, 29(4), 6036. doi:10.1364/OE.413659
22"""
24from __future__ import annotations
26import typing
28import numpy as np
30from colour.algebra import vecmul
32if typing.TYPE_CHECKING:
33 from colour.hints import ArrayLike, Domain1, Literal, NDArrayFloat, Range1
35from colour.models.rgb.transfer_functions import eotf_inverse_ST2084, eotf_ST2084
36from colour.models.rgb.transfer_functions.st_2084 import CONSTANTS_ST2084
37from colour.utilities import (
38 Structure,
39 as_float_array,
40 domain_range_scale,
41 optional,
42 tsplit,
43 tstack,
44 validate_method,
45)
46from colour.utilities.documentation import DocstringTuple, is_documentation_building
48__author__ = "Colour Developers"
49__copyright__ = "Copyright 2013 Colour Developers"
50__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
51__maintainer__ = "Colour Developers"
52__email__ = "colour-developers@colour-science.org"
53__status__ = "Production"
55__all__ = [
56 "CONSTANTS_JZAZBZ_SAFDAR2017",
57 "CONSTANTS_JZAZBZ_SAFDAR2021",
58 "MATRIX_JZAZBZ_XYZ_TO_LMS",
59 "MATRIX_JZAZBZ_LMS_TO_XYZ",
60 "MATRIX_JZAZBZ_LMS_P_TO_IZAZBZ_SAFDAR2017",
61 "MATRIX_JZAZBZ_IZAZBZ_TO_LMS_P_SAFDAR2017",
62 "MATRIX_JZAZBZ_LMS_P_TO_IZAZBZ_SAFDAR2021",
63 "MATRIX_JZAZBZ_IZAZBZ_TO_LMS_P_SAFDAR2021",
64 "IZAZBZ_METHODS",
65 "XYZ_to_Izazbz",
66 "Izazbz_to_XYZ",
67 "XYZ_to_Jzazbz",
68 "Jzazbz_to_XYZ",
69]
71CONSTANTS_JZAZBZ_SAFDAR2017: Structure = Structure(
72 b=1.15, g=0.66, d=-0.56, d_0=1.6295499532821566 * 10**-11
73)
74CONSTANTS_JZAZBZ_SAFDAR2017.update(CONSTANTS_ST2084)
75CONSTANTS_JZAZBZ_SAFDAR2017.m_2 = 1.7 * 2523 / 2**5
76"""
77Constants for :math:`J_za_zb_z` colourspace and its variant of the perceptual
78quantizer (PQ) from Dolby Laboratories.
80Notes
81-----
82- The :math:`m2` constant, i.e., the power factor has been re-optimized during
83 the development of the :math:`J_za_zb_z` colourspace.
84"""
86CONSTANTS_JZAZBZ_SAFDAR2021: Structure = Structure(**CONSTANTS_JZAZBZ_SAFDAR2017)
87CONSTANTS_JZAZBZ_SAFDAR2021.d_0 = 3.7035226210190005 * 10**-11
88""":math:`J_za_zb_z` colourspace constants for the *ZCAM* colour appearance model."""
90MATRIX_JZAZBZ_XYZ_TO_LMS: NDArrayFloat = np.array(
91 [
92 [0.41478972, 0.579999, 0.0146480],
93 [-0.2015100, 1.120649, 0.0531008],
94 [-0.0166008, 0.264800, 0.6684799],
95 ]
96)
97"""
98:math:`J_za_zb_z` *CIE XYZ* tristimulus values to normalised cone responses
99matrix.
100"""
102MATRIX_JZAZBZ_LMS_TO_XYZ: NDArrayFloat = np.linalg.inv(MATRIX_JZAZBZ_XYZ_TO_LMS)
103"""
104:math:`J_za_zb_z` normalised cone responses to *CIE XYZ* tristimulus values
105matrix.
106"""
108MATRIX_JZAZBZ_LMS_P_TO_IZAZBZ_SAFDAR2017: NDArrayFloat = np.array(
109 [
110 [0.500000, 0.500000, 0.000000],
111 [3.524000, -4.066708, 0.542708],
112 [0.199076, 1.096799, -1.295875],
113 ]
114)
115"""
116:math:`LMS_p` *SMPTE ST 2084:2014* encoded normalised cone responses to
117:math:`I_za_zb_z` intermediate colourspace matrix.
118"""
120MATRIX_JZAZBZ_IZAZBZ_TO_LMS_P_SAFDAR2017: NDArrayFloat = np.linalg.inv(
121 MATRIX_JZAZBZ_LMS_P_TO_IZAZBZ_SAFDAR2017
122)
123"""
124:math:`I_za_zb_z` intermediate colourspace to :math:`LMS_p`
125*SMPTE ST 2084:2014* encoded normalised cone responses matrix.
126"""
128MATRIX_JZAZBZ_LMS_P_TO_IZAZBZ_SAFDAR2021: NDArrayFloat = np.array(
129 [
130 [0.000000, 1.000000, 0.000000],
131 [3.524000, -4.066708, 0.542708],
132 [0.199076, 1.096799, -1.295875],
133 ]
134)
135"""
136:math:`LMS_p` *SMPTE ST 2084:2014* encoded normalised cone responses to
137:math:`I_za_zb_z` intermediate colourspace matrix.
139References
140----------
141:cite:`Safdar2021`
142"""
144MATRIX_JZAZBZ_IZAZBZ_TO_LMS_P_SAFDAR2021: NDArrayFloat = np.linalg.inv(
145 MATRIX_JZAZBZ_LMS_P_TO_IZAZBZ_SAFDAR2021
146)
147"""
148:math:`I_za_zb_z` intermediate colourspace to :math:`LMS_p`
149*SMPTE ST 2084:2014* encoded normalised cone responses matrix.
151References
152----------
153:cite:`Safdar2021`
154"""
156IZAZBZ_METHODS: tuple = ("Safdar 2017", "Safdar 2021", "ZCAM")
157if is_documentation_building(): # pragma: no cover
158 IZAZBZ_METHODS = DocstringTuple(IZAZBZ_METHODS)
159 IZAZBZ_METHODS.__doc__ = """
160Supported :math:`I_za_zb_z` computation methods.
162References
163----------
164:cite:`Safdar2017`, :cite:`Safdar2021`
165"""
168def XYZ_to_Izazbz(
169 XYZ_D65: ArrayLike,
170 constants: Structure | None = None,
171 method: (Literal["Safdar 2017", "Safdar 2021", "ZCAM"] | str) = "Safdar 2017",
172) -> Range1:
173 """
174 Convert from *CIE XYZ* tristimulus values to :math:`I_za_zb_z`
175 colourspace.
177 Parameters
178 ----------
179 XYZ_D65
180 *CIE XYZ* tristimulus values under
181 *CIE Standard Illuminant D Series D65*.
182 constants
183 :math:`J_za_zb_z` colourspace constants.
184 method
185 Computation method, *Safdar 2021* and *ZCAM* methods are equivalent.
187 Returns
188 -------
189 :class:`numpy.ndarray`
190 :math:`I_za_zb_z` colourspace array where :math:`I_z` is the
191 achromatic response, :math:`a_z` is redness-greenness and
192 :math:`b_z` is yellowness-blueness.
194 Warnings
195 --------
196 The underlying *SMPTE ST 2084:2014* transfer function is an absolute
197 transfer function.
199 Notes
200 -----
201 - The underlying *SMPTE ST 2084:2014* transfer function is an
202 absolute transfer function, thus the domain and range values for
203 the *Reference* and *1* scales are only indicative that the data
204 is not affected by scale transformations. The effective domain of
205 *SMPTE ST 2084:2014* inverse electro-optical transfer function
206 (EOTF) is [0.0001, 10000].
208 +------------+-----------------------+------------------+
209 | **Domain** | **Scale - Reference** | **Scale - 1** |
210 +============+=======================+==================+
211 | ``XYZ`` | ``UN`` | ``UN`` |
212 +------------+-----------------------+------------------+
214 +------------+-----------------------+------------------+
215 | **Range** | **Scale - Reference** | **Scale - 1** |
216 +============+=======================+==================+
217 | ``Izazbz`` | 1 | 1 |
218 +------------+-----------------------+------------------+
220 References
221 ----------
222 :cite:`Safdar2017`, :cite:`Safdar2021`
224 Examples
225 --------
226 >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952])
227 >>> XYZ_to_Izazbz(XYZ) # doctest: +ELLIPSIS
228 array([ 0.0120779..., 0.0092430..., 0.0052600...])
229 """
231 X_D65, Y_D65, Z_D65 = tsplit(as_float_array(XYZ_D65))
233 method = validate_method(method, IZAZBZ_METHODS)
235 constants = optional(
236 constants,
237 (
238 CONSTANTS_JZAZBZ_SAFDAR2017
239 if method == "safdar 2017"
240 else CONSTANTS_JZAZBZ_SAFDAR2021
241 ),
242 )
244 X_p_D65 = constants.b * X_D65 - (constants.b - 1) * Z_D65
245 Y_p_D65 = constants.g * Y_D65 - (constants.g - 1) * X_D65
247 XYZ_p_D65 = tstack([X_p_D65, Y_p_D65, Z_D65])
249 LMS = vecmul(MATRIX_JZAZBZ_XYZ_TO_LMS, XYZ_p_D65)
251 with domain_range_scale("ignore"):
252 LMS_p = eotf_inverse_ST2084(LMS, 10000, constants)
254 if method == "safdar 2017":
255 Izazbz = vecmul(MATRIX_JZAZBZ_LMS_P_TO_IZAZBZ_SAFDAR2017, LMS_p)
256 else:
257 Izazbz = vecmul(MATRIX_JZAZBZ_LMS_P_TO_IZAZBZ_SAFDAR2021, LMS_p)
258 Izazbz[..., 0] -= constants.d_0
260 return Izazbz
263def Izazbz_to_XYZ(
264 Izazbz: Domain1,
265 constants: Structure | None = None,
266 method: (Literal["Safdar 2017", "Safdar 2021", "ZCAM"] | str) = "Safdar 2017",
267) -> NDArrayFloat:
268 """
269 Convert from :math:`I_za_zb_z` colourspace to *CIE XYZ* tristimulus
270 values.
272 Parameters
273 ----------
274 Izazbz
275 :math:`I_za_zb_z` colourspace array where :math:`I_z` is the
276 achromatic response, :math:`a_z` is redness-greenness and
277 :math:`b_z` is yellowness-blueness.
278 constants
279 :math:`J_za_zb_z` colourspace constants.
280 method
281 Computation method, *Safdar 2021* and *ZCAM* methods are equivalent.
283 Returns
284 -------
285 :class:`numpy.ndarray`
286 *CIE XYZ* tristimulus values under
287 *CIE Standard Illuminant D Series D65*.
289 Warnings
290 --------
291 The underlying *SMPTE ST 2084:2014* transfer function is an absolute
292 transfer function.
294 Notes
295 -----
296 - The underlying *SMPTE ST 2084:2014* transfer function is an
297 absolute transfer function, thus the domain and range values for
298 the *Reference* and *1* scales are only indicative that the data
299 is not affected by scale transformations. The effective domain of
300 *SMPTE ST 2084:2014* inverse electro-optical transfer function
301 (EOTF) is [0.0001, 10000].
303 +------------+-----------------------+------------------+
304 | **Domain** | **Scale - Reference** | **Scale - 1** |
305 +============+=======================+==================+
306 | ``Izazbz`` | 1 | 1 |
307 +------------+-----------------------+------------------+
309 +------------+-----------------------+------------------+
310 | **Range** | **Scale - Reference** | **Scale - 1** |
311 +============+=======================+==================+
312 | ``XYZ`` | ``UN`` | ``UN`` |
313 +------------+-----------------------+------------------+
315 References
316 ----------
317 :cite:`Safdar2017`, :cite:`Safdar2021`
319 Examples
320 --------
321 >>> Izazbz = np.array([0.01207793, 0.00924302, 0.00526007])
322 >>> Izazbz_to_XYZ(Izazbz) # doctest: +ELLIPSIS
323 array([ 0.2065401..., 0.1219723..., 0.0513696...])
324 """
326 Izazbz = as_float_array(Izazbz)
328 method = validate_method(method, IZAZBZ_METHODS)
330 constants = optional(
331 constants,
332 (
333 CONSTANTS_JZAZBZ_SAFDAR2017
334 if method == "safdar 2017"
335 else CONSTANTS_JZAZBZ_SAFDAR2021
336 ),
337 )
339 if method == "safdar 2017":
340 LMS_p = vecmul(MATRIX_JZAZBZ_IZAZBZ_TO_LMS_P_SAFDAR2017, Izazbz)
341 else:
342 Izazbz[..., 0] += constants.d_0
343 LMS_p = vecmul(MATRIX_JZAZBZ_IZAZBZ_TO_LMS_P_SAFDAR2021, Izazbz)
345 with domain_range_scale("ignore"):
346 LMS = eotf_ST2084(LMS_p, 10000, constants)
348 X_p_D65, Y_p_D65, Z_p_D65 = tsplit(vecmul(MATRIX_JZAZBZ_LMS_TO_XYZ, LMS))
350 X_D65 = (X_p_D65 + (constants.b - 1) * Z_p_D65) / constants.b
351 Y_D65 = (Y_p_D65 + (constants.g - 1) * X_D65) / constants.g
353 return tstack([X_D65, Y_D65, Z_p_D65])
356def XYZ_to_Jzazbz(
357 XYZ_D65: ArrayLike, constants: Structure = CONSTANTS_JZAZBZ_SAFDAR2017
358) -> Range1:
359 """
360 Convert from *CIE XYZ* tristimulus values to :math:`J_za_zb_z`
361 colourspace.
363 Parameters
364 ----------
365 XYZ_D65
366 *CIE XYZ* tristimulus values under
367 *CIE Standard Illuminant D Series D65*.
368 constants
369 :math:`J_za_zb_z` colourspace constants.
371 Returns
372 -------
373 :class:`numpy.ndarray`
374 :math:`J_za_zb_z` colourspace array where :math:`J_z` is
375 Lightness, :math:`a_z` is redness-greenness and :math:`b_z` is
376 yellowness-blueness.
378 Warnings
379 --------
380 The underlying *SMPTE ST 2084:2014* transfer function is an absolute
381 transfer function.
383 Notes
384 -----
385 - The underlying *SMPTE ST 2084:2014* transfer function is an
386 absolute transfer function, thus the domain and range values for
387 the *Reference* and *1* scales are only indicative that the data
388 is not affected by scale transformations. The effective domain of
389 *SMPTE ST 2084:2014* inverse electro-optical transfer function
390 (EOTF) is [0.0001, 10000].
391 domain of *SMPTE ST 2084:2014* inverse electro-optical transfer
392 function (EOTF) is [0.0001, 10000].
394 +------------+-----------------------+------------------+
395 | **Domain** | **Scale - Reference** | **Scale - 1** |
396 +============+=======================+==================+
397 | ``XYZ`` | ``UN`` | ``UN`` |
398 +------------+-----------------------+------------------+
400 +------------+-----------------------+------------------+
401 | **Range** | **Scale - Reference** | **Scale - 1** |
402 +============+=======================+==================+
403 | ``Jzazbz`` | 1 | 1 |
404 +------------+-----------------------+------------------+
406 References
407 ----------
408 :cite:`Safdar2017`
410 Examples
411 --------
412 >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952])
413 >>> XYZ_to_Jzazbz(XYZ) # doctest: +ELLIPSIS
414 array([ 0.0053504..., 0.0092430..., 0.0052600...])
415 """
417 XYZ_D65 = as_float_array(XYZ_D65)
419 with domain_range_scale("ignore"):
420 I_z, a_z, b_z = tsplit(
421 XYZ_to_Izazbz(XYZ_D65, CONSTANTS_JZAZBZ_SAFDAR2017, "Safdar 2017")
422 )
424 J_z = ((1 + constants.d) * I_z) / (1 + constants.d * I_z) - constants.d_0
426 return tstack([J_z, a_z, b_z])
429def Jzazbz_to_XYZ(
430 Jzazbz: Domain1, constants: Structure = CONSTANTS_JZAZBZ_SAFDAR2017
431) -> NDArrayFloat:
432 """
433 Convert from :math:`J_za_zb_z` colourspace to *CIE XYZ* tristimulus
434 values.
436 Parameters
437 ----------
438 Jzazbz
439 :math:`J_za_zb_z` colourspace array where :math:`J_z` is Lightness,
440 :math:`a_z` is redness-greenness and :math:`b_z` is
441 yellowness-blueness.
442 constants
443 :math:`J_za_zb_z` colourspace constants.
445 Returns
446 -------
447 :class:`numpy.ndarray`
448 *CIE XYZ* tristimulus values under
449 *CIE Standard Illuminant D Series D65*.
451 Warnings
452 --------
453 The underlying *SMPTE ST 2084:2014* transfer function is an absolute
454 transfer function.
456 Notes
457 -----
458 - The underlying *SMPTE ST 2084:2014* transfer function is an
459 absolute transfer function, thus the domain and range values for
460 the *Reference* and *1* scales are only indicative that the data
461 is not affected by scale transformations. The effective domain of
462 *SMPTE ST 2084:2014* inverse electro-optical transfer function
463 (EOTF) is [0.0001, 10000].
465 +------------+-----------------------+------------------+
466 | **Domain** | **Scale - Reference** | **Scale - 1** |
467 +============+=======================+==================+
468 | ``Jzazbz`` | 1 | 1 |
469 +------------+-----------------------+------------------+
471 +------------+-----------------------+------------------+
472 | **Range** | **Scale - Reference** | **Scale - 1** |
473 +============+=======================+==================+
474 | ``XYZ`` | ``UN`` | ``UN`` |
475 +------------+-----------------------+------------------+
477 References
478 ----------
479 :cite:`Safdar2017`
481 Examples
482 --------
483 >>> Jzazbz = np.array([0.00535048, 0.00924302, 0.00526007])
484 >>> Jzazbz_to_XYZ(Jzazbz) # doctest: +ELLIPSIS
485 array([ 0.2065402..., 0.1219723..., 0.0513696...])
486 """
488 J_z, a_z, b_z = tsplit(as_float_array(Jzazbz))
490 I_z = (J_z + constants.d_0) / (
491 1 + constants.d - constants.d * (J_z + constants.d_0)
492 )
494 with domain_range_scale("ignore"):
495 return Izazbz_to_XYZ(
496 tstack([I_z, a_z, b_z]), CONSTANTS_JZAZBZ_SAFDAR2017, "Safdar 2017"
497 )