Parcel time series from an atlas

Mean BOLD per parcel as a typed clustered vector.

Goal

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 Path
import sys

sys.path.insert(0, str(Path.cwd().parents[1] / "src"))

import numpy as np
import neuroim as ni

repo = 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) in enumerate(np.argwhere(m > 0)):
    labels[i, j, k] = (n % 3) + 1
atlas = 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 NeuroSpace

wrong = NeuroSpace(tuple(mask.space.dim), spacing=(9, 9, 9))
bold.parcel_means(ni.DenseNeuroVol(labels.astype(np.int16), wrong))
---------------------------------------------------------------------------
SpaceMismatchError                        Traceback (most recent call last)
Cell In[2], line 4
      1 from neuroim.neuro_space import NeuroSpace
      3 wrong = NeuroSpace(tuple(mask.space.dim), spacing=(9, 9, 9))
----> 4 bold.parcel_means(ni.DenseNeuroVol(labels.astype(np.int16), wrong))

File ~/code/pycode/neuroim-python/src/neuroim/neuro_vec.py:554, in NeuroVec.parcel_means(self, atlas, label)
    548 else:
    549     raise TypeError(
    550         "atlas must be a DenseNeuroVol or ClusteredNeuroVol, "
    551         f"got {type(atlas).__name__}"
    552     )
--> 554 assert_same_space(self, cvol)
    556 data = np.asarray(self.to_dense().data, dtype=np.float64)
    557 if data.ndim != 4:

File ~/code/pycode/neuroim-python/src/neuroim/verify.py:104, in assert_same_space(a, b)
    102         sa.compatible_with(sb)
    103     except ValueError as exc:
--> 104         raise SpaceMismatchError(
    105             f"assert_same_space: spatial contract mismatch — {exc}"
    106         ) from None
    107     return
    109 ha = _space_hash_of(a)

SpaceMismatchError: assert_same_space: spatial contract mismatch — NeuroSpace mismatch in affine: left=[[2.0, 0.0, 0.0, -8.0], [0.0, 2.0, 0.0, -8.0], [0.0, 0.0, 2.5, -5.0], [0.0, 0.0, 0.0, 1.0]], right=[[9.0, 0.0, 0.0, 0.0], [0.0, 9.0, 0.0, 0.0], [0.0, 0.0, 9.0, 0.0], [0.0, 0.0, 0.0, 1.0]]

A parcellation whose affine disagrees with the BOLD raises ValueError containing spatial contract mismatch — the same leaf refusal every spatial operation bottoms out in.

See also

Back to top