Coverage for models/common.py: 40%
40 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"""
2Common Colour Models Utilities
3==============================
5Define utilities for common colour models and transformations.
7- :attr:`colour.COLOURSPACE_MODELS`
8- :func:`colour.models.Jab_to_JCh`
9- :func:`colour.models.JCh_to_Jab`
10- :func:`colour.models.XYZ_to_Iab`
11- :func:`colour.models.Iab_to_XYZ`
13References
14----------
15- :cite:`CIETC1-482004m` : CIE TC 1-48. (2004). CIE 1976 uniform colour
16 spaces. In CIE 015:2004 Colorimetry, 3rd Edition (p. 24).
17 ISBN:978-3-901906-33-6
18"""
20from __future__ import annotations
22import typing
24import numpy as np
26from colour.algebra import cartesian_to_polar, polar_to_cartesian, vecmul
28if typing.TYPE_CHECKING:
29 from colour.hints import Callable
31from colour.hints import ( # noqa: TC001
32 Annotated,
33 ArrayLike,
34 Domain1,
35 NDArrayFloat,
36 Range1,
37)
38from colour.utilities import (
39 CanonicalMapping,
40 attest,
41 from_range_1,
42 from_range_degrees,
43 to_domain_1,
44 to_domain_degrees,
45 tsplit,
46 tstack,
47)
48from colour.utilities.documentation import DocstringTuple, is_documentation_building
50__author__ = "Colour Developers"
51__copyright__ = "Copyright 2013 Colour Developers"
52__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
53__maintainer__ = "Colour Developers"
54__email__ = "colour-developers@colour-science.org"
55__status__ = "Production"
57__all__ = [
58 "COLOURSPACE_MODELS",
59 "COLOURSPACE_MODELS_AXIS_LABELS",
60 "COLOURSPACE_MODELS_DOMAIN_RANGE_SCALE_1_TO_REFERENCE",
61 "Jab_to_JCh",
62 "JCh_to_Jab",
63 "XYZ_to_Iab",
64 "Iab_to_XYZ",
65]
67COLOURSPACE_MODELS: tuple = (
68 "CAM02LCD",
69 "CAM02SCD",
70 "CAM02UCS",
71 "CAM16LCD",
72 "CAM16SCD",
73 "CAM16UCS",
74 "CIE 1931",
75 "CIE 1960 UCS",
76 "CIE 1976 UCS",
77 "CIE Lab",
78 "CIE Luv",
79 "CIE UCS",
80 "CIE UVW",
81 "CIE XYZ",
82 "CIE xyY",
83 "DIN99",
84 "HCL",
85 "hdr-CIELAB",
86 "hdr-IPT",
87 "HSL",
88 "HSV",
89 "Hunter Lab",
90 "Hunter Rdab",
91 "ICaCb",
92 "ICtCp",
93 "IHLS",
94 "IPT Ragoo 2021",
95 "IPT",
96 "IgPgTg",
97 "Jzazbz",
98 "OSA UCS",
99 "Oklab",
100 "RGB",
101 "sUCS",
102 "YCbCr",
103 "YCoCg",
104 "Yrg",
105)
106if is_documentation_building(): # pragma: no cover
107 COLOURSPACE_MODELS = DocstringTuple(COLOURSPACE_MODELS)
108 COLOURSPACE_MODELS.__doc__ = """
109Colourspace models supporting a direct conversion to *CIE XYZ* tristimulus
110values.
111"""
113COLOURSPACE_MODELS_AXIS_LABELS: CanonicalMapping = CanonicalMapping(
114 {
115 "CAM02LCD": ("$J^'$", "$a^'$", "$b^'$"),
116 "CAM02SCD": ("$J^'$", "$a^'$", "$b^'$"),
117 "CAM02UCS": ("$J^'$", "$a^'$", "$b^'$"),
118 "CAM16LCD": ("$J^'$", "$a^'$", "$b^'$"),
119 "CAM16SCD": ("$J^'$", "$a^'$", "$b^'$"),
120 "CAM16UCS": ("$J^'$", "$a^'$", "$b^'$"),
121 "CIE 1931": ("x", "y", "Y"),
122 "CIE 1960 UCS": ("$u^'$", "$v^'$", "$L^*$"),
123 "CIE 1976 UCS": ("$u^'$", "$v^'$", "$L^*$"),
124 "CIE Lab": ("$L^*$", "$a^*$", "$b^*$"),
125 "CIE Luv": ("$L^*$", "$u^'$", "$v^'$"),
126 "CIE UCS": ("U", "V", "W"),
127 "CIE UVW": ("U", "V", "W"),
128 "CIE XYZ": ("X", "Y", "Z"),
129 "CIE xyY": ("x", "y", "Y"),
130 "DIN99": ("$L_{99}$", "$a_{99}$", "$b_{99}$"),
131 "HCL": ("H", "C", "L"),
132 "hdr-CIELAB": ("L hdr", "a hdr", "b hdr"),
133 "hdr-IPT": ("I hdr", "P hdr", "T hdr"),
134 "HSL": ("H", "S", "L"),
135 "HSV": ("H", "S", "V"),
136 "Hunter Lab": ("$L^*$", "$a^*$", "$b^*$"),
137 "Hunter Rdab": ("Rd", "a", "b"),
138 "ICaCb": ("$I$", "$C_a$", "$C_b$"),
139 "ICtCp": ("$I$", "$C_T$", "$C_P$"),
140 "IHLS": ("H", "Y", "S"),
141 "IPT Ragoo 2021": ("I", "P", "T"),
142 "IPT": ("I", "P", "T"),
143 "IgPgTg": ("$I_G$", "$P_G$", "$T_G$"),
144 "Jzazbz": ("$J_z$", "$a_z$", "$b_z$"),
145 "OSA UCS": ("L", "j", "g"),
146 "Oklab": ("$L$", "$a$", "$b$"),
147 "RGB": ("R", "G", "B"),
148 "sUCS": ("I", "a", "b"),
149 "YCbCr": ("Y", "$C_b$", "$C_r$"),
150 "YCoCg": ("Y", "$C_o$", "$C_g$"),
151 "Yrg": ("Y", "r", "g"),
152 }
153)
154"""Colourspace models labels mapping."""
156attest(tuple(COLOURSPACE_MODELS_AXIS_LABELS.keys()) == COLOURSPACE_MODELS)
158COLOURSPACE_MODELS_DOMAIN_RANGE_SCALE_1_TO_REFERENCE: CanonicalMapping = (
159 CanonicalMapping(
160 {
161 "CAM02LCD": np.array([100, 100, 100]),
162 "CAM02SCD": np.array([100, 100, 100]),
163 "CAM02UCS": np.array([100, 100, 100]),
164 "CAM16LCD": np.array([100, 100, 100]),
165 "CAM16SCD": np.array([100, 100, 100]),
166 "CAM16UCS": np.array([100, 100, 100]),
167 "CIE 1931": np.array([1, 1, 1]),
168 "CIE 1960 UCS": np.array([1, 1, 1]),
169 "CIE 1976 UCS": np.array([1, 1, 100]),
170 "CIE Lab": np.array([100, 100, 100]),
171 "CIE Luv": np.array([100, 100, 100]),
172 "CIE UCS": np.array([1, 1, 1]),
173 "CIE UVW": np.array([100, 100, 100]),
174 "CIE XYZ": np.array([1, 1, 1]),
175 "CIE xyY": np.array([1, 1, 1]),
176 "DIN99": np.array([100, 100, 100]),
177 "HCL": np.array([1, 1, 1]),
178 "hdr-CIELAB": np.array([100, 100, 100]),
179 "hdr-IPT": np.array([100, 100, 100]),
180 "HSL": np.array([1, 1, 1]),
181 "HSV": np.array([1, 1, 1]),
182 "Hunter Lab": np.array([100, 100, 100]),
183 "Hunter Rdab": np.array([100, 100, 100]),
184 "ICaCb": np.array([1, 1, 1]),
185 "ICtCp": np.array([1, 1, 1]),
186 "IHLS": np.array([1, 1, 1]),
187 "IPT Ragoo 2021": np.array([1, 1, 1]),
188 "IPT": np.array([1, 1, 1]),
189 "IgPgTg": np.array([1, 1, 1]),
190 "Jzazbz": np.array([1, 1, 1]),
191 "OSA UCS": np.array([100, 100, 100]),
192 "Oklab": np.array([1, 1, 1]),
193 "RGB": np.array([1, 1, 1]),
194 "sUCS": np.array([100, 100, 100]),
195 "YCbCr": np.array([1, 1, 1]),
196 "YCoCg": np.array([1, 1, 1]),
197 "Yrg": np.array([1, 1, 1]),
198 }
199 )
200)
201"""Colourspace models domain-range scale **'1'** to **'Reference'** mapping."""
204def Jab_to_JCh(Jab: Domain1) -> Annotated[NDArrayFloat, (1, 1, 360)]:
205 """
206 Convert from *Jab* colour representation to *JCh* colour representation.
208 This definition performs conversion from *CIE L\\*a\\*b\\** colourspace to
209 *CIE L\\*C\\*Hab* colourspace and other similar conversions. It implements
210 a generic transformation from *lightness* :math:`J`, :math:`a` and
211 :math:`b` opponent colour dimensions to the correlates of *lightness*
212 :math:`J`, chroma :math:`C` and hue angle :math:`h`.
214 Parameters
215 ----------
216 Jab
217 *Jab* colour representation array.
219 Returns
220 -------
221 :class:`numpy.ndarray`
222 *JCh* colour representation array.
224 Notes
225 -----
226 +------------+-----------------------+-----------------+
227 | **Domain** | **Scale - Reference** | **Scale - 1** |
228 +============+=======================+=================+
229 | ``Jab`` | 1 | 1 |
230 +------------+-----------------------+-----------------+
232 +------------+-----------------------+-----------------+
233 | **Range** | **Scale - Reference** | **Scale - 1** |
234 +============+=======================+=================+
235 | ``JCh`` | ``J`` : 1 | ``J`` : 1 |
236 | | | |
237 | | ``C`` : 1 | ``C`` : 1 |
238 | | | |
239 | | ``h`` : 360 | ``h`` : 1 |
240 +------------+-----------------------+-----------------+
242 References
243 ----------
244 :cite:`CIETC1-482004m`
246 Examples
247 --------
248 >>> Jab = np.array([41.52787529, 52.63858304, 26.92317922])
249 >>> Jab_to_JCh(Jab) # doctest: +ELLIPSIS
250 array([ 41.5278752..., 59.1242590..., 27.0884878...])
251 """
253 L, a, b = tsplit(Jab)
255 C, h = tsplit(cartesian_to_polar(tstack([a, b])))
257 return tstack([L, C, from_range_degrees(np.degrees(h) % 360)])
260def JCh_to_Jab(
261 JCh: Annotated[ArrayLike, (1, 1, 360)],
262) -> Range1:
263 """
264 Convert from *JCh* colour representation to *Jab* colour representation.
266 This definition performs conversion from *CIE L\\*C\\*Hab* colourspace to
267 *CIE L\\*a\\*b\\** colourspace and other similar conversions. It implements
268 a generic transformation from the correlates of *lightness* :math:`J`,
269 chroma :math:`C` and hue angle :math:`h` to *lightness* :math:`J`,
270 :math:`a` and :math:`b` opponent colour dimensions
272 Parameters
273 ----------
274 JCh
275 *JCh* colour representation array.
277 Returns
278 -------
279 :class:`numpy.ndarray`
280 *Jab* colour representation array.
282 Notes
283 -----
284 +-------------+-----------------------+-----------------+
285 | **Domain** | **Scale - Reference** | **Scale - 1** |
286 +=============+=======================+=================+
287 | ``JCh`` | ``J`` : 1 | ``J`` : 1 |
288 | | | |
289 | | ``C`` : 1 | ``C`` : 1 |
290 | | | |
291 | | ``h`` : 360 | ``h`` : 1 |
292 +-------------+-----------------------+-----------------+
294 +-------------+-----------------------+-----------------+
295 | **Range** | **Scale - Reference** | **Scale - 1** |
296 +=============+=======================+=================+
297 | ``Jab`` | 1 | 1 |
298 +-------------+-----------------------+-----------------+
300 References
301 ----------
302 :cite:`CIETC1-482004m`
304 Examples
305 --------
306 >>> JCh = np.array([41.52787529, 59.12425901, 27.08848784])
307 >>> JCh_to_Jab(JCh) # doctest: +ELLIPSIS
308 array([ 41.5278752..., 52.6385830..., 26.9231792...])
309 """
311 L, C, h = tsplit(JCh)
313 a, b = tsplit(polar_to_cartesian(tstack([C, np.radians(to_domain_degrees(h))])))
315 return tstack([L, a, b])
318def XYZ_to_Iab(
319 XYZ: Domain1,
320 LMS_to_LMS_p_callable: Callable,
321 matrix_XYZ_to_LMS: ArrayLike,
322 matrix_LMS_p_to_Iab: ArrayLike,
323) -> Range1:
324 """
325 Convert from *CIE XYZ* tristimulus values to *IPT*-like :math:`Iab`
326 colour representation.
328 Perform conversion from *CIE XYZ* tristimulus values to *IPT*
329 colourspace and other similar conversions. It implements a generic
330 transformation from *CIE XYZ* tristimulus values to *lightness*
331 :math:`I`, :math:`a` representing the red-green dimension (the
332 dimension lost by protanopes), and :math:`b` representing the
333 yellow-blue dimension (the dimension lost by tritanopes).
335 Parameters
336 ----------
337 XYZ
338 *CIE XYZ* tristimulus values.
339 LMS_to_LMS_p_callable
340 Callable applying the forward non-linearity to the :math:`LMS`
341 colourspace array.
342 matrix_XYZ_to_LMS
343 Matrix converting from *CIE XYZ* tristimulus values to
344 :math:`LMS` colourspace.
345 matrix_LMS_p_to_Iab
346 Matrix converting from non-linear :math:`LMS_p` colourspace to
347 *IPT*-like :math:`Iab` colour representation.
349 Returns
350 -------
351 :class:`numpy.ndarray`
352 *IPT*-like :math:`Iab` colour representation.
354 Notes
355 -----
356 +------------+-----------------------+-----------------+
357 | **Domain** | **Scale - Reference** | **Scale - 1** |
358 +============+=======================+=================+
359 | ``XYZ`` | 1 | 1 |
360 +------------+-----------------------+-----------------+
362 +------------+-----------------------+-----------------+
363 | **Range** | **Scale - Reference** | **Scale - 1** |
364 +============+=======================+=================+
365 | ``Iab`` | 1 | 1 |
366 +------------+-----------------------+-----------------+
368 Examples
369 --------
370 >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952])
371 >>> LMS_to_LMS_p = lambda x: x**0.43
372 >>> M_XYZ_to_LMS = np.array(
373 ... [
374 ... [0.4002, 0.7075, -0.0807],
375 ... [-0.2280, 1.1500, 0.0612],
376 ... [0.0000, 0.0000, 0.9184],
377 ... ]
378 ... )
379 >>> M_LMS_p_to_Iab = np.array(
380 ... [
381 ... [0.4000, 0.4000, 0.2000],
382 ... [4.4550, -4.8510, 0.3960],
383 ... [0.8056, 0.3572, -1.1628],
384 ... ]
385 ... )
386 >>> XYZ_to_Iab(XYZ, LMS_to_LMS_p, M_XYZ_to_LMS, M_LMS_p_to_Iab)
387 ... # doctest: +ELLIPSIS
388 array([ 0.3842619..., 0.3848730..., 0.1888683...])
389 """
391 XYZ = to_domain_1(XYZ)
393 LMS = vecmul(matrix_XYZ_to_LMS, XYZ)
394 LMS_p = LMS_to_LMS_p_callable(LMS)
395 Iab = vecmul(matrix_LMS_p_to_Iab, LMS_p)
397 return from_range_1(Iab)
400def Iab_to_XYZ(
401 Iab: Domain1,
402 LMS_p_to_LMS_callable: Callable,
403 matrix_Iab_to_LMS_p: ArrayLike,
404 matrix_LMS_to_XYZ: ArrayLike,
405) -> Range1:
406 """
407 Convert from *IPT*-like :math:`Iab` colour representation to *CIE XYZ*
408 tristimulus values.
410 Perform conversion from *IPT* colourspace to *CIE XYZ* tristimulus
411 values and other similar conversions. It implements a generic
412 transformation from *lightness* :math:`I`, :math:`a` representing the
413 red-green dimension (the dimension lost by protanopes), and :math:`b`
414 representing the yellow-blue dimension (the dimension lost by tritanopes)
415 to *CIE XYZ* tristimulus values.
417 Parameters
418 ----------
419 Iab
420 *IPT*-like :math:`Iab` colour representation.
421 LMS_p_to_LMS_callable
422 Callable applying the reverse non-linearity to the :math:`LMS_p`
423 colourspace array.
424 matrix_Iab_to_LMS_p
425 Matrix converting from *IPT*-like :math:`Iab` colour
426 representation to non-linear :math:`LMS_p` colourspace.
427 matrix_LMS_to_XYZ
428 Matrix converting from :math:`LMS` colourspace to *CIE XYZ*
429 tristimulus values.
431 Returns
432 -------
433 :class:`numpy.ndarray`
434 *CIE XYZ* tristimulus values.
436 Notes
437 -----
438 +------------+-----------------------+-----------------+
439 | **Domain** | **Scale - Reference** | **Scale - 1** |
440 +============+=======================+=================+
441 | ``Iab`` | 1 | 1 |
442 +------------+-----------------------+-----------------+
444 +------------+-----------------------+-----------------+
445 | **Range** | **Scale - Reference** | **Scale - 1** |
446 +============+=======================+=================+
447 | ``XYZ`` | 1 | 1 |
448 +------------+-----------------------+-----------------+
450 Examples
451 --------
452 >>> Iab = np.array([0.38426191, 0.38487306, 0.18886838])
453 >>> LMS_p_to_LMS = lambda x: x ** (1 / 0.43)
454 >>> M_Iab_to_LMS_p = np.linalg.inv(
455 ... np.array(
456 ... [
457 ... [0.4000, 0.4000, 0.2000],
458 ... [4.4550, -4.8510, 0.3960],
459 ... [0.8056, 0.3572, -1.1628],
460 ... ]
461 ... )
462 ... )
463 >>> M_LMS_to_XYZ = np.linalg.inv(
464 ... np.array(
465 ... [
466 ... [0.4002, 0.7075, -0.0807],
467 ... [-0.2280, 1.1500, 0.0612],
468 ... [0.0000, 0.0000, 0.9184],
469 ... ]
470 ... )
471 ... )
472 >>> Iab_to_XYZ(Iab, LMS_p_to_LMS, M_Iab_to_LMS_p, M_LMS_to_XYZ)
473 ... # doctest: +ELLIPSIS
474 array([ 0.2065400..., 0.1219722..., 0.0513695...])
475 """
477 Iab = to_domain_1(Iab)
479 LMS = vecmul(matrix_Iab_to_LMS_p, Iab)
480 LMS_p = LMS_p_to_LMS_callable(LMS)
481 XYZ = vecmul(matrix_LMS_to_XYZ, LMS_p)
483 return from_range_1(XYZ)