Jörn Alexander Quent's notebook

where I share semi-interesting stuff from my work

A way to get MNI coordinates for subcortical voxels of a CIFTI file

What do I want to do?

In this post, I will demonstrate how it is possible to use the R packages cifti and ciftiTools to get the MNI coordinates of the subcortical voxels in your CIFTI file. This table can then be used to select voxels based on their coordinates and do further things with them like creating masks.

For this, we convert the IJK voxel index coordinates found in th XML meta data of the CIFTI file to MNI coordinates.

Libraries used

library(data.table)
library(cifti)
library(ciftiTools)
ciftiTools.setOption("wb_path", "D:/Program Files/workbench/bin_windows64")
## Using this Workbench path: 'D:/Program Files/workbench/bin_windows64/wb_command.exe'.
Click here for detailed session information.

Note that I am using ciftiTools installed from the 12.0 GitHub branch even though it still says 0.11.0 below.

sessioninfo::session_info()
## - Session info ---------------------------------------------------------------
##  setting  value
##  version  R version 4.2.2 (2022-10-31 ucrt)
##  os       Windows 10 x64 (build 19045)
##  system   x86_64, mingw32
##  ui       RTerm
##  language (EN)
##  collate  English_United States.1252
##  ctype    English_United States.1252
##  tz       Asia/Taipei
##  date     2023-07-24
##  pandoc   2.19.2 @ C:/Program Files/RStudio/resources/app/bin/quarto/bin/tools/ (via rmarkdown)
## 
## - Packages -------------------------------------------------------------------
##  package      * version date (UTC) lib source
##  abind          1.4-5   2016-07-21 [1] CRAN (R 4.2.0)
##  base64enc      0.1-3   2015-07-28 [1] CRAN (R 4.2.0)
##  bitops         1.0-7   2021-04-24 [1] CRAN (R 4.2.0)
##  cifti        * 0.4.5   2018-02-01 [1] CRAN (R 4.2.3)
##  ciftiTools   * 0.11.0  2023-07-24 [1] Github (mandymejia/ciftiTools@109fb2e)
##  cli            3.6.0   2023-01-09 [1] CRAN (R 4.2.2)
##  data.table   * 1.14.8  2023-02-17 [1] CRAN (R 4.2.2)
##  digest         0.6.31  2022-12-11 [1] CRAN (R 4.2.2)
##  evaluate       0.20    2023-01-17 [1] CRAN (R 4.2.2)
##  fastmap        1.1.0   2021-01-25 [1] CRAN (R 4.2.2)
##  gifti          0.8.0   2020-11-11 [1] CRAN (R 4.2.3)
##  htmltools      0.5.4   2022-12-07 [1] CRAN (R 4.2.2)
##  knitr          1.42    2023-01-25 [1] CRAN (R 4.2.2)
##  oro.nifti      0.11.4  2022-08-10 [1] CRAN (R 4.2.2)
##  R.methodsS3    1.8.2   2022-06-13 [1] CRAN (R 4.2.2)
##  R.oo           1.25.0  2022-06-12 [1] CRAN (R 4.2.2)
##  R.utils        2.12.2  2022-11-11 [1] CRAN (R 4.2.3)
##  RColorBrewer   1.1-3   2022-04-03 [1] CRAN (R 4.2.0)
##  Rcpp           1.0.10  2023-01-22 [1] CRAN (R 4.2.2)
##  rlang          1.0.6   2022-09-24 [1] CRAN (R 4.2.2)
##  rmarkdown      2.20    2023-01-19 [1] CRAN (R 4.2.2)
##  RNifti         1.4.5   2023-01-30 [1] CRAN (R 4.2.2)
##  rstudioapi     0.14    2022-08-22 [1] CRAN (R 4.2.2)
##  sessioninfo    1.2.2   2021-12-06 [1] CRAN (R 4.2.2)
##  viridisLite    0.4.1   2022-08-22 [1] CRAN (R 4.2.2)
##  xfun           0.37    2023-01-31 [1] CRAN (R 4.2.2)
##  xml2           1.3.3   2021-11-30 [1] CRAN (R 4.2.2)
##  yaml           2.3.7   2023-01-23 [1] CRAN (R 4.2.2)
## 
##  [1] C:/Users/Alex/AppData/Local/R/win-library/4.2
##  [2] C:/Program Files/R/R-4.2.2/library
## 
## ------------------------------------------------------------------------------

Loading the CIFTI files

Below, you can see how I load the CIFTI file from “A Multi-modal Parcellation of Human Cerebral Cortex”.

# Surface and cifti files
cifti_fname  <- "sourceFiles/Q1-Q6_RelatedValidation210.CorticalAreas_dil_Final_Final_Areas_Group_Colors_with_Atlas_ROIs2.32k_fs_LR.dlabel.nii"
surfLeft     <- "sourceFiles/S1200.L.inflated_MSMAll.32k_fs_LR.surf.gii"
surfRight    <- "sourceFiles/S1200.R.inflated_MSMAll.32k_fs_LR.surf.gii"

# Read cifti via ciftiTools
xii <- ciftiTools::read_cifti(cifti_fname, brainstructures = "all", 
                  surfL_fname = surfLeft, 
                  surfR_fname = surfRight)

# Get some info for the cifti file via ciftiTools
xii_info <- info_cifti(cifti_fname)

We can get some information of what we just loaded by using

summary(xii)
## ====CIFTI METADATA===================
## Intent:           3007 (dlabel)
## - names           "vertex areas"
## Measurements:     1 column
## 
## ====BRAIN STRUCTURES=================
## - left cortex     29696 data vertices
##                   2796 medial wall vertices (32492 total)
##                   left surface geometry is present
## 
## - right cortex    29716 data vertices
##                   2776 medial wall vertices (32492 total)
##                   right surface geometry is present
## 
## - subcortex       31870 data voxels
##                   subcortical structures and number of voxels in each:
##                     Cortex-L (0), Cortex-R (0),
##                     Accumbens-L (135), Accumbens-R (140),
##                     Amygdala-L (315), Amygdala-R (332),
##                     Brain Stem (3472),
##                     Caudate-L (728), Caudate-R (755),
##                     Cerebellum-L (8709), Cerebellum-R (9144),
##                     Diencephalon-L (706), Diencephalon-R (712),
##                     Hippocampus-L (764), Hippocampus-R (795),
##                     Pallidum-L (297), Pallidum-R (260),
##                     Putamen-L (1060), Putamen-R (1010),
##                     Thalamus-L (1288), Thalamus-R (1248).

These steps can all be done using ciftiTools, however as far as I know we can’t get the XML meta data with ciftiTools. For this, we can use the package cifti.

# Get the XML meta
xii_xml      <- cifti_xml(cifti_fname)
xii_xml_list <- xml2::as_list(xii_xml)

Get information from XML

After loading the XML data, I convert it to a list using the xml2 package.

Now we have loaded everything that is needed to create a table with all MNI coordinates. xii_xml_list is quite a complicated structure so I am not really going through this. The important bit is that it contains VoxelIndicesIJK (see here), which we need to get our MNI coordinates. The voxel index coordinates can be found in matrix index map:

matrix_indices_map <- xii_xml_list$CIFTI$Matrix[[3]]

In matrix_indices_map, List 4 to 22 contains the information for the subcortical voxels in the order listed in the summary above. I will now extract all voxel index coordinates and put them into one 31870 by 3 matrix.

# First lets get the regions in the order
regions <- c("Accumbens-L", "Accumbens-R", "Amygdala-L", "Amygdala-R", "Brain Stem", 
             "Caudate-L", "Caudate-R", "Cerebellum-L", "Cerebellum-R", "Diencephalon-L", 
             "Diencephalon-R", "Hippocampus-L", "Hippocampus-R", "Pallidum-L", "Pallidum-R", 
             "Putamen-L", "Putamen-R", "Thalamus-L",  "Thalamus-L")

# Loop through regions from 4 to 22
numVoxels       <- rep(NA, length(regions))
VoxelIndicesIJK <- matrix(ncol = 3, nrow = 0)

for(i in 4:22){
  # Get the current region and the coordinates as a string
  currentRegion          <- matrix_indices_map[[i]]
  VoxelIndicesIJK_string <- currentRegion$VoxelIndicesIJK[[1]]
  tempM                  <- as.matrix(fread(VoxelIndicesIJK_string))
  
  # Now the we have a matrix of the coordinates, we need to convert them from integer
  # to numeric because otherwise the matrix multiplication doesn't work
  tempM <- matrix(as.numeric(tempM), ncol = ncol(tempM))
  
  # Get the number of voxels for that region
  numVoxels[i - 3] <- nrow(tempM)
  
  # Add to VoxelIndicesIJK
  VoxelIndicesIJK <- rbind(VoxelIndicesIJK, tempM)
}

After running this, our matrix VoxelIndicesIJK has the desired dimensions (31870, 3). This fits with the number of subcortical voxels listed in the summary, so everything is good. Now, we can repeat the region names so they match the rows of the matrix.

regions_row <- rep(regions, times = numVoxels)

Convert from voxel index coordinates to MNI

After all that preparation, we can simply use the translation matrix and follow these steps. We get the translation matrix from the meta data of the xifti variable.

A <- xii$meta$subcort$trans_mat

The translation matrix is

-2 0 0 90
0 2 0 -126
0 0 2 -72
0 0 0 1

We need to add a column of 1s (no idea actually why) and transpose the matrix.

VoxelIndicesIJK_t <- t(cbind(VoxelIndicesIJK, 1))

Once this is done, we can get the MNI coordinates by using this formula: $MNI^\intercal = A * IJK^\intercal$.

# Convert from IJK to MNI
MNI <- t(A %*% VoxelIndicesIJK_t)

# Remove unnecessary 4th column
MNI <- MNI[, -4]

Finally, we got what we wanted, which is a 31870 by 3 matrix with the MNI coordinates as columns. Here are the first six values, which should all belong to the left accumbens:

head(MNI)
##      [,1] [,2] [,3]
## [1,]   -8    6  -16
## [2,]  -10    6  -16
## [3,]   -6    8  -16
## [4,]   -8    8  -16
## [5,]  -10    8  -16
## [6,]   -6    4  -14

As a last step, I am going to verify whether the MNI coordinates fit. For that, I take a random voxel from the left hippocampus and use wb_view to see if this voxel is indeed in the hippocampus based on the parcellation file used above.

# Select random voxel
set.seed(20230724)
rowIndex <- sample(which(regions_row == "Hippocampus-L"), 1)

# Show coordinate
MNI[rowIndex, ]
## [1] -30 -26 -16

When I use wb_view and look this voxel up, then it is definitely in the hippocampus.


Write MNI coordinates to .csv file

# Convert to DF
MNI_DF <- as.data.frame(MNI)

# Add region names as another column
MNI_DF <- cbind(regions_row, MNI_DF)

# Rename columns
names(MNI_DF) <- c("region", "x", "y", "z")

# Write csv
write.csv(MNI_DF, file = "cifti_subcortical_MNI152_coordinates.csv", 
          row.names = FALSE,
          quote = FALSE)

This file

head(MNI_DF)
##        region   x y   z
## 1 Accumbens-L  -8 6 -16
## 2 Accumbens-L -10 6 -16
## 3 Accumbens-L  -6 8 -16
## 4 Accumbens-L  -8 8 -16
## 5 Accumbens-L -10 8 -16
## 6 Accumbens-L  -6 4 -14

can now be downloaded here.

Edit 25/07/2023

The maintainer of the GitHub page pointed out that the information saved in the xifti variable is actually also enough to calculate the voxel index coordinates. This can be done via:

VoxelIndicesIJK <- which(xii$meta$subcort$mask, arr.ind=TRUE) - 1

The minus 1 seems to be necessary to ensure both values are the same.


Share