Coverage for io/image.py: 72%
146 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"""
2Image Input / Output Utilities
3==============================
5Define image-related input/output utility objects for colour science
6applications.
7"""
9from __future__ import annotations
11import typing
12from dataclasses import dataclass, field
14import numpy as np
16if typing.TYPE_CHECKING:
17 from colour.hints import (
18 Any,
19 ArrayLike,
20 DTypeReal,
21 Literal,
22 NDArrayFloat,
23 PathLike,
24 Sequence,
25 Tuple,
26 Type,
27 )
29from colour.hints import NDArrayReal, cast
30from colour.utilities import (
31 CanonicalMapping,
32 as_float_array,
33 as_int_array,
34 attest,
35 filter_kwargs,
36 is_imageio_installed,
37 is_openimageio_installed,
38 optional,
39 required,
40 tstack,
41 usage_warning,
42 validate_method,
43)
44from colour.utilities.deprecation import handle_arguments_deprecation
46__author__ = "Colour Developers"
47__copyright__ = "Copyright 2013 Colour Developers"
48__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
49__maintainer__ = "Colour Developers"
50__email__ = "colour-developers@colour-science.org"
51__status__ = "Production"
53__all__ = [
54 "Image_Specification_BitDepth",
55 "Image_Specification_Attribute",
56 "MAPPING_BIT_DEPTH",
57 "add_attributes_to_image_specification_OpenImageIO",
58 "image_specification_OpenImageIO",
59 "convert_bit_depth",
60 "read_image_OpenImageIO",
61 "read_image_Imageio",
62 "READ_IMAGE_METHODS",
63 "read_image",
64 "write_image_OpenImageIO",
65 "write_image_Imageio",
66 "WRITE_IMAGE_METHODS",
67 "write_image",
68 "as_3_channels_image",
69]
72@dataclass(frozen=True)
73class Image_Specification_BitDepth:
74 """
75 Define a bit-depth specification for image processing operations.
77 Parameters
78 ----------
79 name
80 Attribute name identifying the bit-depth specification.
81 numpy
82 Object representing the *NumPy* bit-depth data type.
83 openimageio
84 Object representing the *OpenImageIO* bit-depth specification.
85 """
87 name: str
88 numpy: Type[DTypeReal]
89 openimageio: Any
92@dataclass
93class Image_Specification_Attribute:
94 """
95 Define an image specification attribute for OpenImageIO operations.
97 Parameters
98 ----------
99 name
100 Attribute name identifying the metadata field.
101 value
102 Attribute value containing the metadata content.
103 type_
104 Attribute type as an *OpenImageIO* :class:`TypeDesc` class instance
105 specifying the data type of the value.
106 """
108 name: str
109 value: Any
110 type_: OpenImageIO.TypeDesc | None = field( # noqa: F821, RUF100 # pyright: ignore # noqa: F821
111 default_factory=lambda: None
112 )
115if is_openimageio_installed(): # pragma: no cover
116 from OpenImageIO import ImageSpec # pyright: ignore
117 from OpenImageIO import DOUBLE, FLOAT, HALF, UINT8, UINT16
119 MAPPING_BIT_DEPTH: CanonicalMapping = CanonicalMapping(
120 {
121 "uint8": Image_Specification_BitDepth("uint8", np.uint8, UINT8),
122 "uint16": Image_Specification_BitDepth("uint16", np.uint16, UINT16),
123 "float16": Image_Specification_BitDepth("float16", np.float16, HALF),
124 "float32": Image_Specification_BitDepth("float32", np.float32, FLOAT),
125 "float64": Image_Specification_BitDepth("float64", np.float64, DOUBLE),
126 }
127 )
128 if not typing.TYPE_CHECKING and hasattr(np, "float128"): # pragma: no cover
129 MAPPING_BIT_DEPTH["float128"] = Image_Specification_BitDepth(
130 "float128", np.float128, DOUBLE
131 )
132else: # pragma: no cover
134 class ImageSpec:
135 attribute: Any
137 MAPPING_BIT_DEPTH: CanonicalMapping = CanonicalMapping(
138 {
139 "uint8": Image_Specification_BitDepth("uint8", np.uint8, None),
140 "uint16": Image_Specification_BitDepth("uint16", np.uint16, None),
141 "float16": Image_Specification_BitDepth("float16", np.float16, None),
142 "float32": Image_Specification_BitDepth("float32", np.float32, None),
143 "float64": Image_Specification_BitDepth("float64", np.float64, None),
144 }
145 )
146 if not typing.TYPE_CHECKING and hasattr(np, "float128"): # pragma: no cover
147 MAPPING_BIT_DEPTH["float128"] = Image_Specification_BitDepth(
148 "float128", np.float128, None
149 )
152def add_attributes_to_image_specification_OpenImageIO(
153 image_specification: ImageSpec, attributes: Sequence
154) -> ImageSpec:
155 """
156 Add the specified attributes to the specified *OpenImageIO* image
157 specification.
159 Apply metadata attributes to an existing image specification object,
160 enabling customization of image properties such as compression,
161 colour space information, or other format-specific metadata.
163 Parameters
164 ----------
165 image_specification
166 *OpenImageIO* image specification to modify.
167 attributes
168 Sequence of :class:`colour.io.Image_Specification_Attribute`
169 instances containing metadata to apply to the image
170 specification.
172 Returns
173 -------
174 :class:`ImageSpec`
175 Modified *OpenImageIO* image specification with applied
176 attributes.
178 Examples
179 --------
180 >>> image_specification = image_specification_OpenImageIO(
181 ... 1920, 1080, 3, "float16"
182 ... ) # doctest: +SKIP
183 >>> compression = Image_Specification_Attribute("Compression", "none")
184 >>> image_specification = add_attributes_to_image_specification_OpenImageIO(
185 ... image_specification, [compression]
186 ... ) # doctest: +SKIP
187 >>> image_specification.extra_attribs[0].value # doctest: +SKIP
188 'none'
189 """
191 for attribute in attributes:
192 name = str(attribute.name)
193 value = (
194 str(attribute.value)
195 if isinstance(attribute.value, str)
196 else attribute.value
197 )
198 type_ = attribute.type_
199 if attribute.type_ is None:
200 image_specification.attribute(name, value)
201 else:
202 image_specification.attribute(name, type_, value)
204 return image_specification
207def image_specification_OpenImageIO(
208 width: int,
209 height: int,
210 channels: int,
211 bit_depth: Literal[
212 "uint8", "uint16", "float16", "float32", "float64", "float128"
213 ] = "float32",
214 attributes: Sequence | None = None,
215) -> ImageSpec:
216 """
217 Create an *OpenImageIO* image specification.
219 Parameters
220 ----------
221 width
222 Image width.
223 height
224 Image height.
225 channels
226 Image channel count.
227 bit_depth
228 Bit-depth to create the image with. The bit-depth conversion
229 behaviour is ruled directly by *OpenImageIO*.
230 attributes
231 An array of :class:`colour.io.Image_Specification_Attribute`
232 class instances used to set attributes of the image.
234 Returns
235 -------
236 :class:`ImageSpec`
237 *OpenImageIO* image specification.
239 Examples
240 --------
241 >>> compression = Image_Specification_Attribute("Compression", "none")
242 >>> image_specification_OpenImageIO(
243 ... 1920, 1080, 3, "float16", [compression]
244 ... ) # doctest: +SKIP
245 <OpenImageIO.ImageSpec object at 0x...>
246 """
248 from OpenImageIO import ImageSpec # noqa: PLC0415
250 attributes = cast("list", optional(attributes, []))
252 bit_depth_specification = MAPPING_BIT_DEPTH[bit_depth]
254 image_specification = ImageSpec(
255 width, height, channels, bit_depth_specification.openimageio
256 )
258 add_attributes_to_image_specification_OpenImageIO(
259 image_specification, # pyright: ignore
260 attributes or [],
261 )
263 return image_specification # pyright: ignore
266def convert_bit_depth(
267 a: ArrayLike,
268 bit_depth: Literal[
269 "uint8", "uint16", "float16", "float32", "float64", "float128"
270 ] = "float32",
271) -> NDArrayReal:
272 """
273 Convert the specified array to the specified bit-depth.
275 The conversion path is determined by the current bit-depth of the input
276 array and the target bit-depth. Supports conversions between unsigned
277 integers, floating-point types, and mixed type conversions with
278 appropriate scaling.
280 Parameters
281 ----------
282 a
283 Array to convert to the specified bit-depth.
284 bit_depth
285 Target bit-depth. Supported types include unsigned integers
286 ("uint8", "uint16") and floating-point ("float16", "float32",
287 "float64", "float128").
289 Returns
290 -------
291 :class:`numpy.ndarray`
292 Array converted to the specified bit-depth.
294 Raises
295 ------
296 AssertionError
297 If the source or target bit-depth is not supported.
299 Examples
300 --------
301 >>> a = np.array([0.0, 0.5, 1.0])
302 >>> convert_bit_depth(a, "uint8")
303 array([ 0, 128, 255], dtype=uint8)
304 >>> convert_bit_depth(a, "uint16")
305 array([ 0, 32768, 65535], dtype=uint16)
306 >>> convert_bit_depth(a, "float16")
307 array([ 0. , 0.5, 1. ], dtype=float16)
308 >>> a = np.array([0, 128, 255], dtype=np.uint8)
309 >>> convert_bit_depth(a, "uint16")
310 array([ 0, 32896, 65535], dtype=uint16)
311 >>> convert_bit_depth(a, "float32") # doctest: +ELLIPSIS
312 array([ 0. , 0.501960..., 1. ], dtype=float32)
313 """
315 a = np.asarray(a)
317 bit_depths = ", ".join(sorted(MAPPING_BIT_DEPTH.keys()))
319 attest(
320 bit_depth in bit_depths,
321 f'Incorrect bit-depth was specified, it must be one of: "{bit_depths}"!',
322 )
324 attest(
325 str(a.dtype) in bit_depths,
326 f'Image bit-depth must be one of: "{bit_depths}"!',
327 )
329 source_dtype = str(a.dtype)
330 target_dtype = MAPPING_BIT_DEPTH[bit_depth].numpy
332 if source_dtype == "uint8":
333 if bit_depth == "uint16":
334 a = a.astype(target_dtype) * 257
335 elif bit_depth in ("float16", "float32", "float64", "float128"):
336 a = (a / 255).astype(target_dtype)
337 elif source_dtype == "uint16":
338 if bit_depth == "uint8":
339 a = (a / 257).astype(target_dtype)
340 elif bit_depth in ("float16", "float32", "float64", "float128"):
341 a = (a / 65535).astype(target_dtype)
342 elif source_dtype in ("float16", "float32", "float64", "float128"):
343 if bit_depth == "uint8":
344 a = np.around(a * 255).astype(target_dtype)
345 elif bit_depth == "uint16":
346 a = np.around(a * 65535).astype(target_dtype)
347 elif bit_depth in ("float16", "float32", "float64", "float128"):
348 a = a.astype(target_dtype)
350 return a
353@typing.overload
354@required("OpenImageIO")
355def read_image_OpenImageIO(
356 path: str | PathLike,
357 bit_depth: Literal[
358 "uint8", "uint16", "float16", "float32", "float64", "float128"
359 ] = ...,
360 additional_data: Literal[True] = True,
361 **kwargs: Any,
362) -> Tuple[NDArrayReal, Tuple[Image_Specification_Attribute, ...]]: ...
365@typing.overload
366@required("OpenImageIO")
367def read_image_OpenImageIO(
368 path: str | PathLike,
369 bit_depth: Literal[
370 "uint8", "uint16", "float16", "float32", "float64", "float128"
371 ] = ...,
372 *,
373 additional_data: Literal[False],
374 **kwargs: Any,
375) -> NDArrayReal: ...
378@typing.overload
379@required("OpenImageIO")
380def read_image_OpenImageIO(
381 path: str | PathLike,
382 bit_depth: Literal["uint8", "uint16", "float16", "float32", "float64", "float128"],
383 additional_data: Literal[False],
384 **kwargs: Any,
385) -> NDArrayReal: ...
388@required("OpenImageIO")
389def read_image_OpenImageIO(
390 path: str | PathLike,
391 bit_depth: Literal[
392 "uint8", "uint16", "float16", "float32", "float64", "float128"
393 ] = "float32",
394 additional_data: bool = False,
395 **kwargs: Any,
396) -> NDArrayReal | Tuple[NDArrayReal, Tuple[Image_Specification_Attribute, ...]]:
397 """
398 Read image data from the specified path using *OpenImageIO*.
400 Load image data from the file system with support for various bit-depth
401 formats. The bit-depth conversion behaviour is controlled by
402 *OpenImageIO*, with this function performing only the final type
403 conversion after reading.
405 Parameters
406 ----------
407 path
408 Path to the image file.
409 bit_depth
410 Target bit-depth for the returned image data. The bit-depth
411 conversion is handled by *OpenImageIO* during the read operation,
412 with this function converting to the appropriate *NumPy* data type
413 afterwards.
414 additional_data
415 Whether to return additional metadata from the image file.
417 Returns
418 -------
419 :class:`numpy.ndarray` or :class:`tuple`
420 Image data as an array when ``additional_data`` is ``False``, or a
421 tuple containing the image data and a tuple of
422 :class:`colour.io.Image_Specification_Attribute` instances when
423 ``additional_data`` is ``True``.
425 Notes
426 -----
427 - For convenience, single channel images are squeezed to 2D arrays.
429 Examples
430 --------
431 >>> import os
432 >>> import colour
433 >>> path = os.path.join(
434 ... colour.__path__[0],
435 ... "io",
436 ... "tests",
437 ... "resources",
438 ... "CMS_Test_Pattern.exr",
439 ... )
440 >>> image = read_image_OpenImageIO(path) # doctest: +SKIP
441 """
443 from OpenImageIO import ImageInput # noqa: PLC0415
445 path = str(path)
447 kwargs = handle_arguments_deprecation(
448 {
449 "ArgumentRenamed": [["attributes", "additional_data"]],
450 },
451 **kwargs,
452 )
454 additional_data = kwargs.get("additional_data", additional_data)
456 bit_depth_specification = MAPPING_BIT_DEPTH[bit_depth]
458 image_input = ImageInput.open(path)
459 image_specification = image_input.spec()
461 shape = (
462 image_specification.height,
463 image_specification.width,
464 image_specification.nchannels,
465 )
467 image = image_input.read_image(bit_depth_specification.openimageio)
468 image_input.close()
470 image = np.reshape(np.array(image, dtype=bit_depth_specification.numpy), shape)
471 image = cast("NDArrayReal", np.squeeze(image))
473 if additional_data:
474 extra_attributes = [
475 Image_Specification_Attribute(
476 attribute.name, attribute.value, attribute.type
477 )
478 for attribute in image_specification.extra_attribs
479 ]
481 return image, tuple(extra_attributes)
483 return image
486@required("Imageio")
487def read_image_Imageio(
488 path: str | PathLike,
489 bit_depth: Literal[
490 "uint8", "uint16", "float16", "float32", "float64", "float128"
491 ] = "float32",
492 **kwargs: Any,
493) -> NDArrayReal:
494 """
495 Read image data from the specified path using *Imageio*.
497 Parameters
498 ----------
499 path
500 Path to the image file.
501 bit_depth
502 Target bit-depth for the returned image data. The image data is
503 converted with :func:`colour.io.convert_bit_depth` definition after
504 reading the image.
506 Other Parameters
507 ----------------
508 kwargs
509 Keywords arguments.
511 Returns
512 -------
513 :class:`numpy.ndarray`
514 Image data.
516 Notes
517 -----
518 - For convenience, single channel images are squeezed to 2D arrays.
520 Examples
521 --------
522 >>> import os
523 >>> import colour
524 >>> path = os.path.join(
525 ... colour.__path__[0],
526 ... "io",
527 ... "tests",
528 ... "resources",
529 ... "CMS_Test_Pattern.exr",
530 ... )
531 >>> image = read_image_Imageio(path)
532 >>> image.shape # doctest: +SKIP
533 (1267, 1274, 3)
534 >>> image.dtype
535 dtype('float32')
536 """
538 from imageio.v2 import imread # noqa: PLC0415
540 path = str(path)
542 image = np.squeeze(imread(path, **kwargs))
544 return convert_bit_depth(image, bit_depth)
547READ_IMAGE_METHODS: CanonicalMapping = CanonicalMapping(
548 {
549 "Imageio": read_image_Imageio,
550 "OpenImageIO": read_image_OpenImageIO,
551 }
552)
553READ_IMAGE_METHODS.__doc__ = """
554Supported image reading methods.
555"""
558def read_image(
559 path: str | PathLike,
560 bit_depth: Literal[
561 "uint8", "uint16", "float16", "float32", "float64", "float128"
562 ] = "float32",
563 method: Literal["Imageio", "OpenImageIO"] | str = "OpenImageIO",
564 **kwargs: Any,
565) -> NDArrayReal:
566 """
567 Read image data from the specified path.
569 Load and optionally convert image data from various formats,
570 supporting multiple bit-depth conversions and backend libraries for
571 flexible image I/O operations in colour science workflows.
573 Parameters
574 ----------
575 path
576 Path to the image file.
577 bit_depth
578 Target bit-depth for the returned image data. For the *Imageio*
579 method, image data is converted using
580 :func:`colour.io.convert_bit_depth` after reading. For the
581 *OpenImageIO* method, bit-depth conversion is handled by the
582 library with this parameter controlling only the final data type.
583 method
584 Image reading backend library. Defaults to *OpenImageIO* with
585 automatic fallback to *Imageio* if unavailable.
587 Other Parameters
588 ----------------
589 additional_data
590 {:func:`colour.io.read_image_OpenImageIO`},
591 Whether to return additional metadata with the image data.
593 Returns
594 -------
595 :class:`numpy.ndarray`
596 Image data as a NumPy array with the specified bit-depth.
598 Notes
599 -----
600 - If the specified method is *OpenImageIO* but the library is not
601 available, reading will be performed by *Imageio*.
602 - If the specified method is *Imageio*, ``kwargs`` is passed
603 directly to the wrapped definition.
604 - For convenience, single channel images are squeezed to 2D arrays.
606 Examples
607 --------
608 >>> import os
609 >>> import colour
610 >>> path = os.path.join(
611 ... colour.__path__[0],
612 ... "io",
613 ... "tests",
614 ... "resources",
615 ... "CMS_Test_Pattern.exr",
616 ... )
617 >>> image = read_image(path)
618 >>> image.shape # doctest: +SKIP
619 (1267, 1274, 3)
620 >>> image.dtype
621 dtype('float32')
622 """
624 if method.lower() == "imageio" and not is_imageio_installed(): # pragma: no cover
625 usage_warning(
626 '"Imageio" related API features are not available, '
627 'switching to "OpenImageIO"!'
628 )
629 method = "openimageio"
631 method = validate_method(method, tuple(READ_IMAGE_METHODS))
633 function = READ_IMAGE_METHODS[method]
635 if method == "openimageio": # pragma: no cover
636 kwargs = filter_kwargs(function, **kwargs)
638 return function(path, bit_depth, **kwargs)
641@required("OpenImageIO")
642def write_image_OpenImageIO(
643 image: ArrayLike,
644 path: str | PathLike,
645 bit_depth: Literal[
646 "uint8", "uint16", "float16", "float32", "float64", "float128"
647 ] = "float32",
648 attributes: Sequence | None = None,
649) -> bool:
650 """
651 Write image data to the specified path using *OpenImageIO*.
653 Parameters
654 ----------
655 image
656 Image data to write.
657 path
658 Path to the image file.
659 bit_depth
660 Bit-depth to write the image at. The bit-depth conversion behaviour
661 is ruled directly by *OpenImageIO*.
662 attributes
663 An array of :class:`colour.io.Image_Specification_Attribute` class
664 instances used to set attributes of the image.
666 Returns
667 -------
668 :class:`bool`
669 Definition success.
671 Examples
672 --------
673 Basic image writing:
675 >>> import os
676 >>> import colour
677 >>> path = os.path.join(
678 ... colour.__path__[0],
679 ... "io",
680 ... "tests",
681 ... "resources",
682 ... "CMS_Test_Pattern.exr",
683 ... )
684 >>> image = read_image(path) # doctest: +SKIP
685 >>> path = os.path.join(
686 ... colour.__path__[0],
687 ... "io",
688 ... "tests",
689 ... "resources",
690 ... "CMSTestPattern.tif",
691 ... )
692 >>> write_image_OpenImageIO(image, path) # doctest: +SKIP
693 True
695 Advanced image writing while setting attributes:
697 >>> compression = Image_Specification_Attribute("Compression", "none")
698 >>> write_image_OpenImageIO(image, path, "uint8", [compression])
699 ... # doctest: +SKIP
700 True
702 Writing an "ACES" compliant "EXR" file:
704 >>> from OpenImageIO import TypeDesc
705 >>> chromaticities = (
706 ... 0.7347,
707 ... 0.2653,
708 ... 0.0,
709 ... 1.0,
710 ... 0.0001,
711 ... -0.077,
712 ... 0.32168,
713 ... 0.33767,
714 ... )
715 >>> attributes = [
716 ... Image_Specification_Attribute("openexr:ACESContainerPolicy", "relaxed"),
717 ... Image_Specification_Attribute(
718 ... "chromaticities", chromaticities, TypeDesc("float[8]")
719 ... ),
720 ... Image_Specification_Attribute("compression", "none"),
721 ... ]
722 >>> write_image_OpenImageIO(image, path, attributes=attributes) # doctest: +SKIP
723 True
725 Notes
726 -----
727 - When using ``openexr:ACESContainerPolicy`` with ``relaxed`` mode,
728 *OpenImageIO* automatically sets the ``colorInteropId`` attribute to
729 ``lin_ap0_scene`` for ACES-compliant files.
730 - The ``acesImageContainerFlag`` attribute should not be set manually
731 in *OpenImageIO* 3.1.7.0+, as it triggers strict ACES validation.
732 Use ``openexr:ACESContainerPolicy`` instead.
733 """
735 from OpenImageIO import ImageOutput # noqa: PLC0415
737 image = as_float_array(image)
738 path = str(path)
740 attributes = cast("list", optional(attributes, []))
742 bit_depth_specification = MAPPING_BIT_DEPTH[bit_depth]
744 if bit_depth_specification.numpy in [np.uint8, np.uint16]:
745 minimum, maximum = (
746 np.iinfo(bit_depth_specification.numpy).min,
747 np.iinfo(bit_depth_specification.numpy).max,
748 )
749 image = np.clip(image * maximum, minimum, maximum)
751 image = as_int_array(image, bit_depth_specification.numpy)
753 image = image.astype(bit_depth_specification.numpy)
755 if image.ndim == 2:
756 height, width = image.shape
757 channels = 1
758 else:
759 height, width, channels = image.shape
761 image_specification = image_specification_OpenImageIO(
762 width, height, channels, bit_depth, attributes
763 )
765 image_output = ImageOutput.create(path)
767 image_output.open(path, image_specification) # pyright: ignore
768 success = image_output.write_image(image)
770 image_output.close()
772 return success
775@required("Imageio")
776def write_image_Imageio(
777 image: ArrayLike,
778 path: str | PathLike,
779 bit_depth: Literal[
780 "uint8", "uint16", "float16", "float32", "float64", "float128"
781 ] = "float32",
782 **kwargs: Any,
783) -> bytes | None:
784 """
785 Write image data to the specified path using *Imageio*.
787 Parameters
788 ----------
789 image
790 Image data to write.
791 path
792 Path to the image file.
793 bit_depth
794 Bit-depth to write the image at. The image data is converted with
795 :func:`colour.io.convert_bit_depth` definition prior to writing.
797 Other Parameters
798 ----------------
799 kwargs
800 Keywords arguments passed to the underlying *Imageio* ``imwrite``
801 function.
803 Returns
804 -------
805 :class:`bytes` or :py:data:`None`
806 Image data written as bytes if successful, :py:data:`None`
807 otherwise.
809 Notes
810 -----
811 - Control how images are saved by the *Freeimage* backend using the
812 ``flags`` keyword argument with desired values. See the *Load /
813 Save flag constants* section in
814 https://sourceforge.net/p/freeimage/svn/HEAD/tree/FreeImage/trunk/Source/FreeImage.h
816 Examples
817 --------
818 >>> import os
819 >>> import colour
820 >>> path = os.path.join(
821 ... colour.__path__[0],
822 ... "io",
823 ... "tests",
824 ... "resources",
825 ... "CMS_Test_Pattern.exr",
826 ... )
827 >>> image = read_image(path) # doctest: +SKIP
828 >>> path = os.path.join(
829 ... colour.__path__[0],
830 ... "io",
831 ... "tests",
832 ... "resources",
833 ... "CMSTestPattern.tif",
834 ... )
835 >>> write_image_Imageio(image, path) # doctest: +SKIP
836 True
837 """
839 from imageio.v2 import imwrite # noqa: PLC0415
841 path = str(path)
843 if all(
844 [
845 path.lower().endswith(".exr"),
846 bit_depth in ("float32", "float64", "float128"),
847 ]
848 ):
849 # Ensures that "OpenEXR" images are saved as "Float32" according to the
850 # image bit-depth.
851 kwargs["flags"] = 0x0001
853 image = convert_bit_depth(image, bit_depth)
855 return imwrite(path, image, **kwargs)
858WRITE_IMAGE_METHODS: CanonicalMapping = CanonicalMapping(
859 {
860 "Imageio": write_image_Imageio,
861 "OpenImageIO": write_image_OpenImageIO,
862 }
863)
864WRITE_IMAGE_METHODS.__doc__ = """
865Supported image writing methods.
866"""
869def write_image(
870 image: ArrayLike,
871 path: str | PathLike,
872 bit_depth: Literal[
873 "uint8", "uint16", "float16", "float32", "float64", "float128"
874 ] = "float32",
875 method: Literal["Imageio", "OpenImageIO"] | str = "OpenImageIO",
876 **kwargs: Any,
877) -> bool:
878 """
879 Write image data to the specified path.
881 Parameters
882 ----------
883 image
884 Image data to write.
885 path
886 Path to the image file.
887 bit_depth
888 Bit-depth to write the image at. For the *Imageio* method, the
889 image data is converted with :func:`colour.io.convert_bit_depth`
890 definition prior to writing the image.
891 method
892 Image writing backend library.
894 Other Parameters
895 ----------------
896 attributes
897 {:func:`colour.io.write_image_OpenImageIO`},
898 An array of :class:`colour.io.Image_Specification_Attribute` class
899 instances used to set attributes of the image.
901 Returns
902 -------
903 :class:`bool`
904 Definition success.
906 Notes
907 -----
908 - If the specified method is *OpenImageIO* but the library is not
909 available writing will be performed by *Imageio*.
910 - If the specified method is *Imageio*, ``kwargs`` is passed directly
911 to the wrapped definition.
912 - It is possible to control how the images are saved by the
913 *Freeimage* backend by using the ``flags`` keyword argument and
914 passing a desired value. See the *Load / Save flag constants*
915 section in
916 https://sourceforge.net/p/freeimage/svn/HEAD/tree/FreeImage/trunk/Source/FreeImage.h
918 Examples
919 --------
920 Basic image writing:
922 >>> import os
923 >>> import colour
924 >>> path = os.path.join(
925 ... colour.__path__[0],
926 ... "io",
927 ... "tests",
928 ... "resources",
929 ... "CMS_Test_Pattern.exr",
930 ... )
931 >>> image = read_image(path) # doctest: +SKIP
932 >>> path = os.path.join(
933 ... colour.__path__[0],
934 ... "io",
935 ... "tests",
936 ... "resources",
937 ... "CMSTestPattern.tif",
938 ... )
939 >>> write_image(image, path) # doctest: +SKIP
940 True
942 Advanced image writing while setting attributes using *OpenImageIO*:
944 >>> compression = Image_Specification_Attribute("Compression", "none")
945 >>> write_image(image, path, bit_depth="uint8", attributes=[compression])
946 ... # doctest: +SKIP
947 True
948 """
950 if method.lower() == "imageio" and not is_imageio_installed(): # pragma: no cover
951 usage_warning(
952 '"Imageio" related API features are not available, '
953 'switching to "OpenImageIO"!'
954 )
955 method = "openimageio"
957 method = validate_method(method, tuple(WRITE_IMAGE_METHODS))
959 function = WRITE_IMAGE_METHODS[method]
961 if method == "openimageio": # pragma: no cover
962 kwargs = filter_kwargs(function, **kwargs)
964 return function(image, path, bit_depth, **kwargs)
967def as_3_channels_image(a: ArrayLike) -> NDArrayFloat:
968 """
969 Convert the specified array :math:`a` to a 3-channel image-like
970 representation.
972 Parameters
973 ----------
974 a
975 Array :math:`a` to convert to a 3-channel image-like representation.
977 Returns
978 -------
979 :class:`numpy.ndarray`
980 3-channel image-like representation of array :math:`a`.
982 Raises
983 ------
984 ValueError
985 If the array has more than 3 dimensions or more than 1 or 3 channels.
987 Examples
988 --------
989 >>> as_3_channels_image(0.18)
990 array([[[ 0.18, 0.18, 0.18]]])
991 >>> as_3_channels_image([0.18])
992 array([[[ 0.18, 0.18, 0.18]]])
993 >>> as_3_channels_image([0.18, 0.18, 0.18])
994 array([[[ 0.18, 0.18, 0.18]]])
995 >>> as_3_channels_image([[0.18, 0.18, 0.18]])
996 array([[[ 0.18, 0.18, 0.18]]])
997 >>> as_3_channels_image([[[0.18, 0.18, 0.18]]])
998 array([[[ 0.18, 0.18, 0.18]]])
999 >>> as_3_channels_image([[[[0.18, 0.18, 0.18]]]])
1000 array([[[ 0.18, 0.18, 0.18]]])
1001 """
1003 a = np.squeeze(as_float_array(a))
1005 if len(a.shape) > 3:
1006 error = (
1007 "Array has more than 3-dimensions and cannot be converted to a "
1008 "3-channels image-like representation!"
1009 )
1011 raise ValueError(error)
1013 if len(a.shape) > 0 and a.shape[-1] not in (1, 3):
1014 error = (
1015 "Array has more than 1 or 3 channels and cannot be converted to a "
1016 "3-channels image-like representation!"
1017 )
1019 raise ValueError(error)
1021 if len(a.shape) == 0 or a.shape[-1] == 1:
1022 a = tstack([a, a, a])
1024 if len(a.shape) == 1:
1025 a = a[None, None, ...]
1026 elif len(a.shape) == 2:
1027 a = a[None, ...]
1029 return a