pytme 0.1.8__cp311-cp311-macosx_14_0_arm64.whl → 0.2.0b0__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.8.data → pytme-0.2.0b0.data}/scripts/match_template.py +148 -126
- pytme-0.2.0b0.data/scripts/postprocess.py +570 -0
- {pytme-0.1.8.data → pytme-0.2.0b0.data}/scripts/preprocessor_gui.py +244 -60
- {pytme-0.1.8.dist-info → pytme-0.2.0b0.dist-info}/METADATA +3 -1
- pytme-0.2.0b0.dist-info/RECORD +66 -0
- {pytme-0.1.8.dist-info → pytme-0.2.0b0.dist-info}/WHEEL +1 -1
- scripts/extract_candidates.py +218 -0
- scripts/match_template.py +148 -126
- scripts/match_template_filters.py +852 -0
- 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 +545 -78
- tme/backends/cupy_backend.py +80 -15
- tme/backends/npfftw_backend.py +33 -2
- tme/backends/pytorch_backend.py +15 -7
- tme/density.py +156 -63
- tme/extensions.cpython-311-darwin.so +0 -0
- tme/matching_constrained.py +195 -0
- tme/matching_data.py +76 -32
- tme/matching_exhaustive.py +366 -204
- tme/matching_memory.py +1 -0
- tme/matching_optimization.py +728 -651
- tme/matching_utils.py +152 -8
- tme/orientations.py +561 -0
- tme/preprocessor.py +21 -18
- 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.0b0.data}/scripts/estimate_ram_usage.py +0 -0
- {pytme-0.1.8.data → pytme-0.2.0b0.data}/scripts/preprocess.py +0 -0
- {pytme-0.1.8.dist-info → pytme-0.2.0b0.dist-info}/LICENSE +0 -0
- {pytme-0.1.8.dist-info → pytme-0.2.0b0.dist-info}/entry_points.txt +0 -0
- {pytme-0.1.8.dist-info → pytme-0.2.0b0.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,66 @@ 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(
|
431
|
+
backend.subtract(score_space_shape, output_shape),
|
432
|
+
2
|
433
|
+
)
|
434
|
+
starts = backend.astype(starts, int)
|
435
|
+
stops = backend.add(starts, output_shape)
|
436
|
+
|
437
|
+
valid_peaks = (
|
438
|
+
backend.sum(
|
439
|
+
backend.multiply(
|
440
|
+
peak_positions > starts,
|
441
|
+
peak_positions <= stops
|
442
|
+
),
|
443
|
+
axis=1,
|
444
|
+
)
|
445
|
+
== peak_positions.shape[1]
|
446
|
+
)
|
447
|
+
self.peak_list[0] = backend.subtract(peak_positions, starts)
|
448
|
+
self.peak_list = [x[valid_peaks] for x in self.peak_list]
|
449
|
+
return self
|
290
450
|
|
291
451
|
class PeakCallerSort(PeakCaller):
|
292
452
|
"""
|
@@ -297,7 +457,7 @@ class PeakCallerSort(PeakCaller):
|
|
297
457
|
"""
|
298
458
|
|
299
459
|
def call_peaks(
|
300
|
-
self, score_space: NDArray,
|
460
|
+
self, score_space: NDArray, minimum_score: float = None, **kwargs
|
301
461
|
) -> Tuple[NDArray, NDArray]:
|
302
462
|
"""
|
303
463
|
Call peaks in the score space.
|
@@ -307,20 +467,20 @@ class PeakCallerSort(PeakCaller):
|
|
307
467
|
score_space : NDArray
|
308
468
|
Data array of scores.
|
309
469
|
minimum_score : float
|
310
|
-
Minimum score value to consider.
|
311
|
-
|
312
|
-
Minimum distance between maxima.
|
470
|
+
Minimum score value to consider. If provided, superseeds limit given
|
471
|
+
by :py:attr:`PeakCaller.number_of_peaks`.
|
313
472
|
|
314
473
|
Returns
|
315
474
|
-------
|
316
|
-
NDArray
|
317
|
-
Array of peak
|
318
|
-
NDArray
|
319
|
-
Array of peak details.
|
475
|
+
Tuple[NDArray, NDArray]
|
476
|
+
Array of peak coordinates and peak details.
|
320
477
|
"""
|
321
478
|
flat_score_space = score_space.reshape(-1)
|
322
479
|
k = min(self.number_of_peaks, backend.size(flat_score_space))
|
323
480
|
|
481
|
+
if minimum_score is not None:
|
482
|
+
k = backend.sum(score_space >= minimum_score)
|
483
|
+
|
324
484
|
top_k_indices, *_ = backend.topk_indices(flat_score_space, k)
|
325
485
|
|
326
486
|
coordinates = backend.unravel_index(top_k_indices, score_space.shape)
|
@@ -333,12 +493,12 @@ class PeakCallerSort(PeakCaller):
|
|
333
493
|
class PeakCallerMaximumFilter(PeakCaller):
|
334
494
|
"""
|
335
495
|
Find local maxima by applying a maximum filter and enforcing a distance
|
336
|
-
constraint
|
496
|
+
constraint subsequently. This is similar to the strategy implemented in
|
337
497
|
skimage.feature.peak_local_max.
|
338
498
|
"""
|
339
499
|
|
340
500
|
def call_peaks(
|
341
|
-
self, score_space: NDArray,
|
501
|
+
self, score_space: NDArray, minimum_score: float = None, **kwargs
|
342
502
|
) -> Tuple[NDArray, NDArray]:
|
343
503
|
"""
|
344
504
|
Call peaks in the score space.
|
@@ -348,25 +508,25 @@ class PeakCallerMaximumFilter(PeakCaller):
|
|
348
508
|
score_space : NDArray
|
349
509
|
Data array of scores.
|
350
510
|
minimum_score : float
|
351
|
-
Minimum score value to consider.
|
352
|
-
|
353
|
-
Minimum distance between maxima.
|
511
|
+
Minimum score value to consider. If provided, superseeds limit given
|
512
|
+
by :py:attr:`PeakCaller.number_of_peaks`.
|
354
513
|
|
355
514
|
Returns
|
356
515
|
-------
|
357
|
-
NDArray
|
358
|
-
Array of peak
|
359
|
-
NDArray
|
360
|
-
Array of peak details.
|
516
|
+
Tuple[NDArray, NDArray]
|
517
|
+
Array of peak coordinates and peak details.
|
361
518
|
"""
|
362
519
|
peaks = backend.max_filter_coordinates(score_space, self.min_distance)
|
363
520
|
|
521
|
+
scores = score_space[tuple(peaks.T)]
|
522
|
+
|
364
523
|
input_candidates = min(
|
365
524
|
self.number_of_peaks, peaks.shape[0] - 1, backend.size(score_space) - 1
|
366
525
|
)
|
367
|
-
|
368
|
-
|
369
|
-
|
526
|
+
if minimum_score is not None:
|
527
|
+
input_candidates = backend.sum(scores >= minimum_score)
|
528
|
+
|
529
|
+
top_indices = backend.topk_indices(scores, input_candidates)
|
370
530
|
peaks = peaks[top_indices]
|
371
531
|
|
372
532
|
return peaks, None
|
@@ -382,7 +542,7 @@ class PeakCallerFast(PeakCaller):
|
|
382
542
|
"""
|
383
543
|
|
384
544
|
def call_peaks(
|
385
|
-
self, score_space: NDArray,
|
545
|
+
self, score_space: NDArray, minimum_score: float = None, **kwargs
|
386
546
|
) -> Tuple[NDArray, NDArray]:
|
387
547
|
"""
|
388
548
|
Call peaks in the score space.
|
@@ -392,16 +552,13 @@ class PeakCallerFast(PeakCaller):
|
|
392
552
|
score_space : NDArray
|
393
553
|
Data array of scores.
|
394
554
|
minimum_score : float
|
395
|
-
Minimum score value to consider.
|
396
|
-
|
397
|
-
Minimum distance between maxima.
|
555
|
+
Minimum score value to consider. If provided, superseeds limit given
|
556
|
+
by :py:attr:`PeakCaller.number_of_peaks`.
|
398
557
|
|
399
558
|
Returns
|
400
559
|
-------
|
401
|
-
NDArray
|
402
|
-
Array of peak
|
403
|
-
NDArray
|
404
|
-
Array of peak details.
|
560
|
+
Tuple[NDArray, NDArray]
|
561
|
+
Array of peak coordinates and peak details.
|
405
562
|
"""
|
406
563
|
splits = {
|
407
564
|
axis: score_space.shape[axis] // self.min_distance
|
@@ -457,7 +614,14 @@ class PeakCallerRecursiveMasking(PeakCaller):
|
|
457
614
|
"""
|
458
615
|
|
459
616
|
def call_peaks(
|
460
|
-
self,
|
617
|
+
self,
|
618
|
+
score_space: NDArray,
|
619
|
+
rotation_matrix: NDArray,
|
620
|
+
mask: NDArray = None,
|
621
|
+
minimum_score: float = None,
|
622
|
+
rotation_space: NDArray = None,
|
623
|
+
rotation_mapping: Dict = None,
|
624
|
+
**kwargs,
|
461
625
|
) -> Tuple[NDArray, NDArray]:
|
462
626
|
"""
|
463
627
|
Call peaks in the score space.
|
@@ -466,36 +630,195 @@ class PeakCallerRecursiveMasking(PeakCaller):
|
|
466
630
|
----------
|
467
631
|
score_space : NDArray
|
468
632
|
Data array of scores.
|
633
|
+
rotation_matrix : NDArray
|
634
|
+
Rotation matrix.
|
635
|
+
mask : NDArray, optional
|
636
|
+
Mask array, by default None.
|
637
|
+
rotation_space : NDArray, optional
|
638
|
+
Rotation space array, by default None.
|
639
|
+
rotation_mapping : Dict optional
|
640
|
+
Dictionary mapping values in rotation_space to Euler angles.
|
641
|
+
By default None
|
469
642
|
minimum_score : float
|
470
|
-
Minimum score value to consider.
|
471
|
-
|
472
|
-
Minimum distance between maxima.
|
643
|
+
Minimum score value to consider. If provided, superseeds limit given
|
644
|
+
by :py:attr:`PeakCaller.number_of_peaks`.
|
473
645
|
|
474
646
|
Returns
|
475
647
|
-------
|
476
|
-
NDArray
|
477
|
-
Array of peak
|
478
|
-
|
479
|
-
|
648
|
+
Tuple[NDArray, NDArray]
|
649
|
+
Array of peak coordinates and peak details.
|
650
|
+
|
651
|
+
Notes
|
652
|
+
-----
|
653
|
+
By default, scores are masked using a box with edge length self.min_distance.
|
654
|
+
If mask is provided, elements around each peak will be multiplied by the mask
|
655
|
+
values. If rotation_space and rotation_mapping is provided, the respective
|
656
|
+
rotation will be applied to the mask, otherwise rotation_matrix is used.
|
480
657
|
"""
|
481
|
-
|
482
|
-
|
658
|
+
coordinates, masking_function = [], self._mask_scores_rotate
|
659
|
+
|
660
|
+
if mask is None:
|
661
|
+
masking_function = self._mask_scores_box
|
662
|
+
shape = tuple(self.min_distance for _ in range(score_space.ndim))
|
663
|
+
mask = backend.zeros(shape, dtype=backend._default_dtype)
|
664
|
+
|
665
|
+
rotated_template = backend.zeros(mask.shape, dtype=mask.dtype)
|
666
|
+
|
667
|
+
peak_limit = self.number_of_peaks
|
668
|
+
if minimum_score is not None:
|
669
|
+
peak_limit = backend.size(score_space)
|
670
|
+
else:
|
671
|
+
minimum_score = backend.min(score_space) - 1
|
672
|
+
|
483
673
|
while True:
|
484
674
|
backend.argmax(score_space)
|
485
|
-
|
675
|
+
peak = backend.unravel_index(
|
486
676
|
indices=backend.argmax(score_space), shape=score_space.shape
|
487
677
|
)
|
488
|
-
|
489
|
-
start = backend.maximum(backend.subtract(max_coord, score_box), 0)
|
490
|
-
stop = backend.minimum(backend.add(max_coord, score_box), score_space.shape)
|
491
|
-
start, stop = backend.astype(start, int), backend.astype(stop, int)
|
492
|
-
coords = tuple(slice(*pos) for pos in zip(start, stop))
|
493
|
-
score_space[coords] = 0
|
494
|
-
if len(coordinates) >= self.number_of_peaks:
|
678
|
+
if score_space[tuple(peak)] < minimum_score:
|
495
679
|
break
|
680
|
+
|
681
|
+
coordinates.append(peak)
|
682
|
+
|
683
|
+
current_rotation_matrix = self._get_rotation_matrix(
|
684
|
+
peak=peak,
|
685
|
+
rotation_space=rotation_space,
|
686
|
+
rotation_mapping=rotation_mapping,
|
687
|
+
rotation_matrix=rotation_matrix,
|
688
|
+
)
|
689
|
+
|
690
|
+
masking_function(
|
691
|
+
score_space=score_space,
|
692
|
+
rotation_matrix=current_rotation_matrix,
|
693
|
+
peak=peak,
|
694
|
+
mask=mask,
|
695
|
+
rotated_template=rotated_template,
|
696
|
+
)
|
697
|
+
|
698
|
+
if len(coordinates) >= peak_limit:
|
699
|
+
break
|
700
|
+
|
496
701
|
peaks = backend.to_backend_array(coordinates)
|
497
702
|
return peaks, None
|
498
703
|
|
704
|
+
@staticmethod
|
705
|
+
def _get_rotation_matrix(
|
706
|
+
peak: NDArray,
|
707
|
+
rotation_space: NDArray,
|
708
|
+
rotation_mapping: NDArray,
|
709
|
+
rotation_matrix: NDArray,
|
710
|
+
) -> NDArray:
|
711
|
+
"""
|
712
|
+
Get rotation matrix based on peak and rotation data.
|
713
|
+
|
714
|
+
Parameters
|
715
|
+
----------
|
716
|
+
peak : NDArray
|
717
|
+
Peak coordinates.
|
718
|
+
rotation_space : NDArray
|
719
|
+
Rotation space array.
|
720
|
+
rotation_mapping : Dict
|
721
|
+
Dictionary mapping values in rotation_space to Euler angles.
|
722
|
+
rotation_matrix : NDArray
|
723
|
+
Current rotation matrix.
|
724
|
+
|
725
|
+
Returns
|
726
|
+
-------
|
727
|
+
NDArray
|
728
|
+
Rotation matrix.
|
729
|
+
"""
|
730
|
+
if rotation_space is None or rotation_mapping is None:
|
731
|
+
return rotation_matrix
|
732
|
+
|
733
|
+
rotation = rotation_mapping[rotation_space[tuple(peak)]]
|
734
|
+
|
735
|
+
rotation_matrix = backend.to_backend_array(
|
736
|
+
euler_to_rotationmatrix(backend.to_numpy_array(rotation))
|
737
|
+
)
|
738
|
+
return rotation_matrix
|
739
|
+
|
740
|
+
@staticmethod
|
741
|
+
def _mask_scores_box(
|
742
|
+
score_space: NDArray, peak: NDArray, mask: NDArray, **kwargs: Dict
|
743
|
+
) -> None:
|
744
|
+
"""
|
745
|
+
Mask scores in a box around a peak.
|
746
|
+
|
747
|
+
Parameters
|
748
|
+
----------
|
749
|
+
score_space : NDArray
|
750
|
+
Data array of scores.
|
751
|
+
peak : NDArray
|
752
|
+
Peak coordinates.
|
753
|
+
mask : NDArray
|
754
|
+
Mask array.
|
755
|
+
"""
|
756
|
+
start = backend.maximum(backend.subtract(peak, mask.shape), 0)
|
757
|
+
stop = backend.minimum(backend.add(peak, mask.shape), score_space.shape)
|
758
|
+
start, stop = backend.astype(start, int), backend.astype(stop, int)
|
759
|
+
coords = tuple(slice(*pos) for pos in zip(start, stop))
|
760
|
+
score_space[coords] = 0
|
761
|
+
return None
|
762
|
+
|
763
|
+
@staticmethod
|
764
|
+
def _mask_scores_rotate(
|
765
|
+
score_space: NDArray,
|
766
|
+
peak: NDArray,
|
767
|
+
mask: NDArray,
|
768
|
+
rotated_template: NDArray,
|
769
|
+
rotation_matrix: NDArray,
|
770
|
+
**kwargs: Dict,
|
771
|
+
) -> None:
|
772
|
+
"""
|
773
|
+
Mask score_space using mask rotation around a peak.
|
774
|
+
|
775
|
+
Parameters
|
776
|
+
----------
|
777
|
+
score_space : NDArray
|
778
|
+
Data array of scores.
|
779
|
+
peak : NDArray
|
780
|
+
Peak coordinates.
|
781
|
+
mask : NDArray
|
782
|
+
Mask array.
|
783
|
+
rotated_template : NDArray
|
784
|
+
Empty array to write mask rotations to.
|
785
|
+
rotation_matrix : NDArray
|
786
|
+
Rotation matrix.
|
787
|
+
"""
|
788
|
+
left_pad = backend.divide(mask.shape, 2).astype(int)
|
789
|
+
right_pad = backend.add(left_pad, backend.mod(mask.shape, 2).astype(int))
|
790
|
+
|
791
|
+
score_start = backend.subtract(peak, left_pad)
|
792
|
+
score_stop = backend.add(peak, right_pad)
|
793
|
+
|
794
|
+
template_start = backend.subtract(backend.maximum(score_start, 0), score_start)
|
795
|
+
template_stop = backend.subtract(
|
796
|
+
score_stop, backend.minimum(score_stop, score_space.shape)
|
797
|
+
)
|
798
|
+
template_stop = backend.subtract(mask.shape, template_stop)
|
799
|
+
|
800
|
+
score_start = backend.maximum(score_start, 0)
|
801
|
+
score_stop = backend.minimum(score_stop, score_space.shape)
|
802
|
+
score_start = backend.astype(score_start, int)
|
803
|
+
score_stop = backend.astype(score_stop, int)
|
804
|
+
|
805
|
+
template_start = backend.astype(template_start, int)
|
806
|
+
template_stop = backend.astype(template_stop, int)
|
807
|
+
coords_score = tuple(slice(*pos) for pos in zip(score_start, score_stop))
|
808
|
+
coords_template = tuple(
|
809
|
+
slice(*pos) for pos in zip(template_start, template_stop)
|
810
|
+
)
|
811
|
+
|
812
|
+
rotated_template.fill(0)
|
813
|
+
backend.rotate_array(
|
814
|
+
arr=mask, rotation_matrix=rotation_matrix, order=1, out=rotated_template
|
815
|
+
)
|
816
|
+
|
817
|
+
score_space[coords_score] = backend.multiply(
|
818
|
+
score_space[coords_score], (rotated_template[coords_template] <= 0.1)
|
819
|
+
)
|
820
|
+
return None
|
821
|
+
|
499
822
|
|
500
823
|
class PeakCallerScipy(PeakCaller):
|
501
824
|
"""
|
@@ -503,7 +826,7 @@ class PeakCallerScipy(PeakCaller):
|
|
503
826
|
"""
|
504
827
|
|
505
828
|
def call_peaks(
|
506
|
-
self, score_space: NDArray,
|
829
|
+
self, score_space: NDArray, minimum_score: float = None, **kwargs
|
507
830
|
) -> Tuple[NDArray, NDArray]:
|
508
831
|
"""
|
509
832
|
Call peaks in the score space.
|
@@ -513,21 +836,25 @@ class PeakCallerScipy(PeakCaller):
|
|
513
836
|
score_space : NDArray
|
514
837
|
Data array of scores.
|
515
838
|
minimum_score : float
|
516
|
-
Minimum score value to consider.
|
517
|
-
|
518
|
-
Minimum distance between maxima.
|
839
|
+
Minimum score value to consider. If provided, superseeds limit given
|
840
|
+
by :py:attr:`PeakCaller.number_of_peaks`.
|
519
841
|
|
520
842
|
Returns
|
521
843
|
-------
|
522
|
-
NDArray
|
523
|
-
Array of peak
|
524
|
-
NDArray
|
525
|
-
Array of peak details.
|
844
|
+
Tuple[NDArray, NDArray]
|
845
|
+
Array of peak coordinates and peak details.
|
526
846
|
"""
|
847
|
+
|
848
|
+
score_space = backend.to_numpy_array(score_space)
|
849
|
+
num_peaks = self.number_of_peaks
|
850
|
+
if minimum_score is not None:
|
851
|
+
num_peaks = np.inf
|
852
|
+
|
527
853
|
peaks = peak_local_max(
|
528
854
|
score_space,
|
529
|
-
num_peaks=
|
855
|
+
num_peaks=num_peaks,
|
530
856
|
min_distance=self.min_distance,
|
857
|
+
threshold_abs=minimum_score,
|
531
858
|
)
|
532
859
|
return peaks, None
|
533
860
|
|
@@ -879,8 +1206,68 @@ class MaxScoreOverRotations:
|
|
879
1206
|
|
880
1207
|
self.use_memmap = use_memmap
|
881
1208
|
self.lock = Manager().Lock() if thread_safe else nullcontext()
|
1209
|
+
self.lock_is_nullcontext = isinstance(self.score_space, type(backend.zeros((1))))
|
882
1210
|
self.observed_rotations = Manager().dict() if thread_safe else {}
|
883
1211
|
|
1212
|
+
|
1213
|
+
def _postprocess(self,
|
1214
|
+
fourier_shift,
|
1215
|
+
convolution_mode,
|
1216
|
+
targetshape,
|
1217
|
+
templateshape,
|
1218
|
+
shared_memory_handler=None,
|
1219
|
+
**kwargs
|
1220
|
+
):
|
1221
|
+
internal_scores = backend.sharedarr_to_arr(
|
1222
|
+
shape=self.score_space_shape,
|
1223
|
+
dtype=self.score_space_dtype,
|
1224
|
+
shm=self.score_space,
|
1225
|
+
)
|
1226
|
+
internal_rotations = backend.sharedarr_to_arr(
|
1227
|
+
shape=self.score_space_shape,
|
1228
|
+
dtype=self.rotation_space_dtype,
|
1229
|
+
shm=self.rotations,
|
1230
|
+
)
|
1231
|
+
|
1232
|
+
if fourier_shift is not None:
|
1233
|
+
axis = tuple(i for i in range(len(fourier_shift)))
|
1234
|
+
internal_scores = backend.roll(
|
1235
|
+
internal_scores,
|
1236
|
+
shift=fourier_shift,
|
1237
|
+
axis=axis
|
1238
|
+
)
|
1239
|
+
internal_rotations = backend.roll(
|
1240
|
+
internal_rotations,
|
1241
|
+
shift=fourier_shift,
|
1242
|
+
axis=axis
|
1243
|
+
)
|
1244
|
+
|
1245
|
+
if convolution_mode is not None:
|
1246
|
+
internal_scores = apply_convolution_mode(
|
1247
|
+
internal_scores,
|
1248
|
+
convolution_mode=convolution_mode,
|
1249
|
+
s1=targetshape,
|
1250
|
+
s2=templateshape
|
1251
|
+
)
|
1252
|
+
internal_rotations = apply_convolution_mode(
|
1253
|
+
internal_rotations,
|
1254
|
+
convolution_mode=convolution_mode,
|
1255
|
+
s1=targetshape,
|
1256
|
+
s2=templateshape
|
1257
|
+
)
|
1258
|
+
|
1259
|
+
self.score_space_shape = internal_scores.shape
|
1260
|
+
self.score_space = backend.arr_to_sharedarr(
|
1261
|
+
internal_scores,
|
1262
|
+
shared_memory_handler
|
1263
|
+
)
|
1264
|
+
self.rotations = backend.arr_to_sharedarr(
|
1265
|
+
internal_rotations,
|
1266
|
+
shared_memory_handler
|
1267
|
+
)
|
1268
|
+
return self
|
1269
|
+
|
1270
|
+
|
884
1271
|
def __iter__(self):
|
885
1272
|
internal_scores = backend.sharedarr_to_arr(
|
886
1273
|
shape=self.score_space_shape,
|
@@ -938,11 +1325,21 @@ class MaxScoreOverRotations:
|
|
938
1325
|
**kwargs
|
939
1326
|
Arbitrary keyword arguments.
|
940
1327
|
"""
|
1328
|
+
rotation = backend.tobytes(rotation_matrix)
|
1329
|
+
rotation_index = self.observed_rotations.setdefault(
|
1330
|
+
rotation, len(self.observed_rotations)
|
1331
|
+
)
|
1332
|
+
|
1333
|
+
if self.lock_is_nullcontext:
|
1334
|
+
backend.max_score_over_rotations(
|
1335
|
+
score_space=score_space,
|
1336
|
+
internal_scores=self.score_space,
|
1337
|
+
internal_rotations=self.rotations,
|
1338
|
+
rotation_index=rotation_index,
|
1339
|
+
)
|
1340
|
+
return None
|
1341
|
+
|
941
1342
|
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
1343
|
internal_scores = backend.sharedarr_to_arr(
|
947
1344
|
shape=self.score_space_shape,
|
948
1345
|
dtype=self.score_space_dtype,
|
@@ -953,9 +1350,14 @@ class MaxScoreOverRotations:
|
|
953
1350
|
dtype=self.rotation_space_dtype,
|
954
1351
|
shm=self.rotations,
|
955
1352
|
)
|
956
|
-
|
957
|
-
|
958
|
-
|
1353
|
+
|
1354
|
+
backend.max_score_over_rotations(
|
1355
|
+
score_space=score_space,
|
1356
|
+
internal_scores=internal_scores,
|
1357
|
+
internal_rotations=internal_rotations,
|
1358
|
+
rotation_index=rotation_index,
|
1359
|
+
)
|
1360
|
+
return None
|
959
1361
|
|
960
1362
|
@classmethod
|
961
1363
|
def merge(cls, param_stores=List[Tuple], **kwargs) -> Tuple[NDArray]:
|
@@ -1007,14 +1409,22 @@ class MaxScoreOverRotations:
|
|
1007
1409
|
scores_out = np.memmap(
|
1008
1410
|
scores_out_filename, mode="w+", shape=base_max, dtype=scores_out_dtype
|
1009
1411
|
)
|
1412
|
+
scores_out.fill(kwargs.get("score_threshold", 0))
|
1413
|
+
scores_out.flush()
|
1010
1414
|
rotations_out = np.memmap(
|
1011
1415
|
rotations_out_filename,
|
1012
1416
|
mode="w+",
|
1013
1417
|
shape=base_max,
|
1014
1418
|
dtype=rotations_out_dtype,
|
1015
1419
|
)
|
1420
|
+
rotations_out.fill(-1)
|
1421
|
+
rotations_out.flush()
|
1016
1422
|
else:
|
1017
|
-
scores_out = np.
|
1423
|
+
scores_out = np.full(
|
1424
|
+
base_max,
|
1425
|
+
fill_value=kwargs.get("score_threshold", 0),
|
1426
|
+
dtype=scores_out_dtype,
|
1427
|
+
)
|
1018
1428
|
rotations_out = np.full(base_max, fill_value=-1, dtype=rotations_out_dtype)
|
1019
1429
|
|
1020
1430
|
for i in range(len(param_stores)):
|
@@ -1079,6 +1489,61 @@ class MaxScoreOverRotations:
|
|
1079
1489
|
)
|
1080
1490
|
|
1081
1491
|
|
1492
|
+
class _MaxScoreOverTranslations(MaxScoreOverRotations):
|
1493
|
+
"""
|
1494
|
+
Obtain the maximum translation score over various rotations.
|
1495
|
+
|
1496
|
+
Attributes
|
1497
|
+
----------
|
1498
|
+
score_space : NDArray
|
1499
|
+
The score space for the observed rotations.
|
1500
|
+
rotations : NDArray
|
1501
|
+
The rotation identifiers for each score.
|
1502
|
+
translation_offset : NDArray, optional
|
1503
|
+
The offset applied during translation.
|
1504
|
+
observed_rotations : int
|
1505
|
+
Count of observed rotations.
|
1506
|
+
use_memmap : bool, optional
|
1507
|
+
Whether to offload internal data arrays to disk
|
1508
|
+
thread_safe: bool, optional
|
1509
|
+
Whether access to internal data arrays should be thread safe
|
1510
|
+
"""
|
1511
|
+
|
1512
|
+
def __call__(
|
1513
|
+
self, score_space: NDArray, rotation_matrix: NDArray, **kwargs
|
1514
|
+
) -> None:
|
1515
|
+
"""
|
1516
|
+
Update internal parameter store based on `score_space`.
|
1517
|
+
|
1518
|
+
Parameters
|
1519
|
+
----------
|
1520
|
+
score_space : ndarray
|
1521
|
+
Numpy array containing the score space.
|
1522
|
+
rotation_matrix : ndarray
|
1523
|
+
Square matrix describing the current rotation.
|
1524
|
+
**kwargs
|
1525
|
+
Arbitrary keyword arguments.
|
1526
|
+
"""
|
1527
|
+
from tme.matching_utils import centered_mask
|
1528
|
+
|
1529
|
+
with self.lock:
|
1530
|
+
rotation = backend.tobytes(rotation_matrix)
|
1531
|
+
if rotation not in self.observed_rotations:
|
1532
|
+
self.observed_rotations[rotation] = len(self.observed_rotations)
|
1533
|
+
score_space = centered_mask(score_space, kwargs["template_shape"])
|
1534
|
+
rotation_index = self.observed_rotations[rotation]
|
1535
|
+
internal_scores = backend.sharedarr_to_arr(
|
1536
|
+
shape=self.score_space_shape,
|
1537
|
+
dtype=self.score_space_dtype,
|
1538
|
+
shm=self.score_space,
|
1539
|
+
)
|
1540
|
+
max_score = score_space.max(axis=(1, 2, 3))
|
1541
|
+
mean_score = score_space.mean(axis=(1, 2, 3))
|
1542
|
+
std_score = score_space.std(axis=(1, 2, 3))
|
1543
|
+
z_score = (max_score - mean_score) / std_score
|
1544
|
+
internal_scores[rotation_index] = z_score
|
1545
|
+
|
1546
|
+
|
1082
1547
|
class MemmapHandler:
|
1083
1548
|
"""
|
1084
1549
|
Create numpy memmap objects to write score spaces to.
|
@@ -1182,3 +1647,5 @@ class MemmapHandler:
|
|
1182
1647
|
"""
|
1183
1648
|
rotation_string = "_".join(rotation_matrix.ravel().astype(str))
|
1184
1649
|
return self._path_translation[rotation_string]
|
1650
|
+
|
1651
|
+
|