Working with NIfTI images#

NIfTI stands for Neuroimaging Informatics Technology Initiative, which is jointly sponsored by the US National Institute of Mental Health and the National Institute of Neurological Disorders and Stroke. NIfTI defines a file format for neuroimaging data that is meant to meet the needs of the fMRI research community. In particular, NIfTI was developed to support inter-operability of tools and software through a common file format. Prior to NIfTI there were a few major fMRI analysis software packages, and each used a different file format. NIfTI was designed to serve as a common file format for all of these (and future) neuroimaging software packages.

NIfTI was derived from an existing medical image format, called ANALYZE. ANALYZE was originally developed by the Mayo Clinic in the US, and was adopted by several neuroimaging analysis software packages in the 1990s. The ANALYZE header (where meta-data are stored) had extra fields that were not used, and NIfTI format basically expands on ANALYZE by using some of those empty fields to store information relevant to neuroimaging data. In particular the header stores information about the position and orientation of the images. This was a huge issue prior to NIfTI. In particular, there were different standards for how to store the order of the image data. For example, some software packages stored the data in an array that started from the most right, posterior, and inferior voxel, with the three spatial dimensions ordered right-to-left, posterior-to-anterior, and then inferior-to-superior. This is referred to as RPI orientation. Other packages that also used ANALYZE data stored the voxels in RAI format (with the second dimension going anterior-to-posterior) or LPI format (reversing left and right). This caused a lot of problems for researchers, especially if they wanted to try different analysis software, or use a pipeline that involved tools from different software packages. In some cases, this was just annoying (e.g., having to reverse the anterior-posterior dimension of an image). In other cases, it was confounding and potentially created erroneous results. This was especially true of the right-left (x) dimension. While it is immediately obvious when viewing an image which the front and back, and top and bottom, of the brain are, the left and right hemispheres are typically indistinguishable from eahc other, so a left-right swap could easily go undetected, potentially leading researchers to make completely incorrect conclusions about which side of the brain activation occurred on! The NIfTI format was designed to help prevent this by more explicitly storing orientation information in the header.

Another improvement with the NIfTI format was to allow a single file. ANALYZE format requires two files, a header (with a .hdr extension) and the image data itself (.img). These files had to have the same name prior to the extension (e.g., brain_image.hdr and brain_image.img), and doubled the number of files in a directory of images, which created more clutter. NIfTI defines a single image file ending in a .nii extension. As well, NIfTI images can be compressed using a standard, open-source algorithm known as Gzip, which can significantly reduce file sizes and thus the amount of storage required for imaging data. Since neuroimaging data files tend to be large, this compression was an important feature.

Although other file formats are still used by some software, NIfTI has become the most widely used standard for fMRI and other MRI research data file storage. Here we will learn how to convert a DICOM file to NIfTI format, which is typically the first step in an MRI research analysis pipeline, since most MRI scanners produce DICOM files, but the software researchers use to process their data reads NIFTI and not DICOM format.

Import packages#

Here we load in three new Python packages designed to work with NIfTI data:

  • dicom2nifti converst DICOM images to NIfTI format

  • NiBabel reads and converts between NIfTI and several other common neuroimaging file formats, including ANALYZE

  • NiLearn is primarily designed to provide statistical analysis and machine learning tools for neuroimaging data. However, it also provides a number of utilities for reading and writing NIfTI images, and working with and visualizing data

As well we’ll load SciPy’s ndimage package, and Matplotlib

import dicom2nifti
import nibabel as nib
import nilearn as nil
import scipy.ndimage as ndi
import matplotlib.pyplot as plt
import os

We will use dicom2nifti’s convert_directory() function to convert the structural MRI images we worked with in the previous lesson from DICOM to NIfTI. We pass it the name of the folder in which the DICOM images are saved, and also instruct it to compress the resulting NIfTI file (to save space). We also use the reorient=True kwarg to force the image to be written in LAI orientation (i.e., starting with the most left, anterior, and inferior voxel), which ensures there is no ambiguity about the resulting NIfTI image.

convert_directory does not take an argument for the output file name. Instead, it uses the name of the scan that was used when it was acquired on the MRI scanner. This might seem like a frustrating lack of control, however it does ensure that there are no user errors in the conversion process, that could result in mis-identified files. Here we will first list the contents of the data folder, then run convert_directory, then list the contents again to see the new NIfTI file and what it is named:

os.listdir('data')
['.DS_Store',
 '4_sag_3d_t1_spgr.nii.gz',
 'anatomical_aug13_001.hdr',
 'Anat001.20040930.145131.5.T1_GRE_3D_AXIAL.0099.dcm',
 'anatomical_aug13_001.img',
 'DICOM']
dicom2nifti.convert_directory('data/DICOM', 'data', compression=True, reorient=True)
os.listdir('data')
['.DS_Store',
 '4_sag_3d_t1_spgr.nii.gz',
 'anatomical_aug13_001.hdr',
 'Anat001.20040930.145131.5.T1_GRE_3D_AXIAL.0099.dcm',
 'anatomical_aug13_001.img',
 'DICOM']

So the new converted file is 4_sag_3d_t1_spgr.nii.gz

Load NIfTI file#

We use NiBabel to read in the NIfTI file we just created:

brain_vol = nib.load('data/4_sag_3d_t1_spgr.nii.gz')

# What is the type of this object?
type(brain_vol)
nibabel.nifti1.Nifti1Image

View metadata#

We can view the image’s header by printing it (note that due to how the NiBabel Nifti1Image object is coded, we need to print() the header rather than just asking for it as a property):

print(brain_vol.header)
<class 'nibabel.nifti1.Nifti1Header'> object, endian='<'
sizeof_hdr      : 348
data_type       : b''
db_name         : b''
extents         : 0
session_error   : 0
regular         : b''
dim_info        : 0
dim             : [  3 184 256 256   1   1   1   1]
intent_p1       : 0.0
intent_p2       : 0.0
intent_p3       : 0.0
intent_code     : none
datatype        : int16
bitpix          : 16
slice_start     : 0
pixdim          : [-1.  1.  1.  1.  1.  1.  1.  1.]
vox_offset      : 0.0
scl_slope       : nan
scl_inter       : nan
slice_end       : 0
slice_code      : unknown
xyzt_units      : 2
cal_max         : 0.0
cal_min         : 0.0
slice_duration  : 0.0
toffset         : 0.0
glmax           : 0
glmin           : 0
descrip         : b''
aux_file        : b''
qform_code      : unknown
sform_code      : aligned
quatern_b       : 0.0
quatern_c       : 1.0
quatern_d       : 0.0
qoffset_x       : 89.6006
qoffset_y       : -76.964
qoffset_z       : -157.9353
srow_x          : [-1.     -0.     -0.     89.6006]
srow_y          : [  0.      1.     -0.    -76.964]
srow_z          : [  -0.       -0.        1.     -157.9353]
intent_name     : b''
magic           : b'n+1'

Access data in the NIfTI object#

NiBabel’s handling of the NIfTI format data is not quite as elegant as what we saw in the previous lesson. Rather than being able to access the data directly by referencing the name of the object (in this case, brain_vol), we need to use the method get_fdata() to do this (the “f” in this method name stands for “floating point”, as this is the type of data it returns). We will assign the result of this to a new variable so that it’s easy to work with.

brain_vol_data = brain_vol.get_fdata()
type(brain_vol_data)
numpy.ndarray

We see that the data is a familiar NumPy array, and below we see the dimensions are identical to what we saw for this image in the previous lesson:

brain_vol_data.shape
(184, 256, 256)

Visualize a slice#

We can use .plt.imshow() as in the previous lesson:

plt.imshow(brain_vol_data[96], cmap='bone')
plt.axis('off')
plt.show()
../_images/432e1e717d310147726f8b9566a0b833e5439ba52ab4a15db777fa1b90043298.png

Note that our image is rotated, so use can use ndi.rotate to fix this:

plt.imshow(ndi.rotate(brain_vol_data[96], 90), cmap='bone')
plt.axis('off')
plt.show()
../_images/21a3333fa6863b831f442ad889afa4be2e9d4265d00cf7f7658f6459809c9bb0.png

Plot a series of slices#

fig_rows = 4
fig_cols = 4
n_subplots = fig_rows * fig_cols
n_slice = brain_vol_data.shape[0]
step_size = n_slice // n_subplots
plot_range = n_subplots * step_size
start_stop = int((n_slice - plot_range) / 2)

fig, axs = plt.subplots(fig_rows, fig_cols, figsize=[10, 10])

for idx, img in enumerate(range(start_stop, plot_range, step_size)):
    axs.flat[idx].imshow(ndi.rotate(brain_vol_data[img, :, :], 90), cmap='gray')
    axs.flat[idx].axis('off')
        
plt.tight_layout()
plt.show()
../_images/65bab7e52f7f62e2a94298f6d0964fe031046637d8e3d6d72baebcf5a7a7132e.png

Plot with NiLearn#

While SciPy’s ndimage module was designed for working with a wide variety of image types, NiLearn was designed to work with neuroimaging data specifically. As such, it’s tools are a bit easier to use and more purpose-built for tasks that neuroimaging data scientists might want to perform. For example, we can plot the NiBabel NIfTI image object directly without first having to extract the data, using the plot_img() function from NiLearn’s plotting module:

from nilearn import plotting

plotting.plot_img(brain_vol)
plt.show()
../_images/38b7dd1cf179d72394dffafa744b5cf02dc17c8681620fbf2a8cb7d4bc265f9d.png

One nice thing that we see is that since NiLearn is neuroimaging-aware, it explicitly adds labels to our plot showing us clearly which the left and right hemispheres are.

NiLearn’s plotting library uses Matplotlib, so we can use familiar tricks to do things like adjust the image size and colormap:

from nilearn import plotting

fig, ax = plt.subplots(figsize=[10, 5])
plotting.plot_img(brain_vol, cmap='gray', axes=ax)
plt.show()
../_images/09f4a44ee92b61cd9a4e8afce5d9fe0e43a16a6df825affaaf98558000550132.png

The plot_img() function also provides a variety of ways to display the brain, with much less code than we had to use when working with raw NumPy arrays and Matplotlib functions:

plotting.plot_img(brain_vol, display_mode='tiled', cmap='gray')
plt.show()
../_images/c5298f59a0324851745d7ffee09ffe596a3432d59d73d2b1d31c72f0ad44ba7d.png

We can use the cut_coords kwarg to specify there to centre the crosshairs and “cuts” through the image that we visualize. In this image, the coordinates are relative to the *isocenter of the MRI scanner — the centre of the magnetic field inside the scanner. The position of a person’s head relative to this isocenter will vary from individual to individual, and scan to scan, due to variations in head size and the optimizations used by the MRI technician and scanner. But we can use the coordinates printed in the above image (which defaulted to the centre of the image volume) and some trial-and-error to get a different view through the brain:

plotting.plot_img(brain_vol, cmap='gray', cut_coords=(-45, 40, 0))
plt.show()
../_images/079f68f408dc24185d4195cb7e382b9a29030c6165002e75833ee21ef1d055ac.png

plot_img() also has a few other ways to see multiple slices at once:

plotting.plot_img(brain_vol, display_mode='x', cmap='gray')
plt.show()
../_images/76b50c6a0017191a946a3572c0330581bc3c54d7b21091051c81eb1099bd8f54.png
plotting.plot_img(brain_vol, display_mode='mosaic', cmap='gray')
plt.show()
../_images/c6de5a003be996ed59d74efc2f3d2f8eb2b8c80fc59eef4011110e2383e2967a.png

Smoothing#

NiLearn has its own function for applying Gaussian spatial smoothing to images as well. The only real difference from scipy.ndimage’s gaussian_filter() function is that instead of specifying the smoothing kernel in standard deviations, we specify it in units of full width half-maximum (FWHM). This is the standard way that most neuroimaging analysis packages specify smoothing kernel size, so it is preferable to SciPy’s approach. As the term implies, FWHM is the width of the smoothing kernel, in millimetres, at the point in the kernel where it is half of its maximum height. Thus a larger FWHM value applies more smoothing.

from nilearn import image

fwhm = 4

brain_vol_smth = image.smooth_img(brain_vol, fwhm)
plotting.plot_img(brain_vol_smth, cmap='gray', cut_coords=(-45, 40, 0))
plt.show()
../_images/d1d6c77e6a64283c23a99138613b9327da457f01b487e0fcb9d47684a593c21c.png