imops 0.8.8__cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.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.
- _build_utils.py +113 -0
- imops/__init__.py +10 -0
- imops/__version__.py +1 -0
- imops/_configs.py +29 -0
- imops/backend.py +95 -0
- imops/box.py +74 -0
- imops/cpp/cpp_modules.cpython-312-x86_64-linux-gnu.so +0 -0
- imops/cpp/interp2d/delaunator/delaunator-header-only.hpp +33 -0
- imops/cpp/interp2d/delaunator/delaunator.cpp +645 -0
- imops/cpp/interp2d/delaunator/delaunator.hpp +170 -0
- imops/cpp/interp2d/interpolator.h +52 -0
- imops/cpp/interp2d/triangulator.h +198 -0
- imops/cpp/interp2d/utils.h +63 -0
- imops/cpp/main.cpp +13 -0
- imops/crop.py +120 -0
- imops/interp1d.py +207 -0
- imops/interp2d.py +120 -0
- imops/measure.py +228 -0
- imops/morphology.py +525 -0
- imops/numeric.py +384 -0
- imops/pad.py +253 -0
- imops/py.typed +0 -0
- imops/radon.py +247 -0
- imops/src/__init__.py +0 -0
- imops/src/_backprojection.c +27339 -0
- imops/src/_backprojection.cpython-312-x86_64-linux-gnu.so +0 -0
- imops/src/_fast_backprojection.c +27374 -0
- imops/src/_fast_backprojection.cpython-312-x86_64-linux-gnu.so +0 -0
- imops/src/_fast_measure.c +33845 -0
- imops/src/_fast_measure.cpython-312-x86_64-linux-gnu.so +0 -0
- imops/src/_fast_morphology.c +26124 -0
- imops/src/_fast_morphology.cpython-312-x86_64-linux-gnu.so +0 -0
- imops/src/_fast_numeric.c +48686 -0
- imops/src/_fast_numeric.cpython-312-x86_64-linux-gnu.so +0 -0
- imops/src/_fast_radon.c +30749 -0
- imops/src/_fast_radon.cpython-312-x86_64-linux-gnu.so +0 -0
- imops/src/_fast_zoom.c +57238 -0
- imops/src/_fast_zoom.cpython-312-x86_64-linux-gnu.so +0 -0
- imops/src/_measure.c +33810 -0
- imops/src/_measure.cpython-312-x86_64-linux-gnu.so +0 -0
- imops/src/_morphology.c +26089 -0
- imops/src/_morphology.cpython-312-x86_64-linux-gnu.so +0 -0
- imops/src/_numba_zoom.py +503 -0
- imops/src/_numeric.c +48651 -0
- imops/src/_numeric.cpython-312-x86_64-linux-gnu.so +0 -0
- imops/src/_radon.c +30714 -0
- imops/src/_radon.cpython-312-x86_64-linux-gnu.so +0 -0
- imops/src/_zoom.c +57203 -0
- imops/src/_zoom.cpython-312-x86_64-linux-gnu.so +0 -0
- imops/testing.py +57 -0
- imops/utils.py +205 -0
- imops/zoom.py +297 -0
- imops-0.8.8.dist-info/LICENSE +21 -0
- imops-0.8.8.dist-info/METADATA +218 -0
- imops-0.8.8.dist-info/RECORD +58 -0
- imops-0.8.8.dist-info/WHEEL +6 -0
- imops-0.8.8.dist-info/top_level.txt +2 -0
- imops.libs/libgomp-a34b3233.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]
|