Source code for colour.io.image

# -*- coding: utf-8 -*-
"""
Image Input / Output Utilities
==============================

Defines image related input / output utilities objects.
"""

from __future__ import division, unicode_literals

import numpy as np
import platform
from collections import namedtuple
from six import string_types

from colour.utilities import (CaseInsensitiveMapping, as_float_array,
                              is_openimageio_installed, filter_kwargs,
                              usage_warning)

__author__ = 'Colour Developers'
__copyright__ = 'Copyright (C) 2013-2020 - Colour Developers'
__license__ = 'New BSD License - https://opensource.org/licenses/BSD-3-Clause'
__maintainer__ = 'Colour Developers'
__email__ = 'colour-developers@colour-science.org'
__status__ = 'Production'

__all__ = [
    'BitDepth_Specification', 'ImageAttribute_Specification',
    'convert_bit_depth', 'read_image_OpenImageIO', 'read_image_Imageio',
    'READ_IMAGE_METHODS', 'read_image', 'write_image_OpenImageIO',
    'write_image_Imageio', 'WRITE_IMAGE_METHODS', 'write_image'
]

BitDepth_Specification = namedtuple(
    'BitDepth_Specification',
    ('name', 'numpy', 'openimageio', 'domain', 'clip'))


[docs]class ImageAttribute_Specification( namedtuple('ImageAttribute_Specification', ('name', 'value', 'type_'))): """ Defines the an image specification attribute. Parameters ---------- name : unicode Attribute name. value : object Attribute value. type_ : TypeDesc, optional Attribute type as an *OpenImageIO* :class:`TypeDesc` class instance. """ def __new__(cls, name, value, type_=None): """ Returns a new instance of the :class:`colour.ImageAttribute_Specification` class. """ return super(ImageAttribute_Specification, cls).__new__( cls, name, value, type_)
if is_openimageio_installed(): # pragma: no cover from OpenImageIO import UINT8, UINT16, HALF, FLOAT BIT_DEPTH_MAPPING = CaseInsensitiveMapping({ 'uint8': BitDepth_Specification('uint8', np.uint8, UINT8, 255, True), 'uint16': BitDepth_Specification('uint16', np.uint16, UINT16, 65535, True), 'float16': BitDepth_Specification('float16', np.float16, HALF, 1, False), 'float32': BitDepth_Specification('float32', np.float32, FLOAT, 1, False), 'float64': BitDepth_Specification('float64', np.float64, FLOAT, 1, False), }) if platform.system() not in ('Windows', 'Microsoft'): # pragma: no cover BIT_DEPTH_MAPPING['float128'] = BitDepth_Specification( 'float128', np.float128, FLOAT, 1, False) else: # pragma: no cover BIT_DEPTH_MAPPING = CaseInsensitiveMapping({ 'uint8': BitDepth_Specification('uint8', np.uint8, None, 255, True), 'uint16': BitDepth_Specification('uint16', np.uint16, None, 65535, True), 'float16': BitDepth_Specification('float16', np.float16, None, 1, False), 'float32': BitDepth_Specification('float32', np.float32, None, 1, False), 'float64': BitDepth_Specification('float64', np.float64, None, 1, False), }) if platform.system() not in ('Windows', 'Microsoft'): # pragma: no cover BIT_DEPTH_MAPPING['float128'] = BitDepth_Specification( 'float128', np.float128, None, 1, False)
[docs]def convert_bit_depth(a, bit_depth='float32'): """ Converts given array to given bit depth, the current bit depth of the array is used to determine the appropriate conversion path. Parameters ---------- a : array_like Array to convert to given bit depth. bit_depth : unicode Bit depth. Returns ------- ndarray Converted array. Examples -------- >>> a = np.array([0.0, 0.5, 1.0]) >>> convert_bit_depth(a, 'uint8') array([ 0, 128, 255], dtype=uint8) >>> convert_bit_depth(a, 'uint16') array([ 0, 32768, 65535], dtype=uint16) >>> convert_bit_depth(a, 'float16') array([ 0. , 0.5, 1. ], dtype=float16) >>> a = np.array([0, 128, 255], dtype=np.uint8) >>> convert_bit_depth(a, 'uint16') array([ 0, 32896, 65535], dtype=uint16) >>> convert_bit_depth(a, 'float32') # doctest: +ELLIPSIS array([ 0. , 0.501960..., 1. ], dtype=float32) """ a = np.asarray(a) bit_depths = ', '.join(sorted(BIT_DEPTH_MAPPING.keys())) assert bit_depth in bit_depths, ( 'Incorrect bit depth was specified, it must be one of: "{0}"!'.format( bit_depths)) assert str(a.dtype) in bit_depths, ( 'Image bit depth must be one of: "{0}"!'.format(bit_depths)) source_dtype = str(a.dtype) target_dtype = BIT_DEPTH_MAPPING[bit_depth].numpy if source_dtype == 'uint8': if bit_depth == 'uint8': return a elif bit_depth == 'uint16': return (a * 257).astype(target_dtype) elif bit_depth in ('float16', 'float32', 'float64', 'float128'): return (a / 255).astype(target_dtype) elif source_dtype == 'uint16': if bit_depth == 'uint8': return (a / 257).astype(target_dtype) elif bit_depth == 'uint16': return a elif bit_depth in ('float16', 'float32', 'float64', 'float128'): return (a / 65535).astype(target_dtype) elif source_dtype in ('float16', 'float32', 'float64', 'float128'): if bit_depth == 'uint8': return np.around(a * 255).astype(target_dtype) elif bit_depth == 'uint16': return np.around(a * 65535).astype(target_dtype) elif bit_depth in ('float16', 'float32', 'float64', 'float128'): return a.astype(target_dtype)
[docs]def read_image_OpenImageIO(path, bit_depth='float32', attributes=False): """ Reads the image at given path using *OpenImageIO*. Parameters ---------- path : unicode Image path. bit_depth : unicode, optional **{'float32', 'uint8', 'uint16', 'float16'}**, Returned image bit depth, the bit depth conversion behaviour is driven directly by *OpenImageIO*, this definition only converts to the relevant data type after reading. attributes : bool, optional Whether to return the image attributes. Returns ------- ndarray or tuple Image as a ndarray or tuple of image as ndarray and list of attributes Notes ----- - For convenience, single channel images are squeezed to 2d arrays. Examples -------- >>> import os >>> import colour >>> path = os.path.join(colour.__path__[0], 'io', 'tests', 'resources', ... 'CMS_Test_Pattern.exr') >>> image = read_image(path) # doctest: +SKIP """ if is_openimageio_installed(raise_exception=True): # pragma: no cover from OpenImageIO import ImageInput path = str(path) bit_depth = BIT_DEPTH_MAPPING[bit_depth] image = ImageInput.open(path) specification = image.spec() shape = (specification.height, specification.width, specification.nchannels) image_data = image.read_image(bit_depth.openimageio) image.close() image = np.squeeze( np.array(image_data, dtype=bit_depth.numpy).reshape(shape)) if attributes: extra_attributes = [] for i in range(len(specification.extra_attribs)): attribute = specification.extra_attribs[i] extra_attributes.append( ImageAttribute_Specification( attribute.name, attribute.value, attribute.type)) return image, extra_attributes else: return image
[docs]def read_image_Imageio(path, bit_depth='float32', **kwargs): """ Reads the image at given path using *Imageio*. Parameters ---------- path : unicode Image path. bit_depth : unicode, optional **{'float32', 'uint8', 'uint16', 'float16'}**, Returned image bit depth, the image data is converted with :func:`colour.io.convert_bit_depth` definition after reading the image. Other Parameters ---------------- \\**kwargs : dict, optional Keywords arguments. Returns ------- ndarray Image as a ndarray. Notes ----- - For convenience, single channel images are squeezed to 2d arrays. Examples -------- >>> import os >>> import colour >>> path = os.path.join(colour.__path__[0], 'io', 'tests', 'resources', ... 'CMS_Test_Pattern.exr') >>> image = read_image_Imageio(path) >>> image.shape # doctest: +SKIP (1267, 1274, 3) >>> image.dtype dtype('float32') """ from imageio import imread image = imread(path, **kwargs) return convert_bit_depth(image, bit_depth)
READ_IMAGE_METHODS = CaseInsensitiveMapping({ 'Imageio': read_image_Imageio, 'OpenImageIO': read_image_OpenImageIO, }) READ_IMAGE_METHODS.__doc__ = """ Supported read image methods. READ_IMAGE_METHODS : CaseInsensitiveMapping **{'Imageio', 'OpenImageIO'}** """
[docs]def read_image(path, bit_depth='float32', method='OpenImageIO', **kwargs): """ Reads the image at given path using given method. Parameters ---------- path : unicode Image path. bit_depth : unicode, optional **{'float32', 'uint8', 'uint16', 'float16'}**, Returned image bit depth, for the *Imageio* method, the image data is converted with :func:`colour.io.convert_bit_depth` definition after reading the image, for the *OpenImageIO* method, the bit depth conversion behaviour is driven directly by the library, this definition only converts to the relevant data type after reading. method : unicode, optional **{'OpenImageIO', 'Imageio'}**, Read method, i.e. the image library used for reading images. Other Parameters ---------------- attributes : bool, optional {:func:`colour.io.read_image_OpenImageIO`}, Whether to return the image attributes. Returns ------- ndarray Image as a ndarray. Notes ----- - If the given method is *OpenImageIO* but the library is not available writing will be performed by *Imageio*. - If the given method is *Imageio*, ``kwargs`` is passed directly to the wrapped definition. - For convenience, single channel images are squeezed to 2d arrays. Examples -------- >>> import os >>> import colour >>> path = os.path.join(colour.__path__[0], 'io', 'tests', 'resources', ... 'CMS_Test_Pattern.exr') >>> image = read_image(path) >>> image.shape # doctest: +SKIP (1267, 1274, 3) >>> image.dtype dtype('float32') """ if method.lower() == 'openimageio': # pragma: no cover if not is_openimageio_installed(): usage_warning( '"OpenImageIO" related API features are not available, ' 'switching to "Imageio"!') method = 'Imageio' function = READ_IMAGE_METHODS[method] if method.lower() == 'openimageio': # pragma: no cover kwargs = filter_kwargs(function, **kwargs) return function(path, bit_depth, **kwargs)
[docs]def write_image_OpenImageIO(image, path, bit_depth='float32', attributes=None): """ Writes given image at given path using *OpenImageIO*. Parameters ---------- image : array_like Image data. path : unicode Image path. bit_depth : unicode, optional **{'float32', 'uint8', 'uint16', 'float16'}**, Bit depth to write the image at, the bit depth conversion behaviour is ruled directly by *OpenImageIO*. attributes : array_like, optional An array of :class:`colour.io.ImageAttribute_Specification` class instances used to set attributes of the image. Returns ------- bool Definition success. Examples -------- Basic image writing: >>> import os >>> import colour >>> path = os.path.join(colour.__path__[0], 'io', 'tests', 'resources', ... 'CMS_Test_Pattern.exr') >>> image = read_image(path) # doctest: +SKIP >>> path = os.path.join(colour.__path__[0], 'io', 'tests', 'resources', ... 'CMSTestPattern.tif') >>> write_image(image, path) # doctest: +SKIP True Advanced image writing while setting attributes: >>> compression = ImageAttribute_Specification('Compression', 'none') >>> write_image(image, path, 'uint8', [compression]) # doctest: +SKIP True """ if is_openimageio_installed(raise_exception=True): # pragma: no cover from OpenImageIO import VERSION_MAJOR, ImageOutput, ImageSpec path = str(path) if attributes is None: attributes = [] bit_depth_specification = BIT_DEPTH_MAPPING[bit_depth] bit_depth = bit_depth_specification.openimageio image = as_float_array(image) image = image * bit_depth_specification.domain if bit_depth_specification.clip: image = np.clip(image, 0, bit_depth_specification.domain) image = image.astype(bit_depth_specification.numpy) if image.ndim == 2: height, width = image.shape channels = 1 else: height, width, channels = image.shape specification = ImageSpec(width, height, channels, bit_depth) for attribute in attributes: name = str(attribute.name) value = (str(attribute.value) if isinstance( attribute.value, string_types) else attribute.value) type_ = attribute.type_ if attribute.type_ is None: specification.attribute(name, value) else: specification.attribute(name, type_, value) image_output = ImageOutput.create(path) if VERSION_MAJOR == 1: from OpenImageIO import ImageOutputOpenMode image_output.open(path, specification, ImageOutputOpenMode.Create) image_output.write_image(bit_depth, image.tostring()) else: image_output.open(path, specification) image_output.write_image(image) image_output.close() return True
[docs]def write_image_Imageio(image, path, bit_depth='float32', **kwargs): """ Writes given image at given path using *Imageio*. Parameters ---------- image : array_like Image data. path : unicode Image path. bit_depth : unicode, optional **{'float32', 'uint8', 'uint16', 'float16'}**, Bit depth to write the image at, the image data is converted with :func:`colour.io.convert_bit_depth` definition prior to writing the image. Other Parameters ---------------- \\**kwargs : dict, optional Keywords arguments. Returns ------- bool Definition success. Examples -------- >>> import os >>> import colour >>> path = os.path.join(colour.__path__[0], 'io', 'tests', 'resources', ... 'CMS_Test_Pattern.exr') >>> image = read_image(path) # doctest: +SKIP >>> path = os.path.join(colour.__path__[0], 'io', 'tests', 'resources', ... 'CMSTestPattern.tif') >>> write_image(image, path) # doctest: +SKIP True """ from imageio import imwrite image = convert_bit_depth(image, bit_depth) return imwrite(path, image, **kwargs)
WRITE_IMAGE_METHODS = CaseInsensitiveMapping({ 'Imageio': write_image_Imageio, 'OpenImageIO': write_image_OpenImageIO, }) WRITE_IMAGE_METHODS.__doc__ = """ Supported write image methods. WRITE_IMAGE_METHODS : CaseInsensitiveMapping **{'Imageio', 'OpenImageIO'}** """
[docs]def write_image(image, path, bit_depth='float32', method='OpenImageIO', **kwargs): """ Writes given image at given path using given method. Parameters ---------- image : array_like Image data. path : unicode Image path. bit_depth : unicode, optional **{'float32', 'uint8', 'uint16', 'float16'}**, Bit depth to write the image at, for the *Imageio* method, the image data is converted with :func:`colour.io.convert_bit_depth` definition prior to writing the image. method : unicode, optional **{'OpenImageIO', 'Imageio'}**, Write method, i.e. the image library used for writing images. Other Parameters ---------------- attributes : array_like, optional {:func:`colour.io.write_image_OpenImageIO`}, An array of :class:`colour.io.ImageAttribute_Specification` class instances used to set attributes of the image. Returns ------- bool Definition success. Notes ----- - If the given method is *OpenImageIO* but the library is not available writing will be performed by *Imageio*. - If the given method is *Imageio*, ``kwargs`` is passed directly to the wrapped definition. Examples -------- Basic image writing: >>> import os >>> import colour >>> path = os.path.join(colour.__path__[0], 'io', 'tests', 'resources', ... 'CMS_Test_Pattern.exr') >>> image = read_image(path) # doctest: +SKIP >>> path = os.path.join(colour.__path__[0], 'io', 'tests', 'resources', ... 'CMSTestPattern.tif') >>> write_image(image, path) # doctest: +SKIP True Advanced image writing while setting attributes using *OpenImageIO*: >>> compression = ImageAttribute_Specification('Compression', 'none') >>> write_image(image, path, bit_depth='uint8', attributes=[compression]) ... # doctest: +SKIP True """ if method.lower() == 'openimageio': # pragma: no cover if not is_openimageio_installed(): usage_warning( '"OpenImageIO" related API features are not available, ' 'switching to "Imageio"!') method = 'Imageio' function = WRITE_IMAGE_METHODS[method] if method.lower() == 'openimageio': # pragma: no cover kwargs = filter_kwargs(function, **kwargs) return function(image, path, bit_depth, **kwargs)