pytme 0.1.5__cp311-cp311-macosx_14_0_arm64.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.
- pytme-0.1.5.data/scripts/estimate_ram_usage.py +81 -0
- pytme-0.1.5.data/scripts/match_template.py +744 -0
- pytme-0.1.5.data/scripts/postprocess.py +279 -0
- pytme-0.1.5.data/scripts/preprocess.py +93 -0
- pytme-0.1.5.data/scripts/preprocessor_gui.py +729 -0
- pytme-0.1.5.dist-info/LICENSE +153 -0
- pytme-0.1.5.dist-info/METADATA +69 -0
- pytme-0.1.5.dist-info/RECORD +63 -0
- pytme-0.1.5.dist-info/WHEEL +5 -0
- pytme-0.1.5.dist-info/entry_points.txt +6 -0
- pytme-0.1.5.dist-info/top_level.txt +2 -0
- scripts/__init__.py +0 -0
- scripts/estimate_ram_usage.py +81 -0
- scripts/match_template.py +744 -0
- scripts/match_template_devel.py +788 -0
- scripts/postprocess.py +279 -0
- scripts/preprocess.py +93 -0
- scripts/preprocessor_gui.py +729 -0
- tme/__init__.py +6 -0
- tme/__version__.py +1 -0
- tme/analyzer.py +1144 -0
- tme/backends/__init__.py +134 -0
- tme/backends/cupy_backend.py +309 -0
- tme/backends/matching_backend.py +1154 -0
- tme/backends/npfftw_backend.py +763 -0
- tme/backends/pytorch_backend.py +526 -0
- tme/data/__init__.py +0 -0
- tme/data/c48n309.npy +0 -0
- tme/data/c48n527.npy +0 -0
- tme/data/c48n9.npy +0 -0
- tme/data/c48u1.npy +0 -0
- tme/data/c48u1153.npy +0 -0
- tme/data/c48u1201.npy +0 -0
- tme/data/c48u1641.npy +0 -0
- tme/data/c48u181.npy +0 -0
- tme/data/c48u2219.npy +0 -0
- tme/data/c48u27.npy +0 -0
- tme/data/c48u2947.npy +0 -0
- tme/data/c48u3733.npy +0 -0
- tme/data/c48u4749.npy +0 -0
- tme/data/c48u5879.npy +0 -0
- tme/data/c48u7111.npy +0 -0
- tme/data/c48u815.npy +0 -0
- tme/data/c48u83.npy +0 -0
- tme/data/c48u8649.npy +0 -0
- tme/data/c600v.npy +0 -0
- tme/data/c600vc.npy +0 -0
- tme/data/metadata.yaml +80 -0
- tme/data/quat_to_numpy.py +42 -0
- tme/data/scattering_factors.pickle +0 -0
- tme/density.py +2314 -0
- tme/extensions.cpython-311-darwin.so +0 -0
- tme/helpers.py +881 -0
- tme/matching_data.py +377 -0
- tme/matching_exhaustive.py +1553 -0
- tme/matching_memory.py +382 -0
- tme/matching_optimization.py +1123 -0
- tme/matching_utils.py +1180 -0
- tme/parser.py +429 -0
- tme/preprocessor.py +1291 -0
- tme/scoring.py +866 -0
- tme/structure.py +1428 -0
- tme/types.py +10 -0
@@ -0,0 +1,763 @@
|
|
1
|
+
""" Backend using numpy and pyFFTW for template matching.
|
2
|
+
|
3
|
+
Copyright (c) 2023 European Molecular Biology Laboratory
|
4
|
+
|
5
|
+
Author: Valentin Maurer <valentin.maurer@embl-hamburg.de>
|
6
|
+
"""
|
7
|
+
|
8
|
+
from typing import Tuple, Dict, List
|
9
|
+
from multiprocessing import shared_memory
|
10
|
+
from multiprocessing.managers import SharedMemoryManager
|
11
|
+
from contextlib import contextmanager
|
12
|
+
|
13
|
+
import numpy as np
|
14
|
+
from psutil import virtual_memory
|
15
|
+
from numpy.typing import NDArray
|
16
|
+
from pyfftw import zeros_aligned, simd_alignment, FFTW, next_fast_len
|
17
|
+
from pyfftw.builders import rfftn as rfftn_builder, irfftn as irfftn_builder
|
18
|
+
from scipy.ndimage import maximum_filter, affine_transform
|
19
|
+
|
20
|
+
from .matching_backend import MatchingBackend
|
21
|
+
from ..matching_utils import rigid_transform
|
22
|
+
|
23
|
+
|
24
|
+
class NumpyFFTWBackend(MatchingBackend):
|
25
|
+
"""
|
26
|
+
A numpy and pyfftw based backend for template matching.
|
27
|
+
"""
|
28
|
+
|
29
|
+
def __init__(
|
30
|
+
self,
|
31
|
+
array_backend=np,
|
32
|
+
default_dtype=np.float32,
|
33
|
+
complex_dtype=np.complex64,
|
34
|
+
default_dtype_int=np.int32,
|
35
|
+
**kwargs,
|
36
|
+
):
|
37
|
+
super().__init__(
|
38
|
+
array_backend=array_backend,
|
39
|
+
default_dtype=default_dtype,
|
40
|
+
complex_dtype=complex_dtype,
|
41
|
+
default_dtype_int=np.int32,
|
42
|
+
)
|
43
|
+
self.affine_transform = affine_transform
|
44
|
+
|
45
|
+
def to_backend_array(self, arr: NDArray) -> NDArray:
|
46
|
+
if isinstance(arr, self._array_backend.ndarray):
|
47
|
+
return arr
|
48
|
+
return self._array_backend.asarray(arr)
|
49
|
+
|
50
|
+
def to_numpy_array(self, arr: NDArray) -> NDArray:
|
51
|
+
return arr
|
52
|
+
|
53
|
+
def to_cpu_array(self, arr: NDArray) -> NDArray:
|
54
|
+
return arr
|
55
|
+
|
56
|
+
def free_cache(self):
|
57
|
+
pass
|
58
|
+
|
59
|
+
def add(self, x1, x2, *args, **kwargs) -> NDArray:
|
60
|
+
x1 = self.to_backend_array(x1)
|
61
|
+
x2 = self.to_backend_array(x2)
|
62
|
+
return self._array_backend.add(x1, x2, *args, **kwargs)
|
63
|
+
|
64
|
+
def subtract(self, x1, x2, *args, **kwargs) -> NDArray:
|
65
|
+
x1 = self.to_backend_array(x1)
|
66
|
+
x2 = self.to_backend_array(x2)
|
67
|
+
return self._array_backend.subtract(x1, x2, *args, **kwargs)
|
68
|
+
|
69
|
+
def multiply(self, x1, x2, *args, **kwargs) -> NDArray:
|
70
|
+
x1 = self.to_backend_array(x1)
|
71
|
+
x2 = self.to_backend_array(x2)
|
72
|
+
return self._array_backend.multiply(x1, x2, *args, **kwargs)
|
73
|
+
|
74
|
+
def divide(self, x1, x2, *args, **kwargs) -> NDArray:
|
75
|
+
x1 = self.to_backend_array(x1)
|
76
|
+
x2 = self.to_backend_array(x2)
|
77
|
+
return self._array_backend.divide(x1, x2, *args, **kwargs)
|
78
|
+
|
79
|
+
def mod(self, x1, x2, *args, **kwargs):
|
80
|
+
x1 = self.to_backend_array(x1)
|
81
|
+
x2 = self.to_backend_array(x2)
|
82
|
+
return self._array_backend.mod(x1, x2, *args, **kwargs)
|
83
|
+
|
84
|
+
def sum(self, *args, **kwargs) -> NDArray:
|
85
|
+
return self._array_backend.sum(*args, **kwargs)
|
86
|
+
|
87
|
+
def einsum(self, *args, **kwargs) -> NDArray:
|
88
|
+
return self._array_backend.einsum(*args, **kwargs)
|
89
|
+
|
90
|
+
def mean(self, *args, **kwargs) -> NDArray:
|
91
|
+
return self._array_backend.mean(*args, **kwargs)
|
92
|
+
|
93
|
+
def std(self, *args, **kwargs) -> NDArray:
|
94
|
+
return self._array_backend.std(*args, **kwargs)
|
95
|
+
|
96
|
+
def max(self, *args, **kwargs) -> NDArray:
|
97
|
+
return self._array_backend.max(*args, **kwargs)
|
98
|
+
|
99
|
+
def min(self, *args, **kwargs) -> NDArray:
|
100
|
+
return self._array_backend.min(*args, **kwargs)
|
101
|
+
|
102
|
+
def maximum(self, x1, x2, *args, **kwargs) -> NDArray:
|
103
|
+
x1 = self.to_backend_array(x1)
|
104
|
+
x2 = self.to_backend_array(x2)
|
105
|
+
return self._array_backend.maximum(x1, x2, *args, **kwargs)
|
106
|
+
|
107
|
+
def minimum(self, x1, x2, *args, **kwargs) -> NDArray:
|
108
|
+
x1 = self.to_backend_array(x1)
|
109
|
+
x2 = self.to_backend_array(x2)
|
110
|
+
return self._array_backend.minimum(x1, x2, *args, **kwargs)
|
111
|
+
|
112
|
+
def sqrt(self, *args, **kwargs) -> NDArray:
|
113
|
+
return self._array_backend.sqrt(*args, **kwargs)
|
114
|
+
|
115
|
+
def square(self, *args, **kwargs) -> NDArray:
|
116
|
+
return self._array_backend.square(*args, **kwargs)
|
117
|
+
|
118
|
+
def abs(self, *args, **kwargs) -> NDArray:
|
119
|
+
return self._array_backend.abs(*args, **kwargs)
|
120
|
+
|
121
|
+
def transpose(self, arr):
|
122
|
+
return arr.T
|
123
|
+
|
124
|
+
def power(self, *args, **kwargs):
|
125
|
+
return self._array_backend.power(*args, **kwargs)
|
126
|
+
|
127
|
+
def tobytes(self, arr):
|
128
|
+
return arr.tobytes()
|
129
|
+
|
130
|
+
def size(self, arr):
|
131
|
+
return arr.size
|
132
|
+
|
133
|
+
def fill(self, arr: NDArray, value: float) -> None:
|
134
|
+
arr.fill(value)
|
135
|
+
|
136
|
+
def zeros(self, shape, dtype=np.float64) -> NDArray:
|
137
|
+
return self._array_backend.zeros(shape=shape, dtype=dtype)
|
138
|
+
|
139
|
+
def full(self, shape, fill_value, dtype=None, **kwargs) -> NDArray:
|
140
|
+
return self._array_backend.full(
|
141
|
+
shape, dtype=dtype, fill_value=fill_value, **kwargs
|
142
|
+
)
|
143
|
+
|
144
|
+
def eps(self, dtype: type) -> NDArray:
|
145
|
+
"""
|
146
|
+
Returns the eps defined as diffeerence between 1.0 and the next
|
147
|
+
representable floating point value larger than 1.0.
|
148
|
+
|
149
|
+
Parameters
|
150
|
+
----------
|
151
|
+
dtype : type
|
152
|
+
Data type for which eps should be returned.
|
153
|
+
|
154
|
+
Returns
|
155
|
+
-------
|
156
|
+
Scalar
|
157
|
+
The eps for the given data type
|
158
|
+
"""
|
159
|
+
return self._array_backend.finfo(dtype).eps
|
160
|
+
|
161
|
+
def datatype_bytes(self, dtype: type) -> NDArray:
|
162
|
+
"""
|
163
|
+
Return the number of bytes occupied by a given datatype.
|
164
|
+
|
165
|
+
Parameters
|
166
|
+
----------
|
167
|
+
dtype : type
|
168
|
+
Datatype for which the number of bytes is to be determined.
|
169
|
+
|
170
|
+
Returns
|
171
|
+
-------
|
172
|
+
int
|
173
|
+
Number of bytes occupied by the datatype.
|
174
|
+
"""
|
175
|
+
temp = self._array_backend.zeros(1, dtype=dtype)
|
176
|
+
return temp.nbytes
|
177
|
+
|
178
|
+
def clip(self, *args, **kwargs) -> NDArray:
|
179
|
+
return self._array_backend.clip(*args, **kwargs)
|
180
|
+
|
181
|
+
def flip(self, a, axis, **kwargs):
|
182
|
+
return self._array_backend.flip(a, axis, **kwargs)
|
183
|
+
|
184
|
+
@staticmethod
|
185
|
+
def astype(arr, dtype):
|
186
|
+
return arr.astype(dtype)
|
187
|
+
|
188
|
+
def arange(self, *args, **kwargs):
|
189
|
+
return self._array_backend.arange(*args, **kwargs)
|
190
|
+
|
191
|
+
def stack(self, *args, **kwargs):
|
192
|
+
return self._array_backend.stack(*args, **kwargs)
|
193
|
+
|
194
|
+
def concatenate(self, *args, **kwargs):
|
195
|
+
return self._array_backend.concatenate(*args, **kwargs)
|
196
|
+
|
197
|
+
def repeat(self, *args, **kwargs):
|
198
|
+
return self._array_backend.repeat(*args, **kwargs)
|
199
|
+
|
200
|
+
def topk_indices(self, arr: NDArray, k: int):
|
201
|
+
temp = arr.reshape(-1)
|
202
|
+
indices = self._array_backend.argpartition(temp, -k)[-k:][:k]
|
203
|
+
sorted_indices = indices[self._array_backend.argsort(temp[indices])][::-1]
|
204
|
+
sorted_indices = self.unravel_index(indices=sorted_indices, shape=arr.shape)
|
205
|
+
return sorted_indices
|
206
|
+
|
207
|
+
def indices(self, *args, **kwargs) -> NDArray:
|
208
|
+
return self._array_backend.indices(*args, **kwargs)
|
209
|
+
|
210
|
+
def roll(self, a, shift, axis, **kwargs):
|
211
|
+
return self._array_backend.roll(
|
212
|
+
a,
|
213
|
+
shift=shift,
|
214
|
+
axis=axis,
|
215
|
+
**kwargs,
|
216
|
+
)
|
217
|
+
|
218
|
+
def unique(self, *args, **kwargs):
|
219
|
+
return self._array_backend.unique(*args, **kwargs)
|
220
|
+
|
221
|
+
def argsort(self, *args, **kwargs):
|
222
|
+
return self._array_backend.argsort(*args, **kwargs)
|
223
|
+
|
224
|
+
def unravel_index(self, indices, shape):
|
225
|
+
return self._array_backend.unravel_index(indices=indices, shape=shape)
|
226
|
+
|
227
|
+
def tril_indices(self, *args, **kwargs):
|
228
|
+
return self._array_backend.tril_indices(*args, **kwargs)
|
229
|
+
|
230
|
+
def max_filter_coordinates(self, score_space, min_distance: Tuple[int]):
|
231
|
+
score_box = tuple(min_distance for _ in range(score_space.ndim))
|
232
|
+
max_filter = maximum_filter(score_space, size=score_box, mode="constant")
|
233
|
+
max_filter = max_filter == score_space
|
234
|
+
|
235
|
+
peaks = np.array(np.nonzero(max_filter)).T
|
236
|
+
return peaks
|
237
|
+
|
238
|
+
@staticmethod
|
239
|
+
def preallocate_array(shape: Tuple[int], dtype: type) -> NDArray:
|
240
|
+
"""
|
241
|
+
Returns a byte-aligned array of zeros with specified shape and dtype.
|
242
|
+
|
243
|
+
Parameters
|
244
|
+
----------
|
245
|
+
shape : Tuple[int]
|
246
|
+
Desired shape for the array.
|
247
|
+
dtype : type
|
248
|
+
Desired data type for the array.
|
249
|
+
|
250
|
+
Returns
|
251
|
+
-------
|
252
|
+
NDArray
|
253
|
+
Byte-aligned array of zeros with specified shape and dtype.
|
254
|
+
"""
|
255
|
+
arr = zeros_aligned(shape, dtype=dtype, n=simd_alignment)
|
256
|
+
return arr
|
257
|
+
|
258
|
+
def sharedarr_to_arr(
|
259
|
+
self, shape: Tuple[int], dtype: str, shm: shared_memory.SharedMemory
|
260
|
+
) -> NDArray:
|
261
|
+
"""
|
262
|
+
Returns an array of given shape and dtype from shared memory location.
|
263
|
+
|
264
|
+
Parameters
|
265
|
+
----------
|
266
|
+
shape : tuple
|
267
|
+
Tuple of integers specifying the shape of the array.
|
268
|
+
dtype : str
|
269
|
+
String specifying the dtype of the array.
|
270
|
+
shm : shared_memory.SharedMemory
|
271
|
+
Shared memory object where the array is stored.
|
272
|
+
|
273
|
+
Returns
|
274
|
+
-------
|
275
|
+
NDArray
|
276
|
+
Array of the specified shape and dtype from the shared memory location.
|
277
|
+
"""
|
278
|
+
return self.ndarray(shape, dtype, shm.buf)
|
279
|
+
|
280
|
+
def arr_to_sharedarr(
|
281
|
+
self, arr: NDArray, shared_memory_handler: type = None
|
282
|
+
) -> shared_memory.SharedMemory:
|
283
|
+
"""
|
284
|
+
Converts a numpy array to an object shared in memory.
|
285
|
+
|
286
|
+
Parameters
|
287
|
+
----------
|
288
|
+
arr : NDArray
|
289
|
+
Numpy array to convert.
|
290
|
+
shared_memory_handler : type, optional
|
291
|
+
The type of shared memory handler. Default is None.
|
292
|
+
|
293
|
+
Returns
|
294
|
+
-------
|
295
|
+
shared_memory.SharedMemory
|
296
|
+
The shared memory object containing the numpy array.
|
297
|
+
"""
|
298
|
+
if type(shared_memory_handler) == SharedMemoryManager:
|
299
|
+
shm = shared_memory_handler.SharedMemory(size=arr.nbytes)
|
300
|
+
else:
|
301
|
+
shm = shared_memory.SharedMemory(create=True, size=arr.nbytes)
|
302
|
+
np_array = self.ndarray(arr.shape, dtype=arr.dtype, buffer=shm.buf)
|
303
|
+
np_array[:] = arr[:].copy()
|
304
|
+
return shm
|
305
|
+
|
306
|
+
def topleft_pad(self, arr: NDArray, shape: Tuple[int], padval: int = 0) -> NDArray:
|
307
|
+
"""
|
308
|
+
Returns an array that has been padded to a specified shape with a padding
|
309
|
+
value at the top-left corner.
|
310
|
+
|
311
|
+
Parameters
|
312
|
+
----------
|
313
|
+
arr : NDArray
|
314
|
+
Input array to be padded.
|
315
|
+
shape : Tuple[int]
|
316
|
+
Desired shape for the output array.
|
317
|
+
padval : int, optional
|
318
|
+
Value to use for padding, default is 0.
|
319
|
+
|
320
|
+
Returns
|
321
|
+
-------
|
322
|
+
NDArray
|
323
|
+
Array that has been padded to the specified shape.
|
324
|
+
"""
|
325
|
+
b = self.preallocate_array(shape, arr.dtype)
|
326
|
+
self.add(b, padval, out=b)
|
327
|
+
aind = [slice(None, None)] * arr.ndim
|
328
|
+
bind = [slice(None, None)] * arr.ndim
|
329
|
+
for i in range(arr.ndim):
|
330
|
+
if arr.shape[i] > shape[i]:
|
331
|
+
aind[i] = slice(0, shape[i])
|
332
|
+
elif arr.shape[i] < shape[i]:
|
333
|
+
bind[i] = slice(0, arr.shape[i])
|
334
|
+
b[tuple(bind)] = arr[tuple(aind)]
|
335
|
+
return b
|
336
|
+
|
337
|
+
def build_fft(
|
338
|
+
self,
|
339
|
+
fast_shape: Tuple[int],
|
340
|
+
fast_ft_shape: Tuple[int],
|
341
|
+
real_dtype: type,
|
342
|
+
complex_dtype: type,
|
343
|
+
fftargs: Dict = {},
|
344
|
+
temp_real: NDArray = None,
|
345
|
+
temp_fft: NDArray = None,
|
346
|
+
) -> Tuple[FFTW, FFTW]:
|
347
|
+
"""
|
348
|
+
Build pyFFTW builder functions.
|
349
|
+
|
350
|
+
Parameters
|
351
|
+
----------
|
352
|
+
fast_shape : tuple
|
353
|
+
Tuple of integers corresponding to fast convolution shape
|
354
|
+
(see `compute_convolution_shapes`).
|
355
|
+
fast_ft_shape : tuple
|
356
|
+
Tuple of integers corresponding to the shape of the fourier
|
357
|
+
transform array (see `compute_convolution_shapes`).
|
358
|
+
real_dtype : dtype
|
359
|
+
Numpy dtype of the inverse fourier transform.
|
360
|
+
complex_dtype : dtype
|
361
|
+
Numpy dtype of the fourier transform.
|
362
|
+
fftargs : dict, optional
|
363
|
+
Dictionary passed to pyFFTW builders.
|
364
|
+
temp_real : NDArray, optional
|
365
|
+
Temporary real numpy array, by default None.
|
366
|
+
temp_fft : NDArray, optional
|
367
|
+
Temporary fft numpy array, by default None.
|
368
|
+
|
369
|
+
Returns
|
370
|
+
-------
|
371
|
+
tuple
|
372
|
+
Tuple containing callable pyFFTW objects for forward and inverse
|
373
|
+
fourier transform.
|
374
|
+
"""
|
375
|
+
if temp_real is None:
|
376
|
+
temp_real = self.preallocate_array(fast_shape, real_dtype)
|
377
|
+
if temp_fft is None:
|
378
|
+
temp_fft = self.preallocate_array(fast_ft_shape, complex_dtype)
|
379
|
+
|
380
|
+
default_values = {
|
381
|
+
"planner_effort": "FFTW_MEASURE",
|
382
|
+
"auto_align_input": False,
|
383
|
+
"auto_contiguous": False,
|
384
|
+
"avoid_copy": True,
|
385
|
+
"overwrite_input": True,
|
386
|
+
"threads": 1,
|
387
|
+
}
|
388
|
+
for key in default_values:
|
389
|
+
if key in fftargs:
|
390
|
+
continue
|
391
|
+
fftargs[key] = default_values[key]
|
392
|
+
|
393
|
+
rfftn = rfftn_builder(temp_real, s=fast_shape, **fftargs)
|
394
|
+
|
395
|
+
overwrite_input = None
|
396
|
+
if "overwrite_input" in fftargs:
|
397
|
+
overwrite_input = fftargs.pop("overwrite_input")
|
398
|
+
irfftn = irfftn_builder(temp_fft, s=fast_shape, **fftargs)
|
399
|
+
|
400
|
+
if overwrite_input is not None:
|
401
|
+
fftargs["overwrite_input"] = overwrite_input
|
402
|
+
return rfftn, irfftn
|
403
|
+
|
404
|
+
def extract_center(self, arr: NDArray, newshape: Tuple[int]) -> NDArray:
|
405
|
+
"""
|
406
|
+
Extract the centered portion of an array based on a new shape.
|
407
|
+
|
408
|
+
Parameters
|
409
|
+
----------
|
410
|
+
arr : NDArray
|
411
|
+
Input array.
|
412
|
+
newshape : tuple
|
413
|
+
Desired shape for the central portion.
|
414
|
+
|
415
|
+
Returns
|
416
|
+
-------
|
417
|
+
NDArray
|
418
|
+
Central portion of the array with shape `newshape`.
|
419
|
+
|
420
|
+
References
|
421
|
+
----------
|
422
|
+
.. [1] https://github.com/scipy/scipy/blob/v1.11.2/scipy/signal/_signaltools.py
|
423
|
+
"""
|
424
|
+
new_shape = self.to_backend_array(newshape)
|
425
|
+
current_shape = self.to_backend_array(arr.shape)
|
426
|
+
starts = self.subtract(current_shape, new_shape)
|
427
|
+
starts = self.astype(self.divide(starts, 2), int)
|
428
|
+
stops = self.add(starts, newshape)
|
429
|
+
box = tuple(slice(start, stop) for start, stop in zip(starts, stops))
|
430
|
+
return arr[box]
|
431
|
+
|
432
|
+
def compute_convolution_shapes(
|
433
|
+
self, arr1_shape: Tuple[int], arr2_shape: Tuple[int]
|
434
|
+
) -> Tuple[List[int], List[int], List[int]]:
|
435
|
+
"""
|
436
|
+
Computes regular, optimized and fourier convolution shape.
|
437
|
+
|
438
|
+
Parameters
|
439
|
+
----------
|
440
|
+
arr1_shape : tuple
|
441
|
+
Tuple of integers corresponding to array1 shape.
|
442
|
+
arr2_shape : tuple
|
443
|
+
Tuple of integers corresponding to array2 shape.
|
444
|
+
|
445
|
+
Returns
|
446
|
+
-------
|
447
|
+
tuple
|
448
|
+
Tuple with regular convolution shape, convolution shape optimized for faster
|
449
|
+
fourier transform, shape of the forward fourier transform
|
450
|
+
(see :py:meth:`build_fft`).
|
451
|
+
"""
|
452
|
+
convolution_shape = [
|
453
|
+
int(x) + int(y) - 1 for x, y in zip(arr1_shape, arr2_shape)
|
454
|
+
]
|
455
|
+
fast_shape = [next_fast_len(x) for x in convolution_shape]
|
456
|
+
fast_ft_shape = list(fast_shape[:-1]) + [fast_shape[-1] // 2 + 1]
|
457
|
+
|
458
|
+
return convolution_shape, fast_shape, fast_ft_shape
|
459
|
+
|
460
|
+
def rotate_array(
|
461
|
+
self,
|
462
|
+
arr: NDArray,
|
463
|
+
rotation_matrix: NDArray,
|
464
|
+
arr_mask: NDArray = None,
|
465
|
+
translation: NDArray = None,
|
466
|
+
use_geometric_center: bool = False,
|
467
|
+
out: NDArray = None,
|
468
|
+
out_mask: NDArray = None,
|
469
|
+
order: int = 3,
|
470
|
+
) -> None:
|
471
|
+
"""
|
472
|
+
Rotates coordinates of arr according to rotation_matrix.
|
473
|
+
|
474
|
+
If no output array is provided, this method will compute an array with
|
475
|
+
sufficient space to hold all elements. If both `arr` and `arr_mask`
|
476
|
+
are provided, `arr_mask` will be centered according to arr.
|
477
|
+
|
478
|
+
Parameters
|
479
|
+
----------
|
480
|
+
arr : NDArray
|
481
|
+
The input array to be rotated.
|
482
|
+
arr_mask : NDArray, optional
|
483
|
+
The mask of `arr` that will be equivalently rotated.
|
484
|
+
rotation_matrix : NDArray
|
485
|
+
The rotation matrix to apply [d x d].
|
486
|
+
translation : NDArray
|
487
|
+
The translation to apply [d].
|
488
|
+
use_geometric_center : bool, optional
|
489
|
+
Whether the rotation should be centered around the geometric
|
490
|
+
or mass center. Default is mass center.
|
491
|
+
out : NDArray, optional
|
492
|
+
The output array to write the rotation of `arr` to.
|
493
|
+
out_mask : NDArray, optional
|
494
|
+
The output array to write the rotation of `arr_mask` to.
|
495
|
+
order : int, optional
|
496
|
+
Spline interpolation order. Has to be in the range 0-5.
|
497
|
+
"""
|
498
|
+
|
499
|
+
if order is None:
|
500
|
+
mask_coordinates = None
|
501
|
+
if arr_mask is not None:
|
502
|
+
mask_coordinates = np.array(np.where(arr_mask > 0))
|
503
|
+
return self.rotate_array_coordinates(
|
504
|
+
arr=arr,
|
505
|
+
arr_mask=arr_mask,
|
506
|
+
coordinates=np.array(np.where(arr > 0)),
|
507
|
+
mask_coordinates=mask_coordinates,
|
508
|
+
out=out,
|
509
|
+
out_mask=out_mask,
|
510
|
+
rotation_matrix=rotation_matrix,
|
511
|
+
translation=translation,
|
512
|
+
use_geometric_center=use_geometric_center,
|
513
|
+
)
|
514
|
+
|
515
|
+
rotate_mask = arr_mask is not None
|
516
|
+
return_type = (out is None) + 2 * rotate_mask * (out_mask is None)
|
517
|
+
translation = np.zeros(arr.ndim) if translation is None else translation
|
518
|
+
|
519
|
+
center = np.divide(arr.shape, 2)
|
520
|
+
if not use_geometric_center:
|
521
|
+
center = self.center_of_mass(arr, cutoff=0)
|
522
|
+
|
523
|
+
rotation_matrix_inverted = np.linalg.inv(rotation_matrix)
|
524
|
+
transformed_center = rotation_matrix_inverted @ center.reshape(-1, 1)
|
525
|
+
transformed_center = transformed_center.reshape(-1)
|
526
|
+
base_offset = np.subtract(center, transformed_center)
|
527
|
+
offset = np.subtract(base_offset, translation)
|
528
|
+
|
529
|
+
out = np.zeros_like(arr) if out is None else out
|
530
|
+
out_slice = tuple(slice(0, stop) for stop in arr.shape)
|
531
|
+
|
532
|
+
# Applying the prefilter can cause artifacts in the mask
|
533
|
+
affine_transform(
|
534
|
+
input=arr,
|
535
|
+
matrix=rotation_matrix_inverted,
|
536
|
+
offset=offset,
|
537
|
+
mode="constant",
|
538
|
+
output=out[out_slice],
|
539
|
+
order=order,
|
540
|
+
prefilter=True,
|
541
|
+
)
|
542
|
+
|
543
|
+
if rotate_mask:
|
544
|
+
out_mask = np.zeros_like(arr_mask) if out_mask is None else out_mask
|
545
|
+
out_mask_slice = tuple(slice(0, stop) for stop in arr_mask.shape)
|
546
|
+
affine_transform(
|
547
|
+
input=arr_mask,
|
548
|
+
matrix=rotation_matrix_inverted,
|
549
|
+
offset=offset,
|
550
|
+
mode="constant",
|
551
|
+
output=out_mask[out_mask_slice],
|
552
|
+
order=order,
|
553
|
+
prefilter=False,
|
554
|
+
)
|
555
|
+
|
556
|
+
match return_type:
|
557
|
+
case 0:
|
558
|
+
return None
|
559
|
+
case 1:
|
560
|
+
return out
|
561
|
+
case 2:
|
562
|
+
return out_mask
|
563
|
+
case 3:
|
564
|
+
return out, out_mask
|
565
|
+
|
566
|
+
@staticmethod
|
567
|
+
def rotate_array_coordinates(
|
568
|
+
arr: NDArray,
|
569
|
+
coordinates: NDArray,
|
570
|
+
rotation_matrix: NDArray,
|
571
|
+
translation: NDArray = None,
|
572
|
+
out: NDArray = None,
|
573
|
+
use_geometric_center: bool = True,
|
574
|
+
arr_mask: NDArray = None,
|
575
|
+
mask_coordinates: NDArray = None,
|
576
|
+
out_mask: NDArray = None,
|
577
|
+
) -> None:
|
578
|
+
"""
|
579
|
+
Rotates coordinates of arr according to rotation_matrix.
|
580
|
+
|
581
|
+
If no output array is provided, this method will compute an array with
|
582
|
+
sufficient space to hold all elements. If both `arr` and `arr_mask`
|
583
|
+
are provided, `arr_mask` will be centered according to arr.
|
584
|
+
|
585
|
+
No centering will be performed if the rotation matrix is the identity matrix.
|
586
|
+
|
587
|
+
Parameters
|
588
|
+
----------
|
589
|
+
arr : NDArray
|
590
|
+
The input array to be rotated.
|
591
|
+
coordinates : NDArray
|
592
|
+
The pointcloud [d x N] containing elements of `arr` that should be rotated.
|
593
|
+
See :py:meth:`Density.to_pointcloud` on how to obtain the coordinates.
|
594
|
+
rotation_matrix : NDArray
|
595
|
+
The rotation matrix to apply [d x d].
|
596
|
+
rotation_matrix : NDArray
|
597
|
+
The translation to apply [d].
|
598
|
+
out : NDArray, optional
|
599
|
+
The output array to write the rotation of `arr` to.
|
600
|
+
use_geometric_center : bool, optional
|
601
|
+
Whether the rotation should be centered around the geometric
|
602
|
+
or mass center.
|
603
|
+
arr_mask : NDArray, optional
|
604
|
+
The mask of `arr` that will be equivalently rotated.
|
605
|
+
mask_coordinates : NDArray, optional
|
606
|
+
Equivalent to `coordinates`, but containing elements of `arr_mask`
|
607
|
+
that should be rotated.
|
608
|
+
out_mask : NDArray, optional
|
609
|
+
The output array to write the rotation of `arr_mask` to.
|
610
|
+
"""
|
611
|
+
rotate_mask = arr_mask is not None and mask_coordinates is not None
|
612
|
+
return_type = (out is None) + 2 * rotate_mask * (out_mask is None)
|
613
|
+
|
614
|
+
# Otherwise array might be slightly shifted by centering
|
615
|
+
if np.allclose(
|
616
|
+
rotation_matrix,
|
617
|
+
np.eye(rotation_matrix.shape[0], dtype=rotation_matrix.dtype),
|
618
|
+
):
|
619
|
+
center_rotation = False
|
620
|
+
|
621
|
+
coordinates_rotated = np.empty(coordinates.shape, dtype=rotation_matrix.dtype)
|
622
|
+
mask_rotated = (
|
623
|
+
np.empty(mask_coordinates.shape, dtype=rotation_matrix.dtype)
|
624
|
+
if rotate_mask
|
625
|
+
else None
|
626
|
+
)
|
627
|
+
|
628
|
+
center = np.array(arr.shape) // 2 if use_geometric_center else None
|
629
|
+
if translation is None:
|
630
|
+
translation = np.zeros(coordinates_rotated.shape[0])
|
631
|
+
|
632
|
+
rigid_transform(
|
633
|
+
coordinates=coordinates,
|
634
|
+
coordinates_mask=mask_coordinates,
|
635
|
+
out=coordinates_rotated,
|
636
|
+
out_mask=mask_rotated,
|
637
|
+
rotation_matrix=rotation_matrix,
|
638
|
+
translation=translation,
|
639
|
+
use_geometric_center=use_geometric_center,
|
640
|
+
center=center,
|
641
|
+
)
|
642
|
+
|
643
|
+
coordinates_rotated = coordinates_rotated.astype(int)
|
644
|
+
offset = coordinates_rotated.min(axis=1)
|
645
|
+
np.multiply(offset, offset < 0, out=offset)
|
646
|
+
coordinates_rotated -= offset[:, None]
|
647
|
+
|
648
|
+
out_offset = np.zeros(
|
649
|
+
coordinates_rotated.shape[0], dtype=coordinates_rotated.dtype
|
650
|
+
)
|
651
|
+
if out is None:
|
652
|
+
out_offset = coordinates_rotated.min(axis=1)
|
653
|
+
coordinates_rotated -= out_offset[:, None]
|
654
|
+
out = np.zeros(coordinates_rotated.max(axis=1) + 1, dtype=arr.dtype)
|
655
|
+
|
656
|
+
if rotate_mask:
|
657
|
+
mask_rotated = mask_rotated.astype(int)
|
658
|
+
if out_mask is None:
|
659
|
+
mask_rotated -= out_offset[:, None]
|
660
|
+
out_mask = np.zeros(
|
661
|
+
coordinates_rotated.max(axis=1) + 1, dtype=arr.dtype
|
662
|
+
)
|
663
|
+
|
664
|
+
in_box = np.logical_and(
|
665
|
+
mask_rotated < np.array(out_mask.shape)[:, None],
|
666
|
+
mask_rotated >= 0,
|
667
|
+
).min(axis=0)
|
668
|
+
out_of_box = np.invert(in_box).sum()
|
669
|
+
if out_of_box != 0:
|
670
|
+
print(
|
671
|
+
f"{out_of_box} elements out of bounds. Perhaps increase"
|
672
|
+
" *arr_mask* size."
|
673
|
+
)
|
674
|
+
|
675
|
+
mask_coordinates = tuple(mask_coordinates[:, in_box])
|
676
|
+
mask_rotated = tuple(mask_rotated[:, in_box])
|
677
|
+
np.add.at(out_mask, mask_rotated, arr_mask[mask_coordinates])
|
678
|
+
|
679
|
+
# Negative coordinates would be (mis)interpreted as reverse index
|
680
|
+
in_box = np.logical_and(
|
681
|
+
coordinates_rotated < np.array(out.shape)[:, None], coordinates_rotated >= 0
|
682
|
+
).min(axis=0)
|
683
|
+
out_of_box = np.invert(in_box).sum()
|
684
|
+
if out_of_box != 0:
|
685
|
+
print(f"{out_of_box} elements out of bounds. Perhaps increase *out* size.")
|
686
|
+
|
687
|
+
coordinates = coordinates[:, in_box]
|
688
|
+
coordinates_rotated = coordinates_rotated[:, in_box]
|
689
|
+
|
690
|
+
coordinates = tuple(coordinates)
|
691
|
+
coordinates_rotated = tuple(coordinates_rotated)
|
692
|
+
np.add.at(out, coordinates_rotated, arr[coordinates])
|
693
|
+
|
694
|
+
match return_type:
|
695
|
+
case 0:
|
696
|
+
return None
|
697
|
+
case 1:
|
698
|
+
return out
|
699
|
+
case 2:
|
700
|
+
return out_mask
|
701
|
+
case 3:
|
702
|
+
return out, out_mask
|
703
|
+
|
704
|
+
def center_of_mass(self, arr: NDArray, cutoff: float = None) -> NDArray:
|
705
|
+
"""
|
706
|
+
Computes the center of mass of a numpy ndarray instance using all available
|
707
|
+
elements. For template matching it typically makes sense to only input
|
708
|
+
positive densities.
|
709
|
+
|
710
|
+
Parameters
|
711
|
+
----------
|
712
|
+
arr : NDArray
|
713
|
+
Array to compute the center of mass of.
|
714
|
+
cutoff : float, optional
|
715
|
+
Densities less than or equal to cutoff are nullified for center
|
716
|
+
of mass computation. By default considers all values.
|
717
|
+
|
718
|
+
Returns
|
719
|
+
-------
|
720
|
+
NDArray
|
721
|
+
Center of mass with shape (arr.ndim).
|
722
|
+
"""
|
723
|
+
cutoff = arr.min() - 1 if cutoff is None else cutoff
|
724
|
+
arr = self._array_backend.where(arr > cutoff, arr, 0)
|
725
|
+
denominator = self.sum(arr)
|
726
|
+
grids = self._array_backend.ogrid[tuple(slice(0, i) for i in arr.shape)]
|
727
|
+
grids = [grid.astype(self._default_dtype) for grid in grids]
|
728
|
+
|
729
|
+
center_of_mass = self.array(
|
730
|
+
[
|
731
|
+
self.sum(self.multiply(arr, grids[dim])) / denominator
|
732
|
+
for dim in range(arr.ndim)
|
733
|
+
]
|
734
|
+
)
|
735
|
+
|
736
|
+
return center_of_mass
|
737
|
+
|
738
|
+
def get_available_memory(self) -> int:
|
739
|
+
return virtual_memory().available
|
740
|
+
|
741
|
+
@contextmanager
|
742
|
+
def set_device(self, device_index: int):
|
743
|
+
yield None
|
744
|
+
|
745
|
+
def device_count(self) -> int:
|
746
|
+
return 1
|
747
|
+
|
748
|
+
@staticmethod
|
749
|
+
def reverse(arr: NDArray) -> NDArray:
|
750
|
+
"""
|
751
|
+
Reverse the order of elements in an array along all its axes.
|
752
|
+
|
753
|
+
Parameters
|
754
|
+
----------
|
755
|
+
arr : NDArray
|
756
|
+
Input array.
|
757
|
+
|
758
|
+
Returns
|
759
|
+
-------
|
760
|
+
NDArray
|
761
|
+
Reversed array.
|
762
|
+
"""
|
763
|
+
return arr[(slice(None, None, -1),) * arr.ndim]
|