bifti 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bifti-0.1.0/LICENSE +21 -0
- bifti-0.1.0/PKG-INFO +190 -0
- bifti-0.1.0/README.md +163 -0
- bifti-0.1.0/pyproject.toml +57 -0
- bifti-0.1.0/pyproject.toml.orig +53 -0
- bifti-0.1.0/src/bifti/__init__.py +43 -0
- bifti-0.1.0/src/bifti/_backend.py +110 -0
- bifti-0.1.0/src/bifti/_nifti_write.py +112 -0
- bifti-0.1.0/src/bifti/loader.py +456 -0
- bifti-0.1.0/src/bifti/phantom.py +373 -0
- bifti-0.1.0/src/bifti/py.typed +0 -0
- bifti-0.1.0/src/bifti/registry.py +209 -0
- bifti-0.1.0/src/bifti/resample.py +334 -0
bifti-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MRX
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
bifti-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bifti
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Load, save and resample BIfTI MRI simulation phantoms (JSON + NIfTI)
|
|
5
|
+
Keywords: mri,phantom,simulation,nifti,bifti
|
|
6
|
+
Author: J-Endres
|
|
7
|
+
Author-email: J-Endres <jonathan.endres01@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Dist: nibabel>=5.4.2
|
|
16
|
+
Requires-Dist: numpy>=2.5.1
|
|
17
|
+
Requires-Dist: requests>=2.34.2
|
|
18
|
+
Requires-Dist: torch>=2.13.0 ; extra == 'torch'
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Project-URL: Homepage, https://github.com/mrx-org/bifti-phantoms
|
|
21
|
+
Project-URL: Documentation, https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/README.md
|
|
22
|
+
Project-URL: Repository, https://github.com/mrx-org/bifti-phantoms
|
|
23
|
+
Project-URL: Issues, https://github.com/mrx-org/bifti-phantoms/issues
|
|
24
|
+
Project-URL: Registry, https://mrx-org.github.io/bifti-phantoms/
|
|
25
|
+
Provides-Extra: torch
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# bifti
|
|
29
|
+
|
|
30
|
+
A Python package for the [BIfTI phantom format](https://github.com/mrx-org/bifti-phantoms/blob/main/SPEC.md): parse/serialize
|
|
31
|
+
the phantom JSON, load a phantom into plain NumPy arrays, and fetch phantoms
|
|
32
|
+
from the public [registry](https://github.com/mrx-org/bifti-phantoms/blob/main/REGISTRY.md).
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from bifti import NumpyPhantom
|
|
36
|
+
|
|
37
|
+
phantom = NumpyPhantom.load("subj42-3T.json")
|
|
38
|
+
tissue = phantom.tissues["gm"] # a NumpyTissue: density, T1, T2, ... as np.ndarray
|
|
39
|
+
print(tissue.shape, tissue.T1.mean())
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`phantom.config` is the parsed `BiftiPhantom` (the raw JSON structure: units,
|
|
43
|
+
system, tissue definitions) — see [phantom.py](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/src/bifti/phantom.py) for the
|
|
44
|
+
full data model, and [loader.py](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/src/bifti/loader.py) for how each property
|
|
45
|
+
resolves to an array.
|
|
46
|
+
|
|
47
|
+
## API at a glance
|
|
48
|
+
|
|
49
|
+
Everything below is importable from the top-level `bifti` package (`from
|
|
50
|
+
bifti import ...`):
|
|
51
|
+
|
|
52
|
+
| Name | What it is |
|
|
53
|
+
|------|------------|
|
|
54
|
+
| `NumpyPhantom.load(path)` | Load a phantom JSON + its NIfTIs into `NumpyPhantom(config, tissues)`. |
|
|
55
|
+
| `NumpyTissue` | One tissue as NumPy arrays: `density`, `T1`, `T2`, `T2dash`, `ADC`, `dB0`, `B1_tx`, `B1_rx`, plus `.shape`/`.affine`. |
|
|
56
|
+
| `NumpyPhantom.save(dir, json_name="phantom.json")` | Save a loaded phantom as a self-contained folder (float64 NIfTIs + JSON, no `reslice_to`, no `func`); see [Saving](https://github.com/mrx-org/bifti-phantoms/blob/main/README.md#saving). Returns the JSON path. |
|
|
57
|
+
| `BiftiPhantom.load(path)` / `.save(path)` | Parse/serialize just the JSON side (no NIfTI I/O) — `BiftiPhantom(units, system, tissues, reslice_to, schema)`. |
|
|
58
|
+
| `load_catalog()` | Fetch and parse the public [catalog.json](https://github.com/mrx-org/bifti-phantoms/blob/main/catalog.json) — the discovery list, mapping a label to an immutable registry name. |
|
|
59
|
+
| `load_registry()` | Fetch and parse the public [registry.json](https://github.com/mrx-org/bifti-phantoms/blob/main/registry.json) — the immutable archive of every collection. |
|
|
60
|
+
| `load_registry_phantom(collection, name)` | Download one phantom's JSON + NIfTIs from Zenodo into a local cache; returns the JSON path. `collection` is an immutable registry name. |
|
|
61
|
+
|
|
62
|
+
`NumpyPhantom.load` is what you want for simulation/analysis (arrays); use
|
|
63
|
+
`BiftiPhantom` directly only if you're generating or editing phantom JSONs
|
|
64
|
+
without touching pixel data (e.g. [`examples/make_numerical_brain_cropped_bifti.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/make_numerical_brain_cropped_bifti.py)).
|
|
65
|
+
|
|
66
|
+
## Installation
|
|
67
|
+
|
|
68
|
+
From [PyPI](https://pypi.org/project/bifti/):
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
# using uv (recommended):
|
|
72
|
+
uv add bifti
|
|
73
|
+
# or using pip:
|
|
74
|
+
pip install bifti
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Or the latest unreleased version from git:
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
uv add --git https://github.com/mrx-org/bifti-phantoms --subdirectory python/bifti bifti
|
|
81
|
+
pip install "git+https://github.com/mrx-org/bifti-phantoms.git#subdirectory=python/bifti"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Examples
|
|
85
|
+
|
|
86
|
+
[`examples/`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/) has a small, self-contained demo that **loads a BIfTI
|
|
87
|
+
phantom and plots every tissue's data** — a starting point for seeing the
|
|
88
|
+
package in action.
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
git clone https://github.com/mrx-org/bifti-phantoms
|
|
92
|
+
uv add --path bifti-phantoms/python/bifti bifti
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
| File | Purpose |
|
|
96
|
+
|------|---------|
|
|
97
|
+
| [`examples/demo.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/demo.py) | Load a phantom and plot one figure per tissue. |
|
|
98
|
+
| [`examples/nifti_to_koma.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/nifti_to_koma.py) | Convert a phantom to a KomaMRI `.phantom` HDF5 file. |
|
|
99
|
+
| [`examples/mrzero_sim.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/mrzero_sim.py) | Simulate a Pulseq sequence on a phantom with MR-zero. |
|
|
100
|
+
| [`examples/make_numerical_brain_cropped_bifti.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/make_numerical_brain_cropped_bifti.py) | Port the legacy MRzero test phantom to BIfTI. |
|
|
101
|
+
| [`examples/data/`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/data/) | Phantom JSONs + the NIfTIs they reference. |
|
|
102
|
+
| [`examples/data/generate.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/data/generate.py) | Regenerates the example data in `examples/data/` (reproducible). |
|
|
103
|
+
|
|
104
|
+
### Requirements
|
|
105
|
+
|
|
106
|
+
The core package only needs `bifti`'s own dependencies (numpy, nibabel,
|
|
107
|
+
requests). Resampling uses `torch` when it is importable — on CPU as well as
|
|
108
|
+
CUDA — and NumPy otherwise; install it with the `torch` extra
|
|
109
|
+
(`pip install "bifti[torch]"`) or force a backend with
|
|
110
|
+
`BIFTI_RESAMPLE_BACKEND=numpy|torch`. The examples pull in extra,
|
|
111
|
+
examples-only dependencies
|
|
112
|
+
(`matplotlib`, `h5py`, `MRzeroCore`, `torch`) declared as a
|
|
113
|
+
[dependency group](https://docs.astral.sh/uv/concepts/projects/dependencies/#dependency-groups)
|
|
114
|
+
rather than real package dependencies:
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
uv sync --group examples
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Running
|
|
121
|
+
|
|
122
|
+
From this `python/bifti` directory:
|
|
123
|
+
|
|
124
|
+
```sh
|
|
125
|
+
# list the registry, pick one, download + plot
|
|
126
|
+
uv run --group examples examples/demo.py
|
|
127
|
+
# ...or plot a local phantom JSON directly
|
|
128
|
+
uv run --group examples examples/demo.py examples/data/shapes.json
|
|
129
|
+
uv run --group examples examples/demo.py examples/data/shapes_resliced.json
|
|
130
|
+
uv run --group examples examples/demo.py examples/data/shapes_downsampled.json
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
With no argument, `demo.py` fetches the live [`catalog.json`](https://github.com/mrx-org/bifti-phantoms/blob/main/catalog.json),
|
|
134
|
+
resolves each label to its [`registry.json`](https://github.com/mrx-org/bifti-phantoms/blob/main/registry.json) collection,
|
|
135
|
+
prints their phantoms as a numbered list, and downloads the one you pick (its
|
|
136
|
+
JSON and every NIfTI it references) into `examples/cache/` via `bifti.registry`
|
|
137
|
+
before plotting. Passing a local JSON path skips the catalog and plots that
|
|
138
|
+
file directly (the example data is committed in `examples/data/`).
|
|
139
|
+
|
|
140
|
+
`demo.py` saves one PNG per tissue into `examples/figures/` and, on a GUI
|
|
141
|
+
backend, also shows them. Each figure tiles the tissue's maps — `density`,
|
|
142
|
+
`T1`, `T2`, `T2'`, `ADC`, `dB0`, and every `B1+`/`B1-` channel — at the volume's
|
|
143
|
+
middle slice. Spatially uniform properties show as a flat field; properties
|
|
144
|
+
left at their default (e.g. `T1 = inf`) are blanked out.
|
|
145
|
+
|
|
146
|
+
## The example data
|
|
147
|
+
|
|
148
|
+
The NIfTIs in `examples/data/` are committed, but they are all produced by
|
|
149
|
+
[`examples/data/generate.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/data/generate.py) — re-run it
|
|
150
|
+
(`uv run --group examples examples/data/generate.py`) to regenerate them. It
|
|
151
|
+
derives everything from a fixed seed and simple `meshgrid` functions (smooth
|
|
152
|
+
bumps, low-order polynomials, a little noise), so re-running reproduces
|
|
153
|
+
byte-identical files. The phantoms together exercise the whole format:
|
|
154
|
+
|
|
155
|
+
- **`subj42-3T.json`** (hand-written) — a brain-like single slice. Uses
|
|
156
|
+
`reslice_to`, so all maps are resampled from their native 64×64 grid onto a
|
|
157
|
+
100×100 grid on load. Exercises: scalar properties, a shared `dB0` map, an
|
|
158
|
+
**8-channel `B1+`**, and a transformed reference (`fat.dB0 = "x - 420"`).
|
|
159
|
+
- **`shapes.json`** (generated) — a small 40×32×4 phantom with **no
|
|
160
|
+
`reslice_to`**, so it loads on its native grid. Exercises: multiple tissues
|
|
161
|
+
sharing one grid, a polynomial `dB0` map, a 2-channel `B1+`, a `func` mapping
|
|
162
|
+
(`"x * 0.5 + 10"`), property defaults, and a true 3D volume.
|
|
163
|
+
- **`shapes_resliced.json`** (generated) — the same NIfTIs as `shapes.json` but
|
|
164
|
+
with a `reslice_to` onto a finer grid (40×32×4 → **60×48×4**), so loading
|
|
165
|
+
genuinely resamples the 3D volumes.
|
|
166
|
+
- **`shapes_downsampled.json`** (generated) — the same NIfTIs again, resliced
|
|
167
|
+
onto a *coarser* grid (40×32×4 → **16×12×3**) whose FOV deliberately
|
|
168
|
+
overhangs the source. Exercises the case plain interpolation gets wrong:
|
|
169
|
+
output voxels that average many source voxels, and output voxels that
|
|
170
|
+
straddle the edge of the source FOV.
|
|
171
|
+
|
|
172
|
+
> **Note:** loading a tissue with a `func` mapping prints a warning — the loader
|
|
173
|
+
> evaluates `func` with `eval` for brevity, so only load phantoms you trust (see
|
|
174
|
+
> the note in [`bifti/loader.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/src/bifti/loader.py)).
|
|
175
|
+
|
|
176
|
+
## Known limitations
|
|
177
|
+
|
|
178
|
+
- **Complex-valued NIfTI data isn't handled.** A complex `B1+`/`B1-` map gets
|
|
179
|
+
cast with `np.asarray(..., dtype=np.float64)` in
|
|
180
|
+
[`loader.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/src/bifti/loader.py), which silently discards the imaginary
|
|
181
|
+
part (numpy emits a `ComplexWarning`, easy to miss). The
|
|
182
|
+
[Rust crate](https://github.com/mrx-org/bifti-phantoms/blob/main/rust/bifti/) fails loudly instead in this case.
|
|
183
|
+
- Only the default [`units`](https://github.com/mrx-org/bifti-phantoms/blob/main/JSON.md#units) are accepted.
|
|
184
|
+
|
|
185
|
+
## See also
|
|
186
|
+
|
|
187
|
+
- [../../README.md](https://github.com/mrx-org/bifti-phantoms/blob/main/README.md) — repo overview, format spec, and the
|
|
188
|
+
registry.
|
|
189
|
+
- [../../rust/bifti/](https://github.com/mrx-org/bifti-phantoms/blob/main/rust/bifti/) — the Rust crate, including how it
|
|
190
|
+
differs from this package.
|
bifti-0.1.0/README.md
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# bifti
|
|
2
|
+
|
|
3
|
+
A Python package for the [BIfTI phantom format](https://github.com/mrx-org/bifti-phantoms/blob/main/SPEC.md): parse/serialize
|
|
4
|
+
the phantom JSON, load a phantom into plain NumPy arrays, and fetch phantoms
|
|
5
|
+
from the public [registry](https://github.com/mrx-org/bifti-phantoms/blob/main/REGISTRY.md).
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from bifti import NumpyPhantom
|
|
9
|
+
|
|
10
|
+
phantom = NumpyPhantom.load("subj42-3T.json")
|
|
11
|
+
tissue = phantom.tissues["gm"] # a NumpyTissue: density, T1, T2, ... as np.ndarray
|
|
12
|
+
print(tissue.shape, tissue.T1.mean())
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`phantom.config` is the parsed `BiftiPhantom` (the raw JSON structure: units,
|
|
16
|
+
system, tissue definitions) — see [phantom.py](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/src/bifti/phantom.py) for the
|
|
17
|
+
full data model, and [loader.py](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/src/bifti/loader.py) for how each property
|
|
18
|
+
resolves to an array.
|
|
19
|
+
|
|
20
|
+
## API at a glance
|
|
21
|
+
|
|
22
|
+
Everything below is importable from the top-level `bifti` package (`from
|
|
23
|
+
bifti import ...`):
|
|
24
|
+
|
|
25
|
+
| Name | What it is |
|
|
26
|
+
|------|------------|
|
|
27
|
+
| `NumpyPhantom.load(path)` | Load a phantom JSON + its NIfTIs into `NumpyPhantom(config, tissues)`. |
|
|
28
|
+
| `NumpyTissue` | One tissue as NumPy arrays: `density`, `T1`, `T2`, `T2dash`, `ADC`, `dB0`, `B1_tx`, `B1_rx`, plus `.shape`/`.affine`. |
|
|
29
|
+
| `NumpyPhantom.save(dir, json_name="phantom.json")` | Save a loaded phantom as a self-contained folder (float64 NIfTIs + JSON, no `reslice_to`, no `func`); see [Saving](https://github.com/mrx-org/bifti-phantoms/blob/main/README.md#saving). Returns the JSON path. |
|
|
30
|
+
| `BiftiPhantom.load(path)` / `.save(path)` | Parse/serialize just the JSON side (no NIfTI I/O) — `BiftiPhantom(units, system, tissues, reslice_to, schema)`. |
|
|
31
|
+
| `load_catalog()` | Fetch and parse the public [catalog.json](https://github.com/mrx-org/bifti-phantoms/blob/main/catalog.json) — the discovery list, mapping a label to an immutable registry name. |
|
|
32
|
+
| `load_registry()` | Fetch and parse the public [registry.json](https://github.com/mrx-org/bifti-phantoms/blob/main/registry.json) — the immutable archive of every collection. |
|
|
33
|
+
| `load_registry_phantom(collection, name)` | Download one phantom's JSON + NIfTIs from Zenodo into a local cache; returns the JSON path. `collection` is an immutable registry name. |
|
|
34
|
+
|
|
35
|
+
`NumpyPhantom.load` is what you want for simulation/analysis (arrays); use
|
|
36
|
+
`BiftiPhantom` directly only if you're generating or editing phantom JSONs
|
|
37
|
+
without touching pixel data (e.g. [`examples/make_numerical_brain_cropped_bifti.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/make_numerical_brain_cropped_bifti.py)).
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
From [PyPI](https://pypi.org/project/bifti/):
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
# using uv (recommended):
|
|
45
|
+
uv add bifti
|
|
46
|
+
# or using pip:
|
|
47
|
+
pip install bifti
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or the latest unreleased version from git:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
uv add --git https://github.com/mrx-org/bifti-phantoms --subdirectory python/bifti bifti
|
|
54
|
+
pip install "git+https://github.com/mrx-org/bifti-phantoms.git#subdirectory=python/bifti"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Examples
|
|
58
|
+
|
|
59
|
+
[`examples/`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/) has a small, self-contained demo that **loads a BIfTI
|
|
60
|
+
phantom and plots every tissue's data** — a starting point for seeing the
|
|
61
|
+
package in action.
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
git clone https://github.com/mrx-org/bifti-phantoms
|
|
65
|
+
uv add --path bifti-phantoms/python/bifti bifti
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
| File | Purpose |
|
|
69
|
+
|------|---------|
|
|
70
|
+
| [`examples/demo.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/demo.py) | Load a phantom and plot one figure per tissue. |
|
|
71
|
+
| [`examples/nifti_to_koma.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/nifti_to_koma.py) | Convert a phantom to a KomaMRI `.phantom` HDF5 file. |
|
|
72
|
+
| [`examples/mrzero_sim.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/mrzero_sim.py) | Simulate a Pulseq sequence on a phantom with MR-zero. |
|
|
73
|
+
| [`examples/make_numerical_brain_cropped_bifti.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/make_numerical_brain_cropped_bifti.py) | Port the legacy MRzero test phantom to BIfTI. |
|
|
74
|
+
| [`examples/data/`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/data/) | Phantom JSONs + the NIfTIs they reference. |
|
|
75
|
+
| [`examples/data/generate.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/data/generate.py) | Regenerates the example data in `examples/data/` (reproducible). |
|
|
76
|
+
|
|
77
|
+
### Requirements
|
|
78
|
+
|
|
79
|
+
The core package only needs `bifti`'s own dependencies (numpy, nibabel,
|
|
80
|
+
requests). Resampling uses `torch` when it is importable — on CPU as well as
|
|
81
|
+
CUDA — and NumPy otherwise; install it with the `torch` extra
|
|
82
|
+
(`pip install "bifti[torch]"`) or force a backend with
|
|
83
|
+
`BIFTI_RESAMPLE_BACKEND=numpy|torch`. The examples pull in extra,
|
|
84
|
+
examples-only dependencies
|
|
85
|
+
(`matplotlib`, `h5py`, `MRzeroCore`, `torch`) declared as a
|
|
86
|
+
[dependency group](https://docs.astral.sh/uv/concepts/projects/dependencies/#dependency-groups)
|
|
87
|
+
rather than real package dependencies:
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
uv sync --group examples
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Running
|
|
94
|
+
|
|
95
|
+
From this `python/bifti` directory:
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
# list the registry, pick one, download + plot
|
|
99
|
+
uv run --group examples examples/demo.py
|
|
100
|
+
# ...or plot a local phantom JSON directly
|
|
101
|
+
uv run --group examples examples/demo.py examples/data/shapes.json
|
|
102
|
+
uv run --group examples examples/demo.py examples/data/shapes_resliced.json
|
|
103
|
+
uv run --group examples examples/demo.py examples/data/shapes_downsampled.json
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
With no argument, `demo.py` fetches the live [`catalog.json`](https://github.com/mrx-org/bifti-phantoms/blob/main/catalog.json),
|
|
107
|
+
resolves each label to its [`registry.json`](https://github.com/mrx-org/bifti-phantoms/blob/main/registry.json) collection,
|
|
108
|
+
prints their phantoms as a numbered list, and downloads the one you pick (its
|
|
109
|
+
JSON and every NIfTI it references) into `examples/cache/` via `bifti.registry`
|
|
110
|
+
before plotting. Passing a local JSON path skips the catalog and plots that
|
|
111
|
+
file directly (the example data is committed in `examples/data/`).
|
|
112
|
+
|
|
113
|
+
`demo.py` saves one PNG per tissue into `examples/figures/` and, on a GUI
|
|
114
|
+
backend, also shows them. Each figure tiles the tissue's maps — `density`,
|
|
115
|
+
`T1`, `T2`, `T2'`, `ADC`, `dB0`, and every `B1+`/`B1-` channel — at the volume's
|
|
116
|
+
middle slice. Spatially uniform properties show as a flat field; properties
|
|
117
|
+
left at their default (e.g. `T1 = inf`) are blanked out.
|
|
118
|
+
|
|
119
|
+
## The example data
|
|
120
|
+
|
|
121
|
+
The NIfTIs in `examples/data/` are committed, but they are all produced by
|
|
122
|
+
[`examples/data/generate.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/data/generate.py) — re-run it
|
|
123
|
+
(`uv run --group examples examples/data/generate.py`) to regenerate them. It
|
|
124
|
+
derives everything from a fixed seed and simple `meshgrid` functions (smooth
|
|
125
|
+
bumps, low-order polynomials, a little noise), so re-running reproduces
|
|
126
|
+
byte-identical files. The phantoms together exercise the whole format:
|
|
127
|
+
|
|
128
|
+
- **`subj42-3T.json`** (hand-written) — a brain-like single slice. Uses
|
|
129
|
+
`reslice_to`, so all maps are resampled from their native 64×64 grid onto a
|
|
130
|
+
100×100 grid on load. Exercises: scalar properties, a shared `dB0` map, an
|
|
131
|
+
**8-channel `B1+`**, and a transformed reference (`fat.dB0 = "x - 420"`).
|
|
132
|
+
- **`shapes.json`** (generated) — a small 40×32×4 phantom with **no
|
|
133
|
+
`reslice_to`**, so it loads on its native grid. Exercises: multiple tissues
|
|
134
|
+
sharing one grid, a polynomial `dB0` map, a 2-channel `B1+`, a `func` mapping
|
|
135
|
+
(`"x * 0.5 + 10"`), property defaults, and a true 3D volume.
|
|
136
|
+
- **`shapes_resliced.json`** (generated) — the same NIfTIs as `shapes.json` but
|
|
137
|
+
with a `reslice_to` onto a finer grid (40×32×4 → **60×48×4**), so loading
|
|
138
|
+
genuinely resamples the 3D volumes.
|
|
139
|
+
- **`shapes_downsampled.json`** (generated) — the same NIfTIs again, resliced
|
|
140
|
+
onto a *coarser* grid (40×32×4 → **16×12×3**) whose FOV deliberately
|
|
141
|
+
overhangs the source. Exercises the case plain interpolation gets wrong:
|
|
142
|
+
output voxels that average many source voxels, and output voxels that
|
|
143
|
+
straddle the edge of the source FOV.
|
|
144
|
+
|
|
145
|
+
> **Note:** loading a tissue with a `func` mapping prints a warning — the loader
|
|
146
|
+
> evaluates `func` with `eval` for brevity, so only load phantoms you trust (see
|
|
147
|
+
> the note in [`bifti/loader.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/src/bifti/loader.py)).
|
|
148
|
+
|
|
149
|
+
## Known limitations
|
|
150
|
+
|
|
151
|
+
- **Complex-valued NIfTI data isn't handled.** A complex `B1+`/`B1-` map gets
|
|
152
|
+
cast with `np.asarray(..., dtype=np.float64)` in
|
|
153
|
+
[`loader.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/src/bifti/loader.py), which silently discards the imaginary
|
|
154
|
+
part (numpy emits a `ComplexWarning`, easy to miss). The
|
|
155
|
+
[Rust crate](https://github.com/mrx-org/bifti-phantoms/blob/main/rust/bifti/) fails loudly instead in this case.
|
|
156
|
+
- Only the default [`units`](https://github.com/mrx-org/bifti-phantoms/blob/main/JSON.md#units) are accepted.
|
|
157
|
+
|
|
158
|
+
## See also
|
|
159
|
+
|
|
160
|
+
- [../../README.md](https://github.com/mrx-org/bifti-phantoms/blob/main/README.md) — repo overview, format spec, and the
|
|
161
|
+
registry.
|
|
162
|
+
- [../../rust/bifti/](https://github.com/mrx-org/bifti-phantoms/blob/main/rust/bifti/) — the Rust crate, including how it
|
|
163
|
+
differs from this package.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "bifti"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Load, save and resample BIfTI MRI simulation phantoms (JSON + NIfTI)"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
keywords = [
|
|
9
|
+
"mri",
|
|
10
|
+
"phantom",
|
|
11
|
+
"simulation",
|
|
12
|
+
"nifti",
|
|
13
|
+
"bifti",
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Intended Audience :: Science/Research",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Topic :: Scientific/Engineering :: Medical Science Apps.",
|
|
19
|
+
"Topic :: Scientific/Engineering :: Physics",
|
|
20
|
+
"Typing :: Typed",
|
|
21
|
+
]
|
|
22
|
+
requires-python = ">=3.12"
|
|
23
|
+
dependencies = [
|
|
24
|
+
"nibabel>=5.4.2",
|
|
25
|
+
"numpy>=2.5.1",
|
|
26
|
+
"requests>=2.34.2",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[[project.authors]]
|
|
30
|
+
name = "J-Endres"
|
|
31
|
+
email = "jonathan.endres01@gmail.com"
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/mrx-org/bifti-phantoms"
|
|
35
|
+
Documentation = "https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/README.md"
|
|
36
|
+
Repository = "https://github.com/mrx-org/bifti-phantoms"
|
|
37
|
+
Issues = "https://github.com/mrx-org/bifti-phantoms/issues"
|
|
38
|
+
Registry = "https://mrx-org.github.io/bifti-phantoms/"
|
|
39
|
+
|
|
40
|
+
[project.optional-dependencies]
|
|
41
|
+
torch = ["torch>=2.13.0"]
|
|
42
|
+
|
|
43
|
+
[build-system]
|
|
44
|
+
requires = ["uv_build>=0.11.31,<0.12.0"]
|
|
45
|
+
build-backend = "uv_build"
|
|
46
|
+
|
|
47
|
+
[dependency-groups]
|
|
48
|
+
dev = [
|
|
49
|
+
"pytest>=8.4.2",
|
|
50
|
+
"scipy>=1.18.0",
|
|
51
|
+
]
|
|
52
|
+
examples = [
|
|
53
|
+
"h5py>=3.16.0",
|
|
54
|
+
"matplotlib>=3.11.1",
|
|
55
|
+
"mrzerocore>=1.0.4",
|
|
56
|
+
"torch>=2.13.0",
|
|
57
|
+
]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "bifti"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Load, save and resample BIfTI MRI simulation phantoms (JSON + NIfTI)"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "J-Endres", email = "jonathan.endres01@gmail.com" }
|
|
10
|
+
]
|
|
11
|
+
keywords = ["mri", "phantom", "simulation", "nifti", "bifti"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Intended Audience :: Science/Research",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Topic :: Scientific/Engineering :: Medical Science Apps.",
|
|
16
|
+
"Topic :: Scientific/Engineering :: Physics",
|
|
17
|
+
"Typing :: Typed",
|
|
18
|
+
]
|
|
19
|
+
requires-python = ">=3.12"
|
|
20
|
+
dependencies = [
|
|
21
|
+
"nibabel>=5.4.2",
|
|
22
|
+
"numpy>=2.5.1",
|
|
23
|
+
"requests>=2.34.2",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/mrx-org/bifti-phantoms"
|
|
28
|
+
Documentation = "https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/README.md"
|
|
29
|
+
Repository = "https://github.com/mrx-org/bifti-phantoms"
|
|
30
|
+
Issues = "https://github.com/mrx-org/bifti-phantoms/issues"
|
|
31
|
+
Registry = "https://mrx-org.github.io/bifti-phantoms/"
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
# Resampling uses torch when it is importable and NumPy otherwise; see bifti._backend.
|
|
35
|
+
torch = ["torch>=2.13.0"]
|
|
36
|
+
|
|
37
|
+
[build-system]
|
|
38
|
+
requires = ["uv_build>=0.11.31,<0.12.0"]
|
|
39
|
+
build-backend = "uv_build"
|
|
40
|
+
|
|
41
|
+
[dependency-groups]
|
|
42
|
+
dev = [
|
|
43
|
+
"pytest>=8.4.2",
|
|
44
|
+
# test-only: the reference trilinear implementation the upsample path is checked
|
|
45
|
+
# against. Resampling itself no longer needs scipy.
|
|
46
|
+
"scipy>=1.18.0",
|
|
47
|
+
]
|
|
48
|
+
examples = [
|
|
49
|
+
"h5py>=3.16.0",
|
|
50
|
+
"matplotlib>=3.11.1",
|
|
51
|
+
"mrzerocore>=1.0.4",
|
|
52
|
+
"torch>=2.13.0",
|
|
53
|
+
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from .phantom import (
|
|
2
|
+
BiftiPhantom,
|
|
3
|
+
BiftiTissue,
|
|
4
|
+
NiftiMapping,
|
|
5
|
+
NiftiRef,
|
|
6
|
+
PATIENT_POSITIONS,
|
|
7
|
+
Patient,
|
|
8
|
+
PhantomSystem,
|
|
9
|
+
PhantomUnits,
|
|
10
|
+
ResliceTo,
|
|
11
|
+
patient_to_scanner,
|
|
12
|
+
)
|
|
13
|
+
from .loader import (
|
|
14
|
+
NumpyPhantom,
|
|
15
|
+
NumpyTissue,
|
|
16
|
+
to_scanner_affine,
|
|
17
|
+
)
|
|
18
|
+
from .registry import (
|
|
19
|
+
flatten_phantoms,
|
|
20
|
+
load_catalog,
|
|
21
|
+
load_registry,
|
|
22
|
+
load_registry_phantom,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"BiftiPhantom",
|
|
27
|
+
"BiftiTissue",
|
|
28
|
+
"NiftiMapping",
|
|
29
|
+
"NiftiRef",
|
|
30
|
+
"PATIENT_POSITIONS",
|
|
31
|
+
"Patient",
|
|
32
|
+
"PhantomSystem",
|
|
33
|
+
"PhantomUnits",
|
|
34
|
+
"ResliceTo",
|
|
35
|
+
"patient_to_scanner",
|
|
36
|
+
"NumpyPhantom",
|
|
37
|
+
"NumpyTissue",
|
|
38
|
+
"to_scanner_affine",
|
|
39
|
+
"flatten_phantoms",
|
|
40
|
+
"load_catalog",
|
|
41
|
+
"load_registry",
|
|
42
|
+
"load_registry_phantom",
|
|
43
|
+
]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Array backend for resampling: torch when it is installed, NumPy otherwise.
|
|
2
|
+
|
|
3
|
+
The separable resampling path (see :mod:`bifti.resample`) is three matrix contractions, so
|
|
4
|
+
it maps straight onto whichever array library is available. Torch is used whenever it can be
|
|
5
|
+
imported - *not* only when CUDA is present, since torch is faster than NumPy on CPU too and
|
|
6
|
+
falls back to the same code path either way.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import warnings
|
|
13
|
+
from functools import lru_cache
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Backend:
|
|
19
|
+
"""The handful of array operations resampling needs."""
|
|
20
|
+
|
|
21
|
+
name = "numpy"
|
|
22
|
+
|
|
23
|
+
def asarray(self, x):
|
|
24
|
+
return np.asarray(x, dtype=np.float64)
|
|
25
|
+
|
|
26
|
+
def to_numpy(self, x) -> np.ndarray:
|
|
27
|
+
return np.asarray(x, dtype=np.float64)
|
|
28
|
+
|
|
29
|
+
def tensordot(self, a, b, axes):
|
|
30
|
+
return np.tensordot(a, b, axes=axes)
|
|
31
|
+
|
|
32
|
+
def moveaxis(self, x, source, destination):
|
|
33
|
+
return np.moveaxis(x, source, destination)
|
|
34
|
+
|
|
35
|
+
def transpose(self, x, axes):
|
|
36
|
+
return np.transpose(x, axes)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TorchBackend(Backend):
|
|
40
|
+
"""Torch-backed contractions, on CUDA when available.
|
|
41
|
+
|
|
42
|
+
Uses float64 on CPU so results match the NumPy backend exactly, and float32 on CUDA
|
|
43
|
+
where float64 throughput is heavily penalised. That makes the GPU path accurate to
|
|
44
|
+
roughly 1e-6 relative rather than to machine precision - far below the accuracy of the
|
|
45
|
+
phantom data itself.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
name = "torch"
|
|
49
|
+
|
|
50
|
+
def __init__(self, torch):
|
|
51
|
+
self._torch = torch
|
|
52
|
+
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
53
|
+
self.dtype = torch.float32 if self.device.type == "cuda" else torch.float64
|
|
54
|
+
|
|
55
|
+
def asarray(self, x):
|
|
56
|
+
if self._torch.is_tensor(x):
|
|
57
|
+
return x.to(device=self.device, dtype=self.dtype)
|
|
58
|
+
return self._torch.as_tensor(
|
|
59
|
+
np.ascontiguousarray(x, dtype=np.float64),
|
|
60
|
+
dtype=self.dtype,
|
|
61
|
+
device=self.device,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def to_numpy(self, x) -> np.ndarray:
|
|
65
|
+
if self._torch.is_tensor(x):
|
|
66
|
+
return x.detach().to("cpu", self._torch.float64).numpy()
|
|
67
|
+
return np.asarray(x, dtype=np.float64)
|
|
68
|
+
|
|
69
|
+
def tensordot(self, a, b, axes):
|
|
70
|
+
return self._torch.tensordot(a, b, dims=axes)
|
|
71
|
+
|
|
72
|
+
def moveaxis(self, x, source, destination):
|
|
73
|
+
return self._torch.movedim(x, source, destination)
|
|
74
|
+
|
|
75
|
+
def transpose(self, x, axes):
|
|
76
|
+
return self._torch.permute(x, tuple(int(a) for a in axes))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _load_torch_backend() -> Backend | None:
|
|
80
|
+
try:
|
|
81
|
+
import torch
|
|
82
|
+
except ImportError:
|
|
83
|
+
return None
|
|
84
|
+
try:
|
|
85
|
+
return TorchBackend(torch)
|
|
86
|
+
except Exception as exc: # pragma: no cover - torch present but unusable
|
|
87
|
+
warnings.warn(f"torch is installed but unusable ({exc}); falling back to NumPy")
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@lru_cache(maxsize=None)
|
|
92
|
+
def get_backend(name: str | None = None) -> Backend:
|
|
93
|
+
"""The array backend to resample with.
|
|
94
|
+
|
|
95
|
+
``name`` (or ``$BIFTI_RESAMPLE_BACKEND``) may be ``"auto"`` (the default), ``"numpy"``
|
|
96
|
+
or ``"torch"``. ``"torch"`` raises if torch is missing; ``"auto"`` quietly falls back.
|
|
97
|
+
"""
|
|
98
|
+
name = (name or os.environ.get("BIFTI_RESAMPLE_BACKEND", "auto")).strip().lower()
|
|
99
|
+
if name == "numpy":
|
|
100
|
+
return Backend()
|
|
101
|
+
if name == "torch":
|
|
102
|
+
backend = _load_torch_backend()
|
|
103
|
+
if backend is None:
|
|
104
|
+
raise ImportError("BIFTI_RESAMPLE_BACKEND=torch but torch is not installed")
|
|
105
|
+
return backend
|
|
106
|
+
if name != "auto":
|
|
107
|
+
raise ValueError(
|
|
108
|
+
f"invalid resampling backend {name!r} (use auto, numpy or torch)"
|
|
109
|
+
)
|
|
110
|
+
return _load_torch_backend() or Backend()
|