Spatial normalization to MNI space¶
Lacuna expects lesion masks to be in MNI space. This guide demonstrates how to register a brain image and its associated lesion mask to the MNI template using ANTsPy.
We use ANTsPy's built-in test data (the CH2 brain) to create a fully reproducible example. In practice, the moving image would be your subject's native-space T1w scan.
You can also use ANTs via R or directly via the terminal. Besides ANTs, several alternative registration approaches are available. In our experience, the deep learning–based method EasyReg (implemented in FreeSurfer) performs well and is substantially faster than the ANTs SyN algorithm demonstrated here.
What you'll learn:
- Register a brain image to MNI space with ANTsPy
- Apply the warp to a lesion mask
- Verify registration quality
Accepted spaces¶
Lacuna accepts masks in two MNI template spaces:
| Space | Commonly used in |
|---|---|
MNI152NLin6Asym |
FSL, HCP pipelines |
MNI152NLin2009cAsym |
fMRIPrep, SPM12 |
Once your mask is in either space, Lacuna handles all further transformations automatically (resampling, cross-space warping). See the Coordinate Spaces explanation for details.
Setup¶
!pip install antspyx nibabel nilearn
import ants
from ants.utils.nibabel_nifti_to_ants import to_nibabel_nifti
import numpy as np
import nibabel as nib
from nilearn import plotting
Load images¶
We load the CH2 brain (T1w image) as our "subject" and the MNI152NLin6Asym template as the registration target. Both are bundled with ANTsPy.
In practice, you would replace subject_t1 with your own native-space T1w scan.
subject_t1 = ants.image_read(ants.get_ants_data("ch2"))
template = ants.image_read(ants.get_ants_data("mni"))
print(f"Subject T1 — shape: {subject_t1.shape}, voxel size: {subject_t1.spacing}")
print(f"Template — shape: {template.shape}, voxel size: {template.spacing}")
Subject T1 — shape: (181, 217, 181), voxel size: (1.0, 1.0, 1.0) Template — shape: (182, 218, 182), voxel size: (1.0, 1.0, 1.0)
Note: The CH2 brain used in this demonstration is itself an MNI-space template (the Colin27 brain), so the registration here is between two MNI-like brains. With real data, the moving image would be a native-space T1w scan, and the nonlinear deformation would be substantially larger.
Create a simulated lesion¶
To demonstrate the full workflow, we create a synthetic spherical lesion on the subject image.
# Create a binary sphere as a simulated lesion
lesion_data = np.zeros(subject_t1.shape, dtype=np.float32)
# Place the lesion at a voxel coordinate (roughly right occipital)
center = np.array([110, 140, 90])
radius = 15
# Build a spherical mask
coords = np.indices(lesion_data.shape).reshape(3, -1).T
distances = np.linalg.norm(coords - center, axis=1)
lesion_data.flat[distances <= radius] = 1.0
# Mask the sphere with brain tissue so the lesion follows the anatomical boundaries.
# This makes the warping effect more apparent in the visualization.
brain_mask = subject_t1.numpy() > np.percentile(subject_t1.numpy()[subject_t1.numpy() > 0], 15)
lesion_data *= brain_mask.astype(np.float32)
# Convert to an ANTs image with the same spatial metadata as the subject T1
lesion = ants.from_numpy(
lesion_data,
origin=subject_t1.origin,
spacing=subject_t1.spacing,
direction=subject_t1.direction,
)
subject_t1.plot(overlay=lesion, axis=2, slices=(85), black_bg=True, figsize=5)
Visualize the MNI template. This is the registration target.
template.plot(axis=2, slices=(85), black_bg=True, figsize=5)
Register subject T1 to MNI¶
We compute a nonlinear (SyN) registration from the subject T1 to the MNI template. This produces forward transforms that map from subject space to MNI space.
registration = ants.registration(
fixed=template,
moving=subject_t1,
type_of_transform="SyN",
)
# The result contains the warped T1 and the transform files
print("Transform files:", registration["fwdtransforms"])
Transform files: ['/tmp/tmpn559trf31Warp.nii.gz', '/tmp/tmpn559trf30GenericAffine.mat']
Inspect the registered T1 in MNI space
# Registered T1 in MNI space
t1_mni = registration["warpedmovout"]
# Only registered T1 in MNI152NLin6Asym space
t1_mni.plot(axis=2, slices=(85), black_bg=True, figsize=5)
# Registered T1 with MNI152NLin6Asym template overlay
t1_mni.plot(overlay=template, overlay_alpha=0.4, axis=2, slices=(85), black_bg=True, figsize=5)
Apply the transform to the lesion mask¶
We apply the same transforms to the lesion mask. The critical detail is using nearestNeighbor interpolation to preserve the binary nature of the mask — other interpolation methods would introduce non-binary values between 0 and 1.
lesion_mni = ants.apply_transforms(
fixed=template,
moving=lesion,
transformlist=registration["fwdtransforms"],
interpolator="nearestNeighbor", # preserve binary mask values
)
print(f"Lesion in MNI — shape: {lesion_mni.shape}, spacing: {lesion_mni.spacing}")
print(f"Unique values: {np.unique(lesion_mni.numpy())}") # should be [0, 1]
Lesion in MNI — shape: (182, 218, 182), spacing: (1.0, 1.0, 1.0) Unique values: [0. 1.]
t1_mni.plot(overlay=lesion_mni, axis=2, slices=(85), black_bg=True, figsize=5)
Save the result¶
Write the MNI-space lesion mask to disk. This file can then be used as input for Lacuna.
ants.image_write(lesion_mni, "/tmp/lesion_MNI.nii.gz")