mrzerocore 0.3.2__cp37-abi3-win32.whl → 0.4.4__cp37-abi3-win32.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.
- MRzeroCore/__init__.py +3 -0
- MRzeroCore/_prepass.pyd +0 -0
- MRzeroCore/phantom/brainweb/.gitignore +1 -0
- MRzeroCore/phantom/brainweb/__init__.py +146 -125
- MRzeroCore/phantom/brainweb/brainweb_data.json +39 -40
- MRzeroCore/phantom/custom_voxel_phantom.py +19 -46
- MRzeroCore/phantom/nifti_phantom.py +210 -0
- MRzeroCore/phantom/sim_data.py +8 -10
- MRzeroCore/phantom/tissue_dict.py +275 -0
- MRzeroCore/phantom/voxel_grid_phantom.py +53 -25
- MRzeroCore/sequence.py +27 -6
- MRzeroCore/simulation/isochromat_sim.py +6 -6
- MRzeroCore/simulation/main_pass.py +43 -49
- MRzeroCore/simulation/pre_pass.py +45 -12
- MRzeroCore/simulation/sig_to_mrd.py +362 -0
- MRzeroCore/util.py +487 -18
- {MRzeroCore-0.3.2.dist-info → mrzerocore-0.4.4.dist-info}/METADATA +47 -14
- mrzerocore-0.4.4.dist-info/RECORD +40 -0
- {MRzeroCore-0.3.2.dist-info → mrzerocore-0.4.4.dist-info}/WHEEL +1 -1
- MRzeroCore-0.3.2.dist-info/RECORD +0 -36
- {MRzeroCore-0.3.2.dist-info → mrzerocore-0.4.4.dist-info}/licenses/LICENSE +0 -0
MRzeroCore/__init__.py
CHANGED
|
@@ -11,9 +11,12 @@ from .phantom.voxel_grid_phantom import VoxelGridPhantom
|
|
|
11
11
|
from .phantom.custom_voxel_phantom import CustomVoxelPhantom
|
|
12
12
|
from .phantom.sim_data import SimData
|
|
13
13
|
from .phantom.brainweb import generate_brainweb_phantoms
|
|
14
|
+
from .phantom.nifti_phantom import NiftiPhantom, NiftiTissue, NiftiRef, NiftiMapping
|
|
15
|
+
from .phantom.tissue_dict import TissueDict
|
|
14
16
|
from .simulation.isochromat_sim import isochromat_sim
|
|
15
17
|
from .simulation.pre_pass import compute_graph, compute_graph_ext, Graph
|
|
16
18
|
from .simulation.main_pass import execute_graph
|
|
19
|
+
from .simulation.sig_to_mrd import sig_to_mrd
|
|
17
20
|
from .reconstruction import reco_adjoint
|
|
18
21
|
from .pulseq.exporter import pulseq_write_cartesian
|
|
19
22
|
from . import util
|
MRzeroCore/_prepass.pyd
CHANGED
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
brainweb*/
|
|
@@ -1,25 +1,18 @@
|
|
|
1
|
-
from typing import Literal
|
|
2
|
-
import json
|
|
3
|
-
import gzip
|
|
4
|
-
import requests
|
|
5
|
-
import os
|
|
6
1
|
import numpy as np
|
|
2
|
+
from pathlib import Path
|
|
7
3
|
|
|
8
4
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
def load_tissue(subject: int, alias: str, cache_dir: Path) -> np.ndarray:
|
|
6
|
+
import os
|
|
7
|
+
import requests
|
|
8
|
+
import gzip
|
|
13
9
|
|
|
14
|
-
|
|
15
|
-
def load_tissue(subject: int, alias: str, cache_dir: str) -> np.ndarray:
|
|
16
10
|
download_alias = f"subject{subject:02d}_{alias}"
|
|
17
11
|
file_name = download_alias + ".i8.gz" # 8 bit signed int, gnuzip
|
|
18
|
-
file_path =
|
|
12
|
+
file_path = cache_dir / file_name
|
|
19
13
|
|
|
20
14
|
# Download and cache file if it doesn't exist yet
|
|
21
15
|
if not os.path.exists(file_path):
|
|
22
|
-
print(f"Downloading '{download_alias}'", end="", flush=True)
|
|
23
16
|
response = requests.post(
|
|
24
17
|
"https://brainweb.bic.mni.mcgill.ca/cgi/brainweb1",
|
|
25
18
|
data={
|
|
@@ -30,75 +23,56 @@ def load_tissue(subject: int, alias: str, cache_dir: str) -> np.ndarray:
|
|
|
30
23
|
)
|
|
31
24
|
with open(file_path, "wb") as f:
|
|
32
25
|
f.write(response.content)
|
|
33
|
-
print(" - ", end="")
|
|
34
26
|
|
|
35
27
|
# Load the raw BrainWeb data and add it to the return array
|
|
36
28
|
with gzip.open(file_path) as f:
|
|
37
|
-
print(f"Loading {os.path.basename(file_path)}", end="", flush=True)
|
|
38
29
|
# BrainWeb says this data is unsigned, which is a lie
|
|
39
30
|
tmp = np.frombuffer(f.read(), np.uint8) + 128
|
|
40
31
|
|
|
41
32
|
# Vessel bugfix: most of background is 1 instead of zero
|
|
42
33
|
if alias == "ves":
|
|
43
34
|
tmp[tmp == 1] = 0
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return data / 255.0
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def gen_noise(range: float, res: np.ndarray) -> np.ndarray:
|
|
51
|
-
if range == 0:
|
|
52
|
-
return 1
|
|
53
|
-
else:
|
|
54
|
-
freq = 20
|
|
55
|
-
padded_res = (res + freq - 1) // freq * freq
|
|
56
|
-
try:
|
|
57
|
-
from perlin_numpy import generate_perlin_noise_3d
|
|
58
|
-
noise = generate_perlin_noise_3d(padded_res, (freq, freq, freq))
|
|
59
|
-
except:
|
|
60
|
-
print("perlin_numpy@git+https://github.com/pvigier/perlin-numpy")
|
|
61
|
-
print("is not installed, falling back to numpy.random.random()")
|
|
62
|
-
noise = np.random.random(padded_res)
|
|
63
|
-
return 1 + range * noise[:res[0], :res[1], :res[2]]
|
|
64
|
-
|
|
35
|
+
|
|
36
|
+
# Convert to RAS+ [x, y, z] indexing and [0..1] range
|
|
37
|
+
data = tmp.reshape(362, 434, 362).swapaxes(0, 2).astype(np.float32) / 255.0
|
|
65
38
|
|
|
66
|
-
|
|
67
|
-
# crop array to multiple of factor
|
|
68
|
-
shape = (np.array(array.shape) // factor) * factor
|
|
69
|
-
array = array[:shape[0], :shape[1], :shape[2]]
|
|
39
|
+
return data
|
|
70
40
|
|
|
71
|
-
tmp = np.zeros(shape // factor)
|
|
72
|
-
for x in range(factor):
|
|
73
|
-
for y in range(factor):
|
|
74
|
-
for z in range(factor):
|
|
75
|
-
tmp += array[x::factor, y::factor, z::factor]
|
|
76
41
|
|
|
77
|
-
|
|
42
|
+
def generate_B0_B1(mask):
|
|
43
|
+
"""Generate a somewhat plausible B0 and B1 map.
|
|
78
44
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
45
|
+
Visually fitted to look similar to the numerical_brain_cropped
|
|
46
|
+
"""
|
|
47
|
+
x_pos, y_pos, z_pos = np.meshgrid(
|
|
48
|
+
np.linspace(-1, 1, mask.shape[0]),
|
|
49
|
+
np.linspace(-1, 1, mask.shape[1]),
|
|
50
|
+
np.linspace(-1, 1, mask.shape[2]),
|
|
51
|
+
indexing="ij"
|
|
52
|
+
)
|
|
53
|
+
B1 = np.exp(-(0.4*x_pos**2 + 0.2*y_pos**2 + 0.3*z_pos**2))
|
|
54
|
+
dist2 = (0.4*x_pos**2 + 0.2*(y_pos - 0.7)**2 + 0.3*z_pos**2)
|
|
55
|
+
B0 = 7 / (0.05 + dist2) - 45 / (0.3 + dist2)
|
|
56
|
+
|
|
57
|
+
# Normalize such that the average over the mask is 0 or 1
|
|
58
|
+
|
|
59
|
+
B0 -= B0[mask].mean()
|
|
60
|
+
B1 /= B1[mask].mean()
|
|
61
|
+
B0[~mask] = 0
|
|
62
|
+
B1[~mask] = 0
|
|
63
|
+
|
|
64
|
+
return B0, B1
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def generate_brainweb_phantoms(output_dir: str, subject_count: int | None = None):
|
|
83
68
|
"""Generate BrainWeb phantom maps for the selected configuration.
|
|
84
69
|
|
|
85
70
|
Raw tissue segmentation data is provided by the BrainWeb Database:
|
|
86
71
|
http://www.bic.mni.mcgill.ca/brainweb/
|
|
87
72
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
to use, which tissues to include, and the downsampling and noise levels.
|
|
92
|
-
|
|
93
|
-
The emitted files are compressed numpy files, which can be loaded with
|
|
94
|
-
``np.load(file_name)``. They contain the following arrays:
|
|
95
|
-
|
|
96
|
-
- `PD_map`: Proton Density [a.u.]
|
|
97
|
-
- `T1_map`: T1 relaxation time [s]
|
|
98
|
-
- `T2_map`: T2 relaxation time [s]
|
|
99
|
-
- `T2dash_map`: T2' relaxation time [s]
|
|
100
|
-
- `D_map`: Isotropic Diffusion coefficient [10^-3 mm² / s]
|
|
101
|
-
- `tissue_XY`: Tissue segmentation for all included tissues
|
|
73
|
+
The generated phantoms are stored as MR-zero
|
|
74
|
+
[NIfTI phantoms](https://mrsources.github.io/MRzero-Core/nifti-overview.html).
|
|
75
|
+
Settings for the generated phantoms are stored in `brainweb_data.json`.
|
|
102
76
|
|
|
103
77
|
Parameters
|
|
104
78
|
----------
|
|
@@ -107,65 +81,112 @@ def generate_brainweb_phantoms(
|
|
|
107
81
|
addition, a `cache` folder will be generated there too, which contains
|
|
108
82
|
all the data downloaded from BrainWeb to avoid repeating the download
|
|
109
83
|
for all configurations or when generating phantoms again.
|
|
110
|
-
|
|
111
|
-
|
|
84
|
+
subject_count: int
|
|
85
|
+
Number of phantoms to generate. BrainWeb provides 20 different segmented
|
|
86
|
+
phantoms. If you don't need as many, you can lower this number to only
|
|
87
|
+
generate the first `count` phantoms. If `None`, all phantoms are generated.
|
|
112
88
|
"""
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
89
|
+
import os
|
|
90
|
+
import json
|
|
91
|
+
from tqdm import tqdm
|
|
92
|
+
import nibabel as nib
|
|
93
|
+
from ..nifti_phantom import NiftiPhantom, NiftiTissue, NiftiRef, NiftiMapping
|
|
94
|
+
|
|
95
|
+
cache_dir = Path(output_dir) / "cache"
|
|
96
|
+
config_file = Path(__file__).parent / "brainweb_data.json"
|
|
97
|
+
os.makedirs(cache_dir, exist_ok=True)
|
|
98
|
+
|
|
99
|
+
# Load the brainweb data file that contains info about tissues and subjects
|
|
100
|
+
with open(config_file) as f:
|
|
101
|
+
config = json.load(f)
|
|
102
|
+
|
|
103
|
+
if subject_count is None:
|
|
104
|
+
subject_count = len(config["subjects"])
|
|
105
|
+
|
|
106
|
+
# Voxel index to physical coordinates in millimeters:
|
|
107
|
+
# Brainweb has 0.5 mm voxel size; we center the brain.
|
|
108
|
+
affine = np.array(
|
|
109
|
+
[
|
|
110
|
+
[0.5, 0, 0, -90.5], # X: Right
|
|
111
|
+
[0, 0.5, 0, -108.5], # Y: Anterior
|
|
112
|
+
[0, 0, 0.5, -90.5], # Z: Superior
|
|
113
|
+
[0, 0, 0, 0], # ignored
|
|
114
|
+
]
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
number_of_downloads = subject_count * sum(len(maps) for maps in config["download-aliases"].values())
|
|
119
|
+
number_of_saves = subject_count * 3 # density, B0, B1
|
|
120
|
+
number_of_jsons = subject_count * len(config["fields"])
|
|
121
|
+
total = number_of_downloads + number_of_saves + number_of_jsons
|
|
122
|
+
|
|
123
|
+
with tqdm(total=total) as pbar:
|
|
124
|
+
for subject in config["subjects"][:subject_count]:
|
|
125
|
+
phantom_name = f"brainweb-subj{subject:02d}"
|
|
126
|
+
phantom_dir = Path(output_dir) / phantom_name
|
|
127
|
+
|
|
128
|
+
pbar.set_description(str(phantom_dir))
|
|
129
|
+
os.makedirs(phantom_dir, exist_ok=True)
|
|
130
|
+
|
|
131
|
+
tissue_indices = {}
|
|
132
|
+
density_maps = []
|
|
133
|
+
for tissue, density in config["density"].items():
|
|
134
|
+
density_map = 0
|
|
135
|
+
|
|
136
|
+
for alias in config["download-aliases"][tissue]:
|
|
137
|
+
pbar.set_postfix_str(f"loading 'subject{subject:02d}_{alias}'")
|
|
138
|
+
density_map += density * load_tissue(subject, alias, cache_dir)
|
|
139
|
+
pbar.update()
|
|
140
|
+
|
|
141
|
+
tissue_indices[tissue] = len(density_maps)
|
|
142
|
+
density_maps.append(density_map)
|
|
143
|
+
|
|
144
|
+
pbar.set_postfix_str(f"saving '{phantom_name}.nii.gz'")
|
|
145
|
+
nib.loadsave.save(
|
|
146
|
+
nib.nifti1.Nifti1Image(np.stack(density_maps, -1), affine),
|
|
147
|
+
phantom_dir / f"{phantom_name}.nii.gz"
|
|
148
|
+
)
|
|
149
|
+
pbar.update()
|
|
150
|
+
|
|
151
|
+
B0, B1 = generate_B0_B1(sum(density_maps) > 0)
|
|
152
|
+
pbar.set_postfix_str(f"saving '{phantom_name}_dB0.nii.gz'")
|
|
153
|
+
nib.loadsave.save(
|
|
154
|
+
nib.nifti1.Nifti1Image(B0[..., None], affine),
|
|
155
|
+
phantom_dir / f"{phantom_name}_dB0.nii.gz"
|
|
156
|
+
)
|
|
157
|
+
pbar.update()
|
|
158
|
+
pbar.set_postfix_str(f"saving '{phantom_name}_B1+.nii.gz'")
|
|
159
|
+
nib.loadsave.save(
|
|
160
|
+
nib.nifti1.Nifti1Image(B1[..., None], affine),
|
|
161
|
+
phantom_dir / f"{phantom_name}_B1+.nii.gz"
|
|
162
|
+
)
|
|
163
|
+
pbar.update()
|
|
164
|
+
|
|
165
|
+
for field in config["fields"]:
|
|
166
|
+
phantom = NiftiPhantom.default(B0=field)
|
|
167
|
+
pbar.set_postfix_str(f"saving '{phantom_name}-{field}T.json'")
|
|
168
|
+
density_maps = []
|
|
169
|
+
|
|
170
|
+
for tissue in config["tissues"]:
|
|
171
|
+
tissue_config = config["props"][str(field)][tissue]
|
|
172
|
+
|
|
173
|
+
phantom.tissues[tissue] = NiftiTissue(
|
|
174
|
+
density=NiftiRef(Path(f"{phantom_name}.nii.gz"), tissue_indices[tissue]),
|
|
175
|
+
T1=tissue_config["T1"],
|
|
176
|
+
T2=tissue_config["T2"],
|
|
177
|
+
T2dash=tissue_config["T2'"],
|
|
178
|
+
ADC=tissue_config["ADC"],
|
|
179
|
+
dB0=NiftiRef(Path(f"{phantom_name}_dB0.nii.gz"), 0),
|
|
180
|
+
B1_tx=[NiftiRef(Path(f"{phantom_name}_B1+.nii.gz"), 0)],
|
|
181
|
+
B1_rx=[1.0],
|
|
182
|
+
)
|
|
183
|
+
if tissue == "fat":
|
|
184
|
+
phantom.tissues["fat"].dB0 = NiftiMapping(
|
|
185
|
+
file=NiftiRef(Path(f"{phantom_name}_dB0.nii.gz"), 0),
|
|
186
|
+
func=config["dB0-fat-remap"][str(field)]
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
phantom.save(phantom_dir / f"{phantom_name}-{field}T.json")
|
|
190
|
+
pbar.update()
|
|
191
|
+
|
|
192
|
+
pbar.close()
|
|
@@ -1,92 +1,91 @@
|
|
|
1
1
|
{
|
|
2
|
-
"configs": {
|
|
3
|
-
"3T": {
|
|
4
|
-
"field-strength": "3T",
|
|
5
|
-
"tissues": ["gm", "wm", "csf"],
|
|
6
|
-
"downsample": 3,
|
|
7
|
-
"noise": 0
|
|
8
|
-
},
|
|
9
|
-
"7T-noise": {
|
|
10
|
-
"field-strength": "7T",
|
|
11
|
-
"tissues": ["gm", "wm", "csf"],
|
|
12
|
-
"downsample": 3,
|
|
13
|
-
"noise": 0.2
|
|
14
|
-
},
|
|
15
|
-
"3T-highres-fat": {
|
|
16
|
-
"field-strength": "3T",
|
|
17
|
-
"tissues": ["gm", "wm", "csf", "fat"],
|
|
18
|
-
"downsample": 1,
|
|
19
|
-
"noise": 0
|
|
20
|
-
}
|
|
21
|
-
},
|
|
22
2
|
"subjects": [
|
|
23
3
|
4, 5, 6, 18, 20, 38, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54
|
|
24
4
|
],
|
|
5
|
+
"fields": [3, 7],
|
|
6
|
+
"tissues": ["gm", "wm", "csf", "vessels", "fat"],
|
|
25
7
|
"download-aliases": {
|
|
26
8
|
"gm": ["gry"],
|
|
27
9
|
"wm": ["wht"],
|
|
28
|
-
"csf": ["csf"
|
|
10
|
+
"csf": ["csf"],
|
|
11
|
+
"vessels": ["ves"],
|
|
29
12
|
"fat": ["fat", "mus", "m-s", "dura", "fat2"]
|
|
30
13
|
},
|
|
31
|
-
"
|
|
32
|
-
"
|
|
14
|
+
"dB0-fat-remap" : {
|
|
15
|
+
"3": "x - 440",
|
|
16
|
+
"7": "x - 1020"
|
|
17
|
+
},
|
|
18
|
+
"density": {
|
|
19
|
+
"gm": 0.8,
|
|
20
|
+
"wm": 0.7,
|
|
21
|
+
"csf": 1.0,
|
|
22
|
+
"vessels": 1.0,
|
|
23
|
+
"fat": 1.0
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
"props": {
|
|
27
|
+
"3": {
|
|
33
28
|
"gm": {
|
|
34
|
-
"PD": 0.8,
|
|
35
29
|
"T1": 1.56,
|
|
36
30
|
"T2": 0.083,
|
|
37
31
|
"T2'": 0.32,
|
|
38
|
-
"
|
|
32
|
+
"ADC": 0.83
|
|
39
33
|
},
|
|
40
34
|
"wm": {
|
|
41
|
-
"PD": 0.7,
|
|
42
35
|
"T1": 0.83,
|
|
43
36
|
"T2": 0.075,
|
|
44
37
|
"T2'": 0.18,
|
|
45
|
-
"
|
|
38
|
+
"ADC": 0.65
|
|
46
39
|
},
|
|
47
40
|
"csf": {
|
|
48
|
-
"PD": 1,
|
|
49
41
|
"T1": 4.16,
|
|
50
42
|
"T2": 1.65,
|
|
51
43
|
"T2'": 0.059,
|
|
52
|
-
"
|
|
44
|
+
"ADC": 3.19
|
|
45
|
+
},
|
|
46
|
+
"vessels": {
|
|
47
|
+
"T1": 4.16,
|
|
48
|
+
"T2": 1.65,
|
|
49
|
+
"T2'": 0.059,
|
|
50
|
+
"ADC": 3.19
|
|
53
51
|
},
|
|
54
52
|
"fat": {
|
|
55
|
-
"PD": 1,
|
|
56
53
|
"T1": 0.37,
|
|
57
54
|
"T2": 0.125,
|
|
58
55
|
"T2'": 0.012,
|
|
59
|
-
"
|
|
56
|
+
"ADC": 0.1
|
|
60
57
|
}
|
|
61
58
|
},
|
|
62
|
-
"
|
|
59
|
+
"7": {
|
|
63
60
|
"gm": {
|
|
64
|
-
"PD": 0.8,
|
|
65
61
|
"T1": 1.67,
|
|
66
62
|
"T2": 0.043,
|
|
67
63
|
"T2'": 0.82,
|
|
68
|
-
"
|
|
64
|
+
"ADC": 0.83
|
|
69
65
|
},
|
|
70
66
|
"wm": {
|
|
71
|
-
"PD": 0.7,
|
|
72
67
|
"T1": 1.22,
|
|
73
68
|
"T2": 0.037,
|
|
74
69
|
"T2'": 0.65,
|
|
75
|
-
"
|
|
70
|
+
"ADC": 0.65
|
|
76
71
|
},
|
|
77
72
|
"csf": {
|
|
78
|
-
"PD": 1,
|
|
79
73
|
"T1": 4.0,
|
|
80
74
|
"T2": 0.8,
|
|
81
75
|
"T2'": 0.204,
|
|
82
|
-
"
|
|
76
|
+
"ADC": 3.19
|
|
77
|
+
},
|
|
78
|
+
"vessels": {
|
|
79
|
+
"T1": 4.0,
|
|
80
|
+
"T2": 0.8,
|
|
81
|
+
"T2'": 0.204,
|
|
82
|
+
"ADC": 3.19
|
|
83
83
|
},
|
|
84
84
|
"fat": {
|
|
85
|
-
"PD": 1,
|
|
86
85
|
"T1": 0.374,
|
|
87
86
|
"T2": 0.125,
|
|
88
87
|
"T2'": 0.0117,
|
|
89
|
-
"
|
|
88
|
+
"ADC": 0.1
|
|
90
89
|
}
|
|
91
90
|
}
|
|
92
91
|
}
|
|
@@ -148,32 +148,9 @@ class CustomVoxelPhantom:
|
|
|
148
148
|
|
|
149
149
|
def generate_PD_map(self) -> torch.Tensor:
|
|
150
150
|
"""Convenience function for MRTwin_pulseq to generate a PD map."""
|
|
151
|
-
|
|
152
|
-
torch.linspace(-64, 63, 128),
|
|
153
|
-
torch.linspace(-64, 63, 128),
|
|
154
|
-
)
|
|
155
|
-
trajectory = torch.stack([
|
|
156
|
-
kx.flatten(),
|
|
157
|
-
ky.flatten(),
|
|
158
|
-
torch.zeros(kx.numel()),
|
|
159
|
-
], dim=1)
|
|
160
|
-
PD_kspace = torch.zeros(128, 128, dtype=torch.cfloat)
|
|
161
|
-
|
|
162
|
-
# All voxels have the same shape -> same intra-voxel dephasing
|
|
163
|
-
dephasing = build_dephasing_func(
|
|
164
|
-
self.voxel_shape, self.voxel_size
|
|
165
|
-
)(trajectory, None)
|
|
166
|
-
|
|
167
|
-
# Iterate over all voxels and render them into the kspaces
|
|
168
|
-
for i in range(self.PD.numel()):
|
|
169
|
-
rot = torch.exp(-2j*pi * (trajectory @ self.voxel_pos[i, :]))
|
|
170
|
-
kspace = (rot * dephasing).view(128, 128)
|
|
171
|
-
PD_kspace += kspace * self.PD[i]
|
|
172
|
-
|
|
173
|
-
return torch.fft.fftshift(torch.fft.ifft2(PD_kspace)).abs()[:, :, None]
|
|
151
|
+
return self.generate_maps([self.PD, ])[0].abs()[:, :, None]
|
|
174
152
|
|
|
175
|
-
def
|
|
176
|
-
"""Print and plot all data stored in this phantom."""
|
|
153
|
+
def generate_maps(self, props) -> list[torch.Tensor]:
|
|
177
154
|
# Best way to accurately plot this is to generate a k-space -> FFT
|
|
178
155
|
# We only render a 2D image with a FOV of 1
|
|
179
156
|
kx, ky = torch.meshgrid(
|
|
@@ -185,34 +162,30 @@ class CustomVoxelPhantom:
|
|
|
185
162
|
ky.flatten(),
|
|
186
163
|
torch.zeros(kx.numel()),
|
|
187
164
|
], dim=1)
|
|
188
|
-
PD_kspace = torch.zeros(128, 128, dtype=torch.cfloat)
|
|
189
|
-
T1_kspace = torch.zeros(128, 128, dtype=torch.cfloat)
|
|
190
|
-
T2_kspace = torch.zeros(128, 128, dtype=torch.cfloat)
|
|
191
|
-
T2dash_kspace = torch.zeros(128, 128, dtype=torch.cfloat)
|
|
192
|
-
D_kspace = torch.zeros(128, 128, dtype=torch.cfloat)
|
|
193
165
|
|
|
194
166
|
# All voxels have the same shape -> same intra-voxel dephasing
|
|
195
167
|
dephasing = build_dephasing_func(
|
|
196
168
|
self.voxel_shape, self.voxel_size
|
|
197
169
|
)(trajectory, None)
|
|
198
170
|
|
|
171
|
+
kspaces = [torch.zeros(128, 128, dtype=torch.cfloat) for _ in range(len(props))]
|
|
199
172
|
# Iterate over all voxels and render them into the kspaces
|
|
200
|
-
for
|
|
201
|
-
rot = torch.exp(-2j*pi * (trajectory @
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
maps = [PD, T1, T2, T2dash, D]
|
|
173
|
+
for pos in self.voxel_pos:
|
|
174
|
+
rot = torch.exp(-2j*pi * (trajectory @ pos))
|
|
175
|
+
|
|
176
|
+
for i in range(len(props)):
|
|
177
|
+
kspaces[i] += props[i] * (rot * dephasing).view(128, 128)
|
|
178
|
+
|
|
179
|
+
# FFT the rendered k-spaces to get the maps
|
|
180
|
+
norm = self.voxel_size[0] * self.voxel_size[1] # 2D plot, ignore thickness
|
|
181
|
+
return [
|
|
182
|
+
torch.fft.fftshift(torch.fft.ifft2(kspace * norm, norm="forward"))
|
|
183
|
+
for kspace in kspaces
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
def plot(self) -> None:
|
|
187
|
+
"""Print and plot all data stored in this phantom."""
|
|
188
|
+
maps = self.generate_maps([self.PD, self.T1, self.T2, self.T2dash, self.D])
|
|
216
189
|
titles = ["PD", "T1", "T2", "T2'", "D"]
|
|
217
190
|
|
|
218
191
|
print("CustomVoxelPhantom")
|