This Julia package supplies functions
CIFTI.save for reading and writing files of the CIFTI-2 format (https://www.nitrc.org/projects/cifti) for fMRI data, along with some convenience functions for indexing into the data matrix.
The intended use case is for simple, fast reading of CIFTI data. No attempt has been made to comprehensively support the CIFTI specification. For more complex use cases in Julia, I recommend instead using Julia's cross-language interoperability to take advantage of one of several more comprehensive implementations (see the
ciftiTools R packages,
nibabel in Python, etc).
CIFTI.load function supplied here should work for any of the common CIFTI filetypes (dtseries, dscalar, ptseries, dconn, etc). If you have a CIFTI filetype that's not supported, please send me a sample (anonymized and containing only synthetic data) and I'll add support for it.
Version 1.2 introduces an experimental feature,
CIFTI.save, to save data out (either from a
CiftiStruct or simply from a
Matrix) to a copy of an existing CIFTI file on disk. Due to optional matrix transpositions and to conventions of row major versus column major order, it's tricky to ensure that data is written to disk in the right order and orientation in all cases, so please verify that it works as expected in your environment.
Due to Julia's column major storage convention, most CIFTI files will need to be transposed in order to store them in the orientation that users will probably expect. If you don't need to transpose, reading is extremely fast, and if you do, performance suffers but it's still quite fast. Here are some benchmarks achieved on my Ubuntu Linux machine:
|Read a dtseries of size 64k x 8k (w/ transpose)
|Read a dtseries of size 64k x 8k (no transpose)
|Read a dconn of size 59412 x 59412 (no tranpose)
The basic usage of
CIFTI.load is demonstrated below. A
CiftiStruct struct is returned, containing:
data: a numeric matrix of whatever data type is specified in the cifti file header
brainstructure: an OrderedDict of indices into anatomical structures as parsed from the CIFTI file's internal XML data
x = CIFTI.load(filename)
x.data # access the data Matrix
x.brainstructure # access the OrderedDict of anatomical indices
When reading in a CIFTI file, transposition will occur or not occur according to the following logic:
- If the file is stored on disk with spatial dimensions (either parcels or "grayordinates") along the columns but scalars or series elements along the rows (such as timepoints), the data matrix will be transposed for the sake of consistent representation.
- If the rows and columns both represent spatial elements, such as in connectivity matrices (pconns and dconns), then no transposition will be done, in part to avoid the cost of transposing large data in those cases. It is expected in these cases that you'll have a symmetric connectivity matrix, so transposition will not matter; but if this does not hold true for you for some reason, then pay attention to the orientation and make sure to do any transposing yourself if necessary.
In other words: data will be transposed if it's necessary in order to ensure that there's a spatial mapping along the rows, so that indexing can occur in a consistent manner.
Some convenience functions for indexing into
data are also supplied, taking advantage of the
BrainStructure enum types that constitute the keys of the
CiftiStruct.brainstructure dictionary. Constants
LR are supplied as a short-hand for
[CORTEX_LEFT, CORTEX_RIGHT], respectively.
x[L] # return a Matrix where the rows correspond to CORTEX_LEFT anatomical indices
x[R] # return a Matrix where the rows correspond to CORTEX_RIGHT anatomical indices
x[LR] # return a Matrix where the rows correspond to left or right coritical indices
Or you can index into the data using a vector of arbitrary
Important note: order matters in the vector that you specify, so the two lines above will return matrix subsets of the same size but differently sorted.
As of version 1.2, data from a
Matrix can be written to disk by specifying a
template, i.e. an existing CIFTI file that has the desired properties. A copy of
template will be created on disk, with its data component replaced with the new data that you supply. See the note in the introduction, however, about this function's experimental status, and be sure to verify that outputs are oriented correctly.
output_path = "my_output_filename.dtseries.nii"
template_path = "path_to_an_existing_cifti_file.dtseries.nii"
CIFTI.save(output_path, x; template = template_path)
# it also works if you pass a Matrix instead of a CiftiStruct:
my_matrix = randn(Float32, size(x))
CIFTI.save(output_path, my_matrix; template = template_path)