"""Utility functions for data io."""
import numpy as np
import skimage.io
[docs]def imread(path, make_8bit=False, rescale=False,
rescale_min='auto', rescale_max='auto'):
"""Read in an image file and rescale pixel values (if applicable).
Note
----
Because overhead imagery is often either 16-bit or multispectral (i.e. >3
channels or bands that don't directly translate into the RGB scheme of
photographs), this package using scikit-image_ ``io`` algorithms. Though
slightly slower, these algorithms are compatible with any bit depth or
channel count.
.. _scikit-image: https://scikit-image.org
Arguments
---------
path : str
Path to the image file to load.
make_8bit : bool, optional
Should the image be converted to an 8-bit format? Defaults to False.
rescale : bool, optional
Should pixel intensities be rescaled? Defaults to no (False).
rescale_min : ``'auto'`` or :class:`int` or :class:`float` or :class:`list`
The minimum pixel value(s) for rescaling. If ``rescale=True`` but no
value is provided for `rescale_min`, the minimum pixel intensity in
each channel of the image will be subtracted such that the minimum
value becomes zero. If a single number is provided, that number will be
subtracted from each channel. If a list of values is provided that is
the same length as the number of channels, then those values will be
subtracted from the corresponding channels.
rescale_max : ``'auto'`` or :class:`int` or :class:`float` or :class:`list`
The max pixel value(s) for rescaling. If ``rescale=True`` but no
value is provided for `rescale_max`, each channel will be rescaled such
that the maximum value in the channel is set to the bit range's max.
If a single number is provided, that number will be set as the upper
limit for all channels. If a list of values is provided that is the
same length as the number of channels, then those values will be
set to the maximum value in the corresponding channels.
Returns
-------
im : :func:`numpy.array`
A NumPy array of shape ``[Y, X, C]`` containing the imagery, with dtype
``uint8``.
"""
im_arr = skimage.io.imread(path)
# check dtype for preprocessing
if im_arr.dtype == np.uint8:
dtype = 'uint8'
elif im_arr.dtype == np.uint16:
dtype = 'uint16'
elif im_arr.dtype in [np.float16, np.float32, np.float64]:
if np.amax(im_arr) <= 1 and np.amin(im_arr) >= 0:
dtype = 'zero-one normalized' # range = 0-1
elif np.amax(im_arr) > 0 and np.amin(im_arr) < 0:
dtype = 'z-scored'
elif np.amax(im_arr) <= 255:
dtype = '255 float'
elif np.amax(im_arr) <= 65535:
dtype = '65535 float'
else:
raise TypeError('The loaded image array is an unexpected dtype.')
else:
raise TypeError('The loaded image array is an unexpected dtype.')
if make_8bit:
im_arr = preprocess_im_arr(im_arr, dtype, rescale=rescale,
rescale_min=rescale_min,
rescale_max=rescale_max)
return im_arr
[docs]def preprocess_im_arr(im_arr, im_format, rescale=False,
rescale_min='auto', rescale_max='auto'):
"""Convert image to standard shape and dtype for use in the pipeline.
Notes
-----
This repo will require the following of images:
- Their shape is of form [X, Y, C]
- Input images are dtype ``uint8``
This function will take an image array `im_arr` and reshape it accordingly.
Arguments
---------
im_arr : :func:`numpy.array`
A numpy array representation of an image. `im_arr` should have either
two or three dimensions.
im_format : str
One of ``'uint8'``, ``'uint16'``, ``'z-scored'``,
``'zero-one normalized'``, ``'255 float'``, or ``'65535 float'``.
String indicating the dtype of the input, which will dictate the
preprocessing applied.
rescale : bool, optional
Should pixel intensities be rescaled? Defaults to no (False).
rescale_min : ``'auto'`` or :class:`int` or :class:`float` or :class:`list`
The minimum pixel value(s) for rescaling. If ``rescale=True`` but no
value is provided for `rescale_min`, the minimum pixel intensity in
each channel of the image will be subtracted such that the minimum
value becomes zero. If a single number is provided, that number will be
subtracted from each channel. If a list of values is provided that is
the same length as the number of channels, then those values will be
subtracted from the corresponding channels.
rescale_max : ``'auto'`` or :class:`int` or :class:`float` or :class:`list`
The max pixel value(s) for rescaling. If ``rescale=True`` but no
value is provided for `rescale_max`, each channel will be rescaled such
that the maximum value in the channel is set to the bit range's max.
If a single number is provided, that number will be set as the upper
limit for all channels. If a list of values is provided that is the
same length as the number of channels, then those values will be
set to the maximum value in the corresponding channels.
Returns
-------
A :func:`numpy.array` with shape ``[X, Y, C]`` and dtype ``uint8``.
"""
# get [Y, X, C] axis order set up
if im_arr.ndim not in [2, 3]:
raise ValueError('This package can only read two-dimensional'
'image data with an optional channel dimension.')
if im_arr.ndim == 2:
im_arr = im_arr[:, :, np.newaxis]
if im_arr.shape[0] < im_arr.shape[2]: # if the channel axis comes first
im_arr = np.moveaxis(im_arr, 0, -1) # move 0th axis tolast position
# rescale images (if applicable)
if rescale:
im_arr = rescale_arr(im_arr, im_format, rescale_min, rescale_max)
if im_format == 'uint8':
return im_arr.astype('uint8') # just to be sure
elif im_format == 'uint16':
im_arr = (im_arr.astype('float64')*255./65535.).astype('uint8')
elif im_format == 'z-scored':
im_arr = ((im_arr+1)*177.5).astype('uint8')
elif im_format == 'zero-one normalized':
im_arr = (im_arr*255).astype('uint8')
elif im_format == '255 float':
im_arr = im_arr.astype('uint8')
elif im_format == '65535 float':
# why are you using this format?
im_arr = (im_arr*255/65535).astype('uint8')
return im_arr
[docs]def scale_for_model(image, output_type=None):
"""Scale an image to a model's required parameters.
Arguments
---------
image : :class:`np.array`
The image array to be transformed to a desired output format.
output_type : str, optional
The data format of the output to pass into the model. There are five
possible values:
* ``'normalized'`` : values rescaled to 0-1.
* ``'zscored'`` : image converted to zero mean and unit stdev.
* ``'8bit'`` : image converted to 8-bit format.
* ``'16bit'`` : image converted to 16-bit format.
If no value is provided, no re-scaling is performed (input array is
returned directly).
"""
if output_type is None:
return image
elif output_type == 'normalized':
out_im = image/image.max()
return out_im
elif output_type == 'zscored':
return (image - np.mean(image))/np.std(image)
elif output_type == '8bit':
if image.max() > 255:
# assume it's 16-bit, rescale to 8-bit scale to min/max
out_im = 255.*image/65535
return out_im.astype('uint8')
elif image.max() <= 1:
out_im = 255.*image
return out_im.astype('uint8')
else:
return image.astype('uint8')
elif output_type == '16bit':
if (image.max() < 255) and (image.max() > 1):
# scale to min/max
out_im = 65535.*image/255
return out_im.astype('uint16')
elif image.max() <= 1:
out_im = 65535.*image
return out_im.astype('uint16')
else:
return image.astype('uint16')
else:
raise ValueError('output_type must be one of'
' "normalized", "zscored", "8bit", "16bit"')
[docs]def rescale_arr(im_arr, im_format, rescale_min='auto', rescale_max='auto'):
"""Rescale array values in a 3D image array with channel order [Y, X, C].
Arguments
---------
im_arr : :class:`numpy.array`
A numpy array representation of an image. `im_arr` should have either
two or three dimensions.
im_format : str
One of ``'uint8'``, ``'uint16'``, ``'z-scored'``,
``'zero-one normalized'``, ``'255 float'``, or ``'65535 float'``.
String indicating the dtype of the input, which will dictate the
preprocessing applied.
rescale_min : ``'auto'`` or :class:`int` or :class:`float` or :class:`list`
The minimum pixel value(s) for rescaling. If ``rescale=True`` but no
value is provided for `rescale_min`, the minimum pixel intensity in
each channel of the image will be subtracted such that the minimum
value becomes zero. If a single number is provided, that number will be
subtracted from each channel. If a list of values is provided that is
the same length as the number of channels, then those values will be
subtracted from the corresponding channels.
rescale_max : ``'auto'`` or :class:`int` or :class:`float` or :class:`list`
The max pixel value(s) for rescaling. If ``rescale=True`` but no
value is provided for `rescale_max`, each channel will be rescaled such
that the maximum value in the channel is set to the bit range's max.
If a single number is provided, that number will be set as the upper
limit for all channels. If a list of values is provided that is the
same length as the number of channels, then those values will be
set to the maximum value in the corresponding channels.
Returns
-------
normalized_arr : :class:`numpy.array`
"""
if isinstance(rescale_min, list):
if len(rescale_min) != im_arr.shape[2]: # if list len != channels
raise ValueError('The channel rescaling parameters must be '
'either a single value or a list of length '
'n_channels.')
else:
rescale_min = np.array(rescale_min)
elif isinstance(rescale_min, int) or isinstance(rescale_min, float):
rescale_min = np.array([rescale_min]*im_arr.shape[2])
elif rescale_min == 'auto':
rescale_min = np.amin(im_arr, axis=(0, 1))
if isinstance(rescale_max, list):
if len(rescale_max) != im_arr.shape[2]: # if list len != channels
raise ValueError('The channel rescaling parameters must be '
'either a single value or a list of length '
'n_channels.')
else:
rescale_max = np.array(rescale_max)
elif isinstance(rescale_max, int) or isinstance(rescale_max, float):
rescale_max = np.array([rescale_max]*im_arr.shape[2])
elif rescale_max == 'auto':
rescale_max = np.amax(im_arr, axis=(0, 1))
scale_factor = None
if im_format in ['uint8', '255 float']:
scale_factor = 255
elif im_format in ['uint16', '65535 float']:
scale_factor = 65535
elif im_format == 'zero-one normalized':
scale_factor = 1
# set all values above the scale max to the scale max, and all values
# below the scale min to the scale min
for channel in range(im_arr.shape[2]):
subarr = im_arr[:, :, channel]
subarr[subarr < rescale_min[channel]] = rescale_min[channel]
subarr[subarr > rescale_max[channel]] = rescale_max[channel]
im_arr[:, :, channel] = subarr
if scale_factor is not None:
im_arr = (im_arr-rescale_min)*(
scale_factor/(rescale_max-rescale_min))
return im_arr
def _check_channel_order(im_arr, framework):
im_shape = im_arr.shape
if len(im_shape) == 3: # doesn't matter for 1-channel images
if im_shape[0] > im_shape[2] and framework in ['torch', 'pytorch']:
# in [Y, X, C], needs to be in [C, Y, X]
im_arr = np.moveaxis(im_arr, 2, 0)
elif im_shape[2] > im_shape[0] and framework == 'keras':
# in [C, Y, X], needs to be in [Y, X, C]
im_arr = np.moveaxis(im_arr, 0, 2)
elif len(im_shape) == 4: # for a whole minibatch
if im_shape[1] > im_shape[3] and framework in ['torch', 'pytorch']:
# in [Y, X, C], needs to be in [C, Y, X]
im_arr = np.moveaxis(im_arr, 3, 1)
elif im_shape[3] > im_shape[1] and framework == 'keras':
# in [C, Y, X], needs to be in [Y, X, C]
im_arr = np.moveaxis(im_arr, 1, 3)
return im_arr