-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Expand file tree
/
Copy patharray.py
More file actions
287 lines (241 loc) · 11.9 KB
/
array.py
File metadata and controls
287 lines (241 loc) · 11.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# Copyright (c) MONAI Consortium
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from abc import abstractmethod
from collections.abc import Sequence
import numpy as np
from torch import Tensor
from monai.apps.reconstruction.complex_utils import complex_abs, convert_to_tensor_complex
from monai.apps.reconstruction.mri_utils import root_sum_of_squares
from monai.config.type_definitions import NdarrayOrTensor
from monai.data.fft_utils import ifftn_centered
from monai.transforms.transform import RandomizableTransform
from monai.utils.enums import TransformBackends
from monai.utils.type_conversion import convert_to_tensor
class KspaceMask(RandomizableTransform):
"""
A basic class for under-sampling mask setup. It provides common
features for under-sampling mask generators.
For example, RandomMaskFunc and EquispacedMaskFunc (two mask
transform objects defined right after this module)
both inherit MaskFunc to properly setup properties like the
acceleration factor.
"""
def __init__(
self,
center_fractions: Sequence[float],
accelerations: Sequence[float],
spatial_dims: int = 2,
is_complex: bool = True,
):
"""
Args:
center_fractions: Fraction of low-frequency columns to be retained.
If multiple values are provided, then one of these numbers
is chosen uniformly each time.
accelerations: Amount of under-sampling. This should have the
same length as center_fractions. If multiple values are
provided, then one of these is chosen uniformly each time.
spatial_dims: Number of spatial dims (e.g., it's 2 for a 2D data;
it's also 2 for pseudo-3D datasets like the fastMRI dataset).
The last spatial dim is selected for sampling. For the fastMRI
dataset, k-space has the form (...,num_slices,num_coils,H,W)
and sampling is done along W. For a general 3D data with the
shape (...,num_coils,H,W,D), sampling is done along D.
is_complex: if True, then the last dimension will be reserved for
real/imaginary parts.
"""
if len(center_fractions) != len(accelerations):
raise ValueError("Number of center fractions \
should match number of accelerations")
self.center_fractions = center_fractions
self.accelerations = accelerations
self.spatial_dims = spatial_dims
self.is_complex = is_complex
@abstractmethod
def __call__(self, kspace: NdarrayOrTensor) -> Sequence[Tensor]:
"""
This is an extra instance to allow for defining new mask generators.
For creating other mask transforms, define a new class and simply
override __call__. See an example of this in
:py:class:`monai.apps.reconstruction.transforms.array.RandomKspacemask`.
Args:
kspace: The input k-space data. The shape is (...,num_coils,H,W,2)
for complex 2D inputs and (...,num_coils,H,W,D) for real 3D
data.
"""
raise NotImplementedError
def randomize_choose_acceleration(self) -> Sequence[float]:
"""
If multiple values are provided for center_fractions and
accelerations, this function selects one value uniformly
for each training/test sample.
Returns:
A tuple containing
(1) center_fraction: chosen fraction of center kspace
lines to exclude from under-sampling
(2) acceleration: chosen acceleration factor
"""
choice = self.R.randint(0, len(self.accelerations))
center_fraction = self.center_fractions[choice]
acceleration = self.accelerations[choice]
return center_fraction, acceleration
class RandomKspaceMask(KspaceMask):
"""
This k-space mask transform under-samples the k-space according to a
random sampling pattern. Precisely, it uniformly selects a subset of
columns from the input k-space data. If the k-space data has N columns,
the mask picks out:
1. N_low_freqs = (N * center_fraction) columns in the center
corresponding to low-frequencies
2. The other columns are selected uniformly at random with a probability
equal to:
prob = (N / acceleration - N_low_freqs) / (N - N_low_freqs).
This ensures that the expected number of columns selected is equal to
(N / acceleration)
It is possible to use multiple center_fractions and accelerations,
in which case one possible (center_fraction, acceleration) is chosen
uniformly at random each time the transform is called.
Example:
If accelerations = [4, 8] and center_fractions = [0.08, 0.04],
then there is a 50% probability that 4-fold acceleration with 8%
center fraction is selected and a 50% probability that 8-fold
acceleration with 4% center fraction is selected.
Modified and adopted from:
https://github.com/facebookresearch/fastMRI/tree/master/fastmri
"""
backend = [TransformBackends.TORCH]
def __call__(self, kspace: NdarrayOrTensor) -> Sequence[Tensor]:
"""
Args:
kspace: The input k-space data. The shape is (...,num_coils,H,W,2)
for complex 2D inputs and (...,num_coils,H,W,D) for real 3D
data. The last spatial dim is selected for sampling. For the
fastMRI dataset, k-space has the form
(...,num_slices,num_coils,H,W) and sampling is done along W.
For a general 3D data with the shape (...,num_coils,H,W,D),
sampling is done along D.
Returns:
A tuple containing
(1) the under-sampled kspace
(2) absolute value of the inverse fourier of the under-sampled kspace
"""
kspace_t = convert_to_tensor_complex(kspace)
spatial_size = kspace_t.shape
num_cols = spatial_size[-1]
if self.is_complex: # for complex data
num_cols = spatial_size[-2]
center_fraction, acceleration = self.randomize_choose_acceleration()
# Create the mask
num_low_freqs = int(round(num_cols * center_fraction))
prob = (num_cols / acceleration - num_low_freqs) / (num_cols - num_low_freqs)
mask = self.R.uniform(size=num_cols) < prob
pad = (num_cols - num_low_freqs + 1) // 2
mask[pad : pad + num_low_freqs] = True
# Reshape the mask
mask_shape = [1 for _ in spatial_size]
if self.is_complex:
mask_shape[-2] = num_cols
else:
mask_shape[-1] = num_cols
mask = convert_to_tensor(mask.reshape(*mask_shape).astype(np.float32))
# under-sample the ksapce
masked = mask * kspace_t
masked_kspace: Tensor = convert_to_tensor(masked)
self.mask = mask
# compute inverse fourier of the masked kspace
masked_kspace_ifft: Tensor = convert_to_tensor(
complex_abs(ifftn_centered(masked_kspace, spatial_dims=self.spatial_dims, is_complex=self.is_complex))
)
# combine coil images (it is assumed that the coil dimension is
# the first dimension before spatial dimensions)
masked_kspace_ifft_rss: Tensor = convert_to_tensor(
root_sum_of_squares(masked_kspace_ifft, spatial_dim=-self.spatial_dims - 1)
)
return masked_kspace, masked_kspace_ifft_rss
class EquispacedKspaceMask(KspaceMask):
"""
This k-space mask transform under-samples the k-space according to an
equi-distant sampling pattern. Precisely, it selects an equi-distant
subset of columns from the input k-space data. If the k-space data has N
columns, the mask picks out:
1. N_low_freqs = (N * center_fraction) columns in the center corresponding
to low-frequencies
2. The other columns are selected with equal spacing at a proportion that
reaches the desired acceleration rate taking into consideration the number
of low frequencies. This ensures that the expected number of columns
selected is equal to (N / acceleration)
It is possible to use multiple center_fractions and accelerations, in
which case one possible (center_fraction, acceleration) is chosen
uniformly at random each time the EquispacedMaskFunc object is called.
Example:
If accelerations = [4, 8] and center_fractions = [0.08, 0.04],
then there is a 50% probability that 4-fold acceleration with 8%
center fraction is selected and a 50% probability that 8-fold
acceleration with 4% center fraction is selected.
Modified and adopted from:
https://github.com/facebookresearch/fastMRI/tree/master/fastmri
"""
backend = [TransformBackends.TORCH]
def __call__(self, kspace: NdarrayOrTensor) -> Sequence[Tensor]:
"""
Args:
kspace: The input k-space data. The shape is (...,num_coils,H,W,2)
for complex 2D inputs and (...,num_coils,H,W,D) for real 3D
data. The last spatial dim is selected for sampling. For the
fastMRI multi-coil dataset, k-space has the form
(...,num_slices,num_coils,H,W) and sampling is done along W.
For a general 3D data with the shape (...,num_coils,H,W,D),
sampling is done along D.
Returns:
A tuple containing
(1) the under-sampled kspace
(2) absolute value of the inverse fourier of the under-sampled kspace
"""
kspace_t = convert_to_tensor_complex(kspace)
spatial_size = kspace_t.shape
num_cols = spatial_size[-1]
if self.is_complex: # for complex data
num_cols = spatial_size[-2]
center_fraction, acceleration = self.randomize_choose_acceleration()
num_low_freqs = int(round(num_cols * center_fraction))
# Create the mask
mask = np.zeros(num_cols, dtype=np.float32)
pad = (num_cols - num_low_freqs + 1) // 2
mask[pad : pad + num_low_freqs] = True
# Determine acceleration rate by adjusting for the
# number of low frequencies
adjusted_accel = (acceleration * (num_low_freqs - num_cols)) / (num_low_freqs * acceleration - num_cols)
offset = self.R.randint(0, round(adjusted_accel))
accel_samples = np.arange(offset, num_cols - 1, adjusted_accel)
accel_samples = np.around(accel_samples).astype(np.uint)
mask[accel_samples] = True
# Reshape the mask
mask_shape = [1 for _ in spatial_size]
if self.is_complex:
mask_shape[-2] = num_cols
else:
mask_shape[-1] = num_cols
mask = convert_to_tensor(mask.reshape(*mask_shape).astype(np.float32))
# under-sample the ksapce
masked = mask * kspace_t
masked_kspace: Tensor = convert_to_tensor(masked)
self.mask = mask
# compute inverse fourier of the masked kspace
masked_kspace_ifft: Tensor = convert_to_tensor(
complex_abs(ifftn_centered(masked_kspace, spatial_dims=self.spatial_dims, is_complex=self.is_complex))
)
# combine coil images (it is assumed that the coil dimension is
# the first dimension before spatial dimensions)
masked_kspace_ifft_rss: Tensor = convert_to_tensor(
root_sum_of_squares(masked_kspace_ifft, spatial_dim=-self.spatial_dims - 1)
)
return masked_kspace, masked_kspace_ifft_rss