Spatial Conventions¶
ConfUSIus works with three kinds of coordinate systems:
- the voxel space, linked to the underlying array storage and indexed by integer voxel coordinates,
- the physical space embedded in every DataArray's coordinates,
- and any number of world spaces (atlas, scanner, etc.) linked to the physical space
through affine transforms stored in
attrs["affines"].
Understanding these three spaces and the axis-ordering convention used throughout ConfUSIus makes it much easier to reason about I/O, registration, and downstream statistical analysis.
---
config:
layout: elk
---
flowchart LR
V["<b>Voxel space</b><br>(integer indices)"]
P["<b>Physical space</b><br>(probe-relative)"]
W1["<b>Scanner space</b>"]
ellipsis{{"..."}}
W2["<b>Atlas space</b>"]
V -->|".coords"| P
P -->|".attrs[affines]"| W1
P -->|".attrs[affines]"| W2
P --> ellipsis
ellipsis@{ shape: text }
Axis Ordering: (time, z, y, x)¶
Every ConfUSIus DataArray that represents a fUSI recording uses the dimension order
(time, z, y, x), where:
| Dimension | Physical axis | Typical size |
|---|---|---|
time |
Acquisition time | Thousands |
z |
Elevation (stacking direction) | 1 for 2D acquisitions |
y |
Axial / depth | Tens to hundreds |
x |
Lateral | Tens to hundreds |
Dimension ordering is mostly transparent in Xarray
Users familiar with neuroimaging may be more accustomed to spatiotemporal
conventions like (x, y, z, t). Thankfully, Xarray makes dimension ordering largely
transparent in practice: you can always refer to dimensions by name and in any
order (e.g. data.mean("time"), data.sel(x=4.54, y=-2.48, z=0.0)) rather than by
axis index, so you won't have to remember the order of the dimensions.
This ordering is motivated by several considerations.
- Equivalence with NIfTI: NIfTI stores arrays in column-major (Fortran) order as
(x, y, z, time). Transposing to the more Pythonic row-major (C) order is a zero-copy operation that yields(time, z, y, x). - Memory layout for volume-wise processing: In row-major order the last axes are
contiguous in memory, so
data[t]—a single spatial volume—is a contiguous block, which is the natural unit of work for IQ processing, motion correction, and similar operations. - Statistical analysis convention: After spatial processing, fUSI data is typically
reshaped to
(time, voxels)for GLMs and dimensionality reduction. This isdata.stack(voxels=["z", "y", "x"])in Xarray, matching the standard(samples, features)convention of scikit-learn and statsmodels. - Alignment with neuroanatomical atlases: For coronal preclinical fUSI,
(z, y, x) = (elevation, axial/depth, lateral)maps to(antero-posterior, superior-inferior, left-right), sharing the first two axes with BrainGlobe atlases (e.g. Allen CCFv3). The physical → world affine captures any remaining orientation difference (e.g. a lateral mirror). - Visualization: Most visualization tools (e.g. napari) expect the last two axes to
be the display axes of a 2D image.
data.sel(time=t, z=z)yields a(y, x)array that plots correctly without transposing.
Coordinate Systems¶
Voxel Space¶
Voxel space has its origin at voxel (0, 0, 0) and integer indices along each
spatial axis. It is the natural indexing space of the underlying array: DataArrays can
be indexed in voxel space using the standard Xarray integer-location indexer (.isel).
Physical Space¶
The physical space is the coordinate system embedded in the DataArray's dimension
coordinates. Its axes are (z, y, x) corresponding to (elevation, axial/depth,
lateral). The unit of the coordinates is determined by the units attribute of each
coordinate array and is not fixed by ConfUSIus — millimeters are typical for fUSI, but
any consistent unit can be used.
The origin is typically the center of the probe surface, but users are free to define any physical space they find convenient. What matters is that the coordinate values are internally consistent and carry a meaningful physical scale.
Physical coordinates are set at data-loading time and depend on the source format:
- EchoFrame: Lateral and axial coordinates are read from the acquisition metadata file.
- AUTC: Lateral and axial coordinates are supplied by the user as parameters to the conversion function. If coordinates are omitted, ConfUSIus falls back to bare voxel indices and emits a warning.
- Iconeus SCAN: Coordinates are derived from the
voxelsToProbeaffine embedded in the SCAN file. The axial coordinate (y) is sign-flipped so that it is always positive and increases with depth. - NIfTI: Coordinates are derived from the translation and scale components of the "best" affine transformation found in the file header.
- Hand-constructed DataArrays: The physical space is whatever the user assigns to the dimension coordinates.
The "best" NIfTI affine
NIfTI files can store two affine transforms in their header: an sform and a
qform, each with an associated integer code indicating whether the affine is
valid (code > 0) and which space it points to. ConfUSIus follows the NIfTI
specification: if sform_code > 0 the sform is used; otherwise, if
qform_code > 0 the qform is used. If both codes are zero a warning is emitted
and coordinates fall back to a diagonal affine built from the pixdim field.
Each spatial coordinate also carries a voxdim attribute that records the native voxel
size along that dimension:
This is set at load time alongside the physical coordinates and is preserved through
any Xarray operation that propagates coordinate attributes. It is particularly useful
after downsampling: the coordinate values themselves reflect the new, coarser spacing,
but voxdim retains the original acquisition resolution. It is used wherever native
voxel dimensions are needed — for example, for aspect-ratio-correct visualization,
NIfTI export, and as a fallback when a dimension is later reduced to a single point
(e.g. after .isel(z=0)):
da_down = da.isel(y=slice(None, None, 2), x=slice(None, None, 2))
# da_down.coords["y"].attrs["voxdim"] still holds the native voxel size.
da_slice = da_down.isel(z=0)
# da_slice.coords["z"].attrs["voxdim"] is now the only record of the z voxel size,
# since the coordinate has been collapsed to a scalar.
World Spaces¶
World spaces are external coordinate systems defined by other tools or standards—for example, an atlas space (Allen CCFv3), a scanner space, or a user-defined stereotactic space.
ConfUSIus stores affine transformations between the DataArray's physical space and
any world space in da.attrs["affines"], a dictionary keyed by affine name. Each
value is a (4, 4) homogeneous matrix in (z, y, x) convention that maps a
physical-space point to the corresponding world-space point:
where (pz, py, px) are the physical coordinates stored in da.coords.
Several loaders populate da.attrs["affines"] automatically:
- NIfTI: NIfTI files store
qformandsformaffines in their header that map voxel indices to world coordinates.load_niftireads the relevant affine(s), converts them from voxel → world to physical → world form, and stores them under the keys"physical_to_sform"and/or"physical_to_qform"depending on which codes are valid in the header. - Iconeus SCAN:
load_scanstores a"physical_to_lab"affine mapping ConfUSIus physical coordinates(z, y, x)to the Iconeus lab coordinate system. For multi-pose acquisitions (3Dscan,4Dscan), one affine per pose is stored, with shape(npose, 4, 4).
After registration to an atlas, you would typically store the result yourself:
_, affine_to_atlas = cf.registration.register_volume(moving, atlas)
da.attrs["affines"]["physical_to_atlas"] = affine_to_atlas
Why physical → world, not voxel → world?
The standard NIfTI affine maps voxel indices → world coordinates. ConfUSIus uses a physical → world affine instead, for one practical reason: it is invariant to slicing and downsampling.
A voxel → world affine encodes the origin as the world position of voxel (0,0,0)
specifically. The moment you crop or subsample the DataArray—even a simple
.isel(y=slice(10, 50))—voxel (0,0,0) is no longer in the array, and the
affine silently points to the wrong location.
A physical → world affine operates on the coordinate values already stored in
da.coords. Those values travel with the data through any Xarray operation that
preserves coordinates, so the affine remains valid without any adjustment.
save_nifti reconstructs the full voxel → world NIfTI
affine internally by combining the physical → world orientation matrix with the
dimension coordinate origin and spacing.