def test_cross_correlate_masked_output_shape(): """Masked normalized cross-correlation should return a shape of N + M + 1 for each transform axis.""" shape1 = (15, 4, 5) shape2 = (6, 12, 7) expected_full_shape = tuple(np.array(shape1) + np.array(shape2) - 1) expected_same_shape = shape1 arr1 = np.zeros(shape1) arr2 = np.zeros(shape2) # Trivial masks m1 = np.ones_like(arr1) m2 = np.ones_like(arr2) full_xcorr = cross_correlate_masked(arr1, arr2, m1, m2, axes=(0, 1, 2), mode='full') assert_equal(full_xcorr.shape, expected_full_shape) same_xcorr = cross_correlate_masked(arr1, arr2, m1, m2, axes=(0, 1, 2), mode='same') assert_equal(same_xcorr.shape, expected_same_shape)
def test_cross_correlate_masked_over_axes(): """Masked normalized cross-correlation over axes should be equivalent to a loop over non-transform axes.""" # See random number generator for reproducible results np.random.seed(23) arr1 = np.random.random((8, 8, 5)) arr2 = np.random.random((8, 8, 5)) m1 = np.random.choice([True, False], arr1.shape) m2 = np.random.choice([True, False], arr2.shape) # Loop over last axis with_loop = np.empty_like(arr1, dtype=np.complex) for index in range(arr1.shape[-1]): with_loop[:, :, index] = cross_correlate_masked(arr1[:, :, index], arr2[:, :, index], m1[:, :, index], m2[:, :, index], axes=(0, 1), mode='same') over_axes = cross_correlate_masked(arr1, arr2, m1, m2, axes=(0, 1), mode='same') testing.assert_array_almost_equal(with_loop, over_axes)
def test_cross_correlate_masked_test_against_mismatched_dimensions(): """Masked normalized cross-correlation should raise an error if array dimensions along non-transformation axes are mismatched.""" shape1 = (23, 1, 1) shape2 = (6, 2, 2) arr1 = np.zeros(shape1) arr2 = np.zeros(shape2) # Trivial masks m1 = np.ones_like(arr1) m2 = np.ones_like(arr2) with testing.raises(ValueError): cross_correlate_masked(arr1, arr2, m1, m2, axes=(1, 2))
def test_cross_correlate_masked_side_effects(): """Masked normalized cross-correlation should not modify the inputs.""" shape1 = (2, 2, 2) shape2 = (2, 2, 2) arr1 = np.zeros(shape1) arr2 = np.zeros(shape2) # Trivial masks m1 = np.ones_like(arr1) m2 = np.ones_like(arr2) for arr in (arr1, arr2, m1, m2): arr.setflags(write=False) cross_correlate_masked(arr1, arr2, m1, m2)
def test_cross_correlate_masked_output_range(): """Masked normalized cross-correlation should return between 1 and -1.""" # See random number generator for reproducible results np.random.seed(23) # Array dimensions must match along non-transformation axes, in # this case # axis 0 shape1 = (15, 4, 5) shape2 = (15, 12, 7) # Initial array ranges between -5 and 5 arr1 = 10 * np.random.random(shape1) - 5 arr2 = 10 * np.random.random(shape2) - 5 # random masks m1 = np.random.choice([True, False], arr1.shape) m2 = np.random.choice([True, False], arr2.shape) xcorr = cross_correlate_masked(arr1, arr2, m1, m2, axes=(1, 2)) # No assert array less or equal, so we add an eps # Also could not find an `assert_array_greater`, Use (-xcorr) instead eps = np.finfo(np.float).eps testing.assert_array_less(xcorr, 1 + eps) testing.assert_array_less(-xcorr, 1 + eps)
def test_cross_correlate_masked_autocorrelation_trivial_masks(): """Masked normalized cross-correlation between identical arrays should reduce to an autocorrelation even with random masks.""" # See random number generator for reproducible results np.random.seed(23) arr1 = camera() # Random masks with 75% of pixels being valid m1 = np.random.choice([True, False], arr1.shape, p=[3 / 4, 1 / 4]) m2 = np.random.choice([True, False], arr1.shape, p=[3 / 4, 1 / 4]) xcorr = cross_correlate_masked(arr1, arr1, m1, m2, axes=(0, 1), mode='same', overlap_ratio=0).real max_index = np.unravel_index(np.argmax(xcorr), xcorr.shape) # Autocorrelation should have maximum in center of array # uint8 inputs will be processed in float32, so reduce decimal to 5 assert_almost_equal(xcorr.max(), 1, decimal=5) assert_array_equal(max_index, np.array(arr1.shape) / 2)
def calculate_offsets(previous, current, mask, dt): """Calculate offsets between two frames""" cross_corr = cross_correlate_masked(current, previous, mask, mask, axes=(0, 1), overlap_ratio=OVERLAP_RATIO) maxima = np.stack(np.nonzero(cross_corr == cross_corr.max()), axis=1) center = np.mean(maxima, axis=0) shift = center - np.array(previous.shape) + 1 dy, dx = shift * PLATESCALE / dt dr = np.hypot(dx, dy) theta = np.mod(np.rad2deg(np.arctan2(dy, dx)), 360) offsets = np.array([dx, dy, dr, theta]) xy_err = 0.5 * PLATESCALE / dt r_err = np.sqrt(2) * xy_err theta_err = np.rad2deg(xy_err / r_err) errors = np.array([xy_err, xy_err, r_err, theta_err]) return np.real(cross_corr), offsets, errors
# gauss_highpass = coadded_cube - lowpass # Y = np.median(coadded_cube, axis=0) coadded_cube #-= Y logger.debug(f"size after coadding: {coadded_cube.shape}") mask = get_mask() correllelagrams = [] shifts = [] for i in tqdm.trange(coadded_cube.shape[0] - 1): previous, current = coadded_cube[i], coadded_cube[i + 1] cross_corr = cross_correlate_masked(current, previous, mask, mask, axes=(0, 1), overlap_ratio=OVERLAP_RATIO) correllelagrams.append(cross_corr) maxima = np.stack(np.nonzero(cross_corr == cross_corr.max()), axis=1) center = np.mean(maxima, axis=0) shift = center - np.array(previous.shape) + 1 dy, dx = -shift dr = np.sqrt(dx**2 + dy**2) dt = np.arctan2(dy, dx) logger.debug(f"dx: {dx:.01f} px\tdy: {dy:.01f} px\tdr: {dr:.01f} px") speedscale = PLATESCALE * args.rate / args.N logger.debug( f"{dx * speedscale:.01f} m/s\t{dy * speedscale:.01f} m/s\t{dr * speedscale:.01f} m/s"
def bragg_peaks(im, mask=None, center=None, min_dist=None): """ Extract the position of Bragg peaks in a single-crystal diffraction pattern. .. versionadded:: 2.1.3 Parameters ---------- im : ndarray, shape (N,M) Single-crystal diffraction pattern. mask : ndarray, shape (N,M), dtype bool, optional Mask that evaluates to `True` on valid pixels of ``im``. center : 2-tuple, optional Center of the diffraction pattern, in ``(row, col)`` format. If ``None``, the center will be determined via :func:`autocenter`. min_dist : float or None, optional Minimum distance between Bragg peaks (in pixel coordinates). Peaks that are closer than this distance will be considered the same peak, and only one of them will be returned. If `None` (default), the minimum distance is guessed based on the image size. Returns ------- peaks : list of 2-tuples List of coordinates ``[row, col]`` for every detected peak, sorted in order of how close they are to the center of the image. References ---------- Liu, Lai Chung. Chemistry in Action: Making Molecular Movies with Ultrafast Electron Diffraction and Data Science, Chapter 2. Springer Nature, 2020. """ if center is None: center = autocenter(im=im, mask=mask) im = np.array(im, copy=True, dtype=float) im -= im.min() with catch_warnings(): simplefilter("ignore", category=RuntimeWarning) im /= gaussian_filter(input=im, sigma=min(im.shape) / 20, truncate=2) im = np.nan_to_num(im, copy=False) autocorr = np.abs( cross_correlate_masked(arr1=im, arr2=im, m1=mask, m2=mask, mode="same")) # The regions of interest are defined on the labels made # from the autocorrelation of the image. The center of the autocorr # is the center of the array; we need to correct the offset. # This also allows to use the mask on properties derived # from the autocorr autocorr = shift( autocorr, shift=np.asarray(center) - np.array(im.shape) / 2, order=1, mode="nearest", ) laplacian = -1 * laplace(autocorr) regions = (laplacian > 0.02) * mask # To prevent noise from looking like actual peaks, # we erode labels using a small selection area regions = binary_erosion(regions, selem=disk(2)) labels = label(regions, return_num=False) props = regionprops(label_image=labels, intensity_image=im) candidates = [ prop for prop in props if not np.any(np.isnan(prop.weighted_centroid)) ] # Some regions are very close to each other; we prune them! if min_dist is None: min_dist = min(im.shape) / 100 peaks = list() for prop in candidates: pos = np.asarray(prop.weighted_centroid) if any((np.linalg.norm(peak - pos) < min_dist) for peak in peaks): continue else: peaks.append(pos) return sorted(peaks, key=lambda p: np.linalg.norm(p - center))