def seggrappa(kspace, calibs, *args, **kwargs): '''Segmented GRAPPA. See pygrappa.grappa() for full list of arguments. Parameters ---------- calibs : list of array_like List of calibration regions. Notes ----- A generalized implementation of the method described in [1]_. Multiple ACS regions can be supplied to function. GRAPPA is run for each ACS region and then averaged to produce the final reconstruction. References ---------- .. [1] Park, Jaeseok, et al. "Artifact and noise suppression in GRAPPA imaging using improved k‐space coil calibration and variable density sampling." Magnetic Resonance in Medicine: An Official Journal of the International Society for Magnetic Resonance in Medicine 53.1 (2005): 186-193. ''' # Do the reconstruction for each of the calibration regions recons = [cgrappa(kspace, c, *args, **kwargs) for c in calibs] # Average all the reconstructions return np.mean(recons, axis=0)
np.fft.ifftshift(imspace, axes=ax), axes=ax), axes=ax) # crop 20x20 window from the center of k-space for calibration pd = 10 ctr = int(N/2) calib = kspace[ctr-pd:ctr+pd, ctr-pd:ctr+pd, :].copy() # calibrate a kernel kernel_size = (4, 4) # undersample by a factor of 2 in both x and y kspace[::2, 1::2, :] = 0 kspace[1::2, ::2, :] = 0 # reconstruct: res = cgrappa( kspace, calib, kernel_size, coil_axis=-1, lamda=0.01) # Take a look res = np.abs(np.sqrt(N**2)*np.fft.fftshift(np.fft.ifft2( np.fft.ifftshift(res, axes=ax), axes=ax), axes=ax)) M, N = res.shape[:2] res0 = np.zeros((2*M, 2*N)) kk = 0 for idx in np.ndindex((2, 2)): ii, jj = idx[:] res0[ii*M:(ii+1)*M, jj*N:(jj+1)*N] = res[..., kk] kk += 1 plt.imshow(res0, cmap='gray') plt.show()
def nlgrappa(kspace, calib, kernel_size=(5, 5), ml_kernel='polynomial', ml_kernel_args=None, coil_axis=-1): '''NL-GRAPPA. Parameters ---------- kspace : array_like calib : array_like kernel_size : tuple of int, optional ml_kernel : { 'linear', 'polynomial', 'sigmoid', 'rbf', 'laplacian', 'chi2'}, optional Kernel functions modeled on scikit-learn metrics.pairwise module but which can handle complex-valued inputs. ml_kernel_args : dict or None, optional Arguments to pass to kernel functions. coil_axis : int, optional Axis holding the coil data. Returns ------- res : array_like Reconstructed k-space. Notes ----- Implements the algorithm described in [1]_. Bias term is removed from polynomial kernel as it adds a PSF-like overlay onto the reconstruction. Currently only `polynomial` method is implemented. References ---------- .. [1] Chang, Yuchou, Dong Liang, and Leslie Ying. "Nonlinear GRAPPA: A kernel approach to parallel MRI reconstruction." Magnetic resonance in medicine 68.3 (2012): 730-740. ''' raise NotImplementedError("NL-GRAPPA is not currently working!") # Coils to the back kspace = np.moveaxis(kspace, coil_axis, -1) calib = np.moveaxis(calib, coil_axis, -1) _kx, _ky, nc = kspace.shape[:] # Get the correct kernel: _phi = { # 'linear': linear_kernel, 'polynomial': polynomial_kernel, # 'sigmoid': sigmoid_kernel, # 'rbf': rbf_kernel, # 'laplacian': laplacian_kernel, # 'chi2': chi2_kernel, }[ml_kernel] # Get default args if none were passed in if ml_kernel_args is None: ml_kernel_args = { 'cross_term_neighbors': 1, } # Pass arguments to kernel function phi = partial(_phi, **ml_kernel_args) # Get the extra "virtual" channels using kernel function, phi vkspace = phi(kspace) vcalib = phi(calib) # Pass onto cgrappa for the heavy lifting return np.moveaxis( cgrappa(vkspace, vcalib, kernel_size=kernel_size, coil_axis=-1, nc_desired=nc, lamda=0), -1, coil_axis)
def hpgrappa( kspace, calib, fov, kernel_size=(5, 5), w=None, c=None, ret_filter=False, coil_axis=-1, lamda=0.01, silent=True): '''High-pass GRAPPA. Parameters ---------- fov : tuple, (FOV_x, FOV_y) Field of view (in m). w : float, optional Filter parameter: determines the smoothness of the filter boundary. c : float, optional Filter parameter: sets the cutoff frequency. ret_filter : bool, optional Returns the high pass filter determined by (w, c). Notes ----- If w and/or c are None, then the closest values listed in Table 1 from [1]_ will be used. F2 described by Equation [2] in [1]_ is used to generate the high pass filter. References ---------- .. [1] Huang, Feng, et al. "High‐pass GRAPPA: An image support reduction technique for improved partially parallel imaging." Magnetic Resonance in Medicine: An Official Journal of the International Society for Magnetic Resonance in Medicine 59.3 (2008): 642-649. ''' # Pass GRAPPA arguments forward grappa_args = { 'kernel_size': kernel_size, 'coil_axis': -1, 'lamda': lamda, 'silent': silent } # Put the coil dim in the back kspace = np.moveaxis(kspace, coil_axis, -1) calib = np.moveaxis(calib, coil_axis, -1) kx, ky, nc = kspace.shape[:] cx, cy, nc = calib.shape[:] kx2, ky2 = int(kx/2), int(ky/2) cx2, cy2 = int(cx/2), int(cy/2) # Get filter parameters if None provided if w is None or c is None: _w, _c = _filter_parameters(nc, np.min([cx, cy])) if w is None: w = _w if c is None: c = _c # We'll need the filter, seeing as this is high-pass GRAPPA fov_x, fov_y = fov[:] kxx, kyy = np.meshgrid( kx*np.linspace(-1, 1, kx)/(fov_x*2), # I think this gives ky*np.linspace(-1, 1, ky)/(fov_y*2)) # kspace FOV? F2 = (1 - 1/(1 + np.exp((np.sqrt(kxx**2 + kyy**2) - c)/w)) + 1/(1 + np.exp((np.sqrt(kxx**2 + kyy**2) + c)/w))) # Apply the filter to both kspace and calibration data kspace_fil = kspace*F2[..., None] calib_fil = calib*F2[kx2-cx2:kx2+cx2, ky2-cy2:ky2+cy2, None] # Do regular old GRAPPA on filtered data res = cgrappa(kspace_fil, calib_fil, **grappa_args) # Inverse filter res = res/F2[..., None] # Restore measured data mask = np.abs(kspace[..., 0]) > 0 res[mask, :] = kspace[mask, :] res[kx2-cx2:kx2+cx2, ky2-cy2:ky2+cy2, :] = calib res = np.moveaxis(res, -1, coil_axis) # Return the filter if user asked for it if ret_filter: return(res, F2) return res
ax = (0, 1) kspace = np.fft.ifftshift(np.fft.fft2(np.fft.fftshift( ph, axes=ax), axes=ax), axes=ax) # 20x20 calibration region ctr = int(N/2) pad = 20 calib = kspace[ctr-pad:ctr+pad, ctr-pad:ctr+pad, :].copy() # Undersample: R=3 kspace3x1 = kspace.copy() kspace3x1[1::3, ...] = 0 kspace3x1[2::3, ...] = 0 # Reconstruct using both GRAPPA and VC-GRAPPA res_grappa = cgrappa(kspace3x1.copy(), calib) res_nlgrappa = nlgrappa( kspace3x1.copy(), calib, ml_kernel='polynomial', ml_kernel_args={'cross_term_neighbors': 0}) # Bring back to image space imspace_nlgrappa = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( res_nlgrappa, axes=ax), axes=ax), axes=ax) imspace_grappa = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( res_grappa, axes=ax), axes=ax), axes=ax) # Coil combine (sum-of-squares) cc_nlgrappa = np.sqrt( np.sum(np.abs(imspace_nlgrappa)**2, axis=-1)) cc_grappa = np.sqrt(np.sum(np.abs(imspace_grappa)**2, axis=-1)) ph = shepp_logan(N)
ctr = int(N / 2) calib_upper = kspace[ctr - pad + offset:ctr + pad + offset, ...].copy() calib_lower = kspace[ctr - pad - offset:ctr + pad - offset, ...].copy() # A single calibration region at the center for comparison pad_single = 2 * pad calib = kspace[ctr - pad_single:ctr + pad_single, ...].copy() # Undersample kspace kspace[:, ::2, :] = 0 # Reconstruct using segmented GRAPPA with separate ACS regions res_seg = seggrappa(kspace, [calib_lower, calib_upper]) # Reconstruct using single calibration region at the center res_grappa = cgrappa(kspace, calib) # Into image space imspace_seg = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift(res_seg, axes=ax), axes=ax), axes=ax) imspace_grappa = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift(res_grappa, axes=ax), axes=ax), axes=ax) # Coil combine (sum-of-squares) cc_seg = np.sqrt(np.sum(np.abs(imspace_seg)**2, axis=-1)) cc_grappa = np.sqrt(np.sum(np.abs(imspace_grappa)**2, axis=-1)) ph = shepp_logan(N)
axes=ax), axes=ax) # 20x20 calibration region ctr = int(N / 2) pad = 10 calib = kspace[ctr - pad:ctr + pad, ctr - pad:ctr + pad, :].copy() # Undersample: R=4 kspace4x1 = kspace.copy() kspace4x1[1::4, ...] = 0 kspace4x1[2::4, ...] = 0 kspace4x1[3::4, ...] = 0 # Compare to regular ol' GRAPPA grecon4x1 = cgrappa(kspace4x1, calib, kernel_size=(4, 5)) # Get a GRAPPA operator and do the recon Gx, Gy = grappaop(calib) recon4x1 = kspace4x1.copy() recon4x1[1::4, ...] = recon4x1[0::4, ...] @ Gx recon4x1[2::4, ...] = recon4x1[1::4, ...] @ Gx recon4x1[3::4, ...] = recon4x1[2::4, ...] @ Gx # Try different undersampling factors: Rx=2, Ry=2. Same Gx, Gy # will work since we're using the same calibration region! kspace2x2 = kspace.copy() kspace2x2[1::2, ...] = 0 kspace2x2[:, 1::2, :] = 0 grecon2x2 = cgrappa(kspace2x2, calib, kernel_size=(4, 5)) recon2x2 = kspace2x2.copy()
shepp_logan(N))[..., None]*gaussian_csm(N, N, nc) # Trim down to make nonsquare # 1st > 2nd trim = int((N - M)/2) pad = int(calib_lines/2) imspace1 = imspace[:, trim:-trim, :] kspace1 = fft(imspace1) calib1 = kspace1[N2-pad:N2+pad, ...].copy() kspace1[::2, ...] = 0 # Undersample: R=2 # 2nd > 1st imspace2 = imspace[trim:-trim, ...] kspace2 = fft(imspace2) calib2 = kspace2[:, N2-pad:N2+pad, :].copy() kspace2[:, ::2, :] = 0 # Do the thing res1 = cgrappa(kspace1, calib1, kernel_size=(5, 5), coil_axis=-1) res2 = cgrappa(kspace2, calib2, kernel_size=(5, 5), coil_axis=-1) # Show sum-of-squares results sos1 = np.sqrt(np.sum(np.abs(ifft(res1))**2, axis=-1)) sos2 = np.sqrt(np.sum(np.abs(ifft(res2))**2, axis=-1)) plt.subplot(1, 2, 1) plt.imshow(sos1) plt.subplot(1, 2, 2) plt.imshow(sos2) plt.show()
# generate 4 coil phantom ph = shepp_logan(N) imspace = ph[..., None] * mps imspace = imspace.astype('complex') ax = (0, 1) kspace = 1 / np.sqrt(N**2) * np.fft.fftshift( np.fft.fft2(np.fft.ifftshift(imspace, axes=ax), axes=ax), axes=ax) # crop 20x20 window from the center of k-space for calibration pd = 10 ctr = int(N / 2) calib = kspace[ctr - pd:ctr + pd, ctr - pd:ctr + pd, :].copy() # calibrate a kernel kernel_size = (5, 5) # undersample by a factor of 2 in both x and y kspace[::2, 1::2, :] = 0 kspace[1::2, ::2, :] = 0 # Time both implementations t0 = time() recon0 = grappa(kspace, calib, (5, 5)) print(' GRAPPA: %g' % (time() - t0)) t0 = time() recon1 = cgrappa(kspace, calib, (5, 5)) print('CGRAPPA: %g' % (time() - t0)) assert np.allclose(recon0, recon1)
def igrappa(kspace, calib, kernel_size=(5, 5), k=0.3, coil_axis=-1, lamda=0.01, ref=None, niter=10, silent=True): '''Iterative GRAPPA. Parameters ---------- kspace : array_like 2D multi-coil k-space data to reconstruct from. Make sure that the missing entries have exact zeros in them. calib : array_like Calibration data (fully sampled k-space). kernel_size : tuple, optional Size of the 2D GRAPPA kernel (kx, ky). k : float, optional Regularization parameter for iterative reconstruction. Must be in the interval (0, 1). coil_axis : int, optional Dimension holding coil data. The other two dimensions should be image size: (sx, sy). lamda : float, optional Tikhonov regularization for the kernel calibration. ref : array_like or None, optional Reference k-space data. This is the true data that we are attempting to reconstruct. If provided, MSE at each iteration will be returned. If None, only reconstructed kspace is returned. niter : int, optional Number of iterations. silent : bool, optional Suppress messages to user. Returns ------- res : array_like k-space data where missing entries have been filled in. mse : array_like, optional MSE at each iteration. Returned if ref not None. Raises ------ AssertionError If regularization parameter k is not in the interval (0, 1). Notes ----- More or less implements the iterative algorithm described in [1]. References ---------- .. [1] Zhao, Tiejun, and Xiaoping Hu. "Iterative GRAPPA (iGRAPPA) for improved parallel imaging reconstruction." Magnetic Resonance in Medicine: An Official Journal of the International Society for Magnetic Resonance in Medicine 59.4 (2008): 903-907. ''' # Make sure k has a reasonable value assert 0 < k < 1, 'Parameter k should be in (0, 1)!' # Collect arguments to pass to cgrappa: grappa_args = { 'kernel_size': kernel_size, 'coil_axis': -1, 'lamda': lamda, 'silent': silent } # Put the coil dimension at the end kspace = np.moveaxis(kspace, coil_axis, -1) calib = np.moveaxis(calib, coil_axis, -1) kx, ky, _nc = kspace.shape[:] cx, cy, _nc = calib.shape[:] kx2, ky2 = int(kx / 2), int(ky / 2) cx2, cy2 = int(cx / 2), int(cy / 2) # Initial conditions kIm, W = cgrappa(kspace, calib, ret_weights=True, **grappa_args) ax = (0, 1) Im = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift(kIm, axes=ax), axes=ax), axes=ax) Fp = 1e6 # some large number to begin with # If user has provided reference, let's track the MSE if ref is not None: mse = np.zeros(niter) aref = np.abs(ref) # Fixed number of iterations for ii in trange(niter, leave=False, desc='iGRAPPA'): # Update calibration region -- now includes all estimated # lines plus unchanged calibration region calib0 = kIm.copy() calib0[kx2 - cx2:kx2 + cx2, ky2 - cy2:ky2 + cy2, :] = calib.copy() kTm, Wn = cgrappa(kspace, calib0, ret_weights=True, **grappa_args) Tm = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift(kTm, axes=ax), axes=ax), axes=ax) # Estimate relative image intensity change l1_Tm = np.linalg.norm(Tm.flatten(), ord=1) l1_Im = np.linalg.norm(Im.flatten(), ord=1) Tp = np.abs(l1_Tm - l1_Im) / l1_Im # Update weights p = Tp / (k * Fp) if p < 1: # Take this reconstruction and new weights Im = Tm kIm = kTm W = Wn else: # Modify weights to get new reconstruction p = 1 / p W = [(1 - p) * Wn0 + p * W0 for Wn0, W0 in zip(Wn, W)] # Need to be able to supply grappa with weights to use! kIm = cgrappa(kspace, calib0, Wsupp=W, **grappa_args) Im = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift(kIm, axes=ax), axes=ax), axes=ax) # Update Fp Fp = Tp # Track MSE if ref is not None: mse[ii] = compare_mse(aref, np.abs(kIm)) # Return the reconstructed kspace and MSE if ref kspace provided, # otherwise, just return reconstruction if ref is not None: return (kIm, mse) return kIm