"Fossies" - the Fresh Open Source Software Archive

Member "scikit-image-0.19.3/skimage/transform/_warps.py" (12 Jun 2022, 50955 Bytes) of package /linux/misc/scikit-image-0.19.3.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "_warps.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 0.19.2_vs_0.19.3.

    1 import numpy as np
    2 from numpy.lib import NumpyVersion
    3 import scipy
    4 from scipy import ndimage as ndi
    5 
    6 from ._geometric import (SimilarityTransform, AffineTransform,
    7                          ProjectiveTransform)
    8 from ._warps_cy import _warp_fast
    9 from ..measure import block_reduce
   10 
   11 from .._shared.utils import (get_bound_method_class, safe_as_int, warn,
   12                              convert_to_float, _to_ndimage_mode,
   13                              _validate_interpolation_order,
   14                              channel_as_last_axis,
   15                              deprecate_multichannel_kwarg)
   16 
   17 HOMOGRAPHY_TRANSFORMS = (
   18     SimilarityTransform,
   19     AffineTransform,
   20     ProjectiveTransform
   21 )
   22 
   23 
   24 def _preprocess_resize_output_shape(image, output_shape):
   25     """Validate resize output shape according to input image.
   26 
   27     Parameters
   28     ----------
   29     image: ndarray
   30         Image to be resized.
   31     output_shape: iterable
   32         Size of the generated output image `(rows, cols[, ...][, dim])`. If
   33         `dim` is not provided, the number of channels is preserved.
   34 
   35     Returns
   36     -------
   37     image: ndarray
   38         The input image, but with additional singleton dimensions appended in
   39         the case where ``len(output_shape) > input.ndim``.
   40     output_shape: tuple
   41         The output image converted to tuple.
   42 
   43     Raises
   44     ------
   45     ValueError:
   46         If output_shape length is smaller than the image number of
   47         dimensions
   48 
   49     Notes
   50     -----
   51     The input image is reshaped if its number of dimensions is not
   52     equal to output_shape_length.
   53 
   54     """
   55     output_shape = tuple(output_shape)
   56     output_ndim = len(output_shape)
   57     input_shape = image.shape
   58     if output_ndim > image.ndim:
   59         # append dimensions to input_shape
   60         input_shape += (1, ) * (output_ndim - image.ndim)
   61         image = np.reshape(image, input_shape)
   62     elif output_ndim == image.ndim - 1:
   63         # multichannel case: append shape of last axis
   64         output_shape = output_shape + (image.shape[-1], )
   65     elif output_ndim < image.ndim:
   66         raise ValueError("output_shape length cannot be smaller than the "
   67                          "image number of dimensions")
   68 
   69     return image, output_shape
   70 
   71 
   72 def resize(image, output_shape, order=None, mode='reflect', cval=0, clip=True,
   73            preserve_range=False, anti_aliasing=None, anti_aliasing_sigma=None):
   74     """Resize image to match a certain size.
   75 
   76     Performs interpolation to up-size or down-size N-dimensional images. Note
   77     that anti-aliasing should be enabled when down-sizing images to avoid
   78     aliasing artifacts. For downsampling with an integer factor also see
   79     `skimage.transform.downscale_local_mean`.
   80 
   81     Parameters
   82     ----------
   83     image : ndarray
   84         Input image.
   85     output_shape : iterable
   86         Size of the generated output image `(rows, cols[, ...][, dim])`. If
   87         `dim` is not provided, the number of channels is preserved. In case the
   88         number of input channels does not equal the number of output channels a
   89         n-dimensional interpolation is applied.
   90 
   91     Returns
   92     -------
   93     resized : ndarray
   94         Resized version of the input.
   95 
   96     Other parameters
   97     ----------------
   98     order : int, optional
   99         The order of the spline interpolation, default is 0 if
  100         image.dtype is bool and 1 otherwise. The order has to be in
  101         the range 0-5. See `skimage.transform.warp` for detail.
  102     mode : {'constant', 'edge', 'symmetric', 'reflect', 'wrap'}, optional
  103         Points outside the boundaries of the input are filled according
  104         to the given mode.  Modes match the behaviour of `numpy.pad`.
  105     cval : float, optional
  106         Used in conjunction with mode 'constant', the value outside
  107         the image boundaries.
  108     clip : bool, optional
  109         Whether to clip the output to the range of values of the input image.
  110         This is enabled by default, since higher order interpolation may
  111         produce values outside the given input range.
  112     preserve_range : bool, optional
  113         Whether to keep the original range of values. Otherwise, the input
  114         image is converted according to the conventions of `img_as_float`.
  115         Also see https://scikit-image.org/docs/dev/user_guide/data_types.html
  116     anti_aliasing : bool, optional
  117         Whether to apply a Gaussian filter to smooth the image prior
  118         to downsampling. It is crucial to filter when downsampling
  119         the image to avoid aliasing artifacts. If not specified, it is set to
  120         True when downsampling an image whose data type is not bool.
  121     anti_aliasing_sigma : {float, tuple of floats}, optional
  122         Standard deviation for Gaussian filtering used when anti-aliasing.
  123         By default, this value is chosen as (s - 1) / 2 where s is the
  124         downsampling factor, where s > 1. For the up-size case, s < 1, no
  125         anti-aliasing is performed prior to rescaling.
  126 
  127     Notes
  128     -----
  129     Modes 'reflect' and 'symmetric' are similar, but differ in whether the edge
  130     pixels are duplicated during the reflection.  As an example, if an array
  131     has values [0, 1, 2] and was padded to the right by four values using
  132     symmetric, the result would be [0, 1, 2, 2, 1, 0, 0], while for reflect it
  133     would be [0, 1, 2, 1, 0, 1, 2].
  134 
  135     Examples
  136     --------
  137     >>> from skimage import data
  138     >>> from skimage.transform import resize
  139     >>> image = data.camera()
  140     >>> resize(image, (100, 100)).shape
  141     (100, 100)
  142 
  143     """
  144 
  145     image, output_shape = _preprocess_resize_output_shape(image, output_shape)
  146     input_shape = image.shape
  147     input_type = image.dtype
  148 
  149     if input_type == np.float16:
  150         image = image.astype(np.float32)
  151 
  152     if anti_aliasing is None:
  153         anti_aliasing = (not input_type == bool and
  154                          any(x < y for x, y in zip(output_shape, input_shape)))
  155 
  156     if input_type == bool and anti_aliasing:
  157         raise ValueError("anti_aliasing must be False for boolean images")
  158 
  159     factors = np.divide(input_shape, output_shape)
  160     order = _validate_interpolation_order(input_type, order)
  161     if order > 0:
  162         image = convert_to_float(image, preserve_range)
  163 
  164     # Save input value range for clip
  165     img_bounds = np.array([image.min(), image.max()]) if clip else None
  166 
  167     # Translate modes used by np.pad to those used by scipy.ndimage
  168     ndi_mode = _to_ndimage_mode(mode)
  169     if anti_aliasing:
  170         if anti_aliasing_sigma is None:
  171             anti_aliasing_sigma = np.maximum(0, (factors - 1) / 2)
  172         else:
  173             anti_aliasing_sigma = \
  174                 np.atleast_1d(anti_aliasing_sigma) * np.ones_like(factors)
  175             if np.any(anti_aliasing_sigma < 0):
  176                 raise ValueError("Anti-aliasing standard deviation must be "
  177                                  "greater than or equal to zero")
  178             elif np.any((anti_aliasing_sigma > 0) & (factors <= 1)):
  179                 warn("Anti-aliasing standard deviation greater than zero but "
  180                      "not down-sampling along all axes")
  181         image = ndi.gaussian_filter(image, anti_aliasing_sigma,
  182                                     cval=cval, mode=ndi_mode)
  183 
  184     if NumpyVersion(scipy.__version__) >= '1.6.0':
  185         # The grid_mode kwarg was introduced in SciPy 1.6.0
  186         zoom_factors = [1 / f for f in factors]
  187         out = ndi.zoom(image, zoom_factors, order=order, mode=ndi_mode,
  188                        cval=cval, grid_mode=True)
  189 
  190     # TODO: Remove the fallback code below once SciPy >= 1.6.0 is required.
  191 
  192     # 2-dimensional interpolation
  193     elif len(output_shape) == 2 or (len(output_shape) == 3 and
  194                                     output_shape[2] == input_shape[2]):
  195         rows = output_shape[0]
  196         cols = output_shape[1]
  197         input_rows = input_shape[0]
  198         input_cols = input_shape[1]
  199         if rows == 1 and cols == 1:
  200             tform = AffineTransform(translation=(input_cols / 2.0 - 0.5,
  201                                                  input_rows / 2.0 - 0.5))
  202         else:
  203             # 3 control points necessary to estimate exact AffineTransform
  204             src_corners = np.array([[1, 1], [1, rows], [cols, rows]]) - 1
  205             dst_corners = np.zeros(src_corners.shape, dtype=np.double)
  206             # take into account that 0th pixel is at position (0.5, 0.5)
  207             dst_corners[:, 0] = factors[1] * (src_corners[:, 0] + 0.5) - 0.5
  208             dst_corners[:, 1] = factors[0] * (src_corners[:, 1] + 0.5) - 0.5
  209 
  210             tform = AffineTransform()
  211             tform.estimate(src_corners, dst_corners)
  212 
  213         # Make sure the transform is exactly metric, to ensure fast warping.
  214         tform.params[2] = (0, 0, 1)
  215         tform.params[0, 1] = 0
  216         tform.params[1, 0] = 0
  217 
  218         # clip outside of warp to clip w.r.t input values, not filtered values.
  219         out = warp(image, tform, output_shape=output_shape, order=order,
  220                    mode=mode, cval=cval, clip=False,
  221                    preserve_range=preserve_range)
  222 
  223     else:  # n-dimensional interpolation
  224 
  225         coord_arrays = [factors[i] * (np.arange(d) + 0.5) - 0.5
  226                         for i, d in enumerate(output_shape)]
  227 
  228         coord_map = np.array(np.meshgrid(*coord_arrays,
  229                                          sparse=False,
  230                                          indexing='ij'))
  231 
  232         out = ndi.map_coordinates(image, coord_map, order=order,
  233                                   mode=ndi_mode, cval=cval)
  234 
  235     _clip_warp_output(img_bounds, out, mode, cval, clip)
  236 
  237     return out
  238 
  239 
  240 @channel_as_last_axis()
  241 @deprecate_multichannel_kwarg(multichannel_position=7)
  242 def rescale(image, scale, order=None, mode='reflect', cval=0, clip=True,
  243             preserve_range=False, multichannel=False,
  244             anti_aliasing=None, anti_aliasing_sigma=None, *,
  245             channel_axis=None):
  246     """Scale image by a certain factor.
  247 
  248     Performs interpolation to up-scale or down-scale N-dimensional images.
  249     Note that anti-aliasing should be enabled when down-sizing images to avoid
  250     aliasing artifacts. For down-sampling with an integer factor also see
  251     `skimage.transform.downscale_local_mean`.
  252 
  253     Parameters
  254     ----------
  255     image : ndarray
  256         Input image.
  257     scale : {float, tuple of floats}
  258         Scale factors. Separate scale factors can be defined as
  259         `(rows, cols[, ...][, dim])`.
  260 
  261     Returns
  262     -------
  263     scaled : ndarray
  264         Scaled version of the input.
  265 
  266     Other parameters
  267     ----------------
  268     order : int, optional
  269         The order of the spline interpolation, default is 0 if
  270         image.dtype is bool and 1 otherwise. The order has to be in
  271         the range 0-5. See `skimage.transform.warp` for detail.
  272     mode : {'constant', 'edge', 'symmetric', 'reflect', 'wrap'}, optional
  273         Points outside the boundaries of the input are filled according
  274         to the given mode.  Modes match the behaviour of `numpy.pad`.
  275     cval : float, optional
  276         Used in conjunction with mode 'constant', the value outside
  277         the image boundaries.
  278     clip : bool, optional
  279         Whether to clip the output to the range of values of the input image.
  280         This is enabled by default, since higher order interpolation may
  281         produce values outside the given input range.
  282     preserve_range : bool, optional
  283         Whether to keep the original range of values. Otherwise, the input
  284         image is converted according to the conventions of `img_as_float`.
  285         Also see
  286         https://scikit-image.org/docs/dev/user_guide/data_types.html
  287     multichannel : bool, optional
  288         Whether the last axis of the image is to be interpreted as multiple
  289         channels or another spatial dimension. This argument is deprecated:
  290         specify `channel_axis` instead.
  291     anti_aliasing : bool, optional
  292         Whether to apply a Gaussian filter to smooth the image prior
  293         to down-scaling. It is crucial to filter when down-sampling
  294         the image to avoid aliasing artifacts. If input image data
  295         type is bool, no anti-aliasing is applied.
  296     anti_aliasing_sigma : {float, tuple of floats}, optional
  297         Standard deviation for Gaussian filtering to avoid aliasing artifacts.
  298         By default, this value is chosen as (s - 1) / 2 where s is the
  299         down-scaling factor.
  300     channel_axis : int or None, optional
  301         If None, the image is assumed to be a grayscale (single channel) image.
  302         Otherwise, this parameter indicates which axis of the array corresponds
  303         to channels.
  304 
  305         .. versionadded:: 0.19
  306            ``channel_axis`` was added in 0.19.
  307 
  308     Notes
  309     -----
  310     Modes 'reflect' and 'symmetric' are similar, but differ in whether the edge
  311     pixels are duplicated during the reflection.  As an example, if an array
  312     has values [0, 1, 2] and was padded to the right by four values using
  313     symmetric, the result would be [0, 1, 2, 2, 1, 0, 0], while for reflect it
  314     would be [0, 1, 2, 1, 0, 1, 2].
  315 
  316     Examples
  317     --------
  318     >>> from skimage import data
  319     >>> from skimage.transform import rescale
  320     >>> image = data.camera()
  321     >>> rescale(image, 0.1).shape
  322     (51, 51)
  323     >>> rescale(image, 0.5).shape
  324     (256, 256)
  325 
  326     """
  327     scale = np.atleast_1d(scale)
  328     multichannel = channel_axis is not None
  329     if len(scale) > 1:
  330         if ((not multichannel and len(scale) != image.ndim) or
  331                 (multichannel and len(scale) != image.ndim - 1)):
  332             raise ValueError("Supply a single scale, or one value per spatial "
  333                              "axis")
  334         if multichannel:
  335             scale = np.concatenate((scale, [1]))
  336     orig_shape = np.asarray(image.shape)
  337     output_shape = np.maximum(np.round(scale * orig_shape), 1)
  338     if multichannel:  # don't scale channel dimension
  339         output_shape[-1] = orig_shape[-1]
  340 
  341     return resize(image, output_shape, order=order, mode=mode, cval=cval,
  342                   clip=clip, preserve_range=preserve_range,
  343                   anti_aliasing=anti_aliasing,
  344                   anti_aliasing_sigma=anti_aliasing_sigma)
  345 
  346 
  347 def rotate(image, angle, resize=False, center=None, order=None,
  348            mode='constant', cval=0, clip=True, preserve_range=False):
  349     """Rotate image by a certain angle around its center.
  350 
  351     Parameters
  352     ----------
  353     image : ndarray
  354         Input image.
  355     angle : float
  356         Rotation angle in degrees in counter-clockwise direction.
  357     resize : bool, optional
  358         Determine whether the shape of the output image will be automatically
  359         calculated, so the complete rotated image exactly fits. Default is
  360         False.
  361     center : iterable of length 2
  362         The rotation center. If ``center=None``, the image is rotated around
  363         its center, i.e. ``center=(cols / 2 - 0.5, rows / 2 - 0.5)``.  Please
  364         note that this parameter is (cols, rows), contrary to normal skimage
  365         ordering.
  366 
  367     Returns
  368     -------
  369     rotated : ndarray
  370         Rotated version of the input.
  371 
  372     Other parameters
  373     ----------------
  374     order : int, optional
  375         The order of the spline interpolation, default is 0 if
  376         image.dtype is bool and 1 otherwise. The order has to be in
  377         the range 0-5. See `skimage.transform.warp` for detail.
  378     mode : {'constant', 'edge', 'symmetric', 'reflect', 'wrap'}, optional
  379         Points outside the boundaries of the input are filled according
  380         to the given mode.  Modes match the behaviour of `numpy.pad`.
  381     cval : float, optional
  382         Used in conjunction with mode 'constant', the value outside
  383         the image boundaries.
  384     clip : bool, optional
  385         Whether to clip the output to the range of values of the input image.
  386         This is enabled by default, since higher order interpolation may
  387         produce values outside the given input range.
  388     preserve_range : bool, optional
  389         Whether to keep the original range of values. Otherwise, the input
  390         image is converted according to the conventions of `img_as_float`.
  391         Also see
  392         https://scikit-image.org/docs/dev/user_guide/data_types.html
  393 
  394     Notes
  395     -----
  396     Modes 'reflect' and 'symmetric' are similar, but differ in whether the edge
  397     pixels are duplicated during the reflection.  As an example, if an array
  398     has values [0, 1, 2] and was padded to the right by four values using
  399     symmetric, the result would be [0, 1, 2, 2, 1, 0, 0], while for reflect it
  400     would be [0, 1, 2, 1, 0, 1, 2].
  401 
  402     Examples
  403     --------
  404     >>> from skimage import data
  405     >>> from skimage.transform import rotate
  406     >>> image = data.camera()
  407     >>> rotate(image, 2).shape
  408     (512, 512)
  409     >>> rotate(image, 2, resize=True).shape
  410     (530, 530)
  411     >>> rotate(image, 90, resize=True).shape
  412     (512, 512)
  413 
  414     """
  415 
  416     rows, cols = image.shape[0], image.shape[1]
  417 
  418     if image.dtype == np.float16:
  419         image = image.astype(np.float32)
  420 
  421     # rotation around center
  422     if center is None:
  423         center = np.array((cols, rows)) / 2. - 0.5
  424     else:
  425         center = np.asarray(center)
  426     tform1 = SimilarityTransform(translation=center)
  427     tform2 = SimilarityTransform(rotation=np.deg2rad(angle))
  428     tform3 = SimilarityTransform(translation=-center)
  429     tform = tform3 + tform2 + tform1
  430 
  431     output_shape = None
  432     if resize:
  433         # determine shape of output image
  434         corners = np.array([
  435             [0, 0],
  436             [0, rows - 1],
  437             [cols - 1, rows - 1],
  438             [cols - 1, 0]
  439         ])
  440         corners = tform.inverse(corners)
  441         minc = corners[:, 0].min()
  442         minr = corners[:, 1].min()
  443         maxc = corners[:, 0].max()
  444         maxr = corners[:, 1].max()
  445         out_rows = maxr - minr + 1
  446         out_cols = maxc - minc + 1
  447         output_shape = np.around((out_rows, out_cols))
  448 
  449         # fit output image in new shape
  450         translation = (minc, minr)
  451         tform4 = SimilarityTransform(translation=translation)
  452         tform = tform4 + tform
  453 
  454     # Make sure the transform is exactly affine, to ensure fast warping.
  455     tform.params[2] = (0, 0, 1)
  456 
  457     return warp(image, tform, output_shape=output_shape, order=order,
  458                 mode=mode, cval=cval, clip=clip, preserve_range=preserve_range)
  459 
  460 
  461 def downscale_local_mean(image, factors, cval=0, clip=True):
  462     """Down-sample N-dimensional image by local averaging.
  463 
  464     The image is padded with `cval` if it is not perfectly divisible by the
  465     integer factors.
  466 
  467     In contrast to interpolation in `skimage.transform.resize` and
  468     `skimage.transform.rescale` this function calculates the local mean of
  469     elements in each block of size `factors` in the input image.
  470 
  471     Parameters
  472     ----------
  473     image : ndarray
  474         N-dimensional input image.
  475     factors : array_like
  476         Array containing down-sampling integer factor along each axis.
  477     cval : float, optional
  478         Constant padding value if image is not perfectly divisible by the
  479         integer factors.
  480     clip : bool, optional
  481         Unused, but kept here for API consistency with the other transforms
  482         in this module. (The local mean will never fall outside the range
  483         of values in the input image, assuming the provided `cval` also
  484         falls within that range.)
  485 
  486     Returns
  487     -------
  488     image : ndarray
  489         Down-sampled image with same number of dimensions as input image.
  490         For integer inputs, the output dtype will be ``float64``.
  491         See :func:`numpy.mean` for details.
  492 
  493     Examples
  494     --------
  495     >>> a = np.arange(15).reshape(3, 5)
  496     >>> a
  497     array([[ 0,  1,  2,  3,  4],
  498            [ 5,  6,  7,  8,  9],
  499            [10, 11, 12, 13, 14]])
  500     >>> downscale_local_mean(a, (2, 3))
  501     array([[3.5, 4. ],
  502            [5.5, 4.5]])
  503 
  504     """
  505     return block_reduce(image, factors, np.mean, cval)
  506 
  507 
  508 def _swirl_mapping(xy, center, rotation, strength, radius):
  509     x, y = xy.T
  510     x0, y0 = center
  511     rho = np.sqrt((x - x0) ** 2 + (y - y0) ** 2)
  512 
  513     # Ensure that the transformation decays to approximately 1/1000-th
  514     # within the specified radius.
  515     radius = radius / 5 * np.log(2)
  516 
  517     theta = rotation + strength * \
  518         np.exp(-rho / radius) + \
  519         np.arctan2(y - y0, x - x0)
  520 
  521     xy[..., 0] = x0 + rho * np.cos(theta)
  522     xy[..., 1] = y0 + rho * np.sin(theta)
  523 
  524     return xy
  525 
  526 
  527 def swirl(image, center=None, strength=1, radius=100, rotation=0,
  528           output_shape=None, order=None, mode='reflect', cval=0, clip=True,
  529           preserve_range=False):
  530     """Perform a swirl transformation.
  531 
  532     Parameters
  533     ----------
  534     image : ndarray
  535         Input image.
  536     center : (column, row) tuple or (2,) ndarray, optional
  537         Center coordinate of transformation.
  538     strength : float, optional
  539         The amount of swirling applied.
  540     radius : float, optional
  541         The extent of the swirl in pixels.  The effect dies out
  542         rapidly beyond `radius`.
  543     rotation : float, optional
  544         Additional rotation applied to the image.
  545 
  546     Returns
  547     -------
  548     swirled : ndarray
  549         Swirled version of the input.
  550 
  551     Other parameters
  552     ----------------
  553     output_shape : tuple (rows, cols), optional
  554         Shape of the output image generated. By default the shape of the input
  555         image is preserved.
  556     order : int, optional
  557         The order of the spline interpolation, default is 0 if
  558         image.dtype is bool and 1 otherwise. The order has to be in
  559         the range 0-5. See `skimage.transform.warp` for detail.
  560     mode : {'constant', 'edge', 'symmetric', 'reflect', 'wrap'}, optional
  561         Points outside the boundaries of the input are filled according
  562         to the given mode, with 'constant' used as the default. Modes match
  563         the behaviour of `numpy.pad`.
  564     cval : float, optional
  565         Used in conjunction with mode 'constant', the value outside
  566         the image boundaries.
  567     clip : bool, optional
  568         Whether to clip the output to the range of values of the input image.
  569         This is enabled by default, since higher order interpolation may
  570         produce values outside the given input range.
  571     preserve_range : bool, optional
  572         Whether to keep the original range of values. Otherwise, the input
  573         image is converted according to the conventions of `img_as_float`.
  574         Also see
  575         https://scikit-image.org/docs/dev/user_guide/data_types.html
  576 
  577     """
  578     if center is None:
  579         center = np.array(image.shape)[:2][::-1] / 2
  580 
  581     warp_args = {'center': center,
  582                  'rotation': rotation,
  583                  'strength': strength,
  584                  'radius': radius}
  585 
  586     return warp(image, _swirl_mapping, map_args=warp_args,
  587                 output_shape=output_shape, order=order, mode=mode, cval=cval,
  588                 clip=clip, preserve_range=preserve_range)
  589 
  590 
  591 def _stackcopy(a, b):
  592     """Copy b into each color layer of a, such that::
  593 
  594       a[:,:,0] = a[:,:,1] = ... = b
  595 
  596     Parameters
  597     ----------
  598     a : (M, N) or (M, N, P) ndarray
  599         Target array.
  600     b : (M, N)
  601         Source array.
  602 
  603     Notes
  604     -----
  605     Color images are stored as an ``(M, N, 3)`` or ``(M, N, 4)`` arrays.
  606 
  607     """
  608     if a.ndim == 3:
  609         a[:] = b[:, :, np.newaxis]
  610     else:
  611         a[:] = b
  612 
  613 
  614 def warp_coords(coord_map, shape, dtype=np.float64):
  615     """Build the source coordinates for the output of a 2-D image warp.
  616 
  617     Parameters
  618     ----------
  619     coord_map : callable like GeometricTransform.inverse
  620         Return input coordinates for given output coordinates.
  621         Coordinates are in the shape (P, 2), where P is the number
  622         of coordinates and each element is a ``(row, col)`` pair.
  623     shape : tuple
  624         Shape of output image ``(rows, cols[, bands])``.
  625     dtype : np.dtype or string
  626         dtype for return value (sane choices: float32 or float64).
  627 
  628     Returns
  629     -------
  630     coords : (ndim, rows, cols[, bands]) array of dtype `dtype`
  631             Coordinates for `scipy.ndimage.map_coordinates`, that will yield
  632             an image of shape (orows, ocols, bands) by drawing from source
  633             points according to the `coord_transform_fn`.
  634 
  635     Notes
  636     -----
  637 
  638     This is a lower-level routine that produces the source coordinates for 2-D
  639     images used by `warp()`.
  640 
  641     It is provided separately from `warp` to give additional flexibility to
  642     users who would like, for example, to re-use a particular coordinate
  643     mapping, to use specific dtypes at various points along the the
  644     image-warping process, or to implement different post-processing logic
  645     than `warp` performs after the call to `ndi.map_coordinates`.
  646 
  647 
  648     Examples
  649     --------
  650     Produce a coordinate map that shifts an image up and to the right:
  651 
  652     >>> from skimage import data
  653     >>> from scipy.ndimage import map_coordinates
  654     >>>
  655     >>> def shift_up10_left20(xy):
  656     ...     return xy - np.array([-20, 10])[None, :]
  657     >>>
  658     >>> image = data.astronaut().astype(np.float32)
  659     >>> coords = warp_coords(shift_up10_left20, image.shape)
  660     >>> warped_image = map_coordinates(image, coords)
  661 
  662     """
  663     shape = safe_as_int(shape)
  664     rows, cols = shape[0], shape[1]
  665     coords_shape = [len(shape), rows, cols]
  666     if len(shape) == 3:
  667         coords_shape.append(shape[2])
  668     coords = np.empty(coords_shape, dtype=dtype)
  669 
  670     # Reshape grid coordinates into a (P, 2) array of (row, col) pairs
  671     tf_coords = np.indices((cols, rows), dtype=dtype).reshape(2, -1).T
  672 
  673     # Map each (row, col) pair to the source image according to
  674     # the user-provided mapping
  675     tf_coords = coord_map(tf_coords)
  676 
  677     # Reshape back to a (2, M, N) coordinate grid
  678     tf_coords = tf_coords.T.reshape((-1, cols, rows)).swapaxes(1, 2)
  679 
  680     # Place the y-coordinate mapping
  681     _stackcopy(coords[1, ...], tf_coords[0, ...])
  682 
  683     # Place the x-coordinate mapping
  684     _stackcopy(coords[0, ...], tf_coords[1, ...])
  685 
  686     if len(shape) == 3:
  687         coords[2, ...] = range(shape[2])
  688 
  689     return coords
  690 
  691 
  692 def _clip_warp_output(input_image, output_image, mode, cval, clip):
  693     """Clip output image to range of values of input image.
  694 
  695     Note that this function modifies the values of `output_image` in-place
  696     and it is only modified if ``clip=True``.
  697 
  698     Parameters
  699     ----------
  700     input_image : ndarray
  701         Input image.
  702     output_image : ndarray
  703         Output image, which is modified in-place.
  704 
  705     Other parameters
  706     ----------------
  707     mode : {'constant', 'edge', 'symmetric', 'reflect', 'wrap'}
  708         Points outside the boundaries of the input are filled according
  709         to the given mode.  Modes match the behaviour of `numpy.pad`.
  710     cval : float
  711         Used in conjunction with mode 'constant', the value outside
  712         the image boundaries.
  713     clip : bool
  714         Whether to clip the output to the range of values of the input image.
  715         This is enabled by default, since higher order interpolation may
  716         produce values outside the given input range.
  717 
  718     """
  719     if clip:
  720         min_val = np.min(input_image)
  721         if np.isnan(min_val):
  722             # NaNs detected, use NaN-safe min/max
  723             min_func = np.nanmin
  724             max_func = np.nanmax
  725             min_val = min_func(input_image)
  726         else:
  727             min_func = np.min
  728             max_func = np.max
  729         max_val = max_func(input_image)
  730 
  731         # Check if cval has been used such that it expands the effective input
  732         # range
  733         preserve_cval = (mode == 'constant'
  734                          and not min_val <= cval <= max_val
  735                          and min_func(output_image) <= cval <= max_func(output_image))
  736 
  737         # expand min/max range to account for cval
  738         if preserve_cval:
  739             # cast cval to the same dtype as the input image
  740             cval = input_image.dtype.type(cval)
  741             min_val = min(min_val, cval)
  742             max_val = max(max_val, cval)
  743 
  744         np.clip(output_image, min_val, max_val, out=output_image)
  745 
  746 
  747 def warp(image, inverse_map, map_args={}, output_shape=None, order=None,
  748          mode='constant', cval=0., clip=True, preserve_range=False):
  749     """Warp an image according to a given coordinate transformation.
  750 
  751     Parameters
  752     ----------
  753     image : ndarray
  754         Input image.
  755     inverse_map : transformation object, callable ``cr = f(cr, **kwargs)``, or ndarray
  756         Inverse coordinate map, which transforms coordinates in the output
  757         images into their corresponding coordinates in the input image.
  758 
  759         There are a number of different options to define this map, depending
  760         on the dimensionality of the input image. A 2-D image can have 2
  761         dimensions for gray-scale images, or 3 dimensions with color
  762         information.
  763 
  764          - For 2-D images, you can directly pass a transformation object,
  765            e.g. `skimage.transform.SimilarityTransform`, or its inverse.
  766          - For 2-D images, you can pass a ``(3, 3)`` homogeneous
  767            transformation matrix, e.g.
  768            `skimage.transform.SimilarityTransform.params`.
  769          - For 2-D images, a function that transforms a ``(M, 2)`` array of
  770            ``(col, row)`` coordinates in the output image to their
  771            corresponding coordinates in the input image. Extra parameters to
  772            the function can be specified through `map_args`.
  773          - For N-D images, you can directly pass an array of coordinates.
  774            The first dimension specifies the coordinates in the input image,
  775            while the subsequent dimensions determine the position in the
  776            output image. E.g. in case of 2-D images, you need to pass an array
  777            of shape ``(2, rows, cols)``, where `rows` and `cols` determine the
  778            shape of the output image, and the first dimension contains the
  779            ``(row, col)`` coordinate in the input image.
  780            See `scipy.ndimage.map_coordinates` for further documentation.
  781 
  782         Note, that a ``(3, 3)`` matrix is interpreted as a homogeneous
  783         transformation matrix, so you cannot interpolate values from a 3-D
  784         input, if the output is of shape ``(3,)``.
  785 
  786         See example section for usage.
  787     map_args : dict, optional
  788         Keyword arguments passed to `inverse_map`.
  789     output_shape : tuple (rows, cols), optional
  790         Shape of the output image generated. By default the shape of the input
  791         image is preserved.  Note that, even for multi-band images, only rows
  792         and columns need to be specified.
  793     order : int, optional
  794         The order of interpolation. The order has to be in the range 0-5:
  795          - 0: Nearest-neighbor
  796          - 1: Bi-linear (default)
  797          - 2: Bi-quadratic
  798          - 3: Bi-cubic
  799          - 4: Bi-quartic
  800          - 5: Bi-quintic
  801 
  802          Default is 0 if image.dtype is bool and 1 otherwise.
  803     mode : {'constant', 'edge', 'symmetric', 'reflect', 'wrap'}, optional
  804         Points outside the boundaries of the input are filled according
  805         to the given mode.  Modes match the behaviour of `numpy.pad`.
  806     cval : float, optional
  807         Used in conjunction with mode 'constant', the value outside
  808         the image boundaries.
  809     clip : bool, optional
  810         Whether to clip the output to the range of values of the input image.
  811         This is enabled by default, since higher order interpolation may
  812         produce values outside the given input range.
  813     preserve_range : bool, optional
  814         Whether to keep the original range of values. Otherwise, the input
  815         image is converted according to the conventions of `img_as_float`.
  816         Also see
  817         https://scikit-image.org/docs/dev/user_guide/data_types.html
  818 
  819     Returns
  820     -------
  821     warped : double ndarray
  822         The warped input image.
  823 
  824     Notes
  825     -----
  826     - The input image is converted to a `double` image.
  827     - In case of a `SimilarityTransform`, `AffineTransform` and
  828       `ProjectiveTransform` and `order` in [0, 3] this function uses the
  829       underlying transformation matrix to warp the image with a much faster
  830       routine.
  831 
  832     Examples
  833     --------
  834     >>> from skimage.transform import warp
  835     >>> from skimage import data
  836     >>> image = data.camera()
  837 
  838     The following image warps are all equal but differ substantially in
  839     execution time. The image is shifted to the bottom.
  840 
  841     Use a geometric transform to warp an image (fast):
  842 
  843     >>> from skimage.transform import SimilarityTransform
  844     >>> tform = SimilarityTransform(translation=(0, -10))
  845     >>> warped = warp(image, tform)
  846 
  847     Use a callable (slow):
  848 
  849     >>> def shift_down(xy):
  850     ...     xy[:, 1] -= 10
  851     ...     return xy
  852     >>> warped = warp(image, shift_down)
  853 
  854     Use a transformation matrix to warp an image (fast):
  855 
  856     >>> matrix = np.array([[1, 0, 0], [0, 1, -10], [0, 0, 1]])
  857     >>> warped = warp(image, matrix)
  858     >>> from skimage.transform import ProjectiveTransform
  859     >>> warped = warp(image, ProjectiveTransform(matrix=matrix))
  860 
  861     You can also use the inverse of a geometric transformation (fast):
  862 
  863     >>> warped = warp(image, tform.inverse)
  864 
  865     For N-D images you can pass a coordinate array, that specifies the
  866     coordinates in the input image for every element in the output image. E.g.
  867     if you want to rescale a 3-D cube, you can do:
  868 
  869     >>> cube_shape = np.array([30, 30, 30])
  870     >>> rng = np.random.default_rng()
  871     >>> cube = rng.random(cube_shape)
  872 
  873     Setup the coordinate array, that defines the scaling:
  874 
  875     >>> scale = 0.1
  876     >>> output_shape = (scale * cube_shape).astype(int)
  877     >>> coords0, coords1, coords2 = np.mgrid[:output_shape[0],
  878     ...                    :output_shape[1], :output_shape[2]]
  879     >>> coords = np.array([coords0, coords1, coords2])
  880 
  881     Assume that the cube contains spatial data, where the first array element
  882     center is at coordinate (0.5, 0.5, 0.5) in real space, i.e. we have to
  883     account for this extra offset when scaling the image:
  884 
  885     >>> coords = (coords + 0.5) / scale - 0.5
  886     >>> warped = warp(cube, coords)
  887 
  888     """
  889 
  890     if image.size == 0:
  891         raise ValueError("Cannot warp empty image with dimensions",
  892                          image.shape)
  893 
  894     order = _validate_interpolation_order(image.dtype, order)
  895 
  896     if order > 0:
  897         image = convert_to_float(image, preserve_range)
  898         if image.dtype == np.float16:
  899             image = image.astype(np.float32)
  900 
  901     input_shape = np.array(image.shape)
  902 
  903     if output_shape is None:
  904         output_shape = input_shape
  905     else:
  906         output_shape = safe_as_int(output_shape)
  907 
  908     warped = None
  909 
  910     if order == 2:
  911         # When fixing this issue, make sure to fix the branches further
  912         # below in this function
  913         warn("Bi-quadratic interpolation behavior has changed due "
  914              "to a bug in the implementation of scikit-image. "
  915              "The new version now serves as a wrapper "
  916              "around SciPy's interpolation functions, which itself "
  917              "is not verified to be a correct implementation. Until "
  918              "skimage's implementation is fixed, we recommend "
  919              "to use bi-linear or bi-cubic interpolation instead.")
  920 
  921     if order in (1, 3) and not map_args:
  922         # use fast Cython version for specific interpolation orders and input
  923 
  924         matrix = None
  925 
  926         if isinstance(inverse_map, np.ndarray) and inverse_map.shape == (3, 3):
  927             # inverse_map is a transformation matrix as numpy array
  928             matrix = inverse_map
  929 
  930         elif isinstance(inverse_map, HOMOGRAPHY_TRANSFORMS):
  931             # inverse_map is a homography
  932             matrix = inverse_map.params
  933 
  934         elif (hasattr(inverse_map, '__name__') and
  935               inverse_map.__name__ == 'inverse' and
  936               get_bound_method_class(inverse_map) in HOMOGRAPHY_TRANSFORMS):
  937             # inverse_map is the inverse of a homography
  938             matrix = np.linalg.inv(inverse_map.__self__.params)
  939 
  940         if matrix is not None:
  941             matrix = matrix.astype(image.dtype)
  942             ctype = 'float32_t' if image.dtype == np.float32 else 'float64_t'
  943             if image.ndim == 2:
  944                 warped = _warp_fast[ctype](image, matrix,
  945                                            output_shape=output_shape,
  946                                            order=order, mode=mode, cval=cval)
  947             elif image.ndim == 3:
  948                 dims = []
  949                 for dim in range(image.shape[2]):
  950                     dims.append(_warp_fast[ctype](image[..., dim], matrix,
  951                                                   output_shape=output_shape,
  952                                                   order=order, mode=mode,
  953                                                   cval=cval))
  954                 warped = np.dstack(dims)
  955 
  956     if warped is None:
  957         # use ndi.map_coordinates
  958 
  959         if (isinstance(inverse_map, np.ndarray) and
  960                 inverse_map.shape == (3, 3)):
  961             # inverse_map is a transformation matrix as numpy array,
  962             # this is only used for order >= 4.
  963             inverse_map = ProjectiveTransform(matrix=inverse_map)
  964 
  965         if isinstance(inverse_map, np.ndarray):
  966             # inverse_map is directly given as coordinates
  967             coords = inverse_map
  968         else:
  969             # inverse_map is given as function, that transforms (N, 2)
  970             # destination coordinates to their corresponding source
  971             # coordinates. This is only supported for 2(+1)-D images.
  972 
  973             if image.ndim < 2 or image.ndim > 3:
  974                 raise ValueError("Only 2-D images (grayscale or color) are "
  975                                  "supported, when providing a callable "
  976                                  "`inverse_map`.")
  977 
  978             def coord_map(*args):
  979                 return inverse_map(*args, **map_args)
  980 
  981             if len(input_shape) == 3 and len(output_shape) == 2:
  982                 # Input image is 2D and has color channel, but output_shape is
  983                 # given for 2-D images. Automatically add the color channel
  984                 # dimensionality.
  985                 output_shape = (output_shape[0], output_shape[1],
  986                                 input_shape[2])
  987 
  988             coords = warp_coords(coord_map, output_shape)
  989 
  990         # Pre-filtering not necessary for order 0, 1 interpolation
  991         prefilter = order > 1
  992 
  993         ndi_mode = _to_ndimage_mode(mode)
  994         warped = ndi.map_coordinates(image, coords, prefilter=prefilter,
  995                                      mode=ndi_mode, order=order, cval=cval)
  996 
  997     _clip_warp_output(image, warped, mode, cval, clip)
  998 
  999     return warped
 1000 
 1001 
 1002 def _linear_polar_mapping(output_coords, k_angle, k_radius, center):
 1003     """Inverse mapping function to convert from cartesian to polar coordinates
 1004 
 1005     Parameters
 1006     ----------
 1007     output_coords : ndarray
 1008         `(M, 2)` array of `(col, row)` coordinates in the output image
 1009     k_angle : float
 1010         Scaling factor that relates the intended number of rows in the output
 1011         image to angle: ``k_angle = nrows / (2 * np.pi)``
 1012     k_radius : float
 1013         Scaling factor that relates the radius of the circle bounding the
 1014         area to be transformed to the intended number of columns in the output
 1015         image: ``k_radius = ncols / radius``
 1016     center : tuple (row, col)
 1017         Coordinates that represent the center of the circle that bounds the
 1018         area to be transformed in an input image.
 1019 
 1020     Returns
 1021     -------
 1022     coords : ndarray
 1023         `(M, 2)` array of `(col, row)` coordinates in the input image that
 1024         correspond to the `output_coords` given as input.
 1025     """
 1026     angle = output_coords[:, 1] / k_angle
 1027     rr = ((output_coords[:, 0] / k_radius) * np.sin(angle)) + center[0]
 1028     cc = ((output_coords[:, 0] / k_radius) * np.cos(angle)) + center[1]
 1029     coords = np.column_stack((cc, rr))
 1030     return coords
 1031 
 1032 
 1033 def _log_polar_mapping(output_coords, k_angle, k_radius, center):
 1034     """Inverse mapping function to convert from cartesian to polar coordinates
 1035 
 1036     Parameters
 1037     ----------
 1038     output_coords : ndarray
 1039         `(M, 2)` array of `(col, row)` coordinates in the output image
 1040     k_angle : float
 1041         Scaling factor that relates the intended number of rows in the output
 1042         image to angle: ``k_angle = nrows / (2 * np.pi)``
 1043     k_radius : float
 1044         Scaling factor that relates the radius of the circle bounding the
 1045         area to be transformed to the intended number of columns in the output
 1046         image: ``k_radius = width / np.log(radius)``
 1047     center : tuple (row, col)
 1048         Coordinates that represent the center of the circle that bounds the
 1049         area to be transformed in an input image.
 1050 
 1051     Returns
 1052     -------
 1053     coords : ndarray
 1054         `(M, 2)` array of `(col, row)` coordinates in the input image that
 1055         correspond to the `output_coords` given as input.
 1056     """
 1057     angle = output_coords[:, 1] / k_angle
 1058     rr = ((np.exp(output_coords[:, 0] / k_radius)) * np.sin(angle)) + center[0]
 1059     cc = ((np.exp(output_coords[:, 0] / k_radius)) * np.cos(angle)) + center[1]
 1060     coords = np.column_stack((cc, rr))
 1061     return coords
 1062 
 1063 
 1064 @channel_as_last_axis()
 1065 @deprecate_multichannel_kwarg()
 1066 def warp_polar(image, center=None, *, radius=None, output_shape=None,
 1067                scaling='linear', multichannel=False, channel_axis=None,
 1068                **kwargs):
 1069     """Remap image to polar or log-polar coordinates space.
 1070 
 1071     Parameters
 1072     ----------
 1073     image : ndarray
 1074         Input image. Only 2-D arrays are accepted by default. 3-D arrays are
 1075         accepted if a `channel_axis` is specified.
 1076     center : tuple (row, col), optional
 1077         Point in image that represents the center of the transformation (i.e.,
 1078         the origin in cartesian space). Values can be of type `float`.
 1079         If no value is given, the center is assumed to be the center point
 1080         of the image.
 1081     radius : float, optional
 1082         Radius of the circle that bounds the area to be transformed.
 1083     output_shape : tuple (row, col), optional
 1084     scaling : {'linear', 'log'}, optional
 1085         Specify whether the image warp is polar or log-polar. Defaults to
 1086         'linear'.
 1087     multichannel : bool, optional
 1088         Whether the image is a 3-D array in which the third axis is to be
 1089         interpreted as multiple channels. If set to `False` (default), only 2-D
 1090         arrays are accepted. This argument is deprecated: specify
 1091         `channel_axis` instead.
 1092     channel_axis : int or None, optional
 1093         If None, the image is assumed to be a grayscale (single channel) image.
 1094         Otherwise, this parameter indicates which axis of the array corresponds
 1095         to channels.
 1096 
 1097         .. versionadded:: 0.19
 1098            ``channel_axis`` was added in 0.19.
 1099     **kwargs : keyword arguments
 1100         Passed to `transform.warp`.
 1101 
 1102     Returns
 1103     -------
 1104     warped : ndarray
 1105         The polar or log-polar warped image.
 1106 
 1107     Examples
 1108     --------
 1109     Perform a basic polar warp on a grayscale image:
 1110 
 1111     >>> from skimage import data
 1112     >>> from skimage.transform import warp_polar
 1113     >>> image = data.checkerboard()
 1114     >>> warped = warp_polar(image)
 1115 
 1116     Perform a log-polar warp on a grayscale image:
 1117 
 1118     >>> warped = warp_polar(image, scaling='log')
 1119 
 1120     Perform a log-polar warp on a grayscale image while specifying center,
 1121     radius, and output shape:
 1122 
 1123     >>> warped = warp_polar(image, (100,100), radius=100,
 1124     ...                     output_shape=image.shape, scaling='log')
 1125 
 1126     Perform a log-polar warp on a color image:
 1127 
 1128     >>> image = data.astronaut()
 1129     >>> warped = warp_polar(image, scaling='log', channel_axis=-1)
 1130     """
 1131     multichannel = channel_axis is not None
 1132     if image.ndim != 2 and not multichannel:
 1133         raise ValueError(f'Input array must be 2-dimensional when '
 1134                          f'`channel_axis=None`, got {image.ndim}')
 1135 
 1136     if image.ndim != 3 and multichannel:
 1137         raise ValueError(f'Input array must be 3-dimensional when '
 1138                          f'`channel_axis` is specified, got {image.ndim}')
 1139 
 1140     if center is None:
 1141         center = (np.array(image.shape)[:2] / 2) - 0.5
 1142 
 1143     if radius is None:
 1144         w, h = np.array(image.shape)[:2] / 2
 1145         radius = np.sqrt(w ** 2 + h ** 2)
 1146 
 1147     if output_shape is None:
 1148         height = 360
 1149         width = int(np.ceil(radius))
 1150         output_shape = (height, width)
 1151     else:
 1152         output_shape = safe_as_int(output_shape)
 1153         height = output_shape[0]
 1154         width = output_shape[1]
 1155 
 1156     if scaling == 'linear':
 1157         k_radius = width / radius
 1158         map_func = _linear_polar_mapping
 1159     elif scaling == 'log':
 1160         k_radius = width / np.log(radius)
 1161         map_func = _log_polar_mapping
 1162     else:
 1163         raise ValueError("Scaling value must be in {'linear', 'log'}")
 1164 
 1165     k_angle = height / (2 * np.pi)
 1166     warp_args = {'k_angle': k_angle, 'k_radius': k_radius, 'center': center}
 1167 
 1168     warped = warp(image, map_func, map_args=warp_args,
 1169                   output_shape=output_shape, **kwargs)
 1170 
 1171     return warped
 1172 
 1173 
 1174 def _local_mean_weights(old_size, new_size, grid_mode, dtype):
 1175     """Create a 2D weight matrix for resizing with the local mean.
 1176 
 1177     Parameters
 1178     ----------
 1179     old_size: int
 1180         Old size.
 1181     new_size: int
 1182         New size.
 1183     grid_mode : bool
 1184         Whether to use grid data model of pixel/voxel model for
 1185         average weights computation.
 1186     dtype: dtype
 1187         Output array data type.
 1188 
 1189     Returns
 1190     -------
 1191     weights: (new_size, old_size) array
 1192         Rows sum to 1.
 1193 
 1194     """
 1195     if grid_mode:
 1196         old_breaks = np.linspace(0, old_size, num=old_size + 1, dtype=dtype)
 1197         new_breaks = np.linspace(0, old_size, num=new_size + 1, dtype=dtype)
 1198     else:
 1199         old, new = old_size - 1, new_size - 1
 1200         old_breaks = np.pad(np.linspace(0.5, old - 0.5, old, dtype=dtype),
 1201                             1, 'constant', constant_values=(0, old))
 1202         if new == 0:
 1203             val = np.inf
 1204         else:
 1205             val = 0.5 * old / new
 1206         new_breaks = np.pad(np.linspace(val, old - val, new, dtype=dtype),
 1207                             1, 'constant', constant_values=(0, old))
 1208 
 1209     upper = np.minimum(new_breaks[1:, np.newaxis], old_breaks[np.newaxis, 1:])
 1210     lower = np.maximum(new_breaks[:-1, np.newaxis],
 1211                        old_breaks[np.newaxis, :-1])
 1212 
 1213     weights = np.maximum(upper - lower, 0)
 1214     weights /= weights.sum(axis=1, keepdims=True)
 1215 
 1216     return weights
 1217 
 1218 
 1219 def resize_local_mean(image, output_shape, grid_mode=True,
 1220                       preserve_range=False, *, channel_axis=None):
 1221     """Resize an array with the local mean / bilinear scaling.
 1222 
 1223     Parameters
 1224     ----------
 1225     image : ndarray
 1226         Input image. If this is a multichannel image, the axis corresponding
 1227         to channels should be specified using `channel_axis`
 1228     output_shape : iterable
 1229         Size of the generated output image. When `channel_axis` is not None,
 1230         the `channel_axis` should either be omitted from `output_shape` or the
 1231         ``output_shape[channel_axis]`` must match
 1232         ``image.shape[channel_axis]``. If the length of `output_shape` exceeds
 1233         image.ndim, additional singleton dimensions will be appended to the
 1234         input ``image`` as needed.
 1235     grid_mode : bool, optional
 1236         Defines ``image`` pixels position: if True, pixels are assumed to be at
 1237         grid intersections, otherwise at cell centers. As a consequence,
 1238         for example, a 1d signal of length 5 is considered to have length 4
 1239         when `grid_mode` is False, but length 5 when `grid_mode` is True. See
 1240         the following visual illustration:
 1241 
 1242         .. code-block:: text
 1243 
 1244                 | pixel 1 | pixel 2 | pixel 3 | pixel 4 | pixel 5 |
 1245                      |<-------------------------------------->|
 1246                                         vs.
 1247                 |<----------------------------------------------->|
 1248 
 1249         The starting point of the arrow in the diagram above corresponds to
 1250         coordinate location 0 in each mode.
 1251     preserve_range : bool, optional
 1252         Whether to keep the original range of values. Otherwise, the input
 1253         image is converted according to the conventions of `img_as_float`.
 1254         Also see
 1255         https://scikit-image.org/docs/dev/user_guide/data_types.html
 1256 
 1257     Returns
 1258     -------
 1259     resized : ndarray
 1260         Resized version of the input.
 1261 
 1262     See Also
 1263     --------
 1264     resize, downscale_local_mean
 1265 
 1266     Notes
 1267     -----
 1268     This method is sometimes referred to as "area-based" interpolation or
 1269     "pixel mixing" interpolation [1]_. When `grid_mode` is True, it is
 1270     equivalent to using OpenCV's resize with `INTER_AREA` interpolation mode.
 1271     It is commonly used for image downsizing. If the downsizing factors are
 1272     integers, then `downscale_local_mean` should be preferred instead.
 1273 
 1274     References
 1275     ----------
 1276     .. [1] http://entropymine.com/imageworsener/pixelmixing/
 1277 
 1278     Examples
 1279     --------
 1280     >>> from skimage import data
 1281     >>> from skimage.transform import resize_local_mean
 1282     >>> image = data.camera()
 1283     >>> resize_local_mean(image, (100, 100)).shape
 1284     (100, 100)
 1285 
 1286     """
 1287     if channel_axis is not None:
 1288         if channel_axis < -image.ndim or channel_axis >= image.ndim:
 1289             raise ValueError("invalid channel_axis")
 1290 
 1291         # move channels to last position
 1292         image = np.moveaxis(image, channel_axis, -1)
 1293         nc = image.shape[-1]
 1294 
 1295         output_ndim = len(output_shape)
 1296         if output_ndim == image.ndim - 1:
 1297             # insert channels dimension at the end
 1298             output_shape = output_shape + (nc,)
 1299         elif output_ndim == image.ndim:
 1300             if output_shape[channel_axis] != nc:
 1301                 raise ValueError(
 1302                     "Cannot reshape along the channel_axis. Use "
 1303                     "channel_axis=None to reshape along all axes."
 1304                 )
 1305             # move channels to last position in output_shape
 1306             channel_axis = channel_axis % image.ndim
 1307             output_shape = (
 1308                 output_shape[:channel_axis] + output_shape[channel_axis:] +
 1309                 (nc,)
 1310             )
 1311         else:
 1312             raise ValueError(
 1313                 "len(output_shape) must be image.ndim or (image.ndim - 1) "
 1314                 "when a channel_axis is specified."
 1315             )
 1316         resized = image
 1317     else:
 1318         resized, output_shape = _preprocess_resize_output_shape(image,
 1319                                                                 output_shape)
 1320     resized = convert_to_float(resized, preserve_range)
 1321     dtype = resized.dtype
 1322 
 1323     for axis, (old_size, new_size) in enumerate(zip(image.shape,
 1324                                                     output_shape)):
 1325         if old_size == new_size:
 1326             continue
 1327         weights = _local_mean_weights(old_size, new_size, grid_mode, dtype)
 1328         product = np.tensordot(resized, weights, [[axis], [-1]])
 1329         resized = np.moveaxis(product, -1, axis)
 1330 
 1331     if channel_axis is not None:
 1332         # restore channels to original axis
 1333         resized = np.moveaxis(resized, -1, channel_axis)
 1334 
 1335     return resized