# coding=utf-8
"""A wrapper for a multi-dimensional image with an origin offset"""
from abc import abstractmethod
from PIL import Image
import numpy as np
# pylint: disable=super-with-arguments, unnecessary-pass
[docs]class ImageWrapperBase(object):
"""Multi-dimensional image array with an origin"""
def __init__(self, origin, image_size=None, image=None):
self.origin = origin
if image is not None:
self.size = image.get_size()
else:
self.size = image_size
self.image = image
[docs] def set_sub_image(self, sub_image):
"""Replaces part of the image with the corresponding subimage"""
if self.image is None:
self.image = ImageStorage.create_empty(
size=self.size,
dtype=sub_image.image.get_type())
# Transform the image to our local coordinate system
local_subimage = self.transform_sub_image(sub_image)
start, size = self.transform_coords(sub_image)
# Test if image is in bounds
start_indices = np.subtract(start, self.origin)
end_indices = np.add(start_indices, size)
if np.any(np.less(start_indices, 0)) \
or np.any(np.greater(end_indices, self.size)):
raise ValueError("Subimage is not contained within the main image")
# Set the part of the image to this subimage
selector = tuple([slice(s, e) for s, e in # pylint: disable=consider-using-generator
zip(start_indices, end_indices)])
self.image.set(selector, local_subimage)
[docs]class ImageWrapper(ImageWrapperBase):
"""Multi-dimensional image array with an origin"""
def __init__(self, origin, image_size=None, image=None):
super(ImageWrapper, self).__init__(origin=origin,
image_size=image_size,
image=image)
[docs] def get_sub_image(self, start, size):
"""Returns the corresponding subimage"""
start_indices = np.subtract(start, self.origin)
end_indices = np.add(start_indices, np.array(size))
if np.any(np.less(start_indices,
np.zeros_like(start_indices))) \
or np.any(np.greater(end_indices, self.size)):
raise ValueError("Subimage is not contained within the main image")
selector = tuple([slice(s, e) for s, e in # pylint: disable=consider-using-generator
zip(start_indices, end_indices)])
return ImageWrapper(origin=start, image=self.image.get(selector))
[docs]class SmartImage(ImageWrapper):
"""Image wrapper which converts between Axes"""
def __init__(self, start, size, image, transformer):
super(SmartImage, self).__init__(origin=start, image_size=size,
image=image)
self.size = size
self.origin = start
self.image = image
self._transformer = transformer
[docs] def coords_to_other(self, transformer):
"""Converts coordinates to another system"""
return self._transformer.to_other(self.origin, self.size, transformer)
[docs]class ImageStorage(object):
"""Abstraction for storing image data in an arbitrary orientation
Allows storing of data without assuming the order in which the dimensions
are stored in memory or indexed. This allows images to be indexed using
the [x,y,z] convention while the numpy ordering is actually [z,y,x]
"""
def __init__(self, numpy_image=None):
self._numpy_image = numpy_image
[docs] def set(self, selector, image):
"""Replaces part of the image data using the specified selectors"""
self._numpy_image[tuple(list(reversed(selector)))] = image.get_raw()
[docs] def get(self, selector):
"""Returns part of the image data using the specified selectors"""
return ImageStorage(self._numpy_image[tuple(list(reversed(selector)))])
[docs] def get_size(self):
"""Returns the image size in the global dimension ordering scheme"""
return list(reversed(list(np.shape(self._numpy_image))))
[docs] def get_type(self):
"""Returns the underlying data type"""
return self._numpy_image.dtype
[docs] def get_raw(self):
"""Return raw image array; might not be in global dimension ordering"""
return self._numpy_image
[docs] def get_image(self):
"""Creates and returns an Image object for this data"""
return Image.fromarray(np.transpose(self._numpy_image))
[docs] def get_raw_image(self):
"""Return raw image array in the standard PIL dimension ordering"""
transposed = np.transpose(self._numpy_image)
if np.ndim(transposed) == 3 and np.shape(transposed)[2] == 1:
return np.squeeze(transposed, 2)
return transposed
[docs] def transpose(self, order):
"""Return a transpose of the image data using global ordering"""
order = list(np.subtract(len(order) - 1, order))
return ImageStorage(np.transpose(self._numpy_image,
list(reversed(order))))
[docs] def flip(self, do_flip):
"""Return a copy of image data flipped using global ordering"""
image = self._numpy_image
for index, flip in enumerate(do_flip):
if flip:
image = np.flip(image, len(do_flip) - 1 - index)
return ImageStorage(image)
[docs] def reshape(self, new_shape):
"""Return a reshaping of the image data using global ordering"""
return ImageStorage(
np.reshape(self._numpy_image, list(reversed(new_shape))))
def __eq__(self, other):
"""Overrides the default implementation"""
if isinstance(other, self.__class__):
return np.array_equal(self._numpy_image, other.get_raw())
return False
def __ne__(self, other):
"""Overrides the default implementation"""
return not self.__eq__(other)
[docs] def copy(self):
"""Returns a new object with a copy of the underlying data"""
return ImageStorage(self._numpy_image.copy())
[docs] @classmethod
def create_empty(cls, size, dtype):
"""Create empty object of the specified global size and data type"""
raw = np.zeros(shape=list(reversed(size)), dtype=dtype)
return cls(numpy_image=raw)
[docs] @classmethod
def from_raw_image(cls, raw, size=None):
"""Create ImageStorage object from this image data array"""
if size and not np.array_equal(np.shape(raw),
size[:np.size(np.shape(raw))]):
raise ValueError('When loading an image, nonsingleton dimensions '
'must match size')
# Add in singleton dimensions if required
if size:
raw = np.reshape(raw, size)
return cls(np.transpose(raw))