Coverage for appearance/tests/test_scam.py: 100%
126 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"""
2Define the unit tests for the :mod:`colour.appearance.scam` module.
3"""
5from __future__ import annotations
7from itertools import product
9import numpy as np
10import pytest
12from colour.appearance import (
13 CAM_Specification_sCAM,
14 VIEWING_CONDITIONS_sCAM,
15 XYZ_to_sCAM,
16 sCAM_to_XYZ,
17)
18from colour.constants import TOLERANCE_ABSOLUTE_TESTS
19from colour.utilities import (
20 as_float_array,
21 domain_range_scale,
22 ignore_numpy_errors,
23 tsplit,
24)
26__author__ = "Colour Developers, UltraMo114(Molin Li)"
27__copyright__ = "Copyright 2024 Colour Developers"
28__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
29__maintainer__ = "Colour Developers"
30__email__ = "colour-developers@colour-science.org"
31__status__ = "Production"
33__all__ = ["TestXYZ_to_sCAM", "TestsCAM_to_XYZ"]
36class TestXYZ_to_sCAM:
37 """
38 Define :func:`colour.appearance.scam.XYZ_to_sCAM` definition unit
39 tests methods.
40 """
42 def test_XYZ_to_sCAM(self) -> None:
43 """
44 Test :func:`colour.appearance.scam.XYZ_to_sCAM` definition.
45 """
47 XYZ = np.array([19.01, 20.00, 21.78])
48 XYZ_w = np.array([95.05, 100.00, 108.88])
49 L_A = 318.31
50 Y_b = 20
51 surround = VIEWING_CONDITIONS_sCAM["Average"]
52 np.testing.assert_allclose(
53 XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround),
54 np.array(
55 [
56 49.97956680,
57 0.01405311,
58 328.27249244,
59 195.23024234,
60 0.00502448,
61 363.60134377,
62 np.nan,
63 49.97957273,
64 50.02042727,
65 34.97343274,
66 65.02656726,
67 ]
68 ),
69 atol=TOLERANCE_ABSOLUTE_TESTS,
70 )
72 XYZ = np.array([57.06, 43.06, 31.96])
73 L_A = 31.83
74 np.testing.assert_allclose(
75 XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround),
76 np.array(
77 [
78 71.63079886,
79 37.33838127,
80 18.75135858,
81 259.13174065,
82 10.66713872,
83 4.20415978,
84 np.nan,
85 96.50614225,
86 3.49385775,
87 28.37649889,
88 71.62350111,
89 ]
90 ),
91 atol=TOLERANCE_ABSOLUTE_TESTS,
92 )
94 XYZ = np.array([3.53, 6.56, 2.14])
95 XYZ_w = np.array([109.85, 100, 35.58])
96 L_A = 318.31
97 np.testing.assert_allclose(
98 XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround),
99 np.array(
100 [
101 29.61821869,
102 25.97461207,
103 178.56952253,
104 115.69472052,
105 10.76901611,
106 227.46922207,
107 np.nan,
108 53.86353400,
109 46.13646600,
110 -0.97480767,
111 100.97480767,
112 ]
113 ),
114 atol=TOLERANCE_ABSOLUTE_TESTS,
115 )
117 def test_n_dimensional_XYZ_to_sCAM(self) -> None:
118 """
119 Test :func:`colour.appearance.scam.XYZ_to_sCAM` definition
120 n-dimensional support.
121 """
123 XYZ = np.array([19.01, 20.00, 21.78])
124 XYZ_w = np.array([95.05, 100.00, 108.88])
125 L_A = 318.31
126 Y_b = 20
127 surround = VIEWING_CONDITIONS_sCAM["Average"]
128 specification = XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround)
130 XYZ = np.tile(XYZ, (6, 1))
131 specification = np.tile(specification, (6, 1))
132 np.testing.assert_allclose(
133 XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround),
134 specification,
135 atol=TOLERANCE_ABSOLUTE_TESTS,
136 )
138 XYZ_w = np.tile(XYZ_w, (6, 1))
139 np.testing.assert_allclose(
140 XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround),
141 specification,
142 atol=TOLERANCE_ABSOLUTE_TESTS,
143 )
145 XYZ = np.reshape(XYZ, (2, 3, 3))
146 XYZ_w = np.reshape(XYZ_w, (2, 3, 3))
147 specification = np.reshape(specification, (2, 3, 11))
148 np.testing.assert_allclose(
149 XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround),
150 specification,
151 atol=TOLERANCE_ABSOLUTE_TESTS,
152 )
154 @ignore_numpy_errors
155 def test_domain_range_scale_XYZ_to_sCAM(self) -> None:
156 """
157 Test :func:`colour.appearance.scam.XYZ_to_sCAM` definition
158 domain and range scale support.
159 """
161 XYZ = np.array([19.01, 20.00, 21.78])
162 XYZ_w = np.array([95.05, 100.00, 108.88])
163 L_A = 318.31
164 Y_b = 20
165 surround = VIEWING_CONDITIONS_sCAM["Average"]
167 specification = XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround)
169 d_r = (
170 ("reference", 1, 1),
171 (
172 "1",
173 0.01,
174 np.array(
175 [
176 1 / 100,
177 1 / 100,
178 1 / 360,
179 1 / 100,
180 1 / 100,
181 1 / 400,
182 1,
183 1 / 100,
184 1 / 100,
185 1 / 100,
186 1 / 100,
187 ]
188 ),
189 ),
190 (
191 "100",
192 1,
193 np.array([1, 1, 100 / 360, 1, 1, 100 / 400, 1, 1, 1, 1, 1]),
194 ),
195 )
197 for scale, factor_a, factor_b in d_r:
198 with domain_range_scale(scale):
199 np.testing.assert_allclose(
200 XYZ_to_sCAM(XYZ * factor_a, XYZ_w * factor_a, L_A, Y_b, surround),
201 as_float_array(specification) * factor_b,
202 atol=TOLERANCE_ABSOLUTE_TESTS,
203 )
205 @ignore_numpy_errors
206 def test_nan_XYZ_to_sCAM(self) -> None:
207 """
208 Test :func:`colour.appearance.scam.XYZ_to_sCAM` definition
209 nan support.
210 """
212 cases = [-1.0, 0.0, 1.0, -np.inf, np.inf, np.nan]
213 cases = np.array(list(set(product(cases, repeat=3))))
214 surround = VIEWING_CONDITIONS_sCAM["Average"]
215 XYZ_to_sCAM(cases, cases, cases[..., 0], cases[..., 0], surround)
218class TestsCAM_to_XYZ:
219 """
220 Define :func:`colour.appearance.scam.sCAM_to_XYZ` definition unit
221 tests methods.
222 """
224 def test_sCAM_to_XYZ(self) -> None:
225 """
226 Test :func:`colour.appearance.scam.sCAM_to_XYZ` definition.
227 """
229 specification = CAM_Specification_sCAM(49.97956680, 0.01405311, 328.27249244)
230 XYZ_w = np.array([95.05, 100.00, 108.88])
231 L_A = 318.31
232 Y_b = 20
233 surround = VIEWING_CONDITIONS_sCAM["Average"]
234 np.testing.assert_allclose(
235 sCAM_to_XYZ(specification, XYZ_w, L_A, Y_b, surround),
236 np.array([19.01, 20.00, 21.78]),
237 atol=TOLERANCE_ABSOLUTE_TESTS,
238 )
240 specification = CAM_Specification_sCAM(71.63079886, 37.33838127, 18.75135858)
241 L_A = 31.83
242 np.testing.assert_allclose(
243 sCAM_to_XYZ(specification, XYZ_w, L_A, Y_b, surround),
244 np.array([57.06, 43.06, 31.96]),
245 atol=TOLERANCE_ABSOLUTE_TESTS,
246 )
248 specification = CAM_Specification_sCAM(29.61821869, 25.97461207, 178.56952253)
249 XYZ_w = np.array([109.85, 100, 35.58])
250 L_A = 318.31
251 np.testing.assert_allclose(
252 sCAM_to_XYZ(specification, XYZ_w, L_A, Y_b, surround),
253 np.array([3.53256359, 6.56009775, 2.15585716]),
254 atol=TOLERANCE_ABSOLUTE_TESTS,
255 )
257 def test_n_dimensional_sCAM_to_XYZ(self) -> None:
258 """
259 Test :func:`colour.appearance.scam.sCAM_to_XYZ` definition
260 n-dimensional support.
261 """
263 XYZ = np.array([19.01, 20.00, 21.78])
264 XYZ_w = np.array([95.05, 100.00, 108.88])
265 L_A = 318.31
266 Y_b = 20
267 surround = VIEWING_CONDITIONS_sCAM["Average"]
268 specification = XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround)
269 XYZ = sCAM_to_XYZ(specification, XYZ_w, L_A, Y_b, surround)
271 specification = CAM_Specification_sCAM(
272 *np.transpose(np.tile(tsplit(specification), (6, 1))).tolist()
273 )
274 XYZ = np.tile(XYZ, (6, 1))
275 np.testing.assert_allclose(
276 sCAM_to_XYZ(specification, XYZ_w, L_A, Y_b, surround),
277 XYZ,
278 atol=TOLERANCE_ABSOLUTE_TESTS,
279 )
281 XYZ_w = np.tile(XYZ_w, (6, 1))
282 np.testing.assert_allclose(
283 sCAM_to_XYZ(specification, XYZ_w, L_A, Y_b, surround),
284 XYZ,
285 atol=TOLERANCE_ABSOLUTE_TESTS,
286 )
288 specification = CAM_Specification_sCAM(
289 *tsplit(np.reshape(specification, (2, 3, 11))).tolist()
290 )
291 XYZ_w = np.reshape(XYZ_w, (2, 3, 3))
292 XYZ = np.reshape(XYZ, (2, 3, 3))
293 np.testing.assert_allclose(
294 sCAM_to_XYZ(specification, XYZ_w, L_A, Y_b, surround),
295 XYZ,
296 atol=TOLERANCE_ABSOLUTE_TESTS,
297 )
299 @ignore_numpy_errors
300 def test_domain_range_scale_sCAM_to_XYZ(self) -> None:
301 """
302 Test :func:`colour.appearance.scam.sCAM_to_XYZ` definition
303 domain and range scale support.
304 """
306 XYZ = np.array([19.01, 20.00, 21.78])
307 XYZ_w = np.array([95.05, 100.00, 108.88])
308 L_A = 318.31
309 Y_b = 20
310 surround = VIEWING_CONDITIONS_sCAM["Average"]
311 specification = XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround)
312 XYZ = sCAM_to_XYZ(specification, XYZ_w, L_A, Y_b, surround)
314 d_r = (
315 ("reference", 1, 1),
316 (
317 "1",
318 np.array(
319 [
320 1 / 100,
321 1 / 100,
322 1 / 360,
323 1 / 100,
324 1 / 100,
325 1 / 400,
326 1,
327 1 / 100,
328 1 / 100,
329 1 / 100,
330 1 / 100,
331 ]
332 ),
333 0.01,
334 ),
335 (
336 "100",
337 np.array([1, 1, 100 / 360, 1, 1, 100 / 400, 1, 1, 1, 1, 1]),
338 1,
339 ),
340 )
341 for scale, factor_a, factor_b in d_r:
342 with domain_range_scale(scale):
343 np.testing.assert_allclose(
344 sCAM_to_XYZ(
345 specification * factor_a,
346 XYZ_w * factor_b,
347 L_A,
348 Y_b,
349 surround,
350 ),
351 XYZ * factor_b,
352 atol=TOLERANCE_ABSOLUTE_TESTS,
353 )
355 @ignore_numpy_errors
356 def test_raise_exception_sCAM_to_XYZ(self) -> None:
357 """
358 Test :func:`colour.appearance.scam.sCAM_to_XYZ` definition
359 raised exception.
360 """
361 XYZ_w = np.array([95.05, 100.00, 108.88])
362 L_A = 318.31
363 Y_b = 20
364 surround = VIEWING_CONDITIONS_sCAM["Average"]
366 with pytest.raises(ValueError):
367 sCAM_to_XYZ(
368 CAM_Specification_sCAM(J=None, C=20.0, h=210.0),
369 XYZ_w,
370 L_A,
371 Y_b,
372 surround,
373 )
375 with pytest.raises(ValueError):
376 sCAM_to_XYZ(
377 CAM_Specification_sCAM(J=40.0, C=20.0, h=None),
378 XYZ_w,
379 L_A,
380 Y_b,
381 surround,
382 )
384 with pytest.raises(ValueError):
385 sCAM_to_XYZ(
386 CAM_Specification_sCAM(J=40.0, C=None, h=210.0, M=None),
387 XYZ_w,
388 L_A,
389 Y_b,
390 surround,
391 )
393 @ignore_numpy_errors
394 def test_nan_sCAM_to_XYZ(self) -> None:
395 """
396 Test :func:`colour.appearance.scam.sCAM_to_XYZ` definition nan
397 support.
398 """
400 cases = [-1.0, 0.0, 1.0, -np.inf, np.inf, np.nan]
401 cases = np.array(list(set(product(cases, repeat=3))))
402 surround = VIEWING_CONDITIONS_sCAM["Average"]
403 sCAM_to_XYZ(
404 CAM_Specification_sCAM(cases[..., 0], cases[..., 0], cases[..., 0], M=50),
405 cases,
406 cases[..., 0],
407 cases[..., 0],
408 surround,
409 )