Coverage for colour/appearance/tests/test_zcam.py: 100%
146 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"""
2Define the unit tests for the :mod:`colour.appearance.zcam` module.
3"""
5from __future__ import annotations
7from itertools import permutations
9import numpy as np
10import pytest
12from colour.appearance import (
13 VIEWING_CONDITIONS_ZCAM,
14 CAM_Specification_ZCAM,
15 InductionFactors_ZCAM,
16 XYZ_to_ZCAM,
17 ZCAM_to_XYZ,
18)
19from colour.utilities import (
20 as_float_array,
21 domain_range_scale,
22 ignore_numpy_errors,
23 tsplit,
24)
26__author__ = "Colour Developers"
27__copyright__ = "Copyright 2013 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_ZCAM", "TestZCAM_to_XYZ"]
36class TestXYZ_to_ZCAM:
37 """
38 Defines :func:`colour.appearance.zcam.XYZ_to_ZCAM` definition unit tests
39 methods.
40 """
42 def test_XYZ_to_ZCAM(self) -> None:
43 """
44 Tests :func:`colour.appearance.zcam.XYZ_to_ZCAM` definition.
45 """
47 XYZ = np.array([185, 206, 163])
48 XYZ_w = np.array([256, 264, 202])
49 L_a = 264
50 Y_b = 100
51 surround = VIEWING_CONDITIONS_ZCAM["Average"]
52 np.testing.assert_allclose(
53 XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround),
54 np.array(
55 [
56 92.2520,
57 3.0216,
58 196.3524,
59 19.1314,
60 321.3464,
61 10.5252,
62 237.6401,
63 np.nan,
64 34.7022,
65 25.2994,
66 91.6837,
67 ]
68 ),
69 rtol=0.025,
70 atol=0.025,
71 )
73 XYZ = np.array([89, 96, 120])
74 np.testing.assert_allclose(
75 XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround),
76 np.array(
77 [
78 71.2071,
79 6.8539,
80 250.6422,
81 32.7963,
82 248.0394,
83 23.8744,
84 307.0595,
85 np.nan,
86 18.2796,
87 40.4621,
88 70.4026,
89 ]
90 ),
91 rtol=0.025,
92 atol=0.025,
93 )
95 # NOTE: Hue quadrature :math:`H_z` is significantly different for this
96 # test, i.e., 47.748252 vs 43.8258.
97 # NOTE: :math:`F_L` as reported in the supplemental document has the
98 # same value as for :math:`L_a` = 264 instead of 150. The values seem
99 # to be computed for :math:`L_a` = 264 and :math:`Y_b` = 100.
100 XYZ = np.array([79, 81, 62])
101 # L_a = 150
102 # Y_b = 60
103 surround = VIEWING_CONDITIONS_ZCAM["Dim"]
104 np.testing.assert_allclose(
105 XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround),
106 np.array(
107 [
108 68.8890,
109 0.9774,
110 58.7532,
111 12.5916,
112 196.7686,
113 2.7918,
114 43.8258,
115 np.nan,
116 11.0371,
117 44.4143,
118 68.8737,
119 ]
120 ),
121 rtol=0.025,
122 atol=4,
123 )
125 XYZ = np.array([910, 1114, 500])
126 XYZ_w = np.array([2103, 2259, 1401])
127 L_a = 359
128 Y_b = 16
129 surround = VIEWING_CONDITIONS_ZCAM["Dark"]
130 np.testing.assert_allclose(
131 XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround),
132 np.array(
133 [
134 82.6445,
135 13.0838,
136 123.9464,
137 44.7277,
138 114.7431,
139 18.1655,
140 178.6422,
141 np.nan,
142 34.4874,
143 26.8778,
144 78.2653,
145 ]
146 ),
147 rtol=0.025,
148 atol=0.025,
149 )
151 XYZ = np.array([96, 67, 28])
152 np.testing.assert_allclose(
153 XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround),
154 np.array(
155 [
156 33.0139,
157 19.4070,
158 389.7720 % 360,
159 86.1882,
160 45.8363,
161 26.9446,
162 397.3301,
163 np.nan,
164 43.6447,
165 47.9942,
166 30.2593,
167 ]
168 ),
169 rtol=0.025,
170 atol=0.025,
171 )
173 def test_n_dimensional_XYZ_to_ZCAM(self) -> None:
174 """
175 Tests :func:`colour.appearance.zcam.XYZ_to_ZCAM` definition
176 n-dimensional support.
177 """
179 XYZ = np.array([185, 206, 163])
180 XYZ_w = np.array([256, 264, 202])
181 L_a = 264
182 Y_b = 100
183 surround = VIEWING_CONDITIONS_ZCAM["Average"]
184 specification = XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround)
186 XYZ = np.tile(XYZ, (6, 1))
187 specification = np.tile(specification, (6, 1))
188 np.testing.assert_almost_equal(
189 XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround), specification, decimal=7
190 )
192 XYZ_w = np.tile(XYZ_w, (6, 1))
193 np.testing.assert_almost_equal(
194 XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround), specification, decimal=7
195 )
197 XYZ = np.reshape(XYZ, (2, 3, 3))
198 XYZ_w = np.reshape(XYZ_w, (2, 3, 3))
199 specification = np.reshape(specification, (2, 3, 11))
200 np.testing.assert_almost_equal(
201 XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround), specification, decimal=7
202 )
204 @ignore_numpy_errors
205 def test_domain_range_scale_XYZ_to_ZCAM(self) -> None:
206 """
207 Tests :func:`colour.appearance.zcam.XYZ_to_ZCAM` definition
208 domain and range scale support.
209 """
211 XYZ = np.array([185, 206, 163])
212 XYZ_w = np.array([256, 264, 202])
213 L_a = 264
214 Y_b = 100
215 surround = VIEWING_CONDITIONS_ZCAM["Average"]
216 specification = XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround)
218 d_r = (
219 ("reference", 1, 1),
220 ("1", 1, np.array([1, 1, 1 / 360, 1, 1, 1, 1 / 400, np.nan, 1, 1, 1])),
221 (
222 "100",
223 100,
224 np.array(
225 [
226 100,
227 100,
228 100 / 360,
229 100,
230 100,
231 100,
232 100 / 400,
233 np.nan,
234 100,
235 100,
236 100,
237 ]
238 ),
239 ),
240 )
241 for scale, factor_a, factor_b in d_r:
242 with domain_range_scale(scale):
243 np.testing.assert_almost_equal(
244 XYZ_to_ZCAM(XYZ * factor_a, XYZ_w * factor_a, L_a, Y_b, surround),
245 as_float_array(specification) * factor_b,
246 decimal=7,
247 )
249 @ignore_numpy_errors
250 def test_nan_XYZ_to_ZCAM(self) -> None:
251 """
252 Tests :func:`colour.appearance.zcam.XYZ_to_ZCAM` definition
253 nan support.
254 """
256 cases = [-1.0, 0.0, 1.0, -np.inf, np.inf, np.nan]
257 cases = set(permutations(cases * 3, r=3))
258 for case in cases:
259 XYZ = np.array(case)
260 XYZ_w = np.array(case)
261 L_a = case[0]
262 Y_b = 100
263 surround = InductionFactors_ZCAM(case[0], case[0], case[0], case[0])
264 XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround)
267class TestZCAM_to_XYZ:
268 """
269 Defines :func:`colour.appearance.zcam.ZCAM_to_XYZ` definition unit
270 tests methods.
271 """
273 def test_ZCAM_to_XYZ(self) -> None:
274 """
275 Tests :func:`colour.appearance.zcam.ZCAM_to_XYZ` definition.
276 """
278 specification = CAM_Specification_ZCAM(
279 92.2520,
280 3.0216,
281 196.3524,
282 19.1314,
283 321.3464,
284 10.5252,
285 237.6401,
286 np.nan,
287 34.7022,
288 25.2994,
289 91.6837,
290 )
291 XYZ_w = np.array([256, 264, 202])
292 L_a = 264
293 Y_b = 100
294 surround = VIEWING_CONDITIONS_ZCAM["Average"]
295 np.testing.assert_allclose(
296 ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround),
297 np.array([185, 206, 163]),
298 atol=0.01,
299 rtol=0.01,
300 )
302 specification = CAM_Specification_ZCAM(
303 71.2071,
304 6.8539,
305 250.6422,
306 32.7963,
307 248.0394,
308 23.8744,
309 307.0595,
310 np.nan,
311 18.2796,
312 40.4621,
313 70.4026,
314 )
315 np.testing.assert_allclose(
316 ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround),
317 np.array([89, 96, 120]),
318 atol=0.01,
319 rtol=0.01,
320 )
322 specification = CAM_Specification_ZCAM(
323 68.8890,
324 0.9774,
325 58.7532,
326 12.5916,
327 196.7686,
328 2.7918,
329 43.8258,
330 np.nan,
331 11.0371,
332 44.4143,
333 68.8737,
334 )
335 surround = VIEWING_CONDITIONS_ZCAM["Dim"]
336 np.testing.assert_allclose(
337 ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround),
338 np.array([79, 81, 62]),
339 atol=0.01,
340 rtol=0.01,
341 )
343 specification = CAM_Specification_ZCAM(
344 82.6445,
345 13.0838,
346 123.9464,
347 44.7277,
348 114.7431,
349 18.1655,
350 178.6422,
351 np.nan,
352 34.4874,
353 26.8778,
354 78.2653,
355 )
356 XYZ_w = np.array([2103, 2259, 1401])
357 L_a = 359
358 Y_b = 16
359 surround = VIEWING_CONDITIONS_ZCAM["Dark"]
360 np.testing.assert_allclose(
361 ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround),
362 np.array([910, 1114, 500]),
363 atol=0.01,
364 rtol=0.01,
365 )
367 specification = CAM_Specification_ZCAM(
368 33.0139,
369 19.4070,
370 389.7720 % 360,
371 86.1882,
372 45.8363,
373 26.9446,
374 397.3301,
375 np.nan,
376 43.6447,
377 47.9942,
378 30.2593,
379 )
380 np.testing.assert_allclose(
381 ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround),
382 np.array([96, 67, 28]),
383 atol=0.01,
384 rtol=0.01,
385 )
387 # Test using C instead of M
388 specification = CAM_Specification_ZCAM(
389 J=82.61980483202505, C=13.194790413382647, h=123.77987744640157
390 )
391 XYZ_w = np.array([2103, 2259, 1401])
392 L_a = 359
393 Y_b = 16
394 surround = VIEWING_CONDITIONS_ZCAM["Dark"]
395 np.testing.assert_allclose(
396 ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround),
397 np.array([910, 1114, 500]),
398 atol=0.01,
399 rtol=0.01,
400 )
402 def test_n_dimensional_ZCAM_to_XYZ(self) -> None:
403 """
404 Tests :func:`colour.appearance.zcam.ZCAM_to_XYZ` definition
405 n-dimensional support.
406 """
408 XYZ = np.array([185, 206, 163])
409 XYZ_w = np.array([256, 264, 202])
410 L_a = 264
411 Y_b = 100
412 surround = VIEWING_CONDITIONS_ZCAM["Average"]
413 specification = XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround)
414 XYZ = ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround)
416 specification = CAM_Specification_ZCAM(
417 *np.transpose(np.tile(tsplit(specification), (6, 1))).tolist()
418 )
419 XYZ = np.tile(XYZ, (6, 1))
420 np.testing.assert_almost_equal(
421 ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround), XYZ, decimal=7
422 )
424 XYZ_w = np.tile(XYZ_w, (6, 1))
425 np.testing.assert_almost_equal(
426 ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround), XYZ, decimal=7
427 )
429 specification = CAM_Specification_ZCAM(
430 *tsplit(np.reshape(specification, (2, 3, 11))).tolist()
431 )
432 XYZ_w = np.reshape(XYZ_w, (2, 3, 3))
433 XYZ = np.reshape(XYZ, (2, 3, 3))
434 np.testing.assert_almost_equal(
435 ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround), XYZ, decimal=7
436 )
438 @ignore_numpy_errors
439 def test_domain_range_scale_ZCAM_to_XYZ(self) -> None:
440 """
441 Tests :func:`colour.appearance.zcam.ZCAM_to_XYZ` definition
442 domain and range scale support.
443 """
445 XYZ_i = np.array([185, 206, 163])
446 XYZ_w = np.array([256, 264, 202])
447 L_a = 264
448 Y_b = 100
449 surround = VIEWING_CONDITIONS_ZCAM["Average"]
450 specification = XYZ_to_ZCAM(XYZ_i, XYZ_w, L_a, Y_b, surround)
451 XYZ = ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround)
453 d_r = (
454 ("reference", 1, 1),
455 ("1", np.array([1, 1, 1 / 360, 1, 1, 1, 1 / 400, np.nan, 1, 1, 1]), 1),
456 (
457 "100",
458 np.array(
459 [
460 100,
461 100,
462 100 / 360,
463 100,
464 100,
465 100,
466 100 / 400,
467 np.nan,
468 100,
469 100,
470 100,
471 ]
472 ),
473 100,
474 ),
475 )
476 for scale, factor_a, factor_b in d_r:
477 with domain_range_scale(scale):
478 np.testing.assert_almost_equal(
479 ZCAM_to_XYZ(
480 specification * factor_a, XYZ_w * factor_b, L_a, Y_b, surround
481 ),
482 XYZ * factor_b,
483 decimal=7,
484 )
486 @ignore_numpy_errors
487 def test_raise_exception_ZCAM_to_XYZ(self) -> None:
488 """
489 Tests :func:`colour.appearance.zcam.ZCAM_to_XYZ` definition
490 raised exception.
491 """
493 pytest.raises(
494 ValueError,
495 ZCAM_to_XYZ,
496 CAM_Specification_ZCAM(
497 41.731091132513917,
498 None,
499 219.04843265831178,
500 ),
501 np.array([256, 264, 202]),
502 318.31,
503 20.0,
504 VIEWING_CONDITIONS_ZCAM["Average"],
505 )
507 @ignore_numpy_errors
508 def test_nan_ZCAM_to_XYZ(self) -> None:
509 """
510 Tests :func:`colour.appearance.zcam.ZCAM_to_XYZ` definition nan
511 support.
512 """
514 cases = [-1.0, 0.0, 1.0, -np.inf, np.inf, np.nan]
515 cases = set(permutations(cases * 3, r=3))
516 for case in cases:
517 J = case[0]
518 C = case[0]
519 h = case[0]
520 XYZ_w = np.array(case)
521 L_a = case[0]
522 Y_b = 100
523 surround = InductionFactors_ZCAM(case[0], case[0], case[0], case[0])
524 ZCAM_to_XYZ(
525 CAM_Specification_ZCAM(J, C, h, M=50), XYZ_w, L_a, Y_b, surround
526 )