Intro

NiBabel is a low-level Python library that gives access to a variety of imaging formats, with a particular focus on providing a common interface to the various volumetric formats produced by scanners and used in common neuroimaging toolkits.

Saving

All NiBabel images have a .to_filename() method.

nibabel.save will attempt to convert to a reasonable image type, if the extension doesn’t match.

Some image types separate header and data into two separate images. Saving to either filename will generate both.

Serialization

Some NiBabel images can be serialized to and deserialized from byte strings, for performing stream-based I/O.

Spatial Images

For MRI studies, neuroimaging data is typically acquired as one or more volumes. NiBabel represents data as spatial images, a structure containing three things:

  • The image data array; a 3D or 4D array of image data

  • An affine matrix: 4 by 4 array relating voxel coordinates and world coordinates

  • Image metadata: usually a format-specific header

The Data Array

The data is a simple numpy array. It can be accessed, sliced and generally manipulated as we would any array.

Each location in the image data array is a voxel, and can be referred to with voxel coordinates (array indices).

This is ia natural way to describe a block of data, but is practically meaningless with regard to anatomy.

All of this information is encoded in the affine.

Affine transforms

The affine is a $4 \times 44 numpy array. This describes the transformation from the voxel space (indices(i,j,k)) to a reference space (coordinates (x,y,z)). These coordinates are, by convention, distance in mm right, anterior, and superior of an origin.

\[\begin{bmatrix} x\\ y\\ z\\ 1 \\ \end{bmatrix} = A\begin{bmatrix} i\\ j\\ k\\ 1 \\ \end{bmatrix}=\begin{bmatrix} m_{11}&m_{12}&m_{13}&a\\ m_{21}&m_{22}&m_{23}&b\\ m_{31}&m_{32}&m_{33}&c\\ 0&0&0& 1\\ \end{bmatrix}\begin{bmatrix} i\\ j\\ k\\ 1 \\ \end{bmatrix}\]

The Affine as a Series of Transformations

An affine transformation can be decomposed into translation, rotation, scaling and shear transformations, applied right-to-left.

NiBabel provides functions for extracting information from affines:

  • Orientation (or axis codes) indicates the direction an axis encodes. If increasing index along an axis moces to the right or left, the axis is coded “R” or “L”, respectively.

  • Voxel sizes (or zooms).

  • Obliquity measures the amount of rotation from “canonical” axes.

We can also use it to answer specific questions. For instance, the inverse affine allows you to calculate indices from RAS coordinates and look up the image intensity value at that location.

Remark

  • Affines provide the spatial interpretation of the data.

  • NiBabel has some useful methods for working with them.

Image Headers

The image header is specific to each file format. Minimally, it contains information to construct the data and affine arrays, but some methods are useful beyond that.

  • get_zooms(): returns voxel sizes. If the data is 4D, then repetition time is included as a temporal zoom.

  • get_data_dtype(): returns the numpy data type in which the image data is stored (or will be stored when the image is saved)

For images with fixed header structures, header fields are exposed through a dict-like interface.

Often, we are not particularly interested in the header, or even the affine. But it’s important to know they’re there and, especially, to remember to copy them when making new images, so that derivatives stay aligned with the original image.

Creating New Images

In many cases, we will want to use the image to compute a new image in the same space. Such a function might take the form:

def my_function(image):
    orig_data = image.get_fdata()
    new_data = some_operation_on(orig_data)
    return image.__class__(new_data, affine=image.affine, header=image.header)

Note that the image.__class__ ensures that the output image is the same type as the input. If your operation requires specific format features, you might use a specific class like nb.Nifti1Image.

Data Objects - Arrays and Array Proxies

Recall that the spatial image contains data, affine and header objects. When creating a new image, the data array is typically an array.

When loading a file, an ArrayProxy is used for most image types.

An array proxy is an object that knows how to access data, but does not read it until requested. The usual way to request data is get_fdata(), which returns all data as floating point.

Converting Array Proxies to Arrays

Array proxies are designed to be one step away from a numpy array: memory maps are arrays that remain on disk, rather than in RAM. This is only possible with uncompressed images.

We can also cast to any type we please, however unwisely.

Indexing and slicing array proxies

One of the primary motivation for an array proxy is to avoid loading data unnecessarily. Accessing the array proxy with array indices or slices will return the requested values without loading other data.

Scaling Array Proxies

Several image types, including NIfTI, have a slope and intercept (scaling factors) in the header that allows them to extend the data range and/or precision beyond those of the on-disk type.

For instance, uint16 can take the values 0-65535. If a BOLD series includes values from 500-2000, we can calculate a slope that will allow us to utilize the 16 bits of precision to encode our desired values.

Main Points

  • When in doubt, use img.get_fdata() will fetch all of the data, and it will always be a float

  • img.dataobj exists if you want to load only some data or control the data type

  • Both methods transparently scale data when needed

Surface Images

Although the scanner samples data in three dimensions, some brain structures are better represented as a convoluted sheet than a volume. Data may be usefully resampled onto a cortical sheet, but in the process, it loses the intrinsic geometry of a 3D array.

To represent data on a surface, we need the following structures:

  • The surface mesh
    • Vertices: a list of coordinates in world space
    • Faces: a list of 3-tuples of indices into the coordinate list
  • The data array: a 1D or 2D array of values or vectors at each vertex

Unlike spatial images, these components are frequently kept in separate files.

Terminological note: You are likely to encounter multiple names for each of these. Vertices may be called coordinates or a point set. Faces are often called triangles. When a data array is plotted on the surface, it might be called a texture.

Image-specific interfaces

The two suppported surface formats now are GIFTI and FreeSurfer geometry files. Unlike spatial images, there is not yet a common interface for working with surface-based data.

FreeSurfer encodes a great deal of information in its directory structure and file names, allowing the necessary data arrays to have relatively simple formats.

GIFTI is more of an interchange format, and has a rich metadata structure and can store different kinds of data.

CIFTI-2

CIFTI-2 is a file format intended to cover many use cases for connevtivity analysis.

Files have 2-3 dimensions and each dimension is decribed by one of 5 types of axis.

  • Brain models: each row/column is a voxel or vertex

  • Parcels: each row/column is a group of voxels and/or vertices

  • Scalars: each row/column has a unique name

  • Labels: each row/column has a unique name and label table

  • Series: each row/column is a point in a series (typically time series), which increases monotonically

For instance, “a parcellated dense connectivity” CIFTI-2 file has two dimensions, indexed by a brain models axis and a parcels axis.

The interpretation is connectivity from parcels to vertices and/or voxels.