Skip to contents

Cortical parcellations divide the brain surface into labelled regions. They come from FreeSurfer annotation files, where each vertex on the cortical mesh is assigned to a region like “superior frontal gyrus” or “precuneus.”

This tutorial walks through the full pipeline for turning an annotation into a ggseg_atlas you can plot with ggseg and ggseg3d. We’ll use the Yeo 7-network parcellation as a working example — it has only 7 regions per hemisphere, so the pipeline runs quickly.

What you need

  • FreeSurfer installed with the fsaverage5 subject
  • ImageMagick for 2D geometry extraction
  • Chrome/Chromium for 3D screenshots

Locating annotation files

FreeSurfer stores annotation files alongside the subject’s surface data. You need the full paths to the lh. and rh. annotation files:

fs_dir <- freesurfer::fs_dir()
subjects_dir <- file.path(fs_dir, "subjects")

annot_files <- file.path(
  subjects_dir, "fsaverage5", "label",
  c("lh.Yeo2011_7Networks_N1000.annot",
    "rh.Yeo2011_7Networks_N1000.annot")
)

all(file.exists(annot_files))
#> [1] TRUE

Step 1: 3D-only atlas for fast iteration

Before committing to the full 2D extraction pipeline, verify your annotation looks right with a 3D-only atlas. This reads the annotation file, extracts vertex assignments, and returns in seconds:

yeo7_3d <- create_cortical_from_annotation(
  input_annot = annot_files,
  atlas_name = "yeo7",
  steps = 1
)

yeo7_3d
#> 
#> ── yeo7 ggseg atlas ──────────────────────
#> Type: cortical
#> Regions: 8
#> Hemispheres: left, right
#> Palette: ✔
#> Rendering: ✖ ggseg
#> ✔ ggseg3d (vertices)
#> ──────────────────────────────────────────
#> # A tibble: 16 × 3
#>    hemi  region                      label
#>    <chr> <chr>                       <chr>
#>  1 left  FreeSurfer_Defined_Medial_… lh_F…
#>  2 left  7Networks_1                 lh_7…
#>  3 left  7Networks_2                 lh_7…
#>  4 left  7Networks_3                 lh_7…
#>  5 left  7Networks_4                 lh_7…
#>  6 left  7Networks_5                 lh_7…
#>  7 left  7Networks_6                 lh_7…
#>  8 left  7Networks_7                 lh_7…
#>  9 right FreeSurfer_Defined_Medial_… rh_F…
#> 10 right 7Networks_1                 rh_7…
#> 11 right 7Networks_2                 rh_7…
#> 12 right 7Networks_3                 rh_7…
#> 13 right 7Networks_4                 rh_7…
#> 14 right 7Networks_5                 rh_7…
#> 15 right 7Networks_6                 rh_7…
#> 16 right 7Networks_7                 rh_7…

steps = 1 tells the pipeline to stop after reading annotation data. The result works with ggseg3d but has no 2D geometry yet.

Step 2: Full pipeline with 2D geometry

The full pipeline takes screenshots of each region from multiple view angles, extracts contours, and converts them to sf polygons. This is the slow part — it needs FreeSurfer, Chrome, and ImageMagick:

output_dir <- file.path(tempdir(), "yeo7_tutorial")

yeo7_raw <- create_cortical_from_annotation(
  input_annot = annot_files,
  atlas_name = "yeo7",
  output_dir = output_dir,
  tolerance = 1,
  smoothness = 2,
  skip_existing = TRUE,
  cleanup = FALSE,
  verbose = TRUE
)
#> 
#> ── Creating brain atlas "yeo7" ───────────
#> ℹ Input files: ']8;;file:///Applications/freesurfer/7.4.1/subjects/fsaverage5/label/lh.Yeo2011_7Networks_N1000.annot/Applications/freesurfer/7.4.1/subjects/fsaverage5/label/lh.Yeo2011_7Networks_N1000.annot]8;;' and ']8;;file:///Applications/freesurfer/7.4.1/subjects/fsaverage5/label/rh.Yeo2011_7Networks_N1000.annot/Applications/freesurfer/7.4.1/subjects/fsaverage5/label/rh.Yeo2011_7Networks_N1000.annot]8;;'
#> ℹ Setting output directory to ']8;;file:///var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T//Rtmpwix4Bl/yeo7_tutorial/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T//Rtmpwix4Bl/yeo7_tutorial]8;;'
#> ✔ 1/8 Loaded existing atlas data
#> 
2/8 Taking full brain snapshots
file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file22465346108a.html screenshot completed
#> 
2/8 Taking full brain snapshots [2.8s]
#> 
3/8 Taking region snapshots
file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file22464304f777.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file2246e08b720.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file2246464a390c.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file2246761357e2.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file22463092f974.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file2246543cc979.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file22461919aa83.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file22462e9a6b1b.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file22461a563f6f.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file22464fd1b9bf.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file224668aef16a.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file22462bc45ba7.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file224643f9ea1c.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file224619a8bec0.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file22465af31c87.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file22468250cfd.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file22463c62cf7e.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file2246174d4c8f.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file224665b82227.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file22464103fac.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file22467c09bdea.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file22466c1f9e6e.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file224651d5fc02.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file224659fa68d2.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file22463bde26e2.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file224659bdc698.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file22469845086.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file22462112a1cf.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file22461296e87a.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file2246292a78c5.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file22462a708884.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file224656cb8503.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file22463da79aad.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file2246288df2cd.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file22462357a355.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file22467e36203a.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file224635577b3d.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file224665490ca6.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file224666b92a9f.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file2246466637e3.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file22465fe8a4ff.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file2246692cb3cc.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file22463e2f18b0.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file2246613c1425.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file224659d79891.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file2246118ecb65.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file22463a3f9853.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file22464b6ca452.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file224624f9161a.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file22465c18b6b4.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file2246533e0204.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file2246641fe004.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file2246b465b2a.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file2246647b00c0.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file224623aec0c2.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file2246760cfb52.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file2246593c5fac.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file22467507eee8.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file22463ded556c.html screenshot completed
#> file:////private/var/folders/y5/zlbbcqn56gx1tcg6xfb425100000gp/T/Rtmpwix4Bl/file2246423532a8.html screenshot completed
#> 
3/8 Taking region snapshots [2m 10.8s]
#> 
4/8 Isolating regions

4/8 Isolating regions [21.8s]
#> 
5/8 Extracting contours

5/8 Extracting contours [6.8s]
#> 
6/8 Smoothing contours (smoothness = 2)

6/8 Smoothing contours (smoothness = 2)…
#> 
7/8 Reducing vertices (tolerance = 1)

7/8 Reducing vertices (tolerance = 1) […
#> 
8/8 Building final atlas

8/8 Building final atlas [93ms]
#> ✔ Brain atlas created with 16 regions
#> ℹ Pipeline completed in 2.8 minutes

yeo7_raw
#> 
#> ── yeo7 ggseg atlas ──────────────────────
#> Type: cortical
#> Regions: 8
#> Hemispheres: left, right
#> Views: inferior, lateral, medial,
#> superior
#> Palette: ✔
#> Rendering: ✔ ggseg
#> ✔ ggseg3d (vertices)
#> ──────────────────────────────────────────
#> # A tibble: 16 × 3
#>    hemi  region                      label
#>    <chr> <chr>                       <chr>
#>  1 left  FreeSurfer_Defined_Medial_… lh_F…
#>  2 left  7Networks_1                 lh_7…
#>  3 left  7Networks_2                 lh_7…
#>  4 left  7Networks_3                 lh_7…
#>  5 left  7Networks_4                 lh_7…
#>  6 left  7Networks_5                 lh_7…
#>  7 left  7Networks_6                 lh_7…
#>  8 left  7Networks_7                 lh_7…
#>  9 right FreeSurfer_Defined_Medial_… rh_F…
#> 10 right 7Networks_1                 rh_7…
#> 11 right 7Networks_2                 rh_7…
#> 12 right 7Networks_3                 rh_7…
#> 13 right 7Networks_4                 rh_7…
#> 14 right 7Networks_5                 rh_7…
#> 15 right 7Networks_6                 rh_7…
#> 16 right 7Networks_7                 rh_7…

A few things to note about the parameters:

  • tolerance controls vertex reduction in the output polygons. Higher values mean simpler polygons with fewer vertices.
  • smoothness controls contour smoothing. Higher values produce rounder edges.
  • cleanup = FALSE keeps intermediate files (screenshots, contour images) so you can re-run later steps without redoing the slow screenshot capture.
  • skip_existing = TRUE reuses existing intermediate files when resuming an interrupted run.

Step 3: Post-processing

The raw atlas contains every region from the annotation, including labels like “unknown” and “corpuscallosum” that you typically want as background outlines rather than filled regions.

atlas_region_contextual() keeps the geometry for spatial reference but removes the region from $core, so it renders as an outline:

yeo7_raw <- yeo7_raw |>
  atlas_region_contextual("cortex", match_on = "label") |>
  atlas_region_contextual("unknown", match_on = "label") |>
  atlas_region_contextual("corpuscallosum", match_on = "label") |>
  atlas_region_contextual("FreeSurfer_Defined_Medial_Wall", match_on = "label")

Step 4: Adding metadata

The raw atlas has region names derived from the annotation file. For the Yeo atlas, the annotation labels are numeric network IDs. We can map them to descriptive network names:

yeo7_metadata <- data.frame(
  label = c(
    "7Networks_1", "7Networks_2", "7Networks_3", "7Networks_4",
    "7Networks_5", "7Networks_6", "7Networks_7"
  ),
  region_pretty = c(
    "visual", "somatomotor", "dorsal attention",
    "ventral attention", "limbic", "frontoparietal", "default"
  )
)

core_with_meta <- yeo7_raw$core |>
  left_join(yeo7_metadata, by = "label") |>
  mutate(region = coalesce(region_pretty, region)) |>
  select(hemi, region, label)

Step 5: Rebuilding the atlas

Construct the final atlas from the modified core:

yeo7 <- ggseg_atlas(
  atlas = yeo7_raw$atlas,
  type = yeo7_raw$type,
  palette = yeo7_raw$palette,
  core = core_with_meta,
  data = yeo7_raw$data
)

yeo7
#> 
#> ── yeo7 ggseg atlas ──────────────────────
#> Type: cortical
#> Regions: 7
#> Hemispheres: left, right
#> Views: inferior, lateral, medial,
#> superior
#> Palette: ✔
#> Rendering: ✔ ggseg
#> ✔ ggseg3d (vertices)
#> ──────────────────────────────────────────
#> # A tibble: 14 × 3
#>    hemi  region      label         
#>    <chr> <chr>       <chr>         
#>  1 left  7Networks_1 lh_7Networks_1
#>  2 left  7Networks_2 lh_7Networks_2
#>  3 left  7Networks_3 lh_7Networks_3
#>  4 left  7Networks_4 lh_7Networks_4
#>  5 left  7Networks_5 lh_7Networks_5
#>  6 left  7Networks_6 lh_7Networks_6
#>  7 left  7Networks_7 lh_7Networks_7
#>  8 right 7Networks_1 rh_7Networks_1
#>  9 right 7Networks_2 rh_7Networks_2
#> 10 right 7Networks_3 rh_7Networks_3
#> 11 right 7Networks_4 rh_7Networks_4
#> 12 right 7Networks_5 rh_7Networks_5
#> 13 right 7Networks_6 rh_7Networks_6
#> 14 right 7Networks_7 rh_7Networks_7
atlas_regions(yeo7) |> sort()
#> [1] "7Networks_1" "7Networks_2"
#> [3] "7Networks_3" "7Networks_4"
#> [5] "7Networks_5" "7Networks_6"
#> [7] "7Networks_7"
plot(yeo7)
2D brain atlas plot showing the Yeo 7-network cortical parcellation with seven coloured regions across lateral, medial, inferior, and superior views.

Yeo 7-network cortical parcellation plotted with ggseg.

Applying the same pattern to larger atlases

The Yeo 7-network atlas runs in minutes because it has few regions. Larger atlases like aparc (34 regions/hemisphere) or aparc.a2009s (75 regions/hemisphere) take longer but follow the same pattern. Enable parallel processing before running:

library(future)
plan(multisession, workers = 4)

library(progressr)
handlers("cli")
handlers(global = TRUE)

dk_annots <- file.path(
  subjects_dir, "fsaverage5", "label",
  c("lh.aparc.annot", "rh.aparc.annot")
)

dk <- create_cortical_from_annotation(
  input_annot = dk_annots,
  atlas_name = "dk",
  output_dir = "data-raw"
)

plan(sequential)

The screenshot and contour extraction steps parallelize across regions, so more workers means faster completion — up to a point. Four workers is a reasonable default for most machines.

Saving

Once you’re satisfied with the atlas, save it as package data:

usethis::use_data(yeo7, overwrite = TRUE, compress = "xz")

The compress = "xz" flag gives the best compression for sf geometry data.