Coverage for io/tests/test_image.py: 100%
164 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"""Define the unit tests for the :mod:`colour.io.image` module."""
3from __future__ import annotations
5import os
6import platform
7import shutil
8import tempfile
10import numpy as np
11import pytest
13from colour.constants import TOLERANCE_ABSOLUTE_TESTS
14from colour.io import (
15 Image_Specification_Attribute,
16 as_3_channels_image,
17 convert_bit_depth,
18 image_specification_OpenImageIO,
19 read_image,
20 read_image_Imageio,
21 read_image_OpenImageIO,
22 write_image,
23 write_image_Imageio,
24 write_image_OpenImageIO,
25)
26from colour.utilities import attest, full
28__author__ = "Colour Developers"
29__copyright__ = "Copyright 2013 Colour Developers"
30__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
31__maintainer__ = "Colour Developers"
32__email__ = "colour-developers@colour-science.org"
33__status__ = "Production"
35__all__ = [
36 "ROOT_RESOURCES",
37 "TestImageSpecificationOpenImageIO",
38 "TestConvertBitDepth",
39 "TestReadImageOpenImageIO",
40 "TestWriteImageOpenImageIO",
41 "TestReadImageImageio",
42 "TestWriteImageImageio",
43 "TestReadImage",
44 "TestWriteImage",
45 "TestAs3ChannelsImage",
46]
48ROOT_RESOURCES: str = os.path.join(os.path.dirname(__file__), "resources")
51@pytest.mark.skipif(
52 platform.system() == "Windows",
53 reason="OpenImageIO crashes on Windows due to thread-safety issues",
54)
55class TestImageSpecificationOpenImageIO:
56 """
57 Define :func:`colour.io.image.image_specification_OpenImageIO` definition
58 unit tests methods.
59 """
61 def test_image_specification_OpenImageIO(self) -> None: # pragma: no cover
62 """
63 Test :func:`colour.io.image.image_specification_OpenImageIO`
64 definition.
65 """
67 from OpenImageIO import HALF # noqa: PLC0415
69 compression = Image_Specification_Attribute("Compression", "none")
70 specification = image_specification_OpenImageIO(
71 1920, 1080, 3, "float16", [compression]
72 )
74 assert specification.width == 1920 # pyright: ignore
75 assert specification.height == 1080 # pyright: ignore
76 assert specification.nchannels == 3 # pyright: ignore
77 assert specification.format == HALF # pyright: ignore
78 assert specification.extra_attribs[0].name == "Compression" # pyright: ignore
81class TestConvertBitDepth:
82 """
83 Define :func:`colour.io.image.convert_bit_depth` definition unit tests
84 methods.
85 """
87 def test_convert_bit_depth(self) -> None:
88 """Test :func:`colour.io.image.convert_bit_depth` definition."""
90 a = np.around(np.linspace(0, 1, 10) * 255).astype("uint8")
91 assert convert_bit_depth(a, "uint8").dtype is np.dtype("uint8")
92 np.testing.assert_equal(convert_bit_depth(a, "uint8"), a)
94 assert convert_bit_depth(a, "uint16").dtype is np.dtype("uint16")
95 np.testing.assert_equal(
96 convert_bit_depth(a, "uint16"),
97 np.array(
98 [
99 0,
100 7196,
101 14649,
102 21845,
103 29041,
104 36494,
105 43690,
106 50886,
107 58339,
108 65535,
109 ]
110 ),
111 )
113 assert convert_bit_depth(a, "float16").dtype is np.dtype("float16")
114 np.testing.assert_allclose(
115 convert_bit_depth(a, "float16"),
116 np.array(
117 [
118 0.0000,
119 0.1098,
120 0.2235,
121 0.3333,
122 0.443,
123 0.5566,
124 0.6665,
125 0.7764,
126 0.8900,
127 1.0000,
128 ]
129 ),
130 atol=5e-4,
131 )
133 assert convert_bit_depth(a, "float32").dtype is np.dtype("float32")
134 np.testing.assert_allclose(
135 convert_bit_depth(a, "float32"),
136 np.array(
137 [
138 0.00000000,
139 0.10980392,
140 0.22352941,
141 0.33333334,
142 0.44313726,
143 0.55686277,
144 0.66666669,
145 0.77647060,
146 0.89019608,
147 1.00000000,
148 ]
149 ),
150 atol=TOLERANCE_ABSOLUTE_TESTS,
151 )
153 assert convert_bit_depth(a, "float64").dtype is np.dtype("float64")
155 if hasattr(np, "float128"): # pragma: no cover
156 assert convert_bit_depth(a, "float128").dtype is np.dtype("float128")
158 a = np.around(np.linspace(0, 1, 10) * 65535).astype("uint16")
159 assert convert_bit_depth(a, "uint8").dtype is np.dtype("uint8")
160 np.testing.assert_equal(
161 convert_bit_depth(a, "uint8"),
162 np.array([0, 28, 56, 85, 113, 141, 170, 198, 226, 255]),
163 )
165 assert convert_bit_depth(a, "uint16").dtype is np.dtype("uint16")
166 np.testing.assert_equal(convert_bit_depth(a, "uint16"), a)
168 assert convert_bit_depth(a, "float16").dtype is np.dtype("float16")
169 np.testing.assert_allclose(
170 convert_bit_depth(a, "float16"),
171 np.array(
172 [
173 0.0000,
174 0.1098,
175 0.2235,
176 0.3333,
177 0.443,
178 0.5566,
179 0.6665,
180 0.7764,
181 0.8900,
182 1.0000,
183 ]
184 ),
185 atol=5e-2,
186 )
188 assert convert_bit_depth(a, "float32").dtype is np.dtype("float32")
189 np.testing.assert_allclose(
190 convert_bit_depth(a, "float32"),
191 np.array(
192 [
193 0.00000000,
194 0.11111620,
195 0.22221714,
196 0.33333334,
197 0.44444954,
198 0.55555046,
199 0.66666669,
200 0.77778286,
201 0.88888383,
202 1.00000000,
203 ]
204 ),
205 atol=TOLERANCE_ABSOLUTE_TESTS,
206 )
208 assert convert_bit_depth(a, "float64").dtype is np.dtype("float64")
210 if hasattr(np, "float128"): # pragma: no cover
211 assert convert_bit_depth(a, "float128").dtype is np.dtype("float128")
213 a = np.linspace(0, 1, 10, dtype=np.float64)
214 assert convert_bit_depth(a, "uint8").dtype is np.dtype("uint8")
215 np.testing.assert_equal(
216 convert_bit_depth(a, "uint8"),
217 np.array([0, 28, 57, 85, 113, 142, 170, 198, 227, 255]),
218 )
220 assert convert_bit_depth(a, "uint16").dtype is np.dtype("uint16")
221 np.testing.assert_equal(
222 convert_bit_depth(a, "uint16"),
223 np.array(
224 [
225 0,
226 7282,
227 14563,
228 21845,
229 29127,
230 36408,
231 43690,
232 50972,
233 58253,
234 65535,
235 ]
236 ),
237 )
239 assert convert_bit_depth(a, "float16").dtype is np.dtype("float16")
240 np.testing.assert_allclose(
241 convert_bit_depth(a, "float16"),
242 np.array(
243 [
244 0.0000,
245 0.1111,
246 0.2222,
247 0.3333,
248 0.4443,
249 0.5557,
250 0.6665,
251 0.7780,
252 0.8887,
253 1.0000,
254 ]
255 ),
256 atol=5e-4,
257 )
259 assert convert_bit_depth(a, "float32").dtype is np.dtype("float32")
260 np.testing.assert_allclose(
261 convert_bit_depth(a, "float32"), a, atol=TOLERANCE_ABSOLUTE_TESTS
262 )
264 assert convert_bit_depth(a, "float64").dtype is np.dtype("float64")
266 if hasattr(np, "float128"): # pragma: no cover
267 assert convert_bit_depth(a, "float128").dtype is np.dtype("float128")
270@pytest.mark.skipif(
271 platform.system() == "Windows",
272 reason="OpenImageIO crashes on Windows due to thread-safety issues",
273)
274class TestReadImageOpenImageIO:
275 """
276 Define :func:`colour.io.image.read_image_OpenImageIO` definition unit
277 tests methods.
278 """
280 def test_read_image_OpenImageIO(self) -> None: # pragma: no cover
281 """Test :func:`colour.io.image.read_image_OpenImageIO` definition."""
283 image = read_image_OpenImageIO(
284 os.path.join(ROOT_RESOURCES, "CMS_Test_Pattern.exr"),
285 additional_data=False,
286 )
287 assert image.shape == (1267, 1274, 3)
288 assert image.dtype is np.dtype("float32")
290 image = read_image_OpenImageIO(
291 os.path.join(ROOT_RESOURCES, "CMS_Test_Pattern.exr"),
292 "float16",
293 additional_data=False,
294 )
295 assert image.dtype is np.dtype("float16")
297 image, attributes = read_image_OpenImageIO(
298 os.path.join(ROOT_RESOURCES, "CMS_Test_Pattern.exr"),
299 additional_data=True,
300 )
301 assert image.shape == (1267, 1274, 3)
302 assert len(attributes) > 0
303 compression_attribute = next(
304 (attribute for attribute in attributes if attribute.name == "compression"),
305 None,
306 )
307 assert compression_attribute is not None
309 image = read_image_OpenImageIO(
310 os.path.join(ROOT_RESOURCES, "Single_Channel.exr"),
311 additional_data=False,
312 )
313 assert image.shape == (256, 256)
315 image = read_image_OpenImageIO(
316 os.path.join(ROOT_RESOURCES, "Colour_Logo.png"),
317 "uint8",
318 additional_data=False,
319 )
320 assert image.shape == (128, 256, 4)
321 assert image.dtype is np.dtype("uint8")
322 assert np.min(image) == 0
323 assert np.max(image) == 255
325 image = read_image_OpenImageIO(
326 os.path.join(ROOT_RESOURCES, "Colour_Logo.png"),
327 "uint16",
328 additional_data=False,
329 )
330 assert image.shape == (128, 256, 4)
331 assert image.dtype is np.dtype("uint16")
332 assert np.min(image) == 0
333 assert np.max(image) == 65535
335 # TODO: Investigate "OIIO" behaviour here: 1.0 != 15360.0
336 # image = read_image_OpenImageIO(
337 # os.path.join(ROOT_RESOURCES, 'Colour_Logo.png'), 'float16')
338 # self.assertIs(image.dtype, np.dtype('float16'))
339 # self.assertEqual(np.min(image), 0.0)
340 # self.assertEqual(np.max(image), 1.0)
342 image = read_image_OpenImageIO(
343 os.path.join(ROOT_RESOURCES, "Colour_Logo.png"),
344 "float32",
345 additional_data=False,
346 )
347 assert image.dtype is np.dtype("float32")
348 assert np.min(image) == 0.0
349 assert np.max(image) == 1.0
352@pytest.mark.skipif(
353 platform.system() == "Windows",
354 reason="OpenImageIO crashes on Windows due to thread-safety issues",
355)
356class TestWriteImageOpenImageIO:
357 """
358 Define :func:`colour.io.image.write_image_OpenImageIO` definition unit
359 tests methods.
360 """
362 def setup_method(self) -> None:
363 """Initialise the common tests attributes."""
365 self._temporary_directory = tempfile.mkdtemp()
367 def teardown_method(self) -> None:
368 """After tests actions."""
370 shutil.rmtree(self._temporary_directory)
372 def test_write_image_OpenImageIO(self) -> None: # pragma: no cover
373 """Test :func:`colour.io.image.write_image_OpenImageIO` definition."""
375 from OpenImageIO import TypeDesc # noqa: PLC0415
377 path = os.path.join(self._temporary_directory, "8-bit.png")
378 RGB = full((1, 1, 3), 255, np.uint8)
379 write_image_OpenImageIO(RGB, path, bit_depth="uint8")
380 image = read_image_OpenImageIO(path, bit_depth="uint8")
381 np.testing.assert_equal(np.squeeze(RGB), image)
383 path = os.path.join(self._temporary_directory, "16-bit.png")
384 RGB = full((1, 1, 3), 65535, np.uint16)
385 write_image_OpenImageIO(RGB, path, bit_depth="uint16")
386 image = read_image_OpenImageIO(path, bit_depth="uint16")
387 np.testing.assert_equal(np.squeeze(RGB), image)
389 source_path = os.path.join(ROOT_RESOURCES, "Overflowing_Gradient.png")
390 source_image = read_image_OpenImageIO(source_path, bit_depth="uint8")
391 target_path = os.path.join(
392 self._temporary_directory, "Overflowing_Gradient.png"
393 )
394 RGB = np.arange(0, 256, 1, dtype=np.uint8)[None] * 2
395 write_image_OpenImageIO(RGB, target_path, bit_depth="uint8")
396 target_image = read_image_OpenImageIO(source_path, bit_depth="uint8")
397 np.testing.assert_equal(source_image, target_image)
398 np.testing.assert_equal(np.squeeze(RGB), target_image)
400 source_path = os.path.join(ROOT_RESOURCES, "CMS_Test_Pattern.exr")
401 source_image = read_image_OpenImageIO(
402 source_path,
403 additional_data=False,
404 )
405 target_path = os.path.join(self._temporary_directory, "CMS_Test_Pattern.exr")
406 write_image_OpenImageIO(source_image, target_path)
407 target_image = read_image_OpenImageIO(
408 target_path,
409 additional_data=False,
410 )
411 np.testing.assert_equal(source_image, target_image)
412 assert target_image.shape == (1267, 1274, 3)
413 assert target_image.dtype is np.dtype("float32")
415 chromaticities = (
416 0.73470,
417 0.26530,
418 0.00000,
419 1.00000,
420 0.00010,
421 -0.07700,
422 0.32168,
423 0.33767,
424 )
425 write_attributes = [
426 Image_Specification_Attribute("customBooleanFlag", True),
427 Image_Specification_Attribute(
428 "chromaticities", chromaticities, TypeDesc("float[8]")
429 ),
430 Image_Specification_Attribute("compression", "none"),
431 ]
432 write_image_OpenImageIO(target_image, target_path, attributes=write_attributes)
433 target_image, read_attributes = read_image_OpenImageIO(
434 target_path, additional_data=True
435 )
436 for write_attribute in write_attributes:
437 attribute_exists = False
438 for read_attribute in read_attributes:
439 if write_attribute.name == read_attribute.name:
440 attribute_exists = True
441 if isinstance(write_attribute.value, tuple):
442 np.testing.assert_allclose(
443 write_attribute.value,
444 read_attribute.value,
445 atol=TOLERANCE_ABSOLUTE_TESTS,
446 )
447 else:
448 assert write_attribute.value == read_attribute.value
450 attest(
451 attribute_exists,
452 f'"{write_attribute.name}" attribute was not found on image!',
453 )
456class TestReadImageImageio:
457 """
458 Define :func:`colour.io.image.read_image_Imageio` definition unit tests
459 methods.
460 """
462 def test_read_image_Imageio(self) -> None:
463 """Test :func:`colour.io.image.read_image_Imageio` definition."""
465 image = read_image_Imageio(os.path.join(ROOT_RESOURCES, "CMS_Test_Pattern.exr"))
466 assert image.shape == (1267, 1274, 3)
467 assert image.dtype is np.dtype("float32")
469 image = read_image_Imageio(
470 os.path.join(ROOT_RESOURCES, "CMS_Test_Pattern.exr"),
471 "float16",
472 )
473 assert image.shape == (1267, 1274, 3)
474 assert image.dtype is np.dtype("float16")
476 image = read_image_Imageio(os.path.join(ROOT_RESOURCES, "Single_Channel.exr"))
477 assert image.shape == (256, 256)
479 image = read_image_Imageio(
480 os.path.join(ROOT_RESOURCES, "Colour_Logo.png"), "uint8"
481 )
482 assert image.shape == (128, 256, 4)
483 assert image.dtype is np.dtype("uint8")
484 assert np.min(image) == 0
485 assert np.max(image) == 255
487 image = read_image_Imageio(
488 os.path.join(ROOT_RESOURCES, "Colour_Logo.png"), "uint16"
489 )
490 assert image.shape == (128, 256, 4)
491 assert image.dtype is np.dtype("uint16")
492 assert np.min(image) == 0
493 assert np.max(image) == 65535
495 image = read_image_Imageio(
496 os.path.join(ROOT_RESOURCES, "Colour_Logo.png"), "float16"
497 )
498 assert image.dtype is np.dtype("float16")
499 assert np.min(image) == 0.0
500 assert np.max(image) == 1.0
502 image = read_image_Imageio(
503 os.path.join(ROOT_RESOURCES, "Colour_Logo.png"), "float32"
504 )
505 assert image.dtype is np.dtype("float32")
506 assert np.min(image) == 0.0
507 assert np.max(image) == 1.0
510class TestWriteImageImageio:
511 """
512 Define :func:`colour.io.image.write_image_Imageio` definition unit
513 tests methods.
514 """
516 def setup_method(self) -> None:
517 """Initialise the common tests attributes."""
519 self._temporary_directory = tempfile.mkdtemp()
521 def teardown_method(self) -> None:
522 """After tests actions."""
524 shutil.rmtree(self._temporary_directory)
526 def test_write_image_Imageio(self) -> None:
527 """Test :func:`colour.io.image.write_image_Imageio` definition."""
529 source_path = os.path.join(ROOT_RESOURCES, "Overflowing_Gradient.png")
530 source_image = read_image_Imageio(source_path, bit_depth="uint8")
531 target_path = os.path.join(
532 self._temporary_directory, "Overflowing_Gradient.png"
533 )
534 RGB = np.arange(0, 256, 1, dtype=np.uint8)[None] * 2
535 write_image_Imageio(RGB, target_path, bit_depth="uint8")
536 target_image = read_image_Imageio(target_path, bit_depth="uint8")
537 np.testing.assert_equal(np.squeeze(RGB), target_image)
538 np.testing.assert_equal(source_image, target_image)
540 @pytest.mark.skipif(
541 platform.system() == "Linux",
542 reason="EXR tests are breaking on Linux",
543 )
544 def test_write_image_Imageio_exr(self) -> None:
545 """
546 Test :func:`colour.io.image.write_image_Imageio` definition with EXR
547 files.
548 """
550 source_path = os.path.join(ROOT_RESOURCES, "CMS_Test_Pattern.exr")
551 source_image = read_image_Imageio(source_path)
552 target_path = os.path.join(self._temporary_directory, "CMS_Test_Pattern.exr")
553 write_image_Imageio(source_image, target_path)
554 target_image = read_image_Imageio(target_path)
555 np.testing.assert_allclose(
556 source_image, target_image, atol=TOLERANCE_ABSOLUTE_TESTS
557 )
558 assert target_image.shape == (1267, 1274, 3)
559 assert target_image.dtype is np.dtype("float32")
561 target_path = os.path.join(self._temporary_directory, "Full_White.exr")
562 target_image = full((32, 16, 3), 1e6, dtype=np.float16)
563 write_image_Imageio(target_image, target_path)
564 target_image = read_image_Imageio(target_path)
565 assert np.max(target_image) == np.inf
567 target_image = full((32, 16, 3), 1e6)
568 write_image_Imageio(target_image, target_path)
569 target_image = read_image_Imageio(target_path)
570 assert np.max(target_image) == 1e6
573class TestReadImage:
574 """
575 Define :func:`colour.io.image.read_image` definition unit tests
576 methods.
577 """
579 def test_read_image(self) -> None:
580 """Test :func:`colour.io.image.read_image` definition."""
582 image = read_image(os.path.join(ROOT_RESOURCES, "CMS_Test_Pattern.exr"))
583 assert image.shape == (1267, 1274, 3)
584 assert image.dtype is np.dtype("float32")
586 image = read_image(os.path.join(ROOT_RESOURCES, "Single_Channel.exr"))
587 assert image.shape == (256, 256)
590@pytest.mark.skipif(
591 platform.system() == "Windows",
592 reason="OpenImageIO crashes on Windows due to thread-safety issues",
593)
594class TestWriteImage:
595 """Define :func:`colour.io.image.write_image` definition unit tests methods."""
597 def setup_method(self) -> None:
598 """Initialise the common tests attributes."""
600 self._temporary_directory = tempfile.mkdtemp()
602 def teardown_method(self) -> None:
603 """After tests actions."""
605 shutil.rmtree(self._temporary_directory)
607 def test_write_image(self) -> None:
608 """Test :func:`colour.io.image.write_image` definition."""
610 source_path = os.path.join(ROOT_RESOURCES, "CMS_Test_Pattern.exr")
611 source_image = read_image(source_path)
612 target_path = os.path.join(self._temporary_directory, "CMS_Test_Pattern.exr")
613 write_image(source_image, target_path)
614 target_image = read_image(target_path)
615 np.testing.assert_allclose(
616 source_image, target_image, atol=TOLERANCE_ABSOLUTE_TESTS
617 )
618 assert target_image.shape == (1267, 1274, 3)
619 assert target_image.dtype is np.dtype("float32")
622class TestAs3ChannelsImage:
623 """
624 Define :func:`colour.io.image.as_3_channels_image` definition unit tests
625 methods.
626 """
628 def test_as_3_channels_image(self) -> None:
629 """Test :func:`colour.io.image.as_3_channels_image` definition."""
631 a = 0.18
632 b = np.array([[[0.18, 0.18, 0.18]]])
633 np.testing.assert_equal(as_3_channels_image(a), b)
634 a = np.array([0.18])
635 np.testing.assert_equal(as_3_channels_image(a), b)
636 a = np.array([0.18, 0.18, 0.18])
637 np.testing.assert_equal(as_3_channels_image(a), b)
638 a = np.array([[0.18, 0.18, 0.18]])
639 np.testing.assert_equal(as_3_channels_image(a), b)
640 a = np.array([[[0.18, 0.18, 0.18]]])
641 np.testing.assert_equal(as_3_channels_image(a), b)
642 a = np.array([[[[0.18, 0.18, 0.18]]]])
643 np.testing.assert_equal(as_3_channels_image(a), b)
644 a = np.array([[0.18, 0.18, 0.18], [0.20, 0.20, 0.20]])
645 result = as_3_channels_image(a)
646 assert result.shape == (1, 2, 3)
648 def test_raise_exception_as_3_channels_image(self) -> None:
649 """
650 Test :func:`colour.io.image.as_3_channels_image` definition raised
651 exception.
652 """
654 pytest.raises(
655 ValueError,
656 as_3_channels_image,
657 [
658 [
659 [[0.18, 0.18, 0.18], [0.18, 0.18, 0.18]],
660 [[0.18, 0.18, 0.18], [0.18, 0.18, 0.18]],
661 ],
662 [
663 [[0.18, 0.18, 0.18], [0.18, 0.18, 0.18]],
664 [[0.18, 0.18, 0.18], [0.18, 0.18, 0.18]],
665 ],
666 ],
667 )
669 pytest.raises(
670 ValueError,
671 as_3_channels_image,
672 [0.18, 0.18, 0.18, 0.18],
673 )