_warps.py 48 KB

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