--- title: "Working with annotations and labels in ggbrain" author: "Michael Hallquist" date: "26 Oct 2022" output: html_document: rmarkdown::html_vignette: vignette: > %\VignetteIndexEntry{Working with annotations and labels in ggbrain} %\VignetteEncoding{UTF-8} %\VignetteEngine{knitr::rmarkdown} editor_options: chunk_output_type: console --- ```{r setup, include=FALSE} knitr::opts_chunk$set(echo = TRUE) library(ggbrain) library(ggplot2) library(patchwork) library(RNifti) library(dplyr) # MNI 2009c anatomical underlay underlay_file <- system.file("extdata", "mni_template_2009c_3mm.nii.gz", package = "ggbrain") # Schaefer 200-parcel atlas of cortex schaefer200_atlas_file <- system.file("extdata", "Schaefer_200_7networks_2009c_3mm.nii.gz", package = "ggbrain") # Automated labels of 200 parcels schaefer200_atlas_labels <- read.csv(system.file("extdata", "Schaefer_200_7networks_labels.csv", package = "ggbrain")) ``` # Categorical images in ggbrain Many visualizations of brain data rely on continuous-valued images containing intensities or statistics. For example, we might wish to visualize the *z*-statistics of a general linear model. Yet, many images contain integers whose values represent a priori regions of interest or clusters identified using familywise error correction methods. Brain atlases are a common example of integer-valued images. Here, we demonstrate the cortical parcellation developed by Schaefer and colleagues (2018). Schaefer, A., Kong, R., Gordon, E. M., Laumann, T. O., Zuo, X.-N., Holmes, A. J., Eickhoff, S. B., & Yeo, B. T. T. (2018). Local-global parcellation of the human cerebral cortex from intrinsic functional connectivity MRI. *Cerebral Cortex, 28*, 3095-3114. This version of the atlas contains 200 cortical parcels (in the paper, they show 100-1000 parcels). ```{r} schaefer_img <- readNifti(schaefer200_atlas_file) sort(unique(as.vector(schaefer_img))) ``` At a basic level, we can visualize this image in the same way as continuous images, as described in ![ggbrain_introduction.html]. ```{r} gg_obj <- ggbrain() + images(c(underlay = underlay_file, atlas = schaefer200_atlas_file)) + slices(c("z = 30", "z=40")) + geom_brain(definition = "underlay") + geom_brain(definition = "atlas", name = "Parcel number") plot(gg_obj) ``` As we can see, however, the continuous values represent discrete parcels in the atlas. Thus, we may wish to use a categorical/discrete color scale to visualize things. As with base `ggplot2`, we can wrap the fill column in `factor` to force conversion to a discrete data type. Note that the numeric value of any image in a `ggbrain` object is always called `value`. And thus, for `geom_brain` and `geom_outline` objects, the default aesthetic mapping is `aes(fill=value)`. ```{r} gg_obj <- ggbrain() + images(c(underlay = underlay_file, atlas = schaefer200_atlas_file)) + slices(c("z = 30", "z=40")) + geom_brain(definition = "underlay") + geom_brain(definition = "atlas", name = "Parcel number", mapping = aes(fill = factor(value)), fill_scale = scale_fill_hue()) + render() plot(gg_obj) ``` This is a lot to take in! It's not especially easy to resolve 70+ parcels by their color... We could use use subsetting syntax in the layer definition to reduce the number of parcels. For example, perhaps we're interested in just the first 20. ```{r} gg_obj <- ggbrain() + images(c(underlay = underlay_file, atlas = schaefer200_atlas_file)) + slices(c("z = 30", "z=40")) + geom_brain(definition = "underlay") + geom_brain(definition = "atlas[atlas < 20]", name = "Parcel number", mapping = aes(fill = factor(value)), fill_scale = scale_fill_hue()) + render() plot(gg_obj) ``` This is more manageable, though uninspiring. # Mapping image values to labels Categorical images typically contain one integer value at each voxel. The values could be the cluster number from a clusterization procedure (e.g., AFNI's 3dClusterize), a region of interest from a meta-analytically derived mask (e.g., NeuroSynth), or an atlas value from a stereotaxic atlas (e.g., the Schaefer atlas used here). Regardless, the conceptual view is that each integer in the image represents a category of interest. Moreover, we may wish to label these categories with more descriptive labels, not simply the integer value. Thus, integers in an image give us the locations of regions to be labeled, while a separate data table provides the integers <-> labels mapping. Our labels could be region names, intrinsic networks, or other features we wish to highlight on the display. Regardless, there are two major ways in which labels can be displayed on a `ggbrain` plot: mapping the labels to colors displayed in the legend or adding text annotations at the locations of the regions. We will review these two approaches in turn. First, however, let's see how we tell `ggbrain` to merge labels with an integer-valued NIfTI image. Above, we read in a CSV file containing labels for regions in the Schaefer 200 parcellation. These were generated in part using AFNI's whereami command with the centroids of each region serving as an input. This gives us automated labels that we may wish to display on the plot. The CSV also contains a columns called `network` that refers to the network mapping to the Yeo 2011 7-network parcellation. ```{r} knitr::kable(head(schaefer200_atlas_labels, n=10)) ``` The structure of this `data.frame` is relatively flexible. The primary requirement is that it contain a column called `value` that maps to the numeric value of the corresponding NIfTI image of interest. Here, we have a column called `roi_num` that maps to the integer values in the mask. So, we need to rename it to `value` for `ggbrain` to accept it as a lookup table for labeling. ```{r} schaefer200_atlas_labels <- schaefer200_atlas_labels %>% dplyr::rename(value = roi_num) ``` Now that we have this, we can add the labels to the corresponding NIfTI image. This step does not necessarily change the plot, but instead gives us access to additional columns that we can use for labeling. We use the `labels` argument with the `images` function. ```{r} gg_base <- ggbrain() + images(c(underlay = underlay_file)) + images(c(atlas = schaefer200_atlas_file), labels=schaefer200_atlas_labels) + slices(c("z = 30", "z=40")) + geom_brain(definition = "underlay") gg_obj <- gg_base + geom_brain(definition = "atlas[atlas < 20]", name = "Parcel number", mapping = aes(fill = factor(value)), fill_scale = scale_fill_hue()) plot(gg_obj) ``` Notice that I have broken up the addition of images to the object into two `images` steps. This allows for an unambiguous mapping of the labels to the singular NIfTI. An alternative is to use a named list of the sort: `images(c(im1=file1, im2=file2), labels=list(im2=im2labels))`. Also, the plot above is identical to our earlier plot. This is because we have mapped the fill to the numeric value, not another column in the labels file. How about we use the labels from the Eickhoff-Zilles macro labels from N27? ```{r} gg_obj <- gg_base + geom_brain(definition = "atlas[atlas < 20]", name = "Eickhoff-Zilles Label", mapping = aes(fill = CA_ML_18_MNI), fill_scale = scale_fill_hue()) + render() plot(gg_obj) ``` Notice how we went from four colors (one per number) in the previous plot to three labels here. Why did this happen? The labels for the four regions were not unique in the lookup atlas. ```{r} schaefer200_atlas_labels %>% filter(value %in% c(12, 13, 14, 18)) %>% select(value, CA_ML_18_MNI) ``` This highlights a useful point: there can be a one-to-many mapping between labels and unique integer values in the NIfTI image. Indeed, this is often an explicit goal. For example, what if we want to see the Yeo 7 networks assignments for the nodes on these two slices? (Note that I'm removing the filter `atlas < 20` here to let all of the parcels on these slices get to play.) ```{r} gg_obj <- gg_base + geom_brain(definition = "atlas", name = "Yeo 7 Assignment", mapping = aes(fill = network)) plot(gg_obj) ``` If we do not provide a color palette using the `fill_scale` argument, it defaults to [ColorBrewer's "Set3"](https://colorbrewer2.org/#type=qualitative&scheme=Set3&n=12), `scale_fill_brewer(palette = "Set3")`. What if we wanted to use the same coloration as in the original Yeo et al. 2011 paper? These colors are provided in the Yeo2011_7Networks_ColorLUT.txt file from [Freesurfer](https://surfer.nmr.mgh.harvard.edu/fswiki/CorticalParcellation_Yeo2011). ```{r} yeo_colors <- read.table(system.file("extdata", "Yeo2011_7Networks_ColorLUT.txt", package = "ggbrain")) %>% setNames(c("index", "name", "r", "g", "b", "zero")) %>% slice(-1) # Convert RGB to hex. Also, using a named set of colors with scale_fill_manual ensures accurate value -> color mapping yeo7 <- as.character(glue::glue_data(yeo_colors, "{sprintf('#%.2x%.2x%.2x', r, g, b)}")) %>% setNames(c("Vis", "SomMot", "DorsAttn", "SalVentAttn", "Limbic", "Cont", "Default")) gg_obj <- gg_base + geom_brain(definition = "atlas", name = "Yeo 7 Assignment", mapping = aes(fill = network), fill_scale = scale_fill_manual(values=yeo7)) plot(gg_obj) ``` # Combining filled areas with outlines It may be useful to map some labels to the fill of an area and to draw outlines around areas using another label. For example, if regions are nested within networks, we might want to outline the networks with a certain color while having separate colors for regions. ```{r} gg_obj <- gg_base + geom_outline(definition = "atlas", name = "Yeo 7 Assignment", mapping = aes(outline = network), outline_scale = scale_fill_manual(values=yeo7)) + geom_brain(definition = "DAN Region := atlas[atlas.network == 'DorsAttn']", mapping=aes(fill=CA_ML_18_MNI)) plot(gg_obj) ``` This admittedly a busy figure!