04 — Derived maps

Compute a map that remembers how it was made.

A lot of analysis collapses a 4-D BOLD across time into a 3-D map — temporal SNR, a contrast, a searchlight statistic. The map you get back is a normal volume you can index, save, or plot; it just also remembers which image and mask produced it, so you can answer that question later without keeping notes.

Temporal SNR — the canonical case

from pathlib import Path
import sys

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

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

tsnr = bold.temporal_snr(mask=mask)
type(tsnr).__name__, tsnr.shape
('DenseNeuroVol', (np.int64(8), np.int64(8), np.int64(4)))

temporal_snr returns a DenseNeuroVol — the same type a 3-D NIfTI would read as. The numeric payload is in tsnr.data; the spatial frame is in tsnr.space. The new field is .provenance.

tsnr.provenance.method_name
tsnr.provenance.input_space_hash[:12]
tsnr.provenance.mask_hash[:12]
tsnr.provenance.n_voxels
108

This is the same Receipt shape series_roi produced on the previous page, now living on a volume instead of on a Result. The split is deliberate:

  • ROIExtractionResult.provenance belongs to an extraction — a multi-voxel cross-section of a 4-D series.
  • DenseNeuroVol.provenance belongs to a map — a 3-D output of an analysis.

Both are Receipts. Both content-address the same way.

Searchlight — the same shape, scaled

import numpy as np

result = ni.searchlight_apply(mask, radius=4.5, method=np.mean, data=bold)
type(result).__name__, result.values.shape, result.provenance.method_name
('SearchlightResult', (108,), 'mean')

searchlight_apply returns a SearchlightResult — values, centers, space, Receipt. Project it back to volume form for downstream IO:

vol = result.map_to_volume()
type(vol).__name__, vol.shape, vol.provenance.method_name
('DenseNeuroVol', (np.int64(8), np.int64(8), np.int64(4)), 'mean')

The projected volume is again a DenseNeuroVol with .provenance. The shape is the same as tsnr above; the method_name distinguishes them. Two analyses, two maps, same carrier.

Why the carrier matters

A workflow that produces N maps over N subjects ends up with a list of DenseNeuroVols, each carrying its .provenance. A group-mean operation can verify every input’s input_space_hash matches before reducing; the next step’s Receipt records the inputs it consumed. None of this requires a sidecar manifest the user has to remember to write.

When you write tsnr to disk, the Receipt travels with the file: ni.write_vol(...) routes through to_nibabel() and embeds it, and the explicit to_nibabel() + nib.save() form does the same thing — the subject of the next page.

What it refuses

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

  • temporal_snr(mask=...) with a mask from another space. The mask must share the BOLD image’s spatial frame. Matching array shapes are not enough; the affine and dimensions are checked before the reduction runs.
  • searchlight_apply(mask=..., data=bold) with a shifted same-shape mask. Shape agreement is not space agreement. searchlight_apply checks the mask’s affine before building neighborhoods, so a shifted mask raises instead of mixing values from the wrong locations.
  • Hand back an anonymous 3-D array from a reduction. Every reduction over a typed object returns a typed object. temporal_snr does not return np.ndarray; searchlight_apply does not return a tuple.

Next

05 — Round-trip is where the Receipt crosses the file boundary. The carrier on this page is DenseNeuroVol with .provenance; the carrier on the next page is .nii.gz with a comment extension. The contract is that the round-trip is provenance-safe.

See also

Back to top