---
title: "Introduction to neurotransform"
author: "Brad Buchsbaum"
date: "`r Sys.Date()`"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Introduction to neurotransform}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  eval = FALSE
)
library(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.

```{r}
# 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

```{r}
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

```{r}
# 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:

```{r}
# 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`:

```{r}
# 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

```{r}
# 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:

```{r}
# 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:

```{r}
# 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:

```{r}
# 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:

```{r}
# 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:

```{r}
# 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:

```{r}
# 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:

```{r}
# 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:

```{r}
# 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:

```{r}
log_jac <- jacobian_det(morph, coords = coords, log = TRUE)
```

## Volume-to-Surface Mapping

Project volumetric data onto cortical surfaces:

```{r}
# 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:

```{r}
# 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:

```{r}
# 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:

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

## Common Workflows

### Workflow 1: Register Native to Template

```{r}
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

```{r}
# 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

```{r}
# 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

- Package documentation: `help(package = "neurotransform")`
- GitHub repository: https://github.com/bbuchsbaum/neurotransform
- For higher-level neuroimaging workflows, see the `neuroim2` package
