Coverage for colour/models/sucs.py: 100%
54 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"""
2sUCS Colourspace
3================
5Define the *sUCS* colourspace transformations.
7- :func:`colour.XYZ_to_sUCS`
8- :func:`colour.sUCS_to_XYZ`
9- :func:`colour.sUCS_chroma`
10- :func:`colour.sUCS_hue_angle`
12The *sUCS* (Simple Uniform Colour Space) is designed for simplicity and
13perceptual uniformity. This implementation is based on the work by
14*Li & Luo (2024)*.
16References
17----------
18- :cite:`Li2024` : Li, M., & Luo, M. R. (2024). Simple color appearance model
19 (sCAM) based on simple uniform color space (sUCS). Optics Express, 32(3),
20 3100. doi:10.1364/OE.510196
21"""
23from __future__ import annotations
25from functools import partial
27import numpy as np
29from colour.algebra import spow
30from colour.hints import ( # noqa: TC001
31 Domain1,
32 Domain100,
33 Domain100_100_360,
34 NDArrayFloat,
35 Range1,
36 Range100,
37 Range100_100_360,
38 Range360,
39)
40from colour.models import Iab_to_XYZ, XYZ_to_Iab
41from colour.utilities import (
42 as_float,
43 domain_range_scale,
44 from_range_1,
45 from_range_100,
46 from_range_degrees,
47 to_domain_1,
48 to_domain_100,
49 to_domain_degrees,
50 tsplit,
51 tstack,
52)
54__author__ = "UltraMo114(Molin Li), Colour Developers"
55__copyright__ = "Copyright 2024 Colour Developers"
56__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
57__maintainer__ = "Colour Developers"
58__email__ = "colour-developers@colour-science.org"
59__status__ = "Production"
61__all__ = [
62 "MATRIX_SUCS_XYZ_TO_LMS",
63 "MATRIX_SUCS_LMS_TO_XYZ",
64 "MATRIX_SUCS_LMS_P_TO_IAB",
65 "MATRIX_SUCS_IAB_TO_LMS_P",
66 "XYZ_to_sUCS",
67 "sUCS_to_XYZ",
68 "sUCS_chroma",
69 "sUCS_hue_angle",
70 "sUCS_Iab_to_sUCS_ICh",
71 "sUCS_ICh_to_sUCS_Iab",
72]
74MATRIX_SUCS_XYZ_TO_LMS: NDArrayFloat = np.array(
75 [
76 [0.4002, 0.7075, -0.0807],
77 [-0.2280, 1.1500, 0.0612],
78 [0.0000, 0.0000, 0.9184],
79 ]
80)
81"""
82*CIE XYZ* tristimulus values (*CIE Standard Illuminant D Series* *D65*-adapted,
83Y=1 for white) to LMS-like cone responses matrix.
84"""
86MATRIX_SUCS_LMS_TO_XYZ: NDArrayFloat = np.linalg.inv(MATRIX_SUCS_XYZ_TO_LMS)
87"""
88LMS-like cone responses to *CIE XYZ* tristimulus values
89(*CIE Standard Illuminant D Series* *D65*-adapted, Y=1 for white) matrix.
90"""
92MATRIX_SUCS_LMS_P_TO_IAB: NDArrayFloat = np.array(
93 [
94 [200.0 / 3.05, 100.0 / 3.05, 5.0 / 3.05],
95 [430.0, -470.0, 40.0],
96 [49.0, 49.0, -98.0],
97 ]
98)
99"""
100Non-linear LMS-like responses :math:`LMS_p` to intermediate :math:`Iab`
101colourspace matrix.
102"""
104MATRIX_SUCS_IAB_TO_LMS_P: NDArrayFloat = np.linalg.inv(MATRIX_SUCS_LMS_P_TO_IAB)
105"""
106Intermediate :math:`Iab` colourspace to non-linear LMS-like responses
107:math:`LMS_p` matrix.
108"""
111def XYZ_to_sUCS(XYZ: Domain1) -> Range100:
112 """
113 Convert from *CIE XYZ* tristimulus values to *sUCS* colourspace.
115 Parameters
116 ----------
117 XYZ
118 *CIE XYZ* tristimulus values, adapted to
119 *CIE Standard Illuminant D65* and in domain [0, 1] (where white
120 :math:`Y` is 1.0).
122 Returns
123 -------
124 :class:`numpy.ndarray`
125 *sUCS* :math:`Iab` colourspace array.
127 Notes
128 -----
129 +------------+-----------------------+-----------------+
130 | **Domain** | **Scale - Reference** | **Scale - 1** |
131 +============+=======================+=================+
132 | ``XYZ`` | 1 | 1 |
133 +------------+-----------------------+-----------------+
135 +------------+-----------------------+------------------+
136 | **Range** | **Scale - Reference** | **Scale - 1** |
137 +============+=======================+==================+
138 | ``Iab`` | 100 | 1 |
139 +------------+-----------------------+------------------+
141 - Input *CIE XYZ* tristimulus values must be adapted to
142 *CIE Standard Illuminant D Series* *D65*.
144 References
145 ----------
146 :cite:`Li2024`
148 Examples
149 --------
150 >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952])
151 >>> XYZ_to_sUCS(XYZ) # doctest: +ELLIPSIS
152 array([ 42.6292365..., 36.9764683..., 14.1230135...])
153 """
155 XYZ = to_domain_1(XYZ)
157 with domain_range_scale("ignore"):
158 Iab = XYZ_to_Iab(
159 XYZ,
160 partial(spow, p=0.43),
161 MATRIX_SUCS_XYZ_TO_LMS,
162 MATRIX_SUCS_LMS_P_TO_IAB,
163 )
165 return from_range_100(Iab)
168def sUCS_to_XYZ(Iab: Domain100) -> Range1:
169 """
170 Convert from *sUCS* colourspace to *CIE XYZ* tristimulus values.
172 Parameters
173 ----------
174 Iab
175 *sUCS* :math:`Iab` colourspace array.
177 Returns
178 -------
179 :class:`numpy.ndarray`
180 *CIE XYZ* tristimulus values, adapted to
181 *CIE Standard Illuminant D65* and in domain [0, 1] (where white
182 :math:`Y` is 1.0).
184 Notes
185 -----
186 +------------+-----------------------+------------------+
187 | **Domain** | **Scale - Reference** | **Scale - 1** |
188 +============+=======================+==================+
189 | ``Iab`` | 100 | 1 |
190 +------------+-----------------------+------------------+
192 +------------+-----------------------+-----------------+
193 | **Range** | **Scale - Reference** | **Scale - 1** |
194 +============+=======================+=================+
195 | ``XYZ`` | 1 | 1 |
196 +------------+-----------------------+-----------------+
198 References
199 ----------
200 :cite:`Li2024`
202 Examples
203 --------
204 >>> Iab = np.array([42.62923653, 36.97646831, 14.12301358])
205 >>> sUCS_to_XYZ(Iab) # doctest: +ELLIPSIS
206 array([ 0.2065400..., 0.1219722..., 0.0513695...])
207 """
209 Iab = to_domain_100(Iab)
211 with domain_range_scale("ignore"):
212 XYZ = Iab_to_XYZ(
213 Iab,
214 partial(spow, p=1 / 0.43),
215 MATRIX_SUCS_IAB_TO_LMS_P,
216 MATRIX_SUCS_LMS_TO_XYZ,
217 )
219 return from_range_1(XYZ)
222def sUCS_chroma(Iab: Domain100) -> Range100:
223 """
224 Compute the chroma component from the *sUCS* colourspace.
226 Parameters
227 ----------
228 Iab
229 *sUCS* :math:`Iab` colourspace array.
231 Returns
232 -------
233 :class:`numpy.ndarray`
234 Chroma component.
236 Notes
237 -----
238 +------------+-----------------------+------------------+
239 | **Domain** | **Scale - Reference** | **Scale - 1** |
240 +============+=======================+==================+
241 | ``Iab`` | 100 | 1 |
242 +------------+-----------------------+------------------+
244 +------------+-----------------------+-----------------+
245 | **Range** | **Scale - Reference** | **Scale - 1** |
246 +============+=======================+=================+
247 | ``C`` | 100 | 1 |
248 +------------+-----------------------+-----------------+
250 References
251 ----------
252 :cite:`Li2024`
254 Examples
255 --------
256 >>> Iab = np.array([42.62923653, 36.97646831, 14.12301358])
257 >>> sUCS_chroma(Iab) # doctest: +ELLIPSIS
258 40.4205110...
259 """
261 _I, a, b = tsplit(to_domain_100(Iab))
263 C = 1 / 0.0252 * np.log(1 + 0.0447 * np.hypot(a, b))
265 return as_float(from_range_100(C))
268def sUCS_hue_angle(Iab: Domain100) -> Range360:
269 """
270 Compute the hue angle in degrees from the *sUCS* colourspace.
272 Parameters
273 ----------
274 Iab
275 *sUCS* :math:`Iab` colourspace array.
277 Returns
278 -------
279 :class:`numpy.ndarray`
280 Hue angle in degrees.
282 Notes
283 -----
284 +------------+-----------------------+------------------+
285 | **Domain** | **Scale - Reference** | **Scale - 1** |
286 +============+=======================+==================+
287 | ``Iab`` | 100 | 1 |
288 +------------+-----------------------+------------------+
290 +------------+-----------------------+-----------------+
291 | **Range** | **Scale - Reference** | **Scale - 1** |
292 +============+=======================+=================+
293 | ``hue`` | 360 | 1 |
294 +------------+-----------------------+-----------------+
296 References
297 ----------
298 :cite:`Li2024`
300 Examples
301 --------
302 >>> Iab = np.array([42.62923653, 36.97646831, 14.12301358])
303 >>> sUCS_hue_angle(Iab) # doctest: +ELLIPSIS
304 20.9041560...
305 """
307 _I, a, b = tsplit(to_domain_100(Iab))
309 h = np.degrees(np.arctan2(b, a)) % 360
311 return as_float(from_range_degrees(h))
314def sUCS_Iab_to_sUCS_ICh(
315 Iab: Domain100,
316) -> Range100_100_360:
317 """
318 Convert from *sUCS* :math:`Iab` rectangular coordinates to *sUCS*
319 :math:`ICh` cylindrical coordinates.
321 Parameters
322 ----------
323 Iab
324 *sUCS* :math:`Iab` rectangular coordinates array.
326 Returns
327 -------
328 :class:`numpy.ndarray`
329 *sUCS* :math:`ICh` cylindrical coordinates array.
331 Notes
332 -----
333 +------------+-----------------------+------------------+
334 | **Domain** | **Scale - Reference** | **Scale - 1** |
335 +============+=======================+==================+
336 | ``Iab`` | 100 | 1 |
337 +------------+-----------------------+------------------+
339 +------------+-----------------------+------------------+
340 | **Range** | **Scale - Reference** | **Scale - 1** |
341 +============+=======================+==================+
342 | ``ICh`` | ``I`` : 100 | ``I`` : 1 |
343 | | | |
344 | | ``C`` : 100 | ``C`` : 1 |
345 | | | |
346 | | ``h`` : 360 | ``h`` : 1 |
347 +------------+-----------------------+------------------+
349 References
350 ----------
351 :cite:`Li2024`
353 Examples
354 --------
355 >>> Iab = np.array([42.62923653, 36.97646831, 14.12301358])
356 >>> sUCS_Iab_to_sUCS_ICh(Iab) # doctest: +ELLIPSIS
357 array([ 42.6292365..., 40.4205110..., 20.9041560...])
358 """
360 I, a, b = tsplit(to_domain_100(Iab)) # noqa: E741
362 C = 1 / 0.0252 * np.log(1 + 0.0447 * np.hypot(a, b))
364 h = np.degrees(np.arctan2(b, a)) % 360
366 return tstack([from_range_100(I), from_range_100(C), from_range_degrees(h)])
369def sUCS_ICh_to_sUCS_Iab(
370 ICh: Domain100_100_360,
371) -> Range100:
372 """
373 Convert from *sUCS* :math:`ICh` cylindrical coordinates to *sUCS*
374 :math:`Iab` rectangular coordinates.
376 Parameters
377 ----------
378 ICh
379 *sUCS* :math:`ICh` cylindrical coordinates array.
381 Returns
382 -------
383 :class:`numpy.ndarray`
384 *sUCS* :math:`Iab` rectangular coordinates array.
386 Notes
387 -----
388 +------------+-----------------------+------------------+
389 | **Domain** | **Scale - Reference** | **Scale - 1** |
390 +============+=======================+==================+
391 | ``ICh`` | ``I`` : 100 | ``I`` : 1 |
392 | | | |
393 | | ``C`` : 100 | ``C`` : 1 |
394 | | | |
395 | | ``h`` : 360 | ``h`` : 1 |
396 +------------+-----------------------+------------------+
398 +------------+-----------------------+------------------+
399 | **Range** | **Scale - Reference** | **Scale - 1** |
400 +============+=======================+==================+
401 | ``Iab`` | 100 | 1 |
402 +------------+-----------------------+------------------+
404 References
405 ----------
406 :cite:`Li2024`
408 Examples
409 --------
410 >>> ICh = np.array([42.62923653, 40.42051103, 20.90415604])
411 >>> sUCS_ICh_to_sUCS_Iab(ICh) # doctest: +ELLIPSIS
412 array([ 42.6292365..., 36.9764682..., 14.1230135...])
413 """
415 I, C, h = tsplit(ICh) # noqa: E741
416 I = to_domain_100(I) # noqa: E741
417 C = to_domain_100(C)
418 h = to_domain_degrees(h)
420 C = (np.exp(0.0252 * C) - 1) / 0.0447
422 a = C * np.cos(np.radians(h))
423 b = C * np.sin(np.radians(h))
425 return from_range_100(tstack([I, a, b]))