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))
pathUse 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.

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?
- Use
vignette("surface-parcellations")when region identity matters. - Use
vignette("surface-panels")to display parcel or per-vertex values. - Use
vignette("working-with-templateflow")for cache and volume queries.
The durable rule is simple: verify file identity, topology, value length, and coordinate space at each transition.