pytme 0.1.8__cp311-cp311-macosx_14_0_arm64.whl → 0.2.0__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.2.0.data/scripts/match_template.py +1019 -0
- pytme-0.2.0.data/scripts/postprocess.py +570 -0
- {pytme-0.1.8.data → pytme-0.2.0.data}/scripts/preprocessor_gui.py +244 -60
- {pytme-0.1.8.dist-info → pytme-0.2.0.dist-info}/METADATA +3 -1
- pytme-0.2.0.dist-info/RECORD +72 -0
- {pytme-0.1.8.dist-info → pytme-0.2.0.dist-info}/WHEEL +1 -1
- scripts/extract_candidates.py +218 -0
- scripts/match_template.py +459 -218
- pytme-0.1.8.data/scripts/match_template.py → scripts/match_template_filters.py +459 -218
- scripts/postprocess.py +380 -435
- scripts/preprocessor_gui.py +244 -60
- scripts/refine_matches.py +218 -0
- tme/__init__.py +2 -1
- tme/__version__.py +1 -1
- tme/analyzer.py +533 -78
- tme/backends/cupy_backend.py +80 -15
- tme/backends/npfftw_backend.py +35 -6
- tme/backends/pytorch_backend.py +15 -7
- tme/density.py +173 -78
- tme/extensions.cpython-311-darwin.so +0 -0
- tme/matching_constrained.py +195 -0
- tme/matching_data.py +78 -32
- tme/matching_exhaustive.py +369 -221
- tme/matching_memory.py +1 -0
- tme/matching_optimization.py +753 -649
- tme/matching_utils.py +152 -8
- tme/orientations.py +561 -0
- tme/preprocessing/__init__.py +2 -0
- tme/preprocessing/_utils.py +176 -0
- tme/preprocessing/composable_filter.py +30 -0
- tme/preprocessing/compose.py +52 -0
- tme/preprocessing/frequency_filters.py +322 -0
- tme/preprocessing/tilt_series.py +967 -0
- tme/preprocessor.py +35 -25
- tme/structure.py +2 -37
- pytme-0.1.8.data/scripts/postprocess.py +0 -625
- pytme-0.1.8.dist-info/RECORD +0 -61
- {pytme-0.1.8.data → pytme-0.2.0.data}/scripts/estimate_ram_usage.py +0 -0
- {pytme-0.1.8.data → pytme-0.2.0.data}/scripts/preprocess.py +0 -0
- {pytme-0.1.8.dist-info → pytme-0.2.0.dist-info}/LICENSE +0 -0
- {pytme-0.1.8.dist-info → pytme-0.2.0.dist-info}/entry_points.txt +0 -0
- {pytme-0.1.8.dist-info → pytme-0.2.0.dist-info}/top_level.txt +0 -0
tme/analyzer.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
""" Implements classes to analyze
|
1
|
+
""" Implements classes to analyze outputs from exhaustive template matching.
|
2
2
|
|
3
3
|
Copyright (c) 2023 European Molecular Biology Laboratory
|
4
4
|
|
@@ -15,20 +15,22 @@ from numpy.typing import NDArray
|
|
15
15
|
from scipy.stats import entropy
|
16
16
|
from sklearn.cluster import DBSCAN
|
17
17
|
from skimage.feature import peak_local_max
|
18
|
-
|
19
|
-
from .extensions import max_index_by_label, online_statistics
|
18
|
+
from skimage.registration._phase_cross_correlation import _upsampled_dft
|
19
|
+
from .extensions import max_index_by_label, online_statistics, find_candidate_indices
|
20
20
|
from .matching_utils import (
|
21
21
|
split_numpy_array_slices,
|
22
22
|
array_to_memmap,
|
23
23
|
generate_tempfile_name,
|
24
|
+
euler_to_rotationmatrix,
|
25
|
+
apply_convolution_mode,
|
24
26
|
)
|
25
|
-
|
26
27
|
from .backends import backend
|
27
28
|
|
28
29
|
|
29
|
-
def
|
30
|
-
|
31
|
-
|
30
|
+
def filter_points_indices_bucket(
|
31
|
+
coordinates: NDArray, min_distance: Tuple[int]
|
32
|
+
) -> NDArray:
|
33
|
+
coordinates = backend.subtract(coordinates, backend.min(coordinates, axis=0))
|
32
34
|
bucket_indices = backend.astype(backend.divide(coordinates, min_distance), int)
|
33
35
|
multiplier = backend.power(
|
34
36
|
backend.max(bucket_indices, axis=0) + 1, backend.arange(bucket_indices.shape[1])
|
@@ -40,7 +42,24 @@ def filter_points_indices(coordinates: NDArray, min_distance: Tuple[int]):
|
|
40
42
|
return unique_indices
|
41
43
|
|
42
44
|
|
43
|
-
def
|
45
|
+
def filter_points_indices(
|
46
|
+
coordinates: NDArray, min_distance: float, bucket_cutoff: int = 1e4
|
47
|
+
) -> NDArray:
|
48
|
+
if min_distance <= 0:
|
49
|
+
return backend.arange(coordinates.shape[0])
|
50
|
+
|
51
|
+
if isinstance(coordinates, np.ndarray):
|
52
|
+
return find_candidate_indices(coordinates, min_distance)
|
53
|
+
elif coordinates.shape[0] > bucket_cutoff:
|
54
|
+
return filter_points_indices_bucket(coordinates, min_distance)
|
55
|
+
distances = np.linalg.norm(coordinates[:, None] - coordinates, axis=-1)
|
56
|
+
distances = np.tril(distances)
|
57
|
+
keep = np.sum(distances > min_distance, axis=1)
|
58
|
+
indices = np.arange(coordinates.shape[0])
|
59
|
+
return indices[keep == indices]
|
60
|
+
|
61
|
+
|
62
|
+
def filter_points(coordinates: NDArray, min_distance: Tuple[int]) -> NDArray:
|
44
63
|
unique_indices = filter_points_indices(coordinates, min_distance)
|
45
64
|
coordinates = coordinates[unique_indices]
|
46
65
|
return coordinates
|
@@ -95,6 +114,12 @@ class PeakCaller(ABC):
|
|
95
114
|
self.min_boundary_distance = min_boundary_distance
|
96
115
|
self.number_of_peaks = number_of_peaks
|
97
116
|
|
117
|
+
# Postprocesing arguments
|
118
|
+
self.fourier_shift = kwargs.get("fourier_shift", None)
|
119
|
+
self.convolution_mode = kwargs.get("convolution_mode", None)
|
120
|
+
self.targetshape = kwargs.get("targetshape", None)
|
121
|
+
self.templateshape = kwargs.get("templateshape", None)
|
122
|
+
|
98
123
|
def __iter__(self):
|
99
124
|
"""
|
100
125
|
Returns a generator to list objects containing translation,
|
@@ -104,7 +129,10 @@ class PeakCaller(ABC):
|
|
104
129
|
yield from self.peak_list
|
105
130
|
|
106
131
|
def __call__(
|
107
|
-
self,
|
132
|
+
self,
|
133
|
+
score_space: NDArray,
|
134
|
+
rotation_matrix: NDArray,
|
135
|
+
**kwargs,
|
108
136
|
) -> None:
|
109
137
|
"""
|
110
138
|
Update the internal parameter store based on input array.
|
@@ -116,7 +144,7 @@ class PeakCaller(ABC):
|
|
116
144
|
rotation_matrix : NDArray
|
117
145
|
Rotation matrix used to obtain the score array.
|
118
146
|
**kwargs
|
119
|
-
|
147
|
+
Optional keyword arguments passed to :py:meth:`PeakCaller.call_peak`.
|
120
148
|
"""
|
121
149
|
peak_positions, peak_details = self.call_peaks(
|
122
150
|
score_space=score_space, rotation_matrix=rotation_matrix, **kwargs
|
@@ -159,11 +187,12 @@ class PeakCaller(ABC):
|
|
159
187
|
peak_positions.shape[0],
|
160
188
|
axis=0,
|
161
189
|
)
|
190
|
+
peak_scores = score_space[tuple(peak_positions.T)]
|
162
191
|
|
163
192
|
self._update(
|
164
193
|
peak_positions=peak_positions,
|
165
194
|
peak_details=peak_details,
|
166
|
-
peak_scores=
|
195
|
+
peak_scores=peak_scores,
|
167
196
|
rotations=rotations,
|
168
197
|
**kwargs,
|
169
198
|
)
|
@@ -190,10 +219,8 @@ class PeakCaller(ABC):
|
|
190
219
|
|
191
220
|
Returns
|
192
221
|
-------
|
193
|
-
NDArray
|
194
|
-
Array of peak
|
195
|
-
NDArray
|
196
|
-
Array of peak details.
|
222
|
+
Tuple[NDArray, NDArray]
|
223
|
+
Array of peak coordinates and peak details.
|
197
224
|
"""
|
198
225
|
|
199
226
|
@classmethod
|
@@ -231,6 +258,79 @@ class PeakCaller(ABC):
|
|
231
258
|
)
|
232
259
|
return tuple(base)
|
233
260
|
|
261
|
+
@staticmethod
|
262
|
+
def oversample_peaks(
|
263
|
+
score_space: NDArray, peak_positions: NDArray, oversampling_factor: int = 8
|
264
|
+
):
|
265
|
+
"""
|
266
|
+
Refines peaks positions in the corresponding score space.
|
267
|
+
|
268
|
+
Parameters
|
269
|
+
----------
|
270
|
+
score_space : NDArray
|
271
|
+
The d-dimensional array representing the score space.
|
272
|
+
peak_positions : NDArray
|
273
|
+
An array of shape (n, d) containing the peak coordinates
|
274
|
+
to be refined, where n is the number of peaks and d is the
|
275
|
+
dimensionality of the score space.
|
276
|
+
oversampling_factor : int, optional
|
277
|
+
The oversampling factor for Fourier transforms. Defaults to 8.
|
278
|
+
|
279
|
+
Returns
|
280
|
+
-------
|
281
|
+
NDArray
|
282
|
+
An array of shape (n, d) containing the refined subpixel
|
283
|
+
coordinates of the peaks.
|
284
|
+
|
285
|
+
Notes
|
286
|
+
-----
|
287
|
+
Floating point peak positions are determined by oversampling the
|
288
|
+
score_space around peak_positions. The accuracy
|
289
|
+
of refinement scales with 1 / oversampling_factor.
|
290
|
+
|
291
|
+
References
|
292
|
+
----------
|
293
|
+
.. [1] https://scikit-image.org/docs/stable/api/skimage.registration.html
|
294
|
+
.. [2] Manuel Guizar-Sicairos, Samuel T. Thurman, and
|
295
|
+
James R. Fienup, “Efficient subpixel image registration
|
296
|
+
algorithms,” Optics Letters 33, 156-158 (2008).
|
297
|
+
DOI:10.1364/OL.33.000156
|
298
|
+
|
299
|
+
"""
|
300
|
+
score_space = backend.to_numpy_array(score_space)
|
301
|
+
peak_positions = backend.to_numpy_array(peak_positions)
|
302
|
+
|
303
|
+
peak_positions = np.round(
|
304
|
+
np.divide(
|
305
|
+
np.multiply(peak_positions, oversampling_factor), oversampling_factor
|
306
|
+
)
|
307
|
+
)
|
308
|
+
upsampled_region_size = np.ceil(np.multiply(oversampling_factor, 1.5))
|
309
|
+
dftshift = np.round(np.divide(upsampled_region_size, 2.0))
|
310
|
+
sample_region_offset = np.subtract(
|
311
|
+
dftshift, np.multiply(peak_positions, oversampling_factor)
|
312
|
+
)
|
313
|
+
|
314
|
+
score_space_ft = np.fft.fftn(score_space).conj()
|
315
|
+
for index in range(sample_region_offset.shape[0]):
|
316
|
+
cross_correlation_upsampled = _upsampled_dft(
|
317
|
+
data=score_space_ft,
|
318
|
+
upsampled_region_size=upsampled_region_size,
|
319
|
+
upsample_factor=oversampling_factor,
|
320
|
+
axis_offsets=sample_region_offset[index],
|
321
|
+
).conj()
|
322
|
+
|
323
|
+
maxima = np.unravel_index(
|
324
|
+
np.argmax(np.abs(cross_correlation_upsampled)),
|
325
|
+
cross_correlation_upsampled.shape,
|
326
|
+
)
|
327
|
+
maxima = np.divide(np.subtract(maxima, dftshift), oversampling_factor)
|
328
|
+
peak_positions[index] = np.add(peak_positions[index], maxima)
|
329
|
+
|
330
|
+
peak_positions = backend.to_backend_array(peak_positions)
|
331
|
+
|
332
|
+
return peak_positions
|
333
|
+
|
234
334
|
def _update(
|
235
335
|
self,
|
236
336
|
peak_positions: NDArray,
|
@@ -287,6 +387,61 @@ class PeakCaller(ABC):
|
|
287
387
|
self.peak_list[2] = peak_scores[final_order]
|
288
388
|
self.peak_list[3] = peak_details[final_order]
|
289
389
|
|
390
|
+
def _postprocess(
|
391
|
+
self, fourier_shift, convolution_mode, targetshape, templateshape, **kwargs
|
392
|
+
):
|
393
|
+
peak_positions = self.peak_list[0]
|
394
|
+
if not len(peak_positions):
|
395
|
+
return self
|
396
|
+
|
397
|
+
if targetshape is None or templateshape is None:
|
398
|
+
return self
|
399
|
+
|
400
|
+
# Remove padding to next fast fourier length
|
401
|
+
score_space_shape = backend.add(targetshape, templateshape) - 1
|
402
|
+
|
403
|
+
if fourier_shift is not None:
|
404
|
+
peak_positions = backend.add(peak_positions, fourier_shift)
|
405
|
+
backend.divide(peak_positions, score_space_shape).astype(int)
|
406
|
+
|
407
|
+
backend.subtract(
|
408
|
+
peak_positions,
|
409
|
+
backend.multiply(
|
410
|
+
backend.divide(peak_positions, score_space_shape).astype(int),
|
411
|
+
score_space_shape,
|
412
|
+
),
|
413
|
+
out=peak_positions,
|
414
|
+
)
|
415
|
+
|
416
|
+
if convolution_mode is None:
|
417
|
+
return None
|
418
|
+
|
419
|
+
if convolution_mode == "full":
|
420
|
+
output_shape = score_space_shape
|
421
|
+
elif convolution_mode == "same":
|
422
|
+
output_shape = targetshape
|
423
|
+
elif convolution_mode == "valid":
|
424
|
+
output_shape = backend.add(
|
425
|
+
backend.subtract(targetshape, templateshape),
|
426
|
+
backend.mod(templateshape, 2),
|
427
|
+
)
|
428
|
+
|
429
|
+
output_shape = backend.to_backend_array(output_shape)
|
430
|
+
starts = backend.divide(backend.subtract(score_space_shape, output_shape), 2)
|
431
|
+
starts = backend.astype(starts, int)
|
432
|
+
stops = backend.add(starts, output_shape)
|
433
|
+
|
434
|
+
valid_peaks = (
|
435
|
+
backend.sum(
|
436
|
+
backend.multiply(peak_positions > starts, peak_positions <= stops),
|
437
|
+
axis=1,
|
438
|
+
)
|
439
|
+
== peak_positions.shape[1]
|
440
|
+
)
|
441
|
+
self.peak_list[0] = backend.subtract(peak_positions, starts)
|
442
|
+
self.peak_list = [x[valid_peaks] for x in self.peak_list]
|
443
|
+
return self
|
444
|
+
|
290
445
|
|
291
446
|
class PeakCallerSort(PeakCaller):
|
292
447
|
"""
|
@@ -297,7 +452,7 @@ class PeakCallerSort(PeakCaller):
|
|
297
452
|
"""
|
298
453
|
|
299
454
|
def call_peaks(
|
300
|
-
self, score_space: NDArray,
|
455
|
+
self, score_space: NDArray, minimum_score: float = None, **kwargs
|
301
456
|
) -> Tuple[NDArray, NDArray]:
|
302
457
|
"""
|
303
458
|
Call peaks in the score space.
|
@@ -307,20 +462,20 @@ class PeakCallerSort(PeakCaller):
|
|
307
462
|
score_space : NDArray
|
308
463
|
Data array of scores.
|
309
464
|
minimum_score : float
|
310
|
-
Minimum score value to consider.
|
311
|
-
|
312
|
-
Minimum distance between maxima.
|
465
|
+
Minimum score value to consider. If provided, superseeds limit given
|
466
|
+
by :py:attr:`PeakCaller.number_of_peaks`.
|
313
467
|
|
314
468
|
Returns
|
315
469
|
-------
|
316
|
-
NDArray
|
317
|
-
Array of peak
|
318
|
-
NDArray
|
319
|
-
Array of peak details.
|
470
|
+
Tuple[NDArray, NDArray]
|
471
|
+
Array of peak coordinates and peak details.
|
320
472
|
"""
|
321
473
|
flat_score_space = score_space.reshape(-1)
|
322
474
|
k = min(self.number_of_peaks, backend.size(flat_score_space))
|
323
475
|
|
476
|
+
if minimum_score is not None:
|
477
|
+
k = backend.sum(score_space >= minimum_score)
|
478
|
+
|
324
479
|
top_k_indices, *_ = backend.topk_indices(flat_score_space, k)
|
325
480
|
|
326
481
|
coordinates = backend.unravel_index(top_k_indices, score_space.shape)
|
@@ -333,12 +488,12 @@ class PeakCallerSort(PeakCaller):
|
|
333
488
|
class PeakCallerMaximumFilter(PeakCaller):
|
334
489
|
"""
|
335
490
|
Find local maxima by applying a maximum filter and enforcing a distance
|
336
|
-
constraint
|
491
|
+
constraint subsequently. This is similar to the strategy implemented in
|
337
492
|
skimage.feature.peak_local_max.
|
338
493
|
"""
|
339
494
|
|
340
495
|
def call_peaks(
|
341
|
-
self, score_space: NDArray,
|
496
|
+
self, score_space: NDArray, minimum_score: float = None, **kwargs
|
342
497
|
) -> Tuple[NDArray, NDArray]:
|
343
498
|
"""
|
344
499
|
Call peaks in the score space.
|
@@ -348,25 +503,25 @@ class PeakCallerMaximumFilter(PeakCaller):
|
|
348
503
|
score_space : NDArray
|
349
504
|
Data array of scores.
|
350
505
|
minimum_score : float
|
351
|
-
Minimum score value to consider.
|
352
|
-
|
353
|
-
Minimum distance between maxima.
|
506
|
+
Minimum score value to consider. If provided, superseeds limit given
|
507
|
+
by :py:attr:`PeakCaller.number_of_peaks`.
|
354
508
|
|
355
509
|
Returns
|
356
510
|
-------
|
357
|
-
NDArray
|
358
|
-
Array of peak
|
359
|
-
NDArray
|
360
|
-
Array of peak details.
|
511
|
+
Tuple[NDArray, NDArray]
|
512
|
+
Array of peak coordinates and peak details.
|
361
513
|
"""
|
362
514
|
peaks = backend.max_filter_coordinates(score_space, self.min_distance)
|
363
515
|
|
516
|
+
scores = score_space[tuple(peaks.T)]
|
517
|
+
|
364
518
|
input_candidates = min(
|
365
519
|
self.number_of_peaks, peaks.shape[0] - 1, backend.size(score_space) - 1
|
366
520
|
)
|
367
|
-
|
368
|
-
|
369
|
-
|
521
|
+
if minimum_score is not None:
|
522
|
+
input_candidates = backend.sum(scores >= minimum_score)
|
523
|
+
|
524
|
+
top_indices = backend.topk_indices(scores, input_candidates)
|
370
525
|
peaks = peaks[top_indices]
|
371
526
|
|
372
527
|
return peaks, None
|
@@ -382,7 +537,7 @@ class PeakCallerFast(PeakCaller):
|
|
382
537
|
"""
|
383
538
|
|
384
539
|
def call_peaks(
|
385
|
-
self, score_space: NDArray,
|
540
|
+
self, score_space: NDArray, minimum_score: float = None, **kwargs
|
386
541
|
) -> Tuple[NDArray, NDArray]:
|
387
542
|
"""
|
388
543
|
Call peaks in the score space.
|
@@ -392,16 +547,13 @@ class PeakCallerFast(PeakCaller):
|
|
392
547
|
score_space : NDArray
|
393
548
|
Data array of scores.
|
394
549
|
minimum_score : float
|
395
|
-
Minimum score value to consider.
|
396
|
-
|
397
|
-
Minimum distance between maxima.
|
550
|
+
Minimum score value to consider. If provided, superseeds limit given
|
551
|
+
by :py:attr:`PeakCaller.number_of_peaks`.
|
398
552
|
|
399
553
|
Returns
|
400
554
|
-------
|
401
|
-
NDArray
|
402
|
-
Array of peak
|
403
|
-
NDArray
|
404
|
-
Array of peak details.
|
555
|
+
Tuple[NDArray, NDArray]
|
556
|
+
Array of peak coordinates and peak details.
|
405
557
|
"""
|
406
558
|
splits = {
|
407
559
|
axis: score_space.shape[axis] // self.min_distance
|
@@ -457,7 +609,14 @@ class PeakCallerRecursiveMasking(PeakCaller):
|
|
457
609
|
"""
|
458
610
|
|
459
611
|
def call_peaks(
|
460
|
-
self,
|
612
|
+
self,
|
613
|
+
score_space: NDArray,
|
614
|
+
rotation_matrix: NDArray,
|
615
|
+
mask: NDArray = None,
|
616
|
+
minimum_score: float = None,
|
617
|
+
rotation_space: NDArray = None,
|
618
|
+
rotation_mapping: Dict = None,
|
619
|
+
**kwargs,
|
461
620
|
) -> Tuple[NDArray, NDArray]:
|
462
621
|
"""
|
463
622
|
Call peaks in the score space.
|
@@ -466,36 +625,195 @@ class PeakCallerRecursiveMasking(PeakCaller):
|
|
466
625
|
----------
|
467
626
|
score_space : NDArray
|
468
627
|
Data array of scores.
|
628
|
+
rotation_matrix : NDArray
|
629
|
+
Rotation matrix.
|
630
|
+
mask : NDArray, optional
|
631
|
+
Mask array, by default None.
|
632
|
+
rotation_space : NDArray, optional
|
633
|
+
Rotation space array, by default None.
|
634
|
+
rotation_mapping : Dict optional
|
635
|
+
Dictionary mapping values in rotation_space to Euler angles.
|
636
|
+
By default None
|
469
637
|
minimum_score : float
|
470
|
-
Minimum score value to consider.
|
471
|
-
|
472
|
-
Minimum distance between maxima.
|
638
|
+
Minimum score value to consider. If provided, superseeds limit given
|
639
|
+
by :py:attr:`PeakCaller.number_of_peaks`.
|
473
640
|
|
474
641
|
Returns
|
475
642
|
-------
|
476
|
-
NDArray
|
477
|
-
Array of peak
|
478
|
-
|
479
|
-
|
643
|
+
Tuple[NDArray, NDArray]
|
644
|
+
Array of peak coordinates and peak details.
|
645
|
+
|
646
|
+
Notes
|
647
|
+
-----
|
648
|
+
By default, scores are masked using a box with edge length self.min_distance.
|
649
|
+
If mask is provided, elements around each peak will be multiplied by the mask
|
650
|
+
values. If rotation_space and rotation_mapping is provided, the respective
|
651
|
+
rotation will be applied to the mask, otherwise rotation_matrix is used.
|
480
652
|
"""
|
481
|
-
|
482
|
-
|
653
|
+
coordinates, masking_function = [], self._mask_scores_rotate
|
654
|
+
|
655
|
+
if mask is None:
|
656
|
+
masking_function = self._mask_scores_box
|
657
|
+
shape = tuple(self.min_distance for _ in range(score_space.ndim))
|
658
|
+
mask = backend.zeros(shape, dtype=backend._default_dtype)
|
659
|
+
|
660
|
+
rotated_template = backend.zeros(mask.shape, dtype=mask.dtype)
|
661
|
+
|
662
|
+
peak_limit = self.number_of_peaks
|
663
|
+
if minimum_score is not None:
|
664
|
+
peak_limit = backend.size(score_space)
|
665
|
+
else:
|
666
|
+
minimum_score = backend.min(score_space) - 1
|
667
|
+
|
483
668
|
while True:
|
484
669
|
backend.argmax(score_space)
|
485
|
-
|
670
|
+
peak = backend.unravel_index(
|
486
671
|
indices=backend.argmax(score_space), shape=score_space.shape
|
487
672
|
)
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
673
|
+
if score_space[tuple(peak)] < minimum_score:
|
674
|
+
break
|
675
|
+
|
676
|
+
coordinates.append(peak)
|
677
|
+
|
678
|
+
current_rotation_matrix = self._get_rotation_matrix(
|
679
|
+
peak=peak,
|
680
|
+
rotation_space=rotation_space,
|
681
|
+
rotation_mapping=rotation_mapping,
|
682
|
+
rotation_matrix=rotation_matrix,
|
683
|
+
)
|
684
|
+
|
685
|
+
masking_function(
|
686
|
+
score_space=score_space,
|
687
|
+
rotation_matrix=current_rotation_matrix,
|
688
|
+
peak=peak,
|
689
|
+
mask=mask,
|
690
|
+
rotated_template=rotated_template,
|
691
|
+
)
|
692
|
+
|
693
|
+
if len(coordinates) >= peak_limit:
|
495
694
|
break
|
695
|
+
|
496
696
|
peaks = backend.to_backend_array(coordinates)
|
497
697
|
return peaks, None
|
498
698
|
|
699
|
+
@staticmethod
|
700
|
+
def _get_rotation_matrix(
|
701
|
+
peak: NDArray,
|
702
|
+
rotation_space: NDArray,
|
703
|
+
rotation_mapping: NDArray,
|
704
|
+
rotation_matrix: NDArray,
|
705
|
+
) -> NDArray:
|
706
|
+
"""
|
707
|
+
Get rotation matrix based on peak and rotation data.
|
708
|
+
|
709
|
+
Parameters
|
710
|
+
----------
|
711
|
+
peak : NDArray
|
712
|
+
Peak coordinates.
|
713
|
+
rotation_space : NDArray
|
714
|
+
Rotation space array.
|
715
|
+
rotation_mapping : Dict
|
716
|
+
Dictionary mapping values in rotation_space to Euler angles.
|
717
|
+
rotation_matrix : NDArray
|
718
|
+
Current rotation matrix.
|
719
|
+
|
720
|
+
Returns
|
721
|
+
-------
|
722
|
+
NDArray
|
723
|
+
Rotation matrix.
|
724
|
+
"""
|
725
|
+
if rotation_space is None or rotation_mapping is None:
|
726
|
+
return rotation_matrix
|
727
|
+
|
728
|
+
rotation = rotation_mapping[rotation_space[tuple(peak)]]
|
729
|
+
|
730
|
+
rotation_matrix = backend.to_backend_array(
|
731
|
+
euler_to_rotationmatrix(backend.to_numpy_array(rotation))
|
732
|
+
)
|
733
|
+
return rotation_matrix
|
734
|
+
|
735
|
+
@staticmethod
|
736
|
+
def _mask_scores_box(
|
737
|
+
score_space: NDArray, peak: NDArray, mask: NDArray, **kwargs: Dict
|
738
|
+
) -> None:
|
739
|
+
"""
|
740
|
+
Mask scores in a box around a peak.
|
741
|
+
|
742
|
+
Parameters
|
743
|
+
----------
|
744
|
+
score_space : NDArray
|
745
|
+
Data array of scores.
|
746
|
+
peak : NDArray
|
747
|
+
Peak coordinates.
|
748
|
+
mask : NDArray
|
749
|
+
Mask array.
|
750
|
+
"""
|
751
|
+
start = backend.maximum(backend.subtract(peak, mask.shape), 0)
|
752
|
+
stop = backend.minimum(backend.add(peak, mask.shape), score_space.shape)
|
753
|
+
start, stop = backend.astype(start, int), backend.astype(stop, int)
|
754
|
+
coords = tuple(slice(*pos) for pos in zip(start, stop))
|
755
|
+
score_space[coords] = 0
|
756
|
+
return None
|
757
|
+
|
758
|
+
@staticmethod
|
759
|
+
def _mask_scores_rotate(
|
760
|
+
score_space: NDArray,
|
761
|
+
peak: NDArray,
|
762
|
+
mask: NDArray,
|
763
|
+
rotated_template: NDArray,
|
764
|
+
rotation_matrix: NDArray,
|
765
|
+
**kwargs: Dict,
|
766
|
+
) -> None:
|
767
|
+
"""
|
768
|
+
Mask score_space using mask rotation around a peak.
|
769
|
+
|
770
|
+
Parameters
|
771
|
+
----------
|
772
|
+
score_space : NDArray
|
773
|
+
Data array of scores.
|
774
|
+
peak : NDArray
|
775
|
+
Peak coordinates.
|
776
|
+
mask : NDArray
|
777
|
+
Mask array.
|
778
|
+
rotated_template : NDArray
|
779
|
+
Empty array to write mask rotations to.
|
780
|
+
rotation_matrix : NDArray
|
781
|
+
Rotation matrix.
|
782
|
+
"""
|
783
|
+
left_pad = backend.divide(mask.shape, 2).astype(int)
|
784
|
+
right_pad = backend.add(left_pad, backend.mod(mask.shape, 2).astype(int))
|
785
|
+
|
786
|
+
score_start = backend.subtract(peak, left_pad)
|
787
|
+
score_stop = backend.add(peak, right_pad)
|
788
|
+
|
789
|
+
template_start = backend.subtract(backend.maximum(score_start, 0), score_start)
|
790
|
+
template_stop = backend.subtract(
|
791
|
+
score_stop, backend.minimum(score_stop, score_space.shape)
|
792
|
+
)
|
793
|
+
template_stop = backend.subtract(mask.shape, template_stop)
|
794
|
+
|
795
|
+
score_start = backend.maximum(score_start, 0)
|
796
|
+
score_stop = backend.minimum(score_stop, score_space.shape)
|
797
|
+
score_start = backend.astype(score_start, int)
|
798
|
+
score_stop = backend.astype(score_stop, int)
|
799
|
+
|
800
|
+
template_start = backend.astype(template_start, int)
|
801
|
+
template_stop = backend.astype(template_stop, int)
|
802
|
+
coords_score = tuple(slice(*pos) for pos in zip(score_start, score_stop))
|
803
|
+
coords_template = tuple(
|
804
|
+
slice(*pos) for pos in zip(template_start, template_stop)
|
805
|
+
)
|
806
|
+
|
807
|
+
rotated_template.fill(0)
|
808
|
+
backend.rotate_array(
|
809
|
+
arr=mask, rotation_matrix=rotation_matrix, order=1, out=rotated_template
|
810
|
+
)
|
811
|
+
|
812
|
+
score_space[coords_score] = backend.multiply(
|
813
|
+
score_space[coords_score], (rotated_template[coords_template] <= 0.1)
|
814
|
+
)
|
815
|
+
return None
|
816
|
+
|
499
817
|
|
500
818
|
class PeakCallerScipy(PeakCaller):
|
501
819
|
"""
|
@@ -503,7 +821,7 @@ class PeakCallerScipy(PeakCaller):
|
|
503
821
|
"""
|
504
822
|
|
505
823
|
def call_peaks(
|
506
|
-
self, score_space: NDArray,
|
824
|
+
self, score_space: NDArray, minimum_score: float = None, **kwargs
|
507
825
|
) -> Tuple[NDArray, NDArray]:
|
508
826
|
"""
|
509
827
|
Call peaks in the score space.
|
@@ -513,21 +831,25 @@ class PeakCallerScipy(PeakCaller):
|
|
513
831
|
score_space : NDArray
|
514
832
|
Data array of scores.
|
515
833
|
minimum_score : float
|
516
|
-
Minimum score value to consider.
|
517
|
-
|
518
|
-
Minimum distance between maxima.
|
834
|
+
Minimum score value to consider. If provided, superseeds limit given
|
835
|
+
by :py:attr:`PeakCaller.number_of_peaks`.
|
519
836
|
|
520
837
|
Returns
|
521
838
|
-------
|
522
|
-
NDArray
|
523
|
-
Array of peak
|
524
|
-
NDArray
|
525
|
-
Array of peak details.
|
839
|
+
Tuple[NDArray, NDArray]
|
840
|
+
Array of peak coordinates and peak details.
|
526
841
|
"""
|
842
|
+
|
843
|
+
score_space = backend.to_numpy_array(score_space)
|
844
|
+
num_peaks = self.number_of_peaks
|
845
|
+
if minimum_score is not None:
|
846
|
+
num_peaks = np.inf
|
847
|
+
|
527
848
|
peaks = peak_local_max(
|
528
849
|
score_space,
|
529
|
-
num_peaks=
|
850
|
+
num_peaks=num_peaks,
|
530
851
|
min_distance=self.min_distance,
|
852
|
+
threshold_abs=minimum_score,
|
531
853
|
)
|
532
854
|
return peaks, None
|
533
855
|
|
@@ -879,8 +1201,63 @@ class MaxScoreOverRotations:
|
|
879
1201
|
|
880
1202
|
self.use_memmap = use_memmap
|
881
1203
|
self.lock = Manager().Lock() if thread_safe else nullcontext()
|
1204
|
+
self.lock_is_nullcontext = isinstance(
|
1205
|
+
self.score_space, type(backend.zeros((1)))
|
1206
|
+
)
|
882
1207
|
self.observed_rotations = Manager().dict() if thread_safe else {}
|
883
1208
|
|
1209
|
+
def _postprocess(
|
1210
|
+
self,
|
1211
|
+
fourier_shift,
|
1212
|
+
convolution_mode,
|
1213
|
+
targetshape,
|
1214
|
+
templateshape,
|
1215
|
+
shared_memory_handler=None,
|
1216
|
+
**kwargs,
|
1217
|
+
):
|
1218
|
+
internal_scores = backend.sharedarr_to_arr(
|
1219
|
+
shape=self.score_space_shape,
|
1220
|
+
dtype=self.score_space_dtype,
|
1221
|
+
shm=self.score_space,
|
1222
|
+
)
|
1223
|
+
internal_rotations = backend.sharedarr_to_arr(
|
1224
|
+
shape=self.score_space_shape,
|
1225
|
+
dtype=self.rotation_space_dtype,
|
1226
|
+
shm=self.rotations,
|
1227
|
+
)
|
1228
|
+
|
1229
|
+
if fourier_shift is not None:
|
1230
|
+
axis = tuple(i for i in range(len(fourier_shift)))
|
1231
|
+
internal_scores = backend.roll(
|
1232
|
+
internal_scores, shift=fourier_shift, axis=axis
|
1233
|
+
)
|
1234
|
+
internal_rotations = backend.roll(
|
1235
|
+
internal_rotations, shift=fourier_shift, axis=axis
|
1236
|
+
)
|
1237
|
+
|
1238
|
+
if convolution_mode is not None:
|
1239
|
+
internal_scores = apply_convolution_mode(
|
1240
|
+
internal_scores,
|
1241
|
+
convolution_mode=convolution_mode,
|
1242
|
+
s1=targetshape,
|
1243
|
+
s2=templateshape,
|
1244
|
+
)
|
1245
|
+
internal_rotations = apply_convolution_mode(
|
1246
|
+
internal_rotations,
|
1247
|
+
convolution_mode=convolution_mode,
|
1248
|
+
s1=targetshape,
|
1249
|
+
s2=templateshape,
|
1250
|
+
)
|
1251
|
+
|
1252
|
+
self.score_space_shape = internal_scores.shape
|
1253
|
+
self.score_space = backend.arr_to_sharedarr(
|
1254
|
+
internal_scores, shared_memory_handler
|
1255
|
+
)
|
1256
|
+
self.rotations = backend.arr_to_sharedarr(
|
1257
|
+
internal_rotations, shared_memory_handler
|
1258
|
+
)
|
1259
|
+
return self
|
1260
|
+
|
884
1261
|
def __iter__(self):
|
885
1262
|
internal_scores = backend.sharedarr_to_arr(
|
886
1263
|
shape=self.score_space_shape,
|
@@ -938,11 +1315,21 @@ class MaxScoreOverRotations:
|
|
938
1315
|
**kwargs
|
939
1316
|
Arbitrary keyword arguments.
|
940
1317
|
"""
|
1318
|
+
rotation = backend.tobytes(rotation_matrix)
|
1319
|
+
rotation_index = self.observed_rotations.setdefault(
|
1320
|
+
rotation, len(self.observed_rotations)
|
1321
|
+
)
|
1322
|
+
|
1323
|
+
if self.lock_is_nullcontext:
|
1324
|
+
backend.max_score_over_rotations(
|
1325
|
+
score_space=score_space,
|
1326
|
+
internal_scores=self.score_space,
|
1327
|
+
internal_rotations=self.rotations,
|
1328
|
+
rotation_index=rotation_index,
|
1329
|
+
)
|
1330
|
+
return None
|
1331
|
+
|
941
1332
|
with self.lock:
|
942
|
-
rotation = backend.tobytes(rotation_matrix)
|
943
|
-
if rotation not in self.observed_rotations:
|
944
|
-
self.observed_rotations[rotation] = len(self.observed_rotations)
|
945
|
-
rotation_index = self.observed_rotations[rotation]
|
946
1333
|
internal_scores = backend.sharedarr_to_arr(
|
947
1334
|
shape=self.score_space_shape,
|
948
1335
|
dtype=self.score_space_dtype,
|
@@ -953,9 +1340,14 @@ class MaxScoreOverRotations:
|
|
953
1340
|
dtype=self.rotation_space_dtype,
|
954
1341
|
shm=self.rotations,
|
955
1342
|
)
|
956
|
-
|
957
|
-
|
958
|
-
|
1343
|
+
|
1344
|
+
backend.max_score_over_rotations(
|
1345
|
+
score_space=score_space,
|
1346
|
+
internal_scores=internal_scores,
|
1347
|
+
internal_rotations=internal_rotations,
|
1348
|
+
rotation_index=rotation_index,
|
1349
|
+
)
|
1350
|
+
return None
|
959
1351
|
|
960
1352
|
@classmethod
|
961
1353
|
def merge(cls, param_stores=List[Tuple], **kwargs) -> Tuple[NDArray]:
|
@@ -1007,14 +1399,22 @@ class MaxScoreOverRotations:
|
|
1007
1399
|
scores_out = np.memmap(
|
1008
1400
|
scores_out_filename, mode="w+", shape=base_max, dtype=scores_out_dtype
|
1009
1401
|
)
|
1402
|
+
scores_out.fill(kwargs.get("score_threshold", 0))
|
1403
|
+
scores_out.flush()
|
1010
1404
|
rotations_out = np.memmap(
|
1011
1405
|
rotations_out_filename,
|
1012
1406
|
mode="w+",
|
1013
1407
|
shape=base_max,
|
1014
1408
|
dtype=rotations_out_dtype,
|
1015
1409
|
)
|
1410
|
+
rotations_out.fill(-1)
|
1411
|
+
rotations_out.flush()
|
1016
1412
|
else:
|
1017
|
-
scores_out = np.
|
1413
|
+
scores_out = np.full(
|
1414
|
+
base_max,
|
1415
|
+
fill_value=kwargs.get("score_threshold", 0),
|
1416
|
+
dtype=scores_out_dtype,
|
1417
|
+
)
|
1018
1418
|
rotations_out = np.full(base_max, fill_value=-1, dtype=rotations_out_dtype)
|
1019
1419
|
|
1020
1420
|
for i in range(len(param_stores)):
|
@@ -1079,6 +1479,61 @@ class MaxScoreOverRotations:
|
|
1079
1479
|
)
|
1080
1480
|
|
1081
1481
|
|
1482
|
+
class _MaxScoreOverTranslations(MaxScoreOverRotations):
|
1483
|
+
"""
|
1484
|
+
Obtain the maximum translation score over various rotations.
|
1485
|
+
|
1486
|
+
Attributes
|
1487
|
+
----------
|
1488
|
+
score_space : NDArray
|
1489
|
+
The score space for the observed rotations.
|
1490
|
+
rotations : NDArray
|
1491
|
+
The rotation identifiers for each score.
|
1492
|
+
translation_offset : NDArray, optional
|
1493
|
+
The offset applied during translation.
|
1494
|
+
observed_rotations : int
|
1495
|
+
Count of observed rotations.
|
1496
|
+
use_memmap : bool, optional
|
1497
|
+
Whether to offload internal data arrays to disk
|
1498
|
+
thread_safe: bool, optional
|
1499
|
+
Whether access to internal data arrays should be thread safe
|
1500
|
+
"""
|
1501
|
+
|
1502
|
+
def __call__(
|
1503
|
+
self, score_space: NDArray, rotation_matrix: NDArray, **kwargs
|
1504
|
+
) -> None:
|
1505
|
+
"""
|
1506
|
+
Update internal parameter store based on `score_space`.
|
1507
|
+
|
1508
|
+
Parameters
|
1509
|
+
----------
|
1510
|
+
score_space : ndarray
|
1511
|
+
Numpy array containing the score space.
|
1512
|
+
rotation_matrix : ndarray
|
1513
|
+
Square matrix describing the current rotation.
|
1514
|
+
**kwargs
|
1515
|
+
Arbitrary keyword arguments.
|
1516
|
+
"""
|
1517
|
+
from tme.matching_utils import centered_mask
|
1518
|
+
|
1519
|
+
with self.lock:
|
1520
|
+
rotation = backend.tobytes(rotation_matrix)
|
1521
|
+
if rotation not in self.observed_rotations:
|
1522
|
+
self.observed_rotations[rotation] = len(self.observed_rotations)
|
1523
|
+
score_space = centered_mask(score_space, kwargs["template_shape"])
|
1524
|
+
rotation_index = self.observed_rotations[rotation]
|
1525
|
+
internal_scores = backend.sharedarr_to_arr(
|
1526
|
+
shape=self.score_space_shape,
|
1527
|
+
dtype=self.score_space_dtype,
|
1528
|
+
shm=self.score_space,
|
1529
|
+
)
|
1530
|
+
max_score = score_space.max(axis=(1, 2, 3))
|
1531
|
+
mean_score = score_space.mean(axis=(1, 2, 3))
|
1532
|
+
std_score = score_space.std(axis=(1, 2, 3))
|
1533
|
+
z_score = (max_score - mean_score) / std_score
|
1534
|
+
internal_scores[rotation_index] = z_score
|
1535
|
+
|
1536
|
+
|
1082
1537
|
class MemmapHandler:
|
1083
1538
|
"""
|
1084
1539
|
Create numpy memmap objects to write score spaces to.
|