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:
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
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.
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 |
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"
)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:
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 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 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"
)Samplers encapsulate data with an interpolation method:
Define the geometry of your output space:
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_)
}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:
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 valueneurotransform uses RAS (Right-Anterior-Superior) as its internal coordinate system. Utility functions handle conversions:
Construct affines from components:
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)# 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)# 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"neurotransform provides a mathematically principled approach to neuroimaging coordinate transforms with:
The package focuses on being a correct, minimal transform kernel that other neuroimaging tools can build upon.
help(package = "neurotransform")neuroim2 package