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.
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-312-x86_64-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-312-x86_64-linux-gnu.so +0 -0
  27. imops/src/_fast_backprojection.c +27374 -0
  28. imops/src/_fast_backprojection.cpython-312-x86_64-linux-gnu.so +0 -0
  29. imops/src/_fast_measure.c +33845 -0
  30. imops/src/_fast_measure.cpython-312-x86_64-linux-gnu.so +0 -0
  31. imops/src/_fast_morphology.c +26124 -0
  32. imops/src/_fast_morphology.cpython-312-x86_64-linux-gnu.so +0 -0
  33. imops/src/_fast_numeric.c +48686 -0
  34. imops/src/_fast_numeric.cpython-312-x86_64-linux-gnu.so +0 -0
  35. imops/src/_fast_radon.c +30749 -0
  36. imops/src/_fast_radon.cpython-312-x86_64-linux-gnu.so +0 -0
  37. imops/src/_fast_zoom.c +57238 -0
  38. imops/src/_fast_zoom.cpython-312-x86_64-linux-gnu.so +0 -0
  39. imops/src/_measure.c +33810 -0
  40. imops/src/_measure.cpython-312-x86_64-linux-gnu.so +0 -0
  41. imops/src/_morphology.c +26089 -0
  42. imops/src/_morphology.cpython-312-x86_64-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-312-x86_64-linux-gnu.so +0 -0
  46. imops/src/_radon.c +30714 -0
  47. imops/src/_radon.cpython-312-x86_64-linux-gnu.so +0 -0
  48. imops/src/_zoom.c +57203 -0
  49. imops/src/_zoom.cpython-312-x86_64-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-a34b3233.so.1.0.0 +0 -0
imops/radon.py ADDED
@@ -0,0 +1,247 @@
1
+ from typing import Sequence, Tuple, Union
2
+
3
+ import numpy as np
4
+ from scipy.fftpack import fft, ifft
5
+
6
+ from .backend import BackendLike, resolve_backend
7
+ from .numeric import copy
8
+ from .src._backprojection import backprojection3d
9
+ from .src._fast_backprojection import backprojection3d as fast_backprojection3d
10
+ from .src._fast_radon import radon3d as fast_radon3d
11
+ from .src._radon import radon3d
12
+ from .utils import normalize_num_threads
13
+
14
+
15
+ def radon(
16
+ image: np.ndarray,
17
+ axes: Tuple[int, int] = None,
18
+ theta: Union[int, Sequence[float]] = 180,
19
+ return_fill: bool = False,
20
+ num_threads: int = -1,
21
+ backend: BackendLike = None,
22
+ ) -> Union[np.ndarray, Tuple[np.ndarray, float]]:
23
+ """
24
+ Fast implementation of Radon transform. Adapted from scikit-image.
25
+
26
+ Parameters
27
+ ----------
28
+ image: np.ndarray
29
+ an n-dimensional array with at least 2 axes
30
+ axes: tuple[int, int]
31
+ the axes in the `image` along which the Radon transform will be applied.
32
+ The `image` shape along the `axes` must be of the same length
33
+ theta: int | Sequence[float]
34
+ the angles for which the Radon transform will be computed. If it is an integer - the angles will
35
+ be evenly distributed between 0 and 180, `theta` values in total
36
+ return_fill: bool
37
+ whether to return the value that fills the image outside the circle working area
38
+ num_threads: int
39
+ the number of threads to be used for parallel computation. By default - equals to the number of cpu cores
40
+ backend: str | Backend
41
+ the execution backend. Currently only "Cython" is avaliable
42
+
43
+ Returns
44
+ -------
45
+ sinogram: np.ndarray
46
+ the result of the Radon transform
47
+ fill_value: float
48
+ the value that fills the image outside the circle working area. Returned only if `return_fill` is True
49
+
50
+ Examples
51
+ --------
52
+ ```python
53
+ sinogram = radon(image) # 2d image
54
+ sinogram, fill_value = radon(image, return_fill=True) # 2d image with fill value
55
+ sinogram = radon(image, axes=(-2, -1)) # nd image
56
+ ```
57
+ """
58
+ backend = resolve_backend(backend, warn_stacklevel=3)
59
+ if backend.name not in ('Cython',):
60
+ raise ValueError(f'Unsupported backend "{backend.name}".')
61
+
62
+ image, axes, extra = normalize_axes(image, axes)
63
+ if image.shape[1] != image.shape[2]:
64
+ raise ValueError(
65
+ f'The image must be square along the provided axes ({axes}), but has shape: {image.shape[1:]}.'
66
+ )
67
+
68
+ if isinstance(theta, int):
69
+ theta = np.linspace(0, 180, theta, endpoint=False)
70
+
71
+ size = image.shape[1]
72
+ radius = size // 2
73
+ xs = np.arange(-radius, size - radius)
74
+ squared = xs**2
75
+ outside_circle = (squared[:, None] + squared[None, :]) > radius**2
76
+ values = image[:, outside_circle]
77
+ min_, max_ = values.min(), values.max()
78
+ if max_ - min_ > 0.1:
79
+ raise ValueError(
80
+ f'The image must be constant outside the circle. ' f'Got values ranging from {min_} to {max_}.'
81
+ )
82
+
83
+ if min_ != 0 or max_ != 0:
84
+ # FIXME: how to accurately pass `num_threads` and `backend` arguments to `copy`?
85
+ image = copy(image, order='C')
86
+ image[:, outside_circle] = 0
87
+
88
+ # TODO: f(arange)?
89
+ limits = ((squared[:, None] + squared[None, :]) > (radius + 2) ** 2).sum(0) // 2
90
+
91
+ num_threads = normalize_num_threads(num_threads, backend, warn_stacklevel=3)
92
+
93
+ radon3d_ = fast_radon3d if backend.fast else radon3d
94
+
95
+ sinogram = radon3d_(image, np.deg2rad(theta, dtype=image.dtype), limits, num_threads)
96
+
97
+ result = restore_axes(sinogram, axes, extra)
98
+ if return_fill:
99
+ result = result, min_
100
+
101
+ return result
102
+
103
+
104
+ def inverse_radon(
105
+ sinogram: np.ndarray,
106
+ axes: Tuple[int, int] = None,
107
+ theta: Union[int, Sequence[float]] = None,
108
+ fill_value: float = 0,
109
+ a: float = 0,
110
+ b: float = 1,
111
+ num_threads: int = -1,
112
+ backend: BackendLike = None,
113
+ ) -> np.ndarray:
114
+ """
115
+ Fast implementation of inverse Radon transform. Adapted from scikit-image.
116
+
117
+ Parameters
118
+ ----------
119
+ sinogram: np.ndarray
120
+ an n-dimensional array with at least 2 axes
121
+ axes: tuple[int, int]
122
+ the axes in the `image` along which the inverse Radon transform will be applied
123
+ theta: int | Sequence[float]
124
+ the angles for which the inverse Radon transform will be computed. If it is an integer - the angles will
125
+ be evenly distributed between 0 and 180, `theta` values in total
126
+ fill_value: float
127
+ the value that fills the image outside the circle working area. Can be returned by `radon`
128
+ a: float
129
+ the first parameter of the sharpen filter
130
+ b: float
131
+ the second parameter of the sharpen filter
132
+ num_threads: int
133
+ the number of threads to be used for parallel computation. By default - equals to the number of cpu cores
134
+ backend: str | Backend
135
+ the execution backend. Currently only "Cython" is avaliable
136
+
137
+ Returns
138
+ -------
139
+ image: np.ndarray
140
+ the result of the inverse Radon transform
141
+
142
+ Examples
143
+ --------
144
+ ```python
145
+ image = inverse_radon(sinogram) # 2d image
146
+ image = inverse_radon(sinogram, fill_value=-1000) # 2d image with fill value
147
+ image = inverse_radon(sinogram, axes=(-2, -1)) # nd image
148
+ ```
149
+ """
150
+ backend = resolve_backend(backend, warn_stacklevel=3)
151
+ if backend.name not in ('Cython',):
152
+ raise ValueError(f'Unsupported backend "{backend.name}".')
153
+
154
+ sinogram, axes, extra = normalize_axes(sinogram, axes)
155
+
156
+ if theta is None:
157
+ theta = sinogram.shape[-1]
158
+ if isinstance(theta, int):
159
+ theta = np.linspace(0, 180, theta, endpoint=False)
160
+
161
+ angles_count = len(theta)
162
+ if angles_count != sinogram.shape[-1]:
163
+ raise ValueError(
164
+ f'The given `theta` (size {angles_count}) does not match the number of '
165
+ f'projections in `sinogram` ({sinogram.shape[-1]}).'
166
+ )
167
+ output_size = sinogram.shape[1]
168
+ sinogram = _sinogram_circle_to_square(sinogram)
169
+
170
+ img_shape = sinogram.shape[1]
171
+ # Resize image to next power of two (but no less than 64) for
172
+ # Fourier analysis; speeds up Fourier and lessens artifacts
173
+ # TODO: why *2?
174
+ projection_size_padded = max(64, int(2 ** np.ceil(np.log2(2 * img_shape))))
175
+ pad_width = ((0, 0), (0, projection_size_padded - img_shape), (0, 0))
176
+ padded_sinogram = np.pad(sinogram, pad_width, mode='constant', constant_values=0)
177
+ fourier_filter = _smooth_sharpen_filter(projection_size_padded, a, b)
178
+
179
+ # Apply filter in Fourier domain
180
+ fourier_img = fft(padded_sinogram, axis=1) * fourier_filter
181
+ filtered_sinogram = np.real(ifft(fourier_img, axis=1)[:, :img_shape, :])
182
+
183
+ radius = output_size // 2
184
+ xs = np.arange(-radius, output_size - radius)
185
+ squared = xs**2
186
+ inside_circle = (squared[:, None] + squared[None, :]) <= radius**2
187
+
188
+ dtype = sinogram.dtype
189
+ filtered_sinogram = filtered_sinogram.astype(dtype, copy=False)
190
+ theta, xs = np.deg2rad(theta, dtype=dtype), xs.astype(dtype, copy=False)
191
+
192
+ num_threads = normalize_num_threads(num_threads, backend, warn_stacklevel=3)
193
+
194
+ backprojection3d_ = fast_backprojection3d if backend.fast else backprojection3d
195
+
196
+ reconstructed = np.asarray(
197
+ backprojection3d_(filtered_sinogram, theta, xs, inside_circle, fill_value, img_shape, output_size, num_threads)
198
+ )
199
+
200
+ return restore_axes(reconstructed, axes, extra)
201
+
202
+
203
+ def normalize_axes(x: np.ndarray, axes):
204
+ if x.ndim < 2:
205
+ raise ValueError(f'Radon transform requires an array with at least 2 dimensions. {x.ndim}-dim array provided')
206
+ if axes is None:
207
+ if x.ndim > 2:
208
+ raise ValueError('For arrays of higher dimensionality the `axis` arguments is required')
209
+ axes = [0, 1]
210
+
211
+ axes = np.core.numeric.normalize_axis_tuple(axes, x.ndim, 'axes')
212
+ x = np.moveaxis(x, axes, (-2, -1))
213
+ extra = x.shape[:-2]
214
+ x = x.reshape(-1, *x.shape[-2:])
215
+ return x, axes, extra
216
+
217
+
218
+ def restore_axes(x: np.ndarray, axes: tuple, extra: tuple) -> np.ndarray:
219
+ x = x.reshape(*extra, *x.shape[-2:])
220
+ x = np.moveaxis(x, (-2, -1), axes)
221
+ return x
222
+
223
+
224
+ def _ramp_filter(size: int) -> np.ndarray:
225
+ n = np.concatenate((np.arange(1, size / 2 + 1, 2, dtype=int), np.arange(size / 2 - 1, 0, -2, dtype=int)))
226
+ f = np.zeros(size)
227
+ f[0] = 0.25
228
+ f[1::2] = -1 / (np.pi * n) ** 2
229
+ fourier_filter = 2 * np.real(fft(f))
230
+
231
+ return fourier_filter.reshape(-1, 1)
232
+
233
+
234
+ def _smooth_sharpen_filter(size: int, a: float, b: float) -> np.ndarray:
235
+ ramp = _ramp_filter(size)
236
+ return ramp * (1 + a * (ramp**b))
237
+
238
+
239
+ def _sinogram_circle_to_square(sinogram: np.ndarray) -> np.ndarray:
240
+ diagonal = int(np.ceil(np.sqrt(2) * sinogram.shape[1]))
241
+ pad = diagonal - sinogram.shape[1]
242
+ old_center = sinogram.shape[1] // 2
243
+ new_center = diagonal // 2
244
+ pad_before = new_center - old_center
245
+ pad_width = ((0, 0), (pad_before, pad - pad_before), (0, 0))
246
+
247
+ return np.pad(sinogram, pad_width, mode='constant', constant_values=0)
imops/src/__init__.py ADDED
File without changes