Aligning CT and MRI brain scans β voxel by voxel β using classical registration and deep learning.
Medical imaging generates two fundamentally different views of the same patient: MRI captures soft tissue detail, CT guides treatment planning. Before clinicians can use them together, these scans must be precisely aligned. DeepMedAlign automates that process β from raw NIfTI files to a perfectly warped, voxel-registered output β at scale, on 180 real patient brain scans.
Takes a patient's CT scan and warps it to match their MRI β millimetre by millimetre β so both scans occupy the same coordinate space and can be overlaid perfectly.
flowchart LR
subgraph INPUT["Input"]
A["π₯ Raw Patient Scan<br/>CT + MRI NIfTI Files"]
end
subgraph PRE["Preprocessing"]
B["π§ Normalize<br/>Skull Strip<br/>Resample to 1 mm"]
end
subgraph REG["Classical Registration"]
C["π Rigid<br/>β3 sec"]
D["π Affine<br/>β3 sec"]
E["γ°οΈ B-spline<br/>β3 min"]
end
subgraph DL["Deep Learning"]
F["π§ VoxelMorph<br/>DVF Prediction<br/>β50 ms"]
end
subgraph OUT["Output"]
G["β
Registered CT<br/>Aligned to MRI Space"]
end
A --> B --> C --> D --> E --> F --> G
style A fill:#1e3a5f,color:#fff,stroke:#4a90d9
style B fill:#1e3a5f,color:#fff,stroke:#4a90d9
style C fill:#2d5016,color:#fff,stroke:#6abf40
style D fill:#2d5016,color:#fff,stroke:#6abf40
style E fill:#2d5016,color:#fff,stroke:#6abf40
style F fill:#5a2d7a,color:#fff,stroke:#b06ad4
style G fill:#5a1a1a,color:#fff,stroke:#e05252
Evaluated on 36 unseen test subjects from the SynthRad 2023 brain dataset.
| Method | Dice β | HD95 (mm) β | Jac_neg% β | Inference Time |
|---|---|---|---|---|
| Rigid | 0.774 Β± 0.064 | 19.5 Β± 8.2 | 0.000% | ~3 sec |
| Affine | 0.775 Β± 0.064 | 19.5 Β± 8.3 | 0.000% | ~3 sec |
| B-spline (Classical) | 0.776 Β± 0.059 | 19.2 Β± 7.6 | β | ~3 min |
| VoxelMorph v1 (baseline) | 0.965 Β± 0.006 | 1.22 Β± 0.46 | 0.050% | ~50 ms |
| VoxelMorph v2 (elastic + Dice + Jac) | 0.9953 Β± 0.0025 | 0.00 Β± 0.00 | 0.100% | ~50 ms |
Target: Dice > 0.776 Β· HD95 < 19.2 mm Β· Inference in milliseconds
| Approach | Execution Strategy | Computations | Time |
|---|---|---|---|
| Classical B-spline | ~1,000 Iterative Loops on CPU | 1,000 Γ 4.9M voxels = 4.9 Billion calculations | ~3 min (180s) |
| VoxelMorph v2 (ours) | 1 Forward Pass on GPU CUDA Cores | 1 Γ 4.9M voxels (Parallel Matrix Multiplication) | 0.05 sec (50ms) |
Why the massive speedup?
- No Trial-and-Error: Classical algorithms start from scratch for every new patient, iteratively evaluating Mutual Information 1,000 times. VoxelMorph leverages learned priors from 24 hours of training to predict the 3D deformation field in a single forward pass.
- GPU Parallelization: Modern GPUs compute matrix transformations across all 4.9 million voxels simultaneously using thousands of CUDA cores, eliminating the CPU sequential processing bottleneck.
- Source: SynthRad 2023 β Task 1 (MR β CT brain registration)
- Subjects: 180 total β 125 train / 19 val / 36 test
- Resolution: 160 Γ 192 Γ 160 @ 1 mm isotropic
- Modalities: T1-weighted MRI + Planning CT (Hounsfield Units)
β οΈ Raw data (~15 GB) is not tracked in git. Download from SynthRad and place underdata/raw/synthrad/brain/.
Each brain scan is a 3D cube of 160 Γ 192 Γ 160 = ~4.9 million voxels. Loading raw NIfTI files during training is extremely slow (~2 sec each). Converting once to .npy reduces load time from 7 hours β 13 minutes across a full 200-epoch run. Conversion is done once via scripts/build_npy_cache.py.
β οΈ Raw data (~15 GB) not included. Download from SynthRad 2023 first.
πͺ Windows (PowerShell)
# 1. Create and activate virtual environment
python -m venv .venv
.\.venv\Scripts\Activate.ps1
# 2. Install dependencies
pip install -r requirements-windows.txt
# 3. Preprocess all 180 subjects (skull-strip, normalise, resample)
python scripts\run_preprocessing_batch.py --resume --no-hdbet
# 4. Run classical registration (rigid + affine on all subjects)
python scripts\run_classical.py --no-bspline
# 5. Build NPY cache for fast training
python scripts\build_npy_cache.py --verify
# 6. Generate CT brain masks (needed for Dice loss during training)
python scripts\generate_ct_mask_npy.py
# 7. Train VoxelMorph (v2 β full config)
python scripts\train_voxelmorph.py `
--epochs 200 --cosine --diffeomorphic `
--sigma 0.1 --lr 0.0003 `
--elastic --lambda-dice 1.0 --lambda-jacobian 0.5 `
--out-prefix voxelmorph_v2 --device cuda
# 8. Evaluate on test set and compare against B-spline baseline
python scripts\evaluate_voxelmorph.py `
--checkpoint models\voxelmorph_v2_best.pth --compare-baseline
# 9. Generate difference map visualisations
python scripts\visualize_difference_maps.py --method voxelmorphπ§ Linux / π Mac (bash)
# 1. Create and activate virtual environment
python3 -m venv .venv
source .venv/bin/activate
# 2. Install dependencies
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121
pip install nibabel SimpleITK monai numpy pandas scikit-learn
# 3. Preprocess all 180 subjects
python scripts/run_preprocessing_batch.py --resume --no-hdbet
# 4. Run classical registration
python scripts/run_classical.py --no-bspline
# 5. Build NPY cache
python scripts/build_npy_cache.py --verify
# 6. Generate CT brain masks
python scripts/generate_ct_mask_npy.py
# 7. Train VoxelMorph v2
python scripts/train_voxelmorph.py \
--epochs 200 --cosine --diffeomorphic \
--sigma 0.1 --lr 0.0003 \
--elastic --lambda-dice 1.0 --lambda-jacobian 0.5 \
--out-prefix voxelmorph_v2 --device cuda
# 8. Evaluate on test set
python scripts/evaluate_voxelmorph.py \
--checkpoint models/voxelmorph_v2_best.pth --compare-baseline
# 9. Generate difference map visualisations
python scripts/visualize_difference_maps.py --method voxelmorphYour local RTX 4050 takes ~25 min/epoch β 83 hours for 200 epochs.
A Kaggle T4 GPU takes on average ~200 seconds per epoch β ~11 hours for 200 epochs (free!).
Step 1 β Zip just the code:
Compress-Archive -Path src, scripts, data\raw -DestinationPath kaggle_code.zip -ForceStep 2 β Upload the preprocessed data as a Kaggle Dataset:
- Go to Kaggle β Datasets β New Dataset
- Upload
deepmedalign-data-preprocessed.zip(~8.7 GB) - Name it:
deepmedalign-preprocessed-npy
Step 3 β In your Kaggle Notebook, run:
!unzip -q /kaggle/working/kaggle_code.zip -d /kaggle/working/
!pip install -q nibabel SimpleITK monai
!mkdir -p /kaggle/working/data/processed
!ln -s /kaggle/input/deepmedalign-preprocessed-npy/* /kaggle/working/data/processed/
!python /kaggle/working/scripts/train_voxelmorph.py \
--epochs 200 --cosine --diffeomorphic \
--sigma 0.1 --lr 0.0003 \
--elastic --lambda-dice 1.0 --lambda-jacobian 0.5 \
--out-prefix voxelmorph_v2 --device cuda --workers 2| Phase | Status |
|---|---|
| R1 β Data Pipeline | β Done |
| R1 Week 2 β NPY Cache + Manifests | β Done |
| R2 β Classical Registration | β Done |
| R3 β Visualisation & QC | β Done |
| Week 3 β VoxelMorph v1 (MI + Gradient Loss) | β Done |
| Week 4 β VoxelMorph v2 (Elastic + Dice + Jacobian) | β Done β Dice=0.9953, HD95=0.00mm |
| R4 β Final Evaluation & QC Dashboards | β Done β All 36 Test Patients Validated |
flowchart TD
R1["β
R1 Β· Data Pipeline\nDownload Β· Preprocess Β· Split\n180 brain scans ready"]
R1W2["β
R1 Week 2 Β· NPY Cache\nFast loader Β· Manifests\n180/180 ready in 0.01s"]
R2["β
R2 Β· Classical Registration\nRigid β Affine β B-spline\nDice=0.776, HD95=19.2mm"]
R3["β
R3 Β· Visualisation & QC\nCheckerboard overlays Β· Difference maps"]
W3["β
Week 3 Β· VoxelMorph v1\nMI Loss + Multi-Res Pyramid + Diffeomorphic"]
W4["β
Week 4 Β· VoxelMorph v2\nElastic Augmentation + Soft Dice + Jacobian Penalty"]
R4["β
R4 Β· Final Evaluation\nTest-set metrics Β· Side-by-side comparison Β· QC Dashboards"]
GOAL["π Goal Achieved!\nDice = 0.9953 (>0.776)\nHD95 = 0.00 mm (<19.2 mm)\nInference: 50 ms"]
R1 --> R1W2 --> R2 --> R3 --> W3 --> W4 --> R4 --> GOAL
style R1 fill:#1a3a1a,color:#7fff7f,stroke:#4caf50
style R1W2 fill:#1a3a1a,color:#7fff7f,stroke:#4caf50
style R2 fill:#1a3a1a,color:#7fff7f,stroke:#4caf50
style R3 fill:#1a3a1a,color:#7fff7f,stroke:#4caf50
style W3 fill:#1a3a1a,color:#7fff7f,stroke:#4caf50
style W4 fill:#1a3a1a,color:#7fff7f,stroke:#4caf50
style R4 fill:#1a3a1a,color:#7fff7f,stroke:#4caf50
style GOAL fill:#1a3a5f,color:#ffffff,stroke:#4a90d9
DeepMedAlign/
βββ data/
β βββ raw/ # Manifests & CSVs (tracked) Β· SynthRad source (NOT tracked)
β βββ processed/ # Normalised NIfTI + NPY cache (NOT tracked, ~15 GB)
βββ models/ # Saved .pth checkpoints (NOT tracked)
β βββ voxelmorph_best.pth # v1 baseline checkpoint
β βββ voxelmorph_v2_best.pth # v2 (elastic + dice + jacobian) checkpoint
βββ results/
β βββ baseline_metrics_bspline.csv
β βββ voxelmorph_test_metrics.csv
β βββ training_log.csv
β βββ figures/ # Checkerboard PNGs Β· Difference maps
βββ scripts/ # All runnable scripts (train, evaluate, preprocess, QC)
βββ src/ # Core library
β βββ voxelmorph_model.py # U-Net encoder-decoder + SpatialTransformer + VecInt
β βββ losses.py # MI loss Β· Gradient loss Β· Soft Dice loss Β· Jacobian loss
β βββ metrics.py # Dice Β· HD95 Β· NCC Β· Jacobian stats
β βββ dataset.py # MedicalRegistrationDataset (loads NPY + masks)
β βββ dataloader.py # DataLoader factory (train/val/test splits)
β βββ augmentation.py # Elastic deformation augmentation
β βββ classical_reg.py # SimpleITK rigid / affine / B-spline pipelines
β βββ preprocess_ct.py # CT normalisation + skull stripping
β βββ preprocess_mri.py # MRI normalisation + skull stripping
βββ tests/ # Unit tests β run with: pytest tests/ -v
A state-of-the-art VoxelMorph neural network tailored for multimodal MRI-CT registration.
flowchart TD
subgraph Input["Inputs"]
MR["MRI (160Γ192Γ160)"]
CT["CT (160Γ192Γ160)"]
end
subgraph Model["VoxelMorph U-Net"]
ENC["Encoder\n(16β32β32β32 features)\nDownsamples 4Γ"]
DEC["Decoder\n(32β32β32β16 features)\nMulti-resolution DVF pyramid"]
VECINT["VecInt (Diffeomorphic)\nScaling & Squaring (7 steps)\nGuarantees fold-free warps"]
end
subgraph Loss["Loss Functions"]
MI["Mutual Information\n(Parzen-window, Ο=0.1)\nHandles MRIβCT modality gap"]
GRAD["Gradient Smoothness\n(L2 penalty on DVF)\nPrevents jagged warps"]
DICE["Soft Dice Loss\n(Ξ»=1.0)\nBrain mask overlap supervision"]
JAC["Jacobian Penalty\n(Ξ»=0.5)\nPenalizes folded regions only"]
end
MR --> Model
CT --> Model
ENC --> DEC --> VECINT
VECINT --> |"DVF (B,3,D,H,W)"| ST["SpatialTransformer\n(Bilinear warping)"]
CT --> ST --> WarpedCT["Warped CT"]
WarpedCT --> MI
VECINT --> GRAD
VECINT --> JAC
WarpedCT --> DICE
style Input fill:#1e3a5f,color:#fff,stroke:#4a90d9
style Model fill:#5a2d7a,color:#fff,stroke:#b06ad4
style Loss fill:#1a3a1a,color:#7fff7f,stroke:#4caf50
| Loss | Purpose | Ξ» Weight |
|---|---|---|
| Mutual Information | Primary alignment signal β handles different MRI/CT intensities without assuming any relationship | Fixed |
| Gradient Smoothness | Keeps the deformation field smooth β prevents physically impossible jagged warps | 0.2 |
| Soft Dice | Supervises brain mask overlap directly β steers the network to align boundaries correctly | 1.0 |
| Jacobian Penalty | Penalizes only folded (negative determinant) voxels β stops the network from inverting tissue | 0.5 |
| Feature | v1 | v2 |
|---|---|---|
| Elastic Augmentation | β | β Random 3D elastic deformations |
| Soft Dice Loss | β | β Ξ»=1.0 |
| Jacobian Folding Penalty | β | β Ξ»=0.5 |
| Diffeomorphic Integration | β | β |
| Cosine Annealing LR | β | β |
| AMP (Mixed Precision) | β | β |
| Epoch | Val Loss | Val NCC | Jac Loss |
|---|---|---|---|
| 0 | -0.215 | 0.607 | ~0.0 |
| 5 | -0.234 | 0.641 | 3.4e-5 |
| 8 | -0.238 | 0.647 | 4.1e-5 |
NCC is steadily improving. jac_loss remains near-zero β confirming the diffeomorphic constraint is working correctly.
| Metric | What it measures | Target |
|---|---|---|
| Dice | Fraction of brain mask voxels that overlap after alignment | > 0.776 |
| HD95 | 95th-percentile worst-case boundary misalignment in mm | < 19.2 mm |
| Jac_neg% | Percentage of voxels where the warp folds back on itself | ~0% |
| NCC | Normalized Cross-Correlation of intensities (secondary sanity check) | Higher is better |
| Script | What it does |
|---|---|
scripts/train_voxelmorph.py |
Train the VoxelMorph model. Saves models/<prefix>_best.pth. |
scripts/evaluate_voxelmorph.py |
Evaluate a checkpoint on 36 test patients. Prints Dice/HD95/Jac table. |
scripts/build_npy_cache.py |
Convert NIfTI files to fast-loading .npy arrays (run once). |
scripts/generate_ct_mask_npy.py |
Generate CT brain masks needed for Dice loss (run once). |
scripts/run_classical.py |
Run rigid + affine + B-spline registration on all subjects. |
scripts/visualize_difference_maps.py |
Generate before/after alignment difference images. |
scripts/checkerboard_qc.py |
Generate checkerboard overlays for QC. |
scripts/compute_baseline_metrics.py |
Compute Dice/HD95 for classical registration baselines. |
| Scenario | Works? | Reason |
|---|---|---|
| Healthy adult brain MRI + CT (any scanner) | β Yes | Model trained on 180 diverse SynthRAD2023 brain patients |
| Different hospital scanner / brand | β Likely | Preprocessing normalizes all intensities to [0, 1] |
| Large head size variation | May lose precision at skull edges | |
| Extreme head tilt (>30Β°) | Rigid pre-registration recommended first | |
| Brain tumor / resection cavity | No pathological cases in training data | |
| Pelvis, thorax, or other body parts | β No | Model trained on brain anatomy only |
In short: This model works reliably for standard healthy adult brain MRI-CT registration after preprocessing. It is not a general-purpose registration tool and has not been clinically validated. A prospective study with radiologist review would be required before any real hospital deployment.
- Never commit directly to
mainβ open a PR at the end of each day - Keep
mainrunnable at all times - Branch naming:
r{id}/short-description - Never stage
.nii.gz,.npy,.pth, or.logfiles β they are in.gitignore
Research use only. Dataset governed by SynthRad 2023 terms.


