warpkit 1.2.0__tar.gz → 1.2.2__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.
- {warpkit-1.2.0 → warpkit-1.2.2}/.github/workflows/build.yml +2 -1
- {warpkit-1.2.0 → warpkit-1.2.2}/CLAUDE.md +10 -7
- {warpkit-1.2.0 → warpkit-1.2.2}/LICENSE +4 -1
- {warpkit-1.2.0 → warpkit-1.2.2}/PKG-INFO +15 -4
- {warpkit-1.2.0 → warpkit-1.2.2}/README.md +10 -2
- warpkit-1.2.2/codecov.yml +10 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/include/romeo/voxel_quality.h +10 -37
- {warpkit-1.2.0 → warpkit-1.2.2}/include/romeo/weights.h +49 -75
- {warpkit-1.2.0 → warpkit-1.2.2}/include/warps.h +24 -12
- {warpkit-1.2.0 → warpkit-1.2.2}/packaging/pyinstaller/build_bundle.py +15 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/packaging/pyinstaller/bundle_README.md +6 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/pyproject.toml +9 -4
- {warpkit-1.2.0 → warpkit-1.2.2}/src/warpkit.cpp +1 -1
- {warpkit-1.2.0 → warpkit-1.2.2}/tests/test_scripts.py +86 -3
- warpkit-1.2.2/warpkit/api.py +48 -0
- warpkit-1.2.2/warpkit/scripts/_metadata.py +162 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/warpkit/scripts/_warp_io.py +26 -18
- {warpkit-1.2.0 → warpkit-1.2.2}/warpkit/scripts/apply_warp.py +155 -87
- {warpkit-1.2.0 → warpkit-1.2.2}/warpkit/scripts/compute_fieldmap.py +131 -85
- {warpkit-1.2.0 → warpkit-1.2.2}/warpkit/scripts/compute_jacobian.py +85 -29
- {warpkit-1.2.0 → warpkit-1.2.2}/warpkit/scripts/convert_fieldmap.py +157 -78
- {warpkit-1.2.0 → warpkit-1.2.2}/warpkit/scripts/convert_warp.py +134 -78
- warpkit-1.2.2/warpkit/scripts/medic.py +225 -0
- warpkit-1.2.2/warpkit/scripts/unwrap_phase.py +178 -0
- warpkit-1.2.0/compose.yml +0 -5
- warpkit-1.2.0/warpkit/scripts/medic.py +0 -171
- warpkit-1.2.0/warpkit/scripts/unwrap_phase.py +0 -152
- {warpkit-1.2.0 → warpkit-1.2.2}/.gitignore +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/.pre-commit-config.yaml +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/.python-version +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/.vscode/c_cpp_properties.json +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/CMakeLists.txt +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/Dockerfile +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/codecov +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/codecov.SHA256SUM +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/codecov.SHA256SUM.sig +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/include/itk/itkModifiedInvertDisplacementFieldImageFilter.h +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/include/itk/itkModifiedInvertDisplacementFieldImageFilter.hxx +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/include/romeo/LICENSE +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/include/romeo/README.md +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/include/romeo/algorithm.h +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/include/romeo/priority_queue.h +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/include/romeo/romeo.h +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/include/romeo/seed.h +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/include/romeo/unwrap.h +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/include/romeo/utility.h +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/include/romeo/volume_view.h +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/include/utilities.h +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/notes/ROMEO_port_plan.md +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/notes/fmap.png +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/notes/phase.png +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/packaging/pyinstaller/hooks/hook-warpkit.py +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/packaging/pyinstaller/launchers/wk-apply-warp.py +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/packaging/pyinstaller/launchers/wk-compute-fieldmap.py +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/packaging/pyinstaller/launchers/wk-compute-jacobian.py +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/packaging/pyinstaller/launchers/wk-convert-fieldmap.py +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/packaging/pyinstaller/launchers/wk-convert-warp.py +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/packaging/pyinstaller/launchers/wk-medic.py +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/packaging/pyinstaller/launchers/wk-unwrap-phase.py +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/packaging/pyinstaller/warpkit.spec +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/scripts/check-gitmoji.sh +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/scripts/regen-stub.sh +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/tests/conftest.py +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/tests/data/romeo/Mag.nii +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/tests/data/romeo/Phase.nii +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/tests/data/test_data/sub-a01_task-rest_acq-tr1800_echo-1_part-mag_bold.json +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/tests/data/test_data/sub-a01_task-rest_acq-tr1800_echo-1_part-mag_bold.nii.gz +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/tests/data/test_data/sub-a01_task-rest_acq-tr1800_echo-1_part-phase_bold.json +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/tests/data/test_data/sub-a01_task-rest_acq-tr1800_echo-1_part-phase_bold.nii.gz +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/tests/data/test_data/sub-a01_task-rest_acq-tr1800_echo-2_part-mag_bold.json +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/tests/data/test_data/sub-a01_task-rest_acq-tr1800_echo-2_part-mag_bold.nii.gz +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/tests/data/test_data/sub-a01_task-rest_acq-tr1800_echo-2_part-phase_bold.json +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/tests/data/test_data/sub-a01_task-rest_acq-tr1800_echo-2_part-phase_bold.nii.gz +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/tests/data/test_data/sub-a01_task-rest_acq-tr1800_echo-3_part-mag_bold.json +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/tests/data/test_data/sub-a01_task-rest_acq-tr1800_echo-3_part-mag_bold.nii.gz +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/tests/data/test_data/sub-a01_task-rest_acq-tr1800_echo-3_part-phase_bold.json +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/tests/data/test_data/sub-a01_task-rest_acq-tr1800_echo-3_part-phase_bold.nii.gz +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/tests/test_concurrency.py +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/tests/test_distortion.py +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/tests/test_model.py +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/tests/test_romeo.py +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/tests/test_unwrap.py +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/tests/test_utilities.py +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/tests/test_version.py +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/uv.lock +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/warpkit/__init__.py +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/warpkit/concurrency.py +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/warpkit/distortion.py +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/warpkit/model.py +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/warpkit/py.typed +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/warpkit/scripts/__init__.py +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/warpkit/unwrap.py +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/warpkit/utilities.py +0 -0
- {warpkit-1.2.0 → warpkit-1.2.2}/warpkit/warpkit_cpp.pyi +0 -0
|
@@ -41,7 +41,8 @@ jobs:
|
|
|
41
41
|
{version: '3.11', glob: cp311*},
|
|
42
42
|
{version: '3.12', glob: cp312*},
|
|
43
43
|
{version: '3.13', glob: cp313*},
|
|
44
|
-
{version: '3.14', glob: cp314
|
|
44
|
+
{version: '3.14', glob: cp314-*},
|
|
45
|
+
{version: '3.14t', glob: cp314t*}
|
|
45
46
|
]
|
|
46
47
|
name: Python ${{ matrix.python-versions.version }} wheel on ${{ matrix.os }}
|
|
47
48
|
runs-on: ${{ matrix.os }}
|
|
@@ -10,7 +10,7 @@ DIstortion Correction). Phase unwrapping uses a self-contained C++17 port of
|
|
|
10
10
|
**no Julia runtime is involved**; do not reintroduce one (no `julia.py`, no
|
|
11
11
|
conda recipe, no `FindJulia.cmake`).
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Paper (Imaging Neuroscience): <https://doi.org/10.1162/IMAG.a.1262>.
|
|
14
14
|
|
|
15
15
|
## Layout
|
|
16
16
|
|
|
@@ -131,12 +131,15 @@ and call out anything CI-relevant (wheel matrix, pybind11 ABI, ITK).
|
|
|
131
131
|
|
|
132
132
|
## CI specifics
|
|
133
133
|
|
|
134
|
-
GitHub Actions builds wheels for Python 3.11–3.14 on `ubuntu-latest`,
|
|
134
|
+
GitHub Actions builds wheels for Python 3.11–3.14 (both standard and free-threaded) on `ubuntu-latest`,
|
|
135
135
|
`ubuntu-24.04-arm`, and `macos-latest` via cibuildwheel.
|
|
136
|
-
`pyproject.toml`'s `[tool.cibuildwheel]` skips `*musllinux*`
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
136
|
+
`pyproject.toml`'s `[tool.cibuildwheel]` skips only `*musllinux*` builds.
|
|
137
|
+
Free-threaded support is enabled via `py::mod_gil_not_used()` in
|
|
138
|
+
`src/warpkit.cpp`. `PyErr_CheckSignals()` calls in `include/warps.h` are
|
|
139
|
+
wrapped in a `WARPKIT_CHECK_SIGNALS()` macro that compiles to a no-op under
|
|
140
|
+
`Py_GIL_DISABLED`, so GIL builds keep Ctrl+C interruptibility during long ITK
|
|
141
|
+
operations and the free-threaded build skips the GIL-requiring check. The
|
|
142
|
+
sdist job also runs `uv run coverage run` then `coverage report -m` — keep
|
|
143
|
+
coverage healthy when adding code (the `[tool.coverage.report]` config in
|
|
141
144
|
`pyproject.toml` omits the test files). PyPI publish and the GHCR Docker
|
|
142
145
|
image only run on a published GitHub release.
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
Copyright (c) 2022 Washington University.
|
|
2
2
|
Authored by: Andrew Van
|
|
3
3
|
|
|
4
|
-
Please cite
|
|
4
|
+
Please cite:
|
|
5
|
+
Van AN, Montez DF, Laumann TO, Cho PN, Suljic V, Madison T, et al.
|
|
6
|
+
Frame-wise multi-echo distortion correction for superior functional MRI.
|
|
7
|
+
Imaging Neuroscience 2026; 4 IMAG.a.1262. doi: https://doi.org/10.1162/IMAG.a.1262
|
|
5
8
|
|
|
6
9
|
warpkit is free for non-commercial use by academic, government,
|
|
7
10
|
and non-profit/not-for-profit institutions. A commercial version of the software is
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: warpkit
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: A python library for neuroimaging transformations
|
|
5
5
|
Keywords: neuroimaging
|
|
6
6
|
Author-Email: Andrew Van <vanandrew77@gmail.com>
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
License-File: include/romeo/LICENSE
|
|
7
9
|
Classifier: Programming Language :: Python :: 3
|
|
8
10
|
Project-URL: github, https://github.com/vanandrew/warpkit
|
|
11
|
+
Project-URL: paper, https://doi.org/10.1162/IMAG.a.1262
|
|
9
12
|
Requires-Python: >=3.11
|
|
10
13
|
Requires-Dist: nibabel>=4.0.2
|
|
11
14
|
Requires-Dist: numpy>=1.23.3
|
|
@@ -19,10 +22,9 @@ Description-Content-Type: text/markdown
|
|
|
19
22
|
|
|
20
23
|
[](https://github.com/vanandrew/warpkit/actions)
|
|
21
24
|
[](https://pypi.org/project/warpkit/)
|
|
22
|
-
[](https://github.com/vanandrew/warpkit/pkgs/container/warpkit)
|
|
23
25
|
[](https://codecov.io/gh/vanandrew/warpkit)
|
|
24
26
|
|
|
25
|
-
A Python library for neuroimaging transforms, focused on the Multi-Echo DIstortion Correction (MEDIC) algorithm. The
|
|
27
|
+
A Python library for neuroimaging transforms, focused on the Multi-Echo DIstortion Correction (MEDIC) algorithm. The paper is published in *Imaging Neuroscience* at <https://doi.org/10.1162/IMAG.a.1262>.
|
|
26
28
|
|
|
27
29
|
The phase-unwrapping core is a self-contained C++17 port of [ROMEO](https://github.com/korbinian90/ROMEO) — there is no Julia runtime to install, and binary wheels ship with the ITK pieces statically linked. If you used an older release of warpkit that required `julia` on `PATH`, that step is gone.
|
|
28
30
|
|
|
@@ -211,6 +213,15 @@ wk-convert-fieldmap \
|
|
|
211
213
|
--output sub-01_run-01_fieldmap.nii
|
|
212
214
|
```
|
|
213
215
|
|
|
216
|
+
## Citation
|
|
217
|
+
|
|
218
|
+
If you use warpkit, please cite:
|
|
219
|
+
|
|
220
|
+
> Van AN, Montez DF, Laumann TO, Cho PN, Suljic V, Madison T, et al.
|
|
221
|
+
> Frame-wise multi-echo distortion correction for superior functional MRI.
|
|
222
|
+
> *Imaging Neuroscience* 2026; 4 IMAG.a.1262.
|
|
223
|
+
> doi: <https://doi.org/10.1162/IMAG.a.1262>
|
|
224
|
+
|
|
214
225
|
## Authors
|
|
215
226
|
|
|
216
227
|
Vahdeta Suljic <suljic@wustl.edu>, Andrew Van <vanandrew77@gmail.com>
|
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/vanandrew/warpkit/actions)
|
|
4
4
|
[](https://pypi.org/project/warpkit/)
|
|
5
|
-
[](https://github.com/vanandrew/warpkit/pkgs/container/warpkit)
|
|
6
5
|
[](https://codecov.io/gh/vanandrew/warpkit)
|
|
7
6
|
|
|
8
|
-
A Python library for neuroimaging transforms, focused on the Multi-Echo DIstortion Correction (MEDIC) algorithm. The
|
|
7
|
+
A Python library for neuroimaging transforms, focused on the Multi-Echo DIstortion Correction (MEDIC) algorithm. The paper is published in *Imaging Neuroscience* at <https://doi.org/10.1162/IMAG.a.1262>.
|
|
9
8
|
|
|
10
9
|
The phase-unwrapping core is a self-contained C++17 port of [ROMEO](https://github.com/korbinian90/ROMEO) — there is no Julia runtime to install, and binary wheels ship with the ITK pieces statically linked. If you used an older release of warpkit that required `julia` on `PATH`, that step is gone.
|
|
11
10
|
|
|
@@ -194,6 +193,15 @@ wk-convert-fieldmap \
|
|
|
194
193
|
--output sub-01_run-01_fieldmap.nii
|
|
195
194
|
```
|
|
196
195
|
|
|
196
|
+
## Citation
|
|
197
|
+
|
|
198
|
+
If you use warpkit, please cite:
|
|
199
|
+
|
|
200
|
+
> Van AN, Montez DF, Laumann TO, Cho PN, Suljic V, Madison T, et al.
|
|
201
|
+
> Frame-wise multi-echo distortion correction for superior functional MRI.
|
|
202
|
+
> *Imaging Neuroscience* 2026; 4 IMAG.a.1262.
|
|
203
|
+
> doi: <https://doi.org/10.1162/IMAG.a.1262>
|
|
204
|
+
|
|
197
205
|
## Authors
|
|
198
206
|
|
|
199
207
|
Vahdeta Suljic <suljic@wustl.edu>, Andrew Van <vanandrew77@gmail.com>
|
|
@@ -48,50 +48,23 @@ inline std::vector<T> voxel_quality(const T* phase4d, std::size_t nx, std::size_
|
|
|
48
48
|
|
|
49
49
|
std::vector<T> qmap(vol, T(0));
|
|
50
50
|
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// Backward-edge contributions. Julia:
|
|
57
|
-
// qmap[2:end,:,:] .+= weights[1, 1:end-1, :, :]
|
|
58
|
-
// qmap[:,2:end,:] .+= weights[2, :, 1:end-1, :]
|
|
59
|
-
// qmap[:,:,2:end] .+= weights[3, :, :, 1:end-1]
|
|
60
|
-
// In 0-based: each interior voxel adds the forward-edge weight of its
|
|
61
|
-
// previous neighbor along that dim.
|
|
62
|
-
const std::ptrdiff_t sx = 1;
|
|
63
|
-
const std::ptrdiff_t sy = static_cast<std::ptrdiff_t>(nx);
|
|
64
|
-
const std::ptrdiff_t sz = static_cast<std::ptrdiff_t>(nx * ny);
|
|
65
|
-
|
|
51
|
+
// Single fused pass: each voxel sums its three forward edges plus the
|
|
52
|
+
// forward-edge weight of its previous neighbor along each axis (= the
|
|
53
|
+
// backward-edge contribution from Julia's three .+= expressions).
|
|
54
|
+
// Associativity matches the original four-pass form: forward triplet
|
|
55
|
+
// first, then back-x, then back-y, then back-z.
|
|
66
56
|
for (std::size_t k = 0; k < nz; ++k) {
|
|
67
57
|
for (std::size_t j = 0; j < ny; ++j) {
|
|
68
|
-
for (std::size_t i = 1; i < nx; ++i) {
|
|
69
|
-
const std::size_t idx = i + nx * (j + ny * k);
|
|
70
|
-
const std::size_t prev = idx - static_cast<std::size_t>(sx);
|
|
71
|
-
qmap[idx] += w[0u + 3u * prev];
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
for (std::size_t k = 0; k < nz; ++k) {
|
|
76
|
-
for (std::size_t j = 1; j < ny; ++j) {
|
|
77
58
|
for (std::size_t i = 0; i < nx; ++i) {
|
|
78
59
|
const std::size_t idx = i + nx * (j + ny * k);
|
|
79
|
-
|
|
80
|
-
|
|
60
|
+
T q = w[0u + 3u * idx] + w[1u + 3u * idx] + w[2u + 3u * idx];
|
|
61
|
+
if (i > 0) q += w[0u + 3u * (idx - 1)];
|
|
62
|
+
if (j > 0) q += w[1u + 3u * (idx - nx)];
|
|
63
|
+
if (k > 0) q += w[2u + 3u * (idx - nx * ny)];
|
|
64
|
+
qmap[idx] = q / T(6);
|
|
81
65
|
}
|
|
82
66
|
}
|
|
83
67
|
}
|
|
84
|
-
for (std::size_t k = 1; k < nz; ++k) {
|
|
85
|
-
for (std::size_t j = 0; j < ny; ++j) {
|
|
86
|
-
for (std::size_t i = 0; i < nx; ++i) {
|
|
87
|
-
const std::size_t idx = i + nx * (j + ny * k);
|
|
88
|
-
const std::size_t prev = idx - static_cast<std::size_t>(sz);
|
|
89
|
-
qmap[idx] += w[2u + 3u * prev];
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
for (std::size_t i = 0; i < vol; ++i) qmap[i] /= T(6);
|
|
95
68
|
return qmap;
|
|
96
69
|
}
|
|
97
70
|
|
|
@@ -119,91 +119,65 @@ inline std::vector<OutT> calculate_weights_romeo_impl(const T* phase, std::size_
|
|
|
119
119
|
if (phase2 == nullptr || TEs == nullptr) flags.phase_gradient_coherence = false;
|
|
120
120
|
|
|
121
121
|
const std::size_t n = nx * ny * nz;
|
|
122
|
-
const std::ptrdiff_t stride[3] = {1, static_cast<std::ptrdiff_t>(nx), static_cast<std::ptrdiff_t>(nx * ny)};
|
|
123
122
|
const std::ptrdiff_t signed_n = static_cast<std::ptrdiff_t>(n);
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
std::vector<T> masked_mag;
|
|
128
|
-
const T* mag_in_use = mag;
|
|
129
|
-
if (mag != nullptr && mask != nullptr) {
|
|
130
|
-
masked_mag.assign(mag, mag + n);
|
|
131
|
-
for (std::size_t i = 0; i < n; ++i) {
|
|
132
|
-
if (!mask[i]) masked_mag[i] = T(0);
|
|
133
|
-
}
|
|
134
|
-
mag_in_use = masked_mag.data();
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// maxmag: `maximum(mag[isfinite.(mag)])` — infinities and NaNs excluded.
|
|
138
|
-
T maxmag = T(0);
|
|
139
|
-
bool has_finite_mag = false;
|
|
140
|
-
if (mag_in_use != nullptr) {
|
|
141
|
-
for (std::size_t i = 0; i < n; ++i) {
|
|
142
|
-
T v = mag_in_use[i];
|
|
143
|
-
if (std::isfinite(v) && (!has_finite_mag || v > maxmag)) {
|
|
144
|
-
maxmag = v;
|
|
145
|
-
has_finite_mag = true;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
(void)maxmag; // only needed once magweight/magweight2 (flags 5-6) are ported.
|
|
150
|
-
(void)has_finite_mag;
|
|
123
|
+
const std::ptrdiff_t sx = 1;
|
|
124
|
+
const std::ptrdiff_t sy = static_cast<std::ptrdiff_t>(nx);
|
|
125
|
+
const std::ptrdiff_t sz = static_cast<std::ptrdiff_t>(nx * ny);
|
|
151
126
|
|
|
152
127
|
std::vector<OutT> weights(3 * n, zero_value);
|
|
153
128
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
129
|
+
// Mirrors ROMEO's `mag .* mask` pre-pass without materializing a copy: any
|
|
130
|
+
// masked-out voxel reads as 0 magnitude.
|
|
131
|
+
auto mag_at = [&](std::ptrdiff_t i) -> T {
|
|
132
|
+
if (mask != nullptr && !mask[i]) return T(0);
|
|
133
|
+
return mag[i];
|
|
134
|
+
};
|
|
160
135
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
// flags 5-6 (magweight, magweight2) not ported — warpkit uses :romeo, not :romeo6.
|
|
136
|
+
auto edge_weight = [&](std::ptrdiff_t idx, std::ptrdiff_t jdx, std::ptrdiff_t stride) -> T {
|
|
137
|
+
T w = T(1);
|
|
138
|
+
if (flags.phase_coherence) {
|
|
139
|
+
w *= T(0.1) + T(0.9) * detail::phase_coherence(phase[idx], phase[jdx]);
|
|
140
|
+
}
|
|
141
|
+
if (flags.phase_gradient_coherence) {
|
|
142
|
+
w *= T(0.1) + T(0.9) * detail::phase_gradient_coherence(phase[idx], phase[jdx], phase2[idx], phase2[jdx],
|
|
143
|
+
TEs[0], TEs[1]);
|
|
144
|
+
}
|
|
145
|
+
if (flags.phase_linearity) {
|
|
146
|
+
w *= T(0.1) + T(0.9) * detail::phase_linearity(phase, idx, jdx, stride, signed_n);
|
|
147
|
+
}
|
|
148
|
+
if (mag != nullptr) {
|
|
149
|
+
T mi = mag_at(idx);
|
|
150
|
+
T mj = mag_at(jdx);
|
|
151
|
+
T small = std::min(mi, mj);
|
|
152
|
+
T big = std::max(mi, mj);
|
|
153
|
+
if (flags.mag_coherence) {
|
|
154
|
+
w *= T(0.1) + T(0.9) * detail::mag_coherence(small, big);
|
|
182
155
|
}
|
|
183
|
-
|
|
184
|
-
// Column-major (3, nx, ny, nz): index = dim + 3*idx
|
|
185
|
-
weights[static_cast<std::size_t>(dim) + 3u * static_cast<std::size_t>(idx)] = rescale_fn(w);
|
|
156
|
+
// flags 5-6 (magweight, magweight2) not ported — warpkit uses :romeo, not :romeo6.
|
|
186
157
|
}
|
|
187
|
-
|
|
158
|
+
return w;
|
|
159
|
+
};
|
|
188
160
|
|
|
189
|
-
//
|
|
190
|
-
//
|
|
161
|
+
// One pass over voxels in (k, j, i) order. Boundary edges are skipped by
|
|
162
|
+
// construction via the `+1 < n*` guards, so the trailing zero-fill pass
|
|
163
|
+
// from the Julia source is unnecessary — the value-init of `weights`
|
|
164
|
+
// already leaves those slots at `zero_value`.
|
|
191
165
|
for (std::size_t k = 0; k < nz; ++k) {
|
|
192
166
|
for (std::size_t j = 0; j < ny; ++j) {
|
|
193
|
-
std::size_t
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
167
|
+
for (std::size_t i = 0; i < nx; ++i) {
|
|
168
|
+
const std::size_t idx = i + nx * (j + ny * k);
|
|
169
|
+
if (mask != nullptr && !mask[idx]) continue;
|
|
170
|
+
const std::ptrdiff_t sidx = static_cast<std::ptrdiff_t>(idx);
|
|
171
|
+
if (i + 1 < nx) {
|
|
172
|
+
weights[0u + 3u * idx] = rescale_fn(edge_weight(sidx, sidx + sx, sx));
|
|
173
|
+
}
|
|
174
|
+
if (j + 1 < ny) {
|
|
175
|
+
weights[1u + 3u * idx] = rescale_fn(edge_weight(sidx, sidx + sy, sy));
|
|
176
|
+
}
|
|
177
|
+
if (k + 1 < nz) {
|
|
178
|
+
weights[2u + 3u * idx] = rescale_fn(edge_weight(sidx, sidx + sz, sz));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
207
181
|
}
|
|
208
182
|
}
|
|
209
183
|
|
|
@@ -19,6 +19,18 @@
|
|
|
19
19
|
|
|
20
20
|
namespace py = pybind11;
|
|
21
21
|
|
|
22
|
+
// PyErr_CheckSignals() needs the GIL; free-threaded builds (Py_GIL_DISABLED)
|
|
23
|
+
// declare `py::mod_gil_not_used()` and skip the check, trading Ctrl+C
|
|
24
|
+
// interruptibility during long ITK operations for no-GIL execution.
|
|
25
|
+
#ifdef Py_GIL_DISABLED
|
|
26
|
+
#define WARPKIT_CHECK_SIGNALS() ((void)0)
|
|
27
|
+
#else
|
|
28
|
+
#define WARPKIT_CHECK_SIGNALS() \
|
|
29
|
+
do { \
|
|
30
|
+
if (PyErr_CheckSignals() != 0) throw py::error_already_set(); \
|
|
31
|
+
} while (0)
|
|
32
|
+
#endif
|
|
33
|
+
|
|
22
34
|
/**
|
|
23
35
|
* @brief Invert a displacement map
|
|
24
36
|
*
|
|
@@ -139,9 +151,9 @@ py::array_t<T, py::array::f_style> invert_displacement_map(py::array_t<T, py::ar
|
|
|
139
151
|
|
|
140
152
|
// Get output
|
|
141
153
|
typename DisplacementMapType::Pointer inv_map = identity_filter->GetOutput();
|
|
142
|
-
|
|
154
|
+
WARPKIT_CHECK_SIGNALS();
|
|
143
155
|
inv_map->Update();
|
|
144
|
-
|
|
156
|
+
WARPKIT_CHECK_SIGNALS();
|
|
145
157
|
itk::ImageRegionConstIteratorWithIndex<DisplacementMapType> inv_map_it(inv_map,
|
|
146
158
|
inv_map->GetLargestPossibleRegion());
|
|
147
159
|
py::array_t<T, py::array::f_style> inverted_displacement_map(displacement_map);
|
|
@@ -170,7 +182,7 @@ py::array_t<T, py::array::f_style> invert_displacement_field(py::array_t<T, py::
|
|
|
170
182
|
py::array_t<T, py::array::f_style> direction,
|
|
171
183
|
py::array_t<T, py::array::f_style> spacing,
|
|
172
184
|
ssize_t iterations, bool verbose) {
|
|
173
|
-
|
|
185
|
+
WARPKIT_CHECK_SIGNALS();
|
|
174
186
|
|
|
175
187
|
// Get the displacement field shape
|
|
176
188
|
const ssize_t* shape = displacement_field.shape();
|
|
@@ -227,9 +239,9 @@ py::array_t<T, py::array::f_style> invert_displacement_field(py::array_t<T, py::
|
|
|
227
239
|
|
|
228
240
|
// Get output
|
|
229
241
|
typename DisplacementFieldType::Pointer inv_field = invert_displacement_filter->GetOutput();
|
|
230
|
-
|
|
242
|
+
WARPKIT_CHECK_SIGNALS();
|
|
231
243
|
inv_field->Update();
|
|
232
|
-
|
|
244
|
+
WARPKIT_CHECK_SIGNALS();
|
|
233
245
|
itk::ImageRegionConstIteratorWithIndex<DisplacementFieldType> inv_field_it(inv_field,
|
|
234
246
|
inv_field->GetLargestPossibleRegion());
|
|
235
247
|
py::array_t<T, py::array::f_style> inverted_displacement_field(displacement_field);
|
|
@@ -261,7 +273,7 @@ py::array_t<T, py::array::f_style> compute_jacobian_determinant(py::array_t<T, p
|
|
|
261
273
|
py::array_t<T, py::array::f_style> origin,
|
|
262
274
|
py::array_t<T, py::array::f_style> direction,
|
|
263
275
|
py::array_t<T, py::array::f_style> spacing) {
|
|
264
|
-
|
|
276
|
+
WARPKIT_CHECK_SIGNALS();
|
|
265
277
|
|
|
266
278
|
// Get the displacement field shape
|
|
267
279
|
const ssize_t* shape = displacement_field.shape();
|
|
@@ -315,9 +327,9 @@ py::array_t<T, py::array::f_style> compute_jacobian_determinant(py::array_t<T, p
|
|
|
315
327
|
// Get the jacobian determinant fields
|
|
316
328
|
typename DisplacementFieldJacobianDeterminantFilterType::OutputImagePointer jacobian_determinant =
|
|
317
329
|
jacobian_filter->GetOutput();
|
|
318
|
-
|
|
330
|
+
WARPKIT_CHECK_SIGNALS();
|
|
319
331
|
jacobian_determinant->Update();
|
|
320
|
-
|
|
332
|
+
WARPKIT_CHECK_SIGNALS();
|
|
321
333
|
|
|
322
334
|
// Convert to numpy array
|
|
323
335
|
py::array_t<T, py::array::f_style> jacobian_determinant_array({shape[0], shape[1], shape[2]});
|
|
@@ -461,9 +473,9 @@ py::array_t<T, py::array::f_style> resample(
|
|
|
461
473
|
|
|
462
474
|
// Get the output
|
|
463
475
|
typename OutputImageType::Pointer output_image = warp_filter->GetOutput();
|
|
464
|
-
|
|
476
|
+
WARPKIT_CHECK_SIGNALS();
|
|
465
477
|
output_image->Update();
|
|
466
|
-
|
|
478
|
+
WARPKIT_CHECK_SIGNALS();
|
|
467
479
|
itk::ImageRegionConstIteratorWithIndex<OutputImageType> output_iterator(output_image,
|
|
468
480
|
output_image->GetLargestPossibleRegion());
|
|
469
481
|
py::array_t<T, py::array::f_style> output_array({output_shape.at(0), output_shape.at(1), output_shape.at(2)});
|
|
@@ -481,7 +493,7 @@ T compute_hausdorff_distance(
|
|
|
481
493
|
py::array_t<T, py::array::f_style> image1_direction, py::array_t<T, py::array::f_style> image1_spacing,
|
|
482
494
|
py::array_t<T, py::array::f_style> image2, py::array_t<T, py::array::f_style> image2_origin,
|
|
483
495
|
py::array_t<T, py::array::f_style> image2_direction, py::array_t<T, py::array::f_style> image2_spacing) {
|
|
484
|
-
|
|
496
|
+
WARPKIT_CHECK_SIGNALS();
|
|
485
497
|
|
|
486
498
|
// Setup types
|
|
487
499
|
using ImageType = typename itk::Image<T, 3>;
|
|
@@ -545,7 +557,7 @@ T compute_hausdorff_distance(
|
|
|
545
557
|
// Get the hausdorff distance
|
|
546
558
|
hausdorff_filter->Update();
|
|
547
559
|
T hausdorff_distance = hausdorff_filter->GetAverageHausdorffDistance();
|
|
548
|
-
|
|
560
|
+
WARPKIT_CHECK_SIGNALS();
|
|
549
561
|
|
|
550
562
|
// Return the hausdorff distance
|
|
551
563
|
return hausdorff_distance;
|
|
@@ -102,6 +102,20 @@ def write_readme(bundle: Path, version: str, target: str) -> None:
|
|
|
102
102
|
(bundle / "README.md").write_text(body, encoding="utf-8")
|
|
103
103
|
|
|
104
104
|
|
|
105
|
+
def write_licenses(bundle: Path) -> None:
|
|
106
|
+
# Bundle warpkit's own LICENSE plus the upstream ROMEO MIT notice. The
|
|
107
|
+
# ROMEO notice is required: the wk-* binaries link in compiled code derived
|
|
108
|
+
# from include/romeo/, which the MIT terms require we ship the notice with.
|
|
109
|
+
repo_root = Path(__file__).resolve().parents[2]
|
|
110
|
+
licenses_dir = bundle / "LICENSES"
|
|
111
|
+
licenses_dir.mkdir(exist_ok=True)
|
|
112
|
+
shutil.copy2(repo_root / "LICENSE", licenses_dir / "warpkit.LICENSE")
|
|
113
|
+
shutil.copy2(
|
|
114
|
+
repo_root / "include" / "romeo" / "LICENSE",
|
|
115
|
+
licenses_dir / "ROMEO.LICENSE",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
105
119
|
def make_zip(bundle: Path, out_zip: Path) -> None:
|
|
106
120
|
# shutil.make_archive's base_dir keeps a tidy top-level folder inside the zip.
|
|
107
121
|
base_name = str(out_zip.with_suffix(""))
|
|
@@ -144,6 +158,7 @@ def main() -> int:
|
|
|
144
158
|
|
|
145
159
|
adhoc_sign_macos(versioned)
|
|
146
160
|
write_readme(versioned, version, target)
|
|
161
|
+
write_licenses(versioned)
|
|
147
162
|
|
|
148
163
|
out_zip = dist / f"warpkit-{version}-{target}.zip"
|
|
149
164
|
make_zip(versioned, out_zip)
|
|
@@ -47,3 +47,9 @@ xattr -r -d com.apple.quarantine /opt/warpkit-@VERSION@
|
|
|
47
47
|
- `wk-compute-jacobian` — compute the Jacobian determinant of a warp
|
|
48
48
|
|
|
49
49
|
Run any of them with `--help` for usage.
|
|
50
|
+
|
|
51
|
+
## Licenses
|
|
52
|
+
|
|
53
|
+
`LICENSES/warpkit.LICENSE` is the warpkit license. `LICENSES/ROMEO.LICENSE` is
|
|
54
|
+
the upstream MIT notice for the ROMEO phase-unwrapping algorithms compiled
|
|
55
|
+
into these binaries.
|
|
@@ -6,7 +6,11 @@ requires-python = ">=3.11"
|
|
|
6
6
|
authors = [{ name = "Andrew Van", email = "vanandrew77@gmail.com" }]
|
|
7
7
|
keywords = ["neuroimaging"]
|
|
8
8
|
classifiers = ["Programming Language :: Python :: 3"]
|
|
9
|
-
urls = { github = "https://github.com/vanandrew/warpkit" }
|
|
9
|
+
urls = { github = "https://github.com/vanandrew/warpkit", paper = "https://doi.org/10.1162/IMAG.a.1262" }
|
|
10
|
+
# include/romeo/LICENSE is the upstream MIT notice for the ROMEO C++ port; it
|
|
11
|
+
# travels with the headers in source distributions and (via PEP 639
|
|
12
|
+
# license-files) gets bundled into wheels under *.dist-info/licenses/.
|
|
13
|
+
license-files = ["LICENSE", "include/romeo/LICENSE"]
|
|
10
14
|
dynamic = ["version"]
|
|
11
15
|
dependencies = [
|
|
12
16
|
"nibabel >= 4.0.2",
|
|
@@ -118,9 +122,10 @@ command_line = "-m pytest"
|
|
|
118
122
|
omit = ["tests/*"]
|
|
119
123
|
|
|
120
124
|
[tool.cibuildwheel]
|
|
121
|
-
# Skip musllinux (we ship manylinux only)
|
|
122
|
-
#
|
|
123
|
-
|
|
125
|
+
# Skip musllinux (we ship manylinux only). Free-threaded (cp31Xt-*) builds
|
|
126
|
+
# are kept: warps.h guards PyErr_CheckSignals() behind WARPKIT_CHECK_SIGNALS,
|
|
127
|
+
# which is a no-op under Py_GIL_DISABLED.
|
|
128
|
+
skip = "*musllinux*"
|
|
124
129
|
build-frontend = "build"
|
|
125
130
|
build-verbosity = 3
|
|
126
131
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
namespace py = pybind11;
|
|
7
7
|
|
|
8
|
-
PYBIND11_MODULE(warpkit_cpp, m) {
|
|
8
|
+
PYBIND11_MODULE(warpkit_cpp, m, py::mod_gil_not_used()) {
|
|
9
9
|
m.def("calculate_weights", &romeo::calculate_weights<float>,
|
|
10
10
|
"ROMEO edge-weight map (3, nx, ny, nz) uint8. Exposed for port validation; not used by warpkit.",
|
|
11
11
|
py::arg("phase"),
|
|
@@ -600,6 +600,91 @@ def test_unwrap_phase_noiseframes_consumes_all_frames(argv, capsys, tmp_path):
|
|
|
600
600
|
assert "0 frames" in err
|
|
601
601
|
|
|
602
602
|
|
|
603
|
+
def test_unwrap_phase_metadata_accepts_echotime_only(argv, capsys, tmp_path):
|
|
604
|
+
"""``wk-unwrap-phase`` only needs EchoTime; sidecars without
|
|
605
|
+
TotalReadoutTime / PhaseEncodingDirection must work. A mismatched phase
|
|
606
|
+
count forces a clean parser.error *after* the metadata loader resolves —
|
|
607
|
+
if the loader still required TRT/PED we'd see a KeyError instead."""
|
|
608
|
+
sidecar = tmp_path / "m1.json"
|
|
609
|
+
sidecar.write_text(json.dumps({"EchoTime": 0.014}))
|
|
610
|
+
argv(
|
|
611
|
+
[
|
|
612
|
+
"wk-unwrap-phase",
|
|
613
|
+
"--magnitude",
|
|
614
|
+
"m.nii",
|
|
615
|
+
"--phase",
|
|
616
|
+
"p1.nii",
|
|
617
|
+
"p2.nii",
|
|
618
|
+
"--metadata",
|
|
619
|
+
str(sidecar),
|
|
620
|
+
"--out-prefix",
|
|
621
|
+
str(tmp_path / "out"),
|
|
622
|
+
]
|
|
623
|
+
)
|
|
624
|
+
with pytest.raises(SystemExit) as exc:
|
|
625
|
+
unwrap_phase_main()
|
|
626
|
+
assert exc.value.code == 2
|
|
627
|
+
err = capsys.readouterr().err
|
|
628
|
+
assert "must match" in err
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def test_unwrap_phase_metadata_missing_echotime(argv, capsys, tmp_path):
|
|
632
|
+
"""A sidecar without EchoTime must surface as a clean parser error, not a
|
|
633
|
+
KeyError from the metadata loader."""
|
|
634
|
+
sidecar = tmp_path / "m1.json"
|
|
635
|
+
sidecar.write_text(json.dumps({}))
|
|
636
|
+
argv(
|
|
637
|
+
[
|
|
638
|
+
"wk-unwrap-phase",
|
|
639
|
+
"--magnitude",
|
|
640
|
+
"m.nii",
|
|
641
|
+
"--phase",
|
|
642
|
+
"p.nii",
|
|
643
|
+
"--metadata",
|
|
644
|
+
str(sidecar),
|
|
645
|
+
"--out-prefix",
|
|
646
|
+
str(tmp_path / "out"),
|
|
647
|
+
]
|
|
648
|
+
)
|
|
649
|
+
with pytest.raises(SystemExit) as exc:
|
|
650
|
+
unwrap_phase_main()
|
|
651
|
+
assert exc.value.code == 2
|
|
652
|
+
err = capsys.readouterr().err
|
|
653
|
+
assert "EchoTime" in err
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def test_medic_noiseframes_consumes_all_frames(argv, capsys, tmp_path):
|
|
657
|
+
"""``-f`` >= n_frames now raises a clean parser error in medic too: the
|
|
658
|
+
check moved into ``trim_noise_frames`` so every caller is protected from
|
|
659
|
+
silently producing an empty 4D series."""
|
|
660
|
+
mag = _write_nifti(tmp_path / "m.nii", (4, 4, 4, 5))
|
|
661
|
+
phase = _write_nifti(tmp_path / "p.nii", (4, 4, 4, 5))
|
|
662
|
+
argv(
|
|
663
|
+
[
|
|
664
|
+
"wk-medic",
|
|
665
|
+
"--magnitude",
|
|
666
|
+
mag,
|
|
667
|
+
"--phase",
|
|
668
|
+
phase,
|
|
669
|
+
"--TEs",
|
|
670
|
+
"14.0",
|
|
671
|
+
"--total-readout-time",
|
|
672
|
+
"0.05",
|
|
673
|
+
"--phase-encoding-direction",
|
|
674
|
+
"j",
|
|
675
|
+
"--out-prefix",
|
|
676
|
+
str(tmp_path / "out"),
|
|
677
|
+
"-f",
|
|
678
|
+
"5",
|
|
679
|
+
]
|
|
680
|
+
)
|
|
681
|
+
with pytest.raises(SystemExit) as exc:
|
|
682
|
+
medic_main()
|
|
683
|
+
assert exc.value.code == 2
|
|
684
|
+
err = capsys.readouterr().err
|
|
685
|
+
assert "0 frames" in err
|
|
686
|
+
|
|
687
|
+
|
|
603
688
|
# ---------------------------------------------------------------------------
|
|
604
689
|
# compute_fieldmap --help / argument validation
|
|
605
690
|
# ---------------------------------------------------------------------------
|
|
@@ -1693,14 +1778,12 @@ def test_bundle_frames_to_3d_series_clears_vector_intent():
|
|
|
1693
1778
|
|
|
1694
1779
|
def test_write_output_per_frame_map_clears_vector_intent(tmp_path):
|
|
1695
1780
|
"""Per-frame map outputs must round-trip without a stale vector intent."""
|
|
1696
|
-
import argparse
|
|
1697
|
-
|
|
1698
1781
|
from warpkit.scripts._warp_io import write_output
|
|
1699
1782
|
|
|
1700
1783
|
frames = [_vector_intent_frame() for _ in range(2)]
|
|
1701
1784
|
out_paths = [str(tmp_path / "f1.nii"), str(tmp_path / "f2.nii")]
|
|
1702
1785
|
|
|
1703
|
-
write_output(frames, out_paths, "map"
|
|
1786
|
+
write_output(frames, out_paths, "map")
|
|
1704
1787
|
|
|
1705
1788
|
for p in out_paths:
|
|
1706
1789
|
loaded = _load(p)
|