MRPhantom 2.2.0__tar.gz → 2.2.1__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.
- {mrphantom-2.2.0 → mrphantom-2.2.1/MRPhantom.egg-info}/PKG-INFO +1 -1
- {mrphantom-2.2.0 → mrphantom-2.2.1}/MRPhantom.egg-info/SOURCES.txt +5 -1
- {mrphantom-2.2.0/MRPhantom.egg-info → mrphantom-2.2.1}/PKG-INFO +1 -1
- mrphantom-2.2.1/mrphantom_src/Function.py +372 -0
- mrphantom-2.2.1/mrphantom_src/__init__.py +7 -0
- {mrphantom-2.2.0 → mrphantom-2.2.1}/mrphantom_src/ext/main.cpp +15 -13
- {mrphantom-2.2.0 → mrphantom-2.2.1}/mrphantom_src/ext/slime.cpp +31 -34
- {mrphantom-2.2.0 → mrphantom-2.2.1}/mrphantom_src/ext/slime.h +2 -2
- mrphantom-2.2.1/mrphantom_src/nmr_para.xml +42 -0
- mrphantom-2.2.1/mrphantom_src/utility.py +12 -0
- {mrphantom-2.2.0 → mrphantom-2.2.1}/pyproject.toml +1 -1
- mrphantom-2.2.1/test/test.py +15 -0
- mrphantom-2.2.1/test/test_Simple.py +22 -0
- mrphantom-2.2.0/mrphantom_src/Function.py +0 -436
- mrphantom-2.2.0/mrphantom_src/__init__.py +0 -5
- {mrphantom-2.2.0 → mrphantom-2.2.1}/LICENSE +0 -0
- {mrphantom-2.2.0 → mrphantom-2.2.1}/MANIFEST.in +0 -0
- {mrphantom-2.2.0 → mrphantom-2.2.1}/MRPhantom.egg-info/dependency_links.txt +0 -0
- {mrphantom-2.2.0 → mrphantom-2.2.1}/MRPhantom.egg-info/requires.txt +0 -0
- {mrphantom-2.2.0 → mrphantom-2.2.1}/MRPhantom.egg-info/top_level.txt +0 -0
- {mrphantom-2.2.0 → mrphantom-2.2.1}/README.md +0 -0
- {mrphantom-2.2.0 → mrphantom-2.2.1}/mrphantom_src/__pycache__/Function.cpython-312.pyc +0 -0
- {mrphantom-2.2.0 → mrphantom-2.2.1}/mrphantom_src/__pycache__/Type.cpython-312.pyc +0 -0
- {mrphantom-2.2.0 → mrphantom-2.2.1}/mrphantom_src/__pycache__/Utility.cpython-312.pyc +0 -0
- {mrphantom-2.2.0 → mrphantom-2.2.1}/mrphantom_src/__pycache__/__init__.cpython-312.pyc +0 -0
- {mrphantom-2.2.0 → mrphantom-2.2.1}/setup.cfg +0 -0
- {mrphantom-2.2.0 → mrphantom-2.2.1}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MRPhantom
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.1
|
|
4
4
|
Summary: Volumetric dynamic MRI Phantom of a Slime with respiratory and cardiac motion, and M0, phase, T1, T2, B0, coil sensitivity maps, boosted by a parallel C-API Backend.
|
|
5
5
|
Author-email: Ryan <ryan_shanghaitech@proton.me>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -12,10 +12,14 @@ MRPhantom.egg-info/requires.txt
|
|
|
12
12
|
MRPhantom.egg-info/top_level.txt
|
|
13
13
|
mrphantom_src/Function.py
|
|
14
14
|
mrphantom_src/__init__.py
|
|
15
|
+
mrphantom_src/nmr_para.xml
|
|
16
|
+
mrphantom_src/utility.py
|
|
15
17
|
mrphantom_src/__pycache__/Function.cpython-312.pyc
|
|
16
18
|
mrphantom_src/__pycache__/Type.cpython-312.pyc
|
|
17
19
|
mrphantom_src/__pycache__/Utility.cpython-312.pyc
|
|
18
20
|
mrphantom_src/__pycache__/__init__.cpython-312.pyc
|
|
19
21
|
mrphantom_src/ext/main.cpp
|
|
20
22
|
mrphantom_src/ext/slime.cpp
|
|
21
|
-
mrphantom_src/ext/slime.h
|
|
23
|
+
mrphantom_src/ext/slime.h
|
|
24
|
+
test/test.py
|
|
25
|
+
test/test_Simple.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MRPhantom
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.1
|
|
4
4
|
Summary: Volumetric dynamic MRI Phantom of a Slime with respiratory and cardiac motion, and M0, phase, T1, T2, B0, coil sensitivity maps, boosted by a parallel C-API Backend.
|
|
5
5
|
Author-email: Ryan <ryan_shanghaitech@proton.me>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
from . import ext
|
|
2
|
+
from numpy import *
|
|
3
|
+
from numpy.fft import fftn, ifftn, fftshift, ifftshift
|
|
4
|
+
from numpy.typing import NDArray
|
|
5
|
+
from typing import *
|
|
6
|
+
from scipy.signal.windows import gaussian
|
|
7
|
+
from scipy.ndimage import gaussian_filter
|
|
8
|
+
from . import dictNmrPara, lstTissue
|
|
9
|
+
|
|
10
|
+
def genPhant(shape:Tuple, ampRes:float=0, ampCar:float=0) -> NDArray: # call C++ backend to generate a phantom
|
|
11
|
+
"""
|
|
12
|
+
generate a phantom in Enum type
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
shape: shape of the phantom (independent from FOV)
|
|
16
|
+
ampRes: respiratory motion amplitude
|
|
17
|
+
ampCar: cardiac motion amplitude
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
NDArray contains elements in `Tissue` enum type
|
|
21
|
+
"""
|
|
22
|
+
nAx = len(shape)
|
|
23
|
+
shape = (1,)*(3-len(shape)) + shape
|
|
24
|
+
return ext.genPhant(nAx, *shape, ampRes, ampCar)
|
|
25
|
+
|
|
26
|
+
def LPF(arr:NDArray, std:float) -> NDArray:
|
|
27
|
+
lstWind = []
|
|
28
|
+
for l in arr.shape:
|
|
29
|
+
lstWind.append(gaussian(l, 1/(2*pi*std)))
|
|
30
|
+
wind = ones(arr.shape)
|
|
31
|
+
for a, w in enumerate(lstWind):
|
|
32
|
+
shape = [1] * arr.ndim
|
|
33
|
+
shape[a] = len(w)
|
|
34
|
+
wind *= w.reshape(shape)
|
|
35
|
+
|
|
36
|
+
arr = fftshift(fftn(ifftshift(arr)))
|
|
37
|
+
arr *= wind
|
|
38
|
+
arr = fftshift(ifftn(ifftshift(arr)))
|
|
39
|
+
return arr
|
|
40
|
+
|
|
41
|
+
def genPhMap(shape:Tuple, mean:int|float|None=None, std:int|float=pi/16) -> NDArray:
|
|
42
|
+
"""
|
|
43
|
+
generate random phase map
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
shape: shape of the phantom (independent from FOV)
|
|
47
|
+
mean: mean of the noise
|
|
48
|
+
std: std of the noise
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
smooth complex noise with unity magnitude
|
|
52
|
+
"""
|
|
53
|
+
if mean is None: mean = random.uniform(-pi,pi)
|
|
54
|
+
mapPh = random.uniform(-pi, pi, shape)
|
|
55
|
+
mapPh = LPF(mapPh, 1/4).real
|
|
56
|
+
# normalize
|
|
57
|
+
mapPh -= mapPh.mean(); mapPh = asarray(mapPh)
|
|
58
|
+
mapPh /= mapPh.std()
|
|
59
|
+
mapPh *= std
|
|
60
|
+
mapPh += mean
|
|
61
|
+
# convert to rotation factor
|
|
62
|
+
mapPh = exp(1j*mapPh)
|
|
63
|
+
mapPh = mapPh/abs(mapPh)
|
|
64
|
+
return mapPh
|
|
65
|
+
|
|
66
|
+
def genB0Map(shape:Tuple, mean:int|float=0, std:int|float=1e-6*(2*pi*42.58e6*3)) -> NDArray:
|
|
67
|
+
"""
|
|
68
|
+
generate random B0 map
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
shape: shape of the phantom (independent from FOV)
|
|
72
|
+
mean: mean of the noise
|
|
73
|
+
std: std of the noise
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
smooth random noise in `rad/s`
|
|
77
|
+
"""
|
|
78
|
+
mapB0 = random.uniform(-1, 1, shape)
|
|
79
|
+
mapB0 = LPF(mapB0, 1/4).real
|
|
80
|
+
# normalize
|
|
81
|
+
mapB0 -= mapB0.mean(); mapB0 = asarray(mapB0)
|
|
82
|
+
mapB0 /= mapB0.std()
|
|
83
|
+
mapB0 *= std
|
|
84
|
+
mapB0 += mean
|
|
85
|
+
return mapB0
|
|
86
|
+
|
|
87
|
+
def genCsm(shape:Tuple, nCh:int=12, mean:int|float|None=None, std:int|float=pi/16) -> NDArray:
|
|
88
|
+
"""
|
|
89
|
+
generate random coil sensitivity map
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
shape: shape of the phantom (independent from FOV)
|
|
93
|
+
nCh: number of coils
|
|
94
|
+
mean: mean of the noise
|
|
95
|
+
std: std of the noise
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
complex smooth and inhomogeneous map
|
|
99
|
+
"""
|
|
100
|
+
if mean is None: mean = random.uniform(-pi,pi)
|
|
101
|
+
nAx = len(shape)
|
|
102
|
+
mapC = zeros([nCh,*shape], dtype=complex128)
|
|
103
|
+
arrCoor = meshgrid\
|
|
104
|
+
(
|
|
105
|
+
*(linspace(-0.5,0.5,s,0) for s in shape),
|
|
106
|
+
indexing="ij"
|
|
107
|
+
); arrCoor = array(arrCoor).transpose(*arange(1,nAx+1), 0)
|
|
108
|
+
arrTht = linspace(0,2*pi,nCh,0)
|
|
109
|
+
arrCoorCoil = zeros([nCh,nAx], dtype=float64)
|
|
110
|
+
arrCoorCoil[:,-2:] = 1*array([sin(arrTht), cos(arrTht)]).T
|
|
111
|
+
if nAx == 3:
|
|
112
|
+
arrCoorCoil[0::2,0] = 0.2
|
|
113
|
+
arrCoorCoil[1::2,0] = -0.2
|
|
114
|
+
for iCh in range(nCh):
|
|
115
|
+
mapC[iCh] = genPhMap(shape, mean=mean, std=std)
|
|
116
|
+
dist = sqrt(sum((arrCoor - arrCoorCoil[iCh])**2, axis=-1))
|
|
117
|
+
mapC[iCh] *= exp(-dist)
|
|
118
|
+
return mapC
|
|
119
|
+
|
|
120
|
+
def genAmp(tScan:int|float, tRes:int|float, cyc:int|float, isRand:bool=True) -> NDArray:
|
|
121
|
+
"""
|
|
122
|
+
generate amplitude curve
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
tScan: length of the signal in `s`
|
|
126
|
+
tRes: temporal resolution in `s`
|
|
127
|
+
cyc: period of the signal in `s`
|
|
128
|
+
isRand: make the signal have irregular period
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
generated amplitude
|
|
132
|
+
"""
|
|
133
|
+
nT = around(tScan/tRes).astype(int)
|
|
134
|
+
|
|
135
|
+
if isRand:
|
|
136
|
+
arrT = sort(random.rand(nT)*tScan)
|
|
137
|
+
arrAmp = sin(2*pi/cyc*arrT)
|
|
138
|
+
|
|
139
|
+
sigma = cyc/tRes/8
|
|
140
|
+
arrAmp = gaussian_filter(arrAmp, sigma)
|
|
141
|
+
else:
|
|
142
|
+
arrT = linspace(0, tScan, nT)
|
|
143
|
+
arrAmp = sin(2*pi/cyc*arrT)
|
|
144
|
+
|
|
145
|
+
return arrAmp
|
|
146
|
+
|
|
147
|
+
def genResAmp(tScan:int|float, tRes:int|float, cyc:int|float=pi/2) -> NDArray:
|
|
148
|
+
"""
|
|
149
|
+
generate respiratory amplitude curve
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
tScan: length of the signal in `s`
|
|
153
|
+
tRes: temporal resolution in `s`
|
|
154
|
+
cyc: period of the signal in `s`
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
generated amplitude, approx. -0.02~0.02
|
|
158
|
+
"""
|
|
159
|
+
return 20e-3*genAmp(tScan, tRes, cyc, 1)
|
|
160
|
+
|
|
161
|
+
def genCarAmp(tScan:int|float, tRes:int|float, cyc:int|float=1) -> NDArray:
|
|
162
|
+
"""
|
|
163
|
+
generate cardiac amplitude curve
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
tScan: length of the signal in `s`
|
|
167
|
+
tRes: temporal resolution in `s`
|
|
168
|
+
cyc: period of the signal in `s`
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
generated amplitude, approx. -0.01~0.01
|
|
172
|
+
"""
|
|
173
|
+
return 10e-3*genAmp(tScan, tRes, cyc, 0)
|
|
174
|
+
|
|
175
|
+
def fB02strB0(B0:int|float) -> str:
|
|
176
|
+
"""
|
|
177
|
+
convert B0 data type from float/int to string.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
B0: B0 in number format
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
B0 in string format
|
|
184
|
+
"""
|
|
185
|
+
if isclose(B0,0.55): return "B0_0T55"
|
|
186
|
+
if isclose(B0,1.5): return "B0_1T5"
|
|
187
|
+
if isclose(B0,3.0): return "B0_3T"
|
|
188
|
+
if isclose(B0,5.0): return "B0_5T0"
|
|
189
|
+
if isclose(B0,9.4): return "B0_9T4"
|
|
190
|
+
raise RuntimeError("unsupported B0")
|
|
191
|
+
|
|
192
|
+
def initSS_bSSFP(
|
|
193
|
+
B0: float,
|
|
194
|
+
TR: float = 5e-3,
|
|
195
|
+
FA_deg: float = 60.0,
|
|
196
|
+
) -> None:
|
|
197
|
+
"""
|
|
198
|
+
Precalculate and store the bSSFP steady-state signal Mss for every tissue.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
B0: field strength
|
|
202
|
+
TR: repetition time
|
|
203
|
+
FA_deg: flip angle in degree
|
|
204
|
+
"""
|
|
205
|
+
strB0 = fB02strB0(B0)
|
|
206
|
+
FA = deg2rad(FA_deg)
|
|
207
|
+
|
|
208
|
+
for strTissue in lstTissue:
|
|
209
|
+
dictTissue = dictNmrPara[strTissue]
|
|
210
|
+
|
|
211
|
+
PD = dictTissue[strB0]["PD"]
|
|
212
|
+
T1 = dictTissue[strB0]["T1"]
|
|
213
|
+
T2 = dictTissue[strB0]["T2"]
|
|
214
|
+
|
|
215
|
+
E1 = exp(-TR / T1)
|
|
216
|
+
E2 = exp(-TR / T2)
|
|
217
|
+
|
|
218
|
+
dictTissue["Mss"] = (
|
|
219
|
+
PD
|
|
220
|
+
* (1 - E1)
|
|
221
|
+
* sqrt(E2)
|
|
222
|
+
* sin(FA)
|
|
223
|
+
/ (1 - (E1 - E2) * cos(FA) - E1 * E2)
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def initSS_FLASH(
|
|
227
|
+
B0: float,
|
|
228
|
+
TE: float = 1e-3,
|
|
229
|
+
TR: float = 10e-3,
|
|
230
|
+
FA_deg: float = 10.0,
|
|
231
|
+
) -> None:
|
|
232
|
+
"""
|
|
233
|
+
Precalculate and store the FLASH steady-state signal Mss for every tissue.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
B0: field strength
|
|
237
|
+
TE: echo time
|
|
238
|
+
TR: repetition time
|
|
239
|
+
FA_deg: flip angle in degree
|
|
240
|
+
"""
|
|
241
|
+
strB0 = fB02strB0(B0)
|
|
242
|
+
FA = deg2rad(FA_deg)
|
|
243
|
+
|
|
244
|
+
for strTissue in lstTissue:
|
|
245
|
+
dictTissue = dictNmrPara[strTissue]
|
|
246
|
+
|
|
247
|
+
PD = dictTissue[strB0]["PD"]
|
|
248
|
+
T1 = dictTissue[strB0]["T1"]
|
|
249
|
+
T2s = dictTissue[strB0]["T2s"]
|
|
250
|
+
|
|
251
|
+
E1 = exp(-TR / T1)
|
|
252
|
+
E2 = exp(-TE / T2s)
|
|
253
|
+
|
|
254
|
+
dictTissue["Mss"] = (
|
|
255
|
+
PD
|
|
256
|
+
* sin(FA)
|
|
257
|
+
* (1 - E1)
|
|
258
|
+
/ (1 - cos(FA) * E1)
|
|
259
|
+
* E2
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
def Enum2SS(arrPht:NDArray) -> NDArray:
|
|
263
|
+
"""
|
|
264
|
+
get steady-state signal map of a phantom generated by `genPhant()`
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
arrPht: phantom
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
steady-state signal map of the given phantom
|
|
271
|
+
"""
|
|
272
|
+
mapSS = zeros_like(arrPht, dtype=float64)
|
|
273
|
+
for strTissue in lstTissue:
|
|
274
|
+
try: mapSS[arrPht==dictNmrPara[strTissue]["enum"]] = dictNmrPara[strTissue]["Mss"]
|
|
275
|
+
except KeyError: raise RuntimeError("Please call `initS_FLASH()` or `initS_bSSFP()` before `Enum2SS()`.")
|
|
276
|
+
return mapSS
|
|
277
|
+
|
|
278
|
+
def Enum2Para(arrPht:NDArray, B0:str|float="B0_1T5", strPara:str="PD") -> NDArray:
|
|
279
|
+
"""
|
|
280
|
+
get PD map of a phantom generated by `genPhant()`
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
arrPht: phantom
|
|
284
|
+
B0: "B0_0T55" / "B0_1T5" / "B0_3T" / "B0_5T" / "B0_9T4" / 0.55 / 1.5 / 3.0 / 5.0 / 9.4
|
|
285
|
+
strPara: "PD" / "T1" / "T2" / "T2s" / "ADC" / "Om"
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Proton density map of the given phantom, relevant to water
|
|
289
|
+
"""
|
|
290
|
+
mapPara = zeros_like(arrPht, dtype=float64)
|
|
291
|
+
if not isinstance(B0, str): B0 = fB02strB0(B0)
|
|
292
|
+
for strTissue in lstTissue:
|
|
293
|
+
mapPara[arrPht==dictNmrPara[strTissue]["enum"]] = dictNmrPara[strTissue][B0][strPara]
|
|
294
|
+
return mapPara
|
|
295
|
+
|
|
296
|
+
def Enum2PD(arrPht:NDArray, B0:str|int|float) -> NDArray:
|
|
297
|
+
"""
|
|
298
|
+
get PD map of a phantom generated by `genPhant()`
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
arrPht: phantom
|
|
302
|
+
B0: field strength in Tesla
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Proton density map of the given phantom, relevant to water
|
|
306
|
+
"""
|
|
307
|
+
return Enum2Para(arrPht, B0, "PD")
|
|
308
|
+
|
|
309
|
+
def Enum2T1(arrPht:NDArray, B0:str|int|float) -> NDArray:
|
|
310
|
+
"""
|
|
311
|
+
get T1 map of a phantom generated by `genPhant()`
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
arrPht: phantom
|
|
315
|
+
B0: field strength in Tesla
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
T1 map of the given phantom
|
|
319
|
+
"""
|
|
320
|
+
return Enum2Para(arrPht, B0, "T1")
|
|
321
|
+
|
|
322
|
+
def Enum2T2(arrPht:NDArray, B0:str|int|float) -> NDArray:
|
|
323
|
+
"""
|
|
324
|
+
get T2 map of a phantom generated by `genPhant()`
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
arrPht: phantom
|
|
328
|
+
B0: field strength in Tesla
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
T2 map of the given phantom
|
|
332
|
+
"""
|
|
333
|
+
return Enum2Para(arrPht, B0, "T2")
|
|
334
|
+
|
|
335
|
+
def Enum2T2s(arrPht:NDArray, B0:str|int|float) -> NDArray:
|
|
336
|
+
"""
|
|
337
|
+
get T2* map of a phantom generated by `genPhant()`
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
arrPht: phantom
|
|
341
|
+
B0: field strength in Tesla
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
T2* map of the given phantom
|
|
345
|
+
"""
|
|
346
|
+
return Enum2Para(arrPht, B0, "T2s")
|
|
347
|
+
|
|
348
|
+
def Enum2Adc(arrPht:NDArray, B0:str|int|float) -> NDArray:
|
|
349
|
+
"""
|
|
350
|
+
get Apparent Diffusion Coefficient (ADC) map (in `m^2/s`) of a phantom generated by `genPhant()`
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
arrPht: phantom
|
|
354
|
+
B0: field strength in Tesla
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
ADC map of the given phantom
|
|
358
|
+
"""
|
|
359
|
+
return Enum2Para(arrPht, B0, "ADC")
|
|
360
|
+
|
|
361
|
+
def Enum2Om(arrPht:NDArray, B0:str|int|float) -> NDArray:
|
|
362
|
+
"""
|
|
363
|
+
get off-resonance map (in `rad/s`) of a phantom generated by `genPhant()`
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
arrPht: phantom
|
|
367
|
+
B0: field strength in Tesla
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
off-resonance map of the given phantom
|
|
371
|
+
"""
|
|
372
|
+
return Enum2Para(arrPht, B0, "Om")
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from . import utility
|
|
2
|
+
|
|
3
|
+
with open("test/nmr_para.xml") as f:
|
|
4
|
+
dictNmrPara = utility.xml2dict(f)
|
|
5
|
+
lstTissue = list(dictNmrPara.keys())
|
|
6
|
+
|
|
7
|
+
from .Function import genPhant, genPhMap, genB0Map, genCsm, genAmp, genResAmp, genCarAmp, Enum2Para, Enum2PD, Enum2T1, Enum2T2, Enum2Adc, Enum2Om, initSS_bSSFP, initSS_FLASH, Enum2SS, Enum2T2s
|
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
#include <cstring>
|
|
7
7
|
#include <slime.h>
|
|
8
8
|
|
|
9
|
-
bool inline checkNarg(int64_t
|
|
9
|
+
bool inline checkNarg(int64_t nArg, int64_t nArgExp)
|
|
10
10
|
{
|
|
11
|
-
if (
|
|
11
|
+
if (nArg != nArgExp)
|
|
12
12
|
{
|
|
13
|
-
printf("wrong num. of arg, narg=%ld, %ld expected\n",
|
|
13
|
+
printf("wrong num. of arg, narg=%ld, %ld expected\n", nArg, nArgExp);
|
|
14
14
|
abort();
|
|
15
15
|
return false;
|
|
16
16
|
}
|
|
@@ -19,34 +19,36 @@ bool inline checkNarg(int64_t lNarg, int64_t lNargExp)
|
|
|
19
19
|
|
|
20
20
|
static PyObject* genPhant_py(PyObject* self, PyObject* const* args, Py_ssize_t nargs)
|
|
21
21
|
{
|
|
22
|
-
checkNarg(nargs,
|
|
22
|
+
checkNarg(nargs,6);
|
|
23
23
|
int64_t nAx = PyLong_AsLongLong(args[0]);
|
|
24
|
-
int64_t
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
int64_t nZ = nAx==3 ? PyLong_AsLongLong(args[1]) : 1;
|
|
25
|
+
int64_t nY = PyLong_AsLongLong(args[2]);
|
|
26
|
+
int64_t nX = PyLong_AsLongLong(args[3]);
|
|
27
|
+
double ampRes = PyFloat_AsDouble(args[4]);
|
|
28
|
+
double ampCar = PyFloat_AsDouble(args[5]);
|
|
27
29
|
|
|
28
30
|
// Generate into std::vector
|
|
29
|
-
std::vector<uint8_t> vu8Phant;
|
|
30
|
-
genPhant(nAx,
|
|
31
|
+
std::vector<uint8_t> vu8Phant(nZ*nY*nX, 0);
|
|
32
|
+
genPhant(nAx, nZ, nY, nX, ampRes, ampCar, &vu8Phant);
|
|
31
33
|
|
|
32
34
|
// convert vector to numpy array
|
|
33
35
|
PyObject* pPyObj_Arr;
|
|
34
36
|
{
|
|
35
|
-
npy_intp aDims[] = {
|
|
36
|
-
pPyObj_Arr = PyArray_ZEROS(nAx, aDims, NPY_UINT8, 0);
|
|
37
|
+
npy_intp aDims[] = {nZ, nY, nX};
|
|
38
|
+
pPyObj_Arr = PyArray_ZEROS(nAx, aDims+3-nAx, NPY_UINT8, 0);
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
// fill the data in
|
|
40
42
|
std::memcpy(PyArray_DATA((PyArrayObject*)pPyObj_Arr),
|
|
41
43
|
vu8Phant.data(),
|
|
42
44
|
vu8Phant.size() * sizeof(uint8_t));
|
|
43
|
-
|
|
45
|
+
|
|
44
46
|
return pPyObj_Arr;
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
static PyMethodDef aMeth[] =
|
|
48
50
|
{
|
|
49
|
-
{"genPhant", (PyCFunction)genPhant_py, METH_FASTCALL, "genPhant(nAx,
|
|
51
|
+
{"genPhant", (PyCFunction)genPhant_py, METH_FASTCALL, "genPhant(nAx, nZ, nY, nX, ampRes, ampCar) -> np.ndarray[uint8]"},
|
|
50
52
|
{NULL, NULL, 0, NULL}
|
|
51
53
|
};
|
|
52
54
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#include <cstdio>
|
|
1
2
|
#include <vector>
|
|
2
3
|
#include <cstdint>
|
|
3
4
|
#include <cmath>
|
|
@@ -12,8 +13,7 @@ enum Tissue : uint8_t
|
|
|
12
13
|
Fat = 1,
|
|
13
14
|
Myo = 2,
|
|
14
15
|
Blood = 3,
|
|
15
|
-
|
|
16
|
-
Vessel = 5,
|
|
16
|
+
Liver = 4,
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
bool isInsideEllipsoid
|
|
@@ -39,48 +39,45 @@ bool isInsideEllipsoid
|
|
|
39
39
|
|
|
40
40
|
bool genPhant
|
|
41
41
|
(
|
|
42
|
-
int64_t
|
|
43
|
-
double
|
|
42
|
+
int64_t nAx, int64_t nZ, int64_t nY, int64_t nX,
|
|
43
|
+
double ampRes, double ampCar,
|
|
44
44
|
std::vector<uint8_t>* voSlime
|
|
45
45
|
)
|
|
46
46
|
{
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
else if (lNAx==3)lNPix_Flat = lNPix*lNPix*lNPix;
|
|
51
|
-
else throw std::runtime_error("lNAx != 2 && lNAx != 3");
|
|
52
|
-
|
|
53
|
-
voSlime->assign((size_t)(lNPix_Flat), (uint8_t)(Tissue::Air));
|
|
47
|
+
size_t nPixSum = nZ*nY*nX;
|
|
48
|
+
voSlime->resize(nPixSum);
|
|
49
|
+
voSlime->assign(nPixSum, Tissue::Air);
|
|
54
50
|
|
|
55
51
|
// shape parameter; r: radius, c: center
|
|
56
|
-
const double dFatOt_rY =
|
|
57
|
-
const double dFatOt_rX =
|
|
58
|
-
const double dFatOt_rZ =
|
|
52
|
+
const double dFatOt_rY = nY*400e-3 + nY*ampRes;
|
|
53
|
+
const double dFatOt_rX = nX*400e-3 - 5e-1*nX*ampRes;
|
|
54
|
+
const double dFatOt_rZ = nZ*480e-3;
|
|
59
55
|
|
|
60
|
-
const double dFatIn_rY =
|
|
61
|
-
const double dFatIn_rX =
|
|
62
|
-
const double dFatIn_rZ =
|
|
56
|
+
const double dFatIn_rY = nY*380e-3 + nY*ampRes;
|
|
57
|
+
const double dFatIn_rX = nX*380e-3 - 5e-1*nX*ampRes;
|
|
58
|
+
const double dFatIn_rZ = nZ*450e-3;
|
|
63
59
|
|
|
64
|
-
const double dMyoOt_rY =
|
|
65
|
-
const double dMyoOt_rX =
|
|
66
|
-
const double dMyoOt_rZ =
|
|
60
|
+
const double dMyoOt_rY = nY*100e-3 + nY*ampCar;
|
|
61
|
+
const double dMyoOt_rX = nX*120e-3 + nX*ampCar;
|
|
62
|
+
const double dMyoOt_rZ = nZ*100e-3 + nZ*ampCar;;
|
|
67
63
|
|
|
68
|
-
const double dMyoIn_rY =
|
|
69
|
-
const double dMyoIn_rX =
|
|
70
|
-
const double dMyoIn_rZ =
|
|
64
|
+
const double dMyoIn_rY = nY*60e-3 + nY*(2*ampCar);
|
|
65
|
+
const double dMyoIn_rX = nX*60e-3 + nX*(2*ampCar);
|
|
66
|
+
const double dMyoIn_rZ = nZ*60e-3 + nZ*(2*ampCar);;
|
|
71
67
|
|
|
72
68
|
// Centers are at (0,0,0) in your centered coordinate system,
|
|
73
69
|
const double dFat_cx = 0e0, dFat_cy = 0e0, dFat_cz = 0e0;
|
|
74
70
|
const double dMyoOt_cx = 0e0, dMyoOt_cy = 0e0, dMyoOt_cz = 0e0;
|
|
75
|
-
const double dMyoIn_cx = -
|
|
71
|
+
const double dMyoIn_cx = -nX*20e-3, dMyoIn_cy = 0e0, dMyoIn_cz = 0e0;
|
|
76
72
|
|
|
77
|
-
/* generate phantom given by `
|
|
73
|
+
/* generate phantom given by `ampRes` and `ampCar` here */
|
|
78
74
|
#pragma omp parallel for schedule(static)
|
|
79
|
-
for (int64_t i = 0; i <
|
|
75
|
+
for (int64_t i = 0; i < nPixSum; ++i)
|
|
80
76
|
{
|
|
81
|
-
|
|
82
|
-
const int64_t
|
|
83
|
-
const int64_t
|
|
77
|
+
// derive coordinates
|
|
78
|
+
const int64_t x = i%nX - nX/2;
|
|
79
|
+
const int64_t y = (i/nX)%nY - nY/2;
|
|
80
|
+
const int64_t z = (nAx==3) ? (i/(nY*nX) - nZ/2) : 0;
|
|
84
81
|
|
|
85
82
|
// decide what tissue current pixel is
|
|
86
83
|
if (!isInsideEllipsoid(x,y,z, dFat_cx,dFat_cy,dFat_cz, dFatOt_rX,dFatOt_rY,dFatOt_rZ))
|
|
@@ -96,9 +93,9 @@ bool genPhant
|
|
|
96
93
|
}
|
|
97
94
|
|
|
98
95
|
// vessel balls
|
|
99
|
-
#define V_HIT(cx,cy,cz,div) isInsideEllipsoid(x,y,z, ((cx)*
|
|
96
|
+
#define V_HIT(cx,cy,cz,div) isInsideEllipsoid(x,y,z, ((cx)*nX), ((cy)*nY), ((cz)*nZ), (dFatIn_rX/(div)), (dFatIn_rY/(div)), (dFatIn_rZ/(div)))
|
|
100
97
|
|
|
101
|
-
// 48 “random” vessels (cx,cy,cz in
|
|
98
|
+
// 48 “random” vessels (cx,cy,cz in nX,nY,nZ fractions; div in ~[18..44])
|
|
102
99
|
#define VLIST \
|
|
103
100
|
/* 5 on XY plane (z = 0) — slightly farther from heart */ \
|
|
104
101
|
V_HIT( 0.18, 0.09, 0.00, 12) || \
|
|
@@ -128,16 +125,16 @@ bool genPhant
|
|
|
128
125
|
V_HIT( 0.14, -0.14, -0.13, 16) || /* + - - */ \
|
|
129
126
|
V_HIT(-0.15, -0.14, -0.13, 16) /* - - - */
|
|
130
127
|
|
|
131
|
-
if (
|
|
128
|
+
if (VLIST)
|
|
132
129
|
{
|
|
133
|
-
(*voSlime)[(size_t)i] = (uint8_t)Tissue::
|
|
130
|
+
(*voSlime)[(size_t)i] = (uint8_t)Tissue::Fat;
|
|
134
131
|
continue;
|
|
135
132
|
}
|
|
136
133
|
// vessel balls (end)
|
|
137
134
|
|
|
138
135
|
if (!isInsideEllipsoid(x,y,z, dMyoOt_cx,dMyoOt_cy,dMyoOt_cz, dMyoOt_rX,dMyoOt_rY,dMyoOt_rZ))
|
|
139
136
|
{
|
|
140
|
-
(*voSlime)[(size_t)i] = (uint8_t)Tissue::
|
|
137
|
+
(*voSlime)[(size_t)i] = (uint8_t)Tissue::Liver;
|
|
141
138
|
continue;
|
|
142
139
|
}
|
|
143
140
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<root>
|
|
3
|
+
<air enum="0">
|
|
4
|
+
<B0_0T55 PD="0.00" T1="inf" T2="1e-6" T2s="1e-6" ADC="0.0" Om="0e-6"/>
|
|
5
|
+
<B0_1T5 PD="0.00" T1="inf" T2="1e-6" T2s="1e-6" ADC="0.0" Om="0e-6"/>
|
|
6
|
+
<B0_3T PD="0.00" T1="inf" T2="1e-6" T2s="1e-6" ADC="0.0" Om="0e-6"/>
|
|
7
|
+
<B0_5T PD="0.00" T1="inf" T2="1e-6" T2s="1e-6" ADC="0.0" Om="0e-6"/>
|
|
8
|
+
<B0_9T4 PD="0.00" T1="inf" T2="1e-6" T2s="1e-6" ADC="0.0" Om="0e-6"/>
|
|
9
|
+
</air>
|
|
10
|
+
|
|
11
|
+
<fat enum="1">
|
|
12
|
+
<B0_0T55 PD="0.95" T1="280e-3" T2="100e-3" T2s="70e-3" ADC="0.15e-9" Om="-515.01"/>
|
|
13
|
+
<B0_1T5 PD="0.95" T1="343e-3" T2="58e-3" T2s="50e-3" ADC="0.15e-9" Om="-1404.57"/>
|
|
14
|
+
<B0_3T PD="0.95" T1="382e-3" T2="68e-3" T2s="35e-3" ADC="0.15e-9" Om="-2809.15"/>
|
|
15
|
+
<B0_5T PD="0.95" T1="450e-3" T2="60e-3" T2s="25e-3" ADC="0.15e-9" Om="-4681.92"/>
|
|
16
|
+
<B0_9T4 PD="0.95" T1="500e-3" T2="50e-3" T2s="15e-3" ADC="0.15e-9" Om="-8802.00"/>
|
|
17
|
+
</fat>
|
|
18
|
+
|
|
19
|
+
<myo enum="2">
|
|
20
|
+
<B0_0T55 PD="0.80" T1="700e-3" T2="60e-3" T2s="50e-3" ADC="1.55e-9" Om="0e-6"/>
|
|
21
|
+
<B0_1T5 PD="0.80" T1="1008e-3" T2="44e-3" T2s="33e-3" ADC="1.55e-9" Om="0e-6"/>
|
|
22
|
+
<B0_3T PD="0.80" T1="1220e-3" T2="47e-3" T2s="22e-3" ADC="1.55e-9" Om="0e-6"/>
|
|
23
|
+
<B0_5T PD="0.80" T1="1450e-3" T2="35e-3" T2s="15e-3" ADC="1.55e-9" Om="0e-6"/>
|
|
24
|
+
<B0_9T4 PD="0.80" T1="1800e-3" T2="20e-3" T2s="8e-3" ADC="1.55e-9" Om="0e-6"/>
|
|
25
|
+
</myo>
|
|
26
|
+
|
|
27
|
+
<blood enum="3">
|
|
28
|
+
<B0_0T55 PD="0.95" T1="1120e-3" T2="260e-3" T2s="80e-3" ADC="2.10e-9" Om="0e-6"/>
|
|
29
|
+
<B0_1T5 PD="0.95" T1="1441e-3" T2="290e-3" T2s="55e-3" ADC="2.10e-9" Om="0e-6"/>
|
|
30
|
+
<B0_3T PD="0.95" T1="1932e-3" T2="275e-3" T2s="30e-3" ADC="2.10e-9" Om="0e-6"/>
|
|
31
|
+
<B0_5T PD="0.95" T1="2100e-3" T2="90e-3" T2s="18e-3" ADC="2.10e-9" Om="0e-6"/>
|
|
32
|
+
<B0_9T4 PD="0.95" T1="2500e-3" T2="40e-3" T2s="10e-3" ADC="2.10e-9" Om="0e-6"/>
|
|
33
|
+
</blood>
|
|
34
|
+
|
|
35
|
+
<liver enum="4">
|
|
36
|
+
<B0_0T55 PD="0.90" T1="450e-3" T2="55e-3" T2s="45e-3" ADC="1.15e-9" Om="0e-6"/>
|
|
37
|
+
<B0_1T5 PD="0.90" T1="576e-3" T2="46e-3" T2s="30e-3" ADC="1.15e-9" Om="0e-6"/>
|
|
38
|
+
<B0_3T PD="0.90" T1="809e-3" T2="34e-3" T2s="18e-3" ADC="1.15e-9" Om="0e-6"/>
|
|
39
|
+
<B0_5T PD="0.90" T1="1000e-3" T2="34e-3" T2s="10e-3" ADC="1.15e-9" Om="0e-6"/>
|
|
40
|
+
<B0_9T4 PD="0.90" T1="1200e-3" T2="25e-3" T2s="5e-3" ADC="1.15e-9" Om="0e-6"/>
|
|
41
|
+
</liver>
|
|
42
|
+
</root>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import xml.etree.ElementTree as ET
|
|
2
|
+
|
|
3
|
+
def ele2dict(element):
|
|
4
|
+
result = {
|
|
5
|
+
key: int(value) if key=="enum" else float(value)
|
|
6
|
+
for key, value in element.attrib.items()
|
|
7
|
+
}
|
|
8
|
+
for child in element:
|
|
9
|
+
result[child.tag] = ele2dict(child)
|
|
10
|
+
return result
|
|
11
|
+
|
|
12
|
+
xml2dict = lambda fXml: ele2dict(ET.parse(fXml).getroot())
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "MRPhantom"
|
|
7
|
-
version = "2.2.
|
|
7
|
+
version = "2.2.1"
|
|
8
8
|
dependencies = ["numpy", "scipy"]
|
|
9
9
|
|
|
10
10
|
description = "Volumetric dynamic MRI Phantom of a Slime with respiratory and cardiac motion, and M0, phase, T1, T2, B0, coil sensitivity maps, boosted by a parallel C-API Backend."
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import xml.etree.ElementTree as ET
|
|
2
|
+
|
|
3
|
+
def ele2dict(element):
|
|
4
|
+
result = {
|
|
5
|
+
key: float(value)
|
|
6
|
+
for key, value in element.attrib.items()
|
|
7
|
+
}
|
|
8
|
+
for child in element:
|
|
9
|
+
result[child.tag] = ele2dict(child)
|
|
10
|
+
return result
|
|
11
|
+
|
|
12
|
+
xml2dict = lambda fXml: ele2dict(ET.parse(fXml).getroot())
|
|
13
|
+
|
|
14
|
+
with open("test/nmr_para.xml") as f:
|
|
15
|
+
dictNmrPara = xml2dict(f)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from numpy import *
|
|
2
|
+
from matplotlib.pyplot import *
|
|
3
|
+
import mrphantom as mpt
|
|
4
|
+
from scipy.ndimage import zoom
|
|
5
|
+
|
|
6
|
+
# 2D
|
|
7
|
+
nPix = 256
|
|
8
|
+
arrPhant = mpt.genPhant((nPix,nPix//4))
|
|
9
|
+
mpt.initSS_bSSFP(1.5)
|
|
10
|
+
arrM0 = mpt.Enum2SS(arrPhant)*mpt.genPhMap(arrPhant.shape)
|
|
11
|
+
|
|
12
|
+
arrM0 = zoom(arrM0, (1,4))
|
|
13
|
+
|
|
14
|
+
figure(figsize=(6,3), dpi=150)
|
|
15
|
+
subplot(121)
|
|
16
|
+
imshow(abs(arrM0), cmap="gray"); colorbar()
|
|
17
|
+
title("Magnitude")
|
|
18
|
+
subplot(122)
|
|
19
|
+
imshow(angle(arrM0), cmap="hsv", vmin=-pi, vmax=pi); colorbar()
|
|
20
|
+
title("Phase")
|
|
21
|
+
tight_layout()
|
|
22
|
+
show()
|
|
@@ -1,436 +0,0 @@
|
|
|
1
|
-
from . import ext
|
|
2
|
-
from numpy import *
|
|
3
|
-
from numpy.typing import NDArray
|
|
4
|
-
from enum import Enum
|
|
5
|
-
from scipy.ndimage import gaussian_filter
|
|
6
|
-
|
|
7
|
-
class Tissue(Enum):
|
|
8
|
-
Air = 0
|
|
9
|
-
Fat = 1
|
|
10
|
-
Myo = 2 # Myocadium
|
|
11
|
-
Blood = 3
|
|
12
|
-
Liver = 4
|
|
13
|
-
Vessel = 5
|
|
14
|
-
NTissue = 6
|
|
15
|
-
|
|
16
|
-
def genPhant(nAx:int=2, nPix:int=256, ampRes:float=0, ampCar:float=0) -> NDArray: # call C++ backend to generate a phantom
|
|
17
|
-
"""
|
|
18
|
-
generate a phantom in Enum type
|
|
19
|
-
|
|
20
|
-
Args:
|
|
21
|
-
nAx: number of dimensions
|
|
22
|
-
nPix: number of pixels
|
|
23
|
-
ampRes: respiratory motion amplitude
|
|
24
|
-
ampCar: cardiac motion amplitude
|
|
25
|
-
|
|
26
|
-
Returns:
|
|
27
|
-
NDArray contains elements in `Tissue` enum type
|
|
28
|
-
"""
|
|
29
|
-
return ext.genPhant(nAx, nPix, ampRes, ampCar)
|
|
30
|
-
|
|
31
|
-
def genPhMap(nAx:int=2, nPix:int=256, mean:int|float|None=None, std:int|float=pi/16) -> NDArray:
|
|
32
|
-
"""
|
|
33
|
-
generate random phase map
|
|
34
|
-
|
|
35
|
-
Args:
|
|
36
|
-
nAx: number of dimensions
|
|
37
|
-
nPix: number of pixels
|
|
38
|
-
mean: mean of the noise
|
|
39
|
-
std: std of the noise
|
|
40
|
-
|
|
41
|
-
Returns:
|
|
42
|
-
smooth complex noise with unity magnitude
|
|
43
|
-
"""
|
|
44
|
-
if mean is None: mean = random.uniform(-pi,pi)
|
|
45
|
-
mapPh = random.uniform(-pi, pi, [nPix for _ in range(nAx)])
|
|
46
|
-
sigma = nPix/4
|
|
47
|
-
mapPh = gaussian_filter(mapPh, sigma)
|
|
48
|
-
# normalize
|
|
49
|
-
mapPh -= mapPh.mean(); mapPh = asarray(mapPh)
|
|
50
|
-
mapPh /= mapPh.std()
|
|
51
|
-
mapPh *= std
|
|
52
|
-
mapPh += mean
|
|
53
|
-
# convert to rotation factor
|
|
54
|
-
mapPh = exp(1j*mapPh)
|
|
55
|
-
mapPh = mapPh/abs(mapPh)
|
|
56
|
-
return mapPh
|
|
57
|
-
|
|
58
|
-
def genB0Map(nAx:int=2, nPix:int=256, mean:int|float=0, std:int|float=1e-6*(2*pi*42.58e6*3)) -> NDArray:
|
|
59
|
-
"""
|
|
60
|
-
generate random B0 map
|
|
61
|
-
|
|
62
|
-
Args:
|
|
63
|
-
nAx: number of dimensions
|
|
64
|
-
nPix: number of pixels
|
|
65
|
-
mean: mean of the noise
|
|
66
|
-
std: std of the noise
|
|
67
|
-
|
|
68
|
-
Returns:
|
|
69
|
-
smooth random noise in `rad/s`
|
|
70
|
-
"""
|
|
71
|
-
mapB0 = random.uniform(-1, 1, [nPix for _ in range(nAx)])
|
|
72
|
-
sigma = nPix/4
|
|
73
|
-
mapB0 = gaussian_filter(mapB0, sigma)
|
|
74
|
-
# normalize
|
|
75
|
-
mapB0 -= mapB0.mean(); mapB0 = asarray(mapB0)
|
|
76
|
-
mapB0 /= mapB0.std()
|
|
77
|
-
mapB0 *= std
|
|
78
|
-
mapB0 += mean
|
|
79
|
-
return mapB0
|
|
80
|
-
|
|
81
|
-
def genCsm(nAx:int=2, nPix:int=256, nCh:int=12, mean:int|float|None=None, std:int|float=pi/16) -> NDArray:
|
|
82
|
-
"""
|
|
83
|
-
generate random coil sensitivity map
|
|
84
|
-
|
|
85
|
-
Args:
|
|
86
|
-
nAx: number of dimensions
|
|
87
|
-
nPix: number of pixels
|
|
88
|
-
nCh: number of coils
|
|
89
|
-
mean: mean of the noise
|
|
90
|
-
std: std of the noise
|
|
91
|
-
|
|
92
|
-
Returns:
|
|
93
|
-
complex smooth and inhomogeneous map
|
|
94
|
-
"""
|
|
95
|
-
if mean is None: mean = random.uniform(-pi,pi)
|
|
96
|
-
mapC = zeros([nCh,*(nPix for _ in range(nAx))], dtype=complex128)
|
|
97
|
-
arrCoor = meshgrid\
|
|
98
|
-
(
|
|
99
|
-
*(linspace(-0.5,0.5,nPix,0) for _ in range(nAx)),
|
|
100
|
-
indexing="ij"
|
|
101
|
-
); arrCoor = array(arrCoor).transpose(*arange(1,nAx+1), 0)
|
|
102
|
-
arrTht = linspace(0,2*pi,nCh,0)
|
|
103
|
-
arrCoorCoil = zeros([nCh,nAx], dtype=float64)
|
|
104
|
-
arrCoorCoil[:,-2:] = 1*array([sin(arrTht), cos(arrTht)]).T
|
|
105
|
-
if nAx == 3:
|
|
106
|
-
arrCoorCoil[0::2,0] = 0.2
|
|
107
|
-
arrCoorCoil[1::2,0] = -0.2
|
|
108
|
-
for iCh in range(nCh):
|
|
109
|
-
mapC[iCh] = genPhMap(nAx=nAx, nPix=nPix, mean=mean, std=std)
|
|
110
|
-
dist = sqrt(sum((arrCoor - arrCoorCoil[iCh])**2, axis=-1))
|
|
111
|
-
mapC[iCh] *= exp(-dist)
|
|
112
|
-
return mapC
|
|
113
|
-
|
|
114
|
-
def genAmp(tScan:int|float, tRes:int|float, cyc:int|float, isRand:bool=True) -> NDArray:
|
|
115
|
-
"""
|
|
116
|
-
generate amplitude curve
|
|
117
|
-
|
|
118
|
-
Args:
|
|
119
|
-
tScan: length of the signal in `s`
|
|
120
|
-
tRes: temporal resolution in `s`
|
|
121
|
-
cyc: period of the signal in `s`
|
|
122
|
-
isRand: make the signal have irregular period
|
|
123
|
-
|
|
124
|
-
Returns:
|
|
125
|
-
generated amplitude
|
|
126
|
-
"""
|
|
127
|
-
nT = around(tScan/tRes).astype(int)
|
|
128
|
-
|
|
129
|
-
if isRand:
|
|
130
|
-
arrT = sort(random.rand(nT)*tScan)
|
|
131
|
-
arrAmp = sin(2*pi/cyc*arrT)
|
|
132
|
-
|
|
133
|
-
sigma = cyc/tRes/8
|
|
134
|
-
arrAmp = gaussian_filter(arrAmp, sigma)
|
|
135
|
-
else:
|
|
136
|
-
arrT = linspace(0, tScan, nT)
|
|
137
|
-
arrAmp = sin(2*pi/cyc*arrT)
|
|
138
|
-
|
|
139
|
-
return arrAmp
|
|
140
|
-
|
|
141
|
-
def genResAmp(tScan:int|float, tRes:int|float, cyc:int|float=pi/2) -> NDArray:
|
|
142
|
-
"""
|
|
143
|
-
generate respiratory amplitude curve
|
|
144
|
-
|
|
145
|
-
Args:
|
|
146
|
-
tScan: length of the signal in `s`
|
|
147
|
-
tRes: temporal resolution in `s`
|
|
148
|
-
cyc: period of the signal in `s`
|
|
149
|
-
|
|
150
|
-
Returns:
|
|
151
|
-
generated amplitude, approx. -0.02~0.02
|
|
152
|
-
"""
|
|
153
|
-
return 20e-3*genAmp(tScan, tRes, cyc, 1)
|
|
154
|
-
|
|
155
|
-
def genCarAmp(tScan:int|float, tRes:int|float, cyc:int|float=1) -> NDArray:
|
|
156
|
-
"""
|
|
157
|
-
generate cardiac amplitude curve
|
|
158
|
-
|
|
159
|
-
Args:
|
|
160
|
-
tScan: length of the signal in `s`
|
|
161
|
-
tRes: temporal resolution in `s`
|
|
162
|
-
cyc: period of the signal in `s`
|
|
163
|
-
|
|
164
|
-
Returns:
|
|
165
|
-
generated amplitude, approx. -0.01~0.01
|
|
166
|
-
"""
|
|
167
|
-
return 10e-3*genAmp(tScan, tRes, cyc, 0)
|
|
168
|
-
|
|
169
|
-
def Enum2SS(arrPht:NDArray, ampCar:double=0e0, B0:double=5.0, TR:double=None, TE:double=None, FA_deg:double=None, bSSFP:bool=False, mapPh:NDArray=None) -> NDArray:
|
|
170
|
-
"""
|
|
171
|
-
get steady-state signal map of a phantom generated by `genPhant()`
|
|
172
|
-
|
|
173
|
-
Args:
|
|
174
|
-
arrPht: phantom
|
|
175
|
-
ampCar: cardiac motion amplitude
|
|
176
|
-
TR: repetition time
|
|
177
|
-
TE: echo time
|
|
178
|
-
FA_deg: flip angle in degree
|
|
179
|
-
bSSFP: True for bSSFP, False for FLASH
|
|
180
|
-
mapPh: phase map array, can be generated using genPhMap() `NTissue` times
|
|
181
|
-
|
|
182
|
-
Returns:
|
|
183
|
-
steady state signal map of the given phantom
|
|
184
|
-
"""
|
|
185
|
-
mapPD = Enum2PD(arrPht)
|
|
186
|
-
mapT1 = Enum2T1(arrPht, B0)
|
|
187
|
-
if bSSFP:
|
|
188
|
-
if TR is None: TR = 5e-3
|
|
189
|
-
if TE is None: TE = 1e-3
|
|
190
|
-
if FA_deg is None: FA_deg = 60
|
|
191
|
-
FA = FA_deg * pi/180
|
|
192
|
-
mapT2 = Enum2T2(arrPht, B0)
|
|
193
|
-
E1 = exp(-TR/mapT1)
|
|
194
|
-
E2 = exp(-TR/mapT2)
|
|
195
|
-
mapSS = (
|
|
196
|
-
mapPD
|
|
197
|
-
* (1-E1)
|
|
198
|
-
* sqrt(E2)
|
|
199
|
-
* sin(FA)
|
|
200
|
-
/ (1 - (E1-E2)*cos(FA) - E1*E2)
|
|
201
|
-
) + 0j
|
|
202
|
-
else:
|
|
203
|
-
if TR is None: TR = 10e-3
|
|
204
|
-
if TE is None: TE = 1e-3
|
|
205
|
-
if FA_deg is None: FA_deg = 10
|
|
206
|
-
FA = FA_deg * pi/180
|
|
207
|
-
mapT2s = Enum2T2s(arrPht, B0)
|
|
208
|
-
E1 = exp(-TR/mapT1)
|
|
209
|
-
E2 = exp(-TE/mapT2s)
|
|
210
|
-
mapSS = (
|
|
211
|
-
mapPD
|
|
212
|
-
*sin(FA)
|
|
213
|
-
*(1-E1)
|
|
214
|
-
/(1-cos(FA)*E1)
|
|
215
|
-
*E2
|
|
216
|
-
) + 0j
|
|
217
|
-
kInFow = 1.5 + max(-ampCar,0)*20
|
|
218
|
-
mapSS[arrPht==Tissue.Blood.value] *= kInFow
|
|
219
|
-
mapSS[arrPht==Tissue.Vessel.value] *= kInFow
|
|
220
|
-
if mapPh is not None:
|
|
221
|
-
for iTissue in range(Tissue.NTissue.value):
|
|
222
|
-
mapSS[arrPht==iTissue] *= mapPh[iTissue][arrPht==iTissue]
|
|
223
|
-
return mapSS
|
|
224
|
-
|
|
225
|
-
def Enum2PD(arrPht:NDArray) -> NDArray:
|
|
226
|
-
"""
|
|
227
|
-
get PD map of a phantom generated by `genPhant()`
|
|
228
|
-
|
|
229
|
-
Args:
|
|
230
|
-
arrPht: phantom
|
|
231
|
-
|
|
232
|
-
Returns:
|
|
233
|
-
Proton density map of the given phantom, relevant to water
|
|
234
|
-
"""
|
|
235
|
-
mapPD = zeros_like(arrPht, dtype=float64)
|
|
236
|
-
mapPD[arrPht==Tissue.Air.value] = 0
|
|
237
|
-
mapPD[arrPht==Tissue.Fat.value] = 0.95
|
|
238
|
-
mapPD[arrPht==Tissue.Myo.value] = 0.80
|
|
239
|
-
mapPD[arrPht==Tissue.Blood.value] = 0.95
|
|
240
|
-
mapPD[arrPht==Tissue.Liver.value] = 0.90
|
|
241
|
-
mapPD[arrPht==Tissue.Vessel.value] = 0.95
|
|
242
|
-
return mapPD
|
|
243
|
-
|
|
244
|
-
def Enum2T1(arrPht:NDArray, B0:int|float) -> NDArray:
|
|
245
|
-
"""
|
|
246
|
-
get T1 map of a phantom generated by `genPhant()`
|
|
247
|
-
|
|
248
|
-
Args:
|
|
249
|
-
arrPht: phantom
|
|
250
|
-
|
|
251
|
-
Returns:
|
|
252
|
-
T1 map of the given phantom
|
|
253
|
-
"""
|
|
254
|
-
mapT1 = zeros_like(arrPht, dtype=float64)
|
|
255
|
-
if B0==0.55:
|
|
256
|
-
mapT1[arrPht==Tissue.Air.value] = inf
|
|
257
|
-
mapT1[arrPht==Tissue.Fat.value] = 280e-3
|
|
258
|
-
mapT1[arrPht==Tissue.Myo.value] = 700e-3
|
|
259
|
-
mapT1[arrPht==Tissue.Blood.value] = 1120e-3
|
|
260
|
-
mapT1[arrPht==Tissue.Liver.value] = 450e-3
|
|
261
|
-
mapT1[arrPht==Tissue.Vessel.value] = 1120e-3
|
|
262
|
-
elif B0==1.5:
|
|
263
|
-
mapT1[arrPht==Tissue.Air.value] = inf
|
|
264
|
-
mapT1[arrPht==Tissue.Fat.value] = 350e-3
|
|
265
|
-
mapT1[arrPht==Tissue.Myo.value] = 1030e-3
|
|
266
|
-
mapT1[arrPht==Tissue.Blood.value] = 1450e-3
|
|
267
|
-
mapT1[arrPht==Tissue.Liver.value] = 1580e-3
|
|
268
|
-
mapT1[arrPht==Tissue.Vessel.value] = 1450e-3
|
|
269
|
-
elif B0==3.0:
|
|
270
|
-
mapT1[arrPht==Tissue.Air.value] = inf
|
|
271
|
-
mapT1[arrPht==Tissue.Fat.value] = 400e-3
|
|
272
|
-
mapT1[arrPht==Tissue.Myo.value] = 1200e-3
|
|
273
|
-
mapT1[arrPht==Tissue.Blood.value] = 1800e-3
|
|
274
|
-
mapT1[arrPht==Tissue.Liver.value] = 800e-3
|
|
275
|
-
mapT1[arrPht==Tissue.Vessel.value] = 1800e-3
|
|
276
|
-
elif B0==5.0:
|
|
277
|
-
mapT1[arrPht==Tissue.Air.value] = inf
|
|
278
|
-
mapT1[arrPht==Tissue.Fat.value] = 450e-3
|
|
279
|
-
mapT1[arrPht==Tissue.Myo.value] = 1450e-3
|
|
280
|
-
mapT1[arrPht==Tissue.Blood.value] = 2100e-3
|
|
281
|
-
mapT1[arrPht==Tissue.Liver.value] = 1000e-3
|
|
282
|
-
mapT1[arrPht==Tissue.Vessel.value] = 2100e-3
|
|
283
|
-
elif B0==9.4:
|
|
284
|
-
mapT1[arrPht==Tissue.Air.value] = inf
|
|
285
|
-
mapT1[arrPht==Tissue.Fat.value] = 500e-3
|
|
286
|
-
mapT1[arrPht==Tissue.Myo.value] = 1800e-3
|
|
287
|
-
mapT1[arrPht==Tissue.Blood.value] = 2500e-3
|
|
288
|
-
mapT1[arrPht==Tissue.Liver.value] = 1200e-3
|
|
289
|
-
mapT1[arrPht==Tissue.Vessel.value] = 2500e-3
|
|
290
|
-
else:
|
|
291
|
-
raise RuntimeError(f"B0={B0:.2f} not available.")
|
|
292
|
-
return mapT1
|
|
293
|
-
|
|
294
|
-
def Enum2T2(arrPht:NDArray, B0:int|float) -> NDArray:
|
|
295
|
-
"""
|
|
296
|
-
get T2 map of a phantom generated by `genPhant()`
|
|
297
|
-
|
|
298
|
-
Args:
|
|
299
|
-
arrPht: phantom
|
|
300
|
-
|
|
301
|
-
Returns:
|
|
302
|
-
T2 map of the given phantom
|
|
303
|
-
"""
|
|
304
|
-
mapT2 = zeros_like(arrPht, dtype=float64)
|
|
305
|
-
if B0==0.55:
|
|
306
|
-
mapT2[arrPht==Tissue.Air.value] = 1e-6
|
|
307
|
-
mapT2[arrPht==Tissue.Fat.value] = 100e-3
|
|
308
|
-
mapT2[arrPht==Tissue.Myo.value] = 60e-3
|
|
309
|
-
mapT2[arrPht==Tissue.Blood.value] = 260e-3
|
|
310
|
-
mapT2[arrPht==Tissue.Liver.value] = 55e-3
|
|
311
|
-
mapT2[arrPht==Tissue.Vessel.value] = 260e-3
|
|
312
|
-
elif B0==1.5:
|
|
313
|
-
mapT2[arrPht==Tissue.Air.value] = 1e-6
|
|
314
|
-
mapT2[arrPht==Tissue.Fat.value] = 80e-3
|
|
315
|
-
mapT2[arrPht==Tissue.Myo.value] = 45e-3
|
|
316
|
-
mapT2[arrPht==Tissue.Blood.value] = 275e-3
|
|
317
|
-
mapT2[arrPht==Tissue.Liver.value] = 46e-3
|
|
318
|
-
mapT2[arrPht==Tissue.Vessel.value] = 275e-3
|
|
319
|
-
elif B0==3.0:
|
|
320
|
-
mapT2[arrPht==Tissue.Air.value] = 1e-6
|
|
321
|
-
mapT2[arrPht==Tissue.Fat.value] = 70e-3
|
|
322
|
-
mapT2[arrPht==Tissue.Myo.value] = 40e-3
|
|
323
|
-
mapT2[arrPht==Tissue.Blood.value] = 120e-3
|
|
324
|
-
mapT2[arrPht==Tissue.Liver.value] = 40e-3
|
|
325
|
-
mapT2[arrPht==Tissue.Vessel.value] = 120e-3
|
|
326
|
-
elif B0==5.0:
|
|
327
|
-
mapT2[arrPht==Tissue.Air.value] = 1e-6
|
|
328
|
-
mapT2[arrPht==Tissue.Fat.value] = 60e-3
|
|
329
|
-
mapT2[arrPht==Tissue.Myo.value] = 35e-3
|
|
330
|
-
mapT2[arrPht==Tissue.Blood.value] = 90e-3
|
|
331
|
-
mapT2[arrPht==Tissue.Liver.value] = 34e-3
|
|
332
|
-
mapT2[arrPht==Tissue.Vessel.value] = 90e-3
|
|
333
|
-
elif B0==9.4:
|
|
334
|
-
mapT2[arrPht==Tissue.Air.value] = 1e-6
|
|
335
|
-
mapT2[arrPht==Tissue.Fat.value] = 50e-3
|
|
336
|
-
mapT2[arrPht==Tissue.Myo.value] = 20e-3
|
|
337
|
-
mapT2[arrPht==Tissue.Blood.value] = 40e-3
|
|
338
|
-
mapT2[arrPht==Tissue.Liver.value] = 25e-3
|
|
339
|
-
mapT2[arrPht==Tissue.Vessel.value] = 40e-3
|
|
340
|
-
else:
|
|
341
|
-
raise RuntimeError(f"B0={B0:.2f} not available.")
|
|
342
|
-
return mapT2
|
|
343
|
-
|
|
344
|
-
def Enum2T2s(arrPht:NDArray, B0:int|float) -> NDArray:
|
|
345
|
-
"""
|
|
346
|
-
get T2* map of a phantom generated by `genPhant()`
|
|
347
|
-
|
|
348
|
-
Args:
|
|
349
|
-
arrPht: phantom
|
|
350
|
-
|
|
351
|
-
Returns:
|
|
352
|
-
T2* map of the given phantom
|
|
353
|
-
"""
|
|
354
|
-
mapT2s = zeros_like(arrPht, dtype=float64)
|
|
355
|
-
if B0==0.55:
|
|
356
|
-
mapT2s[arrPht==Tissue.Air.value] = 1e-6
|
|
357
|
-
mapT2s[arrPht==Tissue.Fat.value] = 70e-3
|
|
358
|
-
mapT2s[arrPht==Tissue.Myo.value] = 50e-3
|
|
359
|
-
mapT2s[arrPht==Tissue.Blood.value] = 80e-3
|
|
360
|
-
mapT2s[arrPht==Tissue.Liver.value] = 45e-3
|
|
361
|
-
mapT2s[arrPht==Tissue.Vessel.value] = 80e-3
|
|
362
|
-
elif B0==1.5:
|
|
363
|
-
mapT2s[arrPht==Tissue.Air.value] = 1e-6
|
|
364
|
-
mapT2s[arrPht==Tissue.Fat.value] = 50e-3
|
|
365
|
-
mapT2s[arrPht==Tissue.Myo.value] = 33e-3
|
|
366
|
-
mapT2s[arrPht==Tissue.Blood.value] = 55e-3
|
|
367
|
-
mapT2s[arrPht==Tissue.Liver.value] = 30e-3
|
|
368
|
-
mapT2s[arrPht==Tissue.Vessel.value] = 55e-3
|
|
369
|
-
elif B0==3.0:
|
|
370
|
-
mapT2s[arrPht==Tissue.Air.value] = 1e-6
|
|
371
|
-
mapT2s[arrPht==Tissue.Fat.value] = 35e-3
|
|
372
|
-
mapT2s[arrPht==Tissue.Myo.value] = 22e-3
|
|
373
|
-
mapT2s[arrPht==Tissue.Blood.value] = 30e-3
|
|
374
|
-
mapT2s[arrPht==Tissue.Liver.value] = 18e-3
|
|
375
|
-
mapT2s[arrPht==Tissue.Vessel.value] = 30e-3
|
|
376
|
-
elif B0==5.0:
|
|
377
|
-
mapT2s[arrPht==Tissue.Air.value] = 1e-6
|
|
378
|
-
mapT2s[arrPht==Tissue.Fat.value] = 25e-3
|
|
379
|
-
mapT2s[arrPht==Tissue.Myo.value] = 15e-3
|
|
380
|
-
mapT2s[arrPht==Tissue.Blood.value] = 18e-3
|
|
381
|
-
mapT2s[arrPht==Tissue.Liver.value] = 10e-3
|
|
382
|
-
mapT2s[arrPht==Tissue.Vessel.value] = 18e-3
|
|
383
|
-
elif B0==9.4:
|
|
384
|
-
mapT2s[arrPht==Tissue.Air.value] = 1e-6
|
|
385
|
-
mapT2s[arrPht==Tissue.Fat.value] = 15e-3
|
|
386
|
-
mapT2s[arrPht==Tissue.Myo.value] = 8e-3
|
|
387
|
-
mapT2s[arrPht==Tissue.Blood.value] = 10e-3
|
|
388
|
-
mapT2s[arrPht==Tissue.Liver.value] = 5e-3
|
|
389
|
-
mapT2s[arrPht==Tissue.Vessel.value] = 10e-3
|
|
390
|
-
else:
|
|
391
|
-
raise RuntimeError(f"B0={B0:.2f} not available.")
|
|
392
|
-
return mapT2s
|
|
393
|
-
|
|
394
|
-
def Enum2Adc(arrPht:NDArray, B0:int|float) -> NDArray:
|
|
395
|
-
"""
|
|
396
|
-
get Apparent Diffusion Coefficient (ADC) map (in `m^2/s`) of a phantom generated by `genPhant()`
|
|
397
|
-
|
|
398
|
-
Args:
|
|
399
|
-
arrPht: phantom
|
|
400
|
-
B0: field strength in Tesla
|
|
401
|
-
|
|
402
|
-
Returns:
|
|
403
|
-
ADC map of the given phantom
|
|
404
|
-
"""
|
|
405
|
-
mapADC = zeros_like(arrPht, dtype=float64)
|
|
406
|
-
if B0 in [0.55, 1.5, 3.0, 5.0, 9.4]:
|
|
407
|
-
mapADC[arrPht==Tissue.Air.value] = 0.0
|
|
408
|
-
mapADC[arrPht==Tissue.Fat.value] = 0.15e-9
|
|
409
|
-
mapADC[arrPht==Tissue.Myo.value] = 1.55e-9
|
|
410
|
-
mapADC[arrPht==Tissue.Blood.value] = 2.10e-9
|
|
411
|
-
mapADC[arrPht==Tissue.Liver.value] = 1.15e-9
|
|
412
|
-
mapADC[arrPht==Tissue.Vessel.value] = 2.10e-9
|
|
413
|
-
else:
|
|
414
|
-
raise RuntimeError(f"B0={B0:.2f} not available.")
|
|
415
|
-
return mapADC
|
|
416
|
-
|
|
417
|
-
def Enum2Om(arrPht:NDArray, B0:int|float) -> NDArray:
|
|
418
|
-
"""
|
|
419
|
-
get off-resonance map (in `rad/s`) of a phantom generated by `genPhant()`
|
|
420
|
-
|
|
421
|
-
Args:
|
|
422
|
-
arrPht: phantom
|
|
423
|
-
B0: field strength in Tesla
|
|
424
|
-
|
|
425
|
-
Returns:
|
|
426
|
-
off-resonance map of the given phantom
|
|
427
|
-
"""
|
|
428
|
-
mapOm = zeros_like(arrPht, dtype=float64)
|
|
429
|
-
ppm2om = 1e-6*(2*pi*42.58e6*B0)
|
|
430
|
-
mapOm[arrPht==Tissue.Air.value] = 0
|
|
431
|
-
mapOm[arrPht==Tissue.Fat.value] = 3.5*ppm2om
|
|
432
|
-
mapOm[arrPht==Tissue.Myo.value] = 0
|
|
433
|
-
mapOm[arrPht==Tissue.Blood.value] = 0
|
|
434
|
-
mapOm[arrPht==Tissue.Liver.value] = 0
|
|
435
|
-
mapOm[arrPht==Tissue.Vessel.value] = 0
|
|
436
|
-
return mapOm
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|