Multi-Pose Data¶
What is Multi-Pose Imaging?¶
In multi-pose fUSI, a probe is physically stepped to a series of positions along one spatial axis. At each position (a pose), one or more volumes are acquired. Stacking the poses together extends the field of view beyond what a single probe position can cover.
The probe at each pose can image a 2D plane or a 3D volume, depending on the probe type:
- Linear probes (e.g., standard linear probes): each pose yields a single 2D image (one elevation slice). Stepping across N poses and stacking gives a 3D volume of N elevation slices.
- 2D probes (e.g., matrix, RCA, or stacked linear probes): each pose already yields a 3D volume. Stepping across N poses concatenates these volumes into a larger 3D volume.
Multiple fUSI systems support this approach, including Iconeus, EchoFrame, and AUTC.
ConfUSIus represents multi-pose data with a pose dimension and per-pose affine
transformations that record the physical position of each pose.
Rotational sweeps are not yet supported
consolidate_poses requires a
purely translational sweep, where the probe is shifted along one axis without
rotating. Rotational sweeps (so-called tomographic acquisitions) are not yet
supported and will raise a ValueError.
Loading Multi-Pose Data¶
Iconeus SCAN Files¶
Iconeus IcoScan stores recordings in SCAN files (.scan, .source.scan). Three
acquisition modes are supported by ConfUSIus:
| Mode | Dimensions | Typical use |
|---|---|---|
2Dscan |
(time, z, y, x) |
Single-pose fUSI time-series |
3Dscan |
(pose, z, y, x) |
Multi-pose anatomical volume |
4Dscan |
(time, pose, z, y, x) |
Multi-pose fUSI time-series (3D+t fUSI) |
Use load_scan to load SCAN files. This page focuses on
3Dscan and 4Dscan. See the I/O guide for a
general overview of SCAN file loading.
The examples below illustrate a recording from a mouse acquired with an IcoPrime-4D MultiArray probe—four linear probes stacked along the elevation axis, giving 4 elevation slices per pose—translated across multiple regularly spaced positions.
<xarray.DataArray 'scan_data' (pose: 15, z: 4, y: 72, x: 64)> Size: 2MB
dask.array<transpose, shape=(15, 4, 72, 64), dtype=float64, chunksize=(15, 4, 72, 64), chunktype=numpy.ndarray>
Coordinates:
* pose (pose) int64 120B 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
* z (z) float64 32B 0.0 2.1 4.2 6.3
* y (y) float64 576B 2.0 2.099 2.197 2.296 ... 8.702 8.801 8.899 8.998
* x (x) float64 512B -3.465 -3.355 -3.245 -3.135 ... 3.245 3.455 3.465
Attributes:
affines: {'physical_to_lab': ...} # shape (15, 4, 4)
scan_mode: 3Dscan
...
The probe was stepped across 15 positions, each contributing 4 elevation slices — a total of 60 slices once consolidated.
<xarray.DataArray 'scan_data' (time: 750, pose: 4, z: 4, y: 72, x: 64)> Size: 442MB
dask.array<transpose, shape=(750, 4, 4, 72, 64), dtype=float64, chunksize=(227, 4, 4, 72, 64), chunktype=numpy.ndarray>
Coordinates:
* time (time) float64 6kB 0.4 2.8 5.2 ... 1.793e+03 1.796e+03 1.798e+03
* pose (pose) int64 32B 0 1 2 3
pose_time (time, pose) float64 24kB 0.4 2.2 1.0 ... 1.799e+03 1.799e+03
* z (z) float64 32B 0.0 2.1 4.2 6.3
* y (y) float64 576B 2.0 2.099 2.197 2.296 ... 8.801 8.899 8.998
* x (x) float64 512B -3.465 -3.355 -3.245 ... 3.245 3.355 3.465
Attributes:
affines: {'physical_to_lab': ...} # shape (4, 4, 4)
scan_mode: 4Dscan
...
The probe was stepped across 4 positions, each contributing 4 elevation slices — a total of 16 slices once consolidated.
Other Systems¶
For other fUSI systems, multi-pose data must be assembled manually: load or construct
one DataArray per pose, stack them along a new pose dimension, and populate
da.attrs["affines"] with a (npose, 4, 4) array of per-pose affines.
Physical Coordinates and Affines¶
Spatial coordinates in a multi-pose DataArray are pose-relative: the z coordinate
(or whichever dimension is being swept) is defined in the probe frame and is the same for
every pose. The per-pose affines stored in da.attrs["affines"] map these probe-relative
coordinates to a common world space and record how each pose is positioned in that space.
For Iconeus SCAN files, load_scan automatically stores a
physical_to_lab affine of shape (npose, 4, 4)—one matrix per pose.
The pose_time Coordinate¶
When poses are acquired sequentially, each pose is captured at a slightly different
time. The pose_time non-dimension coordinate of shape (time, pose) records the exact
per-pose acquisition timestamp:
This is important for slice timing correction, which accounts for the fact that different poses were not acquired simultaneously.
Pose Consolidation¶
consolidate_poses merges the pose dimension
and the sweep spatial dimension into a single axis with physically meaningful
coordinates, producing a standard ConfUSIus DataArray.
consolidate_poses performs the following
steps:
- Read the per-pose affines to compute the world position of every
(pose, sweep_dim)voxel. - Find the primary sweep direction via SVD of all voxel positions.
- Project each voxel onto that axis and check that the resulting positions form a regular grid.
- Reindex the data in ascending position order, replacing
poseandsweep_dimwith a single consolidated coordinate in world space.
import confusius as cf
anat = cf.load("sub-01_acq-anat_pwd.scan")
volume = cf.multipose.consolidate_poses(anat)
print(volume)
<xarray.DataArray 'scan_data' (z: 60, y: 72, x: 64)> Size: 2MB
array([...])
Coordinates:
* z (z) float64 480B -21.38 -21.24 -21.1 -20.96 ... -13.4 -13.26 -13.12
* y (y) float64 576B 2.0 2.099 2.197 2.296 ... 8.702 8.801 8.899 8.998
* x (x) float64 512B -3.465 -3.355 -3.245 -3.135 ... 3.245 3.355 3.465
Attributes:
affines: {'physical_to_lab': ...} # shape (4, 4)
scan_mode: 3Dscan
...
15 poses × 4 slices = 60 consolidated z positions, spanning −21.4 to −13.1 mm in lab coordinates.
import confusius as cf
fus = cf.load("sub-01_task-awake_pwd.scan")
volume = cf.multipose.consolidate_poses(fus)
print(volume)
<xarray.DataArray 'scan_data' (time: 750, z: 16, y: 72, x: 64)> Size: 442MB
array([...])
Coordinates:
* time (time) float64 6kB 0.4 2.8 5.2 ... 1.793e+03 1.796e+03 1.798e+03
* z (z) float64 128B -21.38 -20.86 -20.33 ... -14.56 -14.03 -13.51
pose_time (time, z) float64 96kB 0.4 2.2 1.0 ... 1.799e+03 1.799e+03
* y (y) float64 576B 2.0 2.099 2.197 2.296 ... 8.801 8.899 8.998
* x (x) float64 512B -3.465 -3.355 -3.245 ... 3.245 3.355 3.465
Attributes:
affines: {'physical_to_lab': ...} # shape (4, 4)
scan_mode: 4Dscan
...
4 poses × 4 slices = 16 consolidated z positions. The pose_time coordinate is
preserved with dims (time, z): each slice retains the timestamp of the pose it
came from.
After consolidation, the per-pose affine stack is reduced to a single (4, 4) matrix
representing the consolidated volume's orientation in world space.
Parameters¶
consolidate_poses accepts two parameters that
may need adjusting depending on your setup:
sweep_dim(default:"z"): the spatial dimension being swept across poses. Change this if your sweep is along a different axis.affines_key(default:"physical_to_lab"): the key intoda.attrs["affines"]that holds the per-pose affine stack. Change this if your affines are stored under a different key.
# Example: sweeping along x using affines stored under a custom key.
volume = cf.multipose.consolidate_poses(
da,
sweep_dim="x",
affines_key="physical_to_scanner",
)
Regularity requirement
consolidate_poses will raise a
ValueError if the consolidated positions are not regularly spaced within a relative
tolerance of 1% (default rtol=0.01). This check ensures uniform voxel spacing,
which is required for registration and NIfTI export. Non-uniform spacing typically
indicates a misconfigured sweep.
Saving¶
After Consolidation¶
Once consolidated, a multi-pose DataArray is a standard ConfUSIus DataArray and can be saved to any format:
import confusius as cf
anat = cf.load("sub-01_acq-anat_pwd.scan")
volume = cf.multipose.consolidate_poses(anat)
# Save to NIfTI (creates .nii.gz and a JSON sidecar).
volume.fusi.save("sub-01_acq-anat_pwd.nii.gz")
# Or to Zarr.
volume.to_zarr("sub-01_acq-anat_pwd.zarr")
Without Consolidation¶
Non-consolidated data can be saved to Zarr directly, preserving the pose dimension
and all per-pose affines:
Saving non-consolidated data to NIfTI is not straightforward because NIfTI stores a single affine per file. If you need NIfTI output before consolidating (e.g., for per-pose slice timing correction), save each pose as a separate file: