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 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
- # Load the brainweb data file that contains info about tissues, subjects, ...
10
- brainweb_data_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
11
- "brainweb_data.json")
12
- brainweb_data = json.load(open(brainweb_data_path))
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 = os.path.join(cache_dir, file_name)
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
- data = tmp.reshape(362, 434, 362).swapaxes(0, 2).astype(np.float32)
45
-
46
- print(" - done")
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
- def downsample(array: np.ndarray, factor: int) -> np.ndarray:
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
- return tmp / factor**3
42
+ def generate_B0_B1(mask):
43
+ """Generate a somewhat plausible B0 and B1 map.
78
44
 
79
-
80
- def generate_brainweb_phantoms(
81
- output_dir: str,
82
- config: Literal["3T", "7T-noise", "3T-highres-fat"] = "3T"):
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
- All tissue data etc. are stored in [brainweb_data.json](https://github.com/MRsources/MRzero-Core/blob/main/python/MRzeroCore/phantom/brainweb/brainweb_data.json).
89
- To ensure consistent configurations and reproducible results, available
90
- configs are stored in this file as well. They specify which field strength
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
- config: ["3T", "7T-noise", "3T-highres-fat"]
111
- The configuration for which the maps are generated.
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
- config_data = brainweb_data["configs"][config]
114
- cache_dir = os.path.join(output_dir, "cache")
115
-
116
- try:
117
- os.makedirs(cache_dir)
118
- except FileExistsError:
119
- pass
120
-
121
- # Map resolution:
122
- res = np.array([362, 434, 362]) // config_data["downsample"]
123
-
124
- def noise() -> np.ndarray:
125
- return gen_noise(config_data["noise"], res)
126
-
127
- for subject in brainweb_data["subjects"]:
128
- print(f"Generating '{config}', subject {subject}")
129
- maps = {
130
- "FOV": np.array([0.181, 0.217, 0.181]),
131
- "PD_map": np.zeros(res, dtype=np.float32),
132
- "T1_map": np.zeros(res, dtype=np.float32),
133
- "T2_map": np.zeros(res, dtype=np.float32),
134
- "T2dash_map": np.zeros(res, dtype=np.float32),
135
- "D_map": np.zeros(res, dtype=np.float32),
136
- }
137
-
138
- for tissue in config_data["tissues"]:
139
- tissue_map = sum([
140
- load_tissue(subject, alias, cache_dir)
141
- for alias in brainweb_data["download-aliases"][tissue]
142
- ])
143
- tissue_map = downsample(tissue_map, config_data["downsample"])
144
- maps["tissue_" + tissue] = tissue_map
145
-
146
- field_strength = config_data["field-strength"]
147
- tissue_data = brainweb_data["tissues"][field_strength][tissue]
148
-
149
- # Separate noise maps is slower but uncorrelated.
150
- # Might be better for training or worse - could be configurable
151
- print("Adding tissue to phantom", end="", flush=True)
152
- maps["PD_map"] += tissue_data["PD"] * tissue_map * noise()
153
- maps["T1_map"] += tissue_data["T1"] * tissue_map * noise()
154
- maps["T2_map"] += tissue_data["T2"] * tissue_map * noise()
155
- maps["T2dash_map"] += tissue_data["T2'"] * tissue_map * noise()
156
- maps["D_map"] += tissue_data["D"] * tissue_map * noise()
157
- print(" - done")
158
-
159
- file = os.path.join(output_dir, f"subject{subject:02d}_{config}.npz")
160
- print(f"Saving to '{os.path.basename(file)}'", end="", flush=True)
161
- np.savez_compressed(file, **maps)
162
- print(" - done\n")
163
-
164
-
165
- if __name__ == "__main__":
166
- print("This is for testing only, use generate_brainweb_phantoms directly!")
167
- file_dir = os.path.dirname(os.path.realpath(__file__))
168
- output_dir = os.path.join(file_dir, "output")
169
-
170
- for config in brainweb_data["configs"].keys():
171
- generate_brainweb_phantoms(output_dir, config)
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", "ves"],
10
+ "csf": ["csf"],
11
+ "vessels": ["ves"],
29
12
  "fat": ["fat", "mus", "m-s", "dura", "fat2"]
30
13
  },
31
- "tissues": {
32
- "3T": {
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
- "D": 0.83
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
- "D": 0.65
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
- "D": 3.19
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
- "D": 0.1
56
+ "ADC": 0.1
60
57
  }
61
58
  },
62
- "7T": {
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
- "D": 0.83
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
- "D": 0.65
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
- "D": 3.19
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
- "D": 0.1
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
- kx, ky = torch.meshgrid(
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 plot(self) -> None:
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 i in range(self.PD.numel()):
201
- rot = torch.exp(-2j*pi * (trajectory @ self.voxel_pos[i, :]))
202
- kspace = (rot * dephasing).view(128, 128)
203
- PD_kspace += kspace * self.PD[i]
204
- T1_kspace += kspace * self.T1[i]
205
- T2_kspace += kspace * self.T2[i]
206
- T2dash_kspace += kspace * self.T2dash[i]
207
- D_kspace += kspace * self.D[i]
208
-
209
- PD = torch.fft.fftshift(torch.fft.ifft2(PD_kspace))
210
- T1 = torch.fft.fftshift(torch.fft.ifft2(T1_kspace))
211
- T2 = torch.fft.fftshift(torch.fft.ifft2(T2_kspace))
212
- T2dash = torch.fft.fftshift(torch.fft.ifft2(T2dash_kspace))
213
- D = torch.fft.fftshift(torch.fft.ifft2(D_kspace))
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")