Generating fibre-to-field angle from DTI

Dear all,
My name is Luke and I am psychiatrist and neuroimaging researcher working for King’s College London. I am trying to emulate aspects from the processing pipeline detailed in the following paper:
“In vivo assessment of anisotropy of apparent magnetic susceptibility in white matter from a single orientation acquisition”
https://doi.org/10.1016/j.neuroimage.2021.118442

I have DWI data (data.nii.gz) which I have generated through a typical FSL pipeline. In line with the pipeline presented in the paper, I need to work out "“The predominant fibre orientation was determined as the angle between the first peak of the orientation distribution function (ODF) and the B0 direction using constrained spherical deconvolution (Tournier et al. 2007: 10.1016/j.neuroimage.2007.02.016).”

I am using the steps below to generate data from the first peak.

mrconvert data.nii.gz dwi.mif -fslgrad bvecs bvals
dwi2response tournier dwi.mif response.txt
dwi2fod csd dwi.mif response.txt fod.mif
sh2peaks fod.mif peaks.mif
mrconvert peaks.mif peaks.nii.gz
sh2peaks -num 1 fod.mif first_peak.mif
mrconvert first_peak.mif first_peak.nii.gz

# Load the peaks NIfTI file
peaks_img = nib.load('/data/project/CARDS/working/007/DTI/peaks.nii.gz')
peaks_data = peaks_img.get_fdata()

# Define the B0 field direction (assuming it's along the z-axis, i.e., [0, 0, 1])
B0 = np.array([0, 0, 1])

# Calculate the angle between each peak and B0 at each voxel
def compute_angle(peak, B0):
    dot_product = np.dot(peak, B0)
    peak_magnitude = np.linalg.norm(peak)
    B0_magnitude = np.linalg.norm(B0)
    cos_angle = dot_product / (peak_magnitude * B0_magnitude)
    return np.arccos(np.clip(cos_angle, -1, 1))  # Ensure valid range for arccos

# Initialize an array to store the angle values (alpha) for each voxel
alpha = np.full(peaks_data.shape[:3], np.nan)

# Loop through each voxel and calculate the angle for the primary peak
for x in range(peaks_data.shape[0]):
    for y in range(peaks_data.shape[1]):
        for z in range(peaks_data.shape[2]):
            # Extract the peak vector for the voxel (only 3 components in this case)
            peak_vector = peaks_data[x, y, z, :]

            # Check if the peak vector is not all zeros (to avoid invalid calculations)
            if np.linalg.norm(peak_vector) > 0:
                # Compute the angle between the peak vector and B0
                alpha[x, y, z] = compute_angle(peak_vector, B0)

Could I check if this appears to be the correct way to work out the fibre-to-field angle which represents the orientation of the fibre with respect to B0? Many thanks.

Dear all,

I think I may have found a simple solution based on the advice given in this thread

Would the following code work to generate an image where each voxel value is the fibre orientation relative to B0?

mrconvert data.nii.gz dwi.mif -fslgrad bvecs bvals
dwi2response tournier dwi.mif response.txt
dwi2fod csd dwi.mif response.txt fod.mif
sh2peaks -num 1 fod.mif first_peak.mif
mrconvert first_peak.mif -coord 3 2 z-component.mif
mrcalc z-component.mif -acos angle.mif
mrconvert angle.mif angle.nii.gz

The angle file does not look correct.
My first_peak.mif image looks great (attached below). This is a 4D image with values between -1.5 and 1.7
Is there a simple way that I can generate a map where the values are the angle between peak direction and B0 (the z-axis)?

Hi @lukevano,

To answer this (and your other related post): yes, that approach can work, but in contrast to using the tensor eigenvectors (with the -modulate none option), the direction vectors produced by sh2peaks are scaled to the amplitude of the fODF along that direction – they are not unit vectors, so they need to be normalised to unit length before computing the angle using the acos function.

This ought to do the trick:

mrconvert data.nii.gz dwi.mif -fslgrad bvecs bvals
dwi2response tournier dwi.mif response.txt
dwi2fod csd dwi.mif response.txt fod.mif
sh2peaks -num 1 fod.mif first_peak.mif
# addition to normalise peak vector:
mrmath first_peak.mif norm -axis 3 first_peak_amplitude.mif
mrcalc first_peak.mif first_peak_amplitude.mif -div first_peak_norm.mif
# then use normalised peak for subsequent angle calculation:
mrconvert first_peak_norm.mif -coord 3 2 z-component.mif
mrcalc z-component.mif -acos angle.mif
mrconvert angle.mif angle.nii.gz

I also note you can simplify all this and avoid the need to store so many intermediate files if you want. This relies on the use of Unix pipes and the fact that most MRtrix commands natively support NIfTI anyway:

dwi2response tournier -fslgrad bvecs bvals data.nii.gz response.txt
dwi2fod csd dwi.mif response.txt - | sh2peaks - -num 1 peak1.mif
mrmath peak1.mif norm -axis 3 - | mrcalc peak1.mif - -div - | mrconvert - -coord 3 2 - | mrcalc - -acos angle.nii.gz

Finally, if you want the answer in degrees rather than radians, you can add this to the final mrcalc call:

... | mrcalc - -acos 180 pi -div -mult angle.nii.gz

Happy to discuss all this face to face if you’re ever in St Thomas, by the way!
All the best,

Donald

Dear Donald,
I did not know you are at St. Thomas! I’m finishing up my PhD and working in East London but if we set up any new DTI projects I definitely discuss with you from the outset.

Thanks for explaining the need to normalize the peaks. It looks to me as though there might be an error with the normalization using the code you’ve outlined as the values are not between -1 and 1. The subsequent angle image has quite a few NaN values. Could you double check the code and see if there is an error?

Oops, indeed, that rms should have been a norm… I’ll correct the original post to match. Hopefully that’ll sort it out!

Thank you. The final images are stunning!