"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,
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
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':
1158         map_func = _linear_polar_mapping
1159     elif scaling == 'log':
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
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
```