1095 lines
41 KiB
Python
1095 lines
41 KiB
Python
import numpy as np
|
|
from scipy import ndimage as ndi
|
|
|
|
from ._geometric import (SimilarityTransform, AffineTransform,
|
|
ProjectiveTransform, _to_ndimage_mode)
|
|
from ._warps_cy import _warp_fast
|
|
from ..measure import block_reduce
|
|
|
|
from .._shared.utils import (get_bound_method_class, safe_as_int, warn,
|
|
convert_to_float, _validate_interpolation_order)
|
|
|
|
HOMOGRAPHY_TRANSFORMS = (
|
|
SimilarityTransform,
|
|
AffineTransform,
|
|
ProjectiveTransform
|
|
)
|
|
|
|
|
|
def resize(image, output_shape, order=None, mode='reflect', cval=0, clip=True,
|
|
preserve_range=False, anti_aliasing=None, anti_aliasing_sigma=None):
|
|
"""Resize image to match a certain size.
|
|
|
|
Performs interpolation to up-size or down-size N-dimensional images. Note
|
|
that anti-aliasing should be enabled when down-sizing images to avoid
|
|
aliasing artifacts. For down-sampling with an integer factor also see
|
|
`skimage.transform.downscale_local_mean`.
|
|
|
|
Parameters
|
|
----------
|
|
image : ndarray
|
|
Input image.
|
|
output_shape : tuple or ndarray
|
|
Size of the generated output image `(rows, cols[, ...][, dim])`. If
|
|
`dim` is not provided, the number of channels is preserved. In case the
|
|
number of input channels does not equal the number of output channels a
|
|
n-dimensional interpolation is applied.
|
|
|
|
Returns
|
|
-------
|
|
resized : ndarray
|
|
Resized version of the input.
|
|
|
|
Other parameters
|
|
----------------
|
|
order : int, optional
|
|
The order of the spline interpolation, default is 0 if
|
|
image.dtype is bool and 1 otherwise. The order has to be in
|
|
the range 0-5. See `skimage.transform.warp` for detail.
|
|
mode : {'constant', 'edge', 'symmetric', 'reflect', 'wrap'}, optional
|
|
Points outside the boundaries of the input are filled according
|
|
to the given mode. Modes match the behaviour of `numpy.pad`.
|
|
cval : float, optional
|
|
Used in conjunction with mode 'constant', the value outside
|
|
the image boundaries.
|
|
clip : bool, optional
|
|
Whether to clip the output to the range of values of the input image.
|
|
This is enabled by default, since higher order interpolation may
|
|
produce values outside the given input range.
|
|
preserve_range : bool, optional
|
|
Whether to keep the original range of values. Otherwise, the input
|
|
image is converted according to the conventions of `img_as_float`.
|
|
Also see https://scikit-image.org/docs/dev/user_guide/data_types.html
|
|
anti_aliasing : bool, optional
|
|
Whether to apply a Gaussian filter to smooth the image prior
|
|
to down-scaling. It is crucial to filter when down-sampling
|
|
the image to avoid aliasing artifacts. If input image data
|
|
type is bool, no anti-aliasing is applied.
|
|
anti_aliasing_sigma : {float, tuple of floats}, optional
|
|
Standard deviation for Gaussian filtering to avoid aliasing artifacts.
|
|
By default, this value is chosen as (s - 1) / 2 where s is the
|
|
down-scaling factor, where s > 1. For the up-size case, s < 1, no
|
|
anti-aliasing is performed prior to rescaling.
|
|
|
|
Notes
|
|
-----
|
|
Modes 'reflect' and 'symmetric' are similar, but differ in whether the edge
|
|
pixels are duplicated during the reflection. As an example, if an array
|
|
has values [0, 1, 2] and was padded to the right by four values using
|
|
symmetric, the result would be [0, 1, 2, 2, 1, 0, 0], while for reflect it
|
|
would be [0, 1, 2, 1, 0, 1, 2].
|
|
|
|
Examples
|
|
--------
|
|
>>> from skimage import data
|
|
>>> from skimage.transform import resize
|
|
>>> image = data.camera()
|
|
>>> resize(image, (100, 100)).shape
|
|
(100, 100)
|
|
|
|
"""
|
|
output_shape = tuple(output_shape)
|
|
output_ndim = len(output_shape)
|
|
input_shape = image.shape
|
|
if output_ndim > image.ndim:
|
|
# append dimensions to input_shape
|
|
input_shape = input_shape + (1, ) * (output_ndim - image.ndim)
|
|
image = np.reshape(image, input_shape)
|
|
elif output_ndim == image.ndim - 1:
|
|
# multichannel case: append shape of last axis
|
|
output_shape = output_shape + (image.shape[-1], )
|
|
elif output_ndim < image.ndim - 1:
|
|
raise ValueError("len(output_shape) cannot be smaller than the image "
|
|
"dimensions")
|
|
|
|
if anti_aliasing is None:
|
|
anti_aliasing = not image.dtype == bool
|
|
|
|
if image.dtype == bool and anti_aliasing:
|
|
warn("Input image dtype is bool. Gaussian convolution is not defined "
|
|
"with bool data type. Please set anti_aliasing to False or "
|
|
"explicitely cast input image to another data type. Starting "
|
|
"from version 0.19 a ValueError will be raised instead of this "
|
|
"warning.", FutureWarning, stacklevel=2)
|
|
|
|
factors = (np.asarray(input_shape, dtype=float) /
|
|
np.asarray(output_shape, dtype=float))
|
|
|
|
if anti_aliasing:
|
|
if anti_aliasing_sigma is None:
|
|
anti_aliasing_sigma = np.maximum(0, (factors - 1) / 2)
|
|
else:
|
|
anti_aliasing_sigma = \
|
|
np.atleast_1d(anti_aliasing_sigma) * np.ones_like(factors)
|
|
if np.any(anti_aliasing_sigma < 0):
|
|
raise ValueError("Anti-aliasing standard deviation must be "
|
|
"greater than or equal to zero")
|
|
elif np.any((anti_aliasing_sigma > 0) & (factors <= 1)):
|
|
warn("Anti-aliasing standard deviation greater than zero but "
|
|
"not down-sampling along all axes")
|
|
|
|
# Translate modes used by np.pad to those used by ndi.gaussian_filter
|
|
np_pad_to_ndimage = {
|
|
'constant': 'constant',
|
|
'edge': 'nearest',
|
|
'symmetric': 'reflect',
|
|
'reflect': 'mirror',
|
|
'wrap': 'wrap'
|
|
}
|
|
try:
|
|
ndi_mode = np_pad_to_ndimage[mode]
|
|
except KeyError:
|
|
raise ValueError("Unknown mode, or cannot translate mode. The "
|
|
"mode should be one of 'constant', 'edge', "
|
|
"'symmetric', 'reflect', or 'wrap'. See the "
|
|
"documentation of numpy.pad for more info.")
|
|
|
|
image = ndi.gaussian_filter(image, anti_aliasing_sigma,
|
|
cval=cval, mode=ndi_mode)
|
|
|
|
# 2-dimensional interpolation
|
|
if len(output_shape) == 2 or (len(output_shape) == 3 and
|
|
output_shape[2] == input_shape[2]):
|
|
rows = output_shape[0]
|
|
cols = output_shape[1]
|
|
input_rows = input_shape[0]
|
|
input_cols = input_shape[1]
|
|
if rows == 1 and cols == 1:
|
|
tform = AffineTransform(translation=(input_cols / 2.0 - 0.5,
|
|
input_rows / 2.0 - 0.5))
|
|
else:
|
|
# 3 control points necessary to estimate exact AffineTransform
|
|
src_corners = np.array([[1, 1], [1, rows], [cols, rows]]) - 1
|
|
dst_corners = np.zeros(src_corners.shape, dtype=np.double)
|
|
# take into account that 0th pixel is at position (0.5, 0.5)
|
|
dst_corners[:, 0] = factors[1] * (src_corners[:, 0] + 0.5) - 0.5
|
|
dst_corners[:, 1] = factors[0] * (src_corners[:, 1] + 0.5) - 0.5
|
|
|
|
tform = AffineTransform()
|
|
tform.estimate(src_corners, dst_corners)
|
|
|
|
# Make sure the transform is exactly metric, to ensure fast warping.
|
|
tform.params[2] = (0, 0, 1)
|
|
tform.params[0, 1] = 0
|
|
tform.params[1, 0] = 0
|
|
|
|
out = warp(image, tform, output_shape=output_shape, order=order,
|
|
mode=mode, cval=cval, clip=clip,
|
|
preserve_range=preserve_range)
|
|
|
|
else: # n-dimensional interpolation
|
|
order = _validate_interpolation_order(image.dtype, order)
|
|
|
|
coord_arrays = [factors[i] * (np.arange(d) + 0.5) - 0.5
|
|
for i, d in enumerate(output_shape)]
|
|
|
|
coord_map = np.array(np.meshgrid(*coord_arrays,
|
|
sparse=False,
|
|
indexing='ij'))
|
|
|
|
image = convert_to_float(image, preserve_range)
|
|
|
|
ndi_mode = _to_ndimage_mode(mode)
|
|
out = ndi.map_coordinates(image, coord_map, order=order,
|
|
mode=ndi_mode, cval=cval)
|
|
|
|
_clip_warp_output(image, out, order, mode, cval, clip)
|
|
|
|
return out
|
|
|
|
|
|
def rescale(image, scale, order=None, mode='reflect', cval=0, clip=True,
|
|
preserve_range=False, multichannel=False,
|
|
anti_aliasing=None, anti_aliasing_sigma=None):
|
|
"""Scale image by a certain factor.
|
|
|
|
Performs interpolation to up-scale or down-scale N-dimensional images.
|
|
Note that anti-aliasing should be enabled when down-sizing images to avoid
|
|
aliasing artifacts. For down-sampling with an integer factor also see
|
|
`skimage.transform.downscale_local_mean`.
|
|
|
|
Parameters
|
|
----------
|
|
image : ndarray
|
|
Input image.
|
|
scale : {float, tuple of floats}
|
|
Scale factors. Separate scale factors can be defined as
|
|
`(rows, cols[, ...][, dim])`.
|
|
|
|
Returns
|
|
-------
|
|
scaled : ndarray
|
|
Scaled version of the input.
|
|
|
|
Other parameters
|
|
----------------
|
|
order : int, optional
|
|
The order of the spline interpolation, default is 0 if
|
|
image.dtype is bool and 1 otherwise. The order has to be in
|
|
the range 0-5. See `skimage.transform.warp` for detail.
|
|
mode : {'constant', 'edge', 'symmetric', 'reflect', 'wrap'}, optional
|
|
Points outside the boundaries of the input are filled according
|
|
to the given mode. Modes match the behaviour of `numpy.pad`.
|
|
cval : float, optional
|
|
Used in conjunction with mode 'constant', the value outside
|
|
the image boundaries.
|
|
clip : bool, optional
|
|
Whether to clip the output to the range of values of the input image.
|
|
This is enabled by default, since higher order interpolation may
|
|
produce values outside the given input range.
|
|
preserve_range : bool, optional
|
|
Whether to keep the original range of values. Otherwise, the input
|
|
image is converted according to the conventions of `img_as_float`.
|
|
Also see
|
|
https://scikit-image.org/docs/dev/user_guide/data_types.html
|
|
multichannel : bool, optional
|
|
Whether the last axis of the image is to be interpreted as multiple
|
|
channels or another spatial dimension.
|
|
anti_aliasing : bool, optional
|
|
Whether to apply a Gaussian filter to smooth the image prior
|
|
to down-scaling. It is crucial to filter when down-sampling
|
|
the image to avoid aliasing artifacts. If input image data
|
|
type is bool, no anti-aliasing is applied.
|
|
anti_aliasing_sigma : {float, tuple of floats}, optional
|
|
Standard deviation for Gaussian filtering to avoid aliasing artifacts.
|
|
By default, this value is chosen as (s - 1) / 2 where s is the
|
|
down-scaling factor.
|
|
|
|
Notes
|
|
-----
|
|
Modes 'reflect' and 'symmetric' are similar, but differ in whether the edge
|
|
pixels are duplicated during the reflection. As an example, if an array
|
|
has values [0, 1, 2] and was padded to the right by four values using
|
|
symmetric, the result would be [0, 1, 2, 2, 1, 0, 0], while for reflect it
|
|
would be [0, 1, 2, 1, 0, 1, 2].
|
|
|
|
Examples
|
|
--------
|
|
>>> from skimage import data
|
|
>>> from skimage.transform import rescale
|
|
>>> image = data.camera()
|
|
>>> rescale(image, 0.1).shape
|
|
(51, 51)
|
|
>>> rescale(image, 0.5).shape
|
|
(256, 256)
|
|
|
|
"""
|
|
scale = np.atleast_1d(scale)
|
|
if len(scale) > 1:
|
|
if ((not multichannel and len(scale) != image.ndim) or
|
|
(multichannel and len(scale) != image.ndim - 1)):
|
|
raise ValueError("Supply a single scale, or one value per spatial "
|
|
"axis")
|
|
if multichannel:
|
|
scale = np.concatenate((scale, [1]))
|
|
orig_shape = np.asarray(image.shape)
|
|
output_shape = np.round(scale * orig_shape)
|
|
if multichannel: # don't scale channel dimension
|
|
output_shape[-1] = orig_shape[-1]
|
|
|
|
return resize(image, output_shape, order=order, mode=mode, cval=cval,
|
|
clip=clip, preserve_range=preserve_range,
|
|
anti_aliasing=anti_aliasing,
|
|
anti_aliasing_sigma=anti_aliasing_sigma)
|
|
|
|
|
|
def rotate(image, angle, resize=False, center=None, order=None,
|
|
mode='constant', cval=0, clip=True, preserve_range=False):
|
|
"""Rotate image by a certain angle around its center.
|
|
|
|
Parameters
|
|
----------
|
|
image : ndarray
|
|
Input image.
|
|
angle : float
|
|
Rotation angle in degrees in counter-clockwise direction.
|
|
resize : bool, optional
|
|
Determine whether the shape of the output image will be automatically
|
|
calculated, so the complete rotated image exactly fits. Default is
|
|
False.
|
|
center : iterable of length 2
|
|
The rotation center. If ``center=None``, the image is rotated around
|
|
its center, i.e. ``center=(cols / 2 - 0.5, rows / 2 - 0.5)``. Please
|
|
note that this parameter is (cols, rows), contrary to normal skimage
|
|
ordering.
|
|
|
|
Returns
|
|
-------
|
|
rotated : ndarray
|
|
Rotated version of the input.
|
|
|
|
Other parameters
|
|
----------------
|
|
order : int, optional
|
|
The order of the spline interpolation, default is 0 if
|
|
image.dtype is bool and 1 otherwise. The order has to be in
|
|
the range 0-5. See `skimage.transform.warp` for detail.
|
|
mode : {'constant', 'edge', 'symmetric', 'reflect', 'wrap'}, optional
|
|
Points outside the boundaries of the input are filled according
|
|
to the given mode. Modes match the behaviour of `numpy.pad`.
|
|
cval : float, optional
|
|
Used in conjunction with mode 'constant', the value outside
|
|
the image boundaries.
|
|
clip : bool, optional
|
|
Whether to clip the output to the range of values of the input image.
|
|
This is enabled by default, since higher order interpolation may
|
|
produce values outside the given input range.
|
|
preserve_range : bool, optional
|
|
Whether to keep the original range of values. Otherwise, the input
|
|
image is converted according to the conventions of `img_as_float`.
|
|
Also see
|
|
https://scikit-image.org/docs/dev/user_guide/data_types.html
|
|
|
|
Notes
|
|
-----
|
|
Modes 'reflect' and 'symmetric' are similar, but differ in whether the edge
|
|
pixels are duplicated during the reflection. As an example, if an array
|
|
has values [0, 1, 2] and was padded to the right by four values using
|
|
symmetric, the result would be [0, 1, 2, 2, 1, 0, 0], while for reflect it
|
|
would be [0, 1, 2, 1, 0, 1, 2].
|
|
|
|
Examples
|
|
--------
|
|
>>> from skimage import data
|
|
>>> from skimage.transform import rotate
|
|
>>> image = data.camera()
|
|
>>> rotate(image, 2).shape
|
|
(512, 512)
|
|
>>> rotate(image, 2, resize=True).shape
|
|
(530, 530)
|
|
>>> rotate(image, 90, resize=True).shape
|
|
(512, 512)
|
|
|
|
"""
|
|
|
|
rows, cols = image.shape[0], image.shape[1]
|
|
|
|
# rotation around center
|
|
if center is None:
|
|
center = np.array((cols, rows)) / 2. - 0.5
|
|
else:
|
|
center = np.asarray(center)
|
|
tform1 = SimilarityTransform(translation=center)
|
|
tform2 = SimilarityTransform(rotation=np.deg2rad(angle))
|
|
tform3 = SimilarityTransform(translation=-center)
|
|
tform = tform3 + tform2 + tform1
|
|
|
|
output_shape = None
|
|
if resize:
|
|
# determine shape of output image
|
|
corners = np.array([
|
|
[0, 0],
|
|
[0, rows - 1],
|
|
[cols - 1, rows - 1],
|
|
[cols - 1, 0]
|
|
])
|
|
corners = tform.inverse(corners)
|
|
minc = corners[:, 0].min()
|
|
minr = corners[:, 1].min()
|
|
maxc = corners[:, 0].max()
|
|
maxr = corners[:, 1].max()
|
|
out_rows = maxr - minr + 1
|
|
out_cols = maxc - minc + 1
|
|
output_shape = np.around((out_rows, out_cols))
|
|
|
|
# fit output image in new shape
|
|
translation = (minc, minr)
|
|
tform4 = SimilarityTransform(translation=translation)
|
|
tform = tform4 + tform
|
|
|
|
# Make sure the transform is exactly affine, to ensure fast warping.
|
|
tform.params[2] = (0, 0, 1)
|
|
|
|
return warp(image, tform, output_shape=output_shape, order=order,
|
|
mode=mode, cval=cval, clip=clip, preserve_range=preserve_range)
|
|
|
|
|
|
def downscale_local_mean(image, factors, cval=0, clip=True):
|
|
"""Down-sample N-dimensional image by local averaging.
|
|
|
|
The image is padded with `cval` if it is not perfectly divisible by the
|
|
integer factors.
|
|
|
|
In contrast to interpolation in `skimage.transform.resize` and
|
|
`skimage.transform.rescale` this function calculates the local mean of
|
|
elements in each block of size `factors` in the input image.
|
|
|
|
Parameters
|
|
----------
|
|
image : ndarray
|
|
N-dimensional input image.
|
|
factors : array_like
|
|
Array containing down-sampling integer factor along each axis.
|
|
cval : float, optional
|
|
Constant padding value if image is not perfectly divisible by the
|
|
integer factors.
|
|
clip : bool, optional
|
|
Unused, but kept here for API consistency with the other transforms
|
|
in this module. (The local mean will never fall outside the range
|
|
of values in the input image, assuming the provided `cval` also
|
|
falls within that range.)
|
|
|
|
Returns
|
|
-------
|
|
image : ndarray
|
|
Down-sampled image with same number of dimensions as input image.
|
|
For integer inputs, the output dtype will be ``float64``.
|
|
See :func:`numpy.mean` for details.
|
|
|
|
Examples
|
|
--------
|
|
>>> a = np.arange(15).reshape(3, 5)
|
|
>>> a
|
|
array([[ 0, 1, 2, 3, 4],
|
|
[ 5, 6, 7, 8, 9],
|
|
[10, 11, 12, 13, 14]])
|
|
>>> downscale_local_mean(a, (2, 3))
|
|
array([[3.5, 4. ],
|
|
[5.5, 4.5]])
|
|
|
|
"""
|
|
return block_reduce(image, factors, np.mean, cval)
|
|
|
|
|
|
def _swirl_mapping(xy, center, rotation, strength, radius):
|
|
x, y = xy.T
|
|
x0, y0 = center
|
|
rho = np.sqrt((x - x0) ** 2 + (y - y0) ** 2)
|
|
|
|
# Ensure that the transformation decays to approximately 1/1000-th
|
|
# within the specified radius.
|
|
radius = radius / 5 * np.log(2)
|
|
|
|
theta = rotation + strength * \
|
|
np.exp(-rho / radius) + \
|
|
np.arctan2(y - y0, x - x0)
|
|
|
|
xy[..., 0] = x0 + rho * np.cos(theta)
|
|
xy[..., 1] = y0 + rho * np.sin(theta)
|
|
|
|
return xy
|
|
|
|
|
|
def swirl(image, center=None, strength=1, radius=100, rotation=0,
|
|
output_shape=None, order=None, mode='reflect', cval=0, clip=True,
|
|
preserve_range=False):
|
|
"""Perform a swirl transformation.
|
|
|
|
Parameters
|
|
----------
|
|
image : ndarray
|
|
Input image.
|
|
center : (column, row) tuple or (2,) ndarray, optional
|
|
Center coordinate of transformation.
|
|
strength : float, optional
|
|
The amount of swirling applied.
|
|
radius : float, optional
|
|
The extent of the swirl in pixels. The effect dies out
|
|
rapidly beyond `radius`.
|
|
rotation : float, optional
|
|
Additional rotation applied to the image.
|
|
|
|
Returns
|
|
-------
|
|
swirled : ndarray
|
|
Swirled version of the input.
|
|
|
|
Other parameters
|
|
----------------
|
|
output_shape : tuple (rows, cols), optional
|
|
Shape of the output image generated. By default the shape of the input
|
|
image is preserved.
|
|
order : int, optional
|
|
The order of the spline interpolation, default is 0 if
|
|
image.dtype is bool and 1 otherwise. The order has to be in
|
|
the range 0-5. See `skimage.transform.warp` for detail.
|
|
mode : {'constant', 'edge', 'symmetric', 'reflect', 'wrap'}, optional
|
|
Points outside the boundaries of the input are filled according
|
|
to the given mode, with 'constant' used as the default. Modes match
|
|
the behaviour of `numpy.pad`.
|
|
cval : float, optional
|
|
Used in conjunction with mode 'constant', the value outside
|
|
the image boundaries.
|
|
clip : bool, optional
|
|
Whether to clip the output to the range of values of the input image.
|
|
This is enabled by default, since higher order interpolation may
|
|
produce values outside the given input range.
|
|
preserve_range : bool, optional
|
|
Whether to keep the original range of values. Otherwise, the input
|
|
image is converted according to the conventions of `img_as_float`.
|
|
Also see
|
|
https://scikit-image.org/docs/dev/user_guide/data_types.html
|
|
|
|
"""
|
|
if center is None:
|
|
center = np.array(image.shape)[:2][::-1] / 2
|
|
|
|
warp_args = {'center': center,
|
|
'rotation': rotation,
|
|
'strength': strength,
|
|
'radius': radius}
|
|
|
|
return warp(image, _swirl_mapping, map_args=warp_args,
|
|
output_shape=output_shape, order=order, mode=mode, cval=cval,
|
|
clip=clip, preserve_range=preserve_range)
|
|
|
|
|
|
def _stackcopy(a, b):
|
|
"""Copy b into each color layer of a, such that::
|
|
|
|
a[:,:,0] = a[:,:,1] = ... = b
|
|
|
|
Parameters
|
|
----------
|
|
a : (M, N) or (M, N, P) ndarray
|
|
Target array.
|
|
b : (M, N)
|
|
Source array.
|
|
|
|
Notes
|
|
-----
|
|
Color images are stored as an ``(M, N, 3)`` or ``(M, N, 4)`` arrays.
|
|
|
|
"""
|
|
if a.ndim == 3:
|
|
a[:] = b[:, :, np.newaxis]
|
|
else:
|
|
a[:] = b
|
|
|
|
|
|
def warp_coords(coord_map, shape, dtype=np.float64):
|
|
"""Build the source coordinates for the output of a 2-D image warp.
|
|
|
|
Parameters
|
|
----------
|
|
coord_map : callable like GeometricTransform.inverse
|
|
Return input coordinates for given output coordinates.
|
|
Coordinates are in the shape (P, 2), where P is the number
|
|
of coordinates and each element is a ``(row, col)`` pair.
|
|
shape : tuple
|
|
Shape of output image ``(rows, cols[, bands])``.
|
|
dtype : np.dtype or string
|
|
dtype for return value (sane choices: float32 or float64).
|
|
|
|
Returns
|
|
-------
|
|
coords : (ndim, rows, cols[, bands]) array of dtype `dtype`
|
|
Coordinates for `scipy.ndimage.map_coordinates`, that will yield
|
|
an image of shape (orows, ocols, bands) by drawing from source
|
|
points according to the `coord_transform_fn`.
|
|
|
|
Notes
|
|
-----
|
|
|
|
This is a lower-level routine that produces the source coordinates for 2-D
|
|
images used by `warp()`.
|
|
|
|
It is provided separately from `warp` to give additional flexibility to
|
|
users who would like, for example, to re-use a particular coordinate
|
|
mapping, to use specific dtypes at various points along the the
|
|
image-warping process, or to implement different post-processing logic
|
|
than `warp` performs after the call to `ndi.map_coordinates`.
|
|
|
|
|
|
Examples
|
|
--------
|
|
Produce a coordinate map that shifts an image up and to the right:
|
|
|
|
>>> from skimage import data
|
|
>>> from scipy.ndimage import map_coordinates
|
|
>>>
|
|
>>> def shift_up10_left20(xy):
|
|
... return xy - np.array([-20, 10])[None, :]
|
|
>>>
|
|
>>> image = data.astronaut().astype(np.float32)
|
|
>>> coords = warp_coords(shift_up10_left20, image.shape)
|
|
>>> warped_image = map_coordinates(image, coords)
|
|
|
|
"""
|
|
shape = safe_as_int(shape)
|
|
rows, cols = shape[0], shape[1]
|
|
coords_shape = [len(shape), rows, cols]
|
|
if len(shape) == 3:
|
|
coords_shape.append(shape[2])
|
|
coords = np.empty(coords_shape, dtype=dtype)
|
|
|
|
# Reshape grid coordinates into a (P, 2) array of (row, col) pairs
|
|
tf_coords = np.indices((cols, rows), dtype=dtype).reshape(2, -1).T
|
|
|
|
# Map each (row, col) pair to the source image according to
|
|
# the user-provided mapping
|
|
tf_coords = coord_map(tf_coords)
|
|
|
|
# Reshape back to a (2, M, N) coordinate grid
|
|
tf_coords = tf_coords.T.reshape((-1, cols, rows)).swapaxes(1, 2)
|
|
|
|
# Place the y-coordinate mapping
|
|
_stackcopy(coords[1, ...], tf_coords[0, ...])
|
|
|
|
# Place the x-coordinate mapping
|
|
_stackcopy(coords[0, ...], tf_coords[1, ...])
|
|
|
|
if len(shape) == 3:
|
|
coords[2, ...] = range(shape[2])
|
|
|
|
return coords
|
|
|
|
|
|
def _clip_warp_output(input_image, output_image, order, mode, cval, clip):
|
|
"""Clip output image to range of values of input image.
|
|
|
|
Note that this function modifies the values of `output_image` in-place
|
|
and it is only modified if ``clip=True``.
|
|
|
|
Parameters
|
|
----------
|
|
input_image : ndarray
|
|
Input image.
|
|
output_image : ndarray
|
|
Output image, which is modified in-place.
|
|
|
|
Other parameters
|
|
----------------
|
|
order : int, optional
|
|
The order of the spline interpolation, default is 1. The order has to
|
|
be in the range 0-5. See `skimage.transform.warp` for detail.
|
|
mode : {'constant', 'edge', 'symmetric', 'reflect', 'wrap'}, optional
|
|
Points outside the boundaries of the input are filled according
|
|
to the given mode. Modes match the behaviour of `numpy.pad`.
|
|
cval : float, optional
|
|
Used in conjunction with mode 'constant', the value outside
|
|
the image boundaries.
|
|
clip : bool, optional
|
|
Whether to clip the output to the range of values of the input image.
|
|
This is enabled by default, since higher order interpolation may
|
|
produce values outside the given input range.
|
|
|
|
"""
|
|
if clip and order != 0:
|
|
min_val = input_image.min()
|
|
max_val = input_image.max()
|
|
|
|
preserve_cval = (mode == 'constant' and not
|
|
(min_val <= cval <= max_val))
|
|
|
|
if preserve_cval:
|
|
cval_mask = output_image == cval
|
|
|
|
np.clip(output_image, min_val, max_val, out=output_image)
|
|
|
|
if preserve_cval:
|
|
output_image[cval_mask] = cval
|
|
|
|
|
|
def warp(image, inverse_map, map_args={}, output_shape=None, order=None,
|
|
mode='constant', cval=0., clip=True, preserve_range=False):
|
|
"""Warp an image according to a given coordinate transformation.
|
|
|
|
Parameters
|
|
----------
|
|
image : ndarray
|
|
Input image.
|
|
inverse_map : transformation object, callable ``cr = f(cr, **kwargs)``, or ndarray
|
|
Inverse coordinate map, which transforms coordinates in the output
|
|
images into their corresponding coordinates in the input image.
|
|
|
|
There are a number of different options to define this map, depending
|
|
on the dimensionality of the input image. A 2-D image can have 2
|
|
dimensions for gray-scale images, or 3 dimensions with color
|
|
information.
|
|
|
|
- For 2-D images, you can directly pass a transformation object,
|
|
e.g. `skimage.transform.SimilarityTransform`, or its inverse.
|
|
- For 2-D images, you can pass a ``(3, 3)`` homogeneous
|
|
transformation matrix, e.g.
|
|
`skimage.transform.SimilarityTransform.params`.
|
|
- For 2-D images, a function that transforms a ``(M, 2)`` array of
|
|
``(col, row)`` coordinates in the output image to their
|
|
corresponding coordinates in the input image. Extra parameters to
|
|
the function can be specified through `map_args`.
|
|
- For N-D images, you can directly pass an array of coordinates.
|
|
The first dimension specifies the coordinates in the input image,
|
|
while the subsequent dimensions determine the position in the
|
|
output image. E.g. in case of 2-D images, you need to pass an array
|
|
of shape ``(2, rows, cols)``, where `rows` and `cols` determine the
|
|
shape of the output image, and the first dimension contains the
|
|
``(row, col)`` coordinate in the input image.
|
|
See `scipy.ndimage.map_coordinates` for further documentation.
|
|
|
|
Note, that a ``(3, 3)`` matrix is interpreted as a homogeneous
|
|
transformation matrix, so you cannot interpolate values from a 3-D
|
|
input, if the output is of shape ``(3,)``.
|
|
|
|
See example section for usage.
|
|
map_args : dict, optional
|
|
Keyword arguments passed to `inverse_map`.
|
|
output_shape : tuple (rows, cols), optional
|
|
Shape of the output image generated. By default the shape of the input
|
|
image is preserved. Note that, even for multi-band images, only rows
|
|
and columns need to be specified.
|
|
order : int, optional
|
|
The order of interpolation. The order has to be in the range 0-5:
|
|
- 0: Nearest-neighbor
|
|
- 1: Bi-linear (default)
|
|
- 2: Bi-quadratic
|
|
- 3: Bi-cubic
|
|
- 4: Bi-quartic
|
|
- 5: Bi-quintic
|
|
|
|
Default is 0 if image.dtype is bool and 1 otherwise.
|
|
mode : {'constant', 'edge', 'symmetric', 'reflect', 'wrap'}, optional
|
|
Points outside the boundaries of the input are filled according
|
|
to the given mode. Modes match the behaviour of `numpy.pad`.
|
|
cval : float, optional
|
|
Used in conjunction with mode 'constant', the value outside
|
|
the image boundaries.
|
|
clip : bool, optional
|
|
Whether to clip the output to the range of values of the input image.
|
|
This is enabled by default, since higher order interpolation may
|
|
produce values outside the given input range.
|
|
preserve_range : bool, optional
|
|
Whether to keep the original range of values. Otherwise, the input
|
|
image is converted according to the conventions of `img_as_float`.
|
|
Also see
|
|
https://scikit-image.org/docs/dev/user_guide/data_types.html
|
|
|
|
Returns
|
|
-------
|
|
warped : double ndarray
|
|
The warped input image.
|
|
|
|
Notes
|
|
-----
|
|
- The input image is converted to a `double` image.
|
|
- In case of a `SimilarityTransform`, `AffineTransform` and
|
|
`ProjectiveTransform` and `order` in [0, 3] this function uses the
|
|
underlying transformation matrix to warp the image with a much faster
|
|
routine.
|
|
|
|
Examples
|
|
--------
|
|
>>> from skimage.transform import warp
|
|
>>> from skimage import data
|
|
>>> image = data.camera()
|
|
|
|
The following image warps are all equal but differ substantially in
|
|
execution time. The image is shifted to the bottom.
|
|
|
|
Use a geometric transform to warp an image (fast):
|
|
|
|
>>> from skimage.transform import SimilarityTransform
|
|
>>> tform = SimilarityTransform(translation=(0, -10))
|
|
>>> warped = warp(image, tform)
|
|
|
|
Use a callable (slow):
|
|
|
|
>>> def shift_down(xy):
|
|
... xy[:, 1] -= 10
|
|
... return xy
|
|
>>> warped = warp(image, shift_down)
|
|
|
|
Use a transformation matrix to warp an image (fast):
|
|
|
|
>>> matrix = np.array([[1, 0, 0], [0, 1, -10], [0, 0, 1]])
|
|
>>> warped = warp(image, matrix)
|
|
>>> from skimage.transform import ProjectiveTransform
|
|
>>> warped = warp(image, ProjectiveTransform(matrix=matrix))
|
|
|
|
You can also use the inverse of a geometric transformation (fast):
|
|
|
|
>>> warped = warp(image, tform.inverse)
|
|
|
|
For N-D images you can pass a coordinate array, that specifies the
|
|
coordinates in the input image for every element in the output image. E.g.
|
|
if you want to rescale a 3-D cube, you can do:
|
|
|
|
>>> cube_shape = np.array([30, 30, 30])
|
|
>>> cube = np.random.rand(*cube_shape)
|
|
|
|
Setup the coordinate array, that defines the scaling:
|
|
|
|
>>> scale = 0.1
|
|
>>> output_shape = (scale * cube_shape).astype(int)
|
|
>>> coords0, coords1, coords2 = np.mgrid[:output_shape[0],
|
|
... :output_shape[1], :output_shape[2]]
|
|
>>> coords = np.array([coords0, coords1, coords2])
|
|
|
|
Assume that the cube contains spatial data, where the first array element
|
|
center is at coordinate (0.5, 0.5, 0.5) in real space, i.e. we have to
|
|
account for this extra offset when scaling the image:
|
|
|
|
>>> coords = (coords + 0.5) / scale - 0.5
|
|
>>> warped = warp(cube, coords)
|
|
|
|
"""
|
|
|
|
if image.size == 0:
|
|
raise ValueError("Cannot warp empty image with dimensions",
|
|
image.shape)
|
|
|
|
order = _validate_interpolation_order(image.dtype, order)
|
|
|
|
image = convert_to_float(image, preserve_range)
|
|
|
|
input_shape = np.array(image.shape)
|
|
|
|
if output_shape is None:
|
|
output_shape = input_shape
|
|
else:
|
|
output_shape = safe_as_int(output_shape)
|
|
|
|
warped = None
|
|
|
|
if order == 2:
|
|
# When fixing this issue, make sure to fix the branches further
|
|
# below in this function
|
|
warn("Bi-quadratic interpolation behavior has changed due "
|
|
"to a bug in the implementation of scikit-image. "
|
|
"The new version now serves as a wrapper "
|
|
"around SciPy's interpolation functions, which itself "
|
|
"is not verified to be a correct implementation. Until "
|
|
"skimage's implementation is fixed, we recommend "
|
|
"to use bi-linear or bi-cubic interpolation instead.")
|
|
|
|
if order in (0, 1, 3) and not map_args:
|
|
# use fast Cython version for specific interpolation orders and input
|
|
|
|
matrix = None
|
|
|
|
if isinstance(inverse_map, np.ndarray) and inverse_map.shape == (3, 3):
|
|
# inverse_map is a transformation matrix as numpy array
|
|
matrix = inverse_map
|
|
|
|
elif isinstance(inverse_map, HOMOGRAPHY_TRANSFORMS):
|
|
# inverse_map is a homography
|
|
matrix = inverse_map.params
|
|
|
|
elif (hasattr(inverse_map, '__name__') and
|
|
inverse_map.__name__ == 'inverse' and
|
|
get_bound_method_class(inverse_map) in HOMOGRAPHY_TRANSFORMS):
|
|
# inverse_map is the inverse of a homography
|
|
matrix = np.linalg.inv(inverse_map.__self__.params)
|
|
|
|
if matrix is not None:
|
|
matrix = matrix.astype(image.dtype)
|
|
ctype = 'float32_t' if image.dtype == np.float32 else 'float64_t'
|
|
if image.ndim == 2:
|
|
warped = _warp_fast[ctype](image, matrix,
|
|
output_shape=output_shape,
|
|
order=order, mode=mode, cval=cval)
|
|
elif image.ndim == 3:
|
|
dims = []
|
|
for dim in range(image.shape[2]):
|
|
dims.append(_warp_fast[ctype](image[..., dim], matrix,
|
|
output_shape=output_shape,
|
|
order=order, mode=mode,
|
|
cval=cval))
|
|
warped = np.dstack(dims)
|
|
|
|
if warped is None:
|
|
# use ndi.map_coordinates
|
|
|
|
if (isinstance(inverse_map, np.ndarray) and
|
|
inverse_map.shape == (3, 3)):
|
|
# inverse_map is a transformation matrix as numpy array,
|
|
# this is only used for order >= 4.
|
|
inverse_map = ProjectiveTransform(matrix=inverse_map)
|
|
|
|
if isinstance(inverse_map, np.ndarray):
|
|
# inverse_map is directly given as coordinates
|
|
coords = inverse_map
|
|
else:
|
|
# inverse_map is given as function, that transforms (N, 2)
|
|
# destination coordinates to their corresponding source
|
|
# coordinates. This is only supported for 2(+1)-D images.
|
|
|
|
if image.ndim < 2 or image.ndim > 3:
|
|
raise ValueError("Only 2-D images (grayscale or color) are "
|
|
"supported, when providing a callable "
|
|
"`inverse_map`.")
|
|
|
|
def coord_map(*args):
|
|
return inverse_map(*args, **map_args)
|
|
|
|
if len(input_shape) == 3 and len(output_shape) == 2:
|
|
# Input image is 2D and has color channel, but output_shape is
|
|
# given for 2-D images. Automatically add the color channel
|
|
# dimensionality.
|
|
output_shape = (output_shape[0], output_shape[1],
|
|
input_shape[2])
|
|
|
|
coords = warp_coords(coord_map, output_shape)
|
|
|
|
# Pre-filtering not necessary for order 0, 1 interpolation
|
|
prefilter = order > 1
|
|
|
|
ndi_mode = _to_ndimage_mode(mode)
|
|
warped = ndi.map_coordinates(image, coords, prefilter=prefilter,
|
|
mode=ndi_mode, order=order, cval=cval)
|
|
|
|
_clip_warp_output(image, warped, order, mode, cval, clip)
|
|
|
|
return warped
|
|
|
|
|
|
def _linear_polar_mapping(output_coords, k_angle, k_radius, center):
|
|
"""Inverse mapping function to convert from cartesian to polar coordinates
|
|
|
|
Parameters
|
|
----------
|
|
output_coords : ndarray
|
|
`(M, 2)` array of `(col, row)` coordinates in the output image
|
|
k_angle : float
|
|
Scaling factor that relates the intended number of rows in the output
|
|
image to angle: ``k_angle = nrows / (2 * np.pi)``
|
|
k_radius : float
|
|
Scaling factor that relates the radius of the circle bounding the
|
|
area to be transformed to the intended number of columns in the output
|
|
image: ``k_radius = ncols / radius``
|
|
center : tuple (row, col)
|
|
Coordinates that represent the center of the circle that bounds the
|
|
area to be transformed in an input image.
|
|
|
|
Returns
|
|
-------
|
|
coords : ndarray
|
|
`(M, 2)` array of `(col, row)` coordinates in the input image that
|
|
correspond to the `output_coords` given as input.
|
|
"""
|
|
angle = output_coords[:, 1] / k_angle
|
|
rr = ((output_coords[:, 0] / k_radius) * np.sin(angle)) + center[0]
|
|
cc = ((output_coords[:, 0] / k_radius) * np.cos(angle)) + center[1]
|
|
coords = np.column_stack((cc, rr))
|
|
return coords
|
|
|
|
|
|
def _log_polar_mapping(output_coords, k_angle, k_radius, center):
|
|
"""Inverse mapping function to convert from cartesian to polar coordinates
|
|
|
|
Parameters
|
|
----------
|
|
output_coords : ndarray
|
|
`(M, 2)` array of `(col, row)` coordinates in the output image
|
|
k_angle : float
|
|
Scaling factor that relates the intended number of rows in the output
|
|
image to angle: ``k_angle = nrows / (2 * np.pi)``
|
|
k_radius : float
|
|
Scaling factor that relates the radius of the circle bounding the
|
|
area to be transformed to the intended number of columns in the output
|
|
image: ``k_radius = width / np.log(radius)``
|
|
center : tuple (row, col)
|
|
Coordinates that represent the center of the circle that bounds the
|
|
area to be transformed in an input image.
|
|
|
|
Returns
|
|
-------
|
|
coords : ndarray
|
|
`(M, 2)` array of `(col, row)` coordinates in the input image that
|
|
correspond to the `output_coords` given as input.
|
|
"""
|
|
angle = output_coords[:, 1] / k_angle
|
|
rr = ((np.exp(output_coords[:, 0] / k_radius)) * np.sin(angle)) + center[0]
|
|
cc = ((np.exp(output_coords[:, 0] / k_radius)) * np.cos(angle)) + center[1]
|
|
coords = np.column_stack((cc, rr))
|
|
return coords
|
|
|
|
|
|
def warp_polar(image, center=None, *, radius=None, output_shape=None,
|
|
scaling='linear', multichannel=False, **kwargs):
|
|
"""Remap image to polar or log-polar coordinates space.
|
|
|
|
Parameters
|
|
----------
|
|
image : ndarray
|
|
Input image. Only 2-D arrays are accepted by default. If
|
|
`multichannel=True`, 3-D arrays are accepted and the last axis is
|
|
interpreted as multiple channels.
|
|
center : tuple (row, col), optional
|
|
Point in image that represents the center of the transformation (i.e.,
|
|
the origin in cartesian space). Values can be of type `float`.
|
|
If no value is given, the center is assumed to be the center point
|
|
of the image.
|
|
radius : float, optional
|
|
Radius of the circle that bounds the area to be transformed.
|
|
output_shape : tuple (row, col), optional
|
|
scaling : {'linear', 'log'}, optional
|
|
Specify whether the image warp is polar or log-polar. Defaults to
|
|
'linear'.
|
|
multichannel : bool, optional
|
|
Whether the image is a 3-D array in which the third axis is to be
|
|
interpreted as multiple channels. If set to `False` (default), only 2-D
|
|
arrays are accepted.
|
|
**kwargs : keyword arguments
|
|
Passed to `transform.warp`.
|
|
|
|
Returns
|
|
-------
|
|
warped : ndarray
|
|
The polar or log-polar warped image.
|
|
|
|
Examples
|
|
--------
|
|
Perform a basic polar warp on a grayscale image:
|
|
|
|
>>> from skimage import data
|
|
>>> from skimage.transform import warp_polar
|
|
>>> image = data.checkerboard()
|
|
>>> warped = warp_polar(image)
|
|
|
|
Perform a log-polar warp on a grayscale image:
|
|
|
|
>>> warped = warp_polar(image, scaling='log')
|
|
|
|
Perform a log-polar warp on a grayscale image while specifying center,
|
|
radius, and output shape:
|
|
|
|
>>> warped = warp_polar(image, (100,100), radius=100,
|
|
... output_shape=image.shape, scaling='log')
|
|
|
|
Perform a log-polar warp on a color image:
|
|
|
|
>>> image = data.astronaut()
|
|
>>> warped = warp_polar(image, scaling='log', multichannel=True)
|
|
"""
|
|
if image.ndim != 2 and not multichannel:
|
|
raise ValueError("Input array must be 2 dimensions "
|
|
"when `multichannel=False`,"
|
|
" got {}".format(image.ndim))
|
|
|
|
if image.ndim != 3 and multichannel:
|
|
raise ValueError("Input array must be 3 dimensions "
|
|
"when `multichannel=True`,"
|
|
" got {}".format(image.ndim))
|
|
|
|
if center is None:
|
|
center = (np.array(image.shape)[:2] / 2) - 0.5
|
|
|
|
if radius is None:
|
|
w, h = np.array(image.shape)[:2] / 2
|
|
radius = np.sqrt(w ** 2 + h ** 2)
|
|
|
|
if output_shape is None:
|
|
height = 360
|
|
width = int(np.ceil(radius))
|
|
output_shape = (height, width)
|
|
else:
|
|
output_shape = safe_as_int(output_shape)
|
|
height = output_shape[0]
|
|
width = output_shape[1]
|
|
|
|
if scaling == 'linear':
|
|
k_radius = width / radius
|
|
map_func = _linear_polar_mapping
|
|
elif scaling == 'log':
|
|
k_radius = width / np.log(radius)
|
|
map_func = _log_polar_mapping
|
|
else:
|
|
raise ValueError("Scaling value must be in {'linear', 'log'}")
|
|
|
|
k_angle = height / (2 * np.pi)
|
|
warp_args = {'k_angle': k_angle, 'k_radius': k_radius, 'center': center}
|
|
|
|
warped = warp(image, map_func, map_args=warp_args,
|
|
output_shape=output_shape, **kwargs)
|
|
|
|
return warped
|