# -*- coding: utf-8 -*-
"""
Image Data & Metadata Utilities
===============================
Defines various image data and metadata utilities classes:
- :class:`colour_hdri.Metadata`
- :class:`colour_hdri.Image`
- :class:`colour_hdri.ImageStack`
"""
from __future__ import division, unicode_literals
import logging
import numpy as np
from collections import MutableSequence
from recordclass import recordclass
from colour import read_image
from colour.utilities import as_float_array, is_string, tsplit, tstack, warning
from colour_hdri.utilities.exif import (parse_exif_array, parse_exif_fraction,
parse_exif_numeric, read_exif_tags)
from colour_hdri.utilities.exposure import average_luminance
__author__ = 'Colour Developers'
__copyright__ = 'Copyright (C) 2015-2019 - Colour Developers'
__license__ = 'New BSD License - http://opensource.org/licenses/BSD-3-Clause'
__maintainer__ = 'Colour Developers'
__email__ = 'colour-science@googlegroups.com'
__status__ = 'Production'
__all__ = ['Metadata', 'Image', 'ImageStack']
LOGGER = logging.getLogger(__name__)
[docs]class Image(object):
"""
Defines the base object for storing an image along its path, pixel data and
metadata needed for HDRI / radiance images generation.
Parameters
----------
path : unicode, optional
Image path.
data : array_like, optional
Image pixel data array.
metadata : Metadata, optional
Image exif metadata.
Attributes
----------
path
data
metadata
Methods
-------
read_data
read_metadata
"""
[docs] def __init__(self, path=None, data=None, metadata=None):
self._path = None
self.path = path
self._data = None
self.data = data
self._metadata = None
self.metadata = metadata
@property
def path(self):
"""
Property for **self._path** private attribute.
Returns
-------
unicode
self._path.
"""
return self._path
@path.setter
def path(self, value):
"""
Setter for **self._path** private attribute.
Parameters
----------
value : unicode
Attribute value.
"""
if value is not None:
assert is_string(value), (('"{0}" attribute: "{1}" is not a '
'"string" like object!').format(
'path', value))
self._path = value
@property
def data(self):
"""
Property for **self._data** private attribute.
Returns
-------
unicode
self._data.
"""
return self._data
@data.setter
def data(self, value):
"""
Setter for **self._data** private attribute.
Parameters
----------
value : unicode
Attribute value.
"""
if value is not None:
assert isinstance(value, (tuple, list, np.ndarray, np.matrix)), ((
'"{0}" attribute: "{1}" is not a "tuple", "list", "ndarray" '
'or "matrix" instance!').format('data', value))
self._data = as_float_array(value)
@property
def metadata(self):
"""
Property for **self._metadata** private attribute.
Returns
-------
unicode
self._metadata.
"""
return self._metadata
@metadata.setter
def metadata(self, value):
"""
Setter for **self._metadata** private attribute.
Parameters
----------
value : unicode
Attribute value.
"""
if value is not None:
assert isinstance(value, Metadata), (
'"{0}" attribute: "{1}" is not a "Metadata" instance!'.format(
'metadata', value))
self._metadata = value
[docs] def read_data(self, decoding_cctf=None):
"""
Reads image pixel data at :attr:`Image.path` attribute.
Parameters
----------
decoding_cctf : object, optional
Decoding colour component transfer function (Decoding CCTF) or
electro-optical transfer function (EOTF / EOCF).
Returns
-------
ndarray
Image pixel data.
"""
LOGGER.info('Reading "{0}" image.'.format(self._path))
self.data = read_image(str(self._path))
if decoding_cctf is not None:
self.data = decoding_cctf(self.data)
return self.data
[docs]class ImageStack(MutableSequence):
"""
Defines a convenient stack storing a sequence of images for HDRI / radiance
images generation.
Methods
-------
ImageStack
__init__
__getitem__
__setitem__
__delitem__
__len__
__getattr__
__setattr__
sort
insert
from_files
"""
[docs] def __init__(self):
self._list = []
[docs] def __getitem__(self, index):
"""
Reimplements the :meth:`MutableSequence.__getitem__` method.
Parameters
----------
index : int
Item index.
Returns
-------
Image
Item at given index.
"""
return self._list[index]
[docs] def __setitem__(self, index, value):
"""
Reimplements the :meth:`MutableSequence.__setitem__` method.
Parameters
----------
index : int
Item index.
value : int
Item value.
"""
self._list[index] = value
[docs] def __delitem__(self, index):
"""
Reimplements the :meth:`MutableSequence.__delitem__` method.
Parameters
----------
index : int
Item index.
"""
del self._list[index]
[docs] def __len__(self):
"""
Reimplements the :meth:`MutableSequence.__len__` method.
"""
return len(self._list)
[docs] def __getattr__(self, attribute):
"""
Reimplements the :meth:`MutableSequence.__getattr__` method.
Parameters
----------
attribute : unicode
Attribute to retrieve the value.
Returns
-------
object
Attribute value.
"""
try:
return self.__dict__[attribute]
except KeyError:
if hasattr(Image, attribute):
value = [getattr(image, attribute) for image in self]
if attribute == 'data':
return tstack(value)
else:
return tuple(value)
elif hasattr(Metadata, attribute):
value = [getattr(image.metadata, attribute) for image in self]
return as_float_array(value)
else:
raise AttributeError(
"'{0}' object has no attribute '{1}'".format(
self.__class__.__name__, attribute))
[docs] def __setattr__(self, attribute, value):
"""
Reimplements the :meth:`MutableSequence.__getattr__` method.
Parameters
----------
attribute : unicode
Attribute to set the value.
value : object
Value to set.
"""
if hasattr(Image, attribute):
if attribute == 'data':
data = tsplit(value)
for i, image in enumerate(self):
image.data = data[i]
else:
for i, image in enumerate(self):
setattr(image, attribute, value[i])
elif hasattr(Metadata, attribute):
for i, image in enumerate(self):
setattr(image.metadata, attribute, value[i])
else:
super(ImageStack, self).__setattr__(attribute, value)
[docs] def insert(self, index, value):
"""
Reimplements the :meth:`MutableSequence.insert` method.
Parameters
----------
index : int
Item index.
value : object
Item value.
"""
self._list.insert(index, value)
[docs] def sort(self, key=None):
"""
Sorts the underlying data structure.
Parameters
----------
key : callable
Function of one argument that is used to extract a comparison key
from each data structure.
"""
self._list = sorted(self._list, key=key)
[docs] @staticmethod
def from_files(image_files, decoding_cctf=None):
"""
Returns a :class:`colour_hdri.ImageStack` instance with given image
files.
Parameters
----------
image_files : array_like
Image files.
decoding_cctf : object, optional
Decoding colour component transfer function (Decoding CCTF) or
electro-optical transfer function (EOTF / EOCF).
Returns
-------
ImageStack
"""
image_stack = ImageStack()
for image_file in image_files:
image = Image(image_file)
image.read_data(decoding_cctf)
image.read_metadata()
image_stack.append(image)
def luminance_average_key(image):
"""
Comparison key function.
"""
f_number = image.metadata.f_number
exposure_time = image.metadata.exposure_time
iso = image.metadata.iso
if None in (f_number, exposure_time, iso):
warning('"{0}" exposure data is missing, average luminance '
'sorting is inapplicable!'.format(image.path))
return None
return 1 / average_luminance(f_number, exposure_time, iso)
image_stack.sort(luminance_average_key)
return image_stack