Skip to contents

A surface file can contain a cortical mesh without containing any measurements or atlas labels. Many confusing workflows begin by treating those things as equivalent. This article keeps the object transitions explicit: path, geometry, per-vertex surface, then labelled atlas.

TemplateFlow-backed examples are not executed during package builds because a first call may download files. No output is simulated.

What are the four layers?

Layer R representation What it knows
file character path where a mesh asset is cached
geometry SurfaceGeometry vertices, triangles, hemisphere
data surface NeuroSurface geometry plus one value per selected vertex
parcellation surfatlas two labelled surfaces plus region metadata

Choose the narrowest layer that matches your task. A path is enough for an external program; a geometry is enough for coordinate calculations; a NeuroSurface carries vertex measurements; a surfatlas carries parcel identity.

How do you find a valid surface asset?

TemplateFlow surface entities use hemi, density, and suffix. Query the live inventory before loading because the archive is versioned independently of neuroatlas:

library(neuroatlas)

tflow_spaces(pattern = "^fs")

tflow_files(
  "fsLR",
  query_args = list(
    hemi = "L",
    density = "32k",
    suffix = "midthickness"
  )
)

At the time this article was built, the practical geometry choices were:

TemplateFlow ID Densities used here Geometry suffixes to expect
fsLR 32k midthickness, inflated, sphere variants
fsaverage 10k, 41k, 164k pial, white, sphere variants

This is a guide, not a frozen catalogue. In particular, do not infer that fsLR 32k has a pial mesh or that TemplateFlow has an fsaverage6 template ID. Discover the current files and require an unambiguous match.

How do you get a file path?

get_surface_template() resolves one mesh and returns its cached path:

path <- get_surface_template(
  template_id = "fsLR",
  surface_type = "midthickness",
  hemi = "L",
  density = "32k"
)

stopifnot(length(path) == 1L, file.exists(path))
path

Use template_id = "fsaverage", density = "41k", and surface_type = "pial" for the corresponding 41k fsaverage geometry. Density is the relevant entity; adding resolution = "06" to that query currently produces no match.

How do you load geometry?

load_surface_template() reads the asset into a neurosurf::SurfaceGeometry:

geometry <- load_surface_template(
  template_id = "fsLR",
  surface_type = "midthickness",
  hemi = "L",
  density = "32k"
)

vertices <- neurosurf::vertices(geometry)
faces <- geometry@mesh$it

stopifnot(
  inherits(geometry, "SurfaceGeometry"),
  ncol(vertices) == 3L,
  nrow(vertices) > 0L,
  nrow(faces) == 3L,
  ncol(faces) > 0L
)

Load both hemispheres by setting hemi = "both"; the result is a named list with L and R geometries.

How do you attach vertex data?

Construct a NeuroSurface only after proving the value vector and vertex indices agree:

vertex_ids <- seq_len(nrow(vertices))
vertex_values <- as.numeric(scale(vertices[, 3]))

surface <- neurosurf::NeuroSurface(
  geometry = geometry,
  indices = vertex_ids,
  data = vertex_values
)

stopifnot(
  length(surface@indices) == nrow(vertices),
  length(surface@data) == nrow(vertices),
  all(is.finite(surface@data))
)

The geometry supplies topology; surface@data supplies one value for each selected vertex. Neither supplies parcel names.

How is a labelled atlas different?

Atlas loaders combine geometry with integer labels and a region catalogue:

atl <- schaefer_surf(
  parcels = 200,
  networks = 7,
  space = "fsaverage6",
  surf = "inflated"
)

lh_labels <- as.integer(atl$lh_atlas@data)
lh_geometry <- neurosurf::geometry(atl$lh_atlas)

stopifnot(
  length(lh_labels) == nrow(neurosurf::vertices(lh_geometry)),
  length(unique(lh_labels[lh_labels > 0])) == sum(atl$hemi == "left")
)

Here is the visible distinction: labelled parcels and their boundaries, not a featureless geometry silhouette.

Schaefer 200-parcel atlas on inflated fsaverage6 cortex, showing many labelled regions with visible boundaries across lateral and medial views of both hemispheres.

The fsaverage6 mesh is bundled, but Schaefer annotations are downloaded and cached on first use. Other surface atlases may also require TemplateFlow geometry. That network boundary is part of the workflow, not something a vignette should hide.

How do coordinate spaces enter the picture?

Mesh identity is not only vertex count. fsaverage variants use MNI305 coordinates; fsLR is an MNI152-family surface space. Before combining a surface with a volume, inspect the surface template and the volume’s declared space. Use needs_coord_transform() or needs_transform() to detect a coordinate-system mismatch, and use a validated transform before sampling.

Two meshes with the same number of vertices are not thereby aligned, and a volume reslice is not a surface registration.

What should you use next?

The durable rule is simple: verify file identity, topology, value length, and coordinate space at each transition.