def __init__(self, data, model, sphere, sh_order=None, tol=1e-2): if sh_order is None: if hasattr(model, "sh_order"): sh_order = model.sh_order else: sh_order = default_SH self.where_dwi = shm.lazy_index(~model.gtab.b0s_mask) if not isinstance(self.where_dwi, slice): msg = ("For optimal bootstrap tracking consider reordering the " "diffusion volumes so that all the b0 volumes are at the " "beginning") warn(msg) x, y, z = model.gtab.gradients[self.where_dwi].T r, theta, phi = cart2sphere(x, y, z) b_range = (r.max() - r.min()) / r.min() if b_range > tol: raise ValueError("BootOdfGen only supports single shell data") B, m, n = shm.real_sym_sh_basis(sh_order, theta, phi) H = shm.hat(B) R = shm.lcr_matrix(H) self.data = np.asarray(data, "float64") self.model = model self.sphere = sphere self.H = H self.R = R
def sh_smooth(data, gtab, sh_order=4): """Smooth the raw diffusion signal with spherical harmonics data : ndarray The diffusion data to smooth. gtab : gradient table object Corresponding gradients table object to data. sh_order : int, default 4 Order of the spherical harmonics to fit. Return --------- pred_sig : ndarray The smoothed diffusion data, fitted through spherical harmonics. """ m, n = sph_harm_ind_list(sh_order) where_b0s = lazy_index(gtab.b0s_mask) where_dwi = lazy_index(~gtab.b0s_mask) x, y, z = gtab.gradients[where_dwi].T r, theta, phi = cart2sphere(x, y, z) # Find the sh coefficients to smooth the signal B_dwi = real_sph_harm(m, n, theta[:, None], phi[:, None]) sh_shape = (np.prod(data.shape[:-1]), -1) sh_coeff = np.linalg.lstsq(B_dwi, data[..., where_dwi].reshape(sh_shape).T)[0] # Find the smoothed signal from the sh fit for the given gtab smoothed_signal = np.dot(B_dwi, sh_coeff).T.reshape(data.shape[:-1] + (-1, )) pred_sig = np.zeros(smoothed_signal.shape[:-1] + (gtab.bvals.shape[0], )) pred_sig[..., ~gtab.b0s_mask] = smoothed_signal # Just give back the signal for the b0s since we can't really do anything about it if np.sum(gtab.b0s_mask) > 1: pred_sig[..., where_b0s] = np.mean(data[..., where_b0s], axis=-1) else: pred_sig[..., where_b0s] = data[..., where_b0s] return pred_sig
def recursive_response(gtab, data, mask=None, sh_order=8, peak_thr=0.01, init_fa=0.08, init_trace=0.0021, iter=8, convergence=0.001, parallel=True, nbr_processes=None, sphere=default_sphere): """ Recursive calibration of response function using peak threshold Parameters ---------- gtab : GradientTable data : ndarray diffusion data mask : ndarray, optional mask for recursive calibration, for example a white matter mask. It has shape `data.shape[0:3]` and dtype=bool. Default: use the entire data array. sh_order : int, optional maximal spherical harmonics order. Default: 8 peak_thr : float, optional peak threshold, how large the second peak can be relative to the first peak in order to call it a single fiber population [1]. Default: 0.01 init_fa : float, optional FA of the initial 'fat' response function (tensor). Default: 0.08 init_trace : float, optional trace of the initial 'fat' response function (tensor). Default: 0.0021 iter : int, optional maximum number of iterations for calibration. Default: 8. convergence : float, optional convergence criterion, maximum relative change of SH coefficients. Default: 0.001. parallel : bool, optional Whether to use parallelization in peak-finding during the calibration procedure. Default: True nbr_processes: int If `parallel` is True, the number of subprocesses to use (default multiprocessing.cpu_count()). sphere : Sphere, optional. The sphere used for peak finding. Default: default_sphere. Returns ------- response : ndarray response function in SH coefficients Notes ----- In CSD there is an important pre-processing step: the estimation of the fiber response function. Using an FA threshold is not a very robust method. It is dependent on the dataset (non-informed used subjectivity), and still depends on the diffusion tensor (FA and first eigenvector), which has low accuracy at high b-value. This function recursively calibrates the response function, for more information see [1]. References ---------- .. [1] Tax, C.M.W., et al. NeuroImage 2014. Recursive calibration of the fiber response function for spherical deconvolution of diffusion MRI data. """ S0 = 1. evals = fa_trace_to_lambdas(init_fa, init_trace) res_obj = (evals, S0) if mask is None: data = data.reshape(-1, data.shape[-1]) else: data = data[mask] n = np.arange(0, sh_order + 1, 2) where_dwi = lazy_index(~gtab.b0s_mask) response_p = np.ones(len(n)) for _ in range(iter): r_sh_all = np.zeros(len(n)) csd_model = ConstrainedSphericalDeconvModel(gtab, res_obj, sh_order=sh_order) csd_peaks = peaks_from_model(model=csd_model, data=data, sphere=sphere, relative_peak_threshold=peak_thr, min_separation_angle=25, parallel=parallel, nbr_processes=nbr_processes) dirs = csd_peaks.peak_dirs vals = csd_peaks.peak_values single_peak_mask = (vals[:, 1] / vals[:, 0]) < peak_thr data = data[single_peak_mask] dirs = dirs[single_peak_mask] for num_vox in range(data.shape[0]): rotmat = vec2vec_rotmat(dirs[num_vox, 0], np.array([0, 0, 1])) rot_gradients = np.dot(rotmat, gtab.gradients.T).T x, y, z = rot_gradients[where_dwi].T r, theta, phi = cart2sphere(x, y, z) # for the gradient sphere B_dwi = real_sph_harm(0, n, theta[:, None], phi[:, None]) r_sh_all += np.linalg.lstsq(B_dwi, data[num_vox, where_dwi], rcond=-1)[0] response = r_sh_all / data.shape[0] res_obj = AxSymShResponse(data[:, gtab.b0s_mask].mean(), response) change = abs((response_p - response) / response_p) if all(change < convergence): break response_p = response return res_obj
def __init__(self, gtab, response, reg_sphere=None, sh_order=8, lambda_=1, tau=0.1, convergence=50): r""" Constrained Spherical Deconvolution (CSD) [1]_. Spherical deconvolution computes a fiber orientation distribution (FOD), also called fiber ODF (fODF) [2]_, as opposed to a diffusion ODF as the QballModel or the CsaOdfModel. This results in a sharper angular profile with better angular resolution that is the best object to be used for later deterministic and probabilistic tractography [3]_. A sharp fODF is obtained because a single fiber *response* function is injected as *a priori* knowledge. The response function is often data-driven and is thus provided as input to the ConstrainedSphericalDeconvModel. It will be used as deconvolution kernel, as described in [1]_. Parameters ---------- gtab : GradientTable response : tuple or AxSymShResponse object A tuple with two elements. The first is the eigen-values as an (3,) ndarray and the second is the signal value for the response function without diffusion weighting. This is to be able to generate a single fiber synthetic signal. The response function will be used as deconvolution kernel ([1]_) reg_sphere : Sphere (optional) sphere used to build the regularization B matrix. Default: 'symmetric362'. sh_order : int (optional) maximal spherical harmonics order. Default: 8 lambda_ : float (optional) weight given to the constrained-positivity regularization part of the deconvolution equation (see [1]_). Default: 1 tau : float (optional) threshold controlling the amplitude below which the corresponding fODF is assumed to be zero. Ideally, tau should be set to zero. However, to improve the stability of the algorithm, tau is set to tau*100 % of the mean fODF amplitude (here, 10% by default) (see [1]_). Default: 0.1 convergence : int Maximum number of iterations to allow the deconvolution to converge. References ---------- .. [1] Tournier, J.D., et al. NeuroImage 2007. Robust determination of the fibre orientation distribution in diffusion MRI: Non-negativity constrained super-resolved spherical deconvolution .. [2] Descoteaux, M., et al. IEEE TMI 2009. Deterministic and Probabilistic Tractography Based on Complex Fibre Orientation Distributions .. [3] Côté, M-A., et al. Medical Image Analysis 2013. Tractometer: Towards validation of tractography pipelines .. [4] Tournier, J.D, et al. Imaging Systems and Technology 2012. MRtrix: Diffusion Tractography in Crossing Fiber Regions """ # Initialize the parent class: SphHarmModel.__init__(self, gtab) m, n = sph_harm_ind_list(sh_order) self.m, self.n = m, n self._where_b0s = lazy_index(gtab.b0s_mask) self._where_dwi = lazy_index(~gtab.b0s_mask) no_params = ((sh_order + 1) * (sh_order + 2)) / 2 if no_params > np.sum(~gtab.b0s_mask): msg = "Number of parameters required for the fit are more " msg += "than the actual data points" warnings.warn(msg, UserWarning) x, y, z = gtab.gradients[self._where_dwi].T r, theta, phi = cart2sphere(x, y, z) # for the gradient sphere self.B_dwi = real_sph_harm(m, n, theta[:, None], phi[:, None]) # for the sphere used in the regularization positivity constraint if reg_sphere is None: self.sphere = small_sphere else: self.sphere = reg_sphere r, theta, phi = cart2sphere(self.sphere.x, self.sphere.y, self.sphere.z) self.B_reg = real_sph_harm(m, n, theta[:, None], phi[:, None]) if response is None: response = (np.array([0.0015, 0.0003, 0.0003]), 1) self.response = response if isinstance(response, AxSymShResponse): r_sh = response.dwi_response self.response_scaling = response.S0 n_response = response.n m_response = response.m else: self.S_r = estimate_response(gtab, self.response[0], self.response[1]) r_sh = np.linalg.lstsq(self.B_dwi, self.S_r[self._where_dwi], rcond=-1)[0] n_response = n m_response = m self.response_scaling = response[1] r_rh = sh_to_rh(r_sh, m_response, n_response) self.R = forward_sdeconv_mat(r_rh, n) # scale lambda_ to account for differences in the number of # SH coefficients and number of mapped directions # This is exactly what is done in [4]_ lambda_ = (lambda_ * self.R.shape[0] * r_rh[0] / (np.sqrt(self.B_reg.shape[0]) * np.sqrt(362.))) self.B_reg *= lambda_ self.sh_order = sh_order self.tau = tau self.convergence = convergence self._X = X = self.R.diagonal() * self.B_dwi self._P = np.dot(X.T, X)
def __init__(self, gtab, ratio, reg_sphere=None, sh_order=8, lambda_=1., tau=0.1): r""" Spherical Deconvolution Transform (SDT) [1]_. The SDT computes a fiber orientation distribution (FOD) as opposed to a diffusion ODF as the QballModel or the CsaOdfModel. This results in a sharper angular profile with better angular resolution. The Constrained SDTModel is similar to the Constrained CSDModel but mathematically it deconvolves the q-ball ODF as oppposed to the HARDI signal (see [1]_ for a comparison and a through discussion). A sharp fODF is obtained because a single fiber *response* function is injected as *a priori* knowledge. In the SDTModel, this response is a single fiber q-ball ODF as opposed to a single fiber signal function for the CSDModel. The response function will be used as deconvolution kernel. Parameters ---------- gtab : GradientTable ratio : float ratio of the smallest vs the largest eigenvalue of the single prolate tensor response function reg_sphere : Sphere sphere used to build the regularization B matrix sh_order : int maximal spherical harmonics order lambda_ : float weight given to the constrained-positivity regularization part of the deconvolution equation tau : float threshold (tau *mean(fODF)) controlling the amplitude below which the corresponding fODF is assumed to be zero. References ---------- .. [1] Descoteaux, M., et al. IEEE TMI 2009. Deterministic and Probabilistic Tractography Based on Complex Fibre Orientation Distributions. """ SphHarmModel.__init__(self, gtab) m, n = sph_harm_ind_list(sh_order) self.m, self.n = m, n self._where_b0s = lazy_index(gtab.b0s_mask) self._where_dwi = lazy_index(~gtab.b0s_mask) no_params = ((sh_order + 1) * (sh_order + 2)) / 2 if no_params > np.sum(~gtab.b0s_mask): msg = "Number of parameters required for the fit are more " msg += "than the actual data points" warnings.warn(msg, UserWarning) x, y, z = gtab.gradients[self._where_dwi].T r, theta, phi = cart2sphere(x, y, z) # for the gradient sphere self.B_dwi = real_sph_harm(m, n, theta[:, None], phi[:, None]) # for the odf sphere if reg_sphere is None: self.sphere = get_sphere('symmetric362') else: self.sphere = reg_sphere r, theta, phi = cart2sphere(self.sphere.x, self.sphere.y, self.sphere.z) self.B_reg = real_sph_harm(m, n, theta[:, None], phi[:, None]) self.R, self.P = forward_sdt_deconv_mat(ratio, n) # scale lambda_ to account for differences in the number of # SH coefficients and number of mapped directions self.lambda_ = (lambda_ * self.R.shape[0] * self.R[0, 0] / self.B_reg.shape[0]) self.tau = tau self.sh_order = sh_order
def __init__(self, gtab, response, reg_sphere=None, sh_order=8, lambda_=1, tau=0.1): r""" Constrained Spherical Deconvolution (CSD) [1]_. Spherical deconvolution computes a fiber orientation distribution (FOD), also called fiber ODF (fODF) [2]_, as opposed to a diffusion ODF as the QballModel or the CsaOdfModel. This results in a sharper angular profile with better angular resolution that is the best object to be used for later deterministic and probabilistic tractography [3]_. A sharp fODF is obtained because a single fiber *response* function is injected as *a priori* knowledge. The response function is often data-driven and thus, comes as input to the ConstrainedSphericalDeconvModel. It will be used as deconvolution kernel, as described in [1]_. Parameters ---------- gtab : GradientTable response : tuple or callable If tuple, then it should have two elements. The first is the eigen-values as an (3,) ndarray and the second is the signal value for the response function without diffusion weighting. This is to be able to generate a single fiber synthetic signal. If callable then the function should return an ndarray with the all the signal values for the response function. The response function will be used as deconvolution kernel ([1]_) reg_sphere : Sphere sphere used to build the regularization B matrix sh_order : int maximal spherical harmonics order lambda_ : float weight given to the constrained-positivity regularization part of the deconvolution equation (see [1]_) tau : float threshold controlling the amplitude below which the corresponding fODF is assumed to be zero. Ideally, tau should be set to zero. However, to improve the stability of the algorithm, tau is set to tau*100 % of the mean fODF amplitude (here, 10% by default) (see [1]_) References ---------- .. [1] Tournier, J.D., et al. NeuroImage 2007. Robust determination of the fibre orientation distribution in diffusion MRI: Non-negativity constrained super-resolved spherical deconvolution .. [2] Descoteaux, M., et al. IEEE TMI 2009. Deterministic and Probabilistic Tractography Based on Complex Fibre Orientation Distributions .. [3] C\^ot\'e, M-A., et al. Medical Image Analysis 2013. Tractometer: Towards validation of tractography pipelines .. [4] Tournier, J.D, et al. Imaging Systems and Technology 2012. MRtrix: Diffusion Tractography in Crossing Fiber Regions """ m, n = sph_harm_ind_list(sh_order) self.m, self.n = m, n self._where_b0s = lazy_index(gtab.b0s_mask) self._where_dwi = lazy_index(~gtab.b0s_mask) no_params = ((sh_order + 1) * (sh_order + 2)) / 2 if no_params > np.sum(gtab.b0s_mask == False): msg = "Number of parameters required for the fit are more " msg += "than the actual data points" warnings.warn(msg, UserWarning) x, y, z = gtab.gradients[self._where_dwi].T r, theta, phi = cart2sphere(x, y, z) # for the gradient sphere self.B_dwi = real_sph_harm(m, n, theta[:, None], phi[:, None]) # for the sphere used in the regularization positivity constraint if reg_sphere is None: self.sphere = get_sphere('symmetric362') else: self.sphere = reg_sphere r, theta, phi = cart2sphere(self.sphere.x, self.sphere.y, self.sphere.z) self.B_reg = real_sph_harm(m, n, theta[:, None], phi[:, None]) if callable(response): S_r = response else: if response is None: S_r = estimate_response(gtab, np.array([0.0015, 0.0003, 0.0003]), 1) else: S_r = estimate_response(gtab, response[0], response[1]) r_sh = np.linalg.lstsq(self.B_dwi, S_r[self._where_dwi])[0] r_rh = sh_to_rh(r_sh, sh_order) self.R = forward_sdeconv_mat(r_rh, sh_order) # scale lambda_ to account for differences in the number of # SH coefficients and number of mapped directions # This is exactly what is done in [4]_ self.lambda_ = lambda_ * self.R.shape[0] * r_rh[0] / self.B_reg.shape[0] self.sh_order = sh_order self.tau = tau
def __init__(self, gtab, ratio, reg_sphere=None, sh_order=8, lambda_=1., tau=0.1): r""" Spherical Deconvolution Transform (SDT) [1]_. The SDT computes a fiber orientation distribution (FOD) as opposed to a diffusion ODF as the QballModel or the CsaOdfModel. This results in a sharper angular profile with better angular resolution. The Contrained SDTModel is similar to the Constrained CSDModel but mathematically it deconvolves the q-ball ODF as oppposed to the HARDI signal (see [1]_ for a comparison and a through discussion). A sharp fODF is obtained because a single fiber *response* function is injected as *a priori* knowledge. In the SDTModel, this response is a single fiber q-ball ODF as opposed to a single fiber signal function for the CSDModel. The response function will be used as deconvolution kernel. Parameters ---------- gtab : GradientTable ratio : float ratio of the smallest vs the largest eigenvalue of the single prolate tensor response function reg_sphere : Sphere sphere used to build the regularization B matrix sh_order : int maximal spherical harmonics order lambda_ : float weight given to the constrained-positivity regularization part of the deconvolution equation tau : float threshold (tau *mean(fODF)) controlling the amplitude below which the corresponding fODF is assumed to be zero. References ---------- .. [1] Descoteaux, M., et al. IEEE TMI 2009. Deterministic and Probabilistic Tractography Based on Complex Fibre Orientation Distributions. """ m, n = sph_harm_ind_list(sh_order) self.m, self.n = m, n self._where_b0s = lazy_index(gtab.b0s_mask) self._where_dwi = lazy_index(~gtab.b0s_mask) no_params = ((sh_order + 1) * (sh_order + 2)) / 2 if no_params > np.sum(gtab.b0s_mask == False): msg = "Number of parameters required for the fit are more " msg += "than the actual data points" warnings.warn(msg, UserWarning) x, y, z = gtab.gradients[self._where_dwi].T r, theta, phi = cart2sphere(x, y, z) # for the gradient sphere self.B_dwi = real_sph_harm(m, n, theta[:, None], phi[:, None]) # for the odf sphere if reg_sphere is None: self.sphere = get_sphere('symmetric362') else: self.sphere = reg_sphere r, theta, phi = cart2sphere(self.sphere.x, self.sphere.y, self.sphere.z) self.B_reg = real_sph_harm(m, n, theta[:, None], phi[:, None]) self.R, self.P = forward_sdt_deconv_mat(ratio, sh_order) # scale lambda_ to account for differences in the number of # SH coefficients and number of mapped directions self.lambda_ = lambda_ * self.R.shape[0] * self.R[0, 0] / self.B_reg.shape[0] self.tau = tau self.sh_order = sh_order
def test_recursive_response_calibration(): """ Test the recursive response calibration method. """ SNR = 100 S0 = 1 sh_order = 8 _, fbvals, fbvecs = get_data('small_64D') bvals = np.load(fbvals) bvecs = np.load(fbvecs) sphere = get_sphere('symmetric724') gtab = gradient_table(bvals, bvecs) evals = np.array([0.0015, 0.0003, 0.0003]) evecs = np.array([[0, 1, 0], [0, 0, 1], [1, 0, 0]]).T mevals = np.array(([0.0015, 0.0003, 0.0003], [0.0015, 0.0003, 0.0003])) angles = [(0, 0), (90, 0)] where_dwi = lazy_index(~gtab.b0s_mask) S_cross, sticks_cross = multi_tensor(gtab, mevals, S0, angles=angles, fractions=[50, 50], snr=SNR) S_single = single_tensor(gtab, S0, evals, evecs, snr=SNR) data = np.concatenate((np.tile(S_cross, (8, 1)), np.tile(S_single, (2, 1))), axis=0) odf_gt_cross = multi_tensor_odf(sphere.vertices, mevals, angles, [50, 50]) odf_gt_single = single_tensor_odf(sphere.vertices, evals, evecs) response = recursive_response(gtab, data, mask=None, sh_order=8, peak_thr=0.01, init_fa=0.05, init_trace=0.0021, iter=8, convergence=0.001, parallel=False) csd = ConstrainedSphericalDeconvModel(gtab, response) csd_fit = csd.fit(data) assert_equal(np.all(csd_fit.shm_coeff[:, 0] >= 0), True) fodf = csd_fit.odf(sphere) directions_gt_single, _, _ = peak_directions(odf_gt_single, sphere) directions_gt_cross, _, _ = peak_directions(odf_gt_cross, sphere) directions_single, _, _ = peak_directions(fodf[8, :], sphere) directions_cross, _, _ = peak_directions(fodf[0, :], sphere) ang_sim = angular_similarity(directions_cross, directions_gt_cross) assert_equal(ang_sim > 1.9, True) assert_equal(directions_cross.shape[0], 2) assert_equal(directions_gt_cross.shape[0], 2) ang_sim = angular_similarity(directions_single, directions_gt_single) assert_equal(ang_sim > 0.9, True) assert_equal(directions_single.shape[0], 1) assert_equal(directions_gt_single.shape[0], 1) sphere = Sphere(xyz=gtab.gradients[where_dwi]) sf = response.on_sphere(sphere) S = np.concatenate(([response.S0], sf)) tenmodel = dti.TensorModel(gtab, min_signal=0.001) tenfit = tenmodel.fit(S) FA = fractional_anisotropy(tenfit.evals) FA_gt = fractional_anisotropy(evals) assert_almost_equal(FA, FA_gt, 1)
def recursive_response(gtab, data, mask=None, sh_order=8, peak_thr=0.01, init_fa=0.08, init_trace=0.0021, iter=8, convergence=0.001, parallel=True, nbr_processes=None, sphere=default_sphere): """ Recursive calibration of response function using peak threshold Parameters ---------- gtab : GradientTable data : ndarray diffusion data mask : ndarray, optional mask for recursive calibration, for example a white matter mask. It has shape `data.shape[0:3]` and dtype=bool. Default: use the entire data array. sh_order : int, optional maximal spherical harmonics order. Default: 8 peak_thr : float, optional peak threshold, how large the second peak can be relative to the first peak in order to call it a single fiber population [1]. Default: 0.01 init_fa : float, optional FA of the initial 'fat' response function (tensor). Default: 0.08 init_trace : float, optional trace of the initial 'fat' response function (tensor). Default: 0.0021 iter : int, optional maximum number of iterations for calibration. Default: 8. convergence : float, optional convergence criterion, maximum relative change of SH coefficients. Default: 0.001. parallel : bool, optional Whether to use parallelization in peak-finding during the calibration procedure. Default: True nbr_processes: int If `parallel` is True, the number of subprocesses to use (default multiprocessing.cpu_count()). sphere : Sphere, optional. The sphere used for peak finding. Default: default_sphere. Returns ------- response : ndarray response function in SH coefficients Notes ----- In CSD there is an important pre-processing step: the estimation of the fiber response function. Using an FA threshold is not a very robust method. It is dependent on the dataset (non-informed used subjectivity), and still depends on the diffusion tensor (FA and first eigenvector), which has low accuracy at high b-value. This function recursively calibrates the response function, for more information see [1]. References ---------- .. [1] Tax, C.M.W., et al. NeuroImage 2014. Recursive calibration of the fiber response function for spherical deconvolution of diffusion MRI data. """ S0 = 1. evals = fa_trace_to_lambdas(init_fa, init_trace) res_obj = (evals, S0) if mask is None: data = data.reshape(-1, data.shape[-1]) else: data = data[mask] n = np.arange(0, sh_order + 1, 2) where_dwi = lazy_index(~gtab.b0s_mask) response_p = np.ones(len(n)) for num_it in range(iter): r_sh_all = np.zeros(len(n)) csd_model = ConstrainedSphericalDeconvModel(gtab, res_obj, sh_order=sh_order) csd_peaks = peaks_from_model(model=csd_model, data=data, sphere=sphere, relative_peak_threshold=peak_thr, min_separation_angle=25, parallel=parallel, nbr_processes=nbr_processes) dirs = csd_peaks.peak_dirs vals = csd_peaks.peak_values single_peak_mask = (vals[:, 1] / vals[:, 0]) < peak_thr data = data[single_peak_mask] dirs = dirs[single_peak_mask] for num_vox in range(data.shape[0]): rotmat = vec2vec_rotmat(dirs[num_vox, 0], np.array([0, 0, 1])) rot_gradients = np.dot(rotmat, gtab.gradients.T).T x, y, z = rot_gradients[where_dwi].T r, theta, phi = cart2sphere(x, y, z) # for the gradient sphere B_dwi = real_sph_harm(0, n, theta[:, None], phi[:, None]) r_sh_all += np.linalg.lstsq(B_dwi, data[num_vox, where_dwi])[0] response = r_sh_all / data.shape[0] res_obj = AxSymShResponse(data[:, gtab.b0s_mask].mean(), response) change = abs((response_p - response) / response_p) if all(change < convergence): break response_p = response return res_obj
def test_recursive_response_calibration(): """ Test the recursive response calibration method. """ SNR = 100 S0 = 1 _, fbvals, fbvecs = get_fnames('small_64D') bvals, bvecs = read_bvals_bvecs(fbvals, fbvecs) sphere = default_sphere gtab = gradient_table(bvals, bvecs) evals = np.array([0.0015, 0.0003, 0.0003]) evecs = np.array([[0, 1, 0], [0, 0, 1], [1, 0, 0]]).T mevals = np.array(([0.0015, 0.0003, 0.0003], [0.0015, 0.0003, 0.0003])) angles = [(0, 0), (90, 0)] where_dwi = lazy_index(~gtab.b0s_mask) S_cross, _ = multi_tensor(gtab, mevals, S0, angles=angles, fractions=[50, 50], snr=SNR) S_single = single_tensor(gtab, S0, evals, evecs, snr=SNR) data = np.concatenate((np.tile(S_cross, (8, 1)), np.tile(S_single, (2, 1))), axis=0) odf_gt_cross = multi_tensor_odf(sphere.vertices, mevals, angles, [50, 50]) odf_gt_single = single_tensor_odf(sphere.vertices, evals, evecs) with warnings.catch_warnings(): warnings.filterwarnings("ignore", message=descoteaux07_legacy_msg, category=PendingDeprecationWarning) response = recursive_response(gtab, data, mask=None, sh_order=8, peak_thr=0.01, init_fa=0.05, init_trace=0.0021, iter=8, convergence=0.001, parallel=False) with warnings.catch_warnings(): warnings.filterwarnings("ignore", message=descoteaux07_legacy_msg, category=PendingDeprecationWarning) csd = ConstrainedSphericalDeconvModel(gtab, response) csd_fit = csd.fit(data) assert_equal(np.all(csd_fit.shm_coeff[:, 0] >= 0), True) with warnings.catch_warnings(): warnings.filterwarnings("ignore", message=descoteaux07_legacy_msg, category=PendingDeprecationWarning) fodf = csd_fit.odf(sphere) directions_gt_single, _, _ = peak_directions(odf_gt_single, sphere) directions_gt_cross, _, _ = peak_directions(odf_gt_cross, sphere) directions_single, _, _ = peak_directions(fodf[8, :], sphere) directions_cross, _, _ = peak_directions(fodf[0, :], sphere) ang_sim = angular_similarity(directions_cross, directions_gt_cross) assert_equal(ang_sim > 1.9, True) assert_equal(directions_cross.shape[0], 2) assert_equal(directions_gt_cross.shape[0], 2) ang_sim = angular_similarity(directions_single, directions_gt_single) assert_equal(ang_sim > 0.9, True) assert_equal(directions_single.shape[0], 1) assert_equal(directions_gt_single.shape[0], 1) with warnings.catch_warnings(record=True) as w: sphere = Sphere(xyz=gtab.gradients[where_dwi]) npt.assert_equal(len(w), 1) npt.assert_(issubclass(w[0].category, UserWarning)) npt.assert_("Vertices are not on the unit sphere" in str(w[0].message)) with warnings.catch_warnings(): warnings.filterwarnings("ignore", message=descoteaux07_legacy_msg, category=PendingDeprecationWarning) sf = response.on_sphere(sphere) S = np.concatenate(([response.S0], sf)) tenmodel = TensorModel(gtab, min_signal=0.001) tenfit = tenmodel.fit(S) FA = fractional_anisotropy(tenfit.evals) FA_gt = fractional_anisotropy(evals) assert_almost_equal(FA, FA_gt, 1)
def generate_kernel(gtab, sphere, wm_response, gm_response, csf_response): ''' Generate deconvolution kernel Compute kernel mapping orientation densities of white matter fiber populations (along each vertex of the sphere) and isotropic volume fractions to a diffusion weighted signal. Parameters ---------- gtab : GradientTable sphere : Sphere Sphere with which to sample discrete fiber orientations in order to construct kernel wm_response : 1d ndarray or 2d ndarray or AxSymShResponse, optional Tensor eigenvalues as a (3,) ndarray, multishell eigenvalues as a (len(unique_bvals_tolerance(gtab.bvals))-1, 3) ndarray in order of smallest to largest b-value, or an AxSymShResponse. gm_response : float, optional Mean diffusivity for GM compartment. If `None`, then grey matter compartment set to all zeros. csf_response : float, optional Mean diffusivity for CSF compartment. If `None`, then CSF compartment set to all zeros. Returns ------- kernel : 2d ndarray (N, M) Computed kernel; can be multiplied with a vector consisting of volume fractions for each of M-2 fiber populations as well as GM and CSF fractions to produce a diffusion weighted signal. ''' # Coordinates of sphere vertices sticks = sphere.vertices n_grad = len(gtab.gradients) # number of gradient directions n_wm_comp = sticks.shape[0] # number of fiber populations n_comp = n_wm_comp + 2 # plus isotropic compartments kernel = np.zeros((n_grad, n_comp)) # White matter compartments list_bvals = unique_bvals_tolerance(gtab.bvals) n_bvals = len(list_bvals) - 1 # number of unique b-values if isinstance(wm_response, AxSymShResponse): # Data-driven response where_dwi = lazy_index(~gtab.b0s_mask) gradients = gtab.gradients[where_dwi] gradients = gradients / np.linalg.norm(gradients, axis=1)[..., None] S0 = wm_response.S0 for i in range(n_wm_comp): # Response oriented along [0, 0, 1], so must rotate sticks[i] rot_mat = vec2vec_rotmat(sticks[i], np.array([0, 0, 1])) rot_gradients = np.dot(rot_mat, gradients.T).T rot_sphere = Sphere(xyz=rot_gradients) # Project onto rotated sphere and scale rot_response = wm_response.on_sphere(rot_sphere) / S0 kernel[where_dwi, i] = rot_response # Set b0 components kernel[gtab.b0s_mask, :] = 1 elif wm_response.shape == (n_bvals, 3): # Multi-shell response bvals = gtab.bvals bvecs = gtab.bvecs for n, bval in enumerate(list_bvals[1:]): indices = get_bval_indices(bvals, bval) with warnings.catch_warnings(): # extract relevant b-value warnings.simplefilter("ignore") gtab_sub = gradient_table(bvals[indices], bvecs[indices]) for i in range(n_wm_comp): # Signal generated by WM-fiber for each gradient direction S = single_tensor(gtab_sub, evals=wm_response[n], evecs=all_tensor_evecs(sticks[i])) kernel[indices, i] = S # Set b0 components b0_indices = get_bval_indices(bvals, list_bvals[0]) kernel[b0_indices, :] = 1 else: # Single-shell response for i in range(n_wm_comp): # Signal generated by WM-fiber for each gradient direction S = single_tensor(gtab, evals=wm_response, evecs=all_tensor_evecs(sticks[i])) kernel[:, i] = S # Set b0 components kernel[gtab.b0s_mask, :] = 1 # GM compartment if gm_response is None: S_gm = np.zeros((n_grad)) else: S_gm = \ single_tensor(gtab, evals=np.array( [gm_response, gm_response, gm_response])) if csf_response is None: S_csf = np.zeros((n_grad)) else: S_csf = \ single_tensor(gtab, evals=np.array( [csf_response, csf_response, csf_response])) kernel[:, n_comp - 2] = S_gm kernel[:, n_comp - 1] = S_csf return kernel
def __init__(self, gtab, wm_response=np.array([1.7e-3, 0.2e-3, 0.2e-3]), gm_response=0.8e-3, csf_response=3.0e-3, n_iter=600, recon_type='smf', n_coils=1, R=1, voxelwise=True, use_tv=False, sphere=None, verbose=False): ''' Robust and Unbiased Model-BAsed Spherical Deconvolution (RUMBA-SD) [1]_ Modification of the Richardson-Lucy algorithm accounting for Rician and Noncentral Chi noise distributions, which more accurately represent MRI noise. Computes a maximum likelihood estimation of the fiber orientation density function (fODF) at each voxel. Includes white matter compartments alongside optional GM and CSF compartments to account for partial volume effects. This fit can be performed voxelwise or globally. The global fit will proceed more quickly than the voxelwise fit provided that the computer has adequate RAM (>= 16 GB should be sufficient for most datasets). Kernel for deconvolution constructed using a priori knowledge of white matter response function, as well as the mean diffusivity of GM and/or CSF. RUMBA-SD is robust against impulse response imprecision, and thus the default diffusivity values are often adequate [2]_. Parameters ---------- gtab : GradientTable wm_response : 1d ndarray or 2d ndarray or AxSymShResponse, optional Tensor eigenvalues as a (3,) ndarray, multishell eigenvalues as a (len(unique_bvals_tolerance(gtab.bvals))-1, 3) ndarray in order of smallest to largest b-value, or an AxSymShResponse. Default: np.array([1.7e-3, 0.2e-3, 0.2e-3]) gm_response : float, optional Mean diffusivity for GM compartment. If `None`, then grey matter volume fraction is not computed. Default: 0.8e-3 csf_response : float, optional Mean diffusivity for CSF compartment. If `None`, then CSF volume fraction is not computed. Default: 3.0e-3 n_iter : int, optional Number of iterations for fODF estimation. Must be a positive int. Default: 600 recon_type : {'smf', 'sos'}, optional MRI reconstruction method: spatial matched filter (SMF) or sum-of-squares (SoS). SMF reconstruction generates Rician noise while SoS reconstruction generates Noncentral Chi noise. Default: 'smf' n_coils : int, optional Number of coils in MRI scanner -- only relevant in SoS reconstruction. Must be a positive int. Default: 1 R : int, optional Acceleration factor of the acquisition. For SIEMENS, R = iPAT factor. For GE, R = ASSET factor. For PHILIPS, R = SENSE factor. Typical values are 1 or 2. Must be a positive int. Default: 1 voxelwise : bool, optional If true, performs a voxelwise fit. If false, performs a global fit on the entire brain at once. The global fit requires a 4D brain volume in `fit`. Default: True use_tv : bool, optional If true, applies total variation regularization. This only takes effect in a global fit (`voxelwise` is set to `False`). TV can only be applied to 4D brain volumes with no singleton dimensions. Default: False sphere : Sphere, optional Sphere on which to construct fODF. If None, uses `repulsion724`. Default: None verbose : bool, optional If true, logs updates on estimated signal-to-noise ratio after each iteration. This only takes effect in a global fit (`voxelwise` is set to `False`). Default: False References ---------- .. [1] Canales-Rodríguez, E. J., Daducci, A., Sotiropoulos, S. N., Caruyer, E., Aja-Fernández, S., Radua, J., Mendizabal, J. M. Y., Iturria-Medina, Y., Melie-García, L., Alemán-Gómez, Y., Thiran, J.-P., Sarró, S., Pomarol-Clotet, E., & Salvador, R. (2015). Spherical Deconvolution of Multichannel Diffusion MRI Data with Non-Gaussian Noise Models and Spatial Regularization. PLOS ONE, 10(10), e0138910. https://doi.org/10.1371/journal.pone.0138910 .. [2] Dell’Acqua, F., Rizzo, G., Scifo, P., Clarke, R., Scotti, G., & Fazio, F. (2007). A Model-Based Deconvolution Approach to Solve Fiber Crossing in Diffusion-Weighted MR Imaging. IEEE Transactions on Bio-Medical Engineering, 54, 462–472. https://doi.org/10.1109/TBME.2006.888830 ''' if not np.any(gtab.b0s_mask): raise ValueError("Gradient table has no b0 measurements") self.gtab_orig = gtab # save for prediction # Masks to extract b0/non-b0 measurements self.where_b0s = lazy_index(gtab.b0s_mask) self.where_dwi = lazy_index(~gtab.b0s_mask) # Correct gradient table to contain b0 data at the beginning bvals_cor = np.concatenate(([0], gtab.bvals[self.where_dwi])) bvecs_cor = np.concatenate(([[0, 0, 0]], gtab.bvecs[self.where_dwi])) gtab_cor = gradient_table(bvals_cor, bvecs_cor) # Initialize self.gtab OdfModel.__init__(self, gtab_cor) # Store responses self.wm_response = wm_response self.gm_response = gm_response self.csf_response = csf_response # Initializing remaining parameters if R < 1 or n_iter < 1 or n_coils < 1: raise ValueError( f"R, n_iter, and n_coils must be >= 1, but R={R}," + f"n_iter={n_iter}, and n_coils={n_coils} ") self.R = R self.n_iter = n_iter self.recon_type = recon_type self.n_coils = n_coils if voxelwise and use_tv: raise ValueError("Total variation has no effect in voxelwise fit") if voxelwise and verbose: warnings.warn("Verbosity has no effect in voxelwise fit", UserWarning) self.voxelwise = voxelwise self.use_tv = use_tv self.verbose = verbose if sphere is None: self.sphere = get_sphere('repulsion724') else: self.sphere = sphere if voxelwise: self.fit = self._voxelwise_fit else: self.fit = self._global_fit # Fitting parameters self.kernel = None