Skip to content

Brain Space and Resolution

Open in molab

Run this tutorial

This page is rendered from the marimo notebook docs/tutorials/data-operations/03_brain_space.py. Click the badge to run it in the cloud (free, no install), or locally: download 03_brain_space.py and run uvx marimo edit --sandbox 03_brain_space.py. The outputs below were produced when this page was built.

Every BrainData object lives on a grid: a template, a resolution, and the brain mask that decides which voxels are in. nltools defaults to a 2 mm MNI152 template, and gives you three levels of control over that choice — a global setting, a scoped override, and a per-object mask.

The current space

get_brainspace() reports the active configuration and the files it resolves to:

from nltools import (
    get_brainspace,
    reset_brainspace,
    set_brainspace,
    with_brainspace,
)
from nltools.data import BrainData, Simulator

get_brainspace()
BrainSpaceConfig(template='default', resolution=2mm)
  mask: 2mm-MNI152-2009fsl-mask.nii.gz
  brain: 2mm-MNI152-2009fsl-brain.nii.gz
  plot: 2mm-MNI152-2009fsl-T1.nii.gz

Simulated data to work with

Simulator generates BrainData with a known signal, which is a convenient way to see which grid an object landed on. create_data drops a spherical signal of each level into a noisy brain; reps sets how many images per level and the second argument sets the noise. Objects it creates have no grid of their own, so they adopt the current brain space — 2 mm here, about 240k voxels.

dummy_brain = Simulator(random_state=0).create_data([0, 1], 1, reps=3)
dummy_brain
nltools.data.braindata.BrainData(data=(6, 238955), resolution=2.0mm, space=mni, mask=2mm-MNI152-2009fsl-mask.nii.gz)

The level of each image is recorded in .Y:

dummy_brain.Y
shape: (6, 1)
y
i64
0
1
0
1
0
1

Save it, so there is a 2 mm file on disk to load back in below:

import tempfile
from pathlib import Path

brain_2mm = Path(tempfile.mkdtemp()) / "dummy_2mm_brain.nii.gz"
dummy_brain.write(str(brain_2mm))
print(f"{brain_2mm.name}: {brain_2mm.stat().st_size / 1e6:.1f} MB")
dummy_2mm_brain.nii.gz: 5.6 MB

Changing the space globally

set_brainspace() changes the default for everything that follows. Set it once at the top of an analysis: changing it midway leaves later objects on a different grid than earlier ones, which is a hard mistake to spot.

print(f"{brain_2mm.name} was written on the 2 mm grid")
print(set_brainspace(resolution=3))

# A fresh simulation now lands on the 3 mm grid, about 71k voxels
dummy_3mm = Simulator(random_state=0).create_data([0, 1], 1, reps=3)
dummy_3mm
dummy_2mm_brain.nii.gz was written on the 2 mm grid
BrainSpaceConfig(template='default', resolution=3mm)
  mask: 3mm-MNI152-2009fsl-mask.nii.gz
  brain: 3mm-MNI152-2009fsl-brain.nii.gz
  plot: 3mm-MNI152-2009fsl-T1.nii.gz
nltools.data.braindata.BrainData(data=(6, 71020), resolution=3.0mm, space=mni, mask=3mm-MNI152-2009fsl-mask.nii.gz)

reset_brainspace() puts the default back:

print(f"simulated on the 3 mm grid: {dummy_3mm.shape[-1]} voxels")
default_space = reset_brainspace()
default_space
simulated on the 3 mm grid: 71020 voxels
BrainSpaceConfig(template='default', resolution=2mm)
  mask: 2mm-MNI152-2009fsl-mask.nii.gz
  brain: 2mm-MNI152-2009fsl-brain.nii.gz
  plot: 2mm-MNI152-2009fsl-T1.nii.gz

Changing it temporarily

with_brainspace() does the same thing for the duration of a block and restores the previous setting on exit, including when the block raises. Prefer it when only part of an analysis belongs in another space.

print(f"before the block: {default_space.resolution} mm")

with with_brainspace(resolution=3):
    scoped = Simulator(random_state=0).create_data([0, 1], 1, reps=1)

print(f"inside the block:  {scoped.shape[-1]} voxels")
print(f"after the block:   {get_brainspace().resolution} mm is active again")
before the block: 2 mm
inside the block:  71020 voxels
after the block:   2 mm is active again

What the global setting does and does not control

The setting is a default for objects that have no grid of their own. A file that already sits on a recognized template grid keeps it: loading the 2 mm file written above gives 2 mm data even while the global setting says 3 mm.

with with_brainspace(resolution=3):
    loaded_under_3mm = BrainData(str(brain_2mm))

print(f"global default: {default_space.resolution} mm")
print(f"3 mm default, 2 mm file -> {loaded_under_3mm.shape[-1]} voxels")
global default: 2 mm
3 mm default, 2 mm file -> 238955 voxels

Per-object masks

To put one object on a specific grid regardless of the global setting, pass mask. It takes a template name, a path to any NIfTI file, or a nibabel image, and the data is resampled to match.

print(f"global default is still {default_space.resolution} mm")
on_3mm_grid = BrainData(str(brain_2mm), mask="3mm-MNI152-2009fsl")
print(f"named 3 mm mask -> {on_3mm_grid.shape[-1]} voxels")
on_3mm_grid
global default is still 2 mm
named 3 mm mask -> 71020 voxels
nltools.data.braindata.BrainData(data=(6, 71020), resolution=3.0mm, space=native, mask=3mm-MNI152-2009fsl-mask.nii.gz)

The global setting is untouched by that:

print(f"the object above holds {on_3mm_grid.shape[-1]} voxels, and yet:")
get_brainspace()
the object above holds 71020 voxels, and yet:
BrainSpaceConfig(template='default', resolution=2mm)
  mask: 2mm-MNI152-2009fsl-mask.nii.gz
  brain: 2mm-MNI152-2009fsl-brain.nii.gz
  plot: 2mm-MNI152-2009fsl-T1.nii.gz

Template names follow '{resolution}mm-MNI152-2009{version}', where the version letter picks the family: fsl for the bundled default, a for nilearn's, c for the fMRIPrep template. get_brainspace().mask, .brain and .plot give the resolved file paths when you need to hand them to another tool.