MRPhantom 0.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mrphantom-0.0.0/LICENSE +7 -0
- mrphantom-0.0.0/MANIFEST.in +1 -0
- mrphantom-0.0.0/MRPhantom.egg-info/PKG-INFO +16 -0
- mrphantom-0.0.0/MRPhantom.egg-info/SOURCES.txt +21 -0
- mrphantom-0.0.0/MRPhantom.egg-info/dependency_links.txt +1 -0
- mrphantom-0.0.0/MRPhantom.egg-info/requires.txt +1 -0
- mrphantom-0.0.0/MRPhantom.egg-info/top_level.txt +1 -0
- mrphantom-0.0.0/PKG-INFO +16 -0
- mrphantom-0.0.0/README.md +2 -0
- mrphantom-0.0.0/example/Example_2D.py +37 -0
- mrphantom-0.0.0/example/Example_3D.py +42 -0
- mrphantom-0.0.0/example/Example_Simple.py +63 -0
- mrphantom-0.0.0/example/Example_Utility.py +61 -0
- mrphantom-0.0.0/pyproject.toml +20 -0
- mrphantom-0.0.0/setup.cfg +4 -0
- mrphantom-0.0.0/setup.py +39 -0
- mrphantom-0.0.0/slime_src/Function.py +144 -0
- mrphantom-0.0.0/slime_src/__init__.py +5 -0
- mrphantom-0.0.0/slime_src/ext/main.cpp +66 -0
- mrphantom-0.0.0/slime_src/ext/slime.cpp +163 -0
- mrphantom-0.0.0/slime_src/ext/slime.h +11 -0
mrphantom-0.0.0/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2025 Rui Luo
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
global-include *.py *.c *.cpp *.h *.hpp
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: MRPhantom
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: MRI Phantom of a Slime with Repiratory and Cardiac Motion, and M0, Phase, T1, T2, B0 maps, Boosted by a C-API Backend.
|
|
5
|
+
Author-email: Ryan <ryan_shanghaitech@proton.me>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/RyanShanghaitech/Slime
|
|
8
|
+
Project-URL: Examples, https://github.com/RyanShanghaitech/Slime/tree/main/example
|
|
9
|
+
Keywords: MRI,Phantom,Motion,Quantitative
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: numpy
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
# MRI Digital Phantom
|
|
16
|
+
A quantitative phantom of a slime with cardiac and respiratory motion.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
setup.py
|
|
6
|
+
./slime_src/ext/main.cpp
|
|
7
|
+
./slime_src/ext/slime.cpp
|
|
8
|
+
MRPhantom.egg-info/PKG-INFO
|
|
9
|
+
MRPhantom.egg-info/SOURCES.txt
|
|
10
|
+
MRPhantom.egg-info/dependency_links.txt
|
|
11
|
+
MRPhantom.egg-info/requires.txt
|
|
12
|
+
MRPhantom.egg-info/top_level.txt
|
|
13
|
+
example/Example_2D.py
|
|
14
|
+
example/Example_3D.py
|
|
15
|
+
example/Example_Simple.py
|
|
16
|
+
example/Example_Utility.py
|
|
17
|
+
slime_src/Function.py
|
|
18
|
+
slime_src/__init__.py
|
|
19
|
+
slime_src/ext/main.cpp
|
|
20
|
+
slime_src/ext/slime.cpp
|
|
21
|
+
slime_src/ext/slime.h
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
numpy
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
slime
|
mrphantom-0.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: MRPhantom
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: MRI Phantom of a Slime with Repiratory and Cardiac Motion, and M0, Phase, T1, T2, B0 maps, Boosted by a C-API Backend.
|
|
5
|
+
Author-email: Ryan <ryan_shanghaitech@proton.me>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/RyanShanghaitech/Slime
|
|
8
|
+
Project-URL: Examples, https://github.com/RyanShanghaitech/Slime/tree/main/example
|
|
9
|
+
Keywords: MRI,Phantom,Motion,Quantitative
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: numpy
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
# MRI Digital Phantom
|
|
16
|
+
A quantitative phantom of a slime with cardiac and respiratory motion.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from numpy import *
|
|
2
|
+
from matplotlib.pyplot import *
|
|
3
|
+
import slime
|
|
4
|
+
from time import time
|
|
5
|
+
|
|
6
|
+
tScan = 10
|
|
7
|
+
tRes = 10e-3
|
|
8
|
+
nT = int(tScan/tRes)
|
|
9
|
+
nPix = 256
|
|
10
|
+
|
|
11
|
+
cycRes = pi/2
|
|
12
|
+
cycCar = 1
|
|
13
|
+
arrAmpRes = 20e-3*slime.genAmp(tScan, tRes, cycRes, 1)
|
|
14
|
+
arrAmpCar = 10e-3*slime.genAmp(tScan, tRes, cycCar, 0)
|
|
15
|
+
|
|
16
|
+
fig = figure()
|
|
17
|
+
ax = fig.add_subplot(211)
|
|
18
|
+
ax.plot(arrAmpRes, ".-")
|
|
19
|
+
ax.set_title("Respiratory")
|
|
20
|
+
ax = fig.add_subplot(212)
|
|
21
|
+
ax.plot(arrAmpCar, ".-")
|
|
22
|
+
ax.set_title("Cardiac")
|
|
23
|
+
|
|
24
|
+
fig = figure(figsize=(6,6), dpi=120)
|
|
25
|
+
ax = fig.add_subplot(111)
|
|
26
|
+
axim = ax.imshow(zeros([nPix,nPix]), cmap='gray', vmin=0, vmax=1)
|
|
27
|
+
|
|
28
|
+
while 1:
|
|
29
|
+
for iT in range(nT):
|
|
30
|
+
arrPhant = slime.genPhant(2, nPix, arrAmpRes[iT], arrAmpCar[iT])
|
|
31
|
+
arrM0 = slime.Enum2M0(arrPhant, arrAmpCar[iT])
|
|
32
|
+
|
|
33
|
+
axim.set_data(arrM0)
|
|
34
|
+
ax.set_title(f"time: {iT*tRes:.2f}s")
|
|
35
|
+
draw()
|
|
36
|
+
pause(tRes/10)
|
|
37
|
+
# pause(1e-2)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from numpy import *
|
|
2
|
+
from matplotlib.pyplot import *
|
|
3
|
+
import slime
|
|
4
|
+
from time import time
|
|
5
|
+
|
|
6
|
+
tScan = 10
|
|
7
|
+
tRes = 10e-3
|
|
8
|
+
nT = int(tScan/tRes)
|
|
9
|
+
nPix = 256
|
|
10
|
+
|
|
11
|
+
cycRes = pi/2
|
|
12
|
+
cycCar = 1
|
|
13
|
+
arrAmpRes = 20e-3*slime.genAmp(tScan, tRes, cycRes, 1)
|
|
14
|
+
arrAmpCar = 10e-3*slime.genAmp(tScan, tRes, cycCar, 0)
|
|
15
|
+
|
|
16
|
+
fig = figure()
|
|
17
|
+
ax = fig.add_subplot(211)
|
|
18
|
+
ax.plot(arrAmpRes, ".-")
|
|
19
|
+
ax.set_title("Respiratory")
|
|
20
|
+
ax = fig.add_subplot(212)
|
|
21
|
+
ax.plot(arrAmpCar, ".-")
|
|
22
|
+
ax.set_title("Cardiac")
|
|
23
|
+
|
|
24
|
+
fig = figure(figsize=(9,3), dpi=120)
|
|
25
|
+
ax1 = fig.add_subplot(131)
|
|
26
|
+
axim1 = ax1.imshow(zeros([nPix,nPix]), cmap='gray', vmin=0, vmax=1)
|
|
27
|
+
ax2 = fig.add_subplot(132)
|
|
28
|
+
axim2 = ax2.imshow(zeros([nPix,nPix]), cmap='gray', vmin=0, vmax=1)
|
|
29
|
+
ax3 = fig.add_subplot(133)
|
|
30
|
+
axim3 = ax3.imshow(zeros([nPix,nPix]), cmap='gray', vmin=0, vmax=1)
|
|
31
|
+
|
|
32
|
+
while 1:
|
|
33
|
+
for iT in range(0,nT,10):
|
|
34
|
+
arrPhant = slime.genPhant(3, nPix, arrAmpRes[iT], arrAmpCar[iT])
|
|
35
|
+
arrM0 = slime.Enum2M0(arrPhant, arrAmpCar[iT])
|
|
36
|
+
|
|
37
|
+
axim1.set_data(arrM0[nPix//2,:,:])
|
|
38
|
+
axim2.set_data(arrM0[:,nPix//2,:])
|
|
39
|
+
axim3.set_data(arrM0[:,:,nPix//2])
|
|
40
|
+
ax2.set_title(f"time: {iT*tRes:.2f}s")
|
|
41
|
+
draw()
|
|
42
|
+
pause(tRes/10)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from numpy import *
|
|
2
|
+
from matplotlib.pyplot import *
|
|
3
|
+
import slime
|
|
4
|
+
|
|
5
|
+
# 2D
|
|
6
|
+
nPix = 4096
|
|
7
|
+
arrPhant = slime.genPhant(nAx=2, nPix=nPix)
|
|
8
|
+
arrM0 = slime.Enum2M0(arrPhant)
|
|
9
|
+
|
|
10
|
+
arrM0Abs = abs(arrM0)
|
|
11
|
+
figure(figsize=(3,3), dpi=120)
|
|
12
|
+
imshow(arrM0Abs, cmap="gray"); colorbar()
|
|
13
|
+
draw(); pause(0.5)
|
|
14
|
+
|
|
15
|
+
# 3D
|
|
16
|
+
nPix = 256
|
|
17
|
+
arrPhant = slime.genPhant(nAx=3, nPix=nPix)
|
|
18
|
+
arrM0 = slime.Enum2M0(arrPhant)
|
|
19
|
+
|
|
20
|
+
arrM0Abs = abs(arrM0)
|
|
21
|
+
figure(figsize=(9,3), dpi=120)
|
|
22
|
+
subplot(131)
|
|
23
|
+
imshow(arrM0Abs[nPix//2,:,:], cmap="gray"); colorbar()
|
|
24
|
+
subplot(132)
|
|
25
|
+
imshow(arrM0Abs[:,nPix//2,:], cmap="gray"); colorbar()
|
|
26
|
+
subplot(133)
|
|
27
|
+
imshow(arrM0Abs[:,:,nPix//2], cmap="gray"); colorbar()
|
|
28
|
+
draw(); pause(0.5)
|
|
29
|
+
|
|
30
|
+
# --- NEW FIGURE: animated 3D slice viewer (does not modify the static figure) ---
|
|
31
|
+
V = arrM0Abs # already computed above (shape: [nPix,nPix,nPix])
|
|
32
|
+
|
|
33
|
+
vmin = V.min()
|
|
34
|
+
vmax = V.max()
|
|
35
|
+
|
|
36
|
+
figA = figure(figsize=(9, 3), dpi=120)
|
|
37
|
+
ax1 = figA.add_subplot(131)
|
|
38
|
+
ax2 = figA.add_subplot(132)
|
|
39
|
+
ax3 = figA.add_subplot(133)
|
|
40
|
+
|
|
41
|
+
k0 = nPix // 2
|
|
42
|
+
im1 = ax1.imshow(V[k0, :, :], cmap="gray", vmin=vmin, vmax=vmax, origin="lower")
|
|
43
|
+
im2 = ax2.imshow(V[:, k0, :], cmap="gray", vmin=vmin, vmax=vmax, origin="lower")
|
|
44
|
+
im3 = ax3.imshow(V[:, :, k0], cmap="gray", vmin=vmin, vmax=vmax, origin="lower")
|
|
45
|
+
|
|
46
|
+
ax1.set_title(f"axial z={k0}")
|
|
47
|
+
ax2.set_title(f"coronal y={k0}")
|
|
48
|
+
ax3.set_title(f"sagittal x={k0}")
|
|
49
|
+
|
|
50
|
+
tight_layout()
|
|
51
|
+
draw()
|
|
52
|
+
pause(0.25)
|
|
53
|
+
|
|
54
|
+
pause_dt = 1e-6 # seconds; increase if too fast
|
|
55
|
+
for k in list(range(nPix)) + list(range(nPix-2, 0, -1)):
|
|
56
|
+
im1.set_data(V[k, :, :]); ax1.set_title(f"axial z={k}")
|
|
57
|
+
im2.set_data(V[:, k, :]); ax2.set_title(f"coronal y={k}")
|
|
58
|
+
im3.set_data(V[:, :, k]); ax3.set_title(f"sagittal x={k}")
|
|
59
|
+
figA.canvas.draw_idle()
|
|
60
|
+
pause(pause_dt)
|
|
61
|
+
# --- end block ---
|
|
62
|
+
|
|
63
|
+
show()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import slime
|
|
2
|
+
from numpy import *
|
|
3
|
+
from matplotlib.pyplot import *
|
|
4
|
+
from matplotlib.colors import ListedColormap
|
|
5
|
+
|
|
6
|
+
nDim = 3
|
|
7
|
+
nPix = 128
|
|
8
|
+
|
|
9
|
+
mapPh = slime.genPhMap(nDim, nPix, std=pi/16)
|
|
10
|
+
mapB0 = slime.genB0Map(nDim, nPix, std=1) # unit: ppm
|
|
11
|
+
|
|
12
|
+
arrPhant = slime.genPhant(nDim, nPix)
|
|
13
|
+
mapM0 = slime.Enum2M0(arrPhant)
|
|
14
|
+
mapT1 = slime.Enum2T1(arrPhant)
|
|
15
|
+
mapT2 = slime.Enum2T2(arrPhant)
|
|
16
|
+
mapOm = slime.Enum2Om(arrPhant)
|
|
17
|
+
mapC = slime.genCsm(nDim, nPix, mean=0, std=pi/16)
|
|
18
|
+
|
|
19
|
+
# plot
|
|
20
|
+
cmT1 = ListedColormap(loadtxt("./Resource/lipari.csv"), name="T1")
|
|
21
|
+
cmT2 = ListedColormap(loadtxt("./Resource/navia.csv"), name="T2")
|
|
22
|
+
|
|
23
|
+
figure(figsize=(9,5), dpi=120)
|
|
24
|
+
subplot(121)
|
|
25
|
+
imshow(angle(mapPh[:,nPix//2,:]), cmap="hsv", vmin=-pi, vmax=pi)
|
|
26
|
+
colorbar()
|
|
27
|
+
title("phase map")
|
|
28
|
+
subplot(122)
|
|
29
|
+
imshow(mapB0[:,nPix//2,:], vmin=-3, vmax=3)
|
|
30
|
+
colorbar().set_label("ppm")
|
|
31
|
+
title("B0 map")
|
|
32
|
+
|
|
33
|
+
mapM0Abs = abs(mapM0)
|
|
34
|
+
figure(figsize=(9,9), dpi=120)
|
|
35
|
+
subplot(221)
|
|
36
|
+
imshow(mapM0Abs[:,nPix//2,:], cmap="gray"); colorbar(); title("M0 map")
|
|
37
|
+
subplot(222)
|
|
38
|
+
imshow(mapT1[:,nPix//2,:]*1000, cmap=cmT1); colorbar().set_label("ms"); title("T1 map")
|
|
39
|
+
subplot(223)
|
|
40
|
+
imshow(mapT2[:,nPix//2,:]*1000, cmap=cmT2); colorbar().set_label("ms"); title("T2 map")
|
|
41
|
+
subplot(224)
|
|
42
|
+
imshow(mapOm[:,nPix//2,:]); colorbar(); title("Om map")
|
|
43
|
+
|
|
44
|
+
mapCsmAbs = abs(mapC)
|
|
45
|
+
mapCsmAng = angle(mapC)
|
|
46
|
+
fig = figure(figsize=(9,9), dpi=120)
|
|
47
|
+
gs = fig.add_gridspec(2,1)
|
|
48
|
+
|
|
49
|
+
subfig = fig.add_subfigure(gs[0])
|
|
50
|
+
for iFig in range(3*4):
|
|
51
|
+
ax = subfig.add_subplot(3,4,iFig+1)
|
|
52
|
+
axim = ax.imshow(mapCsmAbs[iFig,nPix-2,:,:], cmap="gray", vmin=0, vmax=1)
|
|
53
|
+
subfig.colorbar(axim)
|
|
54
|
+
|
|
55
|
+
subfig = fig.add_subfigure(gs[1])
|
|
56
|
+
for iFig in range(3*4):
|
|
57
|
+
ax = subfig.add_subplot(3,4,iFig+1)
|
|
58
|
+
axim = ax.imshow(mapCsmAng[iFig,nPix-2,:,:], cmap="hsv", vmin=-pi, vmax=pi)
|
|
59
|
+
subfig.colorbar(axim)
|
|
60
|
+
|
|
61
|
+
show()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools", "numpy"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "MRPhantom"
|
|
7
|
+
version = "0.0.0"
|
|
8
|
+
dependencies = ["numpy"]
|
|
9
|
+
|
|
10
|
+
description = "MRI Phantom of a Slime with Repiratory and Cardiac Motion, and M0, Phase, T1, T2, B0 maps, Boosted by a C-API Backend."
|
|
11
|
+
keywords = ["MRI", "Phantom", "Motion", "Quantitative"]
|
|
12
|
+
authors = [{name = "Ryan", email = "ryan_shanghaitech@proton.me"}]
|
|
13
|
+
|
|
14
|
+
license = "MIT"
|
|
15
|
+
license-files = ["LICENSE"]
|
|
16
|
+
readme = "README.md"
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Repository = "https://github.com/RyanShanghaitech/Slime"
|
|
20
|
+
Examples = "https://github.com/RyanShanghaitech/Slime/tree/main/example"
|
mrphantom-0.0.0/setup.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from setuptools import setup, Extension
|
|
2
|
+
import numpy
|
|
3
|
+
|
|
4
|
+
_sources = \
|
|
5
|
+
[
|
|
6
|
+
'./slime_src/ext/main.cpp',
|
|
7
|
+
'./slime_src/ext/slime.cpp',
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
modExt = Extension\
|
|
11
|
+
(
|
|
12
|
+
"slime.ext",
|
|
13
|
+
sources = _sources,
|
|
14
|
+
include_dirs = ["./slime_src/ext/", numpy.get_include()],
|
|
15
|
+
language = 'c++',
|
|
16
|
+
extra_compile_args = ["-O3", "-fopenmp"],
|
|
17
|
+
extra_link_args = ["-fopenmp"],
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
_packages = \
|
|
21
|
+
[
|
|
22
|
+
"slime",
|
|
23
|
+
"slime.ext",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
_package_dir = \
|
|
27
|
+
{
|
|
28
|
+
"slime":"./slime_src/",
|
|
29
|
+
"slime.ext":"./slime_src/ext/",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
setup\
|
|
33
|
+
(
|
|
34
|
+
name = 'slime',
|
|
35
|
+
ext_modules = [modExt],
|
|
36
|
+
packages = _packages,
|
|
37
|
+
package_dir = _package_dir,
|
|
38
|
+
include_package_data = True
|
|
39
|
+
)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import slime.ext as ext
|
|
2
|
+
from numpy import *
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from scipy.ndimage import gaussian_filter
|
|
5
|
+
|
|
6
|
+
class Tissue(Enum):
|
|
7
|
+
Air = 0
|
|
8
|
+
Fat = 1
|
|
9
|
+
Myo = 2
|
|
10
|
+
Blood = 3
|
|
11
|
+
Fill = 4
|
|
12
|
+
Vessel = 5
|
|
13
|
+
|
|
14
|
+
def genPhant(nAx:int=2, nPix:int=256, ampRes:float=0, ampCar:float=0) -> ndarray: # call C++ backend to generate a phantom
|
|
15
|
+
'''
|
|
16
|
+
nAx: number of dimensions
|
|
17
|
+
nPix: number of pixels
|
|
18
|
+
ampRes: respiratory motion amplitude
|
|
19
|
+
ampCar: cardiac motion amplitude
|
|
20
|
+
'''
|
|
21
|
+
return ext.genPhant(nAx, nPix, ampRes, ampCar)
|
|
22
|
+
|
|
23
|
+
def genPhMap(nAx:int=2, nPix:int=256, mean:int|float|None=None, std:int|float=pi/16) -> ndarray:
|
|
24
|
+
'''
|
|
25
|
+
# return
|
|
26
|
+
smooth complex rotation factor
|
|
27
|
+
'''
|
|
28
|
+
if mean == None: mean = random.uniform(-pi,pi)
|
|
29
|
+
mapPh = random.uniform(-pi, pi, [nPix for _ in range(nAx)])
|
|
30
|
+
sigma = nPix/4
|
|
31
|
+
mapPh = gaussian_filter(mapPh, sigma)
|
|
32
|
+
# normalize
|
|
33
|
+
mapPh -= mapPh.mean(); mapPh = asarray(mapPh)
|
|
34
|
+
mapPh /= mapPh.std()
|
|
35
|
+
mapPh *= std
|
|
36
|
+
mapPh += mean
|
|
37
|
+
# convert to rotation factor
|
|
38
|
+
mapPh = exp(1j*mapPh)
|
|
39
|
+
mapPh = mapPh/abs(mapPh)
|
|
40
|
+
return mapPh
|
|
41
|
+
|
|
42
|
+
def genB0Map(nAx:int=2, nPix:int=256, mean:int|float=0, std:int|float=1e-6*(2*pi*42.58e6*3)) -> ndarray:
|
|
43
|
+
'''
|
|
44
|
+
# return
|
|
45
|
+
smooth random number
|
|
46
|
+
'''
|
|
47
|
+
mapB0 = random.uniform(-1, 1, [nPix for _ in range(nAx)])
|
|
48
|
+
sigma = nPix/4
|
|
49
|
+
mapB0 = gaussian_filter(mapB0, sigma)
|
|
50
|
+
# normalize
|
|
51
|
+
mapB0 -= mapB0.mean(); mapB0 = asarray(mapB0)
|
|
52
|
+
mapB0 /= mapB0.std()
|
|
53
|
+
mapB0 *= std
|
|
54
|
+
mapB0 += mean
|
|
55
|
+
return mapB0
|
|
56
|
+
|
|
57
|
+
def genCsm(nAx:int=2, nPix:int=256, nCh:int=12, mean:int|float|None=None, std:int|float=pi/16) -> ndarray:
|
|
58
|
+
if mean == None: mean = random.uniform(-pi,pi)
|
|
59
|
+
mapC = zeros([nCh,*(nPix for _ in range(nAx))], dtype=complex128)
|
|
60
|
+
arrCoor = meshgrid\
|
|
61
|
+
(
|
|
62
|
+
*(linspace(-0.5,0.5,nPix,0) for _ in range(nAx)),
|
|
63
|
+
indexing="ij"
|
|
64
|
+
); arrCoor = array(arrCoor).transpose(*arange(1,nAx+1), 0)
|
|
65
|
+
arrTht = linspace(0,2*pi,nCh,0)
|
|
66
|
+
arrCoorCoil = zeros([nCh,nAx], dtype=float64)
|
|
67
|
+
arrCoorCoil[:,-2:] = 1*array([sin(arrTht), cos(arrTht)]).T
|
|
68
|
+
if nAx == 3:
|
|
69
|
+
arrCoorCoil[0::2,0] = 0.2
|
|
70
|
+
arrCoorCoil[1::2,0] = -0.2
|
|
71
|
+
for iCh in range(nCh):
|
|
72
|
+
mapC[iCh] = genPhMap(nAx=nAx, nPix=nPix, mean=mean, std=std)
|
|
73
|
+
dist = sqrt(sum((arrCoor - arrCoorCoil[iCh])**2, axis=-1))
|
|
74
|
+
mapC[iCh] *= exp(-dist)
|
|
75
|
+
return mapC
|
|
76
|
+
|
|
77
|
+
def genAmp(tScan:int|float, tRes:int|float, cyc:int|float, isRand:bool=True) -> ndarray:
|
|
78
|
+
'''
|
|
79
|
+
# parameter
|
|
80
|
+
`tScan`: how many seconds this waveform contains
|
|
81
|
+
`tRes`: how many seconds per frame
|
|
82
|
+
`cyc`: cycle of desired signal in second
|
|
83
|
+
`isRand`: whether to randomize the waveform
|
|
84
|
+
'''
|
|
85
|
+
nT = int(tScan/tRes)
|
|
86
|
+
|
|
87
|
+
if isRand:
|
|
88
|
+
arrT = sort(random.rand(nT)*tScan)
|
|
89
|
+
arrAmp = sin(2*pi/cyc*arrT)
|
|
90
|
+
|
|
91
|
+
sigma = cyc/tRes/8
|
|
92
|
+
arrAmp = gaussian_filter(arrAmp, sigma)
|
|
93
|
+
else:
|
|
94
|
+
arrT = linspace(0, tScan, nT)
|
|
95
|
+
arrAmp = sin(2*pi/cyc*arrT)
|
|
96
|
+
|
|
97
|
+
return arrAmp
|
|
98
|
+
|
|
99
|
+
def genResAmp(tScan:int|float, tRes:int|float, cyc:int|float=pi/2) -> ndarray:
|
|
100
|
+
return 20e-3*genAmp(tScan, tRes, cyc, 1)
|
|
101
|
+
|
|
102
|
+
def genCarAmp(tScan:int|float, tRes:int|float, cyc:int|float=1) -> ndarray:
|
|
103
|
+
return 10e-3*genAmp(tScan, tRes, cyc, 0)
|
|
104
|
+
|
|
105
|
+
def Enum2M0(arrPhan:ndarray, ampCar:double=0e0) -> ndarray:
|
|
106
|
+
mapM0 = zeros_like(arrPhan, dtype=float64)
|
|
107
|
+
mapM0[arrPhan==Tissue.Air.value] = 0 # random.randn(sum(arrPhan==Part.Air.value))*1e-3
|
|
108
|
+
mapM0[arrPhan==Tissue.Fat.value] = 1.0
|
|
109
|
+
mapM0[arrPhan==Tissue.Myo.value] = 0.2
|
|
110
|
+
mapM0[arrPhan==Tissue.Blood.value] = 0.8
|
|
111
|
+
mapM0[arrPhan==Tissue.Fill.value] = 0.5
|
|
112
|
+
mapM0[arrPhan==Tissue.Vessel.value] = 0.8 - ampCar*10
|
|
113
|
+
return mapM0
|
|
114
|
+
|
|
115
|
+
def Enum2T1(arrPhan:ndarray) -> ndarray:
|
|
116
|
+
mapT1 = zeros_like(arrPhan, dtype=float64)
|
|
117
|
+
mapT1[arrPhan==Tissue.Air.value] = inf # 10000e-3 #random.uniform(1e-3, 10000e-3, sum(arrPhan==Part.Air.value))
|
|
118
|
+
mapT1[arrPhan==Tissue.Fat.value] = 350e-3
|
|
119
|
+
mapT1[arrPhan==Tissue.Myo.value] = 1300e-3
|
|
120
|
+
mapT1[arrPhan==Tissue.Blood.value] = 1500e-3
|
|
121
|
+
mapT1[arrPhan==Tissue.Fill.value] = 1600e-3
|
|
122
|
+
mapT1[arrPhan==Tissue.Vessel.value] = 1500e-3
|
|
123
|
+
return mapT1
|
|
124
|
+
|
|
125
|
+
def Enum2T2(arrPhan:ndarray) -> ndarray:
|
|
126
|
+
mapT2 = zeros_like(arrPhan, dtype=float64)
|
|
127
|
+
mapT2[arrPhan==Tissue.Air.value] = 1e-6 # random.uniform(1e-3, 1000e-3, sum(arrPhan==Part.Air.value))
|
|
128
|
+
mapT2[arrPhan==Tissue.Fat.value] = 75e-3
|
|
129
|
+
mapT2[arrPhan==Tissue.Myo.value] = 50e-3
|
|
130
|
+
mapT2[arrPhan==Tissue.Blood.value] = 225e-3
|
|
131
|
+
mapT2[arrPhan==Tissue.Fill.value] = 40e-3
|
|
132
|
+
mapT2[arrPhan==Tissue.Vessel.value] = 225e-3
|
|
133
|
+
return mapT2
|
|
134
|
+
|
|
135
|
+
def Enum2Om(arrPhan:ndarray, B0:int|float=3) -> ndarray:
|
|
136
|
+
mapOm = zeros_like(arrPhan, dtype=float64)
|
|
137
|
+
ppm2om = 1e-6*(2*pi*42.58e6*B0)
|
|
138
|
+
mapOm[arrPhan==Tissue.Air.value] = 0 # random.uniform(1e-3, 10e-3, sum(arrPhan==Part.Air.value))*ppm2om
|
|
139
|
+
mapOm[arrPhan==Tissue.Fat.value] = 3.5*ppm2om
|
|
140
|
+
mapOm[arrPhan==Tissue.Myo.value] = 0
|
|
141
|
+
mapOm[arrPhan==Tissue.Blood.value] = 0
|
|
142
|
+
mapOm[arrPhan==Tissue.Fill.value] = 0
|
|
143
|
+
mapOm[arrPhan==Tissue.Vessel.value] = 0
|
|
144
|
+
return mapOm
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#define PY_SSIZE_T_CLEAN
|
|
2
|
+
#include <Python.h>
|
|
3
|
+
#include <numpy/arrayobject.h>
|
|
4
|
+
|
|
5
|
+
#include <vector>
|
|
6
|
+
#include <cstring>
|
|
7
|
+
#include <slime.h>
|
|
8
|
+
|
|
9
|
+
bool inline checkNarg(int64_t lNarg, int64_t lNargExp)
|
|
10
|
+
{
|
|
11
|
+
if (lNarg != lNargExp)
|
|
12
|
+
{
|
|
13
|
+
printf("wrong num. of arg, narg=%ld, %ld expected\n", lNarg, lNargExp);
|
|
14
|
+
abort();
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static PyObject* genPhant_py(PyObject* self, PyObject* const* args, Py_ssize_t nargs)
|
|
21
|
+
{
|
|
22
|
+
checkNarg(nargs,4);
|
|
23
|
+
int64_t lNAx = PyLong_AsLongLong(args[0]);
|
|
24
|
+
int64_t lNPix = PyLong_AsLongLong(args[1]);
|
|
25
|
+
double dResAmp = PyFloat_AsDouble(args[2]);
|
|
26
|
+
double dCarAmp = PyFloat_AsDouble(args[3]);
|
|
27
|
+
|
|
28
|
+
// Generate into std::vector
|
|
29
|
+
std::vector<uint8_t> vu8Phant;
|
|
30
|
+
genPhant(lNAx, lNPix, dResAmp, dCarAmp, &vu8Phant);
|
|
31
|
+
|
|
32
|
+
// convert vector to numpy array
|
|
33
|
+
PyObject* ppyoNpa;
|
|
34
|
+
{
|
|
35
|
+
npy_intp aDims[] = {lNPix, lNPix, lNPix};
|
|
36
|
+
ppyoNpa = PyArray_ZEROS(lNAx, aDims, NPY_UINT8, 0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// fill the data in
|
|
40
|
+
std::memcpy(PyArray_DATA((PyArrayObject*)ppyoNpa),
|
|
41
|
+
vu8Phant.data(),
|
|
42
|
+
vu8Phant.size() * sizeof(uint8_t));
|
|
43
|
+
|
|
44
|
+
return ppyoNpa;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static PyMethodDef aMeth[] =
|
|
48
|
+
{
|
|
49
|
+
{"genPhant", (PyCFunction)genPhant_py, METH_FASTCALL, "genPhant(lNAx, lNPix, dResAmp, dCarAmp) -> np.ndarray[uint8]"},
|
|
50
|
+
{NULL, NULL, 0, NULL}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
static struct PyModuleDef sMod =
|
|
54
|
+
{
|
|
55
|
+
PyModuleDef_HEAD_INIT,
|
|
56
|
+
"ext",
|
|
57
|
+
NULL,
|
|
58
|
+
-1,
|
|
59
|
+
aMeth
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
PyMODINIT_FUNC PyInit_ext(void)
|
|
63
|
+
{
|
|
64
|
+
import_array(); // required for NumPy C-API
|
|
65
|
+
return PyModule_Create(&sMod);
|
|
66
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#include <vector>
|
|
2
|
+
#include <cstdint>
|
|
3
|
+
#include <cmath>
|
|
4
|
+
#include <stdexcept>
|
|
5
|
+
|
|
6
|
+
#include <omp.h>
|
|
7
|
+
|
|
8
|
+
// Tissue
|
|
9
|
+
enum Tissue : uint8_t
|
|
10
|
+
{
|
|
11
|
+
Air = 0,
|
|
12
|
+
Fat = 1,
|
|
13
|
+
Myo = 2,
|
|
14
|
+
Blood = 3,
|
|
15
|
+
Fill = 4, // we fill the remaining with skeletal mussles
|
|
16
|
+
Vessel = 5,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
bool isInsideEllipsoid
|
|
20
|
+
(
|
|
21
|
+
double x, double y, double z,
|
|
22
|
+
double cx, double cy, double cz,
|
|
23
|
+
double rX, double rY, double rZ
|
|
24
|
+
)
|
|
25
|
+
{
|
|
26
|
+
if (rX <= 0e0 || rY <= 0e0 || rZ <= 0e0) return false;
|
|
27
|
+
|
|
28
|
+
const double dx = x - cx;
|
|
29
|
+
const double dy = y - cy;
|
|
30
|
+
const double dz = z - cz;
|
|
31
|
+
|
|
32
|
+
const double v =
|
|
33
|
+
(dx*dx) / (rX*rX) +
|
|
34
|
+
(dy*dy) / (rY*rY) +
|
|
35
|
+
(dz*dz) / (rZ*rZ);
|
|
36
|
+
|
|
37
|
+
return v <= 1e0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
bool genPhant
|
|
41
|
+
(
|
|
42
|
+
int64_t lNAx, int64_t lNPix,
|
|
43
|
+
double dResAmp, double dCarAmp,
|
|
44
|
+
std::vector<uint8_t>* voSlime
|
|
45
|
+
)
|
|
46
|
+
{
|
|
47
|
+
const double dNPix = (double)lNPix;
|
|
48
|
+
int64_t lNPix_Flat = 0;
|
|
49
|
+
if (lNAx==2) lNPix_Flat = lNPix*lNPix;
|
|
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));
|
|
54
|
+
|
|
55
|
+
// shape parameter; r: radius, c: center
|
|
56
|
+
const double dFatOt_rY = dNPix*400e-3 + dNPix*dResAmp;
|
|
57
|
+
const double dFatOt_rX = dNPix*400e-3 - 5e-1*dNPix*dResAmp;
|
|
58
|
+
const double dFatOt_rZ = dNPix*480e-3;
|
|
59
|
+
|
|
60
|
+
const double dFatIn_rY = dNPix*380e-3 + dNPix*dResAmp;
|
|
61
|
+
const double dFatIn_rX = dNPix*380e-3 - 5e-1*dNPix*dResAmp;
|
|
62
|
+
const double dFatIn_rZ = dNPix*450e-3;
|
|
63
|
+
|
|
64
|
+
const double dMyoOt_rY = dNPix*100e-3 + dNPix*dCarAmp;
|
|
65
|
+
const double dMyoOt_rX = dNPix*120e-3 + dNPix*dCarAmp;
|
|
66
|
+
const double dMyoOt_rZ = dMyoOt_rY;
|
|
67
|
+
|
|
68
|
+
const double dMyoIn_rY = dNPix*60e-3 + dNPix*(2*dCarAmp);
|
|
69
|
+
const double dMyoIn_rX = dNPix*60e-3 + dNPix*(2*dCarAmp);
|
|
70
|
+
const double dMyoIn_rZ = dMyoIn_rY;
|
|
71
|
+
|
|
72
|
+
// Centers are at (0,0,0) in your centered coordinate system,
|
|
73
|
+
const double dFat_cx = 0e0, dFat_cy = 0e0, dFat_cz = 0e0;
|
|
74
|
+
const double dMyoOt_cx = 0e0, dMyoOt_cy = 0e0, dMyoOt_cz = 0e0;
|
|
75
|
+
const double dMyoIn_cx = -dNPix*20e-3, dMyoIn_cy = 0e0, dMyoIn_cz = 0e0;
|
|
76
|
+
|
|
77
|
+
/* generate phantom given by `dResAmp` and `dCarAmp` here */
|
|
78
|
+
#pragma omp parallel for schedule(static)
|
|
79
|
+
for (int64_t i = 0; i < lNPix_Flat; ++i)
|
|
80
|
+
{
|
|
81
|
+
const int64_t x = i % lNPix - lNPix/2;
|
|
82
|
+
const int64_t y = (i / lNPix) % lNPix - lNPix/2;
|
|
83
|
+
const int64_t z = (lNAx == 3) ? (i / (lNPix*lNPix) - lNPix/2) : 0;
|
|
84
|
+
|
|
85
|
+
// decide what tissue current pixel is
|
|
86
|
+
if (!isInsideEllipsoid(x,y,z, dFat_cx,dFat_cy,dFat_cz, dFatOt_rX,dFatOt_rY,dFatOt_rZ))
|
|
87
|
+
{
|
|
88
|
+
(*voSlime)[(size_t)i] = (uint8_t)Tissue::Air;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!isInsideEllipsoid(x,y,z, dFat_cx,dFat_cy,dFat_cz, dFatIn_rX,dFatIn_rY,dFatIn_rZ))
|
|
93
|
+
{
|
|
94
|
+
(*voSlime)[(size_t)i] = (uint8_t)Tissue::Fat;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// vessel balls (move with FatIn distortion; positioned away from heart)
|
|
99
|
+
const double dFatIn0_rX = dNPix*380e-3;
|
|
100
|
+
const double dFatIn0_rY = dNPix*380e-3;
|
|
101
|
+
const double dFatIn0_rZ = dNPix*450e-3;
|
|
102
|
+
|
|
103
|
+
const double sX = dFatIn_rX / dFatIn0_rX;
|
|
104
|
+
const double sY = dFatIn_rY / dFatIn0_rY;
|
|
105
|
+
const double sZ = dFatIn_rZ / dFatIn0_rZ;
|
|
106
|
+
|
|
107
|
+
// vessel balls
|
|
108
|
+
#define V_HIT(cx,cy,cz,div) isInsideEllipsoid(x,y,z, ((cx)*dNPix*sX), ((cy)*dNPix*sY), ((cz)*dNPix*sZ), (dFatIn_rX/(div)), (dFatIn_rY/(div)), (dFatIn_rZ/(div)))
|
|
109
|
+
|
|
110
|
+
// 48 “random” vessels (cx,cy,cz in dNPix fractions; div in ~[18..44])
|
|
111
|
+
#define VLIST \
|
|
112
|
+
/* 5 on XY plane (z = 0) — slightly farther from heart */ \
|
|
113
|
+
V_HIT( 0.18, 0.09, 0.00, 12) || \
|
|
114
|
+
V_HIT(-0.19, 0.07, 0.00, 16) || \
|
|
115
|
+
V_HIT( 0.11, -0.17, 0.00, 19) || \
|
|
116
|
+
V_HIT(-0.13, -0.16, 0.00, 21) || \
|
|
117
|
+
V_HIT( 0.04, 0.20, 0.00, 22) || \
|
|
118
|
+
/* 5 on YZ plane (x = 0) — adjusted to avoid closeness with XZ plane (y=0) */ \
|
|
119
|
+
V_HIT( 0.00, 0.14, 0.13, 12) || \
|
|
120
|
+
V_HIT( 0.00, -0.15, 0.12, 16) || \
|
|
121
|
+
V_HIT( 0.00, 0.09, -0.17, 19) || \
|
|
122
|
+
V_HIT( 0.00, -0.12, -0.16, 21) || \
|
|
123
|
+
V_HIT( 0.00, 0.23, 0.06, 22) || \
|
|
124
|
+
/* 5 on XZ plane (y = 0) — adjusted corresponding “near” one */ \
|
|
125
|
+
V_HIT( 0.16, 0.00, 0.12, 12) || \
|
|
126
|
+
V_HIT(-0.17, 0.00, 0.11, 16) || \
|
|
127
|
+
V_HIT( 0.13, 0.00, -0.16, 19) || \
|
|
128
|
+
V_HIT(-0.12, 0.00, -0.17, 21) || \
|
|
129
|
+
V_HIT( 0.23, 0.00, -0.06, 22) || \
|
|
130
|
+
/* 8 octants (near heart, slightly pushed outward) */ \
|
|
131
|
+
V_HIT( 0.14, 0.13, 0.12, 16) || /* + + + */ \
|
|
132
|
+
V_HIT(-0.15, 0.13, 0.12, 16) || /* - + + */ \
|
|
133
|
+
V_HIT( 0.14, -0.14, 0.12, 16) || /* + - + */ \
|
|
134
|
+
V_HIT(-0.15, -0.14, 0.12, 16) || /* - - + */ \
|
|
135
|
+
V_HIT( 0.14, 0.13, -0.13, 16) || /* + + - */ \
|
|
136
|
+
V_HIT(-0.15, 0.13, -0.13, 16) || /* - + - */ \
|
|
137
|
+
V_HIT( 0.14, -0.14, -0.13, 16) || /* + - - */ \
|
|
138
|
+
V_HIT(-0.15, -0.14, -0.13, 16) /* - - - */
|
|
139
|
+
|
|
140
|
+
if ( VLIST )
|
|
141
|
+
{
|
|
142
|
+
(*voSlime)[(size_t)i] = (uint8_t)Tissue::Vessel;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
// vessel balls (end)
|
|
146
|
+
|
|
147
|
+
if (!isInsideEllipsoid(x,y,z, dMyoOt_cx,dMyoOt_cy,dMyoOt_cz, dMyoOt_rX,dMyoOt_rY,dMyoOt_rZ))
|
|
148
|
+
{
|
|
149
|
+
(*voSlime)[(size_t)i] = (uint8_t)Tissue::Fill;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!isInsideEllipsoid(x,y,z, dMyoIn_cx,dMyoIn_cy,dMyoIn_cz, dMyoIn_rX,dMyoIn_rY,dMyoIn_rZ))
|
|
154
|
+
{
|
|
155
|
+
(*voSlime)[(size_t)i] = (uint8_t)Tissue::Myo;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
(*voSlime)[(size_t)i] = (uint8_t)Tissue::Blood;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return true;
|
|
163
|
+
}
|