A five-step pipeline for measuring cerebrospinal fluid (CSF) velocity from phase-contrast fMRI data, with a graphical interface modelled on the Pseudotime Pipeline.
| Step | Script | What it does |
|---|---|---|
| 1 | step01_extract_csf.py |
Extracts mean BOLD signal within a CSF ROI using fslmeants. Converts .mgz masks automatically. Computes the 15th-percentile no-flow baseline and percent signal change. Saves a stats CSV and time-series plot. |
| 2 | step02_velocity_calc.py |
Extracts phase signals from the CSF ROI (p1) and a matched static control ROI (p2). Computes velocity = (p1 − p2) × (VENC / Smax), where Smax is derived from the full phase image via fslstats. Saves combined time series, velocity CSV, and a 3-panel figure. |
| 3 | step03_resp_annotate.py |
Loads a per-sequence .mat from the Pseudotime pipeline. Detects inspiration peaks and expiration troughs in the respiratory channel, computes a continuous phase signal (0 → 2π), and bins the CSF velocity signal by respiratory phase — one row per breath cycle. |
| 4 | step04_velocity_amplitude.py |
Computes per-breath-cycle velocity amplitude (max − min across phase bins). Single-run mode: histogram + mean breath-cycle shape. Multi-run mode: dot + errorbar group plot (Python equivalent of mean_amps_dot_plot_velocity.m). Saves per-cycle and summary CSVs. |
| 5 | step05_qc_plot.py |
Auto-discovers all Step 1–3 outputs and produces a multi-panel timeline figure with a shared time axis (BOLD signal, PSC, phase signals p1/p2, velocity, respiratory signal with peaks/troughs, respiratory phase). Computes BOLD SNR, motion outliers, velocity amplitude, p1/p2 ratio, breath rate, and velocity–respiration correlation with pass/warn/fail thresholds. |
Run all steps through the graphical interface:
bash gui/run.sh Neuroimaging # replace with your conda environment name# 1. Clone
git clone https://github.com/mariomjr1/csf_velocity.git
cd csf_velocity
# 2. Install Python dependencies (inside your neuroimaging conda env)
pip install -r gui/requirements.txt
# 3. Launch the GUI
bash gui/run.sh NeuroimagingInside the GUI:
- Use the Quick Setup banner to browse to your subject data folder — BOLD, phase, magnitude niftis, and ROI masks are auto-detected and all fields are pre-filled.
- Run Step 1 → Step 2 → Step 3 → Step 4 → Step 5 (QC) in order.
- Each tab shows key metrics and an inline plot after the step completes.
See documentation/gui.md for a full walkthrough.
| Input | Used in | Notes |
|---|---|---|
BOLD fMRI (.nii.gz) |
Step 1 | Standard BOLD sequence; TR auto-read from JSON sidecar |
CSF ROI mask (.nii.gz / .mgz) |
Steps 1, 2 | Binary mask over the 4th ventricle / aqueduct; .mgz is converted automatically via mri_convert |
Static ROI mask (.nii.gz / .mgz) |
Step 2 | Control voxels adjacent to but separate from the CSF region (typically directly above); .mgz converted automatically |
Phase nifti — P (.nii.gz) |
Step 2 | Phase-contrast acquisition |
Magnitude nifti — M (.nii.gz) |
Step 2 | Accepted for completeness; velocity calculation uses phase only |
Parsed .mat from Pseudotime |
Step 3 | Per-sequence file containing RESP, RPIEZO, STIMTRIG, MRTRIG at 1000 Hz |
Annotated .mat from Step 3 |
Step 4 | Must contain binned_signal (Step 3 only writes this when a velocity txt is also provided) |
velocity (cm/s) = (p1 − p2) × (VENC / Smax)
| Symbol | Meaning |
|---|---|
| p1 | Mean phase signal in the CSF ROI (4th ventricle / aqueduct) |
| p2 | Mean phase signal in the static control ROI (stationary tissue, selected vertically above the CSF region) |
| VENC | Velocity-encoding parameter (cm/s); check your scan protocol — default 10 cm/s |
| Smax | Maximum value of the full raw phase image — computed automatically via fslstats -t -R; can be overridden in the GUI |
Important: Smax must come from the entire phase volume, not just the ROI voxels. Using the ROI-local maximum underestimates Smax and inflates the computed velocity. For most Siemens scanners the true Smax is ~2048 or ~4096.
The 15th percentile of the velocity time course is used as a no-flow baseline to calculate percent signal change.
A negative velocity is physically meaningful — it indicates CSF flow in the direction opposite to the velocity-encoding gradient. If the signal is consistently negative with no oscillation, check that the CSF and Static ROI masks are not swapped.
Step 3 consumes the .mat files produced by Pseudotime Step 3 (3_parse.py). Those files contain the physiological channels (RESP, RPIEZO) for one MRI sequence at 1000 Hz. The CSF Velocity pipeline uses the respiratory channel to:
- Detect each breath cycle (peaks = inspiration, troughs = expiration)
- Compute a continuous respiratory phase signal
- Bin the velocity time series by phase within each cycle →
binned_signalmatrix (n_cycles × n_bins)
This binned_signal is what Step 4 uses to compute per-cycle velocity amplitude and produce the group-level summary plot.
csf_velocity/
├── step01_extract_csf.py ← Step 1 — CSF time series + baseline
├── step02_velocity_calc.py ← Step 2 — phase-contrast velocity
├── step03_resp_annotate.py ← Step 3 — respiratory annotation + binning
├── step04_velocity_amplitude.py ← Step 4 — amplitude analysis + group plot
├── step05_qc_plot.py ← Step 5 — QC timeline figure + metrics
├── mean_amps_dot_plot_velocity.m ← Original MATLAB reference script (Step 4 equivalent)
├── step01_cortex_to_csf.sh ← Original shell wrapper (Step 1 reference)
├── step02_csf_velocity.sh ← Original notes (Step 2 reference)
├── gui/
│ ├── app.py ← 5-tab GUI window
│ ├── runner.py ← Thread-safe subprocess runner
│ ├── run.sh ← Launcher script
│ └── requirements.txt
└── documentation/
├── concepts.md ← CSF velocity, phase contrast MRI, respiratory phase
├── steps.md ← Detailed per-step documentation
└── gui.md ← GUI walkthrough
data/and all pipeline output folders are excluded from version control (see.gitignore).
| Package | Purpose |
|---|---|
numpy |
Array operations |
scipy |
Signal filtering, peak detection, .mat I/O |
matplotlib |
All plots |
Pillow |
Inline plot display in the GUI (optional but recommended) |
FSL (fslmeants, fslstats) |
Time-series extraction and image statistics (must be on PATH) |
FreeSurfer (mri_convert) |
.mgz → NIfTI conversion (required only if masks are in .mgz format) |
Install Python packages:
pip install -r gui/requirements.txtFSL installation: https://fsl.fmrib.ox.ac.uk/fsl/docs/#/install/index
| Symptom | Likely cause | Fix |
|---|---|---|
| Velocity values seem 4–8× too large | Smax computed from ROI voxels only | Leave Smax blank — the pipeline now uses fslstats on the full image. Run fslstats -t -R phase.nii.gz to verify. |
| Velocity is consistently negative | ROI masks swapped, or encoding gradient polarity | Swap the CSF and Static ROI inputs in Step 2 |
fslmeants fails on .mgz mask |
FreeSurfer mask not in NIfTI format | Steps 1 & 2 now call mri_convert automatically |
| No peaks detected in Step 3 | Sequence too short (<60 s) or wrong channel | Use FreeBreath / PaceBreath runs (≥5 min); try RPIEZO if RESP fails |
binned_signal not found in Step 4 |
Step 3 ran without a velocity txt | Re-run Step 3 with the velocity txt from Step 2 |
| File | Contents |
|---|---|
| documentation/concepts.md | Phase-contrast MRI, CSF velocity formula, respiratory phase, binning |
| documentation/steps.md | Every script's inputs, outputs, and parameters explained |
| documentation/gui.md | GUI walkthrough — every field, button, and plot described |