Reduce a 4-D BOLD to one mean time series per atlas parcel, with the parcellation’s spatial frame validated against the BOLD’s, and the result carrying provenance.
Code
from pathlib import Pathimport syssys.path.insert(0, str(Path.cwd().parents[1] /"src"))import numpy as npimport neuroim as nirepo = Path.cwd().parents[1] if Path.cwd().name =="cookbook"else Path.cwd()bold = ni.read_image(str(repo /"golden_tests/fixtures/tiny_bold.nii.gz"))mask = ni.read_image(str(repo /"golden_tests/fixtures/tiny_mask.nii.gz"), type="vol")# A 3-parcel label volume in the mask's frame (stand-in for an atlas).m = np.asarray(mask.data).astype(int)labels = np.zeros_like(m)for n, (i, j, k) inenumerate(np.argwhere(m >0)): labels[i, j, k] = (n %3) +1atlas = ni.DenseNeuroVol(labels.astype(np.int16), mask.space)clustered = bold.parcel_means(atlas)(type(clustered).__name__, np.asarray(clustered.ts).shape, # (n_timepoints, n_parcels) clustered.n_clusters, clustered.provenance.method_name)
('ClusteredNeuroVec', (10, 3), 3, 'parcel_means')
parcel_means accepts a label DenseNeuroVol (or a typed neuroim.atlas.VolumetricAtlas) and returns a ClusteredNeuroVec.
What’s guaranteed
The atlas/BOLD spatial frame is checked at the API surface before any reduction — parcel_means routes through the same verify.assert_same_space gate as series_roi.
.ts is time-major (n_timepoints, n_parcels); .n_clusters and .space travel with it; .provenance records the reduction.
What survives the file
ClusteredNeuroVec.provenance is a Receipt. Projecting a parcel statistic back to a volume (map_to_volume() on a derived ParcelEffectResult) yields a DenseNeuroVol whose Receipt embeds on to_nibabel().
Trapdoors
from neuroim.neuro_space import NeuroSpacewrong = NeuroSpace(tuple(mask.space.dim), spacing=(9, 9, 9))bold.parcel_means(ni.DenseNeuroVol(labels.astype(np.int16), wrong))
A parcellation whose affine disagrees with the BOLD raises ValueError containing spatial contract mismatch — the same leaf refusal every spatial operation bottoms out in.