This Julia package supplies functions CIFTI.load
and 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 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 in other languages (see the cifti
and ciftiTools
R packages, nibabel
in Python, etc).
The 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 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 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) | 6 s |
Read a dtseries of size 64k x 8k (no transpose) | 1 s |
Read a dconn of size 59412 x 59412 (no tranpose) | 33 s |
But these speeds will largely depend on your hardware. On other systems, I've had the latter operation run as fast as 10 seconds.
Within Julia:
using Pkg
Pkg.add("CIFTI")
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 headerbrainstructure
: 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 L
, R
, and LR
are supplied as a short-hand for CORTEX_LEFT
, CORTEX_RIGHT
, and [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 BrainStructure
keys:
x[[AMYGDALA_LEFT, AMYGDALA_RIGHT]]
x[[AMYGDALA_RIGHT, AMYGDALA_LEFT]]
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 CiftiStruct
or 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.
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)