Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make rotate available #213

Merged
merged 27 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
568879a
add rotate function (2D and 3D)
May 4, 2021
39db962
rotate doc
May 4, 2021
988d8c4
rotate ND support
May 4, 2021
86539a8
newline at EOF
May 4, 2021
059bbc4
rotate tests
May 4, 2021
059de60
list functions
May 5, 2021
92e1ddb
rotate tests
May 6, 2021
7263bf1
rotate doc and temporary fix for tests
May 6, 2021
000a03d
include output_shape parameter
May 10, 2021
03242d1
rotate: warning if both reshape and output_shape provided
May 10, 2021
443431b
Merge branch 'main' into main
GenevieveBuckley Mar 15, 2023
ee2bf3c
Reformat docstring, remove whitespace & commented code line
GenevieveBuckley Mar 15, 2023
72bf14f
Merge remote-tracking branch 'base_origin/main' into main
Nov 6, 2023
97d7485
rotate: add chunking when not explicitely demanded (suggested by @gca…
Nov 6, 2023
c124bde
test_rotate: fix CI error
Nov 6, 2023
9d73daf
test_rotate: complete coverage
Nov 7, 2023
df13d80
test_rotate: fix tests
Nov 7, 2023
ee06aa8
complete parameters
Nov 8, 2023
981d87c
rotate: clean duplicate code
Nov 8, 2023
a9c7a15
rotate: clean code, fix tests
Nov 8, 2023
a371ebc
rotate: fix comparison bug
Nov 8, 2023
391ff57
test_rotate: fix warning tests
Nov 8, 2023
8d276f8
Merge branch 'dask:main' into main
martinschorb Feb 21, 2024
5f9d626
rotate: formatted docstring and adapted axes type checking to scipy
m-albert Feb 20, 2024
c451c33
rotate: Removed output and output_shape arguments. Updating tests is WIP
m-albert Feb 20, 2024
1c545ce
Outsourced nonspecific arguments to affine_transform and adapted test…
m-albert Feb 21, 2024
b5eea43
Optimized imports
m-albert Feb 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 204 additions & 0 deletions dask_image/ndinterp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import functools
import math
from itertools import product
from numbers import Number
import warnings

import dask.array as da
Expand All @@ -11,6 +12,7 @@
from dask.highlevelgraph import HighLevelGraph
import scipy
from scipy.ndimage import affine_transform as ndimage_affine_transform
from scipy import special

from ..dispatch._dispatch_ndinterp import (
dispatch_affine_transform,
Expand All @@ -25,6 +27,7 @@

__all__ = [
"affine_transform",
"rotate"
]


Expand Down Expand Up @@ -247,6 +250,207 @@ def affine_transform(
return transformed


def rotate(input_arr, angle, axes=(1, 0), reshape=True, output=None, order=1,
mode='constant', cval=0.0, prefilter=False, output_chunks=None, output_shape=None):
"""Rotate an array using Dask.

The array is rotated in the plane defined by the two axes given by the
`axes` parameter using spline interpolation of the requested order.
Chunkwise processing is performed using dask_image.ndinterp.affine_transform.

Parameters
----------
input_arr : array_like (Numpy Array, Cupy Array, Dask Array...)
The image array.
angle : float
The rotation angle in degrees.
axes : tuple of 2 ints, optional
The two axes that define the plane of rotation. Default is the first
two axes.
reshape : bool, optional
If `reshape` is true, the output shape is adapted so that the input
array is contained completely in the output. Default is True.
output : dtype, optional
The dtype of the returned array.
By default, an array of the same dtype as input will be created.

order : int, optional
The order of the spline interpolation, default is 1.
The order has to be in the range 0-5. Note that for order>1
scipy's affine_transform applies prefiltering, which is not
yet supported and skipped in this implementation.
mode : {‘constant’, ‘grid-constant’, ‘nearest’}, optional
The mode parameter determines how the input array is extended beyond its boundaries.
Default is ‘constant’. Behavior for each valid value is as follows (see additional plots and details on boundary modes):

‘constant’ (k k k k | a b c d | k k k k)
The input is extended by filling all values beyond the edge with the same constant value, defined by the cval parameter. No interpolation is performed beyond the edges of the input.
‘grid-constant’ (k k k k | a b c d | k k k k)
The input is extended by filling all values beyond the edge with the same constant value, defined by the cval parameter. Interpolation occurs for samples outside the input’s extent as well.
‘nearest’ (a a a a | a b c d | d d d d)
The input is extended by replicating the last pixel.
cval : scalar, optional
Value to fill past edges of input if mode is ‘constant’. Default is 0.0.
prefilter : bool, optional
currently not supported
output_shape : tuple of ints, optional
The shape of the array to be returned.
output_chunks : tuple of ints, optional
The shape of the chunks of the output Dask Array.

Returns
-------
rotate : Dask Array
A dask array representing the rotated input.

Notes
-----
Differences to `ndimage.affine_transformation`:
- currently, prefiltering is not supported
(affecting the output in case of interpolation `order > 1`)
- default order is 1
- modes 'reflect', 'mirror' and 'wrap' are not supported
- passing array to `output` is not currently supported.

Arguments equal to `ndimage.affine_rotate`,
except for `output_chunks`, `output_shape`.

Examples
--------
>>> from scipy import ndimage, misc
>>> import matplotlib.pyplot as plt
>>> import dask.array as da
>>> fig = plt.figure(figsize=(10, 3))
>>> ax1, ax2, ax3 = fig.subplots(1, 3)
>>> img = da.from_array(misc.ascent(),chunks=(64,64))
>>> img_45 = dask_image.ndinterp.rotate(img, 45, reshape=False)
>>> full_img_45 = dask_image.ndinterp.rotate(img, 45, reshape=True)
>>> ax1.imshow(img, cmap='gray')
>>> ax1.set_axis_off()
>>> ax2.imshow(img_45, cmap='gray')
>>> ax2.set_axis_off()
>>> ax3.imshow(full_img_45, cmap='gray')
>>> ax3.set_axis_off()
>>> fig.set_tight_layout(True)
>>> plt.show()
>>> print(img.shape)
(512, 512)
>>> print(img_45.shape)
(512, 512)
>>> print(full_img_45.shape)
(724, 724)

"""
if not type(input_arr) == da.core.Array:
input_arr = da.from_array(input_arr)

if output_chunks is None:
output_chunks = input_arr.chunksize

if reshape & (output_shape is not None):
warnings.warn('Both reshaping desired and output_shape provided.'
'Will use the explicit output_shape.', UserWarning)

if prefilter:
warnings.warn('Prefilter currently unsupported.', UserWarning)

if output_shape is None:
output_shape = np.asarray(input_arr.shape)

ndim = input_arr.ndim

if ndim < 2:
raise ValueError('input array should be at least 2D')

axes = list(axes)

if len(axes) != 2:
raise ValueError('axes should contain exactly two values')

if not all([isinstance(ax, Number) for ax in axes]):
raise TypeError('axes should contain only integer values')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a specific reason for why you extend here on the scipy.ndimage.rotate implementation?

https://github.com/scipy/scipy/blob/f990b1d2471748c79bc4260baf8923db0a5248af/scipy/ndimage/_interpolation.py#L901

elif not all([float(ax).is_integer() for ax in axes]):
raise TypeError('axes should contain only integer values')

if axes[0] < 0:
axes[0] += ndim
if axes[1] < 0:
axes[1] += ndim
if axes[0] < 0 or axes[1] < 0 or axes[0] >= ndim or axes[1] >= ndim:
raise ValueError('invalid rotation plane specified')

if output is not None:
try:
dtype = np.dtype(output)
except TypeError: # pragma: no cover
raise TypeError( # pragma: no cover
"Could not coerce the provided output to a dtype. "
"Passing array to output is not currently supported."
)
else:
dtype = input_arr.dtype

axes.sort()

c, s = special.cosdg(angle), special.sindg(angle)

rot_matrix = np.array([[c, s],
[-s, c]])

img_shape = np.asarray(input_arr.shape)
in_plane_shape = img_shape[axes]

if reshape:
# Compute transformed input bounds
iy, ix = in_plane_shape
out_bounds = rot_matrix @ [[0, 0, iy, iy],
[0, ix, 0, ix]]
# Compute the shape of the transformed input plane
out_plane_shape = (out_bounds.ptp(axis=1) + 0.5).astype(int)
else:
out_plane_shape = img_shape[axes]

if (output_shape == img_shape).all():
output_shape[axes] = out_plane_shape
else:
out_plane_shape = np.asarray(output_shape)[axes]

out_center = rot_matrix @ ((out_plane_shape - 1) / 2)
in_center = (in_plane_shape - 1) / 2
offset = in_center - out_center
output_shape = tuple(output_shape)


if ndim <= 2:

output = affine_transform(input_arr, rot_matrix,
offset=offset, output_shape=output_shape,
order=order, mode=mode, cval=cval,
prefilter=prefilter,output_chunks=output_chunks)


elif ndim >= 3:
rotmat_nd = np.eye(ndim)
offset_nd = np.zeros(ndim)

for o_x,idx in enumerate(axes):

rotmat_nd[idx,axes[0]] = rot_matrix[o_x,0]
rotmat_nd[idx,axes[1]] = rot_matrix[o_x,1]

offset_nd[idx] = offset[o_x]


output = affine_transform(input_arr, rotmat_nd,
offset=offset_nd, output_shape=output_shape,
order=order, mode=mode, cval=cval,
prefilter=prefilter,output_chunks=output_chunks)

result = output.astype(dtype)

return result


# magnitude of the maximum filter pole for each order
# (obtained from scipy/ndimage/src/ni_splines.c)
_maximum_pole = {
Expand Down
3 changes: 1 addition & 2 deletions docs/coverage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ This table shows which SciPy ndimage functions are supported by dask-image.
- ✓
* - ``rotate``
- ✓
-
-
-
* - ``shift``
- ✓
Expand Down Expand Up @@ -311,4 +311,3 @@ This table shows which SciPy ndimage functions are supported by dask-image.
- ✓
-
-

Loading
Loading