# Copyright 2018 United Kingdom Research and Innovation
# Copyright 2018 The University of Manchester
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Authors:
# CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt
import copy
import warnings
from numbers import Number
import numpy
from .image_data import ImageData
from .labels import ImageDimension, FillType
[docs]
class ImageGeometry:
@property
def CHANNEL(self):
warnings.warn("use ImageDimension.CHANNEL instead", DeprecationWarning, stacklevel=2)
return ImageDimension.CHANNEL
@property
def HORIZONTAL_X(self):
warnings.warn("use ImageDimension.HORIZONTAL_X instead", DeprecationWarning, stacklevel=2)
return ImageDimension.HORIZONTAL_X
@property
def HORIZONTAL_Y(self):
warnings.warn("use ImageDimension.HORIZONTAL_Y instead", DeprecationWarning, stacklevel=2)
return ImageDimension.HORIZONTAL_Y
@property
def RANDOM(self):
warnings.warn("use FillType.RANDOM instead", DeprecationWarning, stacklevel=2)
return FillType.RANDOM
@property
def RANDOM_INT(self):
warnings.warn("use FillType.RANDOM_INT instead", DeprecationWarning, stacklevel=2)
return FillType.RANDOM_INT
@property
def VERTICAL(self):
warnings.warn("use ImageDimension.VERTICAL instead", DeprecationWarning, stacklevel=2)
return ImageDimension.VERTICAL
@property
def shape(self):
shape_dict = {ImageDimension.CHANNEL: self.channels,
ImageDimension.VERTICAL: self.voxel_num_z,
ImageDimension.HORIZONTAL_Y: self.voxel_num_y,
ImageDimension.HORIZONTAL_X: self.voxel_num_x}
return tuple(shape_dict[label] for label in self.dimension_labels)
@property
def spacing(self):
spacing_dict = {ImageDimension.CHANNEL: self.channel_spacing,
ImageDimension.VERTICAL: self.voxel_size_z,
ImageDimension.HORIZONTAL_Y: self.voxel_size_y,
ImageDimension.HORIZONTAL_X: self.voxel_size_x}
return tuple(spacing_dict[label] for label in self.dimension_labels)
@property
def length(self):
return len(self.dimension_labels)
@property
def ndim(self):
return len(self.dimension_labels)
@property
def dimension_labels(self):
labels_default = ImageDimension.get_order_for_engine("cil")
shape_default = [ self.channels,
self.voxel_num_z,
self.voxel_num_y,
self.voxel_num_x]
try:
labels = self._dimension_labels
except AttributeError:
labels = labels_default
labels = list(labels)
for i, x in enumerate(shape_default):
if x == 0 or x==1:
try:
labels.remove(labels_default[i])
except ValueError:
pass #if not in custom list carry on
return tuple(labels)
@dimension_labels.setter
def dimension_labels(self, val):
self.set_labels(val)
def set_labels(self, labels):
if labels is not None:
self._dimension_labels = tuple(map(ImageDimension, labels))
def __eq__(self, other):
if not isinstance(other, self.__class__):
return False
if self.voxel_num_x == other.voxel_num_x \
and self.voxel_num_y == other.voxel_num_y \
and self.voxel_num_z == other.voxel_num_z \
and self.voxel_size_x == other.voxel_size_x \
and self.voxel_size_y == other.voxel_size_y \
and self.voxel_size_z == other.voxel_size_z \
and self.center_x == other.center_x \
and self.center_y == other.center_y \
and self.center_z == other.center_z \
and self.channels == other.channels \
and self.channel_spacing == other.channel_spacing \
and self.dimension_labels == other.dimension_labels:
return True
return False
@property
def dtype(self):
return self._dtype
@dtype.setter
def dtype(self, val):
self._dtype = val
def __init__(self,
voxel_num_x=0,
voxel_num_y=0,
voxel_num_z=0,
voxel_size_x=1,
voxel_size_y=1,
voxel_size_z=1,
center_x=0,
center_y=0,
center_z=0,
channels=1,
**kwargs):
self.voxel_num_x = int(voxel_num_x)
self.voxel_num_y = int(voxel_num_y)
self.voxel_num_z = int(voxel_num_z)
self.voxel_size_x = float(voxel_size_x)
self.voxel_size_y = float(voxel_size_y)
self.voxel_size_z = float(voxel_size_z)
self.center_x = center_x
self.center_y = center_y
self.center_z = center_z
self.channels = channels
self.channel_labels = None
self.channel_spacing = 1.0
self.dimension_labels = kwargs.get('dimension_labels', None)
self.dtype = kwargs.get('dtype', numpy.float32)
[docs]
def get_slice(self,channel=None, vertical=None, horizontal_x=None, horizontal_y=None):
'''
Returns a new ImageGeometry of a single slice of in the requested direction.
'''
geometry_new = self.copy()
if channel is not None:
geometry_new.channels = 1
try:
geometry_new.channel_labels = [self.channel_labels[channel]]
except:
geometry_new.channel_labels = None
if vertical is not None:
geometry_new.voxel_num_z = 0
if horizontal_y is not None:
geometry_new.voxel_num_y = 0
if horizontal_x is not None:
geometry_new.voxel_num_x = 0
return geometry_new
def get_order_by_label(self, dimension_labels, default_dimension_labels):
order = []
for i, el in enumerate(default_dimension_labels):
for j, ek in enumerate(dimension_labels):
if el == ek:
order.append(j)
break
return order
def get_min_x(self):
return self.center_x - 0.5*self.voxel_num_x*self.voxel_size_x
def get_max_x(self):
return self.center_x + 0.5*self.voxel_num_x*self.voxel_size_x
def get_min_y(self):
return self.center_y - 0.5*self.voxel_num_y*self.voxel_size_y
def get_max_y(self):
return self.center_y + 0.5*self.voxel_num_y*self.voxel_size_y
def get_min_z(self):
if not self.voxel_num_z == 0:
return self.center_z - 0.5*self.voxel_num_z*self.voxel_size_z
else:
return 0
def get_max_z(self):
if not self.voxel_num_z == 0:
return self.center_z + 0.5*self.voxel_num_z*self.voxel_size_z
else:
return 0
[docs]
def clone(self):
'''returns a copy of the ImageGeometry'''
return copy.deepcopy(self)
[docs]
def copy(self):
'''alias of clone'''
return self.clone()
def __str__ (self):
repres = ""
repres += "Number of channels: {0}\n".format(self.channels)
repres += "channel_spacing: {0}\n".format(self.channel_spacing)
if self.voxel_num_z > 0:
repres += "voxel_num : x{0},y{1},z{2}\n".format(self.voxel_num_x, self.voxel_num_y, self.voxel_num_z)
repres += "voxel_size : x{0},y{1},z{2}\n".format(self.voxel_size_x, self.voxel_size_y, self.voxel_size_z)
repres += "center : x{0},y{1},z{2}\n".format(self.center_x, self.center_y, self.center_z)
else:
repres += "voxel_num : x{0},y{1}\n".format(self.voxel_num_x, self.voxel_num_y)
repres += "voxel_size : x{0},y{1}\n".format(self.voxel_size_x, self.voxel_size_y)
repres += "center : x{0},y{1}\n".format(self.center_x, self.center_y)
return repres
[docs]
def allocate(self, value=0, **kwargs):
'''Allocates an ImageData according to the geometry
Parameters
----------
value : number or string, default=0
The value to allocate. Accepts a number to allocate a uniform array,
None to allocate an empty memory block, or a string to create a random
array: 'random' allocates floats between 0 and 1, 'random_int' by default
allocates integers between 0 and 100 or between provided `min_value` and
`max_value`
**kwargs:
dtype : numpy data type, optional
The data type to allocate if different from the geometry data type.
Default None allocates an array with the geometry data type.
seed : int, optional
A random seed to fix reproducibility, only used if `value` is a random
method. Default is `None`.
min_value : int, optional
The minimum value random integer to generate, only used if `value`
is 'random_int'. New since version 25.0.0. Default is 0.
max_value : int, optional
The maximum value random integer to generate, only used if `value`
is 'random_int'. Default is 100.
Note
----
Since v25.0.0 the methods used by 'random' or 'random_int' use `numpy.random.default_rng`.
This method does not use the global numpy.random.seed() so if a seed is
required it should be passed directly as a kwarg.
To allocate random numbers using the earlier behaviour use `value='random_deprecated'`
or `value='random_int_deprecated'`
'''
dtype = kwargs.pop('dtype', self.dtype)
out = ImageData(geometry=self.copy(), dtype=dtype)
if value is not None:
out.fill(value, **kwargs)
return out