knitr::opts_chunk$set( collapse = TRUE, comment = "#>", eval = TRUE, echo = TRUE, fig.width = 7, fig.height = 5, fig.align = 'center' )
This page is still under construction! Check back later for updates!
R is not well suited for big data sets. NIfTI images, depending on the image dimension and voxel size, can be quite large when loaded and memory is a concern. As sample size or number of scans increases it becomes difficult to perform simple operations voxel-wise across subjects. For example, calculating the mean image across 800 subjects is computationally intense since not all 800 subjects can be loaded into memory at once. Therefore, there is a need for alternative approaches.
The NiftiArray package allows for fast random access
of imaging data in NIfTI format and supports DelayedArray operations. The package establishes the NiftiArray class, a convenient and memory-efficient array-like container for on-disk representation of NIfTI image(s). The NiftiArray class is an extension of the HDF5Array class and converts NIfTI objects on disk to HDF5 files which allow for block processing and memory-efficient representations in R.
NiftiArray is compatible with the DelayedArray and DelayedMatrixStats packages.
DelayedArray is an R package currently hosted on Bioconductor. DelayedArray allows common array operations on an object without loading it into memory. In order to reduce memory usage and optimize performance, operations on the object are either delayed or executed using a block processing mechanism. DelayedArray works with the NiftiArray class to delay computation and allow fast and efficient data access.
DelayedMatrixStats is an R package currently hosted on Bioconductor. DelayedMatrixStats contains functions for statistical calculations (i.e. row or column median calculation) using DelayedArray efficient block processing on large matrices while keeping local memory usage low. DelayedMatrixStats builds on both the DelayedArray and matrixStats packages to allow for high-performing functions operating on rows and columns of DelayedMatrix objects. The functions are optimized by data type and for subsetted calculations such that both memory usage and processing time is minimized.
You can install the development version of NiftiArray from GitHub using the following:
# install.packages('remotes') remotes::install_github("muschellij2/NiftiArray")
We are working to get a stable version on Neuroconductor.
The packages you will need to load for use with this tutorial are below:
library(DelayedArray) library(DelayedMatrixStats) library(dplyr) library(forcats) library(ggplot2) library(gridExtra) library(kableExtra) library(knitr) library(microbenchmark) library(RNifti) library(NiftiArray) library(neurobase) library(pbapply) library(profmem) library(remotes) library(stringr) library(tibble) library(tidyr) nifti_header = NiftiArray::nifti_header
This tutorial will use data found here. A description of the data is available here. In this tutorial we will use the FLAIR images for subjects 1-5. We will download the data to a temporary folder. If you prefer to load these to a specific directory feel free to change the nii_destination in fileinfo to the file path where you'd like to save each image.
# Information about URL to download and where to save the image locally in destiation urls = file.path("https://raw.githubusercontent.com", "muschellij2", "open_ms_data", "master", "cross_sectional", "MNI", paste0("patient0", 1:5), "FLAIR_N4_noneck_reduced_winsor_regtoFLAIR_brain_N4_regtoMNI.nii.gz") nii_destinations = sapply(urls, function(x) tempfile(fileext = ".nii.gz")) hdf5_destinations = sub(".nii.gz", ".h5", nii_destinations) fileinfo = tibble::tibble(url = urls, nii_destination = nii_destinations, hdf5_destination = hdf5_destinations) # Download all the files to the nii_destination mapply(function(x,y) { download.file(url = x, destfile = y) }, fileinfo$url, fileinfo$nii_destination) # Notice the files were saved to a temporary directory on your machine # Change tempdir() to the directory where you saved the files if you adapted nii_destination list.files(tempdir(), full.names = TRUE)
if (Sys.info()[["user"]] == 'alval') { # Information about URL to download and where to save the image locally in destiation data_dir = '/Users/alval/Box/rpackages/NiftiArray_examples/data/' fileinfo = tibble::tibble( url = fileinfo$url, nii_destination = paste0(data_dir, "nii/", "flairpatient0", 1:5, ".nii.gz"), hdf5_destination = paste0(data_dir, "hdf5/", "flairpatient0", 1:5, ".h5")) } temp = neurobase::readnii(fileinfo$nii_destination[1])
These data are $182 \times 218 \times 182$ with pixel dimension $1 mm \times 1mm \times 1 mm$. On disk they take up approximately 5MB as nii.gz compressed objects. When loaded into R, each image consumes approximately 55 MB worth of memory.
Obviously, imaging data can be smaller or larger in memory depending on dimension and pixel size but naturally all imaging data will run into memory restrictions. NiftiArray attempts to overcome these problems by converting NIfTI objects to HDF5 files, an efficient random access data type on disk. NiftiArray conserves the array structure and NIfTI header. Therefore, interacting with NiftiArray objects in R is similar to regular NIfTI objects. The benefit here is the images remain on disk and are not ever fully loaded so memory is conserved. See Comparison of NiftiArray to Traditional Methods section for memory comparisons.
NiftiArrayIn the Data section we downloaded data for this tutorial into a temporary directory and recorded the file paths in which they were saved. These files are .nii.gz file types. Unfortunately, these NIfTI compressed images cannot be fast random accessed. In order to utilize the NiftiArray functionality these data must be converted to HDF5 files on disk. This will not affect how you interact with the object in R only how it is stored on disk. Unfortunately, saving this objects out as HDF5 objects will require the same amount of memory on disk so if you keep the NIfTI images on disk as well as the HDF5 files you will double your memory usage on disk. In R though, these objects will use almost no memory. This is where NiftiArray shines.
NiftiArray::writeNiftiArrayTo convert a NIfTI object to an HDF5 file and eventually a local NiftiArray object you can use the NiftiArray::writeNiftiArray function. By default this function will store the data in a temporary folder, similar to where we saved the data for this tutorial. You can over-ride this though by specifying the filepath option.
Note: When calling NiftiArray::writeNiftiArray you are converting the NIfTI object on disk to a HDF5 file with a NIfTI-specific grouping or hierarchical format. See this link for more information on groups within HDF5 files. In the event that users have stored other information in groups inside the HDF5 file output from NiftiArray::writeNiftiArray we did not want to support over-writing the file and therefore additional information. Instead, we simply over-write the groups that are associated with a NiftiArray object inside the HDF5 file. To over-write these groups, set overwrite = TRUE in the NiftiArray::writeNiftiArray function.
Let's convert and write the first subject's data as a temporary file to the temporary directory on-disk. This temporary file will have the pattern NiftiArraypatient01 in the file name. Again, if you'd like to save this object somewhere other than the temporary directory simply change the filepath option in NiftiArray::writeNiftiArray.
# Write the NiftiArray object to disk and load it into R as patient01 patient01 = NiftiArray::writeNiftiArray( fileinfo$nii_destination[1], filepath = fileinfo$hdf5_destination[1], overwrite = TRUE) file.exists(fileinfo$hdf5_destination[1])
The NIfTI was converted to the the NIfTI-HDF5 file format and saved on disk. It was also loaded into memory is a NiftiArray object and returned in R as patient01. patient01 is of class NiftiArray.
class(patient01)
The NIfTI header is conserved in the NiftiArray class in case you ever need to quality control or convert a NiftiArray back to a NIfTI image. You can extract the header information from a NiftiArray object using the NiftiArray::nifti_header function.
# Print the NIfTI header associated with patient01 NiftiArray::nifti_header(patient01)
In the next section we will re-load patient01 using the NiftiArray::NiftiArray function. Let's remove the patient01 object from R memory so we can re-load it later.
# Remove patient01 object from R memory so it can be re-loaded later rm(patient01)
The NiftiArray::writeNiftiArray function takes a single file path and converts the NIfTI image on disk to the HDF5 file on disk while also loading the NiftiArray into local memory. By default, the images are saved to a temporary directory. This is useful for quick one time calculations or conversions and ensures that on-disk storage remains minimal since the temporary directories delete the files systematically over time.
When you want to work with NiftiArray objects for lots of subjects frequently though the conversion process may take some valuable compute time and memory. Instead, in these instances if on-disk storage is not an issue we suggest saving the HDF5 NiftiArray objects to their own folder and loading NiftiArray into R using NiftiArray::NiftiArray. For only a few images compute time for both approaches (NiftiArray::writeNiftiArray and NiftiArray::NiftiArray) are fast and comparable but for larger data sets of reasonable dimension and pixel size it can take a few minutes to convert and save the HDF5 object and a few minutes to load into R locally. See the Compute Time section for more details about differences in compute time between NiftiArray::writeNiftiArray and NiftiArray::NiftiArray.
NiftiArray::NiftiArrayThe NiftiArray function can be used to load an on disk NIfTI object into R as a NiftiArray.
# Reload patient01 using NiftiArray::NiftiArray from a NIfTI object on disk patient01 = NiftiArray::NiftiArray(fileinfo$nii_destination[1]) # Remove patient01 object from R memory so it can be re-loaded later rm(patient01)
It can also load the NiftiArray object from the HDF5 file converted using NiftiArray::writeNiftiArray object on-disk and stored as a HDF5 it can be loaded into R using the NiftiArray::NiftiArray function.
# Reload patient01 using NiftiArray::NiftiArray from a HDF5 object on disk patient01 = NiftiArray::NiftiArray(fileinfo$hdf5_destination[1])
Notice this is exactly the same patient01 object as before.
NiftiArray ObjectsThough a NiftiArray object you'll interact with R and the NiftiArray as you would a normal image.
# TODO explain subsetting and loading into memory # drop = FALSE - https://github.com/Bioconductor/DelayedArray/issues/6 # Index a single voxel patient01[90, 89, 101] # Change the value of the background voxels from 0 to -100 ## Note: Normally this should be done using a brain mask but this is just an example for this tutorial patient01[patient01 == 0] = -100 # Summary stats min(patient01) max(patient01) # Image operations patient_sum = patient01 + patient01 # Notice it did sum the NiftiArray objects voxel-wise across objects patient_sum[90, 89, 101]
Due to the block processing some functionality will not be available.
# Due to the block calculation some base functions will not be available # The table function errors # TODO: Does DelayedArray have a table? DelayedArray::table() # table(patient01) table(c(patient01 > 5))
# Remove patient01 object from R memory so it can be re-loaded later rm(patient01)
NiftiArray ObjectsIn practice we have a list of subjects we want to convert from NIfTIs to NiftiArray objects and load into R.
mapply NiftiArray::writeNiftiArrayTo write out multiple NIfTI images on disk to the required HDF5 files we can simply mapply over NiftiArray::writeNiftiArray.
# Write out the entire filepath of NIfTI objects to HDF5s all_patients_list = mapply(function(x,y) { writeNiftiArray(x = x, filepath = y, overwrite = TRUE) }, fileinfo$nii_destination, fileinfo$hdf5_destination) # List the files in the temporary directory -- all subjects should be there as NIfTIs and .h5s list.files(tempdir(), pattern = ".(nii|h5)") # Remove all_patients_list since we will load it in a different way later rm(all_patients_list)
NiftiArray::NiftiArrayListThe NiftiArray::NiftiArrayList function converts and writes NiftiArray objects if the x input is of class NIfTI and then loads all the NiftiArray objects into R in a list as a new class NiftiArrayList. That is, every element in the list is a NiftiArray.
# Convert, write to temporary disk, and load all_patients_list using NiftiArray::NiftiArrayList all_patients_list = NiftiArray::NiftiArrayList(fileinfo$nii_destination) # Show the class is NiftiArrayList class(all_patients_list)
The NiftiArray::NiftiArrayList class also simply loads the NiftiArray objects as a list if the x input is a set of file paths to the HDF5 NiftiArray files on disk.
# Load all_patients_list using NiftiArray::NiftiArrayList all_patients_list = NiftiArray::NiftiArrayList(fileinfo$hdf5_destination) # Show the class is NiftiArrayList class(all_patients_list)
At this point, we have all 5 patients loaded into R as a NiftiArrayList object. We can now convert the NiftiArray object to a NiftiMatrix object in order to run voxel-wise calculations.
NiftiArray to a NiftiMatrixThe NiftiArray object is a 3 dimensional array structure that allows for memory efficient delayed random access of NIfTI objects. The NiftiMatrix is the result of concatenating the NiftiArray. Similar to NiftiArray, NiftiMatrix is a new class object. Rather than an array structure we can strung out the image to a vector. In the code below, we convert a NiftiArray to a NiftiMatrix for one patient. We then verify that the class of this object is in fact a NiftiMatrix, has only a single column, index the vector to print some values, and validate that the object size is as memory efficient as the NiftiMatrix.
# Reload patient01 using NiftiArray::NiftiArray from a HDF5 object on disk patient01 = NiftiArray::NiftiArray(fileinfo$hdf5_destination[[1]]) # Convert the NiftiArray to a NiftiMatrix patient01_niimat = as(patient01, "NiftiMatrix") # Verify patient01_niimat is of class NiftiMatrix class(patient01_niimat) # Notice the NiftiMatrix has only 1 column dim(patient01_niimat) # Index the NiftiMatrix like a vector patient01_niimat[40000:40010] # The NiftiMatrix object is still memory efficient object.size(patient01_niimat) # Obtain the NIfTI header NiftiArray::nifti_header(patient01_niimat)
In this example, we showed the result of converting a single patients NiftiArray to a NiftiMatrix but it will be more useful to create a NiftiMatrix with multiple subjects.
NiftiArrayList to a Big NiftiMatrixIn order to use tools like DelayedArray and DelayedMatrixStats to calculate voxel-level statistics across multiple subjects we need to create a big NiftiMatrix. That is, each row will represent a voxel and each column a new subject. To do this, we can take advantage of the NiftiArrayList class.
# Show all_patients_list is of NiftiArrayList class(all_patients_list) # Convert each element in the list to a NiftiMatrix all_patients_niimat = pbapply::pblapply(all_patients_list, as, "NiftiMatrix") # Concatenate the list elements column-wise all_patients_niimat = do.call(DelayedArray::acbind, all_patients_niimat) # Show the matrix head(all_patients_niimat) # Notice the dimension is all the voxels in the brain by the 5 patients we are working with dim(all_patients_niimat) # Obtain the NIfTI header NiftiArray::nifti_header(all_patients_niimat)
DelayedMatrixStatsThe DelayedMatrixStats package allows for row or column wise statistical operations using delayed block processing to keep both memory and speed optimized. For more information, the package and documentation is available through Bioconductor here.
Below we show a simple example obtaining the voxel level mean and median across subjects.
all_patients_niimat = as(all_patients_niimat, "HDF5Matrix") # Calculate the voxel-wise median across subjects voxel_medians = DelayedMatrixStats::rowMedians(all_patients_niimat) # Index the median vector voxel_medians[1043001] # Show the class of voxel_medians class(voxel_medians) # Calculate the object size object.size(voxel_medians)
Notice the resulting vector voxel_medians is not a DelayedArray or NiftiArray object but rather a normal vector. We can convert it to a NiftiMatrix so that it returns to a memory efficient object using as.
# Convert the median vector back to a `NiftiMatrix` voxel_medians = as(voxel_medians, 'NiftiMatrix') # Notice the size is back to a memory efficient value object.size(voxel_medians) # The header is no longer accurate though NiftiArray::nifti_header(voxel_medians)
Notice the NIfTI header is no longer accurate when we coerce the normal vector to a NiftiMatrix.
# Calculate the voxel-wise mean across subjects voxel_means = DelayedMatrixStats::rowMeans2(all_patients_niimat)
DelayedMatrixStats is a very powerful package so you should spend some time looking through the functions and documentation to see what is available and useful for you.
NiftiMatrix, NiftiArray, and NiftiImage ClassesSo long as the object belongs to the classes available in NiftiArray (i.e. NiftiArray, NiftiMatrix) we can easily convert between the classes and back to a niftiImage class exported from RNifti. It is easy to convert between these object because the NIfTI header is retained among all the objects.
class(patient01_niimat) niimat2niiarr = as(patient01_niimat, "NiftiArray") class(niimat2niiarr)
NiftiMatrix to niftiImageclass(patient01_niimat) niimat2nii = as(patient01_niimat, "niftiImage") class(niimat2nii)
NiftiArray to NiftiMatrixclass(patient01) niiarr2niimat = as(patient01, "NiftiMatrix") class(niiarr2niimat)
NiftiArray to niftiImageclass(patient01) niiarr2nii = as(patient01, "niftiImage") class(niiarr2nii)
DelayedMatrixStats and Vectors to NiftiArrayConverting between the objects returned from DelayedMatrixStats functions and NiftiArray objects is not as easy because the NIfTI header was lost in the calculations involved in DelayedMatrixStats functions. We must extract the header from previous object and then re-initialize the NiftiArray.
# Median # Obtain the image header associated with the all_patients_list NiftiArrayList image_header = NiftiArray::nifti_header(all_patients_list) # Initialize an array median_arr = array(voxel_medians, dim = image_header$dim[2:4]) # The object size of median_arr is normal because it is a normal array object.size(median_arr) # Convert the array to a NiftiArray and write it out to a temporary file median_niiarr = NiftiArray::writeNiftiArray(median_arr, header = image_header) # The object size is back to our efficient NiftiArray object.size(median_niiarr) # Mean # Obtain the image header associated with the all_patients_list NiftiArrayList image_header = NiftiArray::nifti_header(all_patients_list) # Initialize an array mean_arr = array(voxel_means, dim = image_header$dim[2:4]) # The object size of mean_arr is normal because it is a normal array object.size(mean_arr) # Convert the array to a NiftiArray and write it out to a temporary file mean_niiarr = NiftiArray::writeNiftiArray(mean_arr, header = image_header) # The object size is back to our efficient NiftiArray object.size(mean_niiarr)
Once a NiftiArray it is easy to create the niftiImage object.
median_nii = as(median_niiarr, "niftiImage") mean_nii = as(mean_niiarr, "niftiImage")
neurobase::ortho2(median_nii, pdim = nifti_header(median_nii)$pixdim) neurobase::ortho2(mean_nii, pdim = nifti_header(mean_nii)$pixdim)
You could write these objects out as NIfTIs using RNifti::writeNifti
NiftiArray to Traditional Methods# Calculate the a single patient object size using different NIfTI read functions memory = tibble::tibble(read_type = c('NiftiArray::NiftiArray', 'NiftiArray::writeNiftiArray', 'RNifti::readNifti', 'neurobase::readnii'), byte_size = c(object.size(NiftiArray::NiftiArray(fileinfo$nii_destination[1])), object.size(NiftiArray::writeNiftiArray(fileinfo$nii_destination[1])), object.size(RNifti::readNifti(fileinfo$nii_destination[1])), object.size(neurobase::readnii(fileinfo$nii_destination[1]))), log_byte_size = log10(byte_size)) %>% dplyr::mutate(read_type = as.factor(read_type), read_type = forcats::fct_reorder(read_type, byte_size, .desc = TRUE)) # Table of memory knitr::kable( memory, col.names = c('Read Function', 'Byte Size', 'log_10(Byte Size)'), format = 'html', digits = 2, caption = 'Memory mapped in bytes from a single patients image read. The image dimension is 182 by 218 by 182 with pixel dimension 1 mm by 1 mm by 1 mm. On disk the image is 4.9 MB.', booktabs = TRUE ) %>% kableExtra::kable_styling("striped", full_width = FALSE)
# Bar graph in bytes ggplot(memory, aes(x = read_type, y = byte_size, fill = read_type)) + geom_bar(stat="identity") + geom_text(aes(label=byte_size), vjust=-0.3, size=3.5) + ylab('Memory in Bytes') + xlab('Read Function') + labs(title = 'Memory Comparison for Reading a Single NIfTI Image', subtitle = 'Image Dimension: 182 by 218 by 182, Pixel Dimension: 1 mm by 1 mm by 1 mm') + theme_minimal() + theme(plot.title = element_text(hjust=0.5), legend.position = 'none') # Bar graph in log(bytes) ggplot(memory, aes(x = read_type, y = log_byte_size, fill = read_type)) + geom_bar(stat="identity") + geom_text(aes(label=round(log_byte_size), digits = 2), vjust=-0.3, size=3.5) + ylab('Memory in log(Bytes)') + xlab('Read Function') + labs(title = 'Memory Comparison for Reading a Single NIfTI Image on the Log Scale', subtitle = 'Image Dimension: 182 by 218 by 182, Pixel Dimension: 1 mm by 1 mm by 1 mm') + theme_minimal() + theme(plot.title = element_text(hjust=0.5), legend.position = 'none')
# Initalize a list to store the memory profile memory_profile = list() # Memory profile of RNifti::readNifti() memory_profile[[1]] = tibble::tibble(read_type = 'RNifti::readNifti', memory_profile = profmem::profmem(RNifti::readNifti(fileinfo$nii_destination[1]))$bytes) memory_profile[[2]] = tibble::tibble(read_type = 'neurobase::readnii', memory_profile = profmem::profmem(neurobase::readnii(fileinfo$nii_destination[1]))$bytes) memory_profile[[3]] = tibble::tibble(read_type = 'NiftiArray::NiftiArray - NIfTI File', memory_profile = profmem::profmem(NiftiArray::NiftiArray(fileinfo$nii_destination[1]))$bytes) memory_profile[[4]] = tibble::tibble(read_type = 'NiftiArray::writeNiftiArray', memory_profile = profmem::profmem(NiftiArray::writeNiftiArray(fileinfo$nii_destination[1]))$bytes) memory_profile[[5]] = tibble::tibble(read_type = 'NiftiArray::NiftiArray - HDF5 File', memory_profile = profmem::profmem(NiftiArray::NiftiArray(fileinfo$hdf5_destination[1]))$bytes) memory_profile = dplyr::bind_rows(memory_profile) %>% tidyr::drop_na() %>% dplyr::group_by(read_type) %>% dplyr::mutate(x = dplyr::row_number())
ggplot(data=memory_profile, aes(x=x, y=log10(memory_profile))) + geom_line()+ geom_point() + facet_wrap(~read_type, scales = "free_x") ggplot(data=memory_profile, aes(x=x, y=log10(memory_profile))) + geom_line()+ geom_point() + facet_wrap(~read_type, scales = "free")
Speed is very important. Obviously, we want code to be speed efficient to save overall computation time but as a user we also don't want to be distracted by time lags. 0.1 second is approximately the limit for a user to feel as though the system is reacting instantaneously. 1.0 second is about the limit for the user's flow or thought process to stay uninterrupted even though they notice a delay. 10 seconds is the limit to keep a users attention focused on the task at hand. The limit of 10 seconds a user will notice a delay and often lose their train of thought. Beyond 10 seconds and the user may lose track of even the task at hand. That is, they probably opened Twitter or Instagram and have completely lost track of what they were doing [Miller 1968; Card et al. 1991].
Therefore, speeds at 0.1 and 1 second limits are ideal to minimize lag and maximize user attention spans.
Card, S. K., Robertson, G. G., and Mackinlay, J. D. (1991). The information visualizer: An information workspace. Proc. ACM CHI'91 Conf. (New Orleans, LA, 28 April-2 May), 181-188.
Miller, R. B. (1968). Response time in man-computer conversational transactions. Proc. AFIPS Fall Joint Computer Conference Vol. 33, 267-277.
speed = tibble::as_tibble( microbenchmark::microbenchmark( NiftiArray::NiftiArray(fileinfo$hdf5_destination[1]), NiftiArray::NiftiArray(fileinfo$nii_destination[1]), NiftiArray::writeNiftiArray(fileinfo$nii_destination[1]), RNifti::readNifti(fileinfo$nii_destination[1]), neurobase::readnii(fileinfo$nii_destination[1]), times = 5) ) %>% dplyr::mutate(time = time/(1*10^9)) %>% # conver nanoseconds to seconds dplyr::rename(read_type = expr) %>% dplyr::mutate(read_type = dplyr::case_when( stringr::str_detect(read_type, 'RNifti') ~ "RNifti::readNifti", stringr::str_detect(read_type, 'neurobase') ~ "neurobase::readnii", stringr::str_detect(read_type, 'hdf5') ~ "NiftiArray::NiftiArray - HDF5 File", stringr::str_detect(read_type, 'NiftiArray::NiftiArray\\(fileinfo\\$nii') ~ "NiftiArray::NiftiArray - NIfTI File", stringr::str_detect(read_type, 'NiftiArray::writeNiftiArray') ~ "NiftiArray::writeNiftiArray"), read_type = as.factor(read_type), read_type = forcats::fct_reorder(read_type, time, .desc = TRUE)) # Table of memory speed_summary = speed %>% dplyr::group_by(read_type) %>% dplyr::summarise( mean = mean(time, na.rm = TRUE), median = mean(time, na.rm = TRUE), sd = sd(time, na.rm = TRUE), min = min(time, na.rm = TRUE), max = max(time, na.rm = TRUE) ) knitr::kable( speed_summary, col.names = c('Read Function', 'Mean', 'Median', 'Std. Dev.', 'Min.', 'Max.'), format = 'html', digits = 2, caption = 'Memory mapped in bytes from a single patients image read. The image dimension is 182 by 218 by 182 with pixel dimension 1 mm by 1 mm by 1 mm. On disk the image is 4.9 MB.', booktabs = TRUE ) %>% kableExtra::kable_styling("striped", full_width = FALSE)
ggplot(data = speed, aes(x = read_type, y = time)) + geom_boxplot() + coord_flip() + ylab('Time (Seconds)') + xlab('Read Function') + labs(title = 'Speed Comparison for Reading a Single NIfTI Image', subtitle = 'Image Dimension: 182 by 218 by 182, Pixel Dimension: 1 mm by 1 mm by 1 mm') + theme_minimal() + theme(plot.title = element_text(hjust=0.5), legend.position = 'none')
The NiftiArray::NiftiArray - HDF5 and RNifti::readNifti both are around the 0.1 limit of seamless user flow. The remaining functions are around the 1 second limit where a user will notice a lag but not lose their train of thought.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.