Introduction to neurotransform

Overview

neurotransform is a lightweight, self-contained R library for geometric transforms in neuroimaging. It serves as a transform kernel for spatial operations on MRI/CT-derived data, emphasizing correctness, composability, and mathematical elegance.

The package provides:

  • Affine transformations: rotation, translation, scaling, and shear
  • Nonlinear warps: displacement fields from registration tools
  • Volume-to-surface mappings: projecting volumetric data onto cortical meshes
  • Surface-to-surface mappings: inter-mesh correspondence via spherical registration
  • Multi-tool support: ANTs, FSL, AFNI, and FreeSurfer formats

Design Philosophy

neurotransform is built around category-theoretic concepts, treating coordinate transformations as morphisms between spatial domains. This approach provides: 1. Clean composition: transforms combine naturally via compose() 2. Invertibility tracking: each transform knows whether it can be inverted exactly or approximately 3. Pullback semantics: consistent convention where morphisms map target coordinates back to source coordinates

Core Concepts

What is a Morphism?

In neurotransform, a morphism represents a coordinate transformation between two spatial domains. The key insight is that all transforms use pullback semantics: a morphism from domain A to domain B maps coordinates in B back to coordinates in A. This convention is natural for image resampling. When you want to create a new image in space B from data in space A, you need to know: “for each voxel in B, where should I sample from A?” The morphism answers exactly this question.

# Create an affine morphism from "native" space to "mni" space
aff <- Affine3DMorphism("native", "mni", matrix = affine_matrix)

# Transform coordinates: given coords in MNI, find corresponding coords in native
native_coords <- transform(aff, mni_coords)

The Morphism Hierarchy

neurotransform provides five concrete morphism types: | Morphism Type | Purpose | Invertible? | |—————|———|————-| | IdentityMorphism | Maps a domain to itself | Exact | | Affine3DMorphism | Linear 3D transformations | Exact | | Warp3DMorphism | Nonlinear deformation fields | Approximate (requires inverse warp) | | VolToSurfMorphism | Volume → surface projection | Adjoint only | | SurfToSurfMorphism | Surface mesh registration | Approximate |

Quick Start

Creating Morphisms

library(neurotransform)

# Identity morphism
id <- IdentityMorphism("my_space")

# Affine morphism with a 4x4 transformation matrix
mat <- diag(4)
mat[1:3, 4] <- c(10, 20, 30)  # translation
aff <- Affine3DMorphism("native", "template", matrix = mat)

# Warp morphism from a displacement field file
warp <- Warp3DMorphism(
  source = "native",
  target = "template",
  warp_path = "path/to/warp.nii.gz",
  warp_type = "ants"
)

Transforming Coordinates

# Single point
coords <- matrix(c(0, 0, 0), ncol = 3)
result <- transform(aff, coords)

# Multiple points
coords <- matrix(c(
  0, 0, 0,
  10, 20, 30,
  -5, 15, 25
), ncol = 3, byrow = TRUE)
result <- transform(aff, coords)

Composing Transforms

One of neurotransform’s strengths is elegant composition. Multiple transforms can be chained together:

# Two affine transforms
aff1 <- Affine3DMorphism("native", "intermediate", matrix = mat1)
aff2 <- Affine3DMorphism("intermediate", "template", matrix = mat2)

# Compose them
composed <- compose(aff1, aff2)

# Apply the composed transform
result <- transform(composed, coords)

# Equivalent to applying sequentially:
step1 <- transform(aff2, coords)
step2 <- transform(aff1, step1)

When composing two affine transforms, neurotransform automatically fuses them into a single affine (multiplying the matrices). For mixed types, it creates a MorphismPath:

# Mixed composition creates a path
aff <- Affine3DMorphism("native", "intermediate", matrix = mat)
warp <- Warp3DMorphism("intermediate", "template", warp_path = "warp.nii.gz")

path <- compose(aff, warp)  # Returns MorphismPath
result <- transform(path, coords)

Inverting Transforms

# Invert an affine (exact)
inv_aff <- invert(aff)

# Check if a transform is invertible
is_invertible(aff)  # TRUE for affine
is_invertible(warp)  # Depends on whether inverse_path is available

# For non-invertible transforms, use adjoint if available
adj <- adjoint(vol2surf_morphism)

Working with Neuroimaging Tools

ANTs Transforms

ANTs outputs transforms in LPS (Left-Posterior-Superior) coordinates. neurotransform handles this automatically:

# Load ANTs composite transform (H5 format)
morph <- Warp3DMorphism(
  source = "native",
  target = "mni",
  warp_path = "sub-01_Composite.h5",
  warp_type = "ants_h5"
)

# Load ANTs NIfTI warp
warp <- Warp3DMorphism(
  source = "native",
  target = "mni",
  warp_path = "sub-01_1Warp.nii.gz",
  warp_type = "ants"
)

# With inverse path for bidirectional mapping
warp <- Warp3DMorphism(
  source = "native",
  target = "mni",
  warp_path = "sub-01_1Warp.nii.gz",
  warp_type = "ants",
  inverse_path = "sub-01_1InverseWarp.nii.gz"
)

FSL Transforms

FSL FLIRT produces affine matrices in scaled voxel coordinates:

# Load FSL FLIRT affine
# Requires source and target image affines for proper conversion
fsl_aff <- fsl_flirt_to_internal_affine(
  "flirt.mat",
  source_affine = src_img_affine,
  target_affine = tgt_img_affine
)

# Create morphism
aff <- Affine3DMorphism("native", "standard", matrix = fsl_aff)

# FSL FNIRT warps
warp <- Warp3DMorphism(
  source = "native",
  target = "mni",
  warp_path = "fnirt_warp.nii.gz",
  warp_type = "fsl"
)

AFNI Transforms

AFNI uses RAI coordinates for affines and LPS for warps:

# Load AFNI affine (.aff12.1D file)
afni_mat <- afni_read_aff12("anat.aff12.1D")
ras_mat <- afni_aff12_to_ras(afni_mat)
aff <- Affine3DMorphism("native", "template", matrix = ras_mat)

# AFNI 3dQwarp output
warp <- Warp3DMorphism(
  source = "native",
  target = "template",
  warp_path = "WARP+tlrc.nii.gz",
  warp_type = "afni"
)

FreeSurfer Coordinates

FreeSurfer uses tkRAS coordinates offset from scanner RAS:

# Convert FreeSurfer tkRAS to scanner RAS
scanner_ras <- tkras_to_ras(tkras_coords, c_ras_offset)

# Convert back
tkras <- ras_to_tkras(scanner_ras, c_ras_offset)

Resampling Data

Volume Samplers

Samplers encapsulate data with an interpolation method:

# Create a volume sampler
sampler <- volume_sampler(
  data = volume_array,
  affine = voxel_to_world_matrix,
  method = "linear",
  outside = NA_real_
)

# Resample through a morphism
target_coords <- grid_coords(target_grid)
resampled_values <- resample(sampler, morphism = morph, coords = target_coords)

Grid Specifications

Define the geometry of your output space:

# Create a grid specification
target_grid <- grid_spec(
  dims = c(182, 218, 182),
  affine = mni_affine_matrix,
  domain = "mni"
)

# Get world coordinates for all voxels in the grid
coords <- grid_coords(target_grid)

Resampling Plans

For repeated resampling with the same geometry, precompute a plan:

# Create a resampling plan
plan <- make_resampling_plan(
  morphism = morph,
  source_grid = src_grid,
  target_grid = tgt_grid,
  interpolation = "linear",
  reuse_count = 10L
)

# Apply to multiple volumes efficiently
for (vol in volume_list) {
  result <- apply_resampling_plan(plan, source_data = vol, outside = NA_real_)
}

Jacobian Determinants

The Jacobian determinant quantifies local volume change under a transformation:

# Compute Jacobian field at specific coordinates
jac_field <- jacobian(morph, coords = test_coords)

# Extract determinants
dets <- det(jac_field)

# For volume modulation during resampling
resampled <- resample(sampler, morphism = morph, coords = coords, modulate = "jacobian")

# Create a Jacobian determinant volume
jac_vol <- jacobian_det_field(morph, grid = target_grid)

Log-Jacobian values are useful for statistical analysis:

log_jac <- jacobian_det(morph, coords = coords, log = TRUE)

Volume-to-Surface Mapping

Project volumetric data onto cortical surfaces:

# Create volume-to-surface morphism
vol2surf <- VolToSurfMorphism(
  source = "volume",
  target = "surface",
  method = "ribbon",
  ribbon_inner = "lh.white",
  ribbon_outer = "lh.pial",
  n_ribbon_samples = 6L
)

# Transform surface coordinates to volume coordinates
vol_coords <- transform(vol2surf, surface_vertices)

# Or use the adjoint for backprojection (surface values to volume)
backproject_fn <- adjoint(vol2surf)

Sampling methods:

  • "trilinear": Direct trilinear interpolation at vertex coordinates
  • "ribbon": Average samples along the cortical ribbon (white to pial)
  • "mid_thickness": Sample at midpoint between surfaces
  • "nearest": Nearest voxel value

Coordinate Conventions

neurotransform uses RAS (Right-Anterior-Superior) as its internal coordinate system. Utility functions handle conversions:

# ANTs/ITK use LPS
ras_coords <- lps_to_ras(lps_coords)
lps_coords <- ras_to_lps(ras_coords)

# FreeSurfer tkRAS
scanner_ras <- tkras_to_ras(tkras, c_ras)
tkras <- ras_to_tkras(scanner_ras, c_ras)

# Apply any affine matrix to coordinates
transformed <- apply_affine_to_coords(coords, affine_4x4)

Affine Utilities

Building Affine Matrices

Construct affines from components:

# Build from translation, rotation, scale
mat <- build_affine_matrix(
  translation = c(10, 20, 30),
  scales = c(1, 1, 1),
  angles = c(0, 0, pi/4),  # Z-Y-X rotation order
  shears = c(0, 0, 0),
  anchor = c(0, 0, 0)
)

Decomposing Affine Matrices

Extract components from an existing matrix:

components <- decompose_affine_matrix(affine_4x4)
# Returns: translation, rotation_matrix, scales, shears, angles

Common Workflows

Workflow 1: Register Native to Template

library(neurotransform)

# Load the warp from ANTs registration
warp <- Warp3DMorphism(
  source = "native",
  target = "mni",
  warp_path = "sub01_Composite.h5",
  warp_type = "ants_h5"
)

# Define target grid
mni_grid <- grid_spec(dims = c(182, 218, 182), affine = mni_affine)

# Create sampler for native image
native_sampler <- volume_sampler(native_data, native_affine, method = "linear")

# Resample
target_coords <- grid_coords(mni_grid)
warped_values <- resample(native_sampler, morphism = warp, coords = target_coords)
warped_volume <- array(warped_values, dim = mni_grid@dims)

Workflow 2: Transform ROI Coordinates

# ROI coordinates in native space
roi_coords <- matrix(c(
  45, 60, 30,
  48, 62, 32,
  50, 58, 28
), ncol = 3, byrow = TRUE)

# Load transform
warp <- Warp3DMorphism("native", "mni", "warp.nii.gz", warp_type = "ants")

# For native → MNI coordinates, we need the inverse
# (because pullback maps target → source)
inv_warp <- Warp3DMorphism("mni", "native", "InverseWarp.nii.gz", warp_type = "ants")
mni_coords <- transform(inv_warp, roi_coords)

Workflow 3: Chain Multiple Transforms

# Native → Freesurfer → MNI
native_to_fs <- Affine3DMorphism("native", "freesurfer", mat1)
fs_to_mni <- Affine3DMorphism("freesurfer", "mni", mat2)

# Compose into a path
path <- compose(native_to_fs, fs_to_mni)

# Apply to coordinates
mni_coords <- transform(path, native_coords)

# Verify domains
source_of(path)  # "native"
target_of(path)  # "mni"

Performance Tips

  1. Use resampling plans for repeated operations with the same geometry
  2. Compose transforms before applying to leverage automatic fusion
  3. Choose appropriate interpolation: “linear” is fast, “cubic” is smoother but slower
  4. Lazy loading: warp fields are loaded on first use and cached

Summary

neurotransform provides a mathematically principled approach to neuroimaging coordinate transforms with:

  • Consistent pullback semantics across all morphism types
  • Automatic handling of tool-specific coordinate conventions
  • Elegant composition with intelligent optimization
  • Full Jacobian support for volume change quantification

The package focuses on being a correct, minimal transform kernel that other neuroimaging tools can build upon.

Further Reading