Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
cb38ded
ported over files from personal repo to shared repo
arkiev Mar 17, 2026
2155d6e
working pipeline but DWI is python
arkiev Mar 18, 2026
7f4fcb1
test that is running
arkiev Mar 19, 2026
fa62a9e
fixed DWI T1 coregistration and reintroduced it to -rpe_none, updated…
arkiev Mar 19, 2026
5b3437b
fixes to github error 67834536377
arkiev Mar 19, 2026
d515434
fixes to github test (ubuntu-latest, 3.11)
arkiev Mar 20, 2026
4e76e3c
Updated cli.py and test_cli.py to align with the pydra 1.0 Submitter …
arkiev Mar 20, 2026
3586443
updated readme
arkiev Mar 20, 2026
108ca6e
updated docs to reflect integration of single-workflow changes
arkiev Mar 23, 2026
ece2e00
removed unneeded import
tclose Mar 24, 2026
24e3e51
120E template image with segmentations
arkiev Mar 26, 2026
7b3e556
resolve merge conflicts: accept deletion of template_data/SPIRIT/
arkiev Mar 26, 2026
98e6151
Add 120E ADC reference values for vials A-X
arkiev Mar 26, 2026
a11b202
Merge branch 'single_workflow' into 120E_testing
arkiev Mar 26, 2026
cf83b12
Restore template_data/SPIRIT from single_workflow
arkiev Mar 26, 2026
b8d9f5a
suppressed registration check for not SPIRIT, runs to compeletion - C…
arkiev Mar 27, 2026
25b5f91
removed registration check for non-spirit phantoms
arkiev Mar 30, 2026
c0fee27
removed reg check and rotations - seems redundant given scan protocol
arkiev Mar 31, 2026
721de55
removed fallback to SPIRIT if failed for 120E
arkiev Apr 1, 2026
58206e5
reverted back to original jsons
arkiev Apr 1, 2026
2d83ff3
pydrafied phantom_processsor file
arkiev Apr 1, 2026
66f5c64
pydrafied version that seems to work
arkiev Apr 1, 2026
33a31ab
updated flowchart
arkiev Apr 1, 2026
0a8ff69
updated flowchart code again
arkiev Apr 1, 2026
723d664
added in temperature calibration plotter
arkiev Apr 1, 2026
7b1015a
plot mode and estimate mode enabled
arkiev Apr 1, 2026
44be0c5
removed redundant rotations.txt. added functionality to handle adc_me…
arkiev Apr 2, 2026
ecd1212
zoom feature and toggle vials
arkiev Apr 2, 2026
6c1f558
integrated vial temp estimates
arkiev Apr 2, 2026
cab8e7c
tidy up
arkiev Apr 17, 2026
e4b8e15
add compare-plots command for cross-session measurement comparison
arkiev Apr 17, 2026
8e939ad
embed phantom name in T1/T2 mapping HTML; improve compare-plots phant…
arkiev Apr 17, 2026
edde29e
improve compare-plots labels, font sizes, and marker sizes
arkiev Apr 17, 2026
122db18
updated 120E template data
arkiev Apr 17, 2026
4d3bb3a
removed registration check and rotations (redundant with scanning set…
arkiev Apr 17, 2026
28803fb
120E pipeline test works
arkiev Apr 17, 2026
b009721
input type agnostic and added in 120E relaxometry compatibility
arkiev Apr 20, 2026
7c86110
replacing "vials_labelled" with VialsLabelled
arkiev Apr 20, 2026
c15e709
misc plotting updates
arkiev Apr 22, 2026
e9196d1
added missing -interp nearest and created dockerfile
arkiev Apr 22, 2026
00c65d2
update docker to cater beyond macs
arkiev Apr 22, 2026
e85967d
docker build fixes
arkiev Apr 22, 2026
c918890
nifti, mif and zipped variants added to possible inputs
arkiev Apr 22, 2026
93562be
temp calibration inclusion in main workflow
arkiev Apr 23, 2026
80a74b2
reorder docker readme
arkiev Apr 23, 2026
b81d453
consolidate per-contrast CSVs into multi-sheet xlsx files
arkiev Apr 29, 2026
4956f77
improved user functionality (mean/median, errorbar options), condense…
arkiev Apr 29, 2026
e32c733
updated median absolute deviation calculation to use median (not mean…
arkiev Apr 30, 2026
b5c9f33
updating parsing to handle SEQ#
arkiev May 4, 2026
b368c61
now handles filenames with no separator (eg: TE50)
arkiev May 5, 2026
6fab8e0
misc superficial updates
arkiev May 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.git/
__pycache__/
**/*.pyc
**/*.pyo
.pytest_cache/
.mypy_cache/
.coverage
htmlcov/
dist/
build/
**/*.egg-info/
docs/
.github/
*.sh
175 changes: 139 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@

# PhantomKit

**PhantomKit** is a Python toolkit for automated quality assurance (QA) of medical imaging scanners using physical phantoms. It provides pydra-based workflows that register phantom scans to a reference template, extract per-vial signal statistics across multiple contrast types, and generate publication-quality plots — supporting both MRI and PET phantom protocols.
**PhantomKit** is a Python toolkit for automated quality assurance (QA) of MRI scanners using physical phantoms. It provides a high-level processing engine and pydra-based workflows that register phantom scans to a reference template, extract per-vial signal statistics across multiple contrast types, and generate publication-quality plots — with full support for DWI preprocessing and diffusion metric (ADC, FA) extraction.

## Features

- **Template-based registration** — iterative ANTs SyN registration with automatic orientation search across a rotation library
- **Vial metric extraction** — per-vial mean, median, std, min and max across all contrast images, written to CSV
- **Plotting** — scatter plots of vial intensity and parametric map plots (T1/IR, T2/TE) with mrview ROI overlays
- **Protocol support** — extensible `protocols` sub-package for phantom- and project-specific workflow configurations
- **End-to-end pipeline** — single command processes a session directory through DWI preprocessing, phantom QC in DWI space, and native contrast QC
- **Format-agnostic input** — each series sub-directory may contain DICOM, NIfTI (`.nii`/`.nii.gz`), or MRtrix MIF (`.mif`/`.mif.gz`) files; format is detected automatically per sub-directory
- **Automatic series classification** — DWI, reverse phase-encode, T1, IR, and TE series are detected and paired automatically from folder names and sidecar metadata; no manual configuration required
- **DWI preprocessing** — FSL `dwifslpreproc` with automatic phase-encoding correction mode selection (`rpe_none`, `rpe_pair`, `rpe_all`), optional denoising/Gibbs correction, tensor fitting, and T1-to-DWI co-registration via FLIRT
- **Template-based registration** — iterative ANTs rigid registration with automatic orientation search across a rotation library; vial masks propagated to subject space via inverse transform
- **Vial metric extraction** — per-vial mean, median, std, min and max across all contrast images (T1, IR, TE, ADC, FA), written to CSV
- **Plotting** — ADC/FA scatter plots with SPIRIT reference values, T1/IR and T2/TE parametric map plots with mrview ROI overlays and Monte Carlo 95% CI bands
- **Checkpoint-based resumption** — re-running the pipeline skips stages whose outputs already exist
- **Parallel batch processing** — pydra-native splitting and combining for multi-session datasets

## Installation
Expand All @@ -23,65 +27,164 @@
python -m pip install phantomkit
```

### External dependencies

The pipeline requires FSL, MRtrix3, ANTs, and dcm2niix to be available on `PATH`:

- [FSL](https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/) — `dwifslpreproc`, `flirt`, `convert_xfm`
- [MRtrix3](https://www.mrtrix.org/) — `mrconvert`, `dwi2tensor`, `tensor2metric`, `dwidenoise`, `mrdegibbs`, `mrstats`, `mrview`
- [ANTs](http://stnava.github.io/ANTs/) — `antsRegistrationSyN.sh`, `antsApplyTransforms`
- [dcm2niix](https://github.com/rordenlab/dcm2niix) — DICOM to NIfTI conversion (required only when input is DICOM)

## Basic usage

### End-to-end pipeline

Point the pipeline at a session directory whose sub-directories contain DICOM, NIfTI, or MIF series. The format is detected automatically per sub-directory — no conversion needed beforehand.

```bash
# SPIRIT phantom (accepts DICOM, NIfTI, or MIF sub-directories)
phantomkit pipeline \
--input-dir /data/session01 \
--output-dir /results/session01 \
--phantom SPIRIT

# 120E phantom (T1 and T2 mapping supported)
phantomkit pipeline \
--input-dir /data/session01 \
--output-dir /results/session01 \
--phantom 120E
```

Supported phantoms and their reference data:

| Phantom | ADC | T1 mapping | T2 mapping |
|---------|-----|------------|------------|
| SPIRIT | ✓ (vials E–L) | ✓ (12 vials) | ✓ (12 vials) |
| 120E | ✓ (all 24 vials) | ✓ (24 vials) | ✓ (24 vials) |

### Series naming conventions

Series sub-directories are classified automatically by **word-boundary token matching** on the folder or file name (case-insensitive). No manual tagging is required.

| Series type | Radiology term | Token match |
|---|---|---|
| Inversion Recovery | IR / MPRAGE | `ir` |
| Multi-Echo Spin Echo | MESE / T2-SE | `te` |
| T1-weighted | T1 / MPRAGE | `t1` or `mprage` |
| DWI | Diffusion-weighted | auto-detected via bvec/bval |
| Reverse phase-encode | B0 field map | paired with DWI |

For IR and MESE series, the **numeric suffix** in the filename is interpreted as the physical parameter: inversion time TI (ms) for IR, and echo time TE (ms) for MESE. Example: `se_ir_500.nii.gz` → TI = 500 ms; `t2_se_TE_80.nii.gz` → TE = 80 ms.

Optional flags:

```
--denoise-degibbs Apply dwidenoise + mrdegibbs before preprocessing
--gradcheck Run dwigradcheck to verify gradient orientations
--nocleanup Keep intermediate tmp/ directories after completion
--readout-time Override TotalReadoutTime (seconds) for dwifslpreproc
--eddy-options Override FSL eddy options string
--dry-run Print the planned workflow without executing
```

Output structure:

```
/results/session01/
<DWI_series>/
DWI_preproc_biascorr.mif.gz
ADC.nii.gz
FA.nii.gz
T1_in_DWI_space.nii.gz
metrics/ ← per-vial CSVs and QA plots
vial_segmentations/
native_contrasts_staging/
metrics/ ← per-vial CSVs and parametric map plots
vial_segmentations/
images_template_space/
```

### Python API

```python
from phantomkit.phantom_processor import PhantomProcessor

processor = PhantomProcessor(
template_dir="/templates/SPIRIT",
output_base_dir="/results",
rotation_library_file="/templates/rotations.txt",
)
results = processor.process_session("/data/session01/T1.nii.gz")
```

Or via the pydra workflow API:

```python
from phantomkit.protocols.gsp_spirit import GspSpiritAnalysis
from phantomkit.analyses.vial_signal import VialSignalAnalysis

wf = GspSpiritAnalysis(
input_image="/data/session01/t1_mprage.nii.gz",
template_dir="/templates/gsp_spirit",
rotation_library_file="/templates/gsp_spirit/rotations.txt",
wf = VialSignalAnalysis(
input_image="/data/session01/T1.nii.gz",
template_dir="/templates/SPIRIT",
rotation_library_file="/templates/rotations.txt",
)
outputs = wf(cache_root="/data/cache-root")
outputs = wf(cache_root="/data/pydra-cache")
```

Or via the command line:
### CLI — run a protocol directly

```bash
# Single session
phantom-process run gsp-spirit /data/session01/t1_mprage.nii.gz \
--template-dir /templates/gsp_spirit \
--rotation-library-file /templates/gsp_spirit/rotations.txt \
--output-base-dir /results

# Batch — process every matching image found under /data/
phantom-process run gsp-spirit /data/ \
--template-dir /templates/gsp_spirit \
--rotation-library-file /templates/gsp_spirit/rotations.txt \
--output-base-dir /results \
--pattern "*t1*mprage*.nii.gz"
phantomkit run vial-signal /data/session01/T1.nii.gz \
--template-dir /templates/SPIRIT \
--rotation-library-file /templates/rotations.txt \
--output-base-dir /results

# Batch mode — process every matching image under /data/
phantomkit run vial-signal /data/ \
--template-dir /templates/SPIRIT \
--rotation-library-file /templates/rotations.txt \
--output-base-dir /results \
--pattern "*T1*.nii.gz"

# List available protocols
phantom-process list
phantomkit list
```

### Plotting

Generate QA plots from existing CSV metric files:

```bash
# Vial intensity scatter plot for one contrast
phantom-process plot vial-intensity \
/results/session01/metrics/session01_t1_mprage_mean_matrix.csv scatter \
--std_csv /results/session01/metrics/session01_t1_mprage_std_matrix.csv \
--output /results/session01/metrics/session01_t1_PLOTmeanstd.png
# Generic scatter plot
phantomkit plot vial-intensity \
/results/session01/metrics/session01_T1_mean_matrix.csv scatter \
--std-csv /results/session01/metrics/session01_T1_std_matrix.csv \
--output /results/session01/metrics/session01_T1_PLOTmeanstd.png

# ADC scatter plot with SPIRIT reference values
phantomkit plot vial-intensity \
/results/session01/metrics/session01_ADC_mean_matrix.csv scatter \
--std-csv /results/session01/metrics/session01_ADC_std_matrix.csv \
--phantom SPIRIT \
--template-dir /templates \
--output /results/session01/metrics/session01_ADC_PLOTmeanstd.png

# T1 inversion-recovery parametric map plot
phantom-process plot maps-ir \
/results/session01/images_template_space/ir_*.nii.gz \
--metric_dir /results/session01/metrics \
phantomkit plot maps-ir \
/results/session01/images_template_space/se_ir_*.nii.gz \
--metric-dir /results/session01/metrics \
--output /results/session01/metrics/session01_T1map_plot.png

# T2 spin-echo parametric map plot
phantom-process plot maps-te \
/results/session01/images_template_space/te_*.nii.gz \
--metric_dir /results/session01/metrics \
phantomkit plot maps-te \
/results/session01/images_template_space/t2_se_*.nii.gz \
--metric-dir /results/session01/metrics \
--output /results/session01/metrics/session01_T2map_plot.png
```

See the [CLI documentation](https://australian-imaging-service.github.io/phantomkit/cli.html) for the full option reference.

## License

Copyright 2026 Australian Imaging Service. Released under the [Apache License 2.0](LICENSE).
Copyright 2026 Australian Imaging Service. Released under the [Apache License 2.0](LICENSE).
80 changes: 80 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
FROM ubuntu:24.04

LABEL org.opencontainers.image.title="phantomkit" \
org.opencontainers.image.description="MRI phantom QA processing pipeline" \
org.opencontainers.image.authors="Arkiev D'Souza <arkiev.dsouza@sydney.edu.au>" \
org.opencontainers.image.source="https://github.com/Australian-Imaging-Service/phantomkit"

ARG DEBIAN_FRONTEND=noninteractive
ARG ANTS_VERSION=2.5.3

# ---------- Base system packages ----------
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl wget unzip \
python3 python3-pip python3-venv \
libgomp1 \
dc bc \
mrtrix3 \
&& rm -rf /var/lib/apt/lists/*

# ---------- Python virtual environment ----------
RUN python3 -m venv /opt/venv
ENV PATH="/opt/venv/bin:${PATH}"

# ---------- FSL via conda environment spec ----------
# Bypasses the FSL installer (which requires sudo for system PATH setup).
# Downloads miniforge to get conda/mamba, creates the FSL env from FSL's
# own published yml spec, then removes miniforge to save space.
# Downloads ~7 GB; includes eddy, topup, flirt, and related tools.
ARG FSL_VERSION=6.0.7.22
RUN curl -fsSL \
"https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-x86_64.sh" \
-o /tmp/miniforge.sh \
&& bash /tmp/miniforge.sh -b -p /opt/conda \
&& rm /tmp/miniforge.sh

RUN /opt/conda/bin/conda env create -p /opt/fsl \
-f "https://fsl.fmrib.ox.ac.uk/fsldownloads/fslconda/releases/fsl-${FSL_VERSION}_linux-64.yml" \
&& /opt/conda/bin/conda clean --all -y \
&& rm -rf /opt/conda

ENV FSLDIR=/opt/fsl \
FSLOUTPUTTYPE=NIFTI_GZ \
FSLMULTIFILEQUIT=TRUE
ENV PATH="${FSLDIR}/bin:${PATH}"
ENV LD_LIBRARY_PATH="/opt/fsl/lib"

# ---------- ANTs pre-compiled binary ----------
RUN curl -fsSL \
"https://github.com/ANTsX/ANTs/releases/download/v${ANTS_VERSION}/ants-${ANTS_VERSION}-ubuntu-22.04-X64-gcc.zip" \
-o /tmp/ants.zip \
&& unzip -q /tmp/ants.zip -d /opt/ \
&& find /opt -maxdepth 3 -name "antsRegistration" 2>/dev/null \
| head -1 | xargs dirname | xargs -I{} ln -s {} /opt/ants_bin \
&& rm /tmp/ants.zip

ENV PATH="/opt/ants_bin:${PATH}"

# ---------- dcm2niix pre-compiled binary ----------
RUN curl -fsSL \
"https://github.com/rordenlab/dcm2niix/releases/latest/download/dcm2niix_lnx.zip" \
-o /tmp/dcm2niix.zip \
&& unzip -q /tmp/dcm2niix.zip -d /tmp/dcm2niix \
&& install -m 755 /tmp/dcm2niix/dcm2niix /usr/local/bin/dcm2niix \
&& rm -rf /tmp/dcm2niix.zip /tmp/dcm2niix

# Polyfill for imp module removed in Python 3.12 (needed by apt MRtrix3 scripts).
# Placed here so edits to the polyfill don't invalidate the expensive FSL layer.
COPY docker/imp_polyfill.py /usr/lib/python3/dist-packages/imp.py

# ---------- phantomkit (editable install preserves template_data lookup) ----------
WORKDIR /opt/phantomkit
COPY . .
RUN /opt/venv/bin/pip install --no-cache-dir -e .

# ---------- Runtime ----------
WORKDIR /data
VOLUME ["/data/input", "/data/output"]

ENTRYPOINT ["phantomkit"]
CMD ["--help"]
Loading
Loading