mriforge 0.3.0__py3-none-any.whl
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.
- mrif/__init__.py +56 -0
- mrif/_version.py +33 -0
- mrif/cli/__init__.py +3 -0
- mrif/cli/dicom_to_nifti.py +113 -0
- mrif/cli/t2star.py +469 -0
- mrif/dicom/__init__.py +109 -0
- mrif/dicom/enhanced_dicom.py +432 -0
- mrif/dicom/geometry.py +176 -0
- mrif/dicom/metadata.py +132 -0
- mrif/quantitative_MRI/__init__.py +62 -0
- mrif/quantitative_MRI/mapping.py +831 -0
- mrif/reconstruction/__init__.py +28 -0
- mrif/reconstruction/grappa/__init__.py +50 -0
- mrif/reconstruction/grappa/grappa_1D.py +175 -0
- mrif/reconstruction/grappa/grappa_2D.py +228 -0
- mrif/reconstruction/grappa/utils.py +31 -0
- mrif/reconstruction/sense/__init__.py +25 -0
- mrif/reconstruction/sense/cg.py +48 -0
- mrif/segmentation/MRISegmentationTool.py +167 -0
- mrif/segmentation/__init__.py +37 -0
- mrif/segmentation/tools.py +104 -0
- mrif/utilities/__init__.py +126 -0
- mrif/utilities/df_build.py +92 -0
- mrif/utilities/io.py +1724 -0
- mrif/utilities/numerical.py +101 -0
- mrif/utilities/utils.py +376 -0
- mrif/visualization/__init__.py +47 -0
- mrif/visualization/visualization.py +418 -0
- mriforge-0.3.0.dist-info/METADATA +936 -0
- mriforge-0.3.0.dist-info/RECORD +34 -0
- mriforge-0.3.0.dist-info/WHEEL +5 -0
- mriforge-0.3.0.dist-info/entry_points.txt +3 -0
- mriforge-0.3.0.dist-info/licenses/LICENSE +21 -0
- mriforge-0.3.0.dist-info/top_level.txt +1 -0
mrif/__init__.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# File created by: Eisa Hedayati
|
|
2
|
+
# Date: 12/29/2023
|
|
3
|
+
# Description: This file is developed at CMRR
|
|
4
|
+
|
|
5
|
+
from importlib import import_module
|
|
6
|
+
|
|
7
|
+
from ._version import __version__
|
|
8
|
+
|
|
9
|
+
__author__ = "Eisa Hedayati"
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"__version__",
|
|
13
|
+
"utilities",
|
|
14
|
+
"utils",
|
|
15
|
+
"io",
|
|
16
|
+
"visualization",
|
|
17
|
+
"vis",
|
|
18
|
+
"segmentation",
|
|
19
|
+
"seg",
|
|
20
|
+
"quantitative_MRI",
|
|
21
|
+
"qmr",
|
|
22
|
+
"reconstruction",
|
|
23
|
+
"recon",
|
|
24
|
+
"dicom",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
_MODULE_ALIASES = {
|
|
29
|
+
"utilities": ".utilities",
|
|
30
|
+
"utils": ".utilities",
|
|
31
|
+
"io": ".utilities.io",
|
|
32
|
+
"visualization": ".visualization",
|
|
33
|
+
"vis": ".visualization",
|
|
34
|
+
"segmentation": ".segmentation",
|
|
35
|
+
"seg": ".segmentation",
|
|
36
|
+
"quantitative_MRI": ".quantitative_MRI",
|
|
37
|
+
"qmr": ".quantitative_MRI",
|
|
38
|
+
"reconstruction": ".reconstruction",
|
|
39
|
+
"recon": ".reconstruction",
|
|
40
|
+
"dicom": ".dicom",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def __getattr__(name: str):
|
|
45
|
+
if name in _MODULE_ALIASES:
|
|
46
|
+
module = import_module(_MODULE_ALIASES[name], __name__)
|
|
47
|
+
globals()[name] = module
|
|
48
|
+
return module
|
|
49
|
+
|
|
50
|
+
raise AttributeError(
|
|
51
|
+
f"module {__name__!r} has no attribute {name!r}"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def __dir__():
|
|
56
|
+
return sorted(set(globals()) | set(__all__))
|
mrif/_version.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# src/mrif/_version.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version as _dist_version
|
|
4
|
+
|
|
5
|
+
# Name of the installed distribution (usually your [project].name)
|
|
6
|
+
_DIST_NAME = "mriforge"
|
|
7
|
+
|
|
8
|
+
def _from_pyproject() -> str:
|
|
9
|
+
# Fallback for editable/dev checkouts where the dist isn’t installed yet
|
|
10
|
+
import sys, pathlib
|
|
11
|
+
root = pathlib.Path(__file__).resolve().parents[2] # .../src/mrif -> project root?
|
|
12
|
+
# Walk up until we find pyproject.toml
|
|
13
|
+
while root != root.parent and not (root / "pyproject.toml").exists():
|
|
14
|
+
root = root.parent
|
|
15
|
+
pp = root / "pyproject.toml"
|
|
16
|
+
if not pp.exists():
|
|
17
|
+
return "0+unknown"
|
|
18
|
+
try:
|
|
19
|
+
if sys.version_info >= (3, 11):
|
|
20
|
+
import tomllib # stdlib on 3.11+
|
|
21
|
+
else:
|
|
22
|
+
import tomli as tomllib # add 'tomli' as a dev dep if you need this fallback
|
|
23
|
+
with pp.open("rb") as f:
|
|
24
|
+
data = tomllib.load(f)
|
|
25
|
+
return data.get("project", {}).get("version", "0+unknown")
|
|
26
|
+
except Exception:
|
|
27
|
+
return "0+unknown"
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
__version__ = _dist_version(_DIST_NAME)
|
|
31
|
+
except PackageNotFoundError:
|
|
32
|
+
__version__ = _from_pyproject()
|
|
33
|
+
|
mrif/cli/__init__.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface for MRIForge DICOM to NIfTI conversion.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _build_parser():
|
|
10
|
+
parser = argparse.ArgumentParser(
|
|
11
|
+
prog="mriforge-dicom-to-nifti",
|
|
12
|
+
description=(
|
|
13
|
+
"Convert a DICOM series to NIfTI and create a "
|
|
14
|
+
"matching JSON metadata sidecar."
|
|
15
|
+
),
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
parser.add_argument(
|
|
19
|
+
"dicom_directory",
|
|
20
|
+
type=Path,
|
|
21
|
+
help="Path to the directory containing the DICOM series.",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
"nifti_file",
|
|
26
|
+
nargs="?",
|
|
27
|
+
type=Path,
|
|
28
|
+
default=None,
|
|
29
|
+
help=(
|
|
30
|
+
"Output NIfTI file. If omitted, the output is written "
|
|
31
|
+
"to ./<series_directory_name>.nii.gz."
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"--verbose",
|
|
37
|
+
action=argparse.BooleanOptionalAction,
|
|
38
|
+
default=True,
|
|
39
|
+
help=(
|
|
40
|
+
"Show conversion progress. "
|
|
41
|
+
"Enabled by default; use --no-verbose to disable."
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
return parser
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def main():
|
|
49
|
+
parser = _build_parser()
|
|
50
|
+
args = parser.parse_args()
|
|
51
|
+
|
|
52
|
+
dicom_directory = args.dicom_directory.expanduser()
|
|
53
|
+
|
|
54
|
+
if not dicom_directory.exists():
|
|
55
|
+
parser.error(
|
|
56
|
+
f"DICOM directory does not exist: "
|
|
57
|
+
f"{dicom_directory}"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if not dicom_directory.is_dir():
|
|
61
|
+
parser.error(
|
|
62
|
+
f"DICOM input must be a directory: "
|
|
63
|
+
f"{dicom_directory}"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if args.nifti_file is None:
|
|
67
|
+
series_name = dicom_directory.resolve().name
|
|
68
|
+
|
|
69
|
+
nifti_file = (
|
|
70
|
+
Path.cwd()
|
|
71
|
+
/ f"{series_name}.nii.gz"
|
|
72
|
+
)
|
|
73
|
+
else:
|
|
74
|
+
nifti_file = args.nifti_file.expanduser()
|
|
75
|
+
|
|
76
|
+
# Import lazily so importing the CLI itself does not load
|
|
77
|
+
# the heavier DICOM/NIfTI dependencies.
|
|
78
|
+
try:
|
|
79
|
+
from mrif.utilities.io import dicom_to_nifti
|
|
80
|
+
|
|
81
|
+
except ModuleNotFoundError as exc:
|
|
82
|
+
if exc.name in {
|
|
83
|
+
"nibabel",
|
|
84
|
+
"pydicom",
|
|
85
|
+
"SimpleITK",
|
|
86
|
+
"h5py",
|
|
87
|
+
}:
|
|
88
|
+
parser.error(
|
|
89
|
+
"DICOM/NIfTI support requires the MRIForge I/O "
|
|
90
|
+
"dependencies. Install them with:\n"
|
|
91
|
+
" pip install 'mriforge[io]'"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
raise
|
|
95
|
+
|
|
96
|
+
if args.verbose:
|
|
97
|
+
print("MRIForge DICOM to NIfTI conversion")
|
|
98
|
+
print(f"DICOM series: {dicom_directory}")
|
|
99
|
+
print(f"NIfTI output: {nifti_file}")
|
|
100
|
+
print("Reading DICOM series...")
|
|
101
|
+
|
|
102
|
+
dicom_to_nifti(
|
|
103
|
+
dicom_directory,
|
|
104
|
+
nifti_file,
|
|
105
|
+
verbose=args.verbose,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if args.verbose:
|
|
109
|
+
print("Conversion complete.")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
if __name__ == "__main__":
|
|
113
|
+
main()
|
mrif/cli/t2star.py
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
# File created by: Eisa Hedayati
|
|
2
|
+
# Date: 8/26/2026
|
|
3
|
+
# Description: This file is developed at CMRR
|
|
4
|
+
"""
|
|
5
|
+
Command-line interface for 3D T2* calculation.
|
|
6
|
+
|
|
7
|
+
Input:
|
|
8
|
+
4D multi-echo NIfTI: X x Y x Z x echoes
|
|
9
|
+
JSON sidecar containing Acquisition.EchoTimes
|
|
10
|
+
|
|
11
|
+
Output:
|
|
12
|
+
3D T2* map
|
|
13
|
+
3D S0 map
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _nifti_stem(path: Path) -> str:
|
|
24
|
+
"""Return filename without .nii or .nii.gz."""
|
|
25
|
+
|
|
26
|
+
name = path.name
|
|
27
|
+
|
|
28
|
+
if name.endswith(".nii.gz"):
|
|
29
|
+
return name[:-7]
|
|
30
|
+
|
|
31
|
+
if name.endswith(".nii"):
|
|
32
|
+
return name[:-4]
|
|
33
|
+
|
|
34
|
+
raise ValueError(
|
|
35
|
+
"Input must have a .nii or .nii.gz extension."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _build_parser():
|
|
40
|
+
|
|
41
|
+
parser = argparse.ArgumentParser(
|
|
42
|
+
prog="mriforge-t2star",
|
|
43
|
+
description=(
|
|
44
|
+
"Calculate 3D T2* and S0 maps from a 4D "
|
|
45
|
+
"multi-echo NIfTI and JSON metadata sidecar."
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"nifti_file",
|
|
51
|
+
type=Path,
|
|
52
|
+
help=(
|
|
53
|
+
"4D multi-echo NIfTI. "
|
|
54
|
+
"Echoes must be in the last dimension."
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"--json",
|
|
60
|
+
dest="json_file",
|
|
61
|
+
type=Path,
|
|
62
|
+
default=None,
|
|
63
|
+
help=(
|
|
64
|
+
"JSON metadata file. If omitted, a JSON file "
|
|
65
|
+
"with the same basename as the NIfTI is used."
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"-o",
|
|
71
|
+
"--output-prefix",
|
|
72
|
+
type=Path,
|
|
73
|
+
default=None,
|
|
74
|
+
help=(
|
|
75
|
+
"Output prefix. Default: input NIfTI basename."
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
parser.add_argument(
|
|
80
|
+
"--device",
|
|
81
|
+
default="auto",
|
|
82
|
+
help=(
|
|
83
|
+
"PyTorch device: auto, cpu, cuda, cuda:0, etc. "
|
|
84
|
+
"Default: auto."
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
parser.add_argument(
|
|
89
|
+
"--iterations",
|
|
90
|
+
type=int,
|
|
91
|
+
default=10000,
|
|
92
|
+
help="Number of fitting iterations. Default: 10000.",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
parser.add_argument(
|
|
96
|
+
"--lr",
|
|
97
|
+
type=float,
|
|
98
|
+
default=0.01,
|
|
99
|
+
help="Initial Adam learning rate. Default: 0.01.",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
parser.add_argument(
|
|
103
|
+
"--lr-decay-factor",
|
|
104
|
+
type=float,
|
|
105
|
+
default=0.1,
|
|
106
|
+
help="Learning-rate decay factor. Default: 0.1.",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
parser.add_argument(
|
|
110
|
+
"--patience",
|
|
111
|
+
type=int,
|
|
112
|
+
default=100,
|
|
113
|
+
help="Learning-rate scheduler patience. Default: 100.",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
parser.add_argument(
|
|
117
|
+
"--initial-t2star",
|
|
118
|
+
type=float,
|
|
119
|
+
default=20.0,
|
|
120
|
+
help="Initial T2* value in ms. Default: 20.",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return parser
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def main():
|
|
127
|
+
|
|
128
|
+
parser = _build_parser()
|
|
129
|
+
args = parser.parse_args()
|
|
130
|
+
|
|
131
|
+
# ---------------------------------------------------------
|
|
132
|
+
# Input NIfTI
|
|
133
|
+
# ---------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
nifti_file = args.nifti_file.expanduser()
|
|
136
|
+
|
|
137
|
+
if not nifti_file.exists():
|
|
138
|
+
parser.error(
|
|
139
|
+
f"NIfTI file does not exist: {nifti_file}"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
stem = _nifti_stem(nifti_file)
|
|
144
|
+
except ValueError as exc:
|
|
145
|
+
parser.error(str(exc))
|
|
146
|
+
|
|
147
|
+
# ---------------------------------------------------------
|
|
148
|
+
# JSON sidecar
|
|
149
|
+
# ---------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
if args.json_file is None:
|
|
152
|
+
|
|
153
|
+
json_file = nifti_file.with_name(
|
|
154
|
+
f"{stem}.json"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
else:
|
|
158
|
+
|
|
159
|
+
json_file = args.json_file.expanduser()
|
|
160
|
+
|
|
161
|
+
if not json_file.exists():
|
|
162
|
+
|
|
163
|
+
parser.error(
|
|
164
|
+
f"JSON sidecar does not exist: {json_file}"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
with json_file.open(
|
|
168
|
+
"r",
|
|
169
|
+
encoding="utf-8",
|
|
170
|
+
) as f:
|
|
171
|
+
|
|
172
|
+
metadata = json.load(f)
|
|
173
|
+
|
|
174
|
+
acquisition = metadata.get(
|
|
175
|
+
"Acquisition",
|
|
176
|
+
{},
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
echo_times = acquisition.get(
|
|
180
|
+
"EchoTimes"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if echo_times is None:
|
|
184
|
+
|
|
185
|
+
parser.error(
|
|
186
|
+
"JSON does not contain "
|
|
187
|
+
"Acquisition.EchoTimes."
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
echo_times = np.asarray(
|
|
191
|
+
echo_times,
|
|
192
|
+
dtype=np.float32,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
if echo_times.ndim != 1:
|
|
196
|
+
|
|
197
|
+
parser.error(
|
|
198
|
+
"Acquisition.EchoTimes must be a 1D list."
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
if len(echo_times) < 2:
|
|
202
|
+
|
|
203
|
+
parser.error(
|
|
204
|
+
"At least two echo times are required."
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if not np.all(np.isfinite(echo_times)):
|
|
208
|
+
|
|
209
|
+
parser.error(
|
|
210
|
+
"Echo times contain NaN or infinite values."
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
if np.unique(echo_times).size != echo_times.size:
|
|
214
|
+
|
|
215
|
+
parser.error(
|
|
216
|
+
"Echo times must be unique."
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# ---------------------------------------------------------
|
|
220
|
+
# Time units
|
|
221
|
+
# ---------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
time_unit = acquisition.get(
|
|
224
|
+
"TimeUnit"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
if time_unit is None:
|
|
228
|
+
|
|
229
|
+
parser.error(
|
|
230
|
+
"JSON does not contain "
|
|
231
|
+
"Acquisition.TimeUnit."
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
time_unit = str(
|
|
235
|
+
time_unit
|
|
236
|
+
).lower()
|
|
237
|
+
|
|
238
|
+
if time_unit in {
|
|
239
|
+
"ms",
|
|
240
|
+
"millisecond",
|
|
241
|
+
"milliseconds",
|
|
242
|
+
}:
|
|
243
|
+
|
|
244
|
+
pass
|
|
245
|
+
|
|
246
|
+
elif time_unit in {
|
|
247
|
+
"s",
|
|
248
|
+
"sec",
|
|
249
|
+
"second",
|
|
250
|
+
"seconds",
|
|
251
|
+
}:
|
|
252
|
+
|
|
253
|
+
echo_times *= 1000.0
|
|
254
|
+
|
|
255
|
+
else:
|
|
256
|
+
|
|
257
|
+
parser.error(
|
|
258
|
+
f"Unsupported TimeUnit: {time_unit}"
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# ---------------------------------------------------------
|
|
262
|
+
# Heavy imports
|
|
263
|
+
# ---------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
|
|
267
|
+
import nibabel as nib
|
|
268
|
+
import torch
|
|
269
|
+
|
|
270
|
+
from mrif.quantitative_MRI.mapping import (
|
|
271
|
+
t2_star_two_parametric_3D,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
from mrif.utilities.io import (
|
|
275
|
+
save_scalar_map_like,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
except ModuleNotFoundError as exc:
|
|
279
|
+
|
|
280
|
+
parser.error(
|
|
281
|
+
f"Missing dependency: {exc.name}"
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
# ---------------------------------------------------------
|
|
285
|
+
# Load NIfTI
|
|
286
|
+
# ---------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
image = nib.load(
|
|
289
|
+
str(nifti_file)
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
if image.ndim != 4:
|
|
293
|
+
|
|
294
|
+
parser.error(
|
|
295
|
+
"Input must be a 4D multi-echo NIfTI. "
|
|
296
|
+
f"Got shape {image.shape}."
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
if image.shape[-1] != len(echo_times):
|
|
300
|
+
|
|
301
|
+
parser.error(
|
|
302
|
+
"Echo count mismatch: "
|
|
303
|
+
f"NIfTI contains {image.shape[-1]} volumes "
|
|
304
|
+
f"but JSON contains {len(echo_times)} echo times."
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
data = image.get_fdata(
|
|
308
|
+
dtype=np.float32
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# ---------------------------------------------------------
|
|
312
|
+
# Sort echoes
|
|
313
|
+
#
|
|
314
|
+
# This is important:
|
|
315
|
+
# if TE order changes, the 4D volumes must change with it.
|
|
316
|
+
# ---------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
order = np.argsort(
|
|
319
|
+
echo_times
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
if not np.array_equal(
|
|
323
|
+
order,
|
|
324
|
+
np.arange(len(echo_times)),
|
|
325
|
+
):
|
|
326
|
+
|
|
327
|
+
print(
|
|
328
|
+
"Echo times are not sorted. "
|
|
329
|
+
"Reordering echoes and volumes together."
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
echo_times = echo_times[
|
|
333
|
+
order
|
|
334
|
+
]
|
|
335
|
+
|
|
336
|
+
data = data[
|
|
337
|
+
...,
|
|
338
|
+
order
|
|
339
|
+
]
|
|
340
|
+
|
|
341
|
+
# ---------------------------------------------------------
|
|
342
|
+
# Device
|
|
343
|
+
# ---------------------------------------------------------
|
|
344
|
+
|
|
345
|
+
if args.device == "auto":
|
|
346
|
+
|
|
347
|
+
device = None
|
|
348
|
+
|
|
349
|
+
display_device = (
|
|
350
|
+
"cuda"
|
|
351
|
+
if torch.cuda.is_available()
|
|
352
|
+
else "cpu"
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
else:
|
|
356
|
+
|
|
357
|
+
device = args.device
|
|
358
|
+
display_device = args.device
|
|
359
|
+
|
|
360
|
+
if (
|
|
361
|
+
str(display_device).startswith("cuda")
|
|
362
|
+
and not torch.cuda.is_available()
|
|
363
|
+
):
|
|
364
|
+
|
|
365
|
+
parser.error(
|
|
366
|
+
"CUDA requested, but PyTorch "
|
|
367
|
+
"does not detect a CUDA device."
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
# ---------------------------------------------------------
|
|
371
|
+
# Fit
|
|
372
|
+
# ---------------------------------------------------------
|
|
373
|
+
|
|
374
|
+
print("MRIForge 3D T2* calculation")
|
|
375
|
+
print(f"NIfTI: {nifti_file}")
|
|
376
|
+
print(f"JSON: {json_file}")
|
|
377
|
+
print(
|
|
378
|
+
f"Echo times (ms): "
|
|
379
|
+
f"{echo_times.tolist()}"
|
|
380
|
+
)
|
|
381
|
+
print(f"Device: {display_device}")
|
|
382
|
+
print("Calculating T2* and S0...")
|
|
383
|
+
|
|
384
|
+
result = t2_star_two_parametric_3D(
|
|
385
|
+
echo_times,
|
|
386
|
+
data,
|
|
387
|
+
num_iterations=args.iterations,
|
|
388
|
+
initial_lr=args.lr,
|
|
389
|
+
lr_decay_factor=args.lr_decay_factor,
|
|
390
|
+
patience=args.patience,
|
|
391
|
+
initial_T2_star=args.initial_t2star,
|
|
392
|
+
plot_error=False,
|
|
393
|
+
return_RMSE=False,
|
|
394
|
+
device=device,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
# Current fitter returns torch tensors.
|
|
398
|
+
t2star = (
|
|
399
|
+
result["T2_star_map"]
|
|
400
|
+
.detach()
|
|
401
|
+
.cpu()
|
|
402
|
+
.numpy()
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
s0 = (
|
|
406
|
+
result["S0_map"]
|
|
407
|
+
.detach()
|
|
408
|
+
.cpu()
|
|
409
|
+
.numpy()
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
# ---------------------------------------------------------
|
|
413
|
+
# Output names
|
|
414
|
+
# ---------------------------------------------------------
|
|
415
|
+
|
|
416
|
+
if args.output_prefix is None:
|
|
417
|
+
|
|
418
|
+
output_prefix = nifti_file.with_name(
|
|
419
|
+
stem
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
else:
|
|
423
|
+
|
|
424
|
+
output_prefix = (
|
|
425
|
+
args.output_prefix.expanduser()
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
output_prefix.parent.mkdir(
|
|
429
|
+
parents=True,
|
|
430
|
+
exist_ok=True,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
t2star_file = Path(
|
|
434
|
+
f"{output_prefix}_T2star.nii.gz"
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
s0_file = Path(
|
|
438
|
+
f"{output_prefix}_S0.nii.gz"
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
# ---------------------------------------------------------
|
|
442
|
+
# Save maps
|
|
443
|
+
# ---------------------------------------------------------
|
|
444
|
+
|
|
445
|
+
save_scalar_map_like(
|
|
446
|
+
image,
|
|
447
|
+
t2star,
|
|
448
|
+
str(t2star_file),
|
|
449
|
+
dtype=np.float32,
|
|
450
|
+
descrip="MRIForge T2* map (ms)",
|
|
451
|
+
intent_name="T2star",
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
save_scalar_map_like(
|
|
455
|
+
image,
|
|
456
|
+
s0,
|
|
457
|
+
str(s0_file),
|
|
458
|
+
dtype=np.float32,
|
|
459
|
+
descrip="MRIForge S0 map",
|
|
460
|
+
intent_name="S0",
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
print("Done.")
|
|
464
|
+
print(f"T2*: {t2star_file}")
|
|
465
|
+
print(f"S0: {s0_file}")
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
if __name__ == "__main__":
|
|
469
|
+
main()
|