@@ -259,14 +259,19 @@ def _get_shift_equations_approx(self):
259259
260260 n_theta_half = self .n_theta // 2
261261 n_img = self .n_img
262+ pf = self .pf .copy ()
262263
263264 # `estimate_shifts()` requires that rotations have already been estimated.
264- rotations = Rotation ( self .rotations )
265+ rotations = self .rotations
265266
266- pf = self .pf .copy ()
267+ # Apply symmetry group to rotations
268+ sym_rots = self .src .symmetry_group .matrices .astype (self .dtype , copy = False )
269+ n_sym = len (sym_rots )
267270
268- # Estimate number of equations that will be used to calculate the shifts
269- n_equations = self ._estimate_num_shift_equations (n_img )
271+ # Estimate number of equations that will be used to calculate the shifts,
272+ # taking into account particle symmetry.
273+ n_pair_equations = self ._estimate_num_shift_equations (n_img )
274+ n_equations = n_pair_equations * n_sym
270275
271276 # Allocate local variables for estimating 2D shifts based on the estimated number
272277 # of equations. The shift equations are represented using a sparse matrix,
@@ -291,86 +296,94 @@ def _get_shift_equations_approx(self):
291296 d_theta = np .pi / n_theta_half
292297
293298 # Generate two index lists for [i, j] pairs of images
294- idx_i , idx_j = self ._generate_index_pairs (n_equations )
299+ idx_i , idx_j = self ._generate_index_pairs (n_pair_equations )
295300
296301 # Go through all shift equations in the size of n_equations
297302 # Iterate over the common lines pairs and for each pair find the 1D
298303 # relative shift between the two Fourier lines in the pair.
299- for shift_eq_idx in range (n_equations ):
300- i = idx_i [shift_eq_idx ]
301- j = idx_j [shift_eq_idx ]
302- # get the common line indices based on the rotations from i and j images
303- c_ij , c_ji = self ._get_cl_indices (rotations , i , j , n_theta_half )
304-
305- # Extract the Fourier rays that correspond to the common line
306- pf_i = pf [i , c_ij ]
307-
308- # Check whether need to flip or not Fourier ray of j image
309- # Is the common line in image j in the positive
310- # direction of the ray (is_pf_j_flipped=False) or in the
311- # negative direction (is_pf_j_flipped=True).
312- is_pf_j_flipped = c_ji >= n_theta_half
313- if not is_pf_j_flipped :
314- pf_j = pf [j , c_ji ]
315- else :
316- pf_j = pf [j , c_ji - n_theta_half ]
317-
318- # Use ray from opposite side of origin.
319- # Correpsonds to `freqs` convention in PFT,
320- # where the legacy code used a negated frequency grid.
321- pf_i , pf_j = np .conj (pf_i ), np .conj (pf_j )
322-
323- # perform bandpass filter, normalize each ray of each image,
324- pf_i = self ._apply_filter_and_norm ("i, i -> i" , pf_i , r_max , h )
325- pf_j = self ._apply_filter_and_norm ("i, i -> i" , pf_j , r_max , h )
326-
327- # apply the shifts to images
328- pf_i_flipped = np .conj (pf_i )
329- pf_i_stack = pf_i [:, None ] * shift_phases .T
330- pf_i_flipped_stack = pf_i_flipped [:, None ] * shift_phases .T
331-
332- c1 = 2 * np .dot (pf_i_stack .T .conj (), pf_j ).real
333- c2 = 2 * np .dot (pf_i_flipped_stack .T .conj (), pf_j ).real
334-
335- # find the indices for the maximum values
336- # and apply corresponding shifts
337- sidx1 = np .argmax (c1 )
338- sidx2 = np .argmax (c2 )
339- sidx = sidx1 if c1 [sidx1 ] > c2 [sidx2 ] else sidx2
340- dx = - self .offsets_max_shift + sidx * self .offsets_shift_step
341-
342- # angle of common ray in image i
343- shift_alpha = c_ij * d_theta
344- # Angle of common ray in image j.
345- shift_beta = c_ji * d_theta
346- # Row index to construct the sparse equations
347- shift_i [shift_eq_idx ] = shift_eq_idx
348- # Columns of the shift variables that correspond to the current pair [i, j]
349- shift_j [shift_eq_idx ] = [2 * i , 2 * i + 1 , 2 * j , 2 * j + 1 ]
350- # Right hand side of the current equation
351- shift_b [shift_eq_idx ] = dx
352-
353- # Compute the coefficients of the current equation
354- if not is_pf_j_flipped :
355- shift_eq [shift_eq_idx ] = np .array (
356- [
357- np .sin (shift_alpha ),
358- np .cos (shift_alpha ),
359- - np .sin (shift_beta ),
360- - np .cos (shift_beta ),
361- ]
362- )
363- else :
364- shift_beta = shift_beta - np .pi
365- shift_eq [shift_eq_idx ] = np .array (
366- [
367- - np .sin (shift_alpha ),
368- - np .cos (shift_alpha ),
369- - np .sin (shift_beta ),
370- - np .cos (shift_beta ),
371- ]
304+ for pair_eq_idx in range (n_pair_equations ):
305+ i = idx_i [pair_eq_idx ]
306+ j = idx_j [pair_eq_idx ]
307+
308+ for sym_idx , g in enumerate (sym_rots ):
309+ shift_eq_idx = pair_eq_idx + sym_idx * n_pair_equations
310+
311+ # get the common line indices based on the rotations from i and j images
312+ c_ij , c_ji = self ._get_cl_indices_from_rot_pair (
313+ rotations [i ],
314+ g @ rotations [j ],
315+ n_theta_half ,
372316 )
373317
318+ # Extract the Fourier rays that correspond to the common line
319+ pf_i = pf [i , c_ij ]
320+
321+ # Check whether need to flip or not Fourier ray of j image
322+ # Is the common line in image j in the positive
323+ # direction of the ray (is_pf_j_flipped=False) or in the
324+ # negative direction (is_pf_j_flipped=True).
325+ is_pf_j_flipped = c_ji >= n_theta_half
326+ if not is_pf_j_flipped :
327+ pf_j = pf [j , c_ji ]
328+ else :
329+ pf_j = pf [j , c_ji - n_theta_half ]
330+
331+ # Use ray from opposite side of origin.
332+ # Correpsonds to `freqs` convention in PFT,
333+ # where the legacy code used a negated frequency grid.
334+ pf_i , pf_j = np .conj (pf_i ), np .conj (pf_j )
335+
336+ # perform bandpass filter, normalize each ray of each image,
337+ pf_i = self ._apply_filter_and_norm ("i, i -> i" , pf_i , r_max , h )
338+ pf_j = self ._apply_filter_and_norm ("i, i -> i" , pf_j , r_max , h )
339+
340+ # apply the shifts to images
341+ pf_i_flipped = np .conj (pf_i )
342+ pf_i_stack = pf_i [:, None ] * shift_phases .T
343+ pf_i_flipped_stack = pf_i_flipped [:, None ] * shift_phases .T
344+
345+ c1 = 2 * np .dot (pf_i_stack .T .conj (), pf_j ).real
346+ c2 = 2 * np .dot (pf_i_flipped_stack .T .conj (), pf_j ).real
347+
348+ # find the indices for the maximum values
349+ # and apply corresponding shifts
350+ sidx1 = np .argmax (c1 )
351+ sidx2 = np .argmax (c2 )
352+ sidx = sidx1 if c1 [sidx1 ] > c2 [sidx2 ] else sidx2
353+ dx = - self .offsets_max_shift + sidx * self .offsets_shift_step
354+
355+ # angle of common ray in image i
356+ shift_alpha = c_ij * d_theta
357+ # Angle of common ray in image j.
358+ shift_beta = c_ji * d_theta
359+ # Row index to construct the sparse equations
360+ shift_i [shift_eq_idx ] = shift_eq_idx
361+ # Columns of the shift variables that correspond to the current pair [i, j]
362+ shift_j [shift_eq_idx ] = [2 * i , 2 * i + 1 , 2 * j , 2 * j + 1 ]
363+ # Right hand side of the current equation
364+ shift_b [shift_eq_idx ] = dx
365+
366+ # Compute the coefficients of the current equation
367+ if not is_pf_j_flipped :
368+ shift_eq [shift_eq_idx ] = np .array (
369+ [
370+ np .sin (shift_alpha ),
371+ np .cos (shift_alpha ),
372+ - np .sin (shift_beta ),
373+ - np .cos (shift_beta ),
374+ ]
375+ )
376+ else :
377+ shift_beta = shift_beta - np .pi
378+ shift_eq [shift_eq_idx ] = np .array (
379+ [
380+ - np .sin (shift_alpha ),
381+ - np .cos (shift_alpha ),
382+ - np .sin (shift_beta ),
383+ - np .cos (shift_beta ),
384+ ]
385+ )
386+
374387 # create sparse matrix object only containing non-zero elements
375388 shift_equations = sparse .csr_matrix (
376389 (shift_eq .flatten (), (shift_i .flatten (), shift_j .flatten ())),
@@ -458,6 +471,21 @@ def _get_cl_indices(self, rotations, i, j, n_theta):
458471
459472 return c_ij , c_ji
460473
474+ def _get_cl_indices_from_rot_pair (self , Ri , Rj , n_theta ):
475+ """
476+ Get common-line indices for an explicit pair of rotation matrices.
477+ """
478+ rotations = Rotation (np .stack ((Ri , Rj ))).invert ()
479+ c_ij , c_ji = rotations .common_lines (0 , 1 , 2 * n_theta )
480+
481+ if c_ij >= n_theta :
482+ c_ij -= n_theta
483+ c_ji -= n_theta
484+ if c_ji < 0 :
485+ c_ji += 2 * n_theta
486+
487+ return c_ij , c_ji
488+
461489 def _apply_filter_and_norm (self , subscripts , pf , r_max , h ):
462490 """
463491 Apply common line filter and normalize each ray
0 commit comments