def __init__(self, ksp, calib_width=24, thresh=0.01, kernel_width=6, crop=0.8, max_iter=100, device=sp.cpu_device, output_eigenvalue=False, show_pbar=True): self.device = sp.Device(device) self.output_eigenvalue = output_eigenvalue self.crop = crop img_ndim = ksp.ndim - 1 num_coils = len(ksp) with sp.get_device(ksp): # Get calibration region calib_shape = [num_coils] + [calib_width] * img_ndim calib = sp.resize(ksp, calib_shape) calib = sp.to_device(calib, device) xp = self.device.xp with self.device: # Get calibration matrix kernel_shape = [num_coils] + [kernel_width] * img_ndim kernel_strides = [1] * (img_ndim + 1) mat = sp.array_to_blocks(calib, kernel_shape, kernel_strides) mat = mat.reshape([-1, sp.prod(kernel_shape)]) # Perform SVD on calibration matrix _, S, VH = xp.linalg.svd(mat, full_matrices=False) VH = VH[S > thresh * S.max(), :] # Get kernels num_kernels = len(VH) kernels = VH.reshape([num_kernels] + kernel_shape) img_shape = ksp.shape[1:] # Get covariance matrix in image domain AHA = xp.zeros(img_shape[::-1] + (num_coils, num_coils), dtype=ksp.dtype) for kernel in kernels: img_kernel = sp.ifft(sp.resize(kernel, ksp.shape), axes=range(-img_ndim, 0)) aH = xp.expand_dims(img_kernel.T, axis=-1) a = xp.conj(aH.swapaxes(-1, -2)) AHA += aH @ a AHA *= (sp.prod(img_shape) / kernel_width**img_ndim) self.mps = xp.ones(ksp.shape[::-1] + (1, ), dtype=ksp.dtype) alg = sp.alg.PowerMethod( lambda x: AHA @ x, self.mps, norm_func=lambda x: xp.sum( xp.abs(x)**2, axis=-2, keepdims=True)**0.5, max_iter=max_iter) super().__init__(alg, show_pbar=show_pbar)
def image_undersampled_recon(image, accel_factor=1.5, eps=1e-6, recon_type='l1-wavelet', trajectory=poisson_trajectory, *args): """ This function undersamples the input image in k-space and performs a reconstruction with the undersampled data. Inputs: :param image: the image to be undersampled and reconstruced (numpy array complex128) :param accel_factor: the acceleration factor (float) :param eps: precision parameter for constrained reconstruction (float) :param recon_type: specify the reconstruction type. Accepts: 'l1-wavelet' for L1-wavelet constrained reconstruction (default), 'zero-fill' for a zero-filled image reconstruction :param trajectory: the trajectory function to use for generating the kspace mask (function) :param args: extraneous arguments to pass to the trajectory function call (extraneous arguments) :return: reconstructed_image: (numpy array complex128) """ # Convert image to k-space kspace = sigpy.fft(image, center=True, norm='ortho') # Generate trajectory mask from kspace size and accel_factor, as well as extraneous arguments. trajectory_mask = trajectory(kspace.shape, accel_factor, *args) # Generate undersampled k-space data undersampled_kspace = kspace * trajectory_mask if recon_type.lower() == 'zero-fill'.lower(): # Reconstruct zero-filled image zero_filled_img = sigpy.ifft(undersampled_kspace, center=True, norm='ortho') return zero_filled_img if recon_type.lower() == 'l1-wavelet'.lower(): # Convert poisson mask into boolean array for indexing mask = np.bool8(trajectory_mask) # Build coordinates matrix using poisson mask x = np.linspace(-image.shape[0] / 2, image.shape[0] / 2 - 1, image.shape[0]) y = np.linspace(-image.shape[1] / 2, image.shape[1] / 2 - 1, image.shape[1]) X, Y = np.meshgrid(x, y) x_idx = Y[ mask] # Dimensions needs to be swapped because of row-major indexing y_idx = X[mask] coords = np.concatenate([x_idx[:, np.newaxis], y_idx[:, np.newaxis]], axis=1) mask = np.bool8(trajectory_mask) L1ConstrainedWaveletApp = sigpy.mri.app.L1WaveletConstrainedRecon( y=kspace[mask], mps=np.ones((1, image.shape[0], image.shape[1])), coord=coords, eps=eps) L1out = L1ConstrainedWaveletApp.run() return L1out
def b2rf(b, cancel_alpha_phs=False): a = b2a(b) if cancel_alpha_phs: b_a_phase = sp.fft(b, center=False, norm=None) * \ np.exp(-1j * np.angle(sp.fft(a[np.size(a)::-1], center=False, norm=None))) b = sp.ifft(b_a_phase, center=False, norm=None) rf = ab2rf(a, b) return rf
def fmp(h): l = np.size(h) lp = 128 * np.exp(np.ceil(np.log(l) / np.log(2)) * np.log(2)) padwidths = np.array([np.ceil((lp - l) / 2), np.floor((lp - l) / 2)]) hp = np.pad(h, padwidths.astype(int), 'constant') hpf = sp.fft(hp, norm=None) hpfs = hpf - np.min(np.real(hpf)) * 1.000001 hpfmp = mag2mp(np.sqrt(np.abs(hpfs))) hpmp = sp.ifft(np.fft.ifftshift(np.conj(hpfmp)), center=False, norm=None) hmp = hpmp[:int((l + 1) / 2)] return hmp
def mag2mp(x): n = np.size(x) xl = np.log(np.abs(x)) # Log of mag spectrum xlf = sp.fft(xl, center=False, norm=None) xlfp = xlf xlfp[0] = xlf[0] # Keep DC the same xlfp[1:(n // 2):1] = 2 * xlf[1:(n // 2):1] # Double positive frequencies xlfp[n // 2] = xlf[n // 2] # keep half Nyquist the same xlfp[n // 2 + 1:n:1] = 0 # zero negative frequencies xlaf = sp.ifft(xlfp, center=False, norm=None) a = np.exp(xlaf) # complex exponentiation return a
def test_sense_model_with_comm(self): img_shape = [16, 16] mps_shape = [8, 16, 16] comm = sp.Communicator() img = sp.randn(img_shape, dtype=np.complex) mps = sp.randn(mps_shape, dtype=np.complex) comm.allreduce(img) comm.allreduce(mps) ksp = sp.fft(img * mps, axes=[-1, -2]) A = linop.Sense(mps[comm.rank::comm.size], comm=comm) npt.assert_allclose(A.H(ksp[comm.rank::comm.size]), np.sum( sp.ifft(ksp, axes=[-1, -2]) * mps.conjugate(), 0))
def _output(self): xp = self.device.xp # Coil by coil to save memory with self.device: mps_rss = 0 mps = np.empty([self.num_coils] + self.img_shape, dtype=self.dtype) for c in range(self.num_coils): mps_c = sp.ifft(sp.resize(self.mps_ker[c], self.img_shape)) mps_rss += xp.abs(mps_c)**2 sp.copyto(mps[c], mps_c) mps_rss = sp.to_device(mps_rss**0.5, sp.cpu_device) mps /= mps_rss return mps
def _output(self): xp = self.device.xp # Normalize by root-sum-of-squares. with self.device: rss = 0 mps = np.empty([self.num_coils] + self.img_shape, dtype=self.dtype) for c in range(self.num_coils): mps_c = sp.ifft(sp.resize(self.mps_ker[c], self.img_shape)) rss += xp.abs(mps_c)**2 sp.copyto(mps[c], mps_c) rss = sp.to_device(rss) if self.comm is not None: self.comm.allreduce(rss) rss = rss**0.5 mps /= rss return mps
def dzls(n=64, tb=4, d1=0.01, d2=0.01): di = dinf(d1, d2) w = di / tb f = np.asarray([0, (1 - w) * (tb / 2), (1 + w) * (tb / 2), (n / 2)]) f = f / (n / 2) m = [1, 1, 0, 0] w = [1, d1 / d2] h = signal.firls(n + 1, f, m, w) # shift the filter half a sample to make it symmetric, like in MATLAB c = np.exp( 1j * 2 * np.pi / (2 * (n + 1)) * np.concatenate([np.arange(0, n / 2 + 1, 1), np.arange(-n / 2, 0, 1)])) h = np.real(sp.ifft(np.multiply(sp.fft(h, center=False), c), center=False)) # lop off extra sample h = h[:n] return h
def time_ifft(self): y = sp.ifft(self.x)
def prepare_knee_data(ismrmrd_path): """Convert ISMRMRD file to slices along readout. Args: ismrmrd_path (pathlib.Path): file path to ISMRMRD file. """ logging.info('Processing {}'.format(ismrmrd_path.stem)) dset = ismrmrd.Dataset(str(ismrmrd_path)) hdr = ismrmrd.xsd.CreateFromDocument(dset.read_xml_header()) matrix_size_x = hdr.encoding[0].encodedSpace.matrixSize.x matrix_size_y = hdr.encoding[0].encodedSpace.matrixSize.y number_of_slices = hdr.encoding[0].encodingLimits.slice.maximum + 1 number_of_channels = hdr.acquisitionSystemInformation.receiverChannels ksp = np.zeros( [number_of_channels, number_of_slices, matrix_size_y, matrix_size_x], dtype=np.complex64) for acqnum in range(dset.number_of_acquisitions()): acq = dset.read_acquisition(acqnum) y = acq.idx.kspace_encode_step_1 ksp[:, acq.idx.slice, y, :] = acq.data ksp = np.fft.fft(np.fft.ifftshift(ksp, axes=-3), axis=-3) ksp_lr_bf = sp.resize(ksp, [ number_of_channels, number_of_slices // 2, matrix_size_y // 2, matrix_size_x // 2 ]) ksp_lr = sp.resize(ksp_lr_bf, ksp.shape) del ksp #ksp_lr = sp.resize(sp.resize(ksp, [number_of_channels, # number_of_slices // 2, # matrix_size_y // 2, # matrix_size_x]), # ksp.shape) #img = np.sum(np.abs(sp.ifft(ksp, axes=[-1, -2, -3]))**2, axis=0)**0.5 img_lr_bf = np.sum(np.abs(sp.ifft(ksp_lr_bf, axes=[-1, -2, -3]))**2, axis=0)**0.5 #np.save("./stefan_data/img_lr_bf.npy", img_lr_bf) #quit() img_lr = np.sum(np.abs(sp.ifft(ksp_lr, axes=[-1, -2, -3]))**2, axis=0)**0.5 smallMatrixX = matrix_size_x // 2 scale = 1 / img_lr.max() scale2 = 1 / img_lr_bf.max() for i in range(matrix_size_x): logging.info('Processing {}_{:03d}'.format(ismrmrd_path.stem, i)) #img_i_path = ismrmrd_path.parents[1] / 'img' / '{}_{:03d}'.format(ismrmrd_path.stem, i) #img_lr_i_path = ismrmrd_path.parents[1] / 'img_lr' / '{}_{:03d}'.format(ismrmrd_path.stem, i) img_lr_i_path = ismrmrd_path.parents[ 1] / 'img_lr2' / '{}_{:03d}'.format(ismrmrd_path.stem, i) if i < smallMatrixX: img_lr_bf_i_path = ismrmrd_path.parents[ 1] / 'img_lr2_bf' / '{}_{:03d}'.format(ismrmrd_path.stem, i) #img_i = img[..., i] img_lr_i = img_lr[..., i] if i < smallMatrixX: img_lr_bf_i = img_lr_bf[..., i] #np.save(str(img_i_path), img_i * scale) np.save(str(img_lr_i_path), img_lr_i * scale) if i < smallMatrixX: np.save(str(img_lr_bf_i_path), img_lr_bf_i * scale2)
def dz_hadamard_b(n=128, g=5, gind=1, tb=4, d1=0.01, d2=0.01, shift=32): r"""Design a pulse with hadamard encoding Args: n (int): number of time points. g (int): order of the Hadamard matrix. gind (int): index of vector to use from Hadamard matrix for encoding. tb (int): time bandwidth product. d1 (float): passband ripple level in :math:'M_0^{-1}'. d2 (float): stopband ripple level in :math:'M_0^{-1}'. shift (int): n time points shift of pulse. Returns: b (array): SLR beta parameter. References: Souza, S.P., Szumowski, J., Dumoulin, C.L., Plewes, D.P. & Glover, G. 'Sima: Simultaneous multislice acquisition of MR images by hadamard - encoded excitation. J.Comput.Assist.Tomogr. 12, 1026–1030(1988). """ H = linalg.hadamard(g) encode = H[gind - 1, :] ftw = dinf(d1, d2) / tb # fractional transition width of the slab profile if gind == 1: # no sub-slices b = dzls(n, tb, d1, d2) else: # left stopband f = np.asarray([0, shift - (1 + ftw) * (tb / 2)]) m = np.asarray([0, 0]) w = np.asarray([d1 / d2]) # first sub-band ii = 1 gcent = shift + (ii - g / 2 - 1 / 2) * tb / g # first band center # first band left edge f = np.append(f, gcent - (tb / g / 2 - ftw * (tb / 2))) m = np.append(m, encode[ii - 1]) if encode[ii - 1] != encode[ii]: # add the first band's right edge and its amplitude, and a weight f = np.append(f, gcent + (tb / g / 2 - ftw * (tb / 2))) m = np.append(m, encode[ii - 1]) w = np.append(w, 1) # middle sub-bands for ii in range(2, g): gcent = shift + (ii - g / 2 - 1 / 2) * tb / g # center of band if encode[ii - 1] != encode[ii - 2]: # add a left edge and amp for this band f = np.append(f, gcent - (tb / g / 2 - ftw * (tb / 2))) m = np.append(m, encode[ii - 1]) if encode[ii - 1] != encode[ii]: # add a right edge and its amp, and a weight for this band f = np.append(f, gcent + (tb / g / 2 - ftw * (tb / 2))) m = np.append(m, encode[ii - 1]) w = np.append(w, 1) # last sub-band ii = g gcent = shift + (ii - g / 2 - 1 / 2) * tb / g # center of last band if encode[ii - 1] != encode[ii - 2]: # add a left edge and amp for the last band f = np.append(f, gcent - (tb / g / 2 - ftw * (tb / 2))) m = np.append(m, encode[ii - 1]) # add a right edge and its amp, and a weight for the last band f = np.append(f, gcent + (tb / g / 2 - ftw * (tb / 2))) m = np.append(m, encode[ii - 1]) w = np.append(w, 1) # right stop-band f = np.append(f, (shift + (1 + ftw) * (tb / 2), (n / 2))) / (n / 2) m = np.append(m, [0, 0]) w = np.append(w, d1 / d2) # separate the positive and negative bands mp = (m > 0).astype(float) mn = (m < 0).astype(float) # design the positive and negative filters c = np.exp(1j * 2 * np.pi / (2 * (n + 1)) * np.concatenate( [np.arange(0, n / 2 + 1, 1), np.arange(-n / 2, 0, 1)])) bp = signal.firls(n + 1, f, mp, w) # the positive filter bn = signal.firls(n + 1, f, mn, w) # the negative filter # combine the filters and demodulate b = sp.ifft(np.multiply(sp.fft(bp - bn, center=False), c), center=False) b = np.real(b[:n]) # hilbert transform to suppress negative passband b = signal.hilbert(b) # demodulate to DC c_shift = np.exp(-1j * 2 * np.pi / n * shift * np.arange(0, n, 1)) / 2 c_shift *= np.exp(-1j * np.pi / n * shift) b = np.multiply(b, c_shift) return b
def dz_gslider_b(n=128, g=5, gind=1, tb=4, d1=0.01, d2=0.01, phi=np.pi, shift=32): r"""Design a g-slider pulse b Args: n (int): number of time points. g (int): number of sub-slices. gind (int): subslice index. tb (int): time bandwidth product. d1 (float): passband ripple level in :math:'M_0^{-1}'. d2 (float): stopband ripple level in :math:'M_0^{-1}'. phi (float): subslice phase. shift (int): n time points shift of pulse. Returns: b (array): SLR beta parameter. References: Setsompop, K. et al. 'High-resolution in vivo diffusion imaging of the human brain with generalized slice dithered enhanced resolution: Simultaneous multislice (gSlider-SMS). Magn. Reson. Med.79, 141–151 (2018). """ ftw = dinf(d1, d2) / tb # fractional transition width of the slab profile if np.fmod(g, 2) and gind == int(np.ceil(g / 2)): # centered sub-slice if g == 1: # no sub-slices, as a sanity check b = dzls(n, tb, d1, d2) else: # Design 2 filters, to allow arbitrary phases on the subslice the # first is a wider notch filter with '0's where it the subslice # appears, and the second is the subslice. Multiply the subslice by # its phase and add the filters. f = np.asarray([ 0, (1 / g - ftw) * (tb / 2), (1 / g + ftw) * (tb / 2), (1 - ftw) * (tb / 2), (1 + ftw) * (tb / 2), (n / 2) ]) f = f / (n / 2) m_notch = [0, 0, 1, 1, 0, 0] m_sub = [1, 1, 0, 0, 0, 0] w = [1, 1, d1 / d2] b_notch = signal.firls(n + 1, f, m_notch, w) # the notched filter b_sub = signal.firls(n + 1, f, m_sub, w) # the subslice filter # add them with the subslice phase b = np.add(b_notch, np.multiply(np.exp(1j * phi), b_sub)) # shift the filter half a sample to make it symmetric, # like in MATLAB c = np.exp(1j * 2 * np.pi / (2 * (n + 1)) * np.concatenate( [np.arange(0, n / 2 + 1, 1), np.arange(-n / 2, 0, 1)])) b = sp.ifft(np.multiply(sp.fft(b, center=False), c), center=False) # lop off extra sample b = b[:n] else: # design filters for the slab and the subslice, hilbert xform them # to suppress their left bands, # then demodulate the result back to DC gcent = shift + (gind - g / 2 - 1 / 2) * tb / g if gind > 1 and gind < g: # separate transition bands for slab+slice f = np.asarray([ 0, shift - (1 + ftw) * (tb / 2), shift - (1 - ftw) * (tb / 2), gcent - (tb / g / 2 + ftw * (tb / 2)), gcent - (tb / g / 2 - ftw * (tb / 2)), gcent + (tb / g / 2 - ftw * (tb / 2)), gcent + (tb / g / 2 + ftw * (tb / 2)), shift + (1 - ftw) * (tb / 2), shift + (1 + ftw) * (tb / 2), (n / 2) ]) f = f / (n / 2) m_notch = [0, 0, 1, 1, 0, 0, 1, 1, 0, 0] m_sub = [0, 0, 0, 0, 1, 1, 0, 0, 0, 0] w = [d1 / d2, 1, 1, 1, d1 / d2] elif gind == 1: # the slab and slice share a left transition band f = np.asarray([ 0, shift - (1 + ftw) * (tb / 2), shift - (1 - ftw) * (tb / 2), gcent + (tb / g / 2 - ftw * (tb / 2)), gcent + (tb / g / 2 + ftw * (tb / 2)), shift + (1 - ftw) * (tb / 2), shift + (1 + ftw) * (tb / 2), (n / 2) ]) f = f / (n / 2) m_notch = [0, 0, 0, 0, 1, 1, 0, 0] m_sub = [0, 0, 1, 1, 0, 0, 0, 0] w = [d1 / d2, 1, 1, d1 / d2] elif gind == g: # the slab and slice share a right transition band f = np.asarray([ 0, shift - (1 + ftw) * (tb / 2), shift - (1 - ftw) * (tb / 2), gcent - (tb / g / 2 + ftw * (tb / 2)), gcent - (tb / g / 2 - ftw * (tb / 2)), shift + (1 - ftw) * (tb / 2), shift + (1 + ftw) * (tb / 2), (n / 2) ]) f = f / (n / 2) m_notch = [0, 0, 1, 1, 0, 0, 0, 0] m_sub = [0, 0, 0, 0, 1, 1, 0, 0] w = [d1 / d2, 1, 1, d1 / d2] c = np.exp(1j * 2 * np.pi / (2 * (n + 1)) * np.concatenate( [np.arange(0, n / 2 + 1, 1), np.arange(-n / 2, 0, 1)])) b_notch = signal.firls(n + 1, f, m_notch, w) # the notched filter b_notch = sp.ifft(np.multiply(sp.fft(b_notch, center=False), c), center=False) b_notch = np.real(b_notch[:n]) # hilbert transform to suppress negative passband b_notch = signal.hilbert(b_notch) b_sub = signal.firls(n + 1, f, m_sub, w) # the sub-band filter b_sub = sp.ifft(np.multiply(sp.fft(b_sub, center=False), c), center=False) b_sub = np.real(b_sub[:n]) # hilbert transform to suppress negative passband b_sub = signal.hilbert(b_sub) # add them with the subslice phase b = b_notch + np.exp(1j * phi) * b_sub # demodulate to DC c_shift = np.exp(-1j * 2 * np.pi / n * shift * np.arange(0, n, 1)) / 2 c_shift *= np.exp(-1j * np.pi / n * shift) b = np.multiply(b, c_shift) return b
def kspace_precond(mps, weights=None, coord=None, lamda=0, device=sp.cpu_device, oversamp=1.25): r"""Compute a diagonal preconditioner in k-space. Considers the optimization problem: .. math:: \min_P \| P A A^H - I \|_F^2 where A is the Sense operator. Args: mps (array): sensitivity maps of shape [num_coils] + image shape. weights (array): k-space weights. coord (array): k-space coordinates of shape [...] + [ndim]. lamda (float): regularization. Returns: array: k-space preconditioner of same shape as k-space. """ dtype = mps.dtype if weights is not None: weights = sp.to_device(weights, device) device = sp.Device(device) xp = device.xp mps_shape = list(mps.shape) img_shape = mps_shape[1:] img2_shape = [i * 2 for i in img_shape] ndim = len(img_shape) scale = sp.prod(img2_shape)**1.5 / sp.prod(img_shape) with device: if coord is None: idx = (slice(None, None, 2), ) * ndim ones = xp.zeros(img2_shape, dtype=dtype) if weights is None: ones[idx] = 1 else: ones[idx] = weights**0.5 psf = sp.ifft(ones) else: coord2 = coord * 2 ones = xp.ones(coord.shape[:-1], dtype=dtype) if weights is not None: ones *= weights**0.5 psf = sp.nufft_adjoint(ones, coord2, img2_shape, oversamp=oversamp) p_inv = [] for mps_i in mps: mps_i = sp.to_device(mps_i, device) mps_i_norm2 = xp.linalg.norm(mps_i)**2 xcorr_fourier = 0 for mps_j in mps: mps_j = sp.to_device(mps_j, device) xcorr_fourier += xp.abs( sp.fft(mps_i * xp.conj(mps_j), img2_shape))**2 xcorr = sp.ifft(xcorr_fourier) xcorr *= psf if coord is None: p_inv_i = sp.fft(xcorr)[idx] else: p_inv_i = sp.nufft(xcorr, coord2, oversamp=oversamp) if weights is not None: p_inv_i *= weights**0.5 p_inv.append(p_inv_i * scale / mps_i_norm2) p_inv = (xp.abs(xp.stack(p_inv, axis=0)) + lamda) / (1 + lamda) p_inv[p_inv == 0] = 1 p = 1 / p_inv return p.astype(dtype)
plt.imshow(img_lr_bf[:, 100, :], cmap='gray') plt.show() #print (img_lr_bf.shape) quit() ksp = np.load('./stefan_data/ksp.npy') ksp_lr = np.load('./stefan_data/ksp_lr.npy') ksp_lr_bf = np.load('./stefan_data/ksp_lr_bf.npy') print(ksp_lr_bf.shape) #quit() print(ksp.shape) print(ksp_lr.shape) print(ksp_lr_bf.shape) #quit() import sigpy as sp img = np.sum(np.abs(sp.ifft(ksp_lr, axes=[-1, -2, -3]))**2, axis=0)**0.5 #img_lr_bf = np.sum(np.abs(sp.ifft(ksp_lr_bf, axes=[-1, -2, -3]))**2, axis=0)**0.5 plt.imshow(img[:, 100, :], cmap='gray') plt.show() #plt.imshow(np.real(ksp_lr[0, :, 100, :])) #plt.show() #plt.imshow(np.real(lr[0, :, 100, :])) #plt.imshow(np.real(img_lr_bf[:, 100, :]), cmap='gray') #plt.show() quit() ''' #R = np.load('filter.raisr') #R = np.load(open(r'./filter.raisr', 'rb'), allow_pickle=True) #quit()
def dzrf(n=64, tb=4, ptype='st', ftype='ls', d1=0.01, d2=0.01, cancel_alpha_phs=False, custom_profile=None): r"""Primary function for design of pulses using the SLR algorithm. Args: n (int): number of time points. tb (int): pulse time bandwidth product. ptype (string): pulse type, 'st' (small-tip excitation), 'ex' (pi/2 excitation pulse), 'se' (spin-echo pulse), 'inv' (inversion), or 'sat' (pi/2 saturation pulse). ftype (string): type of filter to use: 'ms' (sinc), 'pm' (Parks-McClellan equal-ripple), 'min' (minphase using factored pm), 'max' (maxphase using factored pm), 'ls' (least squares), or 'cp' (custom excitation profile). d1 (float): passband ripple level in :math:'M_0^{-1}'. d2 (float): stopband ripple level in :math:'M_0^{-1}'. cancel_alpha_phs (bool): For 'ex' pulses, absorb the alpha phase profile from beta's profile, so they cancel for a flatter total phase custom_profile (array): if provided, pulse will be designed to excite an arbitrary profile rather than a rectangular one, following [2]. Returns: rf (array): designed RF pulse. References: [1] Pauly, J., Le Roux, Patrick., Nishimura, D., and Macovski, A. (1991). Parameter Relations for the Shinnar-LeRoux Selective Excitation Pulse Design Algorithm. IEEE Transactions on Medical Imaging, Vol 10, No 1, 53-65. [2] Barral, J., Pauly, J., and Nishimura, D. (2008). SLR RF Pulse Design for Arbitrarily-Shaped Excitation Profiles. Proc. Intl. Soc. Mag. Reson. Med. 16, 1323. """ [bsf, d1, d2] = calc_ripples(ptype, d1, d2) if ftype == 'ms': # sinc b = msinc(n, tb / 4) elif ftype == 'pm': # linphase b = dzlp(n, tb, d1, d2) elif ftype == 'min': # minphase b = dzmp(n, tb, d1, d2) b = b[::-1] elif ftype == 'max': # maxphase b = dzmp(n, tb, d1, d2) elif ftype == 'ls': # least squares b = dzls(n, tb, d1, d2) elif ftype == 'cp': # custom profile, [2] if custom_profile is None: raise Exception('cp filter selected but custom_profile not passed') b = np.sin(np.arcsin(custom_profile) / 2) else: raise Exception('Filter type ("{}") is not recognized.'.format(ftype)) if ftype == 'cp': # custom profile rf design, following [2] b_hat = bsf * sp.ifft(b, center=True, norm=None) rf = b2rf(b_hat, cancel_alpha_phs=True) else: if ptype == 'st': rf = b elif ptype == 'ex': b = bsf * b rf = b2rf(b, cancel_alpha_phs) else: b = bsf * b rf = b2rf(b) return rf
def dz_recursive_rf(n_seg, tb, n, se_seq=False, tb_ref=8, z_pad_fact=4, win_fact=1.75, cancel_alpha_phs=True, t1=np.inf, tr_seg=60, use_mz=True, d1=0.01, d2=0.01, d1se=0.01, d2se=0.01): r"""Recursive SLR pulse design. Args: n_seg (int): number of segments designed by recursion. tb (int): time bandwidth product. n (int): pulse length. se_seq (bool): spin echo sequence. tb_ref (int): time bandwidth product of refocusing pulse. z_pad_fact (float): zero padding factor. win_fact (float): applied window factor. cancel_alpha_phs (bool): absorb the alpha phase profile from beta's profile, so they cancel for a flatter total phase t1 (float): t1 tr_seg (int): length of tr use_mz (bool): design the pulses accounting for the actual Mz profile d1 (float): passband ripple level in :math:'M_0^{-1}'. d2 (float): stopband ripple level in :math:'M_0^{-1}'. d1se (float): passband ripple level for se d2se (float): stopband ripple level for se Returns: If se_seq=True, 2-element tuple containing - **rf** (*array*): rf pulse out. - **rf_ref** (*array*): rf refocusing pulse out. """ # get refocusing pulse and its rotation parameters if se_seq is True: [bsf, d1se, d2se] = calc_ripples('se', d1se, d2se) b_ref = bsf * dzls(n, tb_ref, d1se, d2se) b_ref = np.concatenate( (np.zeros(int(z_pad_fact * n / 2 - n / 2)), b_ref, np.zeros(int(z_pad_fact * n / 2 - n / 2)))) rf_ref = b2rf(b_ref) bref = sp.fft(b_ref, norm=None) bref /= np.max(np.abs(bref)) bref_mag = np.abs(bref) aref_mag = np.abs(np.sqrt(1 - bref_mag**2)) flip_ref = 2 * np.arcsin(bref_mag[int(z_pad_fact * n / 2)]) \ * 180 / np.pi # get flip angles flip = np.zeros(n_seg) flip[n_seg - 1] = 90 for jj in range(n_seg - 2, -1, -1): if se_seq is False: flip[jj] = np.arctan(np.sin(flip[jj + 1] * np.pi / 180)) flip[jj] = flip[jj] * 180 / np.pi # deg else: flip[jj] = np.arctan( np.cos(flip_ref * np.pi / 180) * np.sin(flip[jj + 1] * np.pi / 180)) flip[jj] = flip[jj] * 180 / np.pi # deg # design first RF pulse b = np.zeros((int(z_pad_fact * n), n_seg), dtype=complex) b[int(z_pad_fact * n / 2 - n / 2):int(z_pad_fact * n / 2 + n / 2), 0] = \ dzls(n, tb, d1, d2) # b = np.concatenate((np.zeros(int(zPadFact*N/2-N/2)), b, # np.zeros(int(zPadFact*N/2-N/2)))) B = sp.fft(b[:, 0], norm=None) c = np.exp(-1j * 2 * np.pi / (n * z_pad_fact) / 2 * np.arange(-n * z_pad_fact / 2, n * z_pad_fact / 2, 1)) B = np.multiply(B, c) b[:, 0] = sp.ifft(B / np.max(np.abs(B)), norm=None) b[:, 0] *= np.sin(flip[0] * (np.pi / 180) / 2) rf = np.zeros((z_pad_fact * n, n_seg), dtype=complex) a = b2a(b[:, 0]) if cancel_alpha_phs: # cancel a phase by absorbing into b # Note that this is the only time we need to do it b_a_phase = sp.fft(b[:, 0], center=False, norm=None) * \ np.exp(-1j * np.angle(sp.fft(a[np.size(a)::-1], center=False, norm=None))) b[:, 0] = sp.ifft(b_a_phase, center=False, norm=None) rf[:, 0] = b2rf(b[:, 0]) # get min-phase alpha and its response # a = b2a(b[:, 0]) A = sp.fft(a) # calculate beta filter response B = sp.fft(b[:, 0], norm=None) if win_fact < z_pad_fact: win_len = (win_fact - 1) * n npad = n * z_pad_fact - win_fact * n # blackman window? window = signal.blackman(int((win_fact - 1) * n)) # split in half; stick N ones in the middle window = np.concatenate((window[0:int(win_len / 2)], np.ones(n), window[int(win_len / 2):])) window = np.concatenate( (np.zeros(int(npad / 2)), window, np.zeros(int(npad / 2)))) # apply windowing to first pulse for consistency b[:, 0] = np.multiply(b[:, 0], window) rf[:, 0] = b2rf(b[:, 0]) # recalculate B and A B = sp.fft(b[:, 0], norm=None) A = sp.fft(b2a(b[:, 0]), norm=None) # use A and B to get Mxy # Mxy = np.zeros((zPadFact*N, Nseg), dtype = complex) if se_seq is False: mxy0 = 2 * np.conj(A) * B else: mxy0 = 2 * A * np.conj(B) * bref**2 # Amplitude of next pulse's Mxy profile will be # |Mz*2*a*b| = |Mz*2*sqrt(1-abs(B).^2)*B|. # If we set this = |Mxy_1|, we can solve for |B| via solving quadratic # equation 4*Mz^2*(1-B^2)*B^2 = |Mxy_1|^2. # Subsequently solve for |A|, and get phase of A via min-phase, and # then get phase of B by dividing phase of A from first pulse's Mxy phase. mz = np.ones((z_pad_fact * n), dtype=complex) for jj in range(1, n_seg): # calculate Mz profile after previous pulse if se_seq is False: mz = mz * (1 - 2 * np.abs(B) ** 2) * np.exp(-tr_seg / t1) + \ (1 - np.exp(-tr_seg / t1)) else: mz = mz * (1 - 2 * (np.abs(A * bref_mag)**2 + np.abs(aref_mag * B)**2)) # (second term is about 1%) if use_mz is True: # design the pulses accounting for the # actual Mz profile (the full method) # set up quadratic equation to get |B| cq = -np.abs(mxy0)**2 if se_seq is False: bq = 4 * mz**2 aq = -4 * mz**2 else: bq = 4 * (bref_mag**4) * mz**2 aq = -4 * (bref_mag**4) * mz**2 bmag = np.sqrt( (-bq + np.real(np.sqrt(bq**2 - 4 * aq * cq))) / (2 * aq)) bmag[np.isnan(bmag)] = 0 # get A - easier to get complex A than complex B since |A| is # determined by |B|, and phase is gotten by min-phase relationship # Phase of B doesn't matter here since only profile mag is used by # b2a A = sp.fft(b2a(sp.ifft(bmag, norm=None)), norm=None) # trick: now we can get complex B from ratio of Mxy and A B = mxy0 / (2 * np.conj(A) * mz) else: # design assuming ideal Mz (conventional VFA) B *= np.sin(np.pi / 180 * flip[jj] / 2) \ / np.sin(np.pi / 180 * flip[jj - 1] / 2) A = sp.fft(b2a(sp.ifft(B, norm=None)), norm=None) # get polynomial b[:, jj] = sp.ifft(B, norm=None) if win_fact < z_pad_fact: b[:, jj] *= window # recalculate B and A B = sp.fft(b[:, jj], norm=None) A = sp.fft(b2a(b[:, jj]), norm=None) rf[:, jj] = b2rf(b[:, jj]) # truncate the RF if win_fact < z_pad_fact: pulse_len = int(win_fact * n) rf = rf[int(npad / 2):int(npad / 2 + pulse_len), :] if se_seq is False: return rf else: return rf, rf_ref
def circulant_precond(mps, weights=None, coord=None, lamda=0, device=sp.cpu_device): r"""Compute circulant preconditioner. Considers the optimization problem: .. math:: \min_P \| A^H A - F P F^H \|_2^2 where A is the Sense operator, and F is a unitary Fourier transform operator. Args: mps (array): sensitivity maps of shape [num_coils] + image shape. weights (array): k-space weights. coord (array): k-space coordinates of shape [...] + [ndim]. lamda (float): regularization. Returns: array: circulant preconditioner of image shape. """ if coord is not None: coord = sp.to_device(coord, device) if weights is not None: weights = sp.to_device(weights, device) dtype = mps.dtype device = sp.Device(device) xp = device.xp mps_shape = list(mps.shape) img_shape = mps_shape[1:] img2_shape = [i * 2 for i in img_shape] ndim = len(img_shape) scale = sp.prod(img2_shape)**1.5 / sp.prod(img_shape)**2 with device: idx = (slice(None, None, 2), ) * ndim if coord is None: ones = xp.zeros(img2_shape, dtype=dtype) if weights is None: ones[idx] = 1 else: ones[idx] = weights**0.5 psf = sp.ifft(ones) else: coord2 = coord * 2 ones = xp.ones(coord.shape[:-1], dtype=dtype) if weights is not None: ones *= weights**0.5 psf = sp.nufft_adjoint(ones, coord2, img2_shape) p_inv = 0 for mps_i in mps: mps_i = sp.to_device(mps_i, device) xcorr_fourier = xp.abs(sp.fft(xp.conj(mps_i), img2_shape))**2 xcorr = sp.ifft(xcorr_fourier) xcorr *= psf p_inv_i = sp.fft(xcorr) p_inv_i = p_inv_i[idx] p_inv_i *= scale if weights is not None: p_inv_i *= weights**0.5 p_inv += p_inv_i p_inv += lamda p_inv[p_inv == 0] = 1 p = 1 / p_inv return p.astype(dtype)
def time_ifft_non_centered(self): y = sp.ifft(self.x, center=False)
def espirit_maps(ksp, calib_width=24, thresh=0.001, kernel_width=6, crop=0.8, max_power_iter=30, device=sp.cpu_device, output_eigenvalue=False): """Generate ESPIRiT maps from k-space. Currently only supports outputting one set of maps. Args: ksp (array): k-space array of shape [num_coils, n_ndim, ..., n_1] calib (tuple of ints): length-2 image shape. thresh (float): threshold for the calibration matrix. kernel_width (int): kernel width for the calibration matrix. max_power_iter (int): maximum number of power iterations. device (Device): computing device. crop (int): cropping threshold. Returns: array: ESPIRiT maps of the same shape as ksp. References: Martin Uecker, Peng Lai, Mark J. Murphy, Patrick Virtue, Michael Elad, John M. Pauly, Shreyas S. Vasanawala, and Michael Lustig ESPIRIT - An Eigenvalue Approach to Autocalibrating Parallel MRI: Where SENSE meets GRAPPA. Magnetic Resonance in Medicine, 71:990-1001 (2014) """ img_ndim = ksp.ndim - 1 num_coils = len(ksp) with sp.get_device(ksp): # Get calibration region calib_shape = [num_coils] + [calib_width] * img_ndim calib = sp.resize(ksp, calib_shape) calib = sp.to_device(calib, device) device = sp.Device(device) xp = device.xp with device: # Get calibration matrix kernel_shape = [num_coils] + [kernel_width] * img_ndim kernel_strides = [1] * (img_ndim + 1) mat = sp.array_to_blocks(calib, kernel_shape, kernel_strides) mat = mat.reshape([-1, sp.prod(kernel_shape)]) # Perform SVD on calibration matrix _, S, VH = xp.linalg.svd(mat, full_matrices=False) VH = VH[S > thresh * S.max(), :] # Get kernels num_kernels = len(VH) kernels = VH.reshape([num_kernels] + kernel_shape) img_shape = ksp.shape[1:] # Get covariance matrix in image domain AHA = xp.zeros(img_shape[::-1] + (num_coils, num_coils), dtype=ksp.dtype) for kernel in kernels: img_kernel = sp.ifft(sp.resize(kernel, ksp.shape), axes=range(-img_ndim, 0)) aH = xp.expand_dims(img_kernel.T, axis=-1) a = xp.conj(aH.swapaxes(-1, -2)) AHA += aH @ a AHA *= (sp.prod(img_shape) / kernel_width**img_ndim) # Power Iteration to compute top eigenvector mps = xp.ones(ksp.shape[::-1] + (1, ), dtype=ksp.dtype) for _ in range(max_power_iter): sp.copyto(mps, AHA @ mps) eig_value = xp.sum(xp.abs(mps)**2, axis=-2, keepdims=True)**0.5 mps /= eig_value # Normalize phase with respect to first channel mps = mps.T[0] mps *= xp.conj(mps[0] / xp.abs(mps[0])) # Crop maps by thresholding eigenvalue eig_value = eig_value.T[0] mps *= eig_value > crop if output_eigenvalue: return mps, eig_value else: return mps