Brain Space and Resolution¶
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.
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:
| 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:
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.