imops 0.8.8__cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. _build_utils.py +113 -0
  2. imops/__init__.py +10 -0
  3. imops/__version__.py +1 -0
  4. imops/_configs.py +29 -0
  5. imops/backend.py +95 -0
  6. imops/box.py +74 -0
  7. imops/cpp/cpp_modules.cpython-38-i386-linux-gnu.so +0 -0
  8. imops/cpp/interp2d/delaunator/delaunator-header-only.hpp +33 -0
  9. imops/cpp/interp2d/delaunator/delaunator.cpp +645 -0
  10. imops/cpp/interp2d/delaunator/delaunator.hpp +170 -0
  11. imops/cpp/interp2d/interpolator.h +52 -0
  12. imops/cpp/interp2d/triangulator.h +198 -0
  13. imops/cpp/interp2d/utils.h +63 -0
  14. imops/cpp/main.cpp +13 -0
  15. imops/crop.py +120 -0
  16. imops/interp1d.py +207 -0
  17. imops/interp2d.py +120 -0
  18. imops/measure.py +228 -0
  19. imops/morphology.py +525 -0
  20. imops/numeric.py +384 -0
  21. imops/pad.py +253 -0
  22. imops/py.typed +0 -0
  23. imops/radon.py +247 -0
  24. imops/src/__init__.py +0 -0
  25. imops/src/_backprojection.c +27339 -0
  26. imops/src/_backprojection.cpython-38-i386-linux-gnu.so +0 -0
  27. imops/src/_fast_backprojection.c +27339 -0
  28. imops/src/_fast_backprojection.cpython-38-i386-linux-gnu.so +0 -0
  29. imops/src/_fast_measure.c +33810 -0
  30. imops/src/_fast_measure.cpython-38-i386-linux-gnu.so +0 -0
  31. imops/src/_fast_morphology.c +26089 -0
  32. imops/src/_fast_morphology.cpython-38-i386-linux-gnu.so +0 -0
  33. imops/src/_fast_numeric.c +48651 -0
  34. imops/src/_fast_numeric.cpython-38-i386-linux-gnu.so +0 -0
  35. imops/src/_fast_radon.c +30714 -0
  36. imops/src/_fast_radon.cpython-38-i386-linux-gnu.so +0 -0
  37. imops/src/_fast_zoom.c +57203 -0
  38. imops/src/_fast_zoom.cpython-38-i386-linux-gnu.so +0 -0
  39. imops/src/_measure.c +33810 -0
  40. imops/src/_measure.cpython-38-i386-linux-gnu.so +0 -0
  41. imops/src/_morphology.c +26089 -0
  42. imops/src/_morphology.cpython-38-i386-linux-gnu.so +0 -0
  43. imops/src/_numba_zoom.py +503 -0
  44. imops/src/_numeric.c +48651 -0
  45. imops/src/_numeric.cpython-38-i386-linux-gnu.so +0 -0
  46. imops/src/_radon.c +30714 -0
  47. imops/src/_radon.cpython-38-i386-linux-gnu.so +0 -0
  48. imops/src/_zoom.c +57203 -0
  49. imops/src/_zoom.cpython-38-i386-linux-gnu.so +0 -0
  50. imops/testing.py +57 -0
  51. imops/utils.py +205 -0
  52. imops/zoom.py +297 -0
  53. imops-0.8.8.dist-info/LICENSE +21 -0
  54. imops-0.8.8.dist-info/METADATA +218 -0
  55. imops-0.8.8.dist-info/RECORD +58 -0
  56. imops-0.8.8.dist-info/WHEEL +6 -0
  57. imops-0.8.8.dist-info/top_level.txt +2 -0
  58. imops.libs/libgomp-65f46eca.so.1.0.0 +0 -0
imops/interp1d.py ADDED
@@ -0,0 +1,207 @@
1
+ from typing import Union
2
+ from warnings import warn
3
+
4
+ import numpy as np
5
+ from scipy.interpolate import interp1d as scipy_interp1d
6
+
7
+ from .backend import BackendLike, resolve_backend
8
+ from .numeric import copy as _copy
9
+ from .src._fast_zoom import _interp1d as cython_fast_interp1d
10
+ from .src._zoom import _interp1d as cython_interp1d
11
+ from .utils import normalize_num_threads
12
+
13
+
14
+ class interp1d:
15
+ """
16
+ Faster parallelizable version of `scipy.interpolate.interp1d` for fp32 / fp64 inputs.
17
+
18
+ Works faster only for ndim <= 3. Shares interface with `scipy.interpolate.interp1d` except for `num_threads` and
19
+ `backend` arguments.
20
+
21
+ Parameters
22
+ ----------
23
+ x: np.ndarray
24
+ 1-dimensional array of real values (aka coordinates)
25
+ y: np.ndarray
26
+ n-dimensional array of real values. The length of y along the interpolation axis must be equal to the x length
27
+ kind: Union[int, str]
28
+ specifies the kind of interpolation as a string or as an integer specifying the order of interpolation to use.
29
+ Only kind=1 and 'linear` are fast and parallelizable, other kinds will force to use `scipy` implementation
30
+ axis: int
31
+ specifies the axis of y along which to interpolate. Interpolation defaults to the last axis of y
32
+ copy: bool
33
+ if True, the class makes internal copies of x and y. If False, references to x and y are used
34
+ bounds_error: bool
35
+ if True, a ValueError is raised any time interpolation is attempted on a value outside of the range of x where
36
+ extrapolation is necessary. If False, out of bounds values are assigned fill_value. By default, an error is
37
+ raised unless fill_value='extrapolate'
38
+ fill_value: Union[float, str]
39
+ if a float, this value will be used to fill in for requested points outside of the data range. If not provided,
40
+ then the default is NaN. If 'extrapolate', values for points outside of the data range will be extrapolated
41
+ assume_sorted: bool
42
+ if False, values of x can be in any order and they are sorted first. If True, x has to be an array of
43
+ monotonically increasing values
44
+ num_threads: int
45
+ the number of threads to use for computation. Default = the cpu count. If negative value passed
46
+ cpu count + num_threads + 1 threads will be used
47
+ backend: BackendLike
48
+ which backend to use. `numba`, `cython` and `scipy` are available, `cython` is used by default
49
+
50
+ Methods
51
+ -------
52
+ __call__
53
+
54
+ Examples
55
+ --------
56
+ ```python
57
+ import numpy as np
58
+ from imops.interp1d import interp1d
59
+ x = np.arange(0, 10)
60
+ y = np.exp(-x/3.0)
61
+ f = interp1d(x, y)
62
+ xnew = np.arange(0, 9, 0.1)
63
+ ynew = f(xnew) # use interpolation function returned by `interp1d`
64
+ ```
65
+ """
66
+
67
+ def __init__(
68
+ self,
69
+ x: np.ndarray,
70
+ y: np.ndarray,
71
+ kind: Union[int, str] = 'linear',
72
+ axis: int = -1,
73
+ copy: bool = True,
74
+ bounds_error: bool = None,
75
+ fill_value: Union[float, str] = np.nan,
76
+ assume_sorted: bool = False,
77
+ num_threads: int = -1,
78
+ backend: BackendLike = None,
79
+ ) -> None:
80
+ backend = resolve_backend(backend, warn_stacklevel=3)
81
+ if backend.name not in ('Scipy', 'Numba', 'Cython'):
82
+ raise ValueError(f'Unsupported backend "{backend.name}".')
83
+
84
+ self.backend = backend
85
+ self.dtype = y.dtype
86
+ self.num_threads = num_threads
87
+
88
+ if backend.name == 'Scipy':
89
+ self.scipy_interp1d = scipy_interp1d(x, y, kind, axis, copy, bounds_error, fill_value, assume_sorted)
90
+ elif self.dtype not in (np.float32, np.float64) or y.ndim > 3 or kind not in ('linear', 1):
91
+ warn(
92
+ "Fast interpolation is only supported for ndim<=3, dtype=float32 or float64, order=1 or 'linear'. "
93
+ "Falling back to scipy's implementation.",
94
+ stacklevel=2,
95
+ )
96
+ self.scipy_interp1d = scipy_interp1d(x, y, kind, axis, copy, bounds_error, fill_value, assume_sorted)
97
+ else:
98
+ if len(x) != y.shape[axis]:
99
+ raise ValueError(
100
+ f'x and y arrays must be equal in length along interpolation axis: {len(x)} vs {y.shape[axis]}.'
101
+ )
102
+
103
+ if bounds_error and fill_value == 'extrapolate':
104
+ raise ValueError('Cannot extrapolate and raise at the same time.')
105
+
106
+ if fill_value == 'extrapolate' and len(x) < 2 or y.shape[axis] < 2:
107
+ raise ValueError('x and y arrays must have at least 2 entries.')
108
+
109
+ if fill_value == 'extrapolate':
110
+ self.bounds_error = False
111
+ else:
112
+ self.bounds_error = True if bounds_error is None else bounds_error
113
+
114
+ self.axis = axis
115
+
116
+ if axis not in (-1, y.ndim - 1):
117
+ y = np.swapaxes(y, -1, axis)
118
+
119
+ self.fill_value = fill_value
120
+ self.scipy_interp1d = None
121
+ # FIXME: how to accurately pass `num_threads` and `backend` arguments to `copy`?
122
+ self.x = _copy(x, order='C') if copy else x
123
+ self.n_dummy = 3 - y.ndim
124
+ self.y = y[(None,) * self.n_dummy] if self.n_dummy else y
125
+
126
+ if copy:
127
+ self.y = _copy(self.y, order='C')
128
+
129
+ self.assume_sorted = assume_sorted
130
+
131
+ if backend.name == 'Cython':
132
+ self.src_interp1d = cython_fast_interp1d if backend.fast else cython_interp1d
133
+
134
+ if backend.name == 'Numba':
135
+ from numba import njit
136
+
137
+ from .src._numba_zoom import _interp1d as numba_interp1d
138
+
139
+ njit_kwargs = {kwarg: getattr(backend, kwarg) for kwarg in backend.__dataclass_fields__.keys()}
140
+ self.src_interp1d = njit(**njit_kwargs)(numba_interp1d)
141
+
142
+ def __call__(self, x_new: np.ndarray) -> np.ndarray:
143
+ """
144
+ Evaluate the interpolant
145
+
146
+ Parameters
147
+ ----------
148
+ x_new: np.ndarray
149
+ 1d array points to evaluate the interpolant at.
150
+
151
+ Returns
152
+ -------
153
+ y_new: np.ndarray
154
+ interpolated values. Shape is determined by replacing the interpolation axis in the original array with
155
+ the shape of x
156
+ """
157
+ num_threads = normalize_num_threads(self.num_threads, self.backend, warn_stacklevel=3)
158
+
159
+ if self.scipy_interp1d is not None:
160
+ return self.scipy_interp1d(x_new)
161
+
162
+ extrapolate = self.fill_value == 'extrapolate'
163
+ args = () if self.backend.name in ('Numba',) else (num_threads,)
164
+
165
+ if self.backend.name == 'Numba':
166
+ from numba import get_num_threads, set_num_threads
167
+
168
+ old_num_threads = get_num_threads()
169
+ set_num_threads(num_threads)
170
+ # TODO: Figure out how to properly handle multiple type signatures in Cython and remove `.astype`-s
171
+ out = self.src_interp1d(
172
+ self.y,
173
+ self.x.astype(np.float64, copy=False),
174
+ x_new.astype(np.float64, copy=False),
175
+ self.bounds_error,
176
+ 0.0 if extrapolate else self.fill_value,
177
+ extrapolate,
178
+ self.assume_sorted,
179
+ *args,
180
+ )
181
+
182
+ if self.backend.name == 'Numba':
183
+ set_num_threads(old_num_threads)
184
+
185
+ out = out.astype(max(self.y.dtype, self.x.dtype, x_new.dtype, key=lambda x: x.type(0).itemsize), copy=False)
186
+
187
+ if self.n_dummy:
188
+ out = out[(0,) * self.n_dummy]
189
+ if self.axis not in (-1, out.ndim - 1):
190
+ out = np.swapaxes(out, -1, self.axis)
191
+ # FIXME: fix behaviour with np.inf-s
192
+ if np.isnan(out).any():
193
+ if not np.isinf(out).any():
194
+ raise RuntimeError("Can't decide how to handle nans in the output.")
195
+
196
+ have_neg = np.isneginf(out).any()
197
+ have_pos = np.isposinf(out).any()
198
+
199
+ if have_pos and have_neg:
200
+ raise RuntimeError("Can't decide how to handle nans in the output.")
201
+
202
+ if have_pos:
203
+ return np.nan_to_num(out, copy=False, nan=np.inf, posinf=np.inf)
204
+
205
+ return np.nan_to_num(out, copy=False, nan=-np.inf, neginf=-np.inf)
206
+
207
+ return out
imops/interp2d.py ADDED
@@ -0,0 +1,120 @@
1
+ from platform import python_version
2
+
3
+ import numpy as np
4
+ from scipy.spatial import KDTree
5
+
6
+ from .backend import Cython
7
+ from .cpp.cpp_modules import Linear2DInterpolatorCpp
8
+ from .utils import normalize_num_threads
9
+
10
+
11
+ class Linear2DInterpolator(Linear2DInterpolatorCpp):
12
+ """
13
+ 2D Delaunay triangulation and parallel linear interpolation
14
+
15
+ Parameters
16
+ ----------
17
+ points: np.ndarray
18
+ 2-D array of data point coordinates
19
+ values: np.ndarray
20
+ 1-D array of fp32/fp64 values
21
+ num_threads: int
22
+ the number of threads to use for computation. Default = the cpu count. If negative value passed
23
+ cpu count + num_threads + 1 threads will be used
24
+ triangels: np.ndarray
25
+ optional precomputed triangulation in the form of array or arrays of points indices
26
+
27
+ Methods
28
+ -------
29
+ __call__
30
+
31
+ Examples
32
+ --------
33
+ ```python
34
+ n, m = 1024, 2
35
+ points = np.random.randint(low=0, high=1024, size=(n, m))
36
+ points = np.unique(points, axis=0)
37
+ x_points = points[: n // 2]
38
+ values = np.random.uniform(low=0.0, high=1.0, size=(len(x_points),))
39
+ interp_points = points[n // 2:]
40
+ num_threads = -1 # will be equal to num of CPU cores
41
+ interpolator = Linear2DInterpolator(x_points, values, num_threads)
42
+ # Also you can pass values to __call__ and rewrite the ones that were passed to __init__
43
+ interp_values = interpolator(interp_points, values + 1.0, fill_value=0.0)
44
+ ```
45
+ """
46
+
47
+ def __init__(
48
+ self,
49
+ points: np.ndarray,
50
+ values: np.ndarray = None,
51
+ num_threads: int = 1,
52
+ triangles: np.ndarray = None,
53
+ **kwargs,
54
+ ):
55
+ if triangles is not None:
56
+ if not isinstance(triangles, np.ndarray):
57
+ raise TypeError(f'Wrong type of `triangles` argument, expected np.ndarray. Got {type(triangles)}')
58
+ if triangles.ndim != 2 or triangles.shape[1] != 3:
59
+ raise ValueError('Passed `triangles` argument has an incorrect shape')
60
+
61
+ if not isinstance(points, np.ndarray):
62
+ raise TypeError(f'Wrong type of `points` argument, expected np.ndarray. Got {type(points)}')
63
+
64
+ if points.ndim != 2 or points.shape[1] != 2:
65
+ raise ValueError('Passed `points` argument has an incorrect shape')
66
+
67
+ if values is not None:
68
+ if not isinstance(values, np.ndarray):
69
+ raise TypeError(f'Wrong type of `values` argument, expected np.ndarray. Got {type(values)}')
70
+
71
+ if values.ndim > 1:
72
+ raise ValueError(f'Wrong shape of `values` argument, expected ndim=1. Got shape {values.shape}')
73
+
74
+ super().__init__(points, num_threads, triangles)
75
+ self.kdtree = KDTree(data=points, **kwargs)
76
+ self.values = values
77
+ # FIXME: add backend dispatch
78
+ self.num_threads = normalize_num_threads(num_threads, Cython(), warn_stacklevel=3)
79
+
80
+ def __call__(self, points: np.ndarray, values: np.ndarray = None, fill_value: float = 0.0) -> np.ndarray:
81
+ """
82
+ Evaluate the interpolant
83
+
84
+ Parameters
85
+ ----------
86
+ points: np.ndarray
87
+ 2-D array of data point coordinates to interpolate at
88
+ values: np.ndarray
89
+ 1-D array of fp32/fp64 values to use at initial points
90
+ fill_value: float
91
+ value to fill past edges
92
+
93
+ Returns
94
+ -------
95
+ new_values: np.ndarray
96
+ interpolated values at given points
97
+ """
98
+ if values is None:
99
+ values = self.values
100
+
101
+ if values is None:
102
+ raise ValueError('`values` argument was never passed neither in __init__ or __call__ methods')
103
+
104
+ if not isinstance(values, np.ndarray):
105
+ raise TypeError(f'Wrong type of `values` argument, expected np.ndarray. Got {type(values)}')
106
+
107
+ if values.ndim > 1:
108
+ raise ValueError(f'Wrong shape of `values` argument, expected ndim=1. Got shape {values.shape}')
109
+
110
+ if not isinstance(points, np.ndarray):
111
+ raise TypeError(f'Wrong type of `points` argument, expected np.ndarray. Got {type(points)}')
112
+
113
+ if points.ndim != 2 or points.shape[1] != 2:
114
+ raise ValueError('Passed `points` argument has an incorrect shape')
115
+
116
+ _, neighbors = self.kdtree.query(
117
+ points, 1, **{'workers': self.num_threads} if python_version()[:3] != '3.6' else {}
118
+ )
119
+
120
+ return super().__call__(points, values, neighbors, fill_value)
imops/measure.py ADDED
@@ -0,0 +1,228 @@
1
+ from collections import namedtuple
2
+ from platform import python_version
3
+ from typing import List, NamedTuple, Sequence, Tuple, Union
4
+ from warnings import warn
5
+
6
+ import numpy as np
7
+ from cc3d import connected_components
8
+ from fastremap import remap, unique
9
+ from scipy.ndimage import center_of_mass as scipy_center_of_mass
10
+ from skimage.measure import label as skimage_label
11
+
12
+ from .backend import BackendLike, resolve_backend
13
+ from .src._fast_measure import (
14
+ _center_of_mass as _fast_center_of_mass,
15
+ _labeled_center_of_mass as _fast_labeled_center_of_mass,
16
+ )
17
+ from .src._measure import _center_of_mass, _labeled_center_of_mass
18
+ from .utils import normalize_num_threads
19
+
20
+
21
+ # (ndim, skimage_connectivity) -> cc3d_connectivity
22
+ _SKIMAGE2CC3D = {
23
+ (1, 1): 4,
24
+ (2, 1): 4,
25
+ (2, 2): 8,
26
+ (3, 1): 6,
27
+ (3, 2): 18,
28
+ (3, 3): 26,
29
+ }
30
+
31
+
32
+ def label(
33
+ label_image: np.ndarray,
34
+ background: int = None,
35
+ connectivity: int = None,
36
+ return_num: bool = False,
37
+ return_labels: bool = False,
38
+ return_sizes: bool = False,
39
+ dtype: type = None,
40
+ ) -> Union[np.ndarray, NamedTuple]:
41
+ """
42
+ Fast version of `skimage.measure.label` which optionally returns number of connected components, labels and sizes.
43
+ If 2 or more outputs are requested `NamedTuple` is returned.
44
+
45
+ Parameters
46
+ ----------
47
+ label_image: np.ndarray
48
+ image to label
49
+ background: int
50
+ consider all pixels with this value as background pixels, and label them as 0. By default, 0-valued pixels are
51
+ considered as background pixels
52
+ connectivity: int
53
+ maximum number of orthogonal hops to consider a pixel/voxel as a neighbor. Accepted values are ranging from 1
54
+ to input.ndim. If None, a full connectivity of input.ndim is used
55
+ return_num: bool
56
+ whether to return the number of connected components
57
+ return_labels: bool
58
+ whether to return assigned labels
59
+ return_sizes: bool
60
+ whether to return sizes of connected components (excluding background)
61
+ dtype:
62
+ if specified, must be one of np.uint16, np.uint32 or np.uint64. If not specified, it will be automatically
63
+ determined. Most of the time, you should leave this off so that the smallest safe dtype will be used. However,
64
+ in some applications you can save an up-conversion in the next operation by outputting the appropriately sized
65
+ type instead. Has no effect for python3.6
66
+
67
+ Returns
68
+ -------
69
+ labeled_image: np.ndarray
70
+ array of np.uint16, np.uint32 or np.uint64 numbers depending on the number of connected components and
71
+ `dtype`
72
+ num_components: int
73
+ number of connected components excluding background. Returned if `return_num` is True
74
+ labels: np.ndarray
75
+ components labels. Returned if `return_labels` is True
76
+ sizes: np.ndarray
77
+ components sizes. Returned if `return_sizes` is True
78
+
79
+ Examples
80
+ --------
81
+ ```python
82
+ labeled = label(x)
83
+ labeled, num_components, sizes = label(x, return_num=True, return_sizes=True)
84
+ out = label(x, return_labels=True, return_sizes=True)
85
+ out.labeled_image, out.labels, out.sizes # output fields can be accessed this way
86
+ ```
87
+ """
88
+ ndim = label_image.ndim
89
+ connectivity = connectivity or ndim
90
+
91
+ if not 1 <= connectivity <= ndim:
92
+ raise ValueError(f'Connectivity for {ndim}D image should be in [1, ..., {ndim}]. Got {connectivity}.')
93
+
94
+ if ndim > 3:
95
+ warn("Fast label is only supported for ndim<=3, Falling back to scikit-image's implementation.", stacklevel=2)
96
+ labeled_image, num_components = skimage_label(
97
+ label_image, background=background, return_num=True, connectivity=connectivity
98
+ )
99
+ if dtype is not None:
100
+ labeled_image = labeled_image.astype(dtype, copy=False)
101
+ else:
102
+ if ndim == 1:
103
+ label_image = label_image[None]
104
+
105
+ if background:
106
+ label_image = remap(
107
+ label_image,
108
+ {background: 0, 0: background},
109
+ preserve_missing_labels=True,
110
+ in_place=False,
111
+ )
112
+
113
+ labeled_image, num_components = connected_components(
114
+ label_image,
115
+ connectivity=_SKIMAGE2CC3D[(ndim, connectivity)],
116
+ return_N=True,
117
+ **{'out_dtype': dtype} if python_version()[:3] != '3.6' else {},
118
+ )
119
+
120
+ if ndim == 1:
121
+ labeled_image = labeled_image[0]
122
+
123
+ res = [('labeled_image', labeled_image)]
124
+
125
+ if return_num:
126
+ res.append(('num_components', num_components))
127
+ if return_labels:
128
+ res.append(('labels', np.array(range(1, num_components + 1))))
129
+ if return_sizes:
130
+ _, sizes = unique(labeled_image, return_counts=True)
131
+ res.append(('sizes', sizes[1:] if 0 in labeled_image else sizes))
132
+
133
+ if len(res) == 1:
134
+ return labeled_image
135
+
136
+ return namedtuple('Labeling', [subres[0] for subres in res])(*[subres[1] for subres in res])
137
+
138
+
139
+ def center_of_mass(
140
+ array: np.ndarray,
141
+ labels: np.ndarray = None,
142
+ index: Union[int, Sequence[int]] = None,
143
+ num_threads: int = -1,
144
+ backend: BackendLike = None,
145
+ ) -> Union[Tuple[float, ...], List[Tuple[float, ...]]]:
146
+ """
147
+ Calculate the center of mass of the values.
148
+
149
+ Works faster for ndim <= 3
150
+
151
+ Parameters
152
+ ----------
153
+ array: np.ndarray
154
+ data from which to calculate center-of-mass. The masses can either be positive or negative
155
+ labels: np.ndarray
156
+ labels for objects in input, as generated by `imops.measure.label`. Dimensions must be the same as input. If
157
+ specified, `index` also must be specified and have same dtype
158
+ index: Union[int, Sequence[int]]
159
+ labels for which to calculate centers-of-mass. If specified, `labels` also must be specified and have same dtype
160
+ num_threads: int
161
+ the number of threads to use for computation. Default = the cpu count. If negative value passed
162
+ cpu count + num_threads + 1 threads will be used. If `labels` and `index` are specified, only 1 thread will be
163
+ used
164
+ backend: BackendLike
165
+ which backend to use. `cython` and `scipy` are available, `cython` is used by default
166
+
167
+ Returns
168
+ -------
169
+ center_of_mass: tuple, or list of tuples
170
+ coordinates of centers-of-mass
171
+
172
+ Examples
173
+ --------
174
+ ```python
175
+ center = center_of_mass(np.ones((2, 2))) # (0.5, 0.5)
176
+ ```
177
+ """
178
+ if (labels is None) ^ (index is None):
179
+ raise ValueError('`labels` and `index` should be both specified or both not specified.')
180
+
181
+ backend = resolve_backend(backend, warn_stacklevel=3)
182
+
183
+ if backend.name not in ('Scipy', 'Cython'):
184
+ raise ValueError(f'Unsupported backend "{backend.name}".')
185
+
186
+ num_threads = normalize_num_threads(num_threads, backend, warn_stacklevel=3)
187
+
188
+ if backend.name == 'Scipy':
189
+ return scipy_center_of_mass(array, labels, index)
190
+
191
+ ndim = array.ndim
192
+ if ndim > 3:
193
+ warn("Fast center-of-mass is only supported for ndim<=3. Falling back to scipy's implementation.", stacklevel=2)
194
+ return scipy_center_of_mass(array, labels, index)
195
+
196
+ if labels is None:
197
+ src_center_of_mass = _fast_center_of_mass if backend.fast else _center_of_mass
198
+ else:
199
+ is_sequence = isinstance(index, (Sequence, np.ndarray))
200
+ index = np.array([index] if not is_sequence else index)
201
+
202
+ if labels.shape != array.shape:
203
+ raise ValueError(f'`array` and `labels` must be the same shape, got {array.shape} and {labels.shape}.')
204
+
205
+ if labels.dtype != index.dtype:
206
+ raise ValueError(f'`labels` and `index` must have same dtype, got {labels.dtype} and {index.dtype}.')
207
+
208
+ if len(index) != len(unique(index.astype(int, copy=False))):
209
+ raise ValueError('`index` should consist of unique values.')
210
+
211
+ if num_threads > 1:
212
+ warn('Using single-threaded implementation as `labels` and `index` are specified.', stacklevel=2)
213
+
214
+ src_center_of_mass = _fast_labeled_center_of_mass if backend.fast else _labeled_center_of_mass
215
+
216
+ if array.dtype != 'float64':
217
+ array = array.astype(float)
218
+
219
+ n_dummy = 3 - ndim
220
+ if n_dummy:
221
+ array = array[(None,) * n_dummy]
222
+
223
+ if labels is None:
224
+ return tuple(src_center_of_mass(array, num_threads))[n_dummy:]
225
+
226
+ output = [tuple(x)[n_dummy:] for x in src_center_of_mass(array, labels[(None,) * n_dummy], index)]
227
+
228
+ return output if is_sequence else output[0]