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_sensitivity_extraction_2D(self): """ This test ensures that the output of the non cartesian kspace extraction is same a that of mimicked cartesian extraction in 2D """ _mask = np.ones((self.N, self.N)) _samples = convert_mask_to_locations(_mask) fourier_op = NonCartesianFFT(samples=_samples, shape=(self.N, self.N)) Img = (np.random.randn(self.num_channel, self.N, self.N) + 1j * np.random.randn(self.num_channel, self.N, self.N)) F_img = np.asarray( [fourier_op.op(Img[i]) for i in np.arange(self.num_channel)]) Smaps_gridding, SOS_Smaps = get_Smaps(k_space=F_img, img_shape=(self.N, self.N), samples=_samples, thresh=(0.4, 0.4), mode='gridding', min_samples=(-0.5, -0.5), max_samples=(0.5, 0.5), n_cpu=1) Smaps_NFFT, SOS_Smaps = get_Smaps(k_space=F_img, img_shape=(self.N, self.N), thresh=(0.4, 0.4), samples=_samples, min_samples=(-0.5, -0.5), max_samples=(0.5, 0.5), mode='NFFT') np.testing.assert_allclose(Smaps_gridding, Smaps_NFFT)
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 grad_L(**kwargs): # -- # -- Compute gradient of L with respect to p # -- # INPUTS: pk: Point where we want to compute the gradient # u0_mat: Ground_truth image # y: kspace data associated to u0_mat # param: list of lower and upper level parameters. Must contain: # - epsilon: weight of L2 norm in lower level reconstruction # - gamma: parameter for approximation of L1 norm # - c: weight of L for upper level cost function # - beta: weight of P for upper level cost function # Remark: You should choose these parameters to have E(pk)~1 at the beginning (otherwire, L-BFGS-B may stop too early) # fourier_op: Fourier operator for lower level reconstruction # linear_op: Linear operator for lower level reconstruction # # max_cgiter (optional): maximum number of Conjugate Gradient iterations (default: 3000) # cgtol (optional): tolerance of Conjugate Gradient iterations (default: 1e-6) # compute_conv (optional): plot convergence if True (default: False) # verbose (optional) # -- Getting parameters max_cgiter = kwargs.get("max_cgiter", 4000) cgtol = kwargs.get("cgtol", 1e-6) compute_conv = kwargs.get("compute_conv", False) mask_type = kwargs.get("mask_type", "") learn_mask = kwargs.get("learn_mask", True) learn_alpha = kwargs.get("learn_alpha", True) u0_mat = kwargs.get("u0_mat", None) param = kwargs.get("param", None) y = kwargs.get("y", None) pk = kwargs.get("pk", None) samples = kwargs.get("samples", []) fourier_op = NonCartesianFFT(samples=samples, shape=u0_mat.shape, implementation='cpu') wavelet_name = kwargs.get("wavelet_name", "") wavelet_scale = kwargs.get("wavelet_scale", 1) linear_op = WaveletN(wavelet_name=wavelet_name, nb_scale=wavelet_scale, padding_mode="periodization") const = kwargs.get("const", {}) verbose = kwargs.get("verbose", 0) cg_conv = [] if u0_mat is None: raise ValueError("A ground truth image u0_mat is needed") if y is None: raise ValueError("kspace data y are needed") n = len(u0_mat) # -- Compute uk from pk with lower level solver if not given if verbose >= 0: print("\nStarting PDHG") uk, _ = pdhg(y, pk, mask_type=mask_type, fourier_op=fourier_op, linear_op=linear_op, param=param, maxit=50, verbose=verbose, const=const) # -- Defining linear operator from pk and uk def mv(w): w_complex = np.reshape(w[:n**2] + 1j * w[n**2:], (n, n)) fx = np.reshape( Du2_Etot(uk, pk, w_complex, eps=param["epsilon"], fourier_op=fourier_op, y=y, linear_op=linear_op, gamma=param["gamma"]), (n**2, )) return np.concatenate([np.real(fx), np.imag(fx)]) lin = LinearOperator((2 * n**2, 2 * n**2), matvec=mv) if verbose >= 0: print("\nStarting Conjugate Gradient method") t1 = time.time() B = np.reshape(Du_L(uk, u0_mat, param["c"]), (n**2, )) B_real = np.concatenate([np.real(B), np.imag(B)]) def cgcall(x): #CG callback function to plot convergence if compute_conv: cg_conv.append( np.linalg.norm(lin(x) - B_real) / np.linalg.norm(B_real)) x_inter, _ = cg(lin, B_real, tol=cgtol, maxiter=max_cgiter, callback=cgcall) if verbose >= 0: print( f"Finished in {time.time()-t1}s - ||Ax-b||/||b||: {np.linalg.norm(lin(x_inter)-B_real)/np.linalg.norm(B_real)}" ) # -- Plotting if compute_conv: plt.plot(cg_conv) plt.yscale("log") plt.title("Convergence of the conjugate gradient") plt.xlabel("Number of iterations") plt.ylabel("||Ax-b||/||b||") #plt.savefig("Upper Level/CG_conv.png") if np.linalg.norm(lin(x_inter) - B_real) / np.linalg.norm(B_real) > 1e-3: return np.zeros(pk.shape) else: return -Dpu_Etot(uk, pk, np.reshape(x_inter[:n**2] + 1j * x_inter[n**2:], (n, n)), eps=param["epsilon"], fourier_op=fourier_op, y=y, linear_op=linear_op, gamma=param["gamma"], learn_mask=learn_mask, learn_alpha=learn_alpha)
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
radial_mask = get_sample_data("mri-radial-samples") kspace_loc = radial_mask.data density_comp = estimate_density_compensation(kspace_loc, image.shape) ############################################################################# # Generate the kspace # ------------------- # # From the 2D brain slice and the acquisition mask, we retrospectively # undersample the k-space using a radial acquisition mask # We then reconstruct using adjoint with and without density compensation # Get the locations of the kspace samples and the associated observations 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()
# View Input # image.show() # mask.show() ############################################################################# # Generate the kspace # ------------------- # # From the 2D brain slice and the acquisition mask, we retrospectively # undersample the k-space using a non cartesian acquisition mask # We then grid the kspace to get the gridded solution as a baseline # Get the kspace observation values for the kspace locations fourier_op = NonCartesianFFT(samples=kspace_loc, shape=image.shape, n_coils=cartesian_ref_image.shape[0], implementation='gpuNUFFT') kspace_obs = fourier_op.op(cartesian_ref_image) # Gridded solution grid_space = np.linspace(-0.5, 0.5, num=image.shape[0]) grid2D = np.meshgrid(grid_space, grid_space) grid_soln = np.asarray([ gridded_inverse_fourier_transform_nd(kspace_loc, kspace_obs_ch, tuple(grid2D), 'linear') for kspace_obs_ch in kspace_obs ]) image_rec0 = pysap.Image(data=np.sqrt(np.sum(np.abs(grid_soln)**2, axis=0))) # image_rec0.show() base_ssim = ssim(image_rec0, image) print('The Base SSIM is : ' + str(base_ssim))
def test_gridsearch_single_channel(self): """Test Gridsearch script in mri.scripts for single channel reconstruction this is a test of sanity and not if the reconstruction is right. """ image = get_sample_data('2d-mri') mask = np.ones(image.shape) kspace_loc = convert_mask_to_locations(mask) fourier_op = NonCartesianFFT(samples=kspace_loc, shape=image.shape) kspace_data = fourier_op.op(image.data) # Define the keyword dictionaries based on convention metrics = { 'ssim': { 'metric': ssim, 'mapping': { 'x_new': 'test', 'y_new': None }, 'cst_kwargs': { 'ref': image, 'mask': None }, 'early_stopping': True, }, } linear_params = { 'init_class': WaveletN, 'kwargs': { 'wavelet_name': 'sym8', 'nb_scale': 4, } } regularizer_params = { 'init_class': SparseThreshold, 'kwargs': { 'linear': Identity(), 'weights': [0, 1e-5], } } optimizer_params = { # Just following convention 'kwargs': { 'optimization_alg': 'fista', 'num_iterations': 10, 'metrics': metrics, } } # Call the launch grid function and obtain results raw_results, test_cases, key_names, best_idx = launch_grid( kspace_data=kspace_data, fourier_op=fourier_op, linear_params=linear_params, regularizer_params=regularizer_params, optimizer_params=optimizer_params, reconstructor_kwargs={'gradient_formulation': 'synthesis'}, reconstructor_class=SingleChannelReconstructor, compare_metric_details={'metric': 'ssim'}, n_jobs=self.n_jobs, verbose=1, ) # In this test we dont undersample the kspace so the # reconstruction is indeed with mu=0, ie best_idx=0 np.testing.assert_equal(best_idx, 0) np.testing.assert_allclose( raw_results[best_idx][0], image, atol=1e-7, )
img_size = [min(real_img.shape)] * 2 square_mask[ real_img_size[0] // 2 - img_size[0] // 2 : real_img_size[0] // 2 + img_size[0] // 2, real_img_size[1] // 2 - img_size[1] // 2 : real_img_size[1] // 2 + img_size[1] // 2, ] = 1 trajectories = sp.io.loadmat("data/NCTrajectories.mat") #%% md # Offline reconstruction #%% print(trajectories) sp = trajectories["sparkling"] plt.figure() for idx in range(20): plt.scatter( sp[idx * 1536 * 2 : (idx + 1) * 1536 * 2 - 1, 0], sp[idx * 1536 * 2 : (idx + 1) * 1536 * 2 - 1, 1], s=1, ) plt.show() fourier_sparkling = NonCartesianFFT( trajectories["sparkling"], shape=full_k.shape[1:], n_coils=full_k.shape[0], implementation="cpu", density_comp=trajectories["sparkling_w"], )
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)
def E(**kwargs): # -- # -- Computes the cost of a given mask or parametrisation of mask -- # -- # INPUTS: images: list of all images used to evaluate the reconstruction # kspace_data: list of noised kspace data associated to these images # param: list of lower and upper level parameters. Must contain: # - epsilon: weight of L2 norm in lower level reconstruction # - gamma: parameter for approximation of L1 norm # - c: weight of L for upper level cost function # - beta: weight of P for upper level cost function # Remark: You should choose these parameters to have E(pk)~1 at the beginning (otherwire, L-BFGS-B may stop too early) # fourier_op: Fourier operator for lower level reconstruction # linear_op: Linear operator for lower level reconstruction # mask_type (optional): learn cartesian mask if mask_type="cartesian", otherwise each point is independant. # Other parametrisations may be implemented later. # pk: initial mask. Mandatory if mask_type!="cartesian" # lk: initial mask parametrisation. Mandatory if mask_type="cartesian" # verbose (optional) # Getting parameters images = kwargs.get("images", None) kspace_data = kwargs.get("kspace_data", None) param = kwargs.get("param", None) samples = kwargs.get("samples", []) fourier_op = NonCartesianFFT(samples=samples, shape=images[0].shape, implementation='cpu') wavelet_name = kwargs.get("wavelet_name", "") wavelet_scale = kwargs.get("wavelet_scale", 1) linear_op = WaveletN(wavelet_name=wavelet_name, nb_scale=wavelet_scale, padding_mode="periodization") parallel = kwargs.get("parallel", False) mask_type = kwargs.get("mask_type", "") lk = kwargs.get("lk", None) pk = kwargs.get("pk", None) verbose = kwargs.get("verbose", 0) if verbose >= 0: print("\n\nEVALUATING E(p)") # Checking inputs errors if images is None or len(images) < 1: raise ValueError("At least one image is needed") if param is None: raise ValueError("Lower level parameters must be given") if len(images) != len(kspace_data): raise ValueError("Need as many images and kspace data") #Compute P(pk/lk) Ep = 0 if mask_type == "cartesian": pk = pcart(lk) Ep = P(lk, param["beta"]) elif mask_type == "radial_CO": n_rad = kwargs.get("n_rad") pk = pradCO(lk, n_rad) Ep = P(lk, param["beta"]) else: Ep = P(pk, param["beta"]) #Compute L(pk/lk) Nimages = len(images) if parallel: uk_list = Parallel(n_jobs=-1, verbose=0)( delayed(pdhg)(kspace_data[i], pk, samples=samples, shape=images[0].shape, wavelet_name=wavelet_name, wavelet_scale=wavelet_scale, param=param, mask_type=mask_type, const=kwargs.get("const", {}), verbose=-1) for i in range(Nimages)) Ep += np.sum( [L(uk_list[i][0], images[i], param["c"]) for i in range(Nimages)]) / Nimages else: for i in range(Nimages): if verbose >= 0: print(f"\nImage {i+1}:") u0_mat, y = images[i], kspace_data[i] if verbose > 0: print("\nStarting PDHG") uk, _ = pdhg(y, pk, maxit=50, fourier_op=fourier_op, linear_op=linear_op, **kwargs) Ep += L(uk, u0_mat, param["c"]) / Nimages return Ep
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
def test_sensitivity_extraction_2D(self): """ This test ensures that the output of the non cartesian kspace extraction is same a that of mimicked cartesian extraction in 2D """ mask = np.ones((self.N, self.N)) samples = convert_mask_to_locations(mask) fourier_op = NonCartesianFFT(samples=samples, shape=(self.N, self.N)) Img = (np.random.randn(self.num_channel, self.N, self.N) + 1j * np.random.randn(self.num_channel, self.N, self.N)) F_img = np.asarray( [fourier_op.op(Img[i]) for i in np.arange(self.num_channel)]) Smaps_gridding, SOS_Smaps = get_Smaps(k_space=F_img, img_shape=(self.N, self.N), samples=samples, thresh=(0.4, 0.4), mode='gridding', min_samples=(-0.5, -0.5), max_samples=(0.5, 0.5), n_cpu=1) Smaps_NFFT_dc, SOS_Smaps_dc = get_Smaps(k_space=F_img, img_shape=(self.N, self.N), thresh=(0.4, 0.4), samples=samples, min_samples=(-0.5, -0.5), max_samples=(0.5, 0.5), mode='NFFT', density_comp=np.ones( samples.shape[0])) Smaps_NFFT, SOS_Smaps = get_Smaps( k_space=F_img, img_shape=(self.N, self.N), thresh=(0.4, 0.4), samples=samples, min_samples=(-0.5, -0.5), max_samples=(0.5, 0.5), mode='NFFT', ) Smaps_hann_NFFT, SOS_Smaps = get_Smaps( k_space=F_img, img_shape=(self.N, self.N), thresh=0.4, samples=samples, min_samples=(-0.5, -0.5), max_samples=(0.5, 0.5), window_fun="Hann", mode='NFFT', ) Smaps_hann_gridding, SOS_Smaps = get_Smaps( k_space=F_img, img_shape=(self.N, self.N), thresh=0.4, samples=samples, min_samples=(-0.5, -0.5), max_samples=(0.5, 0.5), window_fun="Hann", mode='gridding', ) Smaps_hamming_NFFT, SOS_Smaps = get_Smaps( k_space=F_img, img_shape=(self.N, self.N), thresh=0.4, samples=samples, min_samples=(-0.5, -0.5), max_samples=(0.5, 0.5), window_fun="Hamming", mode='NFFT', ) Smaps_hamming_gridding, SOS_Smaps = get_Smaps( k_space=F_img, img_shape=(self.N, self.N), thresh=0.4, samples=samples, min_samples=(-0.5, -0.5), max_samples=(0.5, 0.5), window_fun="Hamming", mode='gridding', ) Smaps_call_gridding, SOS_Smaps = get_Smaps( k_space=F_img, img_shape=(self.N, self.N), thresh=0.4, samples=samples, min_samples=(-0.5, -0.5), max_samples=(0.5, 0.5), window_fun=lambda x: 1, mode='gridding', ) Smaps_call_NFFT, SOS_Smaps = get_Smaps( k_space=F_img, img_shape=(self.N, self.N), thresh=0.4, samples=samples, min_samples=(-0.5, -0.5), max_samples=(0.5, 0.5), window_fun=lambda x: 1, mode='NFFT', ) np.testing.assert_allclose(Smaps_gridding, Smaps_NFFT_dc) np.testing.assert_allclose(Smaps_gridding, Smaps_NFFT) np.testing.assert_allclose(Smaps_hann_gridding, Smaps_hann_NFFT) np.testing.assert_allclose(Smaps_hamming_gridding, Smaps_hamming_NFFT) np.testing.assert_allclose(Smaps_call_gridding, Smaps_call_NFFT) # Test that we raise assert for bad mode np.testing.assert_raises(ValueError, get_Smaps, k_space=F_img, img_shape=(self.N, self.N), thresh=(0.4, 0.4), samples=samples, min_samples=(-0.5, -0.5), max_samples=(0.5, 0.5), mode='test') # Test that we raise assert for bad window np.testing.assert_raises( ValueError, get_Smaps, k_space=F_img, img_shape=(self.N, self.N), thresh=(0.4, 0.4), samples=samples, min_samples=(-0.5, -0.5), max_samples=(0.5, 0.5), window_fun='test', mode='gridding', )
# View Input # image.show() # mask.show() ############################################################################# # Generate the kspace # ------------------- # # From the 2D brain slice and the acquisition mask, we retrospectively # undersample the k-space using a radial acquisition mask # We then reconstruct the zero order solution as a baseline # Get the locations of the kspace samples and the associated observations fourier_op = NonCartesianFFT(samples=kspace_loc, shape=image.shape, implementation='cpu') kspace_obs = fourier_op.op(image.data) # Gridded solution grid_space = np.linspace(-0.5, 0.5, num=image.shape[0]) grid2D = np.meshgrid(grid_space, grid_space) grid_soln = gridded_inverse_fourier_transform_nd(kspace_loc, kspace_obs, tuple(grid2D), 'linear') image_rec0 = pysap.Image(data=grid_soln) # image_rec0.show() base_ssim = ssim(image_rec0, image) print('The Base SSIM is : ' + str(base_ssim)) ############################################################################# # FISTA optimization