03 — ROI extraction

Pull a region’s time series, and keep track of where it came from.

You have a region — a sphere around a coordinate, or a mask — and you want every voxel’s time series from it. series_roi does that in one call and hands back the matrix together with the coordinates and the frame it came from, so the next step doesn’t need a second handle on the mask. It also checks the region and the image agree before it reads a voxel.

The call

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 == "start" 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")

roi = ni.spherical_roi(mask, centroid=(4, 4, 2), radius=2)
extraction = bold.series_roi(roi)

type(extraction).__name__, extraction.values.shape
('ROIExtractionResult', (10, 5))

spherical_roi returns an ROICoords — voxel indices plus the NeuroSpace they belong to. bold.series_roi(roi) checks that roi.space.compatible_with(bold.space) before it touches a single voxel.

The result object

extraction.values.shape           # (n_timepoints, n_voxels)
extraction.coords.shape           # (n_voxels, 3)
extraction.space.dim              # the frame these coords live in
extraction.provenance.method_name # "series_roi"
extraction.provenance.input_space_hash[:12]
extraction.provenance.mask_hash[:12]
'0865c9a18e97'

Four fields, all typed:

  • values — the time-by-voxel matrix, shape (n_timepoints, n_voxels). The numeric payload. .mean(axis=1) collapses across voxels; .mean(axis=0) collapses across time.
  • coords — the voxel coordinates that produced those values.
  • space — the spatial frame those coordinates live in (same NeuroSpace as the input).
  • provenance — a Receipt. Content-addressable; same inputs produce identical hashes.

Analysis code that wants the array calls extraction.values. Analysis code that wants to write the result back into a volume already has coords and space on the same object — no second handle on the mask, no second affine to keep in sync.

What the Receipt has

import dataclasses
dataclasses.asdict(extraction.provenance)
{'input_space_hash': 'f9fa648f58a28631',
 'mask_hash': '0865c9a18e97d0bf',
 'radius': None,
 'n_voxels': 5,
 'method_name': 'series_roi',
 'seed': None,
 'neuroim_version': '0.1.0',
 'source_affine_hash': '9c692291cc4b2564'}

Every field is a value, not a reference. Two extractions on the same BOLD with the same mask produce identical input_space_hash and mask_hash. The pair is the canonical “same input” check downstream: if two results disagree on either hash, they were not produced from the same source.

This will become important on the next page, where a searchlight chains a receipt onto the BOLD’s frame, and on page 05, where that receipt survives the file boundary.

What it refuses

Skippable on a first read — the edges, for when you hit them.

  • ROI coordinates from a different image space. series_roi checks the ROI’s NeuroSpace against the BOLD image before extracting values. If the affine or dimensions differ, it raises ValueError with spatial contract mismatch, instead of returning values from the wrong locations.
  • World coordinates outside the image grid. series_at_world converts millimetres to voxel coordinates and checks the result before indexing. Coordinates outside the grid raise an error rather than relying on NumPy’s negative-index wrapping.
  • Hand back a bare ndarray. series_roi returns a Result. The numeric payload is .values; the spatial context never strips off.

Why the result shape, not a tuple

A 4-tuple (values, coords, space, receipt) would carry the same information. A Result is preferred because:

  1. One handle for IO and provenance. Writing the extraction’s mean back into a volume only needs extraction.coords and extraction.space; the caller never wires together two unrelated returns.
  2. A Result survives serialization with its identity intact. A pickle round-trip of an ROIExtractionResult produces an equal Receipt; a pickle of (values, coords, space, receipt) is a tuple by another name.
  3. Concept stability. ROIExtractionResult and SearchlightResult share the .values / .space / .provenance shape. The whole rest of the API uses that shape as the unit of derived data.

Next

04 — Derived maps is the same idea at the level of a volume instead of an extraction: searchlight_apply and temporal_snr return a DenseNeuroVol whose .provenance plays the same role.

See also

Back to top