--- title: Introduction to NeuroSurf Data Structures author: Bradley Buchsbaum date: '`r Sys.Date()`' output: rmarkdown::html_vignette: toc: yes toc_depth: 2.0 css: albers.css includes: in_header: albers-header.html vignette: | %\VignetteIndexEntry{Introduction to NeuroSurf Data Structures} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} params: family: teal preset: homage resource_files: - albers.css - albers.js - albers-header.html --- This vignette introduces the fundamental data structures used in the `neurosurf` package for representing and working with brain surface geometries and associated data. For visualization, see the companion vignettes: - `vignette("displaying-surfaces")` for interactive 3D plots based on `plot()` and `view_surface()`. - `vignette("interactive-surfaces")` for HTML-widget based exploration. - `vignette("surface-figures")` for multi-view, publication-quality layouts built with `surface_plot()` and friends. ## Setup First, let's load the necessary libraries and set up the environment. ```{r setup, include = FALSE} # Check for required packages - skip vignette build if rgl unavailable has_rgl <- requireNamespace("rgl", quietly = TRUE) if (!has_rgl) { knitr::opts_chunk$set(eval = FALSE) message("Skipping vignette: rgl package not available") } if (requireNamespace("ggplot2", quietly = TRUE) && requireNamespace("albersdown", quietly = TRUE)) ggplot2::theme_set(albersdown::theme_albers(family = params$family, preset = params$preset)) knitr::opts_chunk$set( collapse = TRUE, comment = "#>", warning = FALSE, message = FALSE, fig.width = 7, fig.height = 5, rgl.newwindow = TRUE, # Required for setupKnitr hook to work properly webgl = TRUE # Use WebGL for embedding rgl plots (if any) ) if (has_rgl) { # Ensure rgl widgets embed correctly options(rgl.useNULL = TRUE, rgl.printRglwidget = TRUE) rgl::setupKnitr() # Setup rgl hook for knitr (if needed) } library(neurosurf) if (has_rgl) library(rgl) # For potential visualization examples library(igraph) # For graph operations library(Matrix) # For sparse matrices used in NeuroSurfaceVector # Prepare paths to example data included in the package white_lh_asc <- system.file("extdata", "std.8_lh.smoothwm.asc", package="neurosurf") white_rh_asc <- system.file("extdata", "std.8_rh.smoothwm.asc", package="neurosurf") ``` ```{r albers-classes, echo=FALSE, results='asis'} cat(sprintf( paste0( '' ), params$family, params$preset )) ``` ## `SurfaceGeometry`: Representing the Mesh The core building block for any surface analysis is the geometry itself. In `neurosurf`, this is represented by the `SurfaceGeometry` class. An object of this class encapsulates: 1. **`mesh`**: An `rgl::mesh3d` object containing the raw vertex coordinates and the triangular faces that define the surface shape. 2. **`graph`**: An `igraph` object representing the connectivity of the mesh vertices. Edges connect adjacent vertices along the surface. 3. **`hemi`**: A character string indicating the hemisphere, typically "lh" (left) or "rh" (right). ### Loading a `SurfaceGeometry` You can load a surface geometry from various file formats (Freesurfer binary, Freesurfer ASCII `.asc`, GIFTI `.gii`) using the `read_surf_geometry()` function. ```{r load-geometry} # Load the example left hemisphere white matter surface (.asc format) lh_geom <- read_surf_geometry(white_lh_asc) # Display summary information about the loaded geometry show(lh_geom) ``` ### Accessing Geometry Properties Several methods allow you to access information within the `SurfaceGeometry` object: ```{r access-geometry} # Total number of vertices (nodes) in the geometry length(nodes(lh_geom)) # Vertex coordinates as an N x 3 matrix; its dimensions and first 3 rows vertex_coords <- coords(lh_geom) dim(vertex_coords) head(vertex_coords, 3) # The underlying igraph connectivity object neurosurf::graph(lh_geom) # The underlying rgl mesh3d object, and the hemisphere label class(lh_geom@mesh) lh_geom@hemi ``` ## `NeuroSurface`: Mapping Data to Geometry Often, we want to associate data values (like cortical thickness, fMRI activation, etc.) with each vertex on the surface. The `NeuroSurface` class links a *single vector* of data to a `SurfaceGeometry`. It contains: 1. **`geometry`**: The associated `SurfaceGeometry` object. 2. **`indices`**: An integer vector specifying *which* vertices in the geometry have corresponding data values. This allows for representing data defined only on a subset of the surface. 3. **`data`**: A numeric vector containing the data values. Its length must match the length of `indices`. ### Creating a `NeuroSurface` You typically create a `NeuroSurface` using its constructor, providing the geometry, indices, and data. ```{r create-neurosurface} # Generate some example data (e.g., based on x-coordinate) # Use all vertices from lh_geom vertex_indices <- nodes(lh_geom) example_data <- coords(lh_geom)[, 1] # Use x-coordinate as data # Create the NeuroSurface object lh_surf_data <- NeuroSurface(geometry = lh_geom, indices = vertex_indices, data = example_data) # Display summary show(lh_surf_data) ``` ### Loading Data with `read_surf` (Optional Data File) If your data is stored in a separate file (e.g., AFNI `.1D.dset`, NIML `.niml.dset`), you can load both the geometry and the data using `read_surf`. Here we create a temporary `.1D.dset` file for demonstration. ``` {r load-surf-with-data, eval=TRUE} # 1. Prepare sample data for a subset of nodes sample_nodes_indices_R <- sample(nodes(lh_geom), size = 500) # R indices (1-based) sample_nodes_indices_0based <- sample_nodes_indices_R - 1 # 0-based for .1D file sample_data <- rnorm(500) # 2. Create a temporary file temp_dset_file <- tempfile(fileext = ".1D.dset") # 3. Write data in AFNI .1D format (node_index value) write.table(cbind(sample_nodes_indices_0based, sample_data), file = temp_dset_file, row.names = FALSE, col.names = FALSE, sep = " ") # 4. Load geometry and the data from the temporary file # read_surf will match nodes in the file to the geometry lh_surf_loaded <- read_surf(surface_name = white_lh_asc, surface_data_name = temp_dset_file) # Display summary of the loaded NeuroSurface show(lh_surf_loaded) # Number of data points loaded, and how many are non-zero (should match size = 500) length(lh_surf_loaded@data) sum(lh_surf_loaded@data != 0) ``` ### Accessing `NeuroSurface` Properties ```{r access-neurosurface} # The geometry can be retrieved and matches the original identical(geometry(lh_surf_data), lh_geom) # The data vector, via direct slot access (lh_surf_data@data) or as.vector(); # its length and first few values data_vec <- lh_surf_data@data length(data_vec) head(data_vec, 5) # The associated vertex indices index_vec <- indices(lh_surf_data) length(index_vec) head(index_vec, 5) ``` ## `NeuroSurfaceVector`: Mapping Multiple Data Vectors When you have multiple measurements per vertex (e.g., time series data, multiple features), the `NeuroSurfaceVector` class is used. It links a *matrix* of data to a `SurfaceGeometry`. It contains: 1. **`geometry`**: The associated `SurfaceGeometry` object. 2. **`indices`**: An integer vector specifying *which* vertices have data (same as `NeuroSurface`). 3. **`data`**: A `Matrix` (from the `Matrix` package, often sparse) where *rows* correspond to vertices (matching `indices`) and *columns* represent different measurements or time points. ### Creating a `NeuroSurfaceVector` ```{r create-neurosurfacevector} # Create example matrix data (e.g., x, y, z coordinates as 3 'features') # Use all vertices num_vertices <- length(nodes(lh_geom)) vertex_indices_vec <- nodes(lh_geom) # Create a dense matrix first example_matrix_data <- coords(lh_geom) # Convert to a Matrix object (can be sparse or dense) example_matrix <- Matrix(example_matrix_data) # Create the NeuroSurfaceVector lh_surf_vec <- NeuroSurfaceVector(geometry = lh_geom, indices = vertex_indices_vec, mat = example_matrix) # Display summary show(lh_surf_vec) ``` ### Accessing `NeuroSurfaceVector` Properties ```{r access-neurosurfacevector} # The geometry matches the original identical(geometry(lh_surf_vec), lh_geom) # The data matrix (sparse Matrix slot, or as.matrix()); its dimensions data_mat <- lh_surf_vec@data dim(data_mat) # Standard matrix indexing: one vertex (row), or one column across vertices data_mat[10, ] head(data_mat[, 2]) # The associated vertex indices length(indices(lh_surf_vec)) ``` ## Specialized NeuroSurface Classes `neurosurf` also provides specialized classes that inherit from `NeuroSurface` and add features primarily for visualization: * **`LabeledNeuroSurface`**: Associates categorical labels and corresponding colors with vertices (useful for atlases or parcellations). Contains `labels` and `cols` slots. * **`ColorMappedNeuroSurface`**: Stores data along with a specific colormap (`cmap`), data range (`irange`), and thresholds (`thresh`) to pre-define how the data should be visualized. * **`VertexColoredNeuroSurface`**: Stores specific hex color codes directly for each vertex (`colors` slot), bypassing data mapping. These classes facilitate plotting with pre-set visual parameters using the `plot()` method discussed in `vignette("displaying-surfaces")`. ## Next Steps Now that you understand the core data structures, explore the visualization vignettes: - `vignette("displaying-surfaces")` — render surfaces with RGL, overlay data, and snapshot to PNG. - `vignette("interactive-surfaces")` — create interactive HTML widgets with `surfwidget()`. - `vignette("surface-figures")` — build publication-quality multi-view figures with colourbars and atlas outlines.