def sim_response(sh_order, bvals, evals=evals_d, csf_md=csf_md, gm_md=gm_md): bvals = np.array(bvals, copy=True) evecs = np.zeros((3, 3)) z = np.array([0, 0, 1.]) evecs[:, 0] = z evecs[:2, 1:] = np.eye(2) n = np.arange(0, sh_order + 1, 2) m = np.zeros_like(n) big_sphere = default_sphere.subdivide() theta, phi = big_sphere.theta, big_sphere.phi B = shm.real_sph_harm(m, n, theta[:, None], phi[:, None]) A = shm.real_sph_harm(0, 0, 0, 0) response = np.empty([len(bvals), len(n) + 2]) for i, bvalue in enumerate(bvals): gtab = GradientTable(big_sphere.vertices * bvalue) wm_response = single_tensor(gtab, 1., evals, evecs, snr=None) response[i, 2:] = np.linalg.lstsq(B, wm_response)[0] response[i, 0] = np.exp(-bvalue * csf_md) / A response[i, 1] = np.exp(-bvalue * gm_md) / A return MultiShellResponse(response, sh_order, bvals)
def multi_shell_fiber_response(sh_order, bvals, evals, csf_md, gm_md, sphere=None): """Fiber response function estimation for multi-shell data. Parameters ---------- sh_order : int Maximum spherical harmonics order. bvals : ndarray Array containing the b-values. evals : (3,) ndarray Eigenvalues of the diffusion tensor. csf_md : float CSF tissue mean diffusivity value. gm_md : float GM tissue mean diffusivity value. sphere : `dipy.core.Sphere` instance, optional Sphere where the signal will be evaluated. Returns ------- MultiShellResponse MultiShellResponse object. """ bvals = np.array(bvals, copy=True) evecs = np.zeros((3, 3)) z = np.array([0, 0, 1.]) evecs[:, 0] = z evecs[:2, 1:] = np.eye(2) n = np.arange(0, sh_order + 1, 2) m = np.zeros_like(n) if sphere is None: sphere = default_sphere big_sphere = sphere.subdivide() theta, phi = big_sphere.theta, big_sphere.phi B = shm.real_sph_harm(m, n, theta[:, None], phi[:, None]) A = shm.real_sph_harm(0, 0, 0, 0) response = np.empty([len(bvals), len(n) + 2]) for i, bvalue in enumerate(bvals): gtab = GradientTable(big_sphere.vertices * bvalue) wm_response = single_tensor(gtab, 1., evals, evecs, snr=None) response[i, 2:] = np.linalg.lstsq(B, wm_response)[0] response[i, 0] = np.exp(-bvalue * csf_md) / A response[i, 1] = np.exp(-bvalue * gm_md) / A return MultiShellResponse(response, sh_order, bvals)
def shore_psi_matrix(radial_order, mu, rgrad): """Computes the K matrix without optimization. """ r, theta, phi = cart2sphere(rgrad[:, 0], rgrad[:, 1], rgrad[:, 2]) theta[np.isnan(theta)] = 0 ind_mat = shore_index_matrix(radial_order) n_elem = ind_mat.shape[0] n_rgrad = rgrad.shape[0] K = np.zeros((n_rgrad, n_elem)) counter = 0 for n in range(0, radial_order + 1, 2): for j in range(1, 2 + n / 2): l = n + 2 - 2 * j const = (-1) ** (j - 1) * \ (np.sqrt(2) * np.pi * mu ** 3) ** (-1) *\ (r ** 2 / (2 * mu ** 2)) ** (l / 2) *\ np.exp(- r ** 2 / (2 * mu ** 2)) *\ genlaguerre(j - 1, l + 0.5)(r ** 2 / mu ** 2) for m in range(-l, l + 1): K[:, counter] = const * real_sph_harm(m, l, theta, phi) counter += 1 return K
def test_mcsd_model_delta(): sh_order = 8 gtab = get_3shell_gtab() shells = np.unique(gtab.bvals // 100.) * 100. response = sim_response(sh_order, shells, evals_d) model = MultiShellDeconvModel(gtab, response) iso = response.iso theta, phi = default_sphere.theta, default_sphere.phi B = shm.real_sph_harm(response.m, response.n, theta[:, None], phi[:, None]) wm_delta = model.delta.copy() # set isotropic components to zero wm_delta[:iso] = 0. wm_delta = _expand(model.m, iso, wm_delta) for i, s in enumerate(shells): g = GradientTable(default_sphere.vertices * s) signal = model.predict(wm_delta, g) expected = np.dot(response.response[i, iso:], B.T) npt.assert_array_almost_equal(signal, expected) signal = model.predict(wm_delta, gtab) fit = model.fit(signal) m = model.m npt.assert_array_almost_equal(fit.shm_coeff[m != 0], 0., 2)
def shore_phi_matrix(radial_order, mu, q): '''Computed the Q matrix completely without separation into mu-depenent / -independent. See shore_Q_mu_independent for help. ''' qval, theta, phi = cart2sphere(q[:, 0], q[:, 1], q[:, 2]) theta[np.isnan(theta)] = 0 ind_mat = shore_index_matrix(radial_order) n_elem = ind_mat.shape[0] n_qgrad = q.shape[0] M = np.zeros((n_qgrad, n_elem)) counter = 0 for n in range(0, radial_order + 1, 2): for j in range(1, 2 + n / 2): l = n + 2 - 2 * j const = (-1) ** (l / 2) * np.sqrt(4.0 * np.pi) *\ (2 * np.pi ** 2 * mu ** 2 * qval ** 2) ** (l / 2) *\ np.exp(-2 * np.pi ** 2 * mu ** 2 * qval ** 2) *\ genlaguerre(j - 1, l + 0.5)(4 * np.pi ** 2 * mu ** 2 * qval ** 2) for m in range(-l, l + 1): M[:, counter] = const * real_sph_harm(m, l, theta, phi) counter += 1 return M
def shore_matrix_odf(radial_order, zeta, sphere_vertices): r"""Compute the SHORE ODF matrix [1]_" Parameters ---------- radial_order : unsigned int, an even integer that represent the order of the basis zeta : unsigned int, scale factor sphere_vertices : array, shape (N,3) vertices of the odf sphere References ---------- .. [1] Merlet S. et. al, "Continuous diffusion signal, EAP and ODF estimation via Compressive Sensing in diffusion MRI", Medical Image Analysis, 2013. """ r, theta, phi = cart2sphere(sphere_vertices[:, 0], sphere_vertices[:, 1], sphere_vertices[:, 2]) theta[np.isnan(theta)] = 0 F = radial_order / 2 n_c = int(np.round(1 / 6.0 * (F + 1) * (F + 2) * (4 * F + 3))) upsilon = np.zeros((len(sphere_vertices), n_c)) counter = 0 for l in range(0, radial_order + 1, 2): for n in range(l, int((radial_order + l) / 2) + 1): for m in range(-l, l + 1): upsilon[:, counter] = (-1) ** (n - l / 2.0) * _kappa_odf(zeta, n, l) * \ hyp2f1(l - n, l / 2.0 + 1.5, l + 1.5, 2.0) * \ real_sph_harm(m, l, theta, phi) counter += 1 return upsilon
def rtap(self, direction=None): """ Recovers the directional Return To Axis Probability (RTAP) [1,2]. Its value is only valid along the direction of the fiber. If no direction is given the biggest eigenvector of the tensor is used. Its value is defined as: ..math:: :nowrap: \begin{equation} RTAP=\int_{\mathbb{R}}P(\textbf{r}_{\parallel}) d\textbf{r}_{\parallel}=\frac{1}{(2\pi) u_0^2} \sum_{N=0}^{N_{max}}\sum_{\{j,l,m\}}\textbf{c}_{\{j,l,m\}} (-1)^{j-1}2^{-l/2}\kappa(j,l)Y_l^m(\textbf{u}_{fiber}) The vector is precomputed to speed up the computation. """ if direction is None: if self.R is None: warn('Tensor linearity too low to use main tensor eigenvalue ' 'as fiber direction. Returning 0.') return 0. else: direction = np.array(self.R[:, 0], ndmin=2) r, theta, phi = cart2sphere(direction[:, 0], direction[:, 1], direction[:, 2]) else: r, theta, phi = cart2sphere(direction[0], direction[1], direction[2]) inx = self.model.ind_mat rtap_vec = self.model.rtap_vec rtap = self._shore_coef * (1 / self.mu ** 2) *\ rtap_vec * real_sph_harm(inx[:, 2], inx[:, 1], theta, phi) return rtap.sum()
def multi_tissue_basis(gtab, sh_order, iso_comp): """ Builds a basis for multi-shell multi-tissue CSD model. Parameters ---------- gtab : GradientTable sh_order : int iso_comp: int Number of tissue compartments for running the MSMT-CSD. Minimum number of compartments required is 2. Returns ------- B : ndarray Matrix of the spherical harmonics model used to fit the data m : int ``|m| <= n`` The order of the harmonic. n : int ``>= 0`` The degree of the harmonic. """ if iso_comp < 2: msg = ("Multi-tissue CSD requires at least 2 tissue compartments") raise ValueError(msg) r, theta, phi = geo.cart2sphere(*gtab.gradients.T) m, n = shm.sph_harm_ind_list(sh_order) B = shm.real_sph_harm(m, n, theta[:, None], phi[:, None]) B[np.ix_(gtab.b0s_mask, n > 0)] = 0. iso = np.empty([B.shape[0], iso_comp]) iso[:] = SH_CONST B = np.concatenate([iso, B], axis=1) return B, m, n
def __init__(self, gtab, response, sh_order, lambda_=1, tau=0.1): super(NumberSmallfODF, self).__init__() m, n = sph_harm_ind_list(sh_order) # x, y, z = gtab.gradients[~gtab.b0s_mask].T # r, theta, phi = cart2sphere(x, y, z) # self.B_dwi = real_sph_harm(m, n, theta[:, None], phi[:, None]) self.B_dwi = shm.get_B_matrix(gtab, sh_order) self.sphere = get_sphere('symmetric362') 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]) S_r = shm.estimate_response(gtab, response[0:3], response[3]) r_sh = np.linalg.lstsq(self.B_dwi, S_r[~gtab.b0s_mask], rcond=-1)[0] n_response = n m_response = m r_rh = sh_to_rh(r_sh, m_response, n_response) 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_ * R.shape[0] * r_rh[0] / (np.sqrt(self.B_reg.shape[0]) * np.sqrt(362.))) self.B_reg = self.B_reg * lambda_ self.B_reg = nn.Parameter(torch.FloatTensor(self.B_reg), requires_grad=False) self.tau = tau
def shore_matrix_odf(radial_order, zeta, sphere_vertices): r"""Compute the SHORE ODF matrix [1]_" Parameters ---------- radial_order : unsigned int, an even integer that represent the order of the basis zeta : unsigned int, scale factor sphere_vertices : array, shape (N,3) vertices of the odf sphere References ---------- .. [1] Merlet S. et. al, "Continuous diffusion signal, EAP and ODF estimation via Compressive Sensing in diffusion MRI", Medical Image Analysis, 2013. """ r, theta, phi = cart2sphere(sphere_vertices[:, 0], sphere_vertices[:, 1], sphere_vertices[:, 2]) theta[np.isnan(theta)] = 0 F = radial_order / 2 n_c = np.round(1 / 6.0 * (F + 1) * (F + 2) * (4 * F + 3)) upsilon = np.zeros((len(sphere_vertices), n_c)) counter = 0 for l in range(0, radial_order + 1, 2): for n in range(l, int((radial_order + l) / 2) + 1): for m in range(-l, l + 1): upsilon[:, counter] = (-1) ** (n - l / 2.0) * _kappa_odf(zeta, n, l) * \ hyp2f1(l - n, l / 2.0 + 1.5, l + 1.5, 2.0) * \ real_sph_harm(m, l, theta, phi) counter += 1 return upsilon
def gen_dirac(pol, azi, sh_order): """ Generate Dirac delta function orientated in (theta, phi) = (azi, pol) on the sphere. The spherical harmonics (SH) representation of this Dirac is returned. Parameters ---------- pol : float [0, pi] The polar (colatitudinal) coordinate (phi) az : float [0, 2*pi] The azimuthal (longitudinal) coordinate (theta) sh_order : int maximal SH order of the SH representation Returns ------- dirac : ndarray (``(sh_order + 1)(sh_order + 2)/2``,) SH coefficients representing the Dirac function """ m, n = sph_harm_ind_list(sh_order) dirac = np.zeros(m.shape) i = 0 for l in np.arange(0, sh_order + 1, 2): for m in np.arange(-l, l + 1): if m == 0: dirac[i] = real_sph_harm(0, l, azi, pol) i = i + 1 return dirac
def gen_dirac(m, n, theta, phi): """ Generate Dirac delta function orientated in (theta, phi) on the sphere The spherical harmonics (SH) representation of this Dirac is returned as coefficients to spherical harmonic functions produced by `shm.real_sph_harm`. Parameters ---------- m : ndarray (N,) The order of the spherical harmonic function associated with each coefficient. n : ndarray (N,) The degree of the spherical harmonic function associated with each coefficient. theta : float [0, 2*pi] The azimuthal (longitudinal) coordinate. phi : float [0, pi] The polar (colatitudinal) coordinate. See Also -------- shm.real_sph_harm, shm.real_sym_sh_basis Returns ------- dirac : ndarray SH coefficients representing the Dirac function """ return real_sph_harm(m, n, theta, phi)
def SHOREmatrix_pdf(radial_order, zeta, rtab): """Compute the SHORE matrix" Parameters ---------- radial_order : unsigned int, Radial Order zeta : unsigned int, scale factor rtab : array, shape (N,3) r-space points in which calculates the pdf """ r, theta, phi = cart2sphere(rtab[:, 0], rtab[:, 1], rtab[:, 2]) theta[np.isnan(theta)] = 0 psi = np.zeros( (r.shape[0], (radial_order + 1) * ((radial_order + 1) / 2) * (2 * radial_order + 1))) counter = 0 for n in range(radial_order + 1): for l in range(0, n + 1, 2): for m in range(-l, l + 1): psi[:, counter] = real_sph_harm(m, l, theta, phi) * \ genlaguerre(n - l, l + 0.5)(4 * np.pi ** 2 * zeta * r ** 2 ) *\ np.exp(-2 * np.pi ** 2 * zeta * r ** 2) *\ __kappa_pdf(zeta, n, l) *\ (4 * np.pi ** 2 * zeta * r ** 2) ** (l / 2) * \ (-1) ** (n - l / 2) counter += 1 return psi[:, 0:counter]
def shore_Q_mu_independent(radial_order, q): r'''Computed the u0 independent part of the design matrix. ..math:: :nowrap: \begin{align} \Xi_{jlm(i)}(u_0,\textbf{q}_k)=&\overbrace{u_0^{l(i)} e^{-2\pi^2u_0^2q_k^2}L_{j(i)-1}^{l(i)+1/2} (4\pi^2u_0^2q_k^2)}^{u_0\,dependent} \overbrace{\sqrt{4\pi}i^{-l(i)}(2\pi^2q_k^2)^{l(i)/2} Y_{l(i)}^{m(i)}(\textbf{u}_{q_k})}^{u_0\,independent} =&A_{jl(i)}(q_k)B_{lm(i)}(\textbf{q}_k) \end{align} ''' ind_mat = shore_index_matrix(radial_order) qval, theta, phi = cart2sphere(q[:, 0], q[:, 1], q[:, 2]) theta[np.isnan(theta)] = 0 n_elem = ind_mat.shape[0] n_rgrad = theta.shape[0] Q_mu_independent = np.zeros((n_rgrad, n_elem)) counter = 0 for n in range(0, radial_order + 1, 2): for j in range(1, 2 + n / 2): l = n + 2 - 2 * j const = np.sqrt(4 * np.pi) * (-1) ** (-l / 2) * \ (2 * np.pi ** 2 * qval ** 2) ** (l / 2) for m in range(-1 * (n + 2 - 2 * j), (n + 3 - 2 * j)): Q_mu_independent[:, counter] = const * \ real_sph_harm(m, l, theta, phi) counter += 1 return Q_mu_independent
def real_sym_rh_basis(sh_order, theta, phi): r"""Samples a real symmetric rotational harmonic basis at point on the sphere Samples the basis functions up to order `sh_order` at points on the sphere given by `theta` and `phi`. The basis functions are defined here the same way as in fibernavigator, where the real harmonic $Y^m_n$ is defined to be: $Y^0_n$ if m = 0 Parameters ----------- sh_order : int even int > 0, max spherical harmonic degree theta : float [0, 2*pi] The azimuthal (longitudinal) coordinate. phi : float [0, pi] The polar (colatitudinal) coordinate. Returns -------- real_rh_matrix : array of shape () The real harmonic $Y^0_n$ sampled at `theta` and `phi` """ n = np.arange(0, sh_order + 1, 2) m = np.zeros(sh_order // 2 + 1) phi = np.reshape(phi, [-1, 1]) theta = np.reshape(theta, [-1, 1]) real_rh_matrix = real_sph_harm(m, n, theta, phi) return real_rh_matrix
def sh_convolution_matrix(self, kernel="rank1"): """ Parameters ---------- kernel Returns ------- """ if self.kernel_type != kernel: self.set_kernel(kernel) # Build matrix that maps ODF to signal M = np.zeros((self.gtab.bvals.shape[0], esh.LENGTH[self.order])) r, theta, phi = cart2sphere(self.gtab.bvecs[:, 0], self.gtab.bvecs[:, 1], self.gtab.bvecs[:, 2]) theta[np.isnan(theta)] = 0 counter = 0 for l in range(0, self.order + 1, 2): for m in range(-l, l + 1): M[:, counter] = (real_sph_harm(m, l, theta, phi) * self.kernel_wm[l // 2]) counter += 1 return M
def SHOREmatrix_odf(radial_order, zeta, sphere_vertices): """Compute the SHORE matrix" Parameters ---------- radial_order : unsigned int, Radial Order zeta : unsigned int, scale factor sphere_vertices : array, shape (N,3) vertices of the odf sphere """ r, theta, phi = cart2sphere(sphere_vertices[:, 0], sphere_vertices[:, 1], sphere_vertices[:, 2]) theta[np.isnan(theta)] = 0 counter = 0 upsilon = np.zeros( (len(sphere_vertices), (radial_order + 1) * ((radial_order + 1) / 2) * (2 * radial_order + 1))) for n in range(radial_order + 1): for l in range(0, n + 1, 2): for m in range(-l, l + 1): upsilon[:, counter] = (-1) ** (n - l / 2.0) * __kappa_odf(zeta, n, l) * \ hyp2f1(l - n, l / 2.0 + 1.5, l + 1.5, 2.0) * \ real_sph_harm(m, l, theta, phi) counter += 1 return upsilon[:, 0:counter]
def sh_smooth(data, bvals, bvecs, sh_order=4, similarity_threshold=50, regul=0.006): """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 8 Order of the spherical harmonics to fit. similarity_threshold : int, default 50 All b-values such that |b_1 - b_2| < similarity_threshold will be considered as identical for smoothing purpose. Must be lower than 200. regul : float, default 0.006 Amount of regularization to apply to sh coefficients computation. Return --------- pred_sig : ndarray The smoothed diffusion data, fitted through spherical harmonics. """ if similarity_threshold > 200: raise ValueError("similarity_threshold = {}, which is higher than 200," " please use a lower value".format(similarity_threshold)) m, n = sph_harm_ind_list(sh_order) L = -n * (n + 1) where_b0s = bvals == 0 pred_sig = np.zeros_like(data, dtype=np.float32) # Round similar bvals together for identifying similar shells rounded_bvals = np.zeros_like(bvals) for unique_bval in np.unique(bvals): idx = np.abs(unique_bval - bvals) < similarity_threshold rounded_bvals[idx] = unique_bval # process each b-value separately for unique_bval in np.unique(rounded_bvals): idx = rounded_bvals == unique_bval # Just give back the signal for the b0s since we can't really do anything about it if np.all(idx == where_b0s): if np.sum(where_b0s) > 1: pred_sig[..., idx] = np.mean(data[..., idx], axis=-1, keepdims=True) else: pred_sig[..., idx] = data[..., idx] continue x, y, z = bvecs[:, idx] 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]) invB = smooth_pinv(B_dwi, np.sqrt(regul) * L) sh_coeff = np.dot(data[..., idx], invB.T) # Find the smoothed signal from the sh fit for the given gtab pred_sig[..., idx] = np.dot(sh_coeff, B_dwi.T) return pred_sig
def odf(self, sphere): sampling_matrix = self.model.cache_get("sampling_matrix", sphere) if sampling_matrix is None: phi = sphere.phi[:, np.newaxis] #sphere.phi.reshape((-1, 1)) theta = sphere.theta.reshape((-1, 1)) sampling_matrix = real_sph_harm(self.model.m, self.model.n, theta, phi) self.model.cache_set("sampling_matrix", sphere, sampling_matrix) return np.dot(self.shm_coeff, sampling_matrix.T)
def shore_odf_matrix(radial_order, mu, s, vertices): r"""The ODF in terms of SHORE coefficients for arbitrary radial moment can be given as [2]: ..math:: :nowrap: \begin{equation} ODF_s(u_0,\textbf{v})=\sum_{N=0}^{N_{max}} \sum_{\{j,l,m\}}\textbf{c}_{\{j,l,m\}} \Omega_s^{jlm}(u_0,\textbf{v}) \end{equation} with $\textbf{v}$ an orientation on the unit sphere and the ODF basis function: ..math:: :nowrap: \begin{equation} \Omega_s^{jlm}(u_0,\textbf{v})=\frac{u_0^s}{\pi}(-1)^{j-1} 2^{-l/2}\kappa(j,l,s)Y^l_m(\textbf{v}) \end{equation} with ..math:: :nowrap: \begin{equation} \kappa(j,l,s)=\sum_{k=0}^{j-1}\frac{(-1)^k}{k!} \binom{j+l-1/2}{j-k-1} \frac{\Gamma((l+s+3)/2+k)}{2^{-((l+s)/2+k)}}. \end{equation} """ r, theta, phi = cart2sphere(vertices[:, 0], vertices[:, 1], vertices[:, 2]) theta[np.isnan(theta)] = 0 ind_mat = shore_index_matrix(radial_order) n_vert = vertices.shape[0] n_elem = ind_mat.shape[0] odf_mat = np.zeros((n_vert, n_elem)) counter = 0 for n in range(0, radial_order + 1, 2): for j in range(1, 2 + n / 2): l = n + 2 - 2 * j kappa = ((-1)**(j - 1) * 2**(-(l + 3) / 2.0) * mu**s) / np.pi matsum = 0 for k in range(0, j): matsum += ((-1) ** k * binomialfloat(j + l - 0.5, j - k - 1) * gamma((l + s + 3) / 2.0 + k)) /\ (factorial(k) * 0.5 ** ((l + s + 3) / 2.0 + k)) for m in range(-l, l + 1): odf_mat[:, counter] = kappa * matsum *\ real_sph_harm(m, l, theta, phi) counter += 1 return odf_mat
def rho_matrix(sh_order, vecs): r"""Compute the SH matrix $\rho$ """ r, theta, phi = cart2sphere(vecs[:, 0], vecs[:, 1], vecs[:, 2]) theta[np.isnan(theta)] = 0 n_c = int((sh_order + 1) * (sh_order + 2) / 2) rho = np.zeros((vecs.shape[0], n_c)) counter = 0 for l in range(0, sh_order + 1, 2): for m in range(-l, l + 1): rho[:, counter] = real_sph_harm(m, l, theta, phi) counter += 1 return rho
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 test_hat_and_lcr(): hemi = hemi_icosahedron.subdivide(3) m, n = sph_harm_ind_list(8) B = real_sph_harm(m, n, hemi.theta[:, None], hemi.phi[:, None]) H = hat(B) B_hat = np.dot(H, B) assert_array_almost_equal(B, B_hat) R = lcr_matrix(H) d = np.arange(len(hemi.theta)) r = d - np.dot(H, d) lev = np.sqrt(1 - H.diagonal()) r /= lev r -= r.mean() r2 = np.dot(R, d) assert_array_almost_equal(r, r2) r3 = np.dot(d, R.T) assert_array_almost_equal(r, r3)
def test_hat_and_lcr(): v, e, f = create_half_unit_sphere(6) m, n = sph_harm_ind_list(8) r, pol, azi = cart2sphere(*v.T) B = real_sph_harm(m, n, azi[:, None], pol[:, None]) H = hat(B) B_hat = np.dot(H, B) assert_array_almost_equal(B, B_hat) R = lcr_matrix(H) d = np.arange(len(azi)) r = d - np.dot(H, d) lev = np.sqrt(1 - H.diagonal()) r /= lev r -= r.mean() r2 = np.dot(R, d) assert_array_almost_equal(r, r2) r3 = np.dot(d, R.T) assert_array_almost_equal(r, r3)
def esh_matrix(order, gtab): """ Matrix that evaluates SH coeffs in the given directions Parameters ---------- order gtab Returns ------- """ bvecs = gtab.bvecs r, theta, phi = cart2sphere(bvecs[:, 0], bvecs[:, 1], bvecs[:, 2]) theta[np.isnan(theta)] = 0 M = np.zeros((bvecs.shape[0], esh.LENGTH[order])) counter = 0 for l in range(0, order + 1, 2): for m in range(-l, l + 1): M[:, counter] = real_sph_harm(m, l, theta, phi) counter += 1 return M
def SHOREmatrix(radial_order, zeta, gtab, tau=1 / (4 * np.pi ** 2)): """Compute the SHORE matrix" Parameters ---------- radial_order : unsigned int, Radial Order zeta : unsigned int, scale factor gtab : GradientTable, Gradient directions and bvalues container class tau : float, diffusion time. By default the value that makes q=sqrt(b). """ qvals = np.sqrt(gtab.bvals / (4 * np.pi ** 2 * tau)) bvecs = gtab.bvecs qgradients = qvals[:, None] * bvecs r, theta, phi = cart2sphere( qgradients[:, 0], qgradients[:, 1], qgradients[:, 2]) theta[np.isnan(theta)] = 0 M = np.zeros( (r.shape[0], (radial_order + 1) * ((radial_order + 1) / 2) * (2 * radial_order + 1))) counter = 0 for n in range(radial_order + 1): for l in range(0, n + 1, 2): for m in range(-l, l + 1): M[:, counter] = real_sph_harm(m, l, theta, phi) * \ genlaguerre(n - l, l + 0.5)(r ** 2 / float(zeta)) * \ np.exp(- r ** 2 / (2.0 * zeta)) * \ __kappa(zeta, n, l) * \ (r ** 2 / float(zeta)) ** (l / 2) counter += 1 return M[:, 0:counter]
def test_smooth_pinv(): hemi = hemi_icosahedron.subdivide(2) m, n = sph_harm_ind_list(4) B = real_sph_harm(m, n, hemi.theta[:, None], hemi.phi[:, None]) L = np.zeros(len(m)) C = smooth_pinv(B, L) D = np.dot(npl.inv(np.dot(B.T, B)), B.T) assert_array_almost_equal(C, D) L = n * (n + 1) * .05 C = smooth_pinv(B, L) L = np.diag(L) D = np.dot(npl.inv(np.dot(B.T, B) + L * L), B.T) assert_array_almost_equal(C, D) L = np.arange(len(n)) * .05 C = smooth_pinv(B, L) L = np.diag(L) D = np.dot(npl.inv(np.dot(B.T, B) + L * L), B.T) assert_array_almost_equal(C, D)
def test_smooth_pinv(): v, e, f = create_half_unit_sphere(3) m, n = sph_harm_ind_list(4) r, pol, azi = cart2sphere(*v.T) B = real_sph_harm(m, n, azi[:, None], pol[:, None]) L = np.zeros(len(m)) C = smooth_pinv(B, L) D = np.dot(npl.inv(np.dot(B.T, B)), B.T) assert_array_almost_equal(C, D) L = n * (n + 1) * 0.05 C = smooth_pinv(B, L) L = np.diag(L) D = np.dot(npl.inv(np.dot(B.T, B) + L * L), B.T) assert_array_almost_equal(C, D) L = np.arange(len(n)) * 0.05 C = smooth_pinv(B, L) L = np.diag(L) D = np.dot(npl.inv(np.dot(B.T, B) + L * L), B.T) assert_array_almost_equal(C, D)
def test_smooth_pinv(): v, e, f = create_half_unit_sphere(3) m, n = sph_harm_ind_list(4) r, pol, azi = cart2sphere(*v.T) B = real_sph_harm(m, n, azi[:, None], pol[:, None]) L = np.zeros(len(m)) C = smooth_pinv(B, L) D = np.dot(npl.inv(np.dot(B.T, B)), B.T) assert_array_almost_equal(C, D) L = n * (n + 1) * .05 C = smooth_pinv(B, L) L = np.diag(L) D = np.dot(npl.inv(np.dot(B.T, B) + L * L), B.T) assert_array_almost_equal(C, D) L = np.arange(len(n)) * .05 C = smooth_pinv(B, L) L = np.diag(L) D = np.dot(npl.inv(np.dot(B.T, B) + L * L), B.T) assert_array_almost_equal(C, D)
def shore_K_mu_independent(radial_order, rgrad): '''Computes mu independent part of K [2]. Same trick as with Q. ''' r, theta, phi = cart2sphere(rgrad[:, 0], rgrad[:, 1], rgrad[:, 2]) theta[np.isnan(theta)] = 0 ind_mat = shore_index_matrix(radial_order) n_elem = ind_mat.shape[0] n_rgrad = rgrad.shape[0] K = np.zeros((n_rgrad, n_elem)) counter = 0 for n in range(0, radial_order + 1, 2): for j in range(1, 2 + n / 2): l = n + 2 - 2 * j const = (-1) ** (j - 1) *\ (np.sqrt(2) * np.pi) ** (-1) *\ (r ** 2 / 2) ** (l / 2) for m in range(-l, l + 1): K[:, counter] = const * real_sph_harm(m, l, theta, phi) counter += 1 return K
def shore_matrix_pdf(radial_order, zeta, rtab): r"""Compute the SHORE propagator matrix [1]_" Parameters ---------- radial_order : unsigned int, an even integer that represent the order of the basis zeta : unsigned int, scale factor rtab : array, shape (N,3) real space points in which calculates the pdf References ---------- .. [1] Merlet S. et. al, "Continuous diffusion signal, EAP and ODF estimation via Compressive Sensing in diffusion MRI", Medical Image Analysis, 2013. """ r, theta, phi = cart2sphere(rtab[:, 0], rtab[:, 1], rtab[:, 2]) theta[np.isnan(theta)] = 0 F = radial_order / 2 n_c = int(np.round(1 / 6.0 * (F + 1) * (F + 2) * (4 * F + 3))) psi = np.zeros((r.shape[0], n_c)) counter = 0 for l in range(0, radial_order + 1, 2): for n in range(l, int((radial_order + l) / 2) + 1): for m in range(-l, l + 1): psi[:, counter] = real_sph_harm(m, l, theta, phi) * \ genlaguerre(n - l, l + 0.5)(4 * np.pi ** 2 * zeta * r ** 2) *\ np.exp(-2 * np.pi ** 2 * zeta * r ** 2) *\ _kappa_pdf(zeta, n, l) *\ (4 * np.pi ** 2 * zeta * r ** 2) ** (l / 2) * \ (-1) ** (n - l / 2) counter += 1 return psi
def shore_matrix_pdf(radial_order, zeta, rtab): r"""Compute the SHORE propagator matrix [1]_" Parameters ---------- radial_order : unsigned int, an even integer that represent the order of the basis zeta : unsigned int, scale factor rtab : array, shape (N,3) real space points in which calculates the pdf References ---------- .. [1] Merlet S. et al., "Continuous diffusion signal, EAP and ODF estimation via Compressive Sensing in diffusion MRI", Medical Image Analysis, 2013. """ r, theta, phi = cart2sphere(rtab[:, 0], rtab[:, 1], rtab[:, 2]) theta[np.isnan(theta)] = 0 F = radial_order / 2 n_c = int(np.round(1 / 6.0 * (F + 1) * (F + 2) * (4 * F + 3))) psi = np.zeros((r.shape[0], n_c)) counter = 0 for l in range(0, radial_order + 1, 2): for n in range(l, int((radial_order + l) / 2) + 1): for m in range(-l, l + 1): psi[:, counter] = real_sph_harm(m, l, theta, phi) * \ genlaguerre(n - l, l + 0.5)(4 * np.pi ** 2 * zeta * r ** 2) *\ np.exp(-2 * np.pi ** 2 * zeta * r ** 2) *\ _kappa_pdf(zeta, n, l) *\ (4 * np.pi ** 2 * zeta * r ** 2) ** (l / 2) * \ (-1) ** (n - l / 2) counter += 1 return psi
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 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 basis(self, sphere): """A basis that maps the response coefficients onto a sphere.""" theta = sphere.theta[:, None] phi = sphere.phi[:, None] return real_sph_harm(self.m, self.n, theta, phi)
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 shore_matrix(radial_order, zeta, gtab, tau=1 / (4 * np.pi**2)): r"""Compute the SHORE matrix for modified Merlet's 3D-SHORE [1]_ ..math:: :nowrap: \begin{equation} \textbf{E}(q\textbf{u})=\sum_{l=0, even}^{N_{max}} \sum_{n=l}^{(N_{max}+l)/2} \sum_{m=-l}^l c_{nlm} \phi_{nlm}(q\textbf{u}) \end{equation} where $\phi_{nlm}$ is ..math:: :nowrap: \begin{equation} \phi_{nlm}^{SHORE}(q\textbf{u})=\Biggl[\dfrac{2(n-l)!} {\zeta^{3/2} \Gamma(n+3/2)} \Biggr]^{1/2} \Biggl(\dfrac{q^2}{\zeta}\Biggr)^{l/2} exp\Biggl(\dfrac{-q^2}{2\zeta}\Biggr) L^{l+1/2}_{n-l} \Biggl(\dfrac{q^2}{\zeta}\Biggr) Y_l^m(\textbf{u}). \end{equation} Parameters ---------- radial_order : unsigned int, an even integer that represent the order of the basis zeta : unsigned int, scale factor gtab : GradientTable, gradient directions and bvalues container class tau : float, diffusion time. By default the value that makes q=sqrt(b). References ---------- .. [1] Merlet S. et al., "Continuous diffusion signal, EAP and ODF estimation via Compressive Sensing in diffusion MRI", Medical Image Analysis, 2013. """ qvals = np.sqrt(gtab.bvals / (4 * np.pi**2 * tau)) qvals[gtab.b0s_mask] = 0 bvecs = gtab.bvecs qgradients = qvals[:, None] * bvecs r, theta, phi = cart2sphere(qgradients[:, 0], qgradients[:, 1], qgradients[:, 2]) theta[np.isnan(theta)] = 0 F = radial_order / 2 n_c = int(np.round(1 / 6.0 * (F + 1) * (F + 2) * (4 * F + 3))) M = np.zeros((r.shape[0], n_c)) counter = 0 for l in range(0, radial_order + 1, 2): for n in range(l, int((radial_order + l) / 2) + 1): for m in range(-l, l + 1): M[:, counter] = real_sph_harm(m, l, theta, phi) * \ genlaguerre(n - l, l + 0.5)(r ** 2 / zeta) * \ np.exp(- r ** 2 / (2.0 * zeta)) * \ _kappa(zeta, n, l) * \ (r ** 2 / zeta) ** (l / 2) counter += 1 return M
def multi_shell_fiber_response(sh_order, bvals, wm_rf, gm_rf, csf_rf, sphere=None, tol=20): """Fiber response function estimation for multi-shell data. Parameters ---------- sh_order : int Maximum spherical harmonics order. bvals : ndarray Array containing the b-values. Must be unique b-values, like outputed by `dipy.core.gradients.unique_bvals_tolerance`. wm_rf : (4, len(bvals)) ndarray Response function of the WM tissue, for each bvals. gm_rf : (4, len(bvals)) ndarray Response function of the GM tissue, for each bvals. csf_rf : (4, len(bvals)) ndarray Response function of the CSF tissue, for each bvals. sphere : `dipy.core.Sphere` instance, optional Sphere where the signal will be evaluated. Returns ------- MultiShellResponse MultiShellResponse object. """ NUMPY_1_14_PLUS = LooseVersion(np.__version__) >= LooseVersion('1.14.0') rcond_value = None if NUMPY_1_14_PLUS else -1 bvals = np.array(bvals, copy=True) evecs = np.zeros((3, 3)) z = np.array([0, 0, 1.]) evecs[:, 0] = z evecs[:2, 1:] = np.eye(2) n = np.arange(0, sh_order + 1, 2) m = np.zeros_like(n) if sphere is None: sphere = default_sphere big_sphere = sphere.subdivide() theta, phi = big_sphere.theta, big_sphere.phi B = shm.real_sph_harm(m, n, theta[:, None], phi[:, None]) A = shm.real_sph_harm(0, 0, 0, 0) response = np.empty([len(bvals), len(n) + 2]) if bvals[0] < tol: gtab = GradientTable(big_sphere.vertices * 0) wm_response = single_tensor(gtab, wm_rf[0, 3], wm_rf[0, :3], evecs, snr=None) response[0, 2:] = np.linalg.lstsq(B, wm_response, rcond=rcond_value)[0] response[0, 1] = gm_rf[0, 3] / A response[0, 0] = csf_rf[0, 3] / A for i, bvalue in enumerate(bvals[1:]): gtab = GradientTable(big_sphere.vertices * bvalue) wm_response = single_tensor(gtab, wm_rf[i, 3], wm_rf[i, :3], evecs, snr=None) response[i + 1, 2:] = np.linalg.lstsq(B, wm_response, rcond=rcond_value)[0] response[i + 1, 1] = gm_rf[i, 3] * np.exp(-bvalue * gm_rf[i, 0]) / A response[i + 1, 0] = csf_rf[i, 3] * np.exp(-bvalue * csf_rf[i, 0]) / A S0 = [csf_rf[0, 3], gm_rf[0, 3], wm_rf[0, 3]] else: warnings.warn("""No b0 given. Proceeding either way.""", UserWarning) for i, bvalue in enumerate(bvals): gtab = GradientTable(big_sphere.vertices * bvalue) wm_response = single_tensor(gtab, wm_rf[i, 3], wm_rf[i, :3], evecs, snr=None) response[i, 2:] = np.linalg.lstsq(B, wm_response, rcond=rcond_value)[0] response[i, 1] = gm_rf[i, 3] * np.exp(-bvalue * gm_rf[i, 0]) / A response[i, 0] = csf_rf[i, 3] * np.exp(-bvalue * csf_rf[i, 0]) / A S0 = [csf_rf[0, 3], gm_rf[0, 3], wm_rf[0, 3]] return MultiShellResponse(response, sh_order, bvals, S0=S0)
def csd_predict(sh_coeff, gtab, response=None, S0=1, R=None): """ Compute a signal prediction given spherical harmonic coefficients and (optionally) a response function for the provided GradientTable class instance Parameters ---------- sh_coeff : ndarray Spherical harmonic coefficients gtab : GradientTable class instance response : tuple 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. Default: (np.array([0.0015, 0.0003, 0.0003]), 1) S0 : ndarray or float The non diffusion-weighted signal value. R : ndarray Optionally, provide an R matrix. If not provided, calculated from the gtab, response function, etc. Returns ------- pred_sig : ndarray The signal predicted from the provided SH coefficients for a measurement with the provided GradientTable. The last dimension of the resulting array is the same as the number of bvals/bvecs in the GradientTable. The first dimensions have shape: `sh_coeff.shape[:-1]`. """ n_coeff = sh_coeff.shape[-1] sh_order = order_from_ncoef(n_coeff) x, y, z = gtab.gradients[~gtab.b0s_mask].T r, theta, phi = cart2sphere(x, y, z) SH_basis, m, n = real_sym_sh_basis(sh_order, theta, phi) if R is None: # for the gradient sphere B_dwi = real_sph_harm(m, n, theta[:, None], phi[:, None]) if response is None: response = (np.array([0.0015, 0.0003, 0.0003]), 1) else: response = response S_r = estimate_response(gtab, response[0], response[1]) r_sh = np.linalg.lstsq(B_dwi, S_r[~gtab.b0s_mask])[0] r_rh = sh_to_rh(r_sh, m, n) R = forward_sdeconv_mat(r_rh, n) predict_matrix = np.dot(SH_basis, R) if np.iterable(S0): # If it's an array, we need to give it one more dimension: S0 = S0[..., None] # This is the key operation: convolve and multiply by S0: pre_pred_sig = S0 * np.dot(predict_matrix, sh_coeff) # Now put everything in its right place: pred_sig = np.zeros(pre_pred_sig.shape[:-1] + (gtab.bvals.shape[0],)) pred_sig[..., ~gtab.b0s_mask] = pre_pred_sig pred_sig[..., gtab.b0s_mask] = S0 return pred_sig
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 shore_matrix(radial_order, zeta, gtab, tau=1 / (4 * np.pi ** 2)): r"""Compute the SHORE matrix for modified Merlet's 3D-SHORE [1]_ ..math:: :nowrap: \begin{equation} \textbf{E}(q\textbf{u})=\sum_{l=0, even}^{N_{max}} \sum_{n=l}^{(N_{max}+l)/2} \sum_{m=-l}^l c_{nlm} \phi_{nlm}(q\textbf{u}) \end{equation} where $\phi_{nlm}$ is ..math:: :nowrap: \begin{equation} \phi_{nlm}^{SHORE}(q\textbf{u})=\Biggl[\dfrac{2(n-l)!} {\zeta^{3/2} \Gamma(n+3/2)} \Biggr]^{1/2} \Biggl(\dfrac{q^2}{\zeta}\Biggr)^{l/2} exp\Biggl(\dfrac{-q^2}{2\zeta}\Biggr) L^{l+1/2}_{n-l} \Biggl(\dfrac{q^2}{\zeta}\Biggr) Y_l^m(\textbf{u}). \end{equation} Parameters ---------- radial_order : unsigned int, an even integer that represent the order of the basis zeta : unsigned int, scale factor gtab : GradientTable, gradient directions and bvalues container class tau : float, diffusion time. By default the value that makes q=sqrt(b). References ---------- .. [1] Merlet S. et. al, "Continuous diffusion signal, EAP and ODF estimation via Compressive Sensing in diffusion MRI", Medical Image Analysis, 2013. """ qvals = np.sqrt(gtab.bvals / (4 * np.pi ** 2 * tau)) qvals[gtab.b0s_mask] = 0 bvecs = gtab.bvecs qgradients = qvals[:, None] * bvecs r, theta, phi = cart2sphere(qgradients[:, 0], qgradients[:, 1], qgradients[:, 2]) theta[np.isnan(theta)] = 0 F = radial_order / 2 n_c = int(np.round(1 / 6.0 * (F + 1) * (F + 2) * (4 * F + 3))) M = np.zeros((r.shape[0], n_c)) counter = 0 for l in range(0, radial_order + 1, 2): for n in range(l, int((radial_order + l) / 2) + 1): for m in range(-l, l + 1): M[:, counter] = real_sph_harm(m, l, theta, phi) * \ genlaguerre(n - l, l + 0.5)(r ** 2 / zeta) * \ np.exp(- r ** 2 / (2.0 * zeta)) * \ _kappa(zeta, n, l) * \ (r ** 2 / zeta) ** (l / 2) counter += 1 return M
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 sh_smooth(data, bvals, bvecs, sh_order=4, similarity_threshold=50, regul=0.006): """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 8 Order of the spherical harmonics to fit. similarity_threshold : int, default 50 All b-values such that |b_1 - b_2| < similarity_threshold will be considered as identical for smoothing purpose. Must be lower than 200. regul : float, default 0.006 Amount of regularization to apply to sh coefficients computation. Return --------- pred_sig : ndarray The smoothed diffusion data, fitted through spherical harmonics. """ if similarity_threshold > 200: raise ValueError( "similarity_threshold = {}, which is higher than 200," " please use a lower value".format(similarity_threshold)) m, n = sph_harm_ind_list(sh_order) L = -n * (n + 1) where_b0s = bvals == 0 pred_sig = np.zeros_like(data, dtype=np.float32) # Round similar bvals together for identifying similar shells rounded_bvals = np.zeros_like(bvals) for unique_bval in np.unique(bvals): idx = np.abs(unique_bval - bvals) < similarity_threshold rounded_bvals[idx] = unique_bval # process each b-value separately for unique_bval in np.unique(rounded_bvals): idx = rounded_bvals == unique_bval # Just give back the signal for the b0s since we can't really do anything about it if np.all(idx == where_b0s): if np.sum(where_b0s) > 1: pred_sig[..., idx] = np.mean(data[..., idx], axis=-1, keepdims=True) else: pred_sig[..., idx] = data[..., idx] continue x, y, z = bvecs[:, idx] 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]) invB = smooth_pinv(B_dwi, np.sqrt(regul) * L) sh_coeff = np.dot(data[..., idx], invB.T) # Find the smoothed signal from the sh fit for the given gtab pred_sig[..., idx] = np.dot(sh_coeff, B_dwi.T) return pred_sig