Coverage for colour/models/cam16_ucs.py: 100%
83 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"""
2CAM16-LCD, CAM16-SCD, and CAM16-UCS Colourspaces - Li et al. (2017)
3===================================================================
5Define the *Li, Li, Wang, Zu, Luo, Cui, Melgosa, Brill and Pointer (2017)*
6*CAM16-LCD*, *CAM16-SCD*, and *CAM16-UCS* colourspaces transformations.
8- :func:`colour.JMh_CAM16_to_CAM16LCD`
9- :func:`colour.CAM16LCD_to_JMh_CAM16`
10- :func:`colour.JMh_CAM16_to_CAM16SCD`
11- :func:`colour.CAM16SCD_to_JMh_CAM16`
12- :func:`colour.JMh_CAM16_to_CAM16UCS`
13- :func:`colour.CAM16UCS_to_JMh_CAM16`
14- :func:`colour.XYZ_to_CAM16LCD`
15- :func:`colour.CAM16LCD_to_XYZ`
16- :func:`colour.XYZ_to_CAM16SCD`
17- :func:`colour.CAM16SCD_to_XYZ`
18- :func:`colour.XYZ_to_CAM16UCS`
19- :func:`colour.CAM16UCS_to_XYZ`
21References
22----------
23- :cite:`Li2017` : Li, C., Li, Z., Wang, Z., Xu, Y., Luo, M. R., Cui, G.,
24 Melgosa, M., Brill, M. H., & Pointer, M. (2017). Comprehensive color
25 solutions: CAM16, CAT16, and CAM16-UCS. Color Research & Application,
26 42(6), 703-718. doi:10.1002/col.22131
27"""
29from __future__ import annotations
31import re
32import typing
33from functools import partial
35if typing.TYPE_CHECKING:
36 from colour.hints import (
37 Any,
38 ArrayLike,
39 Callable,
40 Domain1,
41 Domain100,
42 Range1,
43 Range100,
44 )
46from colour.hints import ArrayLike, NDArrayFloat, cast
47from colour.models.cam02_ucs import (
48 COEFFICIENTS_UCS_LUO2006,
49 CAM02LCD_to_JMh_CIECAM02,
50 CAM02LCD_to_XYZ,
51 CAM02SCD_to_JMh_CIECAM02,
52 CAM02SCD_to_XYZ,
53 CAM02UCS_to_JMh_CIECAM02,
54 CAM02UCS_to_XYZ,
55 JMh_CIECAM02_to_CAM02LCD,
56 JMh_CIECAM02_to_CAM02SCD,
57 JMh_CIECAM02_to_CAM02UCS,
58 JMh_CIECAM02_to_UCS_Luo2006,
59 UCS_Luo2006_to_JMh_CIECAM02,
60 XYZ_to_CAM02LCD,
61 XYZ_to_CAM02SCD,
62 XYZ_to_CAM02UCS,
63)
64from colour.utilities import (
65 as_float_array,
66 copy_definition,
67 get_domain_range_scale,
68 optional,
69 tsplit,
70 tstack,
71)
73__author__ = "Colour Developers"
74__copyright__ = "Copyright 2013 Colour Developers"
75__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
76__maintainer__ = "Colour Developers"
77__email__ = "colour-developers@colour-science.org"
78__status__ = "Production"
80__all__ = [
81 "JMh_CAM16_to_UCS_Li2017",
82 "UCS_Li2017_to_JMh_CAM16",
83 "JMh_CAM16_to_CAM16LCD",
84 "CAM16LCD_to_JMh_CAM16",
85 "JMh_CAM16_to_CAM16SCD",
86 "CAM16SCD_to_JMh_CAM16",
87 "JMh_CAM16_to_CAM16UCS",
88 "CAM16UCS_to_JMh_CAM16",
89 "XYZ_to_UCS_Li2017",
90 "UCS_Li2017_to_XYZ",
91 "XYZ_to_CAM16LCD",
92 "CAM16LCD_to_XYZ",
93 "XYZ_to_CAM16SCD",
94 "CAM16SCD_to_XYZ",
95 "XYZ_to_CAM16UCS",
96 "CAM16UCS_to_XYZ",
97]
100def _UCS_Luo2006_callable_to_UCS_Li2017_docstring(callable_: Callable) -> str:
101 """
102 Convert the docstring of the specified *Luo et al. (2006)* callable to
103 conform to *Li et al. (2017)* conventions.
105 Parameters
106 ----------
107 callable_
108 Callable whose docstring will be adapted.
110 Returns
111 -------
112 :class:`str`
113 Converted docstring following *Li et al. (2017)* conventions.
114 """
116 docstring = callable_.__doc__
117 # NOTE: Required for optimised python launch.
118 docstring = optional(docstring, "")
120 docstring = docstring.replace("Luo et al. (2006)", "Li et al. (2017)")
121 docstring = docstring.replace("CIECAM02", "CAM16")
122 docstring = docstring.replace("CAM02", "CAM16")
123 docstring = docstring.replace("Luo2006b", "Li2017")
125 match = re.match("(.*)Examples", docstring, re.DOTALL)
126 if match is not None:
127 docstring = match.group(1)
129 return docstring
132JMh_CAM16_to_UCS_Li2017 = copy_definition(
133 JMh_CIECAM02_to_UCS_Luo2006, "JMh_CAM16_to_UCS_Li2017"
134)
135JMh_CAM16_to_UCS_Li2017.__doc__ = _UCS_Luo2006_callable_to_UCS_Li2017_docstring(
136 JMh_CIECAM02_to_UCS_Luo2006
137)
139UCS_Li2017_to_JMh_CAM16 = copy_definition(
140 UCS_Luo2006_to_JMh_CIECAM02, "UCS_Li2017_to_JMh_CAM16"
141)
142UCS_Li2017_to_JMh_CAM16.__doc__ = _UCS_Luo2006_callable_to_UCS_Li2017_docstring(
143 UCS_Luo2006_to_JMh_CIECAM02
144)
146JMh_CAM16_to_CAM16LCD = partial(
147 JMh_CAM16_to_UCS_Li2017, coefficients=COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]
148)
149JMh_CAM16_to_CAM16LCD.__doc__ = _UCS_Luo2006_callable_to_UCS_Li2017_docstring(
150 JMh_CIECAM02_to_CAM02LCD
151)
153CAM16LCD_to_JMh_CAM16 = partial(
154 UCS_Li2017_to_JMh_CAM16, coefficients=COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]
155)
156CAM16LCD_to_JMh_CAM16.__doc__ = _UCS_Luo2006_callable_to_UCS_Li2017_docstring(
157 CAM02LCD_to_JMh_CIECAM02
158)
160JMh_CAM16_to_CAM16SCD = partial(
161 JMh_CAM16_to_UCS_Li2017, coefficients=COEFFICIENTS_UCS_LUO2006["CAM02-SCD"]
162)
163JMh_CAM16_to_CAM16SCD.__doc__ = _UCS_Luo2006_callable_to_UCS_Li2017_docstring(
164 JMh_CIECAM02_to_CAM02SCD
165)
167CAM16SCD_to_JMh_CAM16 = partial(
168 UCS_Li2017_to_JMh_CAM16, coefficients=COEFFICIENTS_UCS_LUO2006["CAM02-SCD"]
169)
170CAM16SCD_to_JMh_CAM16.__doc__ = _UCS_Luo2006_callable_to_UCS_Li2017_docstring(
171 CAM02SCD_to_JMh_CIECAM02
172)
174JMh_CAM16_to_CAM16UCS = partial(
175 JMh_CAM16_to_UCS_Li2017, coefficients=COEFFICIENTS_UCS_LUO2006["CAM02-UCS"]
176)
177JMh_CAM16_to_CAM16UCS.__doc__ = _UCS_Luo2006_callable_to_UCS_Li2017_docstring(
178 JMh_CIECAM02_to_CAM02UCS
179)
181CAM16UCS_to_JMh_CAM16 = partial(
182 UCS_Li2017_to_JMh_CAM16, coefficients=COEFFICIENTS_UCS_LUO2006["CAM02-UCS"]
183)
184CAM16UCS_to_JMh_CAM16.__doc__ = _UCS_Luo2006_callable_to_UCS_Li2017_docstring(
185 CAM02UCS_to_JMh_CIECAM02
186)
189def XYZ_to_UCS_Li2017(
190 XYZ: Domain1,
191 coefficients: ArrayLike,
192 **kwargs: Any,
193) -> Range100:
194 """
195 Convert from *CIE XYZ* tristimulus values to one of the *Li et al.
196 (2017)* *CAM16-LCD*, *CAM16-SCD*, or *CAM16-UCS* colourspaces
197 :math:`J'a'b'` array.
199 Parameters
200 ----------
201 XYZ
202 *CIE XYZ* tristimulus values.
203 coefficients
204 Coefficients of one of the *Li et al. (2017)* *CAM16-LCD*,
205 *CAM16-SCD*, or *CAM16-UCS* colourspaces.
207 Other Parameters
208 ----------------
209 kwargs
210 {:func:`colour.XYZ_to_CAM16`},
211 See the documentation of the previously listed definition. The
212 default viewing conditions are those of *IEC 61966-2-1:1999*,
213 i.e., *sRGB* 64 Lux ambient illumination, 80 :math:`cd/m^2`,
214 adapting field luminance about 20% of a white object in the
215 scene.
217 Returns
218 -------
219 :class:`numpy.ndarray`
220 *Li et al. (2017)* *CAM16-LCD*, *CAM16-SCD*, or *CAM16-UCS*
221 colourspaces :math:`J'a'b'` array.
223 Warnings
224 --------
225 The ``XYZ_w`` parameter for :func:`colour.XYZ_to_CAM16` definition must
226 be specified in the same domain-range scale as the ``XYZ`` parameter.
228 Notes
229 -----
230 +------------+------------------------+------------------+
231 | **Domain** | **Scale - Reference** | **Scale - 1** |
232 +============+========================+==================+
233 | ``XYZ`` | 1 | 1 |
234 +------------+------------------------+------------------+
236 +------------+------------------------+------------------+
237 | **Range** | **Scale - Reference** | **Scale - 1** |
238 +============+========================+==================+
239 | ``Jpapbp`` | 100 | 1 |
240 +------------+------------------------+------------------+
242 Examples
243 --------
244 >>> import numpy as np
245 >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952])
246 >>> XYZ_to_UCS_Li2017(XYZ, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"])
247 ... # doctest: +ELLIPSIS
248 array([ 46.0658603..., 41.0758649..., 14.5102582...])
250 >>> from colour.appearance import CAM_KWARGS_CIECAM02_sRGB
251 >>> XYZ_w = CAM_KWARGS_CIECAM02_sRGB["XYZ_w"]
252 >>> XYZ_to_UCS_Li2017(XYZ, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"], XYZ_w=XYZ_w / 100)
253 ... # doctest: +ELLIPSIS
254 array([ 46.0658603..., 41.0758649..., 14.5102582...])
255 """
257 from colour.appearance import ( # noqa: PLC0415
258 CAM_KWARGS_CIECAM02_sRGB,
259 XYZ_to_CAM16,
260 )
262 domain_range_reference = get_domain_range_scale() == "reference"
264 settings = CAM_KWARGS_CIECAM02_sRGB.copy()
265 settings.update(**kwargs)
266 XYZ_w = kwargs.get("XYZ_w")
267 if XYZ_w is not None and domain_range_reference:
268 settings["XYZ_w"] = XYZ_w * 100
270 if domain_range_reference:
271 XYZ = as_float_array(XYZ) * 100
273 specification = XYZ_to_CAM16(XYZ, **settings)
274 JMh = tstack(
275 [
276 cast("NDArrayFloat", specification.J),
277 cast("NDArrayFloat", specification.M),
278 cast("NDArrayFloat", specification.h),
279 ]
280 )
282 return JMh_CAM16_to_UCS_Li2017(JMh, coefficients)
285def UCS_Li2017_to_XYZ(
286 Jpapbp: Domain100,
287 coefficients: ArrayLike,
288 **kwargs: Any,
289) -> Range1:
290 """
291 Convert from one of the *Li et al. (2017)* *CAM16-LCD*, *CAM16-SCD*, or
292 *CAM16-UCS* colourspaces :math:`J'a'b'` array to *CIE XYZ* tristimulus
293 values.
295 Parameters
296 ----------
297 Jpapbp
298 *Li et al. (2017)* *CAM16-LCD*, *CAM16-SCD*, or *CAM16-UCS*
299 colourspaces :math:`J'a'b'` array.
300 coefficients
301 Coefficients of one of the *Li et al. (2017)* *CAM16-LCD*,
302 *CAM16-SCD*, or *CAM16-UCS* colourspaces.
304 Other Parameters
305 ----------------
306 kwargs
307 {:func:`colour.CAM16_to_XYZ`},
308 See the documentation of the previously listed definition. The
309 default viewing conditions are those of *IEC 61966-2-1:1999*,
310 i.e., *sRGB* 64 Lux ambient illumination, 80 :math:`cd/m^2`,
311 adapting field luminance about 20% of a white object in the
312 scene.
314 Returns
315 -------
316 :class:`numpy.ndarray`
317 *CIE XYZ* tristimulus values.
319 Warnings
320 --------
321 The ``XYZ_w`` parameter for :func:`colour.XYZ_to_CAM16` definition must
322 be specified in the same domain-range scale as the ``XYZ`` parameter.
324 Notes
325 -----
326 +------------+------------------------+------------------+
327 | **Domain** | **Scale - Reference** | **Scale - 1** |
328 +============+========================+==================+
329 | ``Jpapbp`` | 100 | 1 |
330 +------------+------------------------+------------------+
332 +------------+------------------------+------------------+
333 | **Range** | **Scale - Reference** | **Scale - 1** |
334 +============+========================+==================+
335 | ``XYZ`` | 1 | 1 |
336 +------------+------------------------+------------------+
338 Examples
339 --------
340 >>> import numpy as np
341 >>> Jpapbp = np.array([46.06586037, 41.07586491, 14.51025828])
342 >>> UCS_Li2017_to_XYZ(Jpapbp, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"])
343 ... # doctest: +ELLIPSIS
344 array([ 0.2065400..., 0.1219722..., 0.0513695...])
346 >>> from colour.appearance import CAM_KWARGS_CIECAM02_sRGB
347 >>> XYZ_w = CAM_KWARGS_CIECAM02_sRGB["XYZ_w"]
348 >>> UCS_Li2017_to_XYZ(
349 ... Jpapbp, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"], XYZ_w=XYZ_w / 100
350 ... )
351 ... # doctest: +ELLIPSIS
352 array([ 0.2065400..., 0.1219722..., 0.0513695...])
353 """
355 from colour.appearance import ( # noqa: PLC0415
356 CAM16_to_XYZ,
357 CAM_KWARGS_CIECAM02_sRGB,
358 CAM_Specification_CAM16,
359 )
361 domain_range_reference = get_domain_range_scale() == "reference"
363 settings = CAM_KWARGS_CIECAM02_sRGB.copy()
364 settings.update(**kwargs)
365 XYZ_w = kwargs.get("XYZ_w")
367 if XYZ_w is not None and domain_range_reference:
368 settings["XYZ_w"] = XYZ_w * 100
370 J, M, h = tsplit(UCS_Li2017_to_JMh_CAM16(Jpapbp, coefficients))
372 specification = CAM_Specification_CAM16(J=J, M=M, h=h)
374 XYZ = CAM16_to_XYZ(specification, **settings)
376 if domain_range_reference:
377 XYZ /= 100
379 return XYZ
382XYZ_to_CAM16LCD = partial(
383 XYZ_to_UCS_Li2017, coefficients=COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]
384)
385XYZ_to_CAM16LCD.__doc__ = _UCS_Luo2006_callable_to_UCS_Li2017_docstring(XYZ_to_CAM02LCD)
386XYZ_to_CAM16LCD.__annotations__ = XYZ_to_UCS_Li2017.__annotations__.copy()
388CAM16LCD_to_XYZ = partial(
389 UCS_Li2017_to_XYZ, coefficients=COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]
390)
391CAM16LCD_to_XYZ.__doc__ = _UCS_Luo2006_callable_to_UCS_Li2017_docstring(CAM02LCD_to_XYZ)
393XYZ_to_CAM16SCD = partial(
394 XYZ_to_UCS_Li2017, coefficients=COEFFICIENTS_UCS_LUO2006["CAM02-SCD"]
395)
396XYZ_to_CAM16SCD.__doc__ = _UCS_Luo2006_callable_to_UCS_Li2017_docstring(XYZ_to_CAM02SCD)
397XYZ_to_CAM16SCD.__annotations__ = XYZ_to_UCS_Li2017.__annotations__.copy()
399CAM16SCD_to_XYZ = partial(
400 UCS_Li2017_to_XYZ, coefficients=COEFFICIENTS_UCS_LUO2006["CAM02-SCD"]
401)
402CAM16SCD_to_XYZ.__doc__ = _UCS_Luo2006_callable_to_UCS_Li2017_docstring(CAM02SCD_to_XYZ)
404XYZ_to_CAM16UCS = partial(
405 XYZ_to_UCS_Li2017, coefficients=COEFFICIENTS_UCS_LUO2006["CAM02-UCS"]
406)
407XYZ_to_CAM16UCS.__doc__ = _UCS_Luo2006_callable_to_UCS_Li2017_docstring(XYZ_to_CAM02UCS)
408XYZ_to_CAM16UCS.__annotations__ = XYZ_to_UCS_Li2017.__annotations__.copy()
410CAM16UCS_to_XYZ = partial(
411 UCS_Li2017_to_XYZ, coefficients=COEFFICIENTS_UCS_LUO2006["CAM02-UCS"]
412)
413CAM16UCS_to_XYZ.__doc__ = _UCS_Luo2006_callable_to_UCS_Li2017_docstring(CAM02UCS_to_XYZ)