augmentrum 0.0.1__py3-none-any.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.
Files changed (42) hide show
  1. augmentrum/__init__.py +23 -0
  2. augmentrum/augmentation/__init__.py +32 -0
  3. augmentrum/augmentation/amplitude_scaling.py +170 -0
  4. augmentrum/augmentation/apodization.py +275 -0
  5. augmentrum/augmentation/artificial_peaks.py +334 -0
  6. augmentrum/augmentation/baseline_augmentation.py +544 -0
  7. augmentrum/augmentation/eddy_current.py +278 -0
  8. augmentrum/augmentation/gaussian_noise.py +211 -0
  9. augmentrum/augmentation/line_broadening.py +179 -0
  10. augmentrum/augmentation/phase_frequency.py +259 -0
  11. augmentrum/augmentation/residual_water.py +264 -0
  12. augmentrum/augmentation/spatial_augmentations.py +859 -0
  13. augmentrum/augmentation/spurious_echoes.py +378 -0
  14. augmentrum/augmentation/zero_fill.py +163 -0
  15. augmentrum/core/__init__.py +10 -0
  16. augmentrum/core/augmentrum.py +1172 -0
  17. augmentrum/core/base_module.py +423 -0
  18. augmentrum/core/dataset_utils.py +417 -0
  19. augmentrum/core/nifti_mrs_plus.py +1079 -0
  20. augmentrum/core/pipeline.py +358 -0
  21. augmentrum/dataset/brainbeats.py +537 -0
  22. augmentrum/dataset/cows.py +302 -0
  23. augmentrum/dataset/fmrsinpain.py +208 -0
  24. augmentrum/processing/bicubic_interpolator.py +296 -0
  25. augmentrum/processing/nifti_raw_processor.py +389 -0
  26. augmentrum/processing/tricubic_interpolator.py +293 -0
  27. augmentrum/processing/utils.py +414 -0
  28. augmentrum/resources/__init__.py +14 -0
  29. augmentrum/resources/scans/__init__.py +0 -0
  30. augmentrum/sampling/coil_average_sampler.py +274 -0
  31. augmentrum/sampling/kspace_reconstructor.py +275 -0
  32. augmentrum/sampling/kspace_sampling.py +2562 -0
  33. augmentrum/sampling/subject_splitter.py +131 -0
  34. augmentrum/utils/__init__.py +10 -0
  35. augmentrum/utils/philips.py +90 -0
  36. augmentrum/utils/plotting.py +651 -0
  37. augmentrum/utils/tensor_ops.py +203 -0
  38. augmentrum-0.0.1.dist-info/METADATA +189 -0
  39. augmentrum-0.0.1.dist-info/RECORD +42 -0
  40. augmentrum-0.0.1.dist-info/WHEEL +5 -0
  41. augmentrum-0.0.1.dist-info/licenses/LICENSE +202 -0
  42. augmentrum-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,274 @@
1
+ ####################################################################################################
2
+ # coil_average_sampler.py #
3
+ ####################################################################################################
4
+ # #
5
+ # Authors: J. P. Merkofer (j.p.merkofer@tue.nl) #
6
+ # #
7
+ # Created: 2025-10-07 #
8
+ # #
9
+ # Purpose: Implements sampling of coils and averages from raw MRS data. #
10
+ # #
11
+ ####################################################################################################
12
+
13
+
14
+ #*************#
15
+ # imports #
16
+ #*************#
17
+ import torch
18
+
19
+ from fsl_mrs.core.nifti_mrs import split
20
+
21
+ # own
22
+ from augmentrum.processing.utils import update_processing_prov
23
+ from augmentrum.core.base_module import BaseModule
24
+ from augmentrum.core import Backend
25
+
26
+
27
+ #**************************************************************************************************#
28
+ # Class CoilAverageSampler #
29
+ #**************************************************************************************************#
30
+ # #
31
+ # Samples coils and averages from raw MRS data. Supports random/deterministic selection #
32
+ # and reweighting. #
33
+ # #
34
+ #**************************************************************************************************#
35
+ class CoilAverageSampler(BaseModule):
36
+ """
37
+ Samples coils and averages from raw MRS data.
38
+
39
+ Uses FSL-MRS `split` on NIfTI-MRS objects, so this module operates
40
+ natively on the NIFTI_LIST backend. For any other backend the base
41
+ class automatically routes the call through process_nifti_list and
42
+ returns the result in the original backend format.
43
+ """
44
+
45
+ SUPPORTED_BACKENDS = [Backend.NIFTI_LIST]
46
+
47
+ def __init__(self, mode='random', n_coils=(1, None), n_averages=(1, None), reweight=False):
48
+ """
49
+ Initializes the sampler.
50
+
51
+ Args:
52
+ mode (str): 'random' or 'deterministic' sampling mode.
53
+ n_coils (tuple or int): Min/max number of coils to sample, or exact value.
54
+ Examples: (1, 8) = range, 4 = exactly 4
55
+ n_averages (tuple or int): Min/max number of averages to sample, or exact value.
56
+ reweight (bool): Whether to reweight signals after sampling.
57
+ """
58
+ super().__init__(mode=mode, n_coils=n_coils, n_averages=n_averages, reweight=reweight)
59
+
60
+ self.mode = mode
61
+
62
+ # Normalize to tuples: int → (value, value), tuple → tuple
63
+ self.n_coils = self._normalize_param(n_coils)
64
+ self.n_averages = self._normalize_param(n_averages)
65
+ self.reweight = reweight
66
+
67
+ def _normalize_param(self, param):
68
+ """
69
+ Normalize parameter to tuple format.
70
+
71
+ Args:
72
+ param: Either tuple (min, max) or int/float (exact value)
73
+
74
+ Returns:
75
+ Tuple (min, max)
76
+
77
+ Examples:
78
+ 4 → (4, 4) # Exactly 4
79
+ (1, 8) → (1, 8) # Range 1-8
80
+ None → (1, None) # Default
81
+ """
82
+ if param is None:
83
+ return (1, None)
84
+ elif isinstance(param, (int, float)):
85
+ # Single value → exact value
86
+ return (int(param), int(param))
87
+ elif isinstance(param, tuple):
88
+ return param
89
+ else:
90
+ raise TypeError(f"Parameter must be int, float, or tuple, got {type(param)}")
91
+
92
+
93
+ def process_nifti_list(self, data_list, water_list=None, coil_indices=None, average_indices=None, **kwargs):
94
+ """
95
+ Samples coils and averages from each NIFTI_MRS in the list.
96
+
97
+ Args:
98
+ data_list: List of metabolite MRS data (NIFTI_MRS objects).
99
+ water_list: List of water reference MRS data (NIFTI_MRS objects), optional.
100
+ coil_indices: List of coil indices to select (for deterministic mode).
101
+ average_indices: List of average indices to select (for deterministic mode).
102
+ **kwargs: Additional arguments.
103
+
104
+ Returns:
105
+ Tuple of (processed_data_list, processed_water_list)
106
+ """
107
+ processed_data = []
108
+ processed_water = []
109
+
110
+ for i, data_met in enumerate(data_list):
111
+ data_wat = water_list[i] if water_list is not None else None
112
+
113
+ # Sample coils and averages for this subject
114
+ data_met, data_wat = self._process_single(data_met, data_wat, coil_indices, average_indices)
115
+
116
+ processed_data.append(data_met)
117
+ if water_list is not None:
118
+ processed_water.append(data_wat if data_wat is not None else water_list[i])
119
+
120
+ return processed_data, (processed_water if water_list is not None else None)
121
+
122
+ def _process_single(self, data_met, data_wat=None, coil_indices=None, average_indices=None):
123
+ """
124
+ Samples coils and averages from a single subject's data.
125
+
126
+ Args:
127
+ data_met: Metabolite MRS data (NiftiMRS object).
128
+ data_wat: Water reference MRS data (NiftiMRS object), optional.
129
+ coil_indices: List of coil indices to select (for deterministic mode).
130
+ average_indices: List of average indices to select (for deterministic mode).
131
+ """
132
+ data_met, data_wat = self.sample_coils(data_met, data_wat, coil_indices)
133
+ data_met, data_wat = self.sample_averages(data_met, data_wat, average_indices)
134
+
135
+ if self.reweight:
136
+ data_met, data_wat = self._reweight_signals(data_met, data_wat)
137
+ return data_met, data_wat
138
+
139
+ def sample_coils(self, data_met, data_wat=None, coil_indices=None):
140
+ """
141
+ Samples specified coils from the data.
142
+
143
+ Args:
144
+ data_met: Metabolite MRS data (NiftiMRS object).
145
+ data_wat: Water reference MRS data (NiftiMRS object), optional
146
+ coil_indices: List of coil indices to select (if provided, use deterministic mode).
147
+ """
148
+ dim_tags = getattr(data_met, 'dim_tags', [])
149
+ has_coil = 'DIM_COIL' in dim_tags
150
+
151
+ # Determine mode at runtime: if coil_indices provided, use them (deterministic)
152
+ # Otherwise, sample randomly (if mode='random') or use all (if mode='deterministic' but no indices)
153
+ if coil_indices is not None:
154
+ # Deterministic mode: use provided indices
155
+ pass # Will apply indices below
156
+ elif self.mode == 'random' and has_coil:
157
+ # Random mode: sample random coils
158
+ coil_dim = data_met.dim_position('DIM_COIL')
159
+ n_total = data_met.shape[coil_dim] # actual coil count
160
+ min_c, max_c = self._get_limits(self.n_coils, n_total)
161
+ if min_c < max_c:
162
+ num_coils = torch.randint(min_c, max_c, (1,)).item()
163
+ coil_indices = torch.randperm(n_total)[:num_coils].tolist()
164
+ elif self.mode == 'deterministic' and has_coil:
165
+ # Deterministic mode but no indices provided: use all coils
166
+ coil_dim = data_met.dim_position('DIM_COIL')
167
+ coil_indices = list(range(data_met.shape[coil_dim]))
168
+
169
+ # apply sampling
170
+ if has_coil and coil_indices is not None:
171
+ if isinstance(coil_indices, list) or (isinstance(coil_indices, int) and coil_indices > 0):
172
+ _, data_met = split(data_met, 'DIM_COIL', coil_indices)
173
+ if data_wat is not None:
174
+ _, data_wat = split(data_wat, 'DIM_COIL', coil_indices)
175
+
176
+ # update processing provenance
177
+ processing_info = f'{__name__}.sample_coils, '
178
+ processing_info += f'coil_indices={coil_indices}.'
179
+ update_processing_prov(data_met, 'Random Coil Sampling', processing_info)
180
+ if data_wat is not None:
181
+ update_processing_prov(data_wat, 'Random Coil Sampling', processing_info)
182
+
183
+ return data_met, data_wat
184
+
185
+ def sample_averages(self, data_met, data_wat=None, average_indices=None):
186
+ """
187
+ Samples specified averages from the data.
188
+
189
+ Args:
190
+ data_met: Metabolite MRS data (NiftiMRS object).
191
+ data_wat: Water reference MRS data (NiftiMRS object), optional.
192
+ average_indices: List of average indices to select.
193
+ """
194
+ dim_tags = getattr(data_met, 'dim_tags', [])
195
+ has_dyn = 'DIM_DYN' in dim_tags
196
+
197
+ # Determine mode at runtime: if average_indices provided, use them (deterministic)
198
+ # Otherwise, sample randomly (if mode='random') or use all (if mode='deterministic' but no indices)
199
+ if average_indices is not None:
200
+ # Deterministic mode: use provided indices
201
+ pass # Will apply indices below
202
+ elif self.mode == 'random' and has_dyn:
203
+ # Random mode: sample random averages
204
+ dyn_dim = data_met.dim_position('DIM_DYN')
205
+ n_total = data_met.shape[dyn_dim] # actual average count
206
+ min_a, max_a = self._get_limits(self.n_averages, n_total)
207
+ if min_a < max_a:
208
+ num_averages = torch.randint(min_a, max_a, (1,)).item()
209
+ average_indices = torch.randperm(n_total)[:num_averages].tolist()
210
+ elif self.mode == 'deterministic' and has_dyn:
211
+ # Deterministic mode but no indices provided: use all averages
212
+ dyn_dim = data_met.dim_position('DIM_DYN')
213
+ average_indices = list(range(data_met.shape[dyn_dim]))
214
+
215
+ # apply sampling
216
+ if has_dyn and average_indices is not None:
217
+ if isinstance(average_indices, list) or (isinstance(average_indices, int) and average_indices > 0):
218
+ _, data_met = split(data_met, 'DIM_DYN', average_indices)
219
+
220
+ # update processing provenance
221
+ processing_info = f'{__name__}.sample_averages, '
222
+ processing_info += f'coil_indices={average_indices}.'
223
+ update_processing_prov(data_met, 'Random Average Sampling', processing_info)
224
+
225
+ return data_met, data_wat
226
+
227
+ def _reweight_signals(self, data, water):
228
+ """
229
+ Reweights coils and averages based on random weights.
230
+ TODO: implement reweighting.
231
+
232
+ Args:
233
+ data: MRS data (NiftiMRS object).
234
+ water: Water reference MRS data (NiftiMRS object), optional.
235
+ """
236
+ raise NotImplementedError("Reweighting not implemented yet.")
237
+
238
+ def _sample_snr_improvement(self, data, water):
239
+ """
240
+ Samples coils and averages to improve SNR.
241
+ TODO: implement SNR improvement sampling.
242
+
243
+ Args:
244
+ data: MRS data (NiftiMRS object).
245
+ water: Water reference MRS data (NiftiMRS object), optional.
246
+ """
247
+ raise NotImplementedError("SNR improvement sampling not implemented yet.")
248
+
249
+ def _get_limits(self, n, n_total):
250
+ """
251
+ Return ``(min_n, max_exclusive)`` for ``torch.randint(min_n, max_exclusive)``.
252
+
253
+ Args:
254
+ n (tuple): ``(min, max)`` — both *inclusive*, either can be ``None``.
255
+ ``None`` on min → 1. ``None`` on max → ``n_total`` (use all).
256
+ n_total (int): Total number of items available (coils / averages).
257
+
258
+ Examples::
259
+
260
+ _get_limits((1, None), 4) → (1, 5) # randint → 1, 2, 3 or 4 ✓
261
+ _get_limits((2, 3), 4) → (2, 4) # randint → 2 or 3 ✓
262
+ _get_limits((4, 4), 4) → (4, 5) # randint → always 4 ✓
263
+ _get_limits((6, None), 4) → (4, 5) # clamps to available ✓
264
+ """
265
+ if n is None:
266
+ return 1, n_total + 1
267
+
268
+ min_val, max_val = n
269
+
270
+ min_n = 1 if min_val is None else max(1, min(int(min_val), n_total))
271
+ max_n = n_total if max_val is None else min(int(max_val), n_total)
272
+
273
+ # +1 because torch.randint upper bound is exclusive
274
+ return min_n, max_n + 1
@@ -0,0 +1,275 @@
1
+ ####################################################################################################
2
+ # kspace_reconstructor.py #
3
+ ####################################################################################################
4
+ # #
5
+ # Authors: J. T. LaMaster (john.t.lamaster@gmail.com) #
6
+ # #
7
+ # Created: 2025-10-22 #
8
+ # #
9
+ # Purpose: Implements reconstruct_with_masking which accepts subsampled k-space data and its #
10
+ # trajectory coordinates to perform density compesnation and regridding. #
11
+ # #
12
+ ####################################################################################################
13
+
14
+ import torch
15
+ import torch.nn as nn
16
+ import torch.nn.functional as F
17
+ import torchkbnufft as tkbn
18
+ from typing import Union, List, Tuple
19
+
20
+
21
+ __all__ = ['reconstruct_with_masking']
22
+
23
+
24
+ # --- Constants for normalization ---
25
+ # TorchKbNufft expects coordinates in the range [-pi, pi]
26
+ PI = torch.pi
27
+
28
+
29
+ def prepare_data_and_coords(
30
+ coords: torch.Tensor,
31
+ kdata: Optional[torch.Tensor] = None
32
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
33
+ """
34
+ Reshapes coordinates and k-data to the format required by TorchKbNufft:
35
+ Coords: [B, D, N_total], Kdata: [B, C, N_total]
36
+ N_total = S * L (shots x length).
37
+ """
38
+ # Assuming input coords shape: [B, S, D, L] (e.g., [4, 10, 3, 100])
39
+ B, S, D, L = coords.shape
40
+ N_total = S * L
41
+
42
+ # 1. Normalize Coordinates (if not already done)
43
+ # The NUFFT forward/adjoint operators typically assume coordinates in [-pi, pi].
44
+ # We assume your input coords are normalized to [-1, 1] relative to k-space extent.
45
+ coords_normalized = coords * PI
46
+
47
+ # 2. Flatten Trajectory: [B, S, D, L] -> [B, D, S, L] -> [B, D, N_total]
48
+ k_traj_flat = coords_normalized.permute(0, 2, 1, 3).reshape(B, D, N_total)
49
+
50
+ kdata_flat = None
51
+ if kdata is not None:
52
+ # Assuming input kdata shape: [B, S, C, L] (e.g., [4, 10, 1, 100])
53
+ B_k, S_k, C, L_k = kdata.shape
54
+
55
+ # Flatten K-data: [B, S, C, L] -> [B, C, S, L] -> [B, C, N_total]
56
+ kdata_flat = kdata.permute(0, 2, 1, 3).reshape(B, C, N_total)
57
+
58
+ return k_traj_flat, kdata_flat
59
+
60
+ def calculate_nufft_dcf_weights(
61
+ coords: torch.Tensor,
62
+ image_size: Union[List[int], Tuple[int]],
63
+ oversampling_factor: Union[List[float], Tuple[float]]
64
+ ) -> torch.Tensor:
65
+ """
66
+ Calculates Density Compensation Function (DCF) weights using the
67
+ iterative Pipe-style method from torchkbnufft.
68
+ """
69
+ device = coords.device
70
+ B, S, D, L = coords.shape
71
+
72
+ # 1. Prepare Trajectory Coordinates
73
+ k_traj_flat, _ = prepare_data_and_coords(coords) # [B, D, N_total]
74
+
75
+ # 2. Determine Grid Size based on Image Size and Oversampling
76
+ # Ensure all sizes are determined on the CPU and converted to a list/tuple of native types
77
+
78
+ # Get sizes as native types (no longer need to keep them as tensors)
79
+ image_size_list = list(image_size)
80
+ oversampling_factor_tensor = torch.tensor(oversampling_factor, dtype=torch.float32)
81
+
82
+ # Grid size = floor(Image Size * Oversampling Factor)
83
+ grid_size_tensor = torch.floor(torch.tensor(image_size) * oversampling_factor_tensor).to(torch.long)
84
+ grid_size_list = grid_size_tensor.tolist() # <--- Pass native list/tuple
85
+
86
+ # 3. Calculate DCF (Iterative Pipe-style)
87
+ # Pass 'im_size' and 'grid_size' as separate arguments, not the KbNufft object.
88
+ with torch.no_grad():
89
+ dcf_flat = tkbn.calc_density_compensation_function(
90
+ k_traj_flat,
91
+ im_size=image_size_list, # <--- FIX 1: Pass im_size
92
+ grid_size=grid_size_list # <--- FIX 2: Pass grid_size
93
+ ).detach() # [B, 1, N_total]
94
+
95
+ # 4. Reshape DCF for easy multiplication with k-data: [B, 1, S*L] -> [B, S, L]
96
+ dcf_weights = dcf_flat.view(B, S, L)
97
+
98
+ return dcf_weights
99
+
100
+
101
+ def regrid_kdata_to_image(
102
+ weighted_kdata: torch.Tensor,
103
+ coords: torch.Tensor,
104
+ image_size: Union[List[int], Tuple[int]],
105
+ oversampling_factor: Union[List[float], Tuple[float]]
106
+ ) -> torch.Tensor:
107
+ """
108
+ Performs Adjoint NUFFT (regridding) to map weighted k-space data to the image domain.
109
+
110
+ Args:
111
+ weighted_kdata (torch.Tensor): Density-compensated k-space data, shape [B, S, C, L].
112
+ coords (torch.Tensor): K-space trajectory coordinates, shape [B, S, D, L].
113
+ image_size (Union[List[int], Tuple[int]]): Target image dimensions (Nx, Ny, Nz).
114
+ oversampling_factor (Union[List[float], Tuple[float]]): Oversampling factors
115
+ for the regridding grid (rho_x, rho_y, rho_z).
116
+
117
+ Returns:
118
+ torch.Tensor: Reconstructed complex image volume, shape [B, C, Nx, Ny, Nz].
119
+ """
120
+ device = weighted_kdata.device
121
+
122
+ # 1. Prepare Coords and K-data for NUFFT
123
+ # k_traj_flat: [B, D, N_total], kdata_flat: [B, C, N_total]
124
+ k_traj_flat, kdata_flat = prepare_data_and_coords(coords, weighted_kdata)
125
+
126
+ # 2. Determine Grid Size
127
+ image_size = torch.tensor(image_size, dtype=torch.long, device=device)
128
+ oversampling_factor = torch.tensor(oversampling_factor, dtype=torch.float32, device=device)
129
+
130
+ # Grid size = floor(Image Size * Oversampling Factor)
131
+ grid_size = torch.floor(image_size * oversampling_factor).to(torch.long)
132
+
133
+ # 3. Instantiate Adjoint NUFFT Operator (Regridding)
134
+ adj_op = tkbn.KbNufftAdjoint(
135
+ im_size=image_size.tolist(), # [Nx, Ny, Nz]
136
+ grid_size=grid_size.tolist() # [Grid_Nx, Grid_Ny, Grid_Nz]
137
+ ).to(device)
138
+
139
+ # 4. Perform Regridding
140
+ # The operation is: Adjoint(k_data) -> IFFT -> Image
141
+ image_domain_output = adj_op(kdata_flat, k_traj_flat)
142
+
143
+ # Output shape: [B, C, Nx, Ny, Nz]
144
+ return image_domain_output
145
+
146
+
147
+ def reconstruct_with_masking(
148
+ coords: torch.Tensor,
149
+ kdata: torch.Tensor,
150
+ undersampling_mask: torch.Tensor,
151
+ image_size: Union[List[int], Tuple[int]],
152
+ oversampling_factor: Union[List[float], Tuple[float]]
153
+ ) -> torch.Tensor:
154
+ """
155
+ High-level wrapper to apply undersampling mask, calculate DCF, and perform regridding.
156
+
157
+ Args:
158
+ coords (torch.Tensor): K-space trajectory coordinates, shape [B, S, D, L].
159
+ kdata (torch.Tensor): Raw k-space data, shape [B, S, C, L].
160
+ undersampling_mask (torch.Tensor): Boolean mask indicating which shots to KEEP.
161
+ Shape: [B, S].
162
+ image_size (Union[List[int], Tuple[int]]): Target image dimensions (Nx, Ny, Nz).
163
+ oversampling_factor (Union[List[float], Tuple[float]]): Oversampling factors
164
+ for the regridding grid (rho_x, rho_y, rho_z).
165
+
166
+ Returns:
167
+ torch.Tensor: Reconstructed complex image volume, shape [B, C, Nx, Ny, Nz].
168
+ """
169
+ # --- 1. Apply Undersampling Mask (Zero-Filling) ---
170
+ # The mask is [B, S]. We need to expand it to [B, S, C, L] to multiply with kdata.
171
+ B, S, C, L = kdata.shape
172
+
173
+ # Expand mask to match kdata shape: [B, S] -> [B, S, 1, 1] -> [B, S, C, L]
174
+ # Invert the boolean mask to create a float mask (True -> 0, False -> 1)
175
+ # Since the mask is for samples to KEEP (conventionally True),
176
+ # we zero the kdata samples where the mask is False (discard).
177
+
178
+ # Convert mask to float tensor (0 or 1) and expand
179
+ mask_float = undersampling_mask.to(dtype=kdata.real.dtype).unsqueeze(2).unsqueeze(3)
180
+
181
+ # Expand across Coil (C) and Length (L) dimensions
182
+ mask_bcast = mask_float.expand(B, S, C, L)
183
+
184
+ # Apply mask (zero out discarded k-space samples/shots)
185
+ masked_kdata = kdata * mask_bcast
186
+
187
+ # --- 2. Calculate DCF Weights ---
188
+ # DCF calculation should use ALL coordinates to reflect the desired density.
189
+ dcf_weights = calculate_nufft_dcf_weights(
190
+ coords=coords,
191
+ image_size=image_size,
192
+ oversampling_factor=oversampling_factor
193
+ )
194
+
195
+ # Reshape DCF for multiplication: [B, S, L] -> [B, S, 1, L]
196
+ dcf_bcast = dcf_weights.unsqueeze(2)
197
+
198
+ # --- 3. Apply DCF to Masked K-space Data ---
199
+ weighted_kdata = masked_kdata * dcf_bcast
200
+
201
+ # --- 4. Perform Regridding (Adjoint NUFFT) ---
202
+ reconstructed_image = regrid_kdata_to_image(
203
+ weighted_kdata=weighted_kdata,
204
+ coords=coords,
205
+ image_size=image_size,
206
+ oversampling_factor=oversampling_factor
207
+ )
208
+
209
+ return reconstructed_image
210
+
211
+
212
+ if __name__ == "__ main __":
213
+ # Define parameters for a 3D example
214
+ B, S, D, L, C = 4, 10, 3, 100, 1 # Batch, Shots, Dimensions, Length, Channels
215
+ Target_Image_Size = (64, 64, 64)
216
+ Oversampling_Factor = (1.5, 1.5, 1.5)
217
+
218
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
219
+
220
+ # Dummy Data: Coords must be float, Kdata must be complex
221
+ coords_in = (torch.rand(B, S, D, L) * 2 - 1).to(dtype=torch.float32, device=device) # [-1, 1] range
222
+ kdata_in = torch.randn(B, S, C, L, dtype=torch.complex64, device=device)
223
+
224
+ # --- NEW: Create an Undersampling Mask ---
225
+ # Mask [B, S] (e.g., keeping only 50% of the shots randomly per batch sample)
226
+ undersampling_mask_in = torch.rand(B, S) < 0.5
227
+ undersampling_mask_in = undersampling_mask_in.to(device)
228
+
229
+ # --- 1. Full Reconstruction Pipeline with Masking ---
230
+ print("--- Starting Full Reconstruction Pipeline (Masked) ---")
231
+ reconstructed_image_masked = reconstruct_with_masking(
232
+ coords=coords_in,
233
+ kdata=kdata_in,
234
+ undersampling_mask=undersampling_mask_in,
235
+ image_size=Target_Image_Size,
236
+ oversampling_factor=Oversampling_Factor
237
+ )
238
+
239
+ print(f"Masked Reconstruction successful. Final Image Shape: {reconstructed_image_masked.shape}")
240
+ print(f"Shots kept per batch: {undersampling_mask_in.sum(dim=1).tolist()}")
241
+ print(f"Expected Final Image Shape: [{B}, {C}, {Target_Image_Size[0]}, {Target_Image_Size[1]}, {Target_Image_Size[2]}]")
242
+
243
+ # The reconstructed image is now a complex tensor [B, C, Nx, Ny, Nz]
244
+
245
+ # Define parameters for a 2D example
246
+ B, S, D, L, C = 4, 10, 2, 100, 1 # Batch, Shots, Dimensions, Length, Channels
247
+ Target_Image_Size = (64, 64)
248
+ Oversampling_Factor = (1.5, 1.5)
249
+
250
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
251
+
252
+ # Dummy Data: Coords must be float, Kdata must be complex
253
+ coords_in = (torch.rand(B, S, D, L) * 2 - 1).to(dtype=torch.float32, device=device) # [-1, 1] range
254
+ kdata_in = torch.randn(B, S, C, L, dtype=torch.complex64, device=device)
255
+
256
+ # --- NEW: Create an Undersampling Mask ---
257
+ # Mask [B, S] (e.g., keeping only 50% of the shots randomly per batch sample)
258
+ undersampling_mask_in = torch.rand(B, S) < 0.5
259
+ undersampling_mask_in = undersampling_mask_in.to(device)
260
+
261
+ # --- 1. Full Reconstruction Pipeline with Masking ---
262
+ print("--- Starting Full Reconstruction Pipeline (Masked) ---")
263
+ reconstructed_image_masked = reconstruct_with_masking(
264
+ coords=coords_in,
265
+ kdata=kdata_in,
266
+ undersampling_mask=undersampling_mask_in,
267
+ image_size=Target_Image_Size,
268
+ oversampling_factor=Oversampling_Factor
269
+ )
270
+
271
+ print(f"Masked Reconstruction successful. Final Image Shape: {reconstructed_image_masked.shape}")
272
+ print(f"Shots kept per batch: {undersampling_mask_in.sum(dim=1).tolist()}")
273
+ print("Expected Final Image Shape: [{}, {}, {}]".format(B, C, [x for x in Target_Image_Size]))
274
+
275
+ # The reconstructed image is now a complex tensor [B, C, Nx, Ny]