# -*- coding: utf-8 -*-
__author__ = "Ngoc Huynh Bao"
__email__ = "ngoc.huynh.bao@nmbu.no"
import numpy as np
from deoxys_image import normalize, apply_affine_transform, apply_flip
from deoxys_image import ImageAugmentation
from tensorflow.keras.preprocessing.image import ImageDataGenerator
from ..utils import Singleton
[docs]class BasePreprocessor:
def __init__(self, **kwargs):
"""
Wrapper for creating a trained/fitted Preprocessor instance
"""
pass
[docs]class DummyPreprocessor:
def __init__(self):
pass
[docs]class SingleChannelPreprocessor(BasePreprocessor):
"""Make single channel images have one axis for the channel
"""
[docs]class WindowingPreprocessor(BasePreprocessor):
"""Set the range of the images
Parameters
----------
window_center : int, float
the center of the range
window_width : int, float
the range
channel : int
the index of the channel to apply windowing
"""
def __init__(self, window_center, window_width, channel):
self.window_center, self.window_width = window_center, window_width
self.channel = channel
[docs]class HounsfieldWindowingPreprocessor(WindowingPreprocessor):
"""Set the range of the images
Parameters
----------
window_center : int, float
the center of the range
window_width : int, float
the range
channel : int
the index of the channel to apply windowing
hounsfield_offset: int, float
the Hounsfield offset
"""
def __init__(self, window_center, window_width, channel,
hounsfield_offset=1024):
super().__init__(
window_center+hounsfield_offset, window_width, channel)
[docs]class ImageNormalizerPreprocessor(BasePreprocessor):
"""
Normalize all channels to the range of the close interval [0, 1]
Parameters
----------
vmin : int, float, list, tuple, optional.
If an int or a float, it will be the lower limits in all channels,
else it should be a list of lower values associated with all axes.
By default None (choosing the minimum value of
each channel in the image batch)
vmax : int, float, list, tuple, optional
If an int or a float, it will be the upper limits in all channels,
else it should be a list of upper values associated with all axes.
By default None (choosing the maximum value of
each channel in the image batch)
"""
def __init__(self, vmin=None, vmax=None):
self.vmin = vmin
self.vmax = vmax
[docs]class ChannelRemoval(BasePreprocessor):
"""Remove one or more channels from the images
Parameters
----------
channel : int, list, tuple, optional
the index of the channel to be removed, by default 1
"""
def __init__(self, channel=1):
self.channel = channel
[docs]class ChannelSelector(BasePreprocessor):
"""Select / filter one or more channels from the images
Parameters
----------
channel : int, list, tuple, optional
the index of the channel to be selected, by default 0
"""
def __init__(self, channel=0):
if '__iter__' not in dir(channel):
self.channel = [channel]
else:
self.channel = channel
[docs]class UnetPaddingPreprocessor(BasePreprocessor):
"""
Pad the images so that their sizes would not change after going
through a standard Unet model.
Parameters
----------
depth : int, optional
number of maxpooling layer in the Unet, by default 4
mode : str, optional
way to fill the values in the paddings,
currently only support 'auto' (pad with 0)
"""
def __init__(self, depth=4, mode='auto'):
self.depth = depth
self.mode = mode
[docs]class ImageAugmentation2D(BasePreprocessor):
r"""
Apply transformation in 2d image (and mask label) for augmentation.
Check `ImageAugmentation3D` for augmentation on 3d images
Parameters
----------
rotation_range : int, optional
range of the angle rotation, in degree, by default 0 (no rotation)
rotation_chance : float, optional
probability to apply rotation transformation to an image,
by default 0.2
zoom_range : float, list, tuple optional
the range of zooming, zooming in when the number is less than 1,
and zoom out when the number if larger than 1.
If a `float`, then it is the range between that number and 1,
by default 1 (no zooming)
zoom_chance : float, optional
probability to apply zoom transformation to an image,
by default 0.2
shift_range : tuple or list, optional
the range of translation in each axis, by default None (no shifts)
shift_chance : float, optional
probability to apply translation transformation to an image,
by default 0.1
flip_axis : int, tuple, list, optional
flip by one or more axis (in the single image) with a probability
of 0.5, by default None (no flipping).
`flip_axis=0` means the image will be flipped vertically, while
`flip_axis=1` means the image will be flipped horizontally.
brightness_range : float, tuple, list, optional
range of the brightness portion,
based on the max intensity value of each channel.
For example, when the max intensity value of one channel is 1.0,
and the brightness is chaned by 1.2, then every pixel in that
channel will increase the intensity value by 0.2.
.. math:: 0.2 = 1.0 \cdot (1.2 - 1)
By default 1 (no changes in brightness)
brightness_channel : int, tuple, list, optional
the channel(s) to apply changes in brightness,
by default None (apply to all channels)
brightness_chance : float, optional
probability to apply brightness change transform to an image,
by default 0.1
contrast_range : float, tuple, list, optional
range of the contrast portion,
(the histogram range is scaled up or down).
By default 1 (no changes in contrast)
contrast_channel : int, tuple, list, optional
the channel(s) to apply changes in contrast,
by default None (apply to all channels)
contrast_chance : float, optional
probability to apply contrast change transform to an image,
by default 0.1
noise_variance : float, tuple, list, optional
range of the noise variance
when adding Gaussian noise to the image,
by default 0 (no adding noise)
noise_channel : int, tuple, list, optional
the channel(s) to apply Gaussian noise,
by default None (apply to all channels)
noise_chance : float, optional
probability to apply gaussian noise to an image,
by default 0.1
blur_range : int, tuple, list, optional
range of the blur sigma
when applying the Gaussian filter to the image,
by default 0 (no blur)
blur_channel :int, tuple, list, optional
the channel(s) to apply Gaussian blur,
by default None (apply to all channels)
blur_chance : float, optional
probability to apply gaussian blur to an image,
by default 0.1
fill_mode : str, optional
the fill mode in affine transformation
(rotation, zooming, shifting / translation),
one of {'reflect', 'constant', 'nearest', 'mirror', 'wrap'},
by default 'constant'
cval : int, optional
When rotation, or zooming, or shifting is applied to the image,
`cval` is the value to fill past edges of input
if `fill_mode` is 'constant'.
By default 0
"""
_RANK = 3
_ROTATION_AXIS = 2
def __init__(self, rotation_range=0, rotation_chance=0.2,
zoom_range=1, zoom_chance=0.2,
shift_range=None, shift_chance=0.1,
flip_axis=None,
brightness_range=1, brightness_channel=None,
brightness_chance=0.1,
contrast_range=1, contrast_channel=None,
contrast_chance=0.1,
noise_variance=0, noise_channel=None,
noise_chance=0.1,
blur_range=0, blur_channel=None, blur_chance=0.1,
fill_mode='constant', cval=0):
self.augmentation_obj = ImageAugmentation(
self._RANK,
rotation_range, self._ROTATION_AXIS, rotation_chance,
zoom_range, zoom_chance,
shift_range, shift_chance,
flip_axis,
brightness_range, brightness_channel,
brightness_chance,
contrast_range, contrast_channel,
contrast_chance,
noise_variance, noise_channel,
noise_chance,
blur_range, blur_channel, blur_chance,
fill_mode, cval
)
[docs]class ImageAugmentation3D(ImageAugmentation2D):
r"""
Apply transformation in 3d image (and mask label) for augmentation
Parameters
----------
rotation_range : int, optional
range of the angle rotation, in degree, by default 0 (no rotation)
rotation_axis : int, optional
the axis of one image to apply rotation,
by default 0
rotation_chance : float, optional
probability to apply rotation transformation to an image,
by default 0.2
zoom_range : float, list, tuple optional
the range of zooming, zooming in when the number is less than 1,
and zoom out when the number if larger than 1.
If a `float`, then it is the range between that number and 1,
by default 1 (no zooming)
zoom_chance : float, optional
probability to apply zoom transformation to an image,
by default 0.2
shift_range : tuple or list, optional
the range of translation in each axis, by default None (no shifts)
shift_chance : float, optional
probability to apply translation transformation to an image,
by default 0.1
flip_axis : int, tuple, list, optional
flip by one or more axis (in the single image),
by default None (no flipping)
brightness_range : float, tuple, list, optional
range of the brightness portion,
based on the max intensity value of each channel.
For example, when the max intensity value of one channel is 1.0,
and the brightness is chaned by 1.2, then every pixel in that
channel will increase the intensity value by 0.2.
.. math:: 0.2 = 1.0 \cdot (1.2 - 1)
By default 1 (no changes in brightness)
brightness_channel : int, tuple, list, optional
the channel(s) to apply changes in brightness,
by default None (apply to all channels)
brightness_chance : float, optional
probability to apply brightness change transform to an image,
by default 0.1
contrast_range : float, tuple, list, optional
range of the contrast portion,
(the histogram range is scaled up or down).
By default 1 (no changes in contrast)
contrast_channel : int, tuple, list, optional
the channel(s) to apply changes in contrast,
by default None (apply to all channels)
contrast_chance : float, optional
probability to apply contrast change transform to an image,
by default 0.1
noise_variance : float, tuple, list, optional
range of the noise variance
when adding Gaussian noise to the image,
by default 0 (no adding noise)
noise_channel : int, tuple, list, optional
the channel(s) to apply Gaussian noise,
by default None (apply to all channels)
noise_chance : float, optional
probability to apply gaussian noise to an image,
by default 0.1
blur_range : int, tuple, list, optional
range of the blur sigma
when applying the Gaussian filter to the image,
by default 0 (no blur)
blur_channel :int, tuple, list, optional
the channel(s) to apply Gaussian blur,
by default None (apply to all channels)
blur_chance : float, optional
probability to apply gaussian blur to an image,
by default 0.1
fill_mode : str, optional
the fill mode in affine transformation
(rotation, zooming, shifting / translation),
one of {'reflect', 'constant', 'nearest', 'mirror', 'wrap'},
by default 'constant'
cval : int, optional
When rotation, or zooming, or shifting is applied to the image,
`cval` is the value to fill past edges of input
if `fill_mode` is 'constant'.
By default 0
"""
_RANK = 4
def __init__(self, rotation_range=0, rotation_axis=0, rotation_chance=0.2,
zoom_range=1, zoom_chance=0.2,
shift_range=None, shift_chance=0.1,
flip_axis=None,
brightness_range=1, brightness_channel=None,
brightness_chance=0.1,
contrast_range=1, contrast_channel=None,
contrast_chance=0.1,
noise_variance=0, noise_channel=None,
noise_chance=0.1,
blur_range=0, blur_channel=None, blur_chance=0.1,
fill_mode='constant', cval=0):
self.augmentation_obj = ImageAugmentation(
self._RANK,
rotation_range, rotation_axis, rotation_chance,
zoom_range, zoom_chance,
shift_range, shift_chance,
flip_axis,
brightness_range, brightness_channel,
brightness_chance,
contrast_range, contrast_channel,
contrast_chance,
noise_variance, noise_channel,
noise_chance,
blur_range, blur_channel, blur_chance,
fill_mode, cval
)
[docs]class ClassificationImageAugmentation2D(ImageAugmentation2D):
r"""
Apply transformation in 2d input images for augmentation
in classification.
Check `ClassificationImageAugmentation3D` for augmentation on 3d images
Parameters
----------
rotation_range : int, optional
range of the angle rotation, in degree, by default 0 (no rotation)
rotation_chance : float, optional
probability to apply rotation transformation to an image,
by default 0.2
zoom_range : float, list, tuple optional
the range of zooming, zooming in when the number is less than 1,
and zoom out when the number if larger than 1.
If a `float`, then it is the range between that number and 1,
by default 1 (no zooming)
zoom_chance : float, optional
probability to apply zoom transformation to an image,
by default 0.2
shift_range : tuple or list, optional
the range of translation in each axis, by default None (no shifts)
shift_chance : float, optional
probability to apply translation transformation to an image,
by default 0.1
flip_axis : int, tuple, list, optional
flip by one or more axis (in the single image) with a probability
of 0.5, by default None (no flipping).
`flip_axis=0` means the image will be flipped vertically, while
`flip_axis=1` means the image will be flipped horizontally.
brightness_range : float, tuple, list, optional
range of the brightness portion,
based on the max intensity value of each channel.
For example, when the max intensity value of one channel is 1.0,
and the brightness is chaned by 1.2, then every pixel in that
channel will increase the intensity value by 0.2.
.. math:: 0.2 = 1.0 \cdot (1.2 - 1)
By default 1 (no changes in brightness)
brightness_channel : int, tuple, list, optional
the channel(s) to apply changes in brightness,
by default None (apply to all channels)
brightness_chance : float, optional
probability to apply brightness change transform to an image,
by default 0.1
contrast_range : float, tuple, list, optional
range of the contrast portion,
(the histogram range is scaled up or down).
By default 1 (no changes in contrast)
contrast_channel : int, tuple, list, optional
the channel(s) to apply changes in contrast,
by default None (apply to all channels)
contrast_chance : float, optional
probability to apply contrast change transform to an image,
by default 0.1
noise_variance : float, tuple, list, optional
range of the noise variance
when adding Gaussian noise to the image,
by default 0 (no adding noise)
noise_channel : int, tuple, list, optional
the channel(s) to apply Gaussian noise,
by default None (apply to all channels)
noise_chance : float, optional
probability to apply gaussian noise to an image,
by default 0.1
blur_range : int, tuple, list, optional
range of the blur sigma
when applying the Gaussian filter to the image,
by default 0 (no blur)
blur_channel :int, tuple, list, optional
the channel(s) to apply Gaussian blur,
by default None (apply to all channels)
blur_chance : float, optional
probability to apply gaussian blur to an image,
by default 0.1
fill_mode : str, optional
the fill mode in affine transformation
(rotation, zooming, shifting / translation),
one of {'reflect', 'constant', 'nearest', 'mirror', 'wrap'},
by default 'constant'
cval : int, optional
When rotation, or zooming, or shifting is applied to the image,
`cval` is the value to fill past edges of input
if `fill_mode` is 'constant'.
By default 0
"""
[docs]class ClassificationImageAugmentation3D(ImageAugmentation3D):
r"""
Apply transformation in 3d input images for augmentation
in classification.
Parameters
----------
rotation_range : int, optional
range of the angle rotation, in degree, by default 0 (no rotation)
rotation_chance : float, optional
probability to apply rotation transformation to an image,
by default 0.2
zoom_range : float, list, tuple optional
the range of zooming, zooming in when the number is less than 1,
and zoom out when the number if larger than 1.
If a `float`, then it is the range between that number and 1,
by default 1 (no zooming)
zoom_chance : float, optional
probability to apply zoom transformation to an image,
by default 0.2
shift_range : tuple or list, optional
the range of translation in each axis, by default None (no shifts)
shift_chance : float, optional
probability to apply translation transformation to an image,
by default 0.1
flip_axis : int, tuple, list, optional
flip by one or more axis (in the single image) with a probability
of 0.5, by default None (no flipping).
`flip_axis=0` means the image will be flipped vertically, while
`flip_axis=1` means the image will be flipped horizontally.
brightness_range : float, tuple, list, optional
range of the brightness portion,
based on the max intensity value of each channel.
For example, when the max intensity value of one channel is 1.0,
and the brightness is chaned by 1.2, then every pixel in that
channel will increase the intensity value by 0.2.
.. math:: 0.2 = 1.0 \cdot (1.2 - 1)
By default 1 (no changes in brightness)
brightness_channel : int, tuple, list, optional
the channel(s) to apply changes in brightness,
by default None (apply to all channels)
brightness_chance : float, optional
probability to apply brightness change transform to an image,
by default 0.1
contrast_range : float, tuple, list, optional
range of the contrast portion,
(the histogram range is scaled up or down).
By default 1 (no changes in contrast)
contrast_channel : int, tuple, list, optional
the channel(s) to apply changes in contrast,
by default None (apply to all channels)
contrast_chance : float, optional
probability to apply contrast change transform to an image,
by default 0.1
noise_variance : float, tuple, list, optional
range of the noise variance
when adding Gaussian noise to the image,
by default 0 (no adding noise)
noise_channel : int, tuple, list, optional
the channel(s) to apply Gaussian noise,
by default None (apply to all channels)
noise_chance : float, optional
probability to apply gaussian noise to an image,
by default 0.1
blur_range : int, tuple, list, optional
range of the blur sigma
when applying the Gaussian filter to the image,
by default 0 (no blur)
blur_channel :int, tuple, list, optional
the channel(s) to apply Gaussian blur,
by default None (apply to all channels)
blur_chance : float, optional
probability to apply gaussian blur to an image,
by default 0.1
fill_mode : str, optional
the fill mode in affine transformation
(rotation, zooming, shifting / translation),
one of {'reflect', 'constant', 'nearest', 'mirror', 'wrap'},
by default 'constant'
cval : int, optional
When rotation, or zooming, or shifting is applied to the image,
`cval` is the value to fill past edges of input
if `fill_mode` is 'constant'.
By default 0
"""
[docs]class KerasImagePreprocessorX(BasePreprocessor):
"""Apply keras image augmentation to the input images
"""
def __init__(self,
shuffle=False,
featurewise_center=False,
samplewise_center=False,
featurewise_std_normalization=False,
samplewise_std_normalization=False,
zca_whitening=False, zca_epsilon=1e-06,
rotation_range=0, width_shift_range=0.0,
height_shift_range=0.0,
brightness_range=None, shear_range=0.0, zoom_range=0.0,
channel_shift_range=0.0, fill_mode='nearest', cval=0.0,
horizontal_flip=False, vertical_flip=False, rescale=None,
scale_down=None,
preprocessing_function=None, data_format='channels_last',
interpolation_order=1, dtype='float32'):
self.shuffle = shuffle
if scale_down and (rescale is None):
rescale = 1 / scale_down
self.preprocessor = ImageDataGenerator(
featurewise_center=featurewise_center,
samplewise_center=samplewise_center,
featurewise_std_normalization=featurewise_std_normalization,
samplewise_std_normalization=samplewise_std_normalization,
zca_whitening=zca_whitening, zca_epsilon=zca_epsilon,
rotation_range=rotation_range, width_shift_range=width_shift_range,
height_shift_range=height_shift_range,
brightness_range=brightness_range, shear_range=shear_range,
zoom_range=zoom_range,
channel_shift_range=channel_shift_range, fill_mode=fill_mode,
cval=cval,
horizontal_flip=horizontal_flip, vertical_flip=vertical_flip,
rescale=rescale,
preprocessing_function=preprocessing_function,
data_format=data_format, dtype=dtype)
[docs]class KerasImagePreprocessorY(BasePreprocessor):
def __init__(self,
shuffle=True,
featurewise_center=False,
samplewise_center=False,
featurewise_std_normalization=False,
samplewise_std_normalization=False,
zca_whitening=False, zca_epsilon=1e-06,
rotation_range=0, width_shift_range=0.0,
height_shift_range=0.0,
brightness_range=None, shear_range=0.0, zoom_range=0.0,
channel_shift_range=0.0, fill_mode='nearest', cval=0.0,
horizontal_flip=False, vertical_flip=False, rescale=None,
scale_down=None,
preprocessing_function=None, data_format='channels_last',
dtype='float32'):
self.shuffle = shuffle
if scale_down and (rescale is None):
rescale = 1 / scale_down
self.preprocessor = ImageDataGenerator(
featurewise_center=featurewise_center,
samplewise_center=samplewise_center,
featurewise_std_normalization=featurewise_std_normalization,
samplewise_std_normalization=samplewise_std_normalization,
zca_whitening=zca_whitening, zca_epsilon=zca_epsilon,
rotation_range=rotation_range, width_shift_range=width_shift_range,
height_shift_range=height_shift_range,
brightness_range=brightness_range, shear_range=shear_range,
zoom_range=zoom_range,
channel_shift_range=channel_shift_range, fill_mode=fill_mode,
cval=cval,
horizontal_flip=horizontal_flip, vertical_flip=vertical_flip,
rescale=rescale,
preprocessing_function=preprocessing_function,
data_format=data_format,
dtype=dtype)
[docs]class Preprocessors(metaclass=Singleton):
"""
A singleton that contains all the registered customized preprocessors
"""
def __init__(self):
self._preprocessors = {
'WindowingPreprocessor': WindowingPreprocessor,
'HounsfieldWindowingPreprocessor': HounsfieldWindowingPreprocessor,
'ImageNormalizerPreprocessor': ImageNormalizerPreprocessor,
'UnetPaddingPreprocessor': UnetPaddingPreprocessor,
'ChannelSelector': ChannelSelector,
'ChannelRemoval': ChannelRemoval,
'ImageAffineTransformPreprocessor':
ImageAffineTransformPreprocessor,
'ImageAugmentation2D': ImageAugmentation2D,
'ImageAugmentation3D': ImageAugmentation3D,
'ClassificationImageAugmentation2D':
ClassificationImageAugmentation2D,
'ClassificationImageAugmentation3D':
ClassificationImageAugmentation3D,
'ClassImageAugmentation2D':
ClassificationImageAugmentation2D,
'ClassImageAugmentation3D':
ClassificationImageAugmentation3D,
'SingleChannelPreprocessor': SingleChannelPreprocessor,
'KerasImagePreprocessorX': KerasImagePreprocessorX,
'KerasImagePreprocessorY': KerasImagePreprocessorY
}
[docs] def register(self, key, preprocessor):
if not issubclass(preprocessor, BasePreprocessor):
raise ValueError(
"The customized preprocessor has to be a subclass"
+ " of deoxys.data.BasePreprocessor"
)
if key in self._preprocessors:
raise KeyError(
"Duplicated key, please use another key for this preprocessor"
)
else:
self._preprocessors[key] = preprocessor
[docs] def unregister(self, key):
if key in self._preprocessors:
del self._preprocessors[key]
@property
def preprocessors(self):
return self._preprocessors
[docs]def register_preprocessor(key, preprocessor):
"""
Register the customized preprocessor.
If the key name is already registered, it will raise a KeyError exception
Parameters
----------
key : str
The unique key-name of the preprocessor
preprocessor : deoxys.data.BasePreprocessor
The customized preprocessor class
"""
Preprocessors().register(key, preprocessor)
[docs]def unregister_preprocessor(key):
"""
Remove the registered preprocessor with the key-name
Parameters
----------
key : str
The key-name of the preprocessor to be removed
"""
Preprocessors().unregister(key)
def _deserialize(config, custom_objects={}):
predefined_obj = {
'DummyPreprocessor': DummyPreprocessor
}
predefined_obj.update(custom_objects)
return predefined_obj[config['class_name']](**config['config'])
[docs]def preprocessor_from_config(config):
if 'class_name' not in config:
raise ValueError('class_name is needed to define preprocessor')
if 'config' not in config:
# auto add empty config for preprocessor with only class_name
config['config'] = {}
return _deserialize(config, custom_objects=Preprocessors().preprocessors)