brain_deer.infrastructure.orientation

Single source of truth for spatial orientation in BrainDeer.

The whole app agrees on one world frame: RAS+ world millimetres, exactly as defined by a NIfTI image’s affine (the 4x4 voxel-index -> world-mm map that nibabel resolves from the qform/sform). Every coordinate that leaves voxel space - slice planes, crosshairs, tractography, DBS electrodes, atlas overlays - lives in this frame. Nothing invents its own centred/offset frame.

Why this module exists

Historically the 3D scene placed volumes in a synthetic frame, (voxel - (n-1)/2) * spacing, which silently discards the affine’s origin and rotation. That frame only coincides with true world mm when a volume’s field of view happens to be centred on the world origin, so objects authored in real world mm (MNI coordinates, Lead-DBS electrode markers, RASmm streamlines) drifted by a per-volume offset. Routing all voxel<->world conversions through the affine here removes that entire class of bugs: things already in world mm simply coincide.

Everything in this module is pure numpy/nibabel (no VTK, no Qt) so it can be unit-tested against synthetic affines without a display.

Functions

voxel_to_world(→ numpy.ndarray)

Map voxel indices (i, j, k) to world mm (x, y, z) via affine.

world_to_voxel(→ numpy.ndarray)

Inverse of voxel_to_world() using inv(affine).

axis_codes(→ AxisCodes)

Anatomical axis codes for an affine, e.g. ("R", "A", "S") or ("L", "P", "S").

is_ras(→ bool)

True when the affine is in strict RAS+ orientation (axis codes R, A, S).

decompose_affine(→ tuple[numpy.ndarray, numpy.ndarray, ...)

Split an affine into (spacing, direction, origin).

to_ras(→ nibabel.Nifti1Image)

Reorient any NIfTI image to strict RAS+ without moving it in world space.

Module Contents

brain_deer.infrastructure.orientation.voxel_to_world(affine: numpy.ndarray, voxel: numpy.ndarray) → numpy.ndarray

Map voxel indices (i, j, k) to world mm (x, y, z) via affine.

Accepts a single point of shape (3,) or a batch of shape (N, 3) and returns the matching shape. This is world = affine @ [i, j, k, 1] done for every point; it is the only correct voxel->world map because it honours the affine’s rotation and origin, not just its diagonal spacing.

brain_deer.infrastructure.orientation.world_to_voxel(affine: numpy.ndarray, world: numpy.ndarray, *, integer: bool = False) → numpy.ndarray

Inverse of voxel_to_world() using inv(affine).

Accepts (3,) or (N, 3) and returns the matching shape. With integer=True the result is rounded to nearest voxel indices (int64); otherwise continuous indices are returned for sub-voxel accuracy.

brain_deer.infrastructure.orientation.axis_codes(affine: numpy.ndarray) → AxisCodes

Anatomical axis codes for an affine, e.g. ("R", "A", "S") or ("L", "P", "S").

brain_deer.infrastructure.orientation.is_ras(affine: numpy.ndarray, *, tol: float = 0.0001) → bool

True when the affine is in strict RAS+ orientation (axis codes R, A, S).

tol is accepted for API symmetry with callers that pass one; axis-code comparison is exact, so it is currently unused but kept to avoid churn.

brain_deer.infrastructure.orientation.decompose_affine(affine: numpy.ndarray) → tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray]

Split an affine into (spacing, direction, origin).

  • spacing (3,): voxel size along each axis (Euclidean norm of each column).

  • direction (3, 3): unit direction cosines (columns), i.e. the pure rotation/flip.

  • origin (3,): world position of voxel (0, 0, 0) (the affine’s translation).

affine[:3, :3] == direction @ diag(spacing), so this cleanly separates the “where/orientation” from the “how big” of a volume.

brain_deer.infrastructure.orientation.to_ras(img: nibabel.Nifti1Image) → nibabel.Nifti1Image

Reorient any NIfTI image to strict RAS+ without moving it in world space.

Reorientation only relabels/reorders/flips voxel axes; the world-mm location of every voxel is preserved (voxel_to_world gives the same answer before and after). This is the single canonicaliser all loaders route through - NIfTI, DICOM (LPS patient space -> RAS), ROI masks, and Lead-DBS volumes - so that once data is inside BrainDeer it has exactly one orientation convention.

Memory notes: after nib.as_closest_canonical() volumes are usually already RAS, so the second (array-materialising) reorientation is skipped when possible - important for 4-D fMRI series. The rare manual-flip path uses float32 from the array proxy to avoid get_fdata()’s float64 doubling.