"""Module of image adjustment functions
This module contains an assortment of functions for use in adjusting the
specifics of images. This includes noise, domain expansion/contraction,
shifts, etc.
"""
# This library was developed for the Georgia Tech graduate course ECE 6258:
# Digital Image Processing with Professor Ghassan AlRegib.
# For comments and feedback, please email dippykit[at]gmail.com
# Internal imports
from . import _utils
from . import image_io
# Functional imports
import numpy as np
from skimage.util import random_noise
# General imports
import warnings
__author__ = 'Brighton Ancelin, Motaz Alfarraj, Ghassan AlRegib'
__all__ = ['image_noise', 'image_adjust', 'image_translate', 'image_shift']
[docs]def image_noise(
im: np.ndarray,
mode: str='gaussian',
**kwargs
) -> np.ndarray:
"""Adds random noise to an image
Adds random noise to an input image. For images with an integer dtype,
the returned image will retain the original image's dtype. In all other
cases, the returned image will have a float dtype and be within the
normalized range from 0 to 1.
This function is essentially a wrapper for `skimage.util.random_noise`_,
so more detailed documentation may be found there.
:type im: ``numpy.ndarray``
:param im: The original image.
:type mode: ``str``
:param mode: (default='gaussian') The type of noise to add to the image.
May be one of the following: *'gaussian', 'localvar', 'poisson',
'salt', 'pepper', 's&p', 'speckle'*. Passed as the mode argument to
`skimage.util.random_noise`_.
:param kwargs: See below.
:rtype: ``numpy.ndarray``
:return: The noisy image.
:Keyword Arguments:
* **mean** (``int`` or ``float``) --
The mean of the randomly distributed noise. This value's meaning
is dependent on the dtype of the image (e.g. mean=127 for an image
with dtype=uint8 is analogous to mean=(127 / 255) for an image with
dtype=float).
* **var** (``int`` or ``float``) --
The variance of the randomly distributed noise. This value's
meaning is dependent on the dtype of the image (e.g. var=(0.01 * (
255 ** 2)) for an image with dtype=uint8 is analogous to var=0.01
for an image with dtype=float).
* **local_vars** (``numpy.ndarray``) --
The array of local variances for the randomly distributed noise.
This value's meaning is dependent on the dtype of the image. Each
element in the array is treated in the same way the var keyword
argument would be.
* A full list of keyword arguments along with descriptions can be
found at `skimage.util.random_noise`_.
.. note::
This function wraps around functions from other packages. Reading
these functions' documentations may be useful. See the **See also**
section for more information.
.. seealso::
`skimage.util.random_noise`_
Documentation of the random_noise function from Scikit Image
.. _skimage.util.random_noise: http://scikit-image.org/docs/dev/api/
skimage.util.html#skimage.util.random_noise
Examples:
>>> import numpy as np
>>> im = np.random.randint(0, 256, size=(8, 8), dtype=np.uint8)
>>> im
array([[238, 208, 243, 228, 198, 186, 72, 181],
[ 92, 247, 98, 197, 203, 210, 219, 175],
[124, 66, 231, 193, 154, 153, 136, 59],
[218, 162, 65, 196, 181, 160, 13, 148],
[164, 26, 150, 202, 55, 138, 40, 116],
[ 46, 151, 97, 78, 169, 249, 181, 167],
[ 79, 72, 31, 98, 152, 80, 220, 152],
[ 19, 233, 111, 126, 176, 68, 40, 49]], dtype=uint8)
>>> image_noise(im)
array([[246, 220, 211, 216, 216, 157, 51, 176],
[ 79, 230, 68, 217, 211, 175, 244, 180],
[ 86, 75, 184, 178, 123, 183, 106, 61],
[201, 154, 73, 193, 163, 180, 33, 180],
[192, 19, 144, 197, 64, 89, 36, 99],
[ 58, 182, 81, 53, 154, 255, 193, 145],
[ 60, 62, 55, 122, 132, 97, 244, 167],
[ 33, 181, 132, 163, 190, 9, 41, 45]], dtype=uint8)
"""
try:
im_float = image_io.im_to_float(im)
success = True
dtype_max_val = np.iinfo(im.dtype).max
if 'mean' in kwargs:
kwargs['mean'] /= dtype_max_val
if 'var' in kwargs:
kwargs['var'] /= (dtype_max_val ** 2)
if 'local_vars' in kwargs:
kwargs['local_vars'] /= (dtype_max_val ** 2)
except ValueError:
im_float = im
success = False
if np.any(0 > im) or np.any(1 < im):
warnings.warn('Image passed to image_noise() was not able to be '
'converted to a normalized floating point range. This '
'is most likely due to having the image in a '
'non-integer dtype. Returned noisy image will have a '
'normalized floating point range.')
if success:
return image_io.float_to_im(random_noise(im_float, mode=mode,
**kwargs), np.iinfo(im.dtype).bits)
else:
return random_noise(im_float, mode=mode, **kwargs)
[docs]def image_adjust(
im: np.ndarray,
lower_in: _utils.NumericType=None,
upper_in: _utils.NumericType=None,
lower_out: _utils.NumericType=None,
upper_out: _utils.NumericType=None,
keep_dtype: bool=True,
) -> np.ndarray:
"""Adjusts a given image by clipping and scaling its range
Given an image and parameters, this function clips the values in the
image to within a specific range and subsequently scales the image to a
new range.
:type im: ``numpy.ndarray``
:param im: The original image.
:type lower_in: ``NumericType``
:param lower_in: The lower clipping bound for the input image. All
values less than this will be set to this in the image before
scaling. By default, this value will be set to 1st percentile of the
image.
:type upper_in: ``NumericType``
:param upper_in: The upper clipping bound for the input image. All
values greater than this will be set to this in the image before
scaling. By default, this value will be set to 99th percentile of the
image.
:type lower_out: ``NumericType``
:param lower_out: The lower bound for the range of the scaled output
image. By default, this value is the minimum value of the dtype for
integer dtypes or 0.0 for float dtypes.
:type upper_out: ``NumericType``
:param upper_out: The upper bound for the range of the scaled output
image. By default, this value is the maximum value of the dtype for
integer dtypes or 1.0 for float dtypes.
:type keep_dtype: ``bool``
:param keep_dtype: (default=True) Whether or not to cast the output
image back to the dtype of the input image
:rtype: ``numpy.ndarray``
:return: The adjusted image.
Examples:
>>> import numpy as np
>>> im = np.array([[ 0, 32, 64],
... [ 96, 128, 160],
... [192, 224, 255]], dtype=np.uint8)
>>> image_adjust(im, 64, 192)
array([[ 0, 0, 0],
[ 63, 127, 191],
[255, 255, 255]], dtype=uint8)
>>> image_adjust(im, 64, 192, 64, 192)
array([[ 64, 64, 64],
[ 96, 128, 160],
[192, 192, 192]], dtype=uint8)
"""
sorted_im_ravel = None
if keep_dtype:
orig_dtype = im.dtype
if lower_in is None:
# Set to the 1st percentile
sorted_im_ravel = np.sort(im.ravel())
lower_in = sorted_im_ravel[im.size // 100]
if upper_in is None:
# Set to the 99th percentile
if sorted_im_ravel is not None:
upper_in = sorted_im_ravel[im.size - (im.size // 100) - 1]
else:
upper_in = np.sort(im.ravel())[im.size - (im.size // 100) - 1]
if lower_out is None:
if im.dtype.kind in 'iu':
lower_out = np.iinfo(im.dtype).min
elif im.dtype.kind == 'f':
lower_out = 0.0
else:
# Should never occur
lower_out = 0.0
if upper_out is None:
if im.dtype.kind in 'iu':
upper_out = np.iinfo(im.dtype).max
elif im.dtype.kind == 'f':
upper_out = 1.0
else:
# Should never occur
upper_out = 1.0
im[im < lower_in] = lower_in
im[im > upper_in] = upper_in
im = ((im - lower_in).astype(float) * (upper_out - lower_out) /
(upper_in - lower_in)) + lower_out
if keep_dtype:
im = im.astype(orig_dtype)
return im
[docs]def image_translate(
im: np.ndarray,
dist_vec: _utils.ShapeType,
pad_value: _utils.NumericType=0,
) -> np.ndarray:
"""Translates an image
Given an image and a two-dimensional vector of distances to translate,
this function translates the original image. Values left in the wake of
the translation are padded with pad_value, which is by default set to 0.
:type im: ``numpy.ndarray``
:param im: The original image.
:type dist_vec: ``ShapeType``
:param dist_vec: A vector of distances to translate.
:type pad_value: ``NumericType``
:param pad_value: The value to pad for elements left in the wake of the
translation.
:rtype: ``numpy.ndarray``
:return: The translated image.
Examples:
>>> import numpy as np
>>> im = np.array(array([[ 1, 2, 3, 4, 5],
... [ 6, 7, 8, 9, 10],
... [11, 12, 13, 14, 15],
... [16, 17, 18, 19, 20],
... [21, 22, 23, 24, 25]])
>>> image_translate(im, (0, 2))
array([[ 0, 0, 1, 2, 3],
[ 0, 0, 6, 7, 8],
[ 0, 0, 11, 12, 13],
[ 0, 0, 16, 17, 18],
[ 0, 0, 21, 22, 23]])
>>> image_translate(im, (-3, 2), pad_value=-1)
array([[-1, -1, 16, 17, 18],
[-1, -1, 21, 22, 23],
[-1, -1, -1, -1, -1],
[-1, -1, -1, -1, -1],
[-1, -1, -1, -1, -1]])
"""
try:
dist_vec_abs = [abs(dist) for dist in dist_vec]
except TypeError:
dist_vec = (dist_vec, dist_vec)
dist_vec_abs = [abs(dist) for dist in dist_vec]
dist_vec_abs_px = _utils.resolve_shape_arg(dist_vec_abs, im.shape,
arg_name='dist_vec',
allow_larger_than_max=False)
im_out = np.ones_like(im) * pad_value
if 0 == dist_vec_abs_px[0]:
if 0 == dist_vec_abs_px[1]:
im_out[:, :] = im[:, :]
elif dist_vec[1] < 0:
im_out[:, :-dist_vec_abs_px[1]] = im[:, dist_vec_abs_px[1]:]
else:
im_out[:, dist_vec_abs_px[1]:] = im[:, :-dist_vec_abs_px[1]]
elif dist_vec[0] < 0:
if 0 == dist_vec_abs_px[1]:
im_out[:-dist_vec_abs_px[0], :] = im[dist_vec_abs_px[0]:, :]
elif dist_vec[1] < 0:
im_out[:-dist_vec_abs_px[0], :-dist_vec_abs_px[1]] = \
im[dist_vec_abs_px[0]:, dist_vec_abs_px[1]:]
else:
im_out[:-dist_vec_abs_px[0], dist_vec_abs_px[1]:] = \
im[dist_vec_abs_px[0]:, :-dist_vec_abs_px[1]]
else:
if 0 == dist_vec_abs_px[1]:
im_out[dist_vec_abs_px[0]:, :] = im[:-dist_vec_abs_px[0], :]
elif dist_vec[1] < 0:
im_out[dist_vec_abs_px[0]:, :-dist_vec_abs_px[1]] = \
im[:-dist_vec_abs_px[0], dist_vec_abs_px[1]:]
else:
im_out[dist_vec_abs_px[0]:, dist_vec_abs_px[1]:] = \
im[:-dist_vec_abs_px[0], :-dist_vec_abs_px[1]]
return im_out
[docs]def image_shift(
im: np.ndarray,
dist_vec: _utils.ShapeType,
) -> np.ndarray:
"""Shifts an image by consecutively applying and summing translations
Given an image and a two-dimensional vector of distances to shift,
this function shifts the image by weighting and summing many
intermediate translations. This simulates an image taken with a shaky
camera.
:type im: ``numpy.ndarray``
:param im: The original image.
:type dist_vec: ``ShapeType``
:param dist_vec: A vector of distances to shift.
:rtype: ``numpy.ndarray``
:return: The translated image.
Examples:
>>> import numpy as np
>>> im = np.array(array([[ 1, 2, 4],
... [ 8, 16, 32],
... [ 64, 128, 256]])
>>> image_translate(im, (0, 1))
array([[ 1, 1, 3],
[ 8, 12, 24],
[ 64, 96, 192]])
>>> image_translate(im, (-1, 1))
array([[ 1, 5, 10],
[ 8, 40, 80],
[ 64, 128, 256]])
"""
try:
dist_vec_abs = [abs(dist) for dist in dist_vec]
except TypeError:
dist_vec = (dist_vec, dist_vec)
dist_vec_abs = [abs(dist) for dist in dist_vec]
dist_vec_abs_px = _utils.resolve_shape_arg(dist_vec_abs, im.shape,
arg_name='dist_vec',
allow_larger_than_max=False)
im_out = im.copy().astype(float)
im_ones = np.ones_like(im, dtype=np.uint16)
weights = im_ones.copy()
if 0 == dist_vec_abs_px[0]:
if 0 == dist_vec_abs_px[1]:
pass
elif dist_vec[1] < 0:
for j in range(1, dist_vec_abs_px[1] + 1):
weights += image_translate(im_ones, (0, -j), pad_value=0)
im_out += image_translate(im, (0, -j), pad_value=0)
else:
for j in range(1, dist_vec_abs_px[1] + 1):
weights += image_translate(im_ones, (0, j), pad_value=0)
im_out += image_translate(im, (0, j), pad_value=0)
elif dist_vec[0] < 0:
if 0 == dist_vec_abs_px[1]:
for i in range(1, dist_vec_abs_px[0] + 1):
weights += image_translate(im_ones, (-i, 0), pad_value=0)
im_out += image_translate(im, (-i, 0), pad_value=0)
elif dist_vec[1] < 0:
iterations = min(dist_vec_abs_px)
for k in range(1, iterations + 1):
i = (k * dist_vec_abs_px[0]) // iterations
j = (k * dist_vec_abs_px[1]) // iterations
weights += image_translate(im_ones, (-i, -j), pad_value=0)
im_out += image_translate(im, (-i, -j), pad_value=0)
else:
iterations = min(dist_vec_abs_px)
for k in range(1, iterations + 1):
i = (k * dist_vec_abs_px[0]) // iterations
j = (k * dist_vec_abs_px[1]) // iterations
weights += image_translate(im_ones, (-i, j), pad_value=0)
im_out += image_translate(im, (-i, j), pad_value=0)
else:
if 0 == dist_vec_abs_px[1]:
for i in range(1, dist_vec_abs_px[0] + 1):
weights += image_translate(im_ones, (i, 0), pad_value=0)
im_out += image_translate(im, (i, 0), pad_value=0)
elif dist_vec[1] < 0:
iterations = min(dist_vec_abs_px)
for k in range(1, iterations + 1):
i = (k * dist_vec_abs_px[0]) // iterations
j = (k * dist_vec_abs_px[1]) // iterations
weights += image_translate(im_ones, (i, -j), pad_value=0)
im_out += image_translate(im, (i, -j), pad_value=0)
else:
iterations = min(dist_vec_abs_px)
for k in range(1, iterations + 1):
i = (k * dist_vec_abs_px[0]) // iterations
j = (k * dist_vec_abs_px[1]) // iterations
weights += image_translate(im_ones, (i, j), pad_value=0)
im_out += image_translate(im, (i, j), pad_value=0)
im_out /= weights
return im_out.astype(im.dtype)