Load a NIfTI file and get your data — with the spatial frame still attached.
Reading an image is one function call. You get the array you expect, plus the spatial frame that came with the file — so later steps can’t quietly mix images that don’t line up.
Load a file
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 =="start"else Path.cwd()bold = ni.read_image(str(repo /"golden_tests/fixtures/tiny_bold.nii.gz"))arr = np.asarray(bold.data)bold.shape, arr.dtype, float(arr.mean())
((8, 8, 4, 10), dtype('<f4'), 100.4787368774414)
That’s your data: a 4-D array, its dtype, and a value you can sanity-check. Indexing works the way you’d expect — bold.data[..., 0] is the first volume, bold.data[x, y, z, :] is one voxel’s time series.
The one extra thing each object carries is .space — the spatial frame (dimensions, voxel sizes, orientation) read from the file. It travels with the data automatically; you never assemble it by hand. The next page is about what that frame does for you.
Which reader runs
read_image picked DenseNeuroVec for the 4-D BOLD (the trailing axis is time) and, with type="vol", DenseNeuroVol for the 3-D mask. type= chooses the reader, it does not reinterpret bytes:
type="auto" (default) — pick by dimensionality.
type="vol" — volume reader; on a 4-D file it takes volume 0.
type="vec" — vector reader; on a 3-D file it promotes to a singleton-time 4-D (…, 1).
For most workflows the default is what you want; pass type= only when you need a specific shape.
Why a typed object?
The return type is not bookkeeping — it’s what lets you write analysis code without defensive checks. A DenseNeuroVec is the 4-D, time-aware type, so its time methods are just there:
bold.temporal_snr # a time-aware method, available because this is a NeuroVec
<bound method NeuroVec.temporal_snr of DenseNeuroVec
Type : DenseNeuroVec
Dimension : 8 X 8 X 4 X 10
Spacing : 2.0 X 2.0 X 2.5 X 1.25
Origin : -8.0, -8.0, -5.0, 0.0>
You call bold.temporal_snr(mask=mask) directly — no if data.ndim == 4: guard, no passing the affine around by hand. If you call a time method on a 3-D volume, that’s a wrong call your editor and a type checker can catch, instead of a confusing array error deep in a reduction. The safety is a side effect of getting the right object back; you don’t have to think about it to benefit from it.
What it refuses
You can skip this on a first read — it’s here so you know the edges.
Unknown reader mode.type= accepts only "auto", "vol", or "vec". Anything else raises ValueError: type must be one of 'auto', 'vol', or 'vec' rather than guessing.
A bare array as the return.read_image always returns a typed object with .space attached; it never hands back a plain numpy.ndarray. When you explicitly want the raw array, bold.data[...] works for any image, vol.as_array() is available on volume objects, and neuroim.compat.as_array(obj) is the generic helper — opting out is deliberate, never accidental.
read_image is the entry point. It uses nibabel underneath, but that’s an implementation detail. If you already hold a nibabel image for some other reason, ni.from_nibabel(img) is the explicit bridge — see interop.
Next
02 — Spatial frames introduces NeuroSpace, the frame that travels with every image and quietly keeps later steps from mixing mismatched data.