def test_NUFFT_3D(self): """Test the adjoint operator for the 3D non-Cartesian Fourier transform on GPU """ for num_channels in self.num_channels: for platform in self.platforms: _mask = np.random.randint(2, size=(self.N, self.N, self.N)) _samples = convert_mask_to_locations(_mask) fourier_op_dir = NonCartesianFFT(samples=_samples, shape=(self.N, self.N, self.N), implementation=platform, n_coils=num_channels) Img = (np.random.randn(num_channels, self.N, self.N, self.N) + 1j * np.random.randn(num_channels, self.N, self.N, self.N)) f = (np.random.randn(num_channels, _samples.shape[0]) + 1j * np.random.randn(num_channels, _samples.shape[0])) f_p = fourier_op_dir.op(Img) I_p = fourier_op_dir.adj_op(f) x_d = np.vdot(Img, I_p) x_ad = np.vdot(f_p, f) np.testing.assert_allclose(x_d, x_ad, rtol=1e-5) print("NFFT in 3D adjoint test passes on GPU with" "num_channels = " + str(num_channels) + " on " "platform " + platform)
def test_NFFT_2D(self): """Test the adjoint operator for the 2D non-Cartesian Fourier transform, with density compensator set to 1, to vet the code path, the test is unchanged otherwise """ for num_channels in self.num_channels: print("Testing with num_channels=" + str(num_channels)) for i in range(self.max_iter): _mask = np.random.randint(2, size=(self.N, self.N)) _samples = convert_mask_to_locations(_mask) print("Process NFFT in 2D test '{0}'...", i) fourier_op = NonCartesianFFT(samples=_samples, shape=(self.N, self.N), n_coils=num_channels, implementation='cpu', density_comp=np.ones( (num_channels, _samples.shape[0]))) Img = np.random.randn(num_channels, self.N, self.N) + \ 1j * np.random.randn(num_channels, self.N, self.N) f = np.random.randn(num_channels, _samples.shape[0]) + \ 1j * np.random.randn(num_channels, _samples.shape[0]) f_p = fourier_op.op(Img) I_p = fourier_op.adj_op(f) x_d = np.vdot(Img, I_p) x_ad = np.vdot(f_p, f) np.testing.assert_allclose(x_d, x_ad, rtol=1e-10) print(" NFFT in 2D adjoint test passes")
def test_similarity_stack_3D_nfft(self): """Test the similarity of stacked implementation of Fourier transform to that of NFFT """ for channel in self.num_channels: print("Testing with num_channels=" + str(channel)) for N in [16, 32]: # Nz is the number of slices, this would check both N=Nz # and N!=Nz Nz = 16 _mask = np.random.randint(2, size=(N, N)) # Generate random mask along z sampling_z = np.random.randint(2, size=Nz) _mask3D = np.zeros((N, N, Nz)) for idx, acq_z in enumerate(sampling_z): _mask3D[:, :, idx] = _mask * acq_z _samples = convert_mask_to_locations(_mask3D) print("Process Stack-3D similarity with NFFT for N=" + str(N)) fourier_op_stack = Stacked3DNFFT(kspace_loc=_samples, shape=(N, N, Nz), implementation='cpu', n_coils=channel) fourier_op_nfft = NonCartesianFFT(samples=_samples, shape=(N, N, Nz), implementation='cpu', n_coils=channel) Img = np.random.random((channel, N, N, Nz)) + \ 1j * np.random.random((channel, N, N, Nz)) f = np.random.random((channel, _samples.shape[0])) + \ 1j * np.random.random((channel, _samples.shape[0])) start_time = time.time() stack_f_p = fourier_op_stack.op(Img) stack_I_p = fourier_op_stack.adj_op(f) stack_runtime = time.time() - start_time start_time = time.time() nfft_f_p = fourier_op_nfft.op(Img) nfft_I_p = fourier_op_nfft.adj_op(f) np.testing.assert_allclose(stack_f_p, nfft_f_p, rtol=1e-9) np.testing.assert_allclose(stack_I_p, nfft_I_p, rtol=1e-9) nfft_runtime = time.time() - start_time print("For N=" + str(N) + " Speedup = " + str(nfft_runtime / stack_runtime)) print("Stacked FFT in 3D adjoint test passes")
def generate_test_NFFT(self, shape, field_scale): """ Factorized code to test 2D and 3D wrapped NFFT with different homogeneous B0 field shifts at constant time. """ for L, i, n_coils in product(self.L, range(self.max_iter), self.n_coils): mask = np.random.randint(2, size=shape) samples = convert_mask_to_locations(mask) samples = samples[:samples.shape[0] - (samples.shape[0] % shape[0])] field_shift = field_scale * np.random.randint(-150, 150) field_map = field_shift * np.ones(shape) # Prepare reference and wrapper operators fourier_op = NonCartesianFFT( samples=samples, shape=shape, n_coils=n_coils, implementation='cpu', density_comp=np.ones((n_coils, samples.shape[0])) ) wrapper_op = ORCFFTWrapper(fourier_op, field_map=field_map, time_vec=np.ones(shape[0]), mask=np.ones(shape), num_interpolators=L, n_bins=self.n_bins) # Forward operator img = np.squeeze(np.random.randn(n_coils, *shape) \ + 1j * np.random.randn(n_coils, *shape)) ksp_fft = fourier_op.op(img) ksp_wra = wrapper_op.op(img * np.exp(-2j * np.pi * field_shift)) np.testing.assert_allclose(ksp_fft, ksp_wra, rtol=self.rtol) # Adjoint operator ksp = np.squeeze(np.random.randn(n_coils, samples.shape[0]) \ + 1j * np.random.randn(n_coils, samples.shape[0])) img_fft = fourier_op.adj_op(ksp) img_wra = wrapper_op.adj_op(ksp * np.exp(2j * np.pi * field_shift)) np.testing.assert_allclose(img_fft, img_wra, rtol=self.rtol)
def test_NFFT_3D(self): """Test the adjoint operator for the 3D non-Cartesian Fourier transform """ for num_channels in self.num_channels: print("Testing with num_channels=" + str(num_channels)) for i in range(self.max_iter): _mask = np.random.randint(2, size=(self.N, self.N, self.N)) _samples = convert_mask_to_locations(_mask) print("Process NFFT test in 3D '{0}'...", i) fourier_op = NonCartesianFFT(samples=_samples, shape=(self.N, self.N, self.N), n_coils=num_channels, implementation='cpu') Img = np.random.randn(num_channels, self.N, self.N, self.N) + \ 1j * np.random.randn(num_channels, self.N, self.N, self.N) f = np.random.randn(num_channels, _samples.shape[0]) + \ 1j * np.random.randn(num_channels, _samples.shape[0]) f_p = fourier_op.op(Img) I_p = fourier_op.adj_op(f) x_d = np.vdot(Img, I_p) x_ad = np.vdot(f_p, f) np.testing.assert_allclose(x_d, x_ad, rtol=1e-10) print(" NFFT in 3D adjoint test passes")
def get_Smaps(k_space, img_shape, samples, thresh, min_samples, max_samples, mode='gridding', method='linear', window_fun=None, density_comp=None, n_cpu=1, fourier_op_kwargs=None): r""" Get Smaps for from pMRI sample data. Estimate the sensitivity maps information from parallel mri acquisition and for variable density sampling scheme where the k-space center had been heavily sampled. Reference : Self-Calibrating Nonlinear Reconstruction Algorithms for Variable Density Sampling and Parallel Reception MRI https://ieeexplore.ieee.org/abstract/document/8448776 Parameters ---------- k_space: np.ndarray The acquired kspace of shape (M,L), where M is the number of samples acquired and L is the number of coils used img_shape: tuple The final output shape of Sensitivity Maps. samples: np.ndarray The sample locations where the above k_space data was acquired thresh: tuple The value of threshold in kspace for thresholding k-space center min_samples: tuple The minimum values in k-space where gridding must be done max_samples: tuple The maximum values in k-space where gridding must be done mode: string 'FFT' | 'NFFT' | 'gridding' | 'Stack', default='gridding' Defines the mode in which we would want to interpolate, NOTE: FFT should be considered only if the input has been sampled on the grid method: string 'linear' | 'cubic' | 'nearest', default='linear' For gridding mode, it defines the way interpolation must be done window_fun: "Hann", "Hanning", "Hamming", or a callable, default None. The window function to apply to the selected data. It is computed with the center locations selected. Only works with circular mask. If window_fun is a callable, it takes as input the n_samples x n_dims of samples position and would return an array of n_samples weight to be applied to the selected k-space values, before the smaps estimation. density_comp: np.ndarray default None The density compensation for kspace data in case it exists and we use density compensated adjoint for Smap estimation n_cpu: int default=1 Number of parallel jobs in case of parallel MRI fourier_op_kwargs: dict, default None The keyword arguments given to fourier_op initialization if mode == 'NFFT'. If None, we choose implementation of fourier op to 'gpuNUFFT' if library is installed. Returns ------- Smaps: np.ndarray the estimated sensitivity maps of shape (img_shape, L) with L the number of channels SOS: np.ndarray The sum of Square used to extract the sensitivity maps Notes ----- The Hann (or Hanning) and Hamming window of width :math:`2\theta` are defined as: .. math:: w(x,y) = a_0 - (1-a_0) * \cos(\pi * \sqrt{x^2+y^2}/\theta), \sqrt{x^2+y^2} \le \theta In the case of Hann window :math:`a_0=0.5`. For Hamming window we consider the optimal value in the equiripple sens: :math:`a_0=0.53836`. .. Wikipedia:: https://en.wikipedia.org/wiki/Window_function#Hann_and_Hamming_windows """ if len(min_samples) != len(img_shape) \ or len(max_samples) != len(img_shape): raise NameError('The img_shape, max_samples, and ' 'min_samples must be of same length') k_space, samples, *density_comp = \ extract_k_space_center_and_locations( data_values=k_space, samples_locations=samples, thr=thresh, img_shape=img_shape, is_fft=mode == 'FFT', window_fun=window_fun, density_comp=density_comp, ) if density_comp: density_comp = density_comp[0] else: density_comp = None if samples is None: mode = 'FFT' L, M = k_space.shape Smaps_shape = (L, *img_shape) Smaps = np.zeros(Smaps_shape).astype('complex128') if mode == 'FFT': if not M == img_shape[0] * img_shape[1]: raise ValueError([ 'The number of samples in the k-space must be', 'equal to the (image size, the number of coils)' ]) k_space = k_space.reshape(Smaps_shape) for l in range(Smaps_shape[2]): Smaps[l] = pfft.ifftshift(pfft.ifft2(pfft.fftshift(k_space[l]))) elif mode == 'NFFT': if fourier_op_kwargs is None: if gpunufft_available: fourier_op_kwargs = {'implementation': 'gpuNUFFT'} else: fourier_op_kwargs = {} fourier_op = NonCartesianFFT( samples=samples, shape=img_shape, density_comp=density_comp, n_coils=L, **fourier_op_kwargs, ) Smaps = fourier_op.adj_op(np.ascontiguousarray(k_space)) elif mode == 'Stack': grid_space = [ np.linspace(min_samples[i], max_samples[i], num=img_shape[i], endpoint=False) for i in np.arange(np.size(img_shape) - 1) ] grid = np.meshgrid(*grid_space) kspace_plane_loc, _, sort_pos, idx_mask_z = \ get_stacks_fourier(samples, img_shape) Smaps = Parallel(n_jobs=n_cpu, mmap_mode='r+')( delayed(gridded_inverse_fourier_transform_stack)( kspace_data_sorted=k_space[l, sort_pos], kspace_plane_loc=kspace_plane_loc, idx_mask_z=idx_mask_z, grid=tuple(grid), volume_shape=img_shape, method=method) for l in range(L)) Smaps = np.asarray(Smaps) elif mode == 'gridding': grid_space = [ np.linspace(min_samples[i], max_samples[i], num=img_shape[i], endpoint=False) for i in np.arange(np.size(img_shape)) ] grid = np.meshgrid(*grid_space) Smaps = \ Parallel(n_jobs=n_cpu, verbose=1, mmap_mode='r+')( delayed(gridded_inverse_fourier_transform_nd)( kspace_loc=samples, kspace_data=k_space[l], grid=tuple(grid), method=method ) for l in range(L) ) Smaps = np.asarray(Smaps) else: raise ValueError('Bad smap_extract_mode chosen! ' 'Please choose between : ' '`FFT` | `NFFT` | `gridding` | `Stack`') SOS = np.sqrt(np.sum(np.abs(Smaps)**2, axis=0)) for r in range(L): Smaps[r] /= SOS return Smaps, SOS
def get_Smaps(k_space, img_shape, samples, thresh, min_samples, max_samples, mode='Gridding', method='linear', n_cpu=1): """ This method estimate the sensitivity maps information from parallel mri acquisition and for variable density sampling scheme where teh k-space center had been heavily sampled. Reference : Self-Calibrating Nonlinear Reconstruction Algorithms for Variable Density Sampling and Parallel Reception MRI https://ieeexplore.ieee.org/abstract/document/8448776 Parameters ---------- k_space: np.ndarray The acquired kspace of shape (M,L), where M is the number of samples acquired and L is the number of coils used img_shape: tuple The final output shape of Sensitivity Maps. samples: np.ndarray The sample locations where the above k_space data was acquired thresh: tuple The value of threshold in kspace for thresholding k-space center min_samples: tuple The minimum values in k-space where gridding must be done max_samples: tuple The maximum values in k-space where gridding must be done mode: string 'FFT' | 'NFFT' | 'gridding', default='gridding' Defines the mode in which we would want to interpolate, NOTE: FFT should be considered only if the input has been sampled on the grid method: string 'linear' | 'cubic' | 'nearest', default='linear' For gridding mode, it defines the way interpolation must be done n_cpu: intm default=1 Number of parallel jobs in case of parallel MRI Returns ------- Smaps: np.ndarray the estimated sensitivity maps of shape (img_shape, L) with L the number of channels ISOS: np.ndarray The sum of Square used to extract the sensitivity maps """ if len(min_samples) != len(img_shape) \ or len(max_samples) != len(img_shape) \ or len(thresh) != len(img_shape): raise NameError('The img_shape, max_samples, ' 'min_samples and thresh must be of same length') k_space, samples = \ extract_k_space_center_and_locations( data_values=k_space, samples_locations=samples, thr=thresh, img_shape=img_shape) if samples is None: mode = 'FFT' L, M = k_space.shape Smaps_shape = (L, *img_shape) Smaps = np.zeros(Smaps_shape).astype('complex128') if mode == 'FFT': if not M == img_shape[0]*img_shape[1]: raise ValueError(['The number of samples in the k-space must be', 'equal to the (image size, the number of coils)' ]) k_space = k_space.reshape(Smaps_shape) for l in range(Smaps_shape[2]): Smaps[l] = pfft.ifftshift(pfft.ifft2(pfft.fftshift(k_space[l]))) elif mode == 'NFFT': fourier_op = NonCartesianFFT(samples=samples, shape=img_shape, implementation='cpu') Smaps = np.asarray([fourier_op.adj_op(k_space[l]) for l in range(L)]) elif mode == 'Stack': grid_space = [np.linspace(min_samples[i], max_samples[i], num=img_shape[i], endpoint=False) for i in np.arange(np.size(img_shape)-1)] grid = np.meshgrid(*grid_space) kspace_plane_loc, z_sample_loc, sort_pos = \ get_stacks_fourier(samples) Smaps = Parallel(n_jobs=n_cpu)( delayed(gridded_inverse_fourier_transform_stack) (kspace_plane_loc=kspace_plane_loc, z_samples=z_sample_loc, kspace_data=k_space[l, sort_pos], grid=tuple(grid), method=method) for l in range(L)) Smaps = np.asarray(Smaps) else: grid_space = [np.linspace(min_samples[i], max_samples[i], num=img_shape[i], endpoint=False) for i in np.arange(np.size(img_shape))] grid = np.meshgrid(*grid_space) Smaps = \ Parallel(n_jobs=n_cpu)(delayed( gridded_inverse_fourier_transform_nd) (kspace_loc=samples, kspace_data=k_space[l], grid=tuple(grid), method=method) for l in range(L)) Smaps = np.asarray(Smaps) SOS = np.sqrt(np.sum(np.abs(Smaps)**2, axis=0)) for r in range(L): Smaps[r] /= SOS return Smaps, SOS
fourier_op = NonCartesianFFT( samples=kspace_loc, shape=image.shape, implementation='gpuNUFFT', ) fourier_op_density_comp = NonCartesianFFT( samples=kspace_loc, shape=image.shape, implementation='gpuNUFFT', density_comp=density_comp ) # Get the kspace data retrospectively. Note that this can be done with # `fourier_op_density_comp` as the forward operator is the same kspace_obs = fourier_op.op(image.data) # Simple adjoint image_rec0 = pysap.Image(data=np.abs(fourier_op.adj_op(kspace_obs))) # image_rec0.show() base_ssim = ssim(image_rec0, image) print('The SSIM from Adjoint is : ' + str(base_ssim)) # Density Compensation adjoint: # This preconditions k-space giving a result closer to inverse image_rec1 = pysap.Image(data=np.abs( fourier_op_density_comp.adj_op(kspace_obs)) ) # image_rec1.show() new_ssim = ssim(image_rec1, image) print('The SSIM from Density ' 'compensated Adjoint is : ' + str(new_ssim))
def pdhg(data, p, **kwargs): # -- # -- MAIN LOWER LEVEL FUNCTION # -- # INPUTS: - data: kspace measurements # - p: p[:-1]=subsampling mask S(p), p[-1]=regularisation parameter alpha(p) # So len(p)=len(data)+1 # - fourier_op: fourier operator from a full mask of same shape as the final image. # - linear_op: linear operator used in regularisation functions # For the moment, only use waveletN. # - param: lower level energy parameters # Must contain parameters keys "epsilon" and "gamma". # mask_type (optional): type of mask used ("cartesian", "radial"). Assume a cartesian mask if not given. # -- # OPTIONAL INPUTS: # - const: algorithm constants if we already know the values we want to use for tau and sigma # If not given, will compute them according to what is said in the article. # - compute_energy: bool, we compute and return energy over iterations if True (default: False) # - ground_truth: matrix representing the true image the data come from (default: None). If not None, we compute the ssim over iterations. # - maxit,tol: We stop the algorithm when the norm of the difference between two steps # is smaller than tol or after maxit iterations (default: 200, 1e-4) # -- # OUTPUTS: - uk: final image # - norms(, energy, ssims): evolution of stopping criterion (and energy if compute_energy is True / ssims if ground_truth not None) fourier_op = kwargs.get("fourier_op", None) linear_op = kwargs.get("linear_op", None) param = kwargs.get("param", None) # Create fourier_op and linear_op if not given for multithreading if fourier_op is None: samples = kwargs.get("samples", []) shape = kwargs.get("shape", ()) if samples is not None: fourier_op = NonCartesianFFT(samples=samples, shape=shape, implementation='cpu') if fourier_op is None: raise ValueError("A fourier operator fourier_op must be given") if linear_op is None: wavelet_name = kwargs.get("wavelet_name", "") wavelet_scale = kwargs.get("wavelet_scale", 1) if wavelet_name != "": linear_op = WaveletN(wavelet_name=wavelet_name, nb_scale=wavelet_scale, padding_mode="periodization") if linear_op is None: raise ValueError("A linear operator linear_op must be given") if param is None: raise ValueError("Lower level parameters must be given") mask_type = kwargs.get("mask_type", "") const = kwargs.get("const", {}) compute_energy = kwargs.get("compute_energy", False) ground_truth = kwargs.get("ground_truth", None) maxit = kwargs.get("maxit", 200) tol = kwargs.get("tol", 1e-6) verbose = kwargs.get("verbose", 1) #Global parameters p, pn1 = p[:-1], p[-1] epsilon = param["epsilon"] gamma = param["gamma"] n_iter = 0 #Algorithm constants const = compute_constants(param, const, p) if verbose >= 0: print("Sigma:", const["sigma"], "\nTau:", const["tau"]) #Initializing uk = fourier_op.adj_op(p * data) vk = np.copy(uk) wk = linear_op.op(uk) uk_bar = np.copy(uk) norm = 2 * tol #For plots if compute_energy: energy = [] if ground_truth is not None: ssims = [] norms = [] #Main loop t1 = time.time() while n_iter < maxit and norm > tol: uk, vk, wk, uk_bar, norm = step(uk, vk, wk, uk_bar, const, p, pn1, data, param, linear_op, fourier_op, mask_type) n_iter += 1 #Saving informations norms.append(norm) if compute_energy: energy.append( energy_wavelet(uk, p, pn1, data, gamma, epsilon, linear_op, fourier_op)) if ground_truth is not None: ssims.append(ssim(uk, ground_truth)) #Printing if n_iter % 10 == 0 and verbose > 0: if compute_energy: print(n_iter, " iterations:\nCost:", energy[-1], "\nNorm:", norm, "\n") else: print(n_iter, " iterations:\nNorm:", norm, "\n") if verbose >= 0: print("Finished in", time.time() - t1, "seconds.") #Return if compute_energy and ground_truth is not None: return uk, norms, energy, ssims elif ground_truth is not None: return uk, norms, ssims elif compute_energy: return uk, norms, energy else: return uk, norms
import numpy as np from mri.operators import NonCartesianFFT traj = np.load('/volatile/temp_traj.npy') fourier = NonCartesianFFT(traj, (384, 384, 208), 'gpuNUFFT', n_coils=20, smaps=np.ones((20, 384, 384, 208)), osf=2) for i in range(10): print(i) K = fourier.op(np.zeros((384, 384, 208))) I = fourier.adj_op(K)