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/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
|