iQSM+
August 5, 2026 · View on GitHub
MIA 2024 | arXiv | HuggingFace | deepMRI collection
iQSM+ extends iQSM to handle arbitrary acquisition orientations — not just axial scans. It uses orientation-adaptive latent feature editing (OA-LFE) blocks that learn the encoding of acquisition-orientation vectors and integrate them into the network. iQSM+ accepts single-echo and multi-echo phase + (optional) magnitude inputs.
Jump to: Highlights · Layout · Overview · Quick Start · Docker · DICOM → NIfTI conversion · Web App · Command-Line Interface · Run Demo Examples · Troubleshooting · Citation
Highlights
- Orientation-aware reconstruction — the OA-LFE encoder consumes a B0 direction vector, so non-axial scans (oblique, sagittal, coronal) reconstruct correctly without any re-sampling.
- NIfTI / MAT input — phase + (optional) magnitude as 3D-per-echo files or a single 4D volume;
.nii,.nii.gz,.matv5/v7.3 all supported. - Multi-echo magnitude × TE² weighted combination — runs the network once per echo (mirroring iQSM) and combines the per-echo χ outputs externally; automatic TE²-only fallback when no magnitude is provided.
- Echo Selection / Recombine — after a multi-echo run, uncheck noisy short-TE echoes and recombine in seconds (no re-inference). The per-echo files (
iQSM_plus_e1.nii.gz,iQSM_plus_e2.nii.gz, …) are kept on disk for this. - Browser-based UI — collapsible sections, live progress log, slice slider, orientation-preview panels (last-echo phase / magnitude / mask) for quick alignment checks, shape verification, port auto-fallback, SSH-aware launch.
- Standalone DICOM helper —
dicom_to_nifti.pyconverts raw GRE DICOMs into the NIfTI files the web app and CLI consume. One folder or many (--dicom_dir A B …), any subset of phase / magnitude / real / imaginary; auto-derives phase + magnitude from real + imaginary when both are present (with optional GE slice-direction chopper); computes B0 direction fromImageOrientationPatientfor you. Modality flags (--phase_dir,--mag_dir, …) are a rescue path for mis-tagged DICOMs. - Outputs —
iQSM_plus.nii.gz(final combined χ) plus per-echoiQSM_plus_e<i>.nii.gzfiles for multi-echo runs.
Layout
| File / folder | Purpose |
|---|---|
app.py | Gradio web app for browser-based inference. |
run.py | Command-line driver (per-echo files, 4D NIfTI, converted folder, or YAML config). |
dicom_to_nifti.py | Standalone, self-contained DICOM → NIfTI converter (run once, before app.py / run.py). Byte-identical with the copies in iQSM and DeepRelaxo. |
inference.py | Pure-Python iQSM+ pipeline (single-echo per call; multi-echo combination is in run.py / app.py). |
data_utils.py | NIfTI / MAT loaders and shape utilities. |
models/ | OA-LFE LoT-Unet architecture. |
config.yaml | Example YAML config for run.py --config. |
bet2_utils.py | Automatic brain-mask generation via vendor/bet2/ -- used by run.py / app.py when no mask is supplied. |
vendor/bet2/ | FSL's bet2 binary + its runtime shared libraries, vendored directly (not a full FSL install). Same copy as iQSM and DeepRelaxo. |
Dockerfile | Self-contained image (Python env, checkpoints, bet2 all baked in) -- see Docker. |
Overview

Fig. 1: Orientation-adaptive neural network with OA-LFE blocks.

Fig. 2: Comparison of iQSM, iQSM-Mixed, and iQSM+ at different acquisition orientations.
Quick Start
1. Get the code
Option A — Git
git clone https://github.com/sunhongfu/iQSM_Plus.git
cd iQSM_Plus
Option B — Download ZIP
- Open the GitHub repository page.
- Click Code → Download ZIP.
- Unzip and open a terminal in the folder.
2. Install dependencies
A fresh virtual environment isolates iQSM+'s dependencies and avoids version conflicts. You need Python 3.10 or 3.11.
python --version # check
If Python is missing, install from python.org. On Windows, tick Add Python to PATH during installation.
Create and activate a virtual environment:
macOS / Linux:
python -m venv venv
source venv/bin/activate
Windows:
python -m venv venv
venv\Scripts\activate
Re-activate each new terminal.
Install PyTorch. Go to pytorch.org/get-started/locally, pick your OS / CUDA version, and run the command it gives you. Examples:
# NVIDIA GPU (CUDA 12.4):
pip install torch --index-url https://download.pytorch.org/whl/cu124
# CPU only (works without GPU, slower):
pip install torch --index-url https://download.pytorch.org/whl/cpu
Install the rest:
-
Web app + CLI (recommended):
pip install -r requirements-webapp.txt -
CLI only (lighter, no Gradio / FastAPI / Pydantic / Uvicorn):
pip install -r requirements.txt
3. Download checkpoints (and optionally demo data)
Checkpoints and demo data are excluded from git and hosted on Hugging Face: sunhongfu/iQSM_Plus.
Checkpoints (required, one-time):
python run.py --download-checkpoints
Demo data (optional, see Run Demo Examples):
python run.py --download-demo
Manual download (if behind a firewall) — grab the files from Hugging Face and place them as:
iQSM_Plus/
├── checkpoints/
│ ├── iQSM_plus.pth
│ └── LoTLayer_chi.pth
└── demo/
├── ph_multi_echo.nii.gz
├── mag_multi_echo.nii.gz
├── mask_multi_echo.nii.gz
└── params.json
4. Run
If you're starting from raw DICOMs, do the DICOM → NIfTI conversion step first. Then choose the Web App (recommended) or the Command-Line Interface -- natively, or via Docker if you'd rather not set up a Python environment (also the only way to get automatic bet2 brain extraction on Windows or macOS -- see Troubleshooting).
Docker
Builds a self-contained image (Python env, checkpoints, and bet2 all baked in) that runs the same way on Windows, macOS, or Linux -- Docker Desktop runs a real Linux VM on all three, which is what lets vendor/bet2/'s Linux/x86_64 binary actually execute regardless of host OS (see Troubleshooting).
# Build (from this repo's root; needs network access -- checkpoints are downloaded
# during the build). --platform linux/amd64 is required explicitly on Apple Silicon
# hosts (see the Dockerfile's header comment for why).
docker build --platform linux/amd64 -t iqsmplus .
# Run the web app (default) -- open http://localhost:7860 in your browser
docker run --rm -p 7860:7860 iqsmplus
# With GPU acceleration (needs the NVIDIA Container Toolkit on the host)
docker run --rm --gpus all -p 7860:7860 iqsmplus
# Run the CLI instead, e.g. against a folder of NIfTIs already on the host
docker run --rm -v /path/to/data:/data iqsmplus \
python3 run.py --from_converted /data/converted --output /data/output
CPU-only hosts work too (just slower) -- drop --gpus all entirely, no other changes needed. See the Dockerfile itself for the full annotated build.
DICOM → NIfTI conversion
The web app and CLI consume NIfTI files, not raw DICOMs. dicom_to_nifti.py walks one or more folders, classifies DICOMs by modality, groups slices by EchoTime, computes the B0 direction in image coordinates from ImageOrientationPatient, and emits ready-to-use NIfTIs plus a params.json you can paste values out of.
The script is byte-identical with the copies in iQSM and DeepRelaxo — it's intentionally independent of the downstream pipeline. Pick whichever repo's copy is closest at hand.
Two modes
Normal path — --dicom_dir DIR [DIR …] · auto-classifies by DICOM ImageType, ComplexImageComponent, and the GE private tag (0043, 102f). Use this whenever your DICOM tags are reliable. Phase and magnitude in separate folders? Just pass both:
# Single mixed folder (any subset of phase / mag / real / imag):
python dicom_to_nifti.py --dicom_dir /path/to/dicoms
# Phase and magnitude in separate folders — still auto-classified:
python dicom_to_nifti.py --dicom_dir /path/to/phase /path/to/magnitude
# Real + imaginary in separate folders. Phase and magnitude are derived
# from the complex signal: phase = angle(R+jI), magnitude = |R+jI|.
# When both pairs are in one folder, real + imaginary is preferred:
python dicom_to_nifti.py --dicom_dir /path/to/real /path/to/imaginary
Rescue path — --phase_dir / --mag_dir / --real_dir / --imag_dir · for when tags are wrong, missing, or ambiguous (e.g. ImageType = ORIGINAL/PRIMARY/OTHER with no ComplexImageComponent). Each folder's files go straight into the named bucket — the auto-classifier is bypassed:
# Phase only — useful when phase DICOMs are mis-tagged and --dicom_dir
# would route them to the wrong bucket:
python dicom_to_nifti.py --phase_dir /path/to/phase
# Magnitude only:
python dicom_to_nifti.py --mag_dir /path/to/magnitude
# Phase + magnitude, both forced:
python dicom_to_nifti.py --phase_dir /path/to/phase --mag_dir /path/to/magnitude
# Real + imaginary, both forced (must be paired — the complex signal
# can't be formed from one alone):
python dicom_to_nifti.py --real_dir /path/to/real --imag_dir /path/to/imaginary
The two modes can't be mixed (--dicom_dir together with any rescue flag is rejected). They represent two different mental models — trust the tags vs override the tags.
GE slice-direction chopper (real + imaginary only)
GE 3D-GRE recon inserts an alternating ±1 along the slice direction in image space (a missing fftshift in their pipeline). Magnitude is invariant under this, but phase derived from raw real + imag shows π flips on every other slice — looking like massive wrapping. The script automatically applies the (-1)^z chopper when Manufacturer = GE MEDICAL SYSTEMS. Override:
python dicom_to_nifti.py --dicom_dir /path/to/dicoms --chopper on # always apply
python dicom_to_nifti.py --dicom_dir /path/to/dicoms --chopper off # never apply
--chopper auto (default) only fires for GE. --chopper has no effect when phase + magnitude DICOMs are used directly.
After conversion
You'll see a copy-paste-friendly summary you can paste into the iQSM+ web app form:
─── Acquisition values (paste these into the web app) ───
Echo Times (ms) : 3.2, 6.5, 9.8, 13.1, 16.4, 19.7, 23.1, 26.4
Voxel size (mm) : 1 1 1
B0 (Tesla) : 3.0
B0 direction : 0 0 1
─────────────────────────────────────────────────────────
The output folder will contain (names depend on echo count, and on which modalities were present):
converted/
├── dcm_converted_phase[_4d].nii.gz # 3D for single-echo, 4D for multi-echo
├── dcm_converted_magnitude[_4d].nii.gz # written if magnitude was present / derived
└── params.json
params.json carries machine-readable values (te_ms, voxel_size_mm, b0_T, b0_direction) and copy-paste strings (te_ms_string, voxel_size_string, b0_direction_string) formatted exactly the way the web app's input fields expect them. The b0_direction value is especially useful for iQSM+ — for non-axial acquisitions it's what makes the network orientation-aware. Open the JSON, copy the relevant string, paste into the form. Or skip the form altogether and use the CLI's --from_converted flag (auto-fills everything from the same JSON).
--out_diris overwritten in place on each run. A single consolidatedparams.jsonis written (not per-NIfTI sidecars — phase and magnitude share their metadata). Ifparams.jsonor anydcm_converted_*.nii(.gz)already exists in--out_dir, the script lists them and prints a clear warning before overwriting. Use a fresh--out_dirper subject / acquisition.
python dicom_to_nifti.py --help lists all flags. The output folder feeds directly into:
- the web app (drop the NIfTIs into the upload buttons; paste the copy-paste strings from
params.jsoninto the form), or - the CLI (
run.py --from_converted ./convertedreadsparams.jsonautomatically — no retyping).
Web App
python app.py
The app picks port 7860 by default; if it's busy it falls back automatically (7861, 7862, …). Your default browser opens once the server is ready.
Usage walk-through
1. Phase + Magnitude Input
Two side-by-side upload buttons:
- Add Phase NIfTI / MAT (left, required)
- Add Magnitude NIfTI / MAT (optional) (right)
Each accepts single-echo (one 3D file) or multi-echo input (multiple 3D files — one per echo — or a single 4D volume). Supported: .nii, .nii.gz, .mat (v5 or v7.3).
Phase is required. Magnitude is optional — used for magnitude × TE² weighted averaging on multi-echo input; without it, multi-echo falls back to TE²-only weighting (uniform magnitude). Single-echo runs don't combine anything, so magnitude only shows up in the orientation-preview panel after the run. The combiner runs externally (in run.py / app.py), so per-echo iQSM+ outputs are kept on disk and the web app's Echo Selection panel can recombine subsets without re-running inference.
Have raw DICOMs? See DICOM → NIfTI conversion.
2. Processing Order
Two parallel lists (Phase left, Magnitude right) showing every uploaded file in natural numeric order (mag1, mag2, …, mag10). Each list shows a shape summary so mismatches are obvious before you run, plus an explicit "✕ Remove all …" button per modality. When both modalities are supplied, the two columns must have the same echo count.
3. Echo Times (ms)
A single textbox accepts two equivalent formats:
- Comma-separated values (any spacing) —
2.4, 3.6, 9.2, 20.8 - Compact
first_TE : spacing : count—4.5 : 5.0 : 5expands to4.5, 9.5, 14.5, 19.5, 24.5
Voxel size is auto-filled from the first NIfTI's header on upload (overridable below).
4. Brain Mask (optional but recommended)
This section is open by default. A brain mask improves iQSM+ reconstruction quality by concentrating the network on tissue voxels. If you don't upload one and magnitude is provided, one is generated automatically via bet2 (FSL's Brain Extraction Tool, vendored at vendor/bet2/) when you click Run — uncheck "Auto brain-extract via bet2 if no mask is uploaded" to skip that and reconstruct whole-head instead.
Default mask erosion is 3 voxels (applies to any mask used, uploaded or auto-generated); adjust under Acquisition & Hyper-parameters below if you'd rather keep more cortical brain region.
⚠️ Make sure an uploaded mask is oriented and aligned to the phase / magnitude volumes. After the run finishes you can confirm in the Visualisation panel — the brain-mask preview shares the same slice slider as the phase / magnitude previews.
5. Acquisition & Hyper-parameters (collapsed by default)
| Field | Notes |
|---|---|
| Voxel size (mm) | Overrides NIfTI header. Auto-filled on upload. |
| B0 (Tesla) | Defaults to 3.0. |
| B0 direction (unit vector) | Auto-filled from params.json when using a converted DICOM folder; for hand-prepared NIfTIs, leave blank for axial scans (defaults to [0, 0, 1]). For oblique / sagittal / coronal scans, enter the unit vector — this is what makes iQSM+ orientation-aware. |
| Mask erosion radius (voxels) | Defaults to 3; applies to any mask used (uploaded or bet2-auto-generated). No effect on whole-head runs (no mask at all). |
| Reverse phase sign | Enable if iron-rich deep grey matter appears dark (rather than bright) in the QSM output. |
6. Run Reconstruction
Click the green Run Reconstruction button. Below it you'll see, in order:
- Log — streaming console output, including a RUN CONFIGURATION block that prints the equivalent CLI invocation so you can reproduce the run from a terminal.
- Visualisation — middle-slice grayscale preview of
iQSM_plus.nii.gzwith a Z-slice slider and editable display window (± 0.2 ppm). Below it, three orientation-preview panels show the last-echo raw phase, last-echo raw magnitude, and brain mask (auto-windowed) sharing the same slice slider so you can verify alignment. - Echo Selection (refine combination) — visible after every multi-echo run; disabled (greyed out) for single-echo runs. Uncheck the echoes you want to exclude (early echoes with short TEs may produce artifacts) and click 🔁 Recombine selected echoes. The per-echo
iQSM_plus_e<i>.nii.gzfiles are reused, so this is fast (no re-inference). The recombined file uses a versioned name (iQSM_plus_recombined_e2_e3_e4.nii.gz) so the original all-echoes combination stays available. - Results — every produced file listed for download. For multi-echo:
iQSM_plus.nii.gz(combined) andiQSM_plus_e1.nii.gz,iQSM_plus_e2.nii.gz, … (per echo). For single-echo: justiQSM_plus.nii.gz. Click a file size to download a single file, or click 📦 Download all (ZIP) at the bottom for the whole bundle. Each Echo-Selection recombine refreshes the bundle.
GPU memory is released between runs, so you can upload a new dataset and re-run without restarting the page.
The web app accepts files up to 5 GB (max_file_size="5gb" on launch).
Running over SSH
The launch script detects SSH_CONNECTION and skips the auto-open browser step (which would only try to launch a browser on the remote box). It prints the URL and a port-forward hint instead:
Running over SSH — auto-open skipped.
Open this URL in your local browser:
http://127.0.0.1:7860/
If the host isn't reachable from your laptop, forward the port:
ssh -L 7860:127.0.0.1:7860 <user>@<host>
Command-Line Interface
run.py can be driven via flags, a YAML config, or the converted-folder shortcut. It always validates inputs and prints an Equivalent command-line invocation line so a web-app run can be reproduced verbatim from a terminal.
One-step from a converted DICOM folder
After DICOM conversion:
python run.py --from_converted ./converted --mask BET_mask.nii
--from_converted reads phase.nii.gz, magnitude.nii.gz, and params.json (TEs, voxel size, B0, and B0 direction) automatically — no retyping.
Multiple 3D phase NIfTI echoes
(stacked into a 4D volume internally; iQSM+ takes 4D phase natively):
python run.py \
--data_dir Data/your_subject \
--echo_files ph1.nii ph2.nii ph3.nii \
--te_ms 4 8 12 \
--mag mag_4d.nii.gz \
--mask BET_mask.nii
Single 4D phase NIfTI
python run.py \
--echo_4d phase_4d.nii.gz \
--te_ms 3.2 6.5 9.8 13.1 16.4 19.7 23.1 26.4 \
--mag mag_multi_echo.nii.gz \
--mask mask_multi_echo.nii.gz
Non-axial acquisition
Provide B0 direction in image coordinates:
python run.py --echo_4d phase_4d.nii.gz --te_ms 4 8 12 \
--mag mag.nii.gz --b0-dir 0.1 0.0 0.995
Single-echo (3D phase, TE in seconds)
python run.py --phase ph.nii.gz --te 0.020 --mag mag.nii.gz --mask mask.nii.gz
MATLAB inputs
python run.py \
--data_dir Data/your_subject \
--echo_files ph1.mat ph2.mat ph3.mat \
--te_ms 4 8 12 \
--mag mag.mat \
--mask BET_mask.mat
.mat files (v5 or v7.3) must contain a single numeric array per file.
YAML config
python run.py --config config.yaml
Example config.yaml:
data_dir: demo
echo_4d: ph_multi_echo.nii.gz
te_ms: [3.2, 6.5, 9.8, 13.1, 16.4, 19.7, 23.1, 26.4]
mag: mag_multi_echo.nii.gz
mask: mask_multi_echo.nii.gz
b0: 3.0
b0_dir: null # null → read from params.json or [0, 0, 1]
eroded_rad: 3
output: ./iqsm_plus_output
Arguments at a glance
- Input (mutually exclusive):
--from_converted,--echo_files,--echo_4d,--phase. - Echo times:
--te_ms(preferred, milliseconds) or--te(seconds, legacy). - Required: a phase-input flag (above).
- Optional:
--mag(omit → uniform magnitude / TE²-only weighting; also needed for automatic bet2 brain extraction, below),--mask(or--bet_mask) — if omitted and magnitude is provided, a mask is generated automatically via bet2 (--no_bet2to reconstruct whole-head instead),--voxel-size,--b0,--b0-dir,--eroded-rad,--reverse-phase-sign. - Output:
--output(default./iqsm_plus_output). - Setup:
--download-checkpoints,--download-demo. --data_diris optional (defaults to current working directory) — relative input paths are resolved against it.
python run.py --help lists everything.
Run Demo Examples
After python run.py --download-demo, try iQSM+ in any of the following ways. The bundled demo is an 8-echo GRE acquisition (3 T, 1×1×1 mm isotropic).
Option 1 — Web app
python app.py
Drop the demo files into the upload buttons (Phase + Magnitude), paste the TEs into Echo Times, add the mask. Hit Run Reconstruction.
Option 2 — Command line (multi-echo)
python run.py \
--echo_4d demo/ph_multi_echo.nii.gz \
--te_ms 3.2 6.5 9.8 13.1 16.4 19.7 23.1 26.4 \
--mag demo/mag_multi_echo.nii.gz \
--mask demo/mask_multi_echo.nii.gz
Option 3 — YAML config
python run.py --config config.yaml
All three options produce the same output: iQSM_plus.nii.gz (susceptibility, ppm). View in FSLeyes, ITK-SNAP, or 3D Slicer.
Troubleshooting
- Web app upload fails with "Connection errored out / Load failed" — usually a stale browser tab from before the server was restarted. Close all tabs from earlier sessions and open a fresh one.
- SSH "channel N: open failed: connect failed: Connection refused" — comes from your local SSH client, not Gradio. Browser is hitting the forwarded port before Gradio has bound it (race during startup), or a stale tab is polling. After
* Running on local URLprints, refresh once. To silence the noise add-q -o LogLevel=ERRORto yoursshcommand. - Phase from real + imaginary looks "wrapped" every other slice — that's the GE FFT-shift quirk. Re-run conversion with
--chopper on. - Iron-rich deep grey matter appears dark in the QSM output — flip the phase sign convention with
--reverse-phase-sign 1(CLI) or the Reverse phase sign checkbox (web app). - Non-axial acquisition produces unrealistic susceptibility values — make sure B0 direction is set. Use
--from_converted(auto), or pass--b0-dir x y z(CLI), or fill the B0 direction field (web app). - Checkpoint download fails behind a firewall — manually grab the two
.pthfiles from Hugging Face and drop them incheckpoints/. - "bet2 not found" warning, no automatic brain mask — bet2 is a Linux/x86_64 binary (
vendor/bet2/bin/bet2); it won't run natively on Windows or macOS. Reconstruction still completes without a mask (whole-head) rather than failing, but for automatic masking on those platforms, use Docker instead of a native install (Docker Desktop runs a real Linux VM on both Windows and macOS, which is what lets the vendored binary execute), or setBET2_DIRto point at your own FSL/WSL install.
Citation
@article{iqsmplus2024,
title={Plug-and-Play Latent Feature Editing for Orientation-Adaptive Quantitative Susceptibility Mapping Neural Networks},
journal={Medical Image Analysis},
year={2024},
doi={10.1016/j.media.2024.103160}
}