A small, GitHub-ready tool to bulk upload three NIfTI files per subject (image + vessel label + aneurysm label) into an XNAT Project (as Subject-level resource files).
Optionally, it can also populate basic demographics for each subject (M/F, Hand, YOB in the XNAT UI).
This repository is tailored for the following local layout:
raw_data/images/<subject>.nii.gzraw_data/labels_vessel/<subject>.nii.gzraw_data/labels_aneurysm/<subject>.nii.gz
After upload, the three files are stored without any subfolders under the same subject resource (default: RAW).
- ✅ One-command bulk upload: auto-discover subjects, auto-create subjects, auto-create the subject resource
- ✅ No subfolders on XNAT: files are placed directly under the resource root
- ✅ Avoid name collisions: locally all three files are named
<subject>.nii.gz. On XNAT they are renamed to:image.nii.gzlabels_vessel.nii.gzlabels_aneurysm.nii.gz
- ✅ Optional demographics: randomly generate and set
gender / handedness / yob(shown asM/F,Hand,YOBin the UI) - ✅ Config-driven: XNAT URL, project ID, local paths, overwrite policy, demographics, etc. are controlled in
config.yaml - ✅ Safe-by-default:
config/config.yamlis included in.gitignoreto avoid accidentally committing credentials
Example (Windows):
D:\lfm\data\domain\SHINY\raw_data
├─ images
│ ├─ 147.nii.gz
│ ├─ 149.nii.gz
│ └─ ...
├─ labels_vessel
│ ├─ 147.nii.gz
│ ├─ 149.nii.gz
│ └─ ...
└─ labels_aneurysm
├─ 147.nii.gz
├─ 149.nii.gz
└─ ...
Rule: for the same subject (e.g. 147) the file 147.nii.gz should exist in all three folders.
The subject list is derived from
images/*.nii.gzby default.
Example for subject 147:
- Project:
Aneurysm_ISBI - Subject:
147 - Resource:
RAW(configurable)
Files inside the resource (no subfolders):
image.nii.gz
labels_vessel.nii.gz
labels_aneurysm.nii.gz
Why do we rename the files?
- Because locally all three files are named
147.nii.gz. If we uploaded them “as-is” into the same folder, they would overwrite each other.
- Windows 10/11
- Python 3.9+ (recommended: 3.10 / 3.11)
- Network access to your XNAT instance (e.g.
https://multi-x.com/xnat/)
Assume you place it at:
C:\lfm\code\xnat-shiny-uploader
cd C:\lfm\code\xnat-shiny-uploader
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
pip install -r requirements.txtYou can run once:
python run.pyIf config/config.yaml does not exist, the program will create it from config/config.example.yaml and exit, asking you to edit it.
Open:
config/config.yaml
At minimum, set:
xnat.base_url(example:https://multi-x.com/xnat)xnat.project(example:Aneurysm_ISBI)local_data.root_dir(example:D:\lfm\data\domain\SHINY\raw_data)
Two options:
Option A (simplest): put them in config.yaml (do NOT commit them)
xnat:
alias: "YOUR_ALIAS"
secret: "YOUR_SECRET"Option B (recommended): use environment variables
$env:XNAT_ALIAS="YOUR_ALIAS"
$env:XNAT_SECRET="YOUR_SECRET"…and keep alias: null / secret: null in config.yaml.
From the repo root:
python run.pyOptional: explicitly specify the config path:
python run.py --config .\config\config.yamlpython run.py --dry-run- Windows: run
scripts\windows\run.bat - Linux/WSL/macOS: run
scripts/linux/run.sh(first time:chmod +x scripts/linux/run.sh)
They will create .venv, install dependencies, and then run run.py.
cd /path/to/xnat-shiny-uploader
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
pip install -r requirements.txt
python run.pyIf you are on WSL and your data is on a Windows drive, the path usually looks like:
- Windows:
D:\lfm\data\... - WSL:
/mnt/d/lfm/data/...
Set local_data.root_dir accordingly.
In the XNAT Subjects table:
- M/F maps to
gender - Hand maps to
handedness - YOB maps to
yob(year of birth)
Enable demographics in config/config.yaml:
demographics:
enabled: true
mode: "random" # supported: random
only_missing: true # true = do not overwrite existing values
seed: 20250107 # fixed seed for reproducibility
yob_min: 1940
yob_max: 1980
include_unknown: false
unknown_probability: 0.05
out_csv: "outputs/random_demographics.csv"Then run the same command:
python run.pyA CSV record will be written to outputs/random_demographics.csv (recommended for audit / reproducibility).
Common options in config/config.yaml:
upload.resource: resource name (default:RAW)upload.overwrite: overwrite remote files with the same name (default:true)upload.require_all_files:true: require all 3 files per subject (default)false: upload whatever exists (still requires at least 1 file)
Full explanation: see docs/CONFIG.md.
Examples below use curl.exe (available on Windows).
Prepare AUTH:
$AUTH="$env:XNAT_ALIAS`:$env:XNAT_SECRET"curl.exe -sS -u $AUTH "https://multi-x.com/xnat/data/projects/Aneurysm_ISBI/subjects?format=json"curl.exe -sS -u $AUTH "https://multi-x.com/xnat/data/projects/Aneurysm_ISBI/subjects/147/resources/RAW/files?format=json"You should see image.nii.gz / labels_vessel.nii.gz / labels_aneurysm.nii.gz.
curl.exe -sS -u $AUTH "https://multi-x.com/xnat/data/projects/Aneurysm_ISBI/subjects/147?format=json"See docs/TROUBLESHOOTING.md.
Common issues:
- 401/403: wrong alias/secret or no permission on the project
- 404: wrong
base_url(missing/xnator duplicated/xnat/xnat) - HTML response instead of JSON: you are hitting the UI page, not the REST API
- Do not commit
alias/secretto GitHub. config/config.yamlis ignored by default (.gitignore).- If your secret was ever pasted into logs or chat, rotate/revoke it in XNAT.
.
├─ run.py # entrypoint (reads config/config.yaml by default)
├─ config/
│ ├─ config.example.yaml # template
│ └─ config.yaml # your local config (gitignored)
├─ src/xnat_shiny_uploader/
│ ├─ main.py # pipeline
│ ├─ upload.py # upload logic
│ ├─ demographics.py # demographics logic
│ ├─ xnat_client.py # requests wrapper + retries
│ └─ config.py # YAML config loader
└─ docs/
├─ CONFIG.md
└─ TROUBLESHOOTING.md