All code for this document is located at here.

The nifti object

Note: Throughout this post, I will refer to an image on hard disk as a NIfTI, which is a file that generally has the extension “.nii” or “.nii.gz”. I will refer to the object in R as a nifti (note the change of font and case).

In this tutorial we will discuss the basics of the nifti object in R. There are many objects in R that represent imaging data. The Neuroconductor project chose the nifti object from the oro.nifti package as one of the the basic building blocks because it has been widely used in other packages, has been tested over a period of time, and inherits the properties of an array in R.

To run this code, you must have oro.nifti installed. You can either use the stable version on CRAN (using install.packages) or the development version (using devtools::install_github):

packages = installed.packages()
packages = packages[, "Package"]
if (!"oro.nifti" %in% packages) {
  install.packages("oro.nifti")
  ### development version
  # devtools::install_github("bjw34032/oro.nifti")
}

S4 Implementation

As nifti objects inherits the properties of an array, you can perform a series of operations on them, such as addition/subtraction/division, as you would an array. A nifti object has additional attributes and the nifti object is an S4 object. This means that you do not reference additional information using the $ operator.

library(oro.nifti)
set.seed(20161007)
dims = rep(10, 3)
arr = array(rnorm(10*10*10), dim = dims)
nim = oro.nifti::nifti(arr)
print(nim)
NIfTI-1 format
  Type            : nifti
  Data Type       : 2 (UINT8)
  Bits per Pixel  : 8
  Slice Code      : 0 (Unknown)
  Intent Code     : 0 (None)
  Qform Code      : 0 (Unknown)
  Sform Code      : 0 (Unknown)
  Dimension       : 10 x 10 x 10
  Pixel Dimension : 1 x 1 x 1
  Voxel Units     : Unknown
  Time Units      : Unknown
print(class(nim))
[1] "nifti"
attr(,"package")
[1] "oro.nifti"
oro.nifti::is.nifti(nim)
[1] TRUE

Accessing Information from a nifti

To access additional information, called a slot, you can use the @ operator. We do not recommend this, as there should be a function implemented to “access” this slot. These are hence called accessor functions (they access things!). For example, if you want to get the cal_max slot of a nifti object, you should use the cal_max function. If an accessor function is not implemented, you should still use the slot(object, name) syntax over @.

Here’s an example where we make an array of random normal data, and put that array into a nifti object with the nifti function:

nim@cal_max
[1] 2.706524
cal_max(nim)
[1] 2.706524
slot(nim, "cal_max")
[1] 2.706524

Accessing the “data”

If you want to access the “data” of the image, you can access that using:

data = slot(nim, ".Data")
class(data)
[1] "array"

With newer versions of oro.nifti (especially that on GitHub and in Neuroconductor), there is a img_data function to access the data:

data = oro.nifti::img_data(nim)
class(data)
[1] "array"
dim(data)
[1] 10 10 10

This array is 3-dimensional and can be subset using normal square-bracket notations ([row, column, slice]). Thus, if we want the 3rd “slice” of the image, we can use:

slice = data[,,3]
class(slice)
[1] "matrix"

Thus we see we get a matrix of values from the 3rd “slice”. We should note that we generally reference an image by x, y, and z planes (in that order). Most of the time, the x direction refers to going left/right on an image, y refers to front/back (or anterior/posterior), and the z direction refers to up/down (superior/inferior). The actual direction depends on the header information of the NIfTI image.

slice = data[,,3, drop = FALSE]
class(slice)
[1] "array"

Show all slots

You can see which slots exist for a nifti object by using slotNames

slotNames(nim)
 [1] ".Data"          "sizeof_hdr"     "data_type"      "db_name"       
 [5] "extents"        "session_error"  "regular"        "dim_info"      
 [9] "dim_"           "intent_p1"      "intent_p2"      "intent_p3"     
[13] "intent_code"    "datatype"       "bitpix"         "slice_start"   
[17] "pixdim"         "vox_offset"     "scl_slope"      "scl_inter"     
[21] "slice_end"      "slice_code"     "xyzt_units"     "cal_max"       
[25] "cal_min"        "slice_duration" "toffset"        "glmax"         
[29] "glmin"          "descrip"        "aux_file"       "qform_code"    
[33] "sform_code"     "quatern_b"      "quatern_c"      "quatern_d"     
[37] "qoffset_x"      "qoffset_y"      "qoffset_z"      "srow_x"        
[41] "srow_y"         "srow_z"         "intent_name"    "magic"         
[45] "extender"       "reoriented"    

If you would like to see information about each one of these slots, please see this blog post about the NIfTI header.

Other objects

Other packages, such as ANTsR and RNifti have implemented faster reading/writing functions of NIfTI images. These rely on pointers to object in memory and are very useful. They have specific implementations for extracting information from them and saving them out, such as in an Rda/rda (R data file). A series of conversion tools for ANTsR objects are included in the extrantsr package (function ants2oro) and nii2oro in oro.nifti for RNifti objects.

NIfTI Input/Output: readnii/writenii vs. readNIfTI/writeNIfTI

In the neurobase package, we provide wrapper functions readnii/writenii, which wrap the oro.nifti functions readNIfTI/writeNIfTI. There are a few reasons for this:

  1. You can pass a filename with a “.nii.gz” extension to writenii and an additional “.nii.gz” will not be added, whereas this will happen in writeNIfTI.
  2. writenii will try to discern the data type of the image before writing, which may be useful if you created a nifti by copying information from a previous nifti object.
  3. The default in readnii is reorient = FALSE, which generally does not error when reading in data, whereas readNIfTI defaults to reorient = TRUE. This is discussed below.
  4. Extraneous dimensions are automatically deleted with readnii. Note this may cause errors and is not desired 100% of the time.

Option reorient = FALSE

In readNIfTI default reorient = TRUE implicity uses the reorient function from oro.nifti. Although many neuroimaging software suites read the header and reorient the data based on that information, oro.nifti::reorient can only handle simple orientations, see oro.nifti::performPermutation documentation. Although reading the data in without reorienting can cause problems, such as not knowing right/left orientation, if multiple NIfTI files were created in the same way (assumingly from dcm2nii), they should ideally have the same orientation.

Derived data from an image will have the exact same orientation because derived nifti objects will copy the nifti header information from the nifti object it was derived from. Moreover, in many analyses, registration to an image or a template is common, and these have known orientations. We have found that if a user wants to reorient their data in R, using the reorient function can be used, but we prefer the default to be FALSE, otherwise reading in many NIfTI files result in an error from the orientation.

Operations of nifti objects

Although the nifti object is not a standard R object, you can perform standard operations on these objects, such as addition/subtraction and logic. This is referred to “overloaded” operators.

Logical operators

For example, if we want to create a nifti object with binary values, where the values are TRUE if the values in nim are greater than 0, we can simply write:

above_zero = nim > 0
class(above_zero)
[1] "nifti"
attr(,"package")
[1] "oro.nifti"
img_data(above_zero)[1]
[1] TRUE

We will refer to binary images/nifti objects as “masks”.

We can combine multiple operators, such as creating a binary mask for value greater than 0 and less than 2.

class(nim > 0 & nim < 2)
[1] "nifti"
attr(,"package")
[1] "oro.nifti"

Arithmetic on nifti objects

We can also show the

class(nim * 2)
[1] "nifti"
attr(,"package")
[1] "oro.nifti"
class(nim + (nim / 4))
[1] "nifti"
attr(,"package")
[1] "oro.nifti"
class(nim * nim)
[1] "nifti"
attr(,"package")
[1] "oro.nifti"
class(nim^2)
[1] "nifti"
attr(,"package")
[1] "oro.nifti"

Summaries

How many values actually are greater than zero? Here, we can use standard statistical functions, such as sum to count the number of TRUE indices:

sum(above_zero)
[1] 513

and similarly find the proportion of TRUE indices by taking the mean of these indicators:

mean(above_zero)
[1] 0.513

Again, as nifti is an S4 object, it should have the functionality described in the details of the help file for methods::S4groupGeneric:

min(nim)
[1] -3.517075
max(nim)
[1] 2.706524
range(nim)
[1] -3.517075  2.706524
class(abs(nim))
[1] "nifti"
attr(,"package")
[1] "oro.nifti"

Visualization of nifti objects

Here we will use real imaging data from the EveTemplate package:

library(EveTemplate)
eve = readEve(what = "Brain")

Orthographic view

The oro.nifti::orthographic function provides great functionality on displaying nifti objects in 3 different planes.

oro.nifti::orthographic(eve)

The neurobase::ortho2 function expands upon this with some different defaults.

neurobase::ortho2(eve)

We see that in ortho2 there are annotations of the orientation of the image. Again, if the image was not reoriented, then these many not be corrrect. You can turn these off with the add.orient argument:

neurobase::ortho2(eve, add.orient = FALSE)

Differences between orthographic and ortho2

The above code does not fully illustrate the differences between orthographic and ortho2. One marked difference is when you would like to “overlay” an image on top of another in an orthograhic view. Here we will highlight voxels greater than the 90th quantile of the image:

orthographic(eve, y = eve > quantile(eve, 0.9))

We see that the white matter is represented here, but we would like to see areas of the brain that are not over this quantile to be shown as the image. Let us contrast this with:

ortho2(eve, y = eve > quantile(eve, 0.9))

We see the image where the mask is 0 shows the original image. This is due to the NA.y argument in ortho2. The ortho2 (and orthograhic) function is based on the graphics::image function in R, as well as many other functions we will discuss below. When graphics::image sees an NA value, it does not plot anything there. The NA.y argument in ortho2 makes it so any values in the y argument (in this case the mask) that are equal to zero are turned to NA.

Bright values

If you have artifacts or simply large values of an image, it can “dampen” the viewing of an image. Let’s make one value of the eve template very large. We will set the voxel with the largest value to be that value times 5 :

eve2 = eve
eve2[which.max(eve)] = eve2[which.max(eve)] * 5

Let’s plot this image again:

ortho2(eve2)

We see a faint outline of the image, but this single large value affects how we view the image. The function robust_window calculates quantiles of an image, by default the 0 (min) and 99.9th quantile, and sets values outside of this range to that quantile. If you are familiar with the process of Winsorizing, this is the exact same procedure. Many times we use this function to plotting, but could be thought of an outlier dampening procedure. Let’s plot this windowed image:

ortho2(robust_window(eve2))

Changing the probs argument in robust_window, which is passed to quantile, can also be used to limit artifacts with remarkably low values. The zlim option can also denote which range of intensities that can be plotted:

ortho2(eve2, zlim = quantile(eve2, probs = c(0, 0.999)))

This is a bit more like trimming, however.

Double orthographic view

Sometimes you would like to represent 2 images side by side, of the same dimensions and orientation of course. The double_ortho function allows you to do this. Let’s read in the full Eve image, not just the brain

eve_full = readEve(what = "T1")

We can view the original T1 alongside the brain-extracted image:

double_ortho(eve_full, eve)

Single slice view

We may want to view a single slice of an image. The oro.nifti::image function can be used here. Note, graphics::image exists and oro.nifti::image both exist. The oro.nifti::image allows you to just write image(nifti_object), which performs operations and calls functions using graphics::image. This allows the user to use a “generic” version of image, which oro.nifti adapted specifically for nifti objects. You can see the help for this function in ?image.nifti.

Let’s plot an image of the 90th slice of eve

image(eve, z = 90)