| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404 |
- import numpy as np
- from scipy import ndimage as ndi
- from ._geometric import SimilarityTransform, AffineTransform, ProjectiveTransform
- 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,
- _to_ndimage_mode,
- _validate_interpolation_order,
- channel_as_last_axis,
- )
- HOMOGRAPHY_TRANSFORMS = (SimilarityTransform, AffineTransform, ProjectiveTransform)
- def _preprocess_resize_output_shape(image, output_shape):
- """Validate resize output shape according to input image.
- Parameters
- ----------
- image : ndarray
- Image to be resized.
- output_shape : iterable
- Size of the generated output image `(rows, cols[, ...][, dim])`. If
- `dim` is not provided, the number of channels is preserved.
- Returns
- -------
- image: ndarray
- The input image, but with additional singleton dimensions appended in
- the case where ``len(output_shape) > input.ndim``.
- output_shape: tuple
- The output image converted to tuple.
- Raises
- ------
- ValueError:
- If output_shape length is smaller than the image number of
- dimensions
- Notes
- -----
- The input image is reshaped if its number of dimensions is not
- equal to output_shape_length.
- """
- 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 += (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:
- raise ValueError(
- "output_shape length cannot be smaller than the "
- "image number of dimensions"
- )
- return image, output_shape
- 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 downsampling with an integer factor also see
- `skimage.transform.downscale_local_mean`.
- Parameters
- ----------
- image : ndarray
- Input image.
- output_shape : iterable
- 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. See Notes regarding dtype.
- 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 downsampling. It is crucial to filter when downsampling
- the image to avoid aliasing artifacts. If not specified, it is set to
- True when downsampling an image whose data type is not bool.
- It is also set to False when using nearest neighbor interpolation
- (``order`` == 0) with integer input data type.
- anti_aliasing_sigma : {float, tuple of floats}, optional
- Standard deviation for Gaussian filtering used when anti-aliasing.
- By default, this value is chosen as (s - 1) / 2 where s is the
- downsampling factor, where s > 1. For the up-size case, s < 1, no
- anti-aliasing is performed prior to rescaling.
- See Also
- --------
- scipy.ndimage.zoom
- 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].
- `resize` uses interpolation. Unless the interpolation method is nearest-neighbor
- (``order==0``), the algorithm will generate output values as weighted averages
- of input values. Accordingly, the output dtype is ``float64`` with the following
- exceptions:
- - When ``order==0``, the output dtype is ``image.dtype``.
- - When ``image.dtype`` is ``float16`` or ``float32``, the output dtype is
- ``float32``.
- For a similar function that preserves the dtype of the input, consider
- `scipy.ndimage.zoom`.
- Examples
- --------
- >>> from skimage import data
- >>> from skimage.transform import resize
- >>> image = data.camera()
- >>> resize(image, (100, 100)).shape
- (100, 100)
- """
- image, output_shape = _preprocess_resize_output_shape(image, output_shape)
- input_shape = image.shape
- input_type = image.dtype
- if input_type == np.float16:
- image = image.astype(np.float32)
- if anti_aliasing is None:
- anti_aliasing = (
- not input_type == bool
- and not (np.issubdtype(input_type, np.integer) and order == 0)
- and any(x < y for x, y in zip(output_shape, input_shape))
- )
- if input_type == bool and anti_aliasing:
- raise ValueError("anti_aliasing must be False for boolean images")
- factors = np.divide(input_shape, output_shape)
- order = _validate_interpolation_order(input_type, order)
- if order > 0:
- image = convert_to_float(image, preserve_range)
- # Translate modes used by np.pad to those used by scipy.ndimage
- ndi_mode = _to_ndimage_mode(mode)
- 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"
- )
- filtered = ndi.gaussian_filter(
- image, anti_aliasing_sigma, cval=cval, mode=ndi_mode
- )
- else:
- filtered = image
- zoom_factors = [1 / f for f in factors]
- out = ndi.zoom(
- filtered, zoom_factors, order=order, mode=ndi_mode, cval=cval, grid_mode=True
- )
- _clip_warp_output(image, out, mode, cval, clip)
- return out
- @channel_as_last_axis()
- def rescale(
- image,
- scale,
- order=None,
- mode='reflect',
- cval=0,
- clip=True,
- preserve_range=False,
- anti_aliasing=None,
- anti_aliasing_sigma=None,
- *,
- channel_axis=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 : (M, N[, ...][, C]) ndarray
- Input image.
- scale : {float, tuple of floats}
- Scale factors for spatial dimensions. Separate scale factors can be defined as
- (m, n[, ...]).
- 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
- 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.
- channel_axis : int or None, optional
- If None, the image is assumed to be a grayscale (single channel) image.
- Otherwise, this parameter indicates which axis of the array corresponds
- to channels.
- .. versionadded:: 0.19
- ``channel_axis`` was added in 0.19.
- 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)
- multichannel = channel_axis is not None
- 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.maximum(np.round(scale * orig_shape), 1)
- 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]
- if image.dtype == np.float16:
- image = image.astype(np.float32)
- # rotation around center
- if center is None:
- center = np.array((cols, rows)) / 2.0 - 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 : (M[, ...]) ndarray
- 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 'reflect' 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, 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
- ----------------
- mode : {'constant', 'edge', 'symmetric', 'reflect', 'wrap'}
- Points outside the boundaries of the input are filled according
- to the given mode. Modes match the behaviour of `numpy.pad`.
- cval : float
- Used in conjunction with mode 'constant', the value outside
- the image boundaries.
- clip : bool
- 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:
- min_val = np.min(input_image)
- if np.isnan(min_val):
- # NaNs detected, use NaN-safe min/max
- min_func = np.nanmin
- max_func = np.nanmax
- min_val = min_func(input_image)
- else:
- min_func = np.min
- max_func = np.max
- max_val = max_func(input_image)
- # Check if cval has been used such that it expands the effective input
- # range
- preserve_cval = (
- mode == 'constant'
- and not min_val <= cval <= max_val
- and min_func(output_image) <= cval <= max_func(output_image)
- )
- # expand min/max range to account for cval
- if preserve_cval:
- # cast cval to the same dtype as the input image
- cval = input_image.dtype.type(cval)
- min_val = min(min_val, cval)
- max_val = max(max_val, cval)
- # Convert array-like types to ndarrays (gh-7159)
- min_val, max_val = np.asarray(min_val), np.asarray(max_val)
- np.clip(output_image, min_val, max_val, out=output_image)
- def warp(
- image,
- inverse_map,
- map_args=None,
- output_shape=None,
- order=None,
- mode='constant',
- cval=0.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])
- >>> rng = np.random.default_rng()
- >>> cube = rng.random(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 map_args is None:
- map_args = {}
- if image.size == 0:
- raise ValueError("Cannot warp empty image with dimensions", image.shape)
- order = _validate_interpolation_order(image.dtype, order)
- if order > 0:
- image = convert_to_float(image, preserve_range)
- if image.dtype == np.float16:
- image = image.astype(np.float32)
- 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 (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, 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 : (M, 2) ndarray
- 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 : (M, 2) ndarray
- 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 : (M, 2) ndarray
- 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 : 2-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, shape (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
- @channel_as_last_axis()
- def warp_polar(
- image,
- center=None,
- *,
- radius=None,
- output_shape=None,
- scaling='linear',
- channel_axis=None,
- **kwargs,
- ):
- """Remap image to polar or log-polar coordinates space.
- Parameters
- ----------
- image : (M, N[, C]) ndarray
- Input image. For multichannel images `channel_axis` has to be specified.
- center : 2-tuple, optional
- `(row, col)` coordinates of the 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 `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'.
- channel_axis : int or None, optional
- If None, the image is assumed to be a grayscale (single channel) image.
- Otherwise, this parameter indicates which axis of the array corresponds
- to channels.
- .. versionadded:: 0.19
- ``channel_axis`` was added in 0.19.
- **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', channel_axis=-1)
- """
- multichannel = channel_axis is not None
- if image.ndim != 2 and not multichannel:
- raise ValueError(
- f'Input array must be 2-dimensional when '
- f'`channel_axis=None`, got {image.ndim}'
- )
- if image.ndim != 3 and multichannel:
- raise ValueError(
- f'Input array must be 3-dimensional when '
- f'`channel_axis` is specified, got {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
- def _local_mean_weights(old_size, new_size, grid_mode, dtype):
- """Create a 2D weight matrix for resizing with the local mean.
- Parameters
- ----------
- old_size : int
- Old size.
- new_size : int
- New size.
- grid_mode : bool
- Whether to use grid data model of pixel/voxel model for
- average weights computation.
- dtype : dtype
- Output array data type.
- Returns
- -------
- weights: (new_size, old_size) array
- Rows sum to 1.
- """
- if grid_mode:
- old_breaks = np.linspace(0, old_size, num=old_size + 1, dtype=dtype)
- new_breaks = np.linspace(0, old_size, num=new_size + 1, dtype=dtype)
- else:
- old, new = old_size - 1, new_size - 1
- old_breaks = np.pad(
- np.linspace(0.5, old - 0.5, old, dtype=dtype),
- 1,
- 'constant',
- constant_values=(0, old),
- )
- if new == 0:
- val = np.inf
- else:
- val = 0.5 * old / new
- new_breaks = np.pad(
- np.linspace(val, old - val, new, dtype=dtype),
- 1,
- 'constant',
- constant_values=(0, old),
- )
- upper = np.minimum(new_breaks[1:, np.newaxis], old_breaks[np.newaxis, 1:])
- lower = np.maximum(new_breaks[:-1, np.newaxis], old_breaks[np.newaxis, :-1])
- weights = np.maximum(upper - lower, 0)
- weights /= weights.sum(axis=1, keepdims=True)
- return weights
- def resize_local_mean(
- image, output_shape, grid_mode=True, preserve_range=False, *, channel_axis=None
- ):
- """Resize an array with the local mean / bilinear scaling.
- Parameters
- ----------
- image : ndarray
- Input image. If this is a multichannel image, the axis corresponding
- to channels should be specified using `channel_axis`.
- output_shape : iterable
- Size of the generated output image. When `channel_axis` is not None,
- the `channel_axis` should either be omitted from `output_shape` or the
- ``output_shape[channel_axis]`` must match
- ``image.shape[channel_axis]``. If the length of `output_shape` exceeds
- image.ndim, additional singleton dimensions will be appended to the
- input ``image`` as needed.
- grid_mode : bool, optional
- Defines ``image`` pixels position: if True, pixels are assumed to be at
- grid intersections, otherwise at cell centers. As a consequence,
- for example, a 1d signal of length 5 is considered to have length 4
- when `grid_mode` is False, but length 5 when `grid_mode` is True. See
- the following visual illustration:
- .. code-block:: text
- | pixel 1 | pixel 2 | pixel 3 | pixel 4 | pixel 5 |
- |<-------------------------------------->|
- vs.
- |<----------------------------------------------->|
- The starting point of the arrow in the diagram above corresponds to
- coordinate location 0 in each mode.
- 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
- -------
- resized : ndarray
- Resized version of the input.
- See Also
- --------
- resize, downscale_local_mean
- Notes
- -----
- This method is sometimes referred to as "area-based" interpolation or
- "pixel mixing" interpolation [1]_. When `grid_mode` is True, it is
- equivalent to using OpenCV's resize with `INTER_AREA` interpolation mode.
- It is commonly used for image downsizing. If the downsizing factors are
- integers, then `downscale_local_mean` should be preferred instead.
- References
- ----------
- .. [1] http://entropymine.com/imageworsener/pixelmixing/
- Examples
- --------
- >>> from skimage import data
- >>> from skimage.transform import resize_local_mean
- >>> image = data.camera()
- >>> resize_local_mean(image, (100, 100)).shape
- (100, 100)
- """
- if channel_axis is not None:
- if channel_axis < -image.ndim or channel_axis >= image.ndim:
- raise ValueError("invalid channel_axis")
- # move channels to last position
- image = np.moveaxis(image, channel_axis, -1)
- nc = image.shape[-1]
- output_ndim = len(output_shape)
- if output_ndim == image.ndim - 1:
- # insert channels dimension at the end
- output_shape = output_shape + (nc,)
- elif output_ndim == image.ndim:
- if output_shape[channel_axis] != nc:
- raise ValueError(
- "Cannot reshape along the channel_axis. Use "
- "channel_axis=None to reshape along all axes."
- )
- # move channels to last position in output_shape
- channel_axis = channel_axis % image.ndim
- output_shape = (
- output_shape[:channel_axis] + output_shape[channel_axis:] + (nc,)
- )
- else:
- raise ValueError(
- "len(output_shape) must be image.ndim or (image.ndim - 1) "
- "when a channel_axis is specified."
- )
- resized = image
- else:
- resized, output_shape = _preprocess_resize_output_shape(image, output_shape)
- resized = convert_to_float(resized, preserve_range)
- dtype = resized.dtype
- for axis, (old_size, new_size) in enumerate(zip(image.shape, output_shape)):
- if old_size == new_size:
- continue
- weights = _local_mean_weights(old_size, new_size, grid_mode, dtype)
- product = np.tensordot(resized, weights, [[axis], [-1]])
- resized = np.moveaxis(product, -1, axis)
- if channel_axis is not None:
- # restore channels to original axis
- resized = np.moveaxis(resized, -1, channel_axis)
- return resized
|