---
title: Surface Parcellations with neurosurf
author: neuroatlas Dev Team
date: '`r Sys.Date()`'
output: rmarkdown::html_vignette
vignette: >
%\VignetteIndexEntry{Surface Parcellations with neurosurf}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
params:
family: red
css: albers.css
resource_files:
- albers.css
- albers.js
includes:
in_header: |-
---
```{r setup, include = FALSE}
if (requireNamespace("ggplot2", quietly = TRUE) && requireNamespace("albersdown", quietly = TRUE)) ggplot2::theme_set(albersdown::theme_albers(params$family))
knitr::opts_chunk$set(
collapse = TRUE,
comment = "#>",
echo = TRUE,
eval = FALSE # keep builds fast / offline
)
suppressWarnings(suppressMessages(library(neuroatlas)))
can_plot <- requireNamespace("neuroatlas", quietly = TRUE) &&
requireNamespace("neuroim2", quietly = TRUE) &&
requireNamespace("neurosurf", quietly = TRUE) &&
requireNamespace("ggplot2", quietly = TRUE)
can_layout <- can_plot &&
requireNamespace("patchwork", quietly = TRUE) &&
requireNamespace("scico", quietly = TRUE)
templateflow_ok <- FALSE
try({
templateflow_ok <- reticulate::py_available(initialize = TRUE) &&
reticulate::py_module_available("templateflow")
}, silent = TRUE)
```
## What this vignette covers
This guide focuses on loading **surface parcellations** as actual meshes
(`neurosurf::LabeledNeuroSurface`)—not ggseg flats. We cover:
- Discovering available surface templates in TemplateFlow
- Schaefer on fsaverage6 (packaged geometry)
- Schaefer on fsaverage5/fsaverage (via TemplateFlow, when available)
- Glasser MMP1.0 (via TemplateFlow, when available)
- Loading raw surface geometry from TemplateFlow
- Downloading surfaces to a local folder
- What files are fetched, what the `data` slot contains, and how labels map.
- Minimal patterns to attach your own per-vertex values.
## Discovering available surface templates
Before loading surfaces, you can query what's available in TemplateFlow:
```{r discover_surfaces, eval=FALSE}
# Find surface-related template spaces
tflow_spaces(pattern = "fs")
#> [1] "fsLR" "fsaverage"
# List available fsLR surfaces
tflow_files("fsLR", query_args = list(hemi = "L"))
# Filter by specific density and surface type
tflow_files("fsLR", query_args = list(
hemi = "L",
density = "32k",
suffix = "midthickness"
))
# List fsaverage surfaces at a specific resolution (e.g., fsaverage6)
tflow_files("fsaverage", query_args = list(
hemi = "L",
resolution = "06" # fsaverage6
))
```
Common query parameters for surfaces:
- `hemi`: "L" or "R"
- `density`: "32k", "164k" (for fsLR)
- `resolution`: "06" (fsaverage6), "05" (fsaverage5)
- `suffix`: "pial", "white", "inflated", "midthickness", "sphere"
## Schaefer surface atlases
```{r schaefer-main}
# 200 parcels, 7 networks, fsaverage6 inflated (packaged geometry, no TF needed)
atl <- schaefer_surf(
parcels = 200,
networks = 7,
space = "fsaverage6",
surf = "inflated"
)
```
Returned structure:
- `atl$lh_atlas`, `atl$rh_atlas`: `LabeledNeuroSurface` objects.
- `slot(..., "data")`: integer parcel IDs per vertex.
- `atl$labels` / `atl$orig_labels`: name lookups; `atl$network`: network per ID.
### Using TemplateFlow spaces (when available)
Note: TemplateFlow surface support requires the `fsaverage` template to be available
in your TemplateFlow installation. As of this writing, surface templates may have
limited availability. Use `fsaverage6` (above) for reliable access to Schaefer atlases.
```{r schaefer-tf, eval=FALSE}
# fsaverage5 white surface; uses TemplateFlow geometry (if available)
atl_tf <- schaefer_surf(
parcels = 400,
networks = 17,
space = "fsaverage5",
surf = "white"
)
```
If TemplateFlow surfaces are available, this uses the same labels/data layout as above.
### Attaching your own values
```{r schaefer-map, eval=FALSE}
vals <- rnorm(length(atl$ids)) # one value per parcel
mapped <- map_atlas(atl, vals) # returns LabeledNeuroSurface per hemi with data filled
```
`map_atlas()` writes your parcel values into the `data` slot of each
hemisphere surface.
## Glasser MMP1.0 surface atlas (when available)
Note: Like Schaefer TemplateFlow surfaces, Glasser surface support requires the
`fsaverage` template in TemplateFlow. Check availability before using.
```{r glasser, eval=FALSE}
# Requires fsaverage template in TemplateFlow
glas <- glasser_surf(space = "fsaverage", surf = "pial")
slot(glas$lh_atlas, "data")[1:5] # parcel IDs
glas$labels[1:5] # names for those IDs
```
If available, geometry comes from TemplateFlow (fsaverage pial/white/inflated/midthickness);
labels come from the HCP-MMP1.0 fsaverage annotations published on Figshare as
the "HCP-MMP1_0 projected on fsaverage" dataset (ID 3498446; DOI:
10.6084/m9.figshare.3498446).
The `data` vector length equals the vertex count of the mesh; each entry is
the parcel ID at that vertex.
## What’s in the `data` slot?
- Schaefer / Glasser surface atlases: integer parcel IDs per vertex.
- If you construct a `NeuroSurface` yourself (e.g., from `load_surface_template()`),
you supply the per-vertex numeric values.
## Saving / reusing
```{r save}
saveRDS(atl, "schaefer200_7_fs6_inflated.rds")
# reload later
atl2 <- readRDS("schaefer200_7_fs6_inflated.rds")
```
### Sanity check: parcel surface renders
We snapshot a labeled Schaefer surface to verify geometry+labels load.
Runs only when TemplateFlow is available (for non-packaged spaces).
```{r snapshot-parc, eval=templateflow_ok, cache=TRUE}
dir.create("figures", showWarnings = FALSE)
png_path <- file.path("figures", "schaefer200_fs6_L_inflated.png")
atl <- schaefer_surf(
parcels = 200,
networks = 7,
space = "fsaverage6", # packaged geometry (no TF), keeps it fast
surf = "inflated"
)
geom_l <- neurosurf::geometry(atl$lh_atlas)
neurosurf::snapshot_surface(geom_l, file = png_path)
png_path
```
```{r snapshot-parc-show, echo=FALSE, eval=templateflow_ok, fig.alt="Schaefer 200x7 fsaverage6 inflated left hemisphere labeled surface"}
knitr::include_graphics(file.path("figures", "schaefer200_fs6_L_inflated.png"))
```
If the image renders, the parcellated surface is displayable.
## Loading raw surface geometry from TemplateFlow
To load just the surface geometry (without parcellation labels), use
`get_surface_template()` or `load_surface_template()`:
```{r load_surface, eval=FALSE}
# Get path to a surface file
fslr_path <- get_surface_template(
template_id = "fsLR",
surface_type = "midthickness",
hemi = "L",
density = "32k"
)
print(fslr_path)
# Load as a neurosurf SurfaceGeometry object
fslr_geom <- load_surface_template(
template_id = "fsLR",
surface_type = "inflated",
hemi = "L",
density = "32k"
)
# Load both hemispheres at once
both_hemi <- load_surface_template(
template_id = "fsLR",
surface_type = "pial",
hemi = "both",
density = "32k"
)
# Returns list with $L and $R
```
## Downloading surfaces to a local folder
To copy TemplateFlow surfaces to a local directory:
```{r download_surfaces, eval=FALSE}
# Get paths to surfaces (automatically downloaded to cache)
files <- tflow_files("fsLR", query_args = list(hemi = "L", density = "32k"))
# Copy to local folder
dest_folder <- "~/my_surfaces/fsLR"
dir.create(dest_folder, recursive = TRUE, showWarnings = FALSE)
file.copy(files, dest_folder)
# Or use a helper function for bulk downloads
download_surfaces <- function(space, query_args, dest_folder) {
dir.create(dest_folder, recursive = TRUE, showWarnings = FALSE)
files <- tflow_files(space, query_args = query_args)
if (length(files) > 0) {
dest_paths <- file.path(dest_folder, basename(files))
file.copy(files, dest_paths, overwrite = TRUE)
message("Copied ", length(files), " files to ", dest_folder)
}
invisible(dest_paths)
}
# Download all fsLR 32k surfaces
download_surfaces("fsLR", list(density = "32k"), "~/my_surfaces/fsLR_32k")
```
## Common workflows
- **Vertex-wise stats:** build your own `NeuroSurface` from `load_surface_template()`
and write stats into `data`.
- **Parcel summaries:** use volumetric data with `reduce_atlas()` or
surface atlases with `map_atlas()` to project parcel statistics back onto
the mesh for visualization.
## Static figure layout and annotations
For publication-style static figures, `plot_brain()` now exposes the
layout controls that usually require manual patchwork editing: you can
move the colorbar, relabel panels, and add figure-level annotations
directly in the plotting call.
If panel composition is your main task, see
`vignette("surface-panels", package = "neuroatlas")` for a focused guide
to multi-panel layout and hemisphere-label conventions.
```{r layout-single, eval=can_layout, fig.width=8, fig.height=4.5}
atl_layout <- schaefer_surf(200, 7, space = "fsaverage6", surf = "inflated")
parcel_vals <- seq(-2, 2, length.out = length(atl_layout$ids))
plot_brain(
atl_layout,
vals = parcel_vals,
views = c("lateral", "medial"),
interactive = FALSE,
style = "ggseg_like",
colorbar = "bottom",
colorbar_title = "Standardized effect",
title = "Parcel-level summary on fsaverage6",
subtitle = "Bottom colorbar plus custom panel labels",
panel_labels = c(
"Left Lateral" = "LH lateral",
"Right Lateral" = "RH lateral",
"Left Medial" = "LH medial",
"Right Medial" = "RH medial"
)
)
```
The same interface carries over to `plot_brain_grid()`, which is useful
when you want several parcel maps to share a single scale and colorbar.
This is the easiest way to build a small comparison figure where every
panel uses the same legend and the same surface view labels.
```{r layout-grid, eval=can_layout, fig.width=9, fig.height=6}
atl_layout <- schaefer_surf(200, 7, space = "fsaverage6", surf = "inflated")
vals_list <- list(
Baseline = sin(seq_along(atl_layout$ids) / 15),
Follow_up = cos(seq_along(atl_layout$ids) / 18),
Difference = sin(seq_along(atl_layout$ids) / 15) -
cos(seq_along(atl_layout$ids) / 18),
Network_shift = 0.5 * sin(seq_along(atl_layout$ids) / 10)
)
plot_brain_grid(
atl_layout,
vals_list,
views = c("lateral", "medial"),
titles = c(
"Baseline map",
"Follow-up map",
"Difference map",
"Network shift"
),
shared_scale = TRUE,
colorbar = "right",
colorbar_title = "z-score",
title = "Comparing parcel maps with a shared legend",
subtitle = paste(
"Every panel uses the same limits, so colours mean the same thing",
"across baseline, follow-up, and derived contrasts."
),
caption = paste(
"Use titles to name each map, panel_labels to simplify the view labels,",
"and shared_scale = TRUE when the legend should be interpreted globally."
),
panel_labels = c(
"Left Lateral" = "LH lateral",
"Right Lateral" = "RH lateral",
"Left Medial" = "LH medial",
"Right Medial" = "RH medial"
),
style = "ggseg_like",
ncol = 2
)
```
What this figure is doing:
- `titles` names each parcel map, so the reader can distinguish conditions or contrasts at a glance.
- `panel_labels` shortens the repeated view labels, which matters once each map has both lateral and medial views.
- `shared_scale = TRUE` forces one global value range across all panels; this is what makes the single legend interpretable.
- `colorbar = "right"` and `colorbar_title` turn that shared scale into an explicit legend instead of leaving the viewer to infer it.
If you want each panel to use its own dynamic range, set `shared_scale = FALSE`.
That can improve within-panel contrast, but the legend no longer supports direct
comparison across panels.
## Dense vertex-wise overlays
`plot_brain()` supports dense (per-vertex) overlays in addition to
parcel-level colouring. You can pass a list with `lh` and `rh` numeric
vectors — one value per vertex — and the result is a continuous colour
map draped over the cortical surface.
```{r overlay-dense, eval=can_plot, fig.width=8, fig.height=4}
atl <- schaefer_surf(200, 7, space = "fsaverage6", surf = "inflated")
make_spatial_overlay <- function(atlas_hemi) {
verts <- neurosurf::vertices(neurosurf::geometry(atlas_hemi))
y <- verts[, 2]
z <- verts[, 3]
vals <- sin(y / 25) * cos(z / 30)
as.numeric(vals)
}
ov <- list(
lh = make_spatial_overlay(atl$lh_atlas),
rh = make_spatial_overlay(atl$rh_atlas)
)
plot_brain(
atl,
overlay = ov,
overlay_alpha = 0.7,
overlay_palette = "vik",
interactive = FALSE
)
```
### Automatic NeuroVol projection
When `overlay` is a `NeuroVol` (e.g. a whole-brain statistical map),
`plot_brain()` automatically projects it onto the surface via
`neurosurf::vol_to_surf()` — no manual preprocessing required.
Here we build a synthetic volume with two positive "activation" blobs
over frontal cortex and one negative blob over occipital cortex.
```{r overlay-vol, eval=can_plot, fig.width=8, fig.height=4}
spacing <- c(4, 4, 4)
origin <- c(-80, -120, -60)
dims <- c(40L, 50L, 40L)
sp <- neuroim2::NeuroSpace(dims, spacing = spacing, origin = origin)
arr <- array(0, dim = dims)
for (i in seq_len(dims[1])) {
for (j in seq_len(dims[2])) {
for (k in seq_len(dims[3])) {
coord <- origin + (c(i, j, k) - 1) * spacing
d1 <- sum((coord - c(-20, 30, 40))^2)
d2 <- sum((coord - c( 20, 30, 40))^2)
d3 <- sum((coord - c( 0,-60, 20))^2)
arr[i, j, k] <- 3 * exp(-d1 / 800) +
3 * exp(-d2 / 800) -
2.5 * exp(-d3 / 600)
}
}
}
stat_vol <- neuroim2::NeuroVol(arr, sp)
plot_brain(
atl,
overlay = stat_vol,
overlay_threshold = 0.5,
overlay_palette = "vik",
overlay_alpha = 0.7,
interactive = FALSE
)
```
In practice you would load a real statistical map instead:
```{r overlay-real, eval=FALSE}
stat_vol <- neuroim2::read_vol("my_tstat.nii.gz")
plot_brain(atl, overlay = stat_vol, overlay_threshold = 2,
overlay_palette = "vik", interactive = FALSE)
```
Two optional parameters control the volume-to-surface projection:
- **`overlay_fun`**: interpolation function — `"avg"` (default), `"nn"`
(nearest-neighbour), or `"mode"` (most frequent label).
- **`overlay_sampling`**: sampling strategy — `"midpoint"` (default,
halfway between white and pial), `"normal_line"` (multiple samples
along the surface normal), or `"thickness"` (cortical-thickness–aware
sampling).
```{r overlay-opts, eval=FALSE}
plot_brain(
atl,
overlay = stat_vol,
overlay_fun = "nn",
overlay_sampling = "normal_line",
overlay_threshold = 2,
interactive = FALSE
)
```
## Notes on dependencies
- fsaverage6 geometry is bundled; other spaces require TemplateFlow.
- Chunk `eval = FALSE` by default; set to `TRUE` locally with network and TemplateFlow.