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,417 @@
1
+ ####################################################################################################
2
+ # dataset_utils.py #
3
+ ####################################################################################################
4
+ # #
5
+ # Authors: J. P. Merkofer (j.p.merkofer@tue.nl) #
6
+ # #
7
+ # Created: 2026-02-07 #
8
+ # #
9
+ # Purpose: Backend-agnostic dataset utilities for Augmentrum. #
10
+ # #
11
+ ####################################################################################################
12
+
13
+
14
+ #*************#
15
+ # imports #
16
+ #*************#
17
+ import random
18
+ import numpy as np
19
+ from typing import List, Optional, Tuple, Dict, Callable, Union
20
+
21
+ # internal
22
+ from augmentrum.core import NIfTI_MRS_Plus, Backend
23
+ from augmentrum.sampling.subject_splitter import SubjectSplitter
24
+
25
+
26
+ __all__ = [
27
+ 'create_random_generator',
28
+ 'create_fixed_generator',
29
+ 'create_deterministic_generator',
30
+ 'create_deterministic_indices',
31
+ 'convert_batch_to_backend',
32
+ 'wrap_generator_for_framework',
33
+ 'SubjectSplitter',
34
+ ]
35
+
36
+
37
+ #**************************************************************************************************#
38
+ # Helper Functions #
39
+ #**************************************************************************************************#
40
+
41
+ def create_deterministic_indices(data: NIfTI_MRS_Plus, n_coils: Tuple, n_averages: Tuple) -> List[Tuple]:
42
+ """
43
+ Create all possible (subject_idx, coil_idx, average_idx) combinations.
44
+
45
+ Args:
46
+ data: NIfTI_MRS_Plus containing subjects
47
+ n_coils: (min, max) coils to sample
48
+ n_averages: (min, max) averages to sample
49
+
50
+ Returns:
51
+ List of (subject_idx, coil_idx, average_idx) tuples
52
+ """
53
+ indices = []
54
+ nifti_list = data.list()
55
+
56
+ for subj_idx, nifti in enumerate(nifti_list):
57
+ # Get coil limits
58
+ if 'DIM_COIL' in getattr(nifti, 'dim_tags', []):
59
+ c_max = nifti.shape[nifti.dim_position('DIM_COIL')] - 1
60
+ min_c, max_c = _get_limits(n_coils, c_max)
61
+ else:
62
+ min_c, max_c, c_max = 0, 1, 0
63
+
64
+ # Get average limits
65
+ if 'DIM_DYN' in getattr(nifti, 'dim_tags', []):
66
+ a_max = nifti.shape[nifti.dim_position('DIM_DYN')] - 1
67
+ min_a, max_a = _get_limits(n_averages, a_max)
68
+ else:
69
+ min_a, max_a, a_max = 0, 1, 0
70
+
71
+ # Generate all combinations
72
+ for n_c in range(min_c, max_c + 1):
73
+ for n_a in range(min_a, max_a + 1):
74
+ indices.append((subj_idx, c_max - n_c, a_max - n_a))
75
+
76
+ return indices
77
+
78
+
79
+ def _get_limits(n: Tuple, n_max: int) -> Tuple[int, int]:
80
+ """Convert (min, max) with possible None to concrete limits."""
81
+ min_n = n_max if n[0] is None else max(0, min(n[0], n_max))
82
+ max_n = n_max if n[1] is None else min(n[1], n_max)
83
+ return min_n, max_n
84
+
85
+
86
+ #**************************************************************************************************#
87
+ # Backend-Agnostic Generators #
88
+ #**************************************************************************************************#
89
+
90
+ def _make_batch(data, water, indices, copy=False):
91
+ """
92
+ Build a (batch_data, batch_water) NIfTI_MRS_Plus pair from subject indices.
93
+
94
+ Args:
95
+ data: NIfTI_MRS_Plus pool of subjects.
96
+ water: Optional NIfTI_MRS_Plus pool of water references.
97
+ indices: List of integer subject indices for this batch.
98
+ copy: If True, copy each NIfTI-MRS object (use for fixed mode
99
+ where the same objects are yielded multiple times).
100
+
101
+ Returns:
102
+ (batch_data, batch_water) as NIfTI_MRS_Plus objects.
103
+ """
104
+ from augmentrum.core.nifti_mrs_plus import NIfTI_MRS_Plus
105
+
106
+ get = (lambda idx: data[idx].copy()) if copy else (lambda idx: data[idx])
107
+ batch_data = NIfTI_MRS_Plus(
108
+ [get(i) for i in indices], backend=data.backend, volatile=data.volatile,
109
+ )
110
+
111
+ batch_water = None
112
+ if water is not None:
113
+ get_w = (lambda idx: water[idx].copy()) if copy else (lambda idx: water[idx])
114
+ batch_water = NIfTI_MRS_Plus(
115
+ [get_w(i) for i in indices], backend=water.backend, volatile=water.volatile,
116
+ )
117
+
118
+ return batch_data, batch_water
119
+
120
+
121
+ def create_random_generator(data: NIfTI_MRS_Plus,
122
+ water: Optional[NIfTI_MRS_Plus],
123
+ pipeline,
124
+ batch_size: int):
125
+ """
126
+ Infinite random-sampling generator (on-the-fly mode).
127
+
128
+ Each iteration: pick *batch_size* random subjects, sample fresh
129
+ augmentation parameters, apply the pipeline, yield the batch.
130
+
131
+ Yields:
132
+ (batch_data, batch_water) — NIfTI_MRS_Plus objects.
133
+ """
134
+ n_subjects = len(data)
135
+
136
+ while True:
137
+ indices = [random.randint(0, n_subjects - 1) for _ in range(batch_size)]
138
+ batch_data, batch_water = _make_batch(data, water, indices)
139
+ batch_params = pipeline.sample_batch_parameters(batch_size)
140
+ yield pipeline(batch_data, batch_water, batch_params=batch_params)
141
+
142
+
143
+ def create_fixed_generator(data: NIfTI_MRS_Plus,
144
+ water: Optional[NIfTI_MRS_Plus],
145
+ pipeline,
146
+ batch_size: int,
147
+ shuffle: bool = False):
148
+ """
149
+ Single-pass generator with fixed augmentation parameters.
150
+
151
+ Parameters are sampled **once** at creation and reused for every batch,
152
+ giving consistent results across epochs.
153
+
154
+ Yields:
155
+ (batch_data, batch_water) — NIfTI_MRS_Plus objects.
156
+ """
157
+ fixed_params = pipeline.sample_batch_parameters(batch_size)
158
+
159
+ n_subjects = len(data)
160
+ indices = list(range(n_subjects))
161
+ if shuffle:
162
+ random.shuffle(indices)
163
+
164
+ for start in range(0, n_subjects, batch_size):
165
+ batch_idx = indices[start : start + batch_size]
166
+ batch_data, batch_water = _make_batch(data, water, batch_idx, copy=True)
167
+ yield pipeline(batch_data, batch_water, batch_params=fixed_params)
168
+
169
+
170
+ def create_deterministic_generator(data: NIfTI_MRS_Plus,
171
+ water: Optional[NIfTI_MRS_Plus],
172
+ pipeline,
173
+ batch_size: int,
174
+ n_coils: Tuple,
175
+ n_averages: Tuple,
176
+ shuffle: bool = False):
177
+ """
178
+ Create deterministic generator over all combinations (backend-agnostic).
179
+
180
+ DEPRECATED: Use 'fixed' mode instead for simpler iteration.
181
+ This creates a combinatorial explosion of (subject × coils × averages).
182
+
183
+ Args:
184
+ data: NIfTI_MRS_Plus with subjects
185
+ water: Optional water reference
186
+ pipeline: Augmentation pipeline to apply
187
+ batch_size: Batch size
188
+ n_coils: (min, max) coils
189
+ n_averages: (min, max) averages
190
+ shuffle: Whether to shuffle indices
191
+
192
+ Yields:
193
+ (batch_data, batch_water) tuples
194
+ """
195
+ # Create all possible indices
196
+ indices = create_deterministic_indices(data, n_coils, n_averages)
197
+
198
+ if shuffle:
199
+ import random
200
+ random.shuffle(indices)
201
+
202
+ # Yield batches
203
+ for i in range(0, len(indices), batch_size):
204
+ batch_indices = indices[i:i + batch_size]
205
+ batch_data_list, batch_water_list = [], []
206
+
207
+ for subj_idx, coil_idx, avg_idx in batch_indices:
208
+ # Get subject
209
+ subj_data = data[subj_idx]
210
+ subj_water = water[subj_idx] if water is not None else None
211
+
212
+ # Apply pipeline with specific indices
213
+ aug_data, aug_water = pipeline(
214
+ subj_data, subj_water,
215
+ coil_indices=coil_idx,
216
+ average_indices=avg_idx
217
+ )
218
+
219
+ batch_data_list.append(aug_data)
220
+ batch_water_list.append(aug_water)
221
+
222
+ yield batch_data_list, batch_water_list
223
+
224
+
225
+ #**************************************************************************************************#
226
+ # Backend Conversion Functions #
227
+ #**************************************************************************************************#
228
+
229
+ def convert_batch_to_backend(batch_data: List, batch_water: List, backend: Backend):
230
+ """
231
+ Convert batch of NIFTI_MRS objects to target backend format.
232
+
233
+ Args:
234
+ batch_data: List of NIFTI_MRS or data arrays
235
+ batch_water: List of NIFTI_MRS or None
236
+ backend: Target backend
237
+
238
+ Returns:
239
+ (converted_data, converted_water)
240
+ """
241
+ if backend == Backend.NIFTI_LIST:
242
+ return batch_data, batch_water
243
+
244
+ elif backend == Backend.NUMPY:
245
+ # Convert to numpy arrays
246
+ data_arrays = []
247
+ for item in batch_data:
248
+ if hasattr(item, '__array__'):
249
+ data_arrays.append(np.array(item))
250
+ elif isinstance(item, NIfTI_MRS_Plus):
251
+ data_arrays.append(item.numpy())
252
+ else:
253
+ # Assume it's a NIFTI_MRS
254
+ data_arrays.append(item[:])
255
+
256
+ water_arrays = None
257
+ if batch_water and batch_water[0] is not None:
258
+ water_arrays = []
259
+ for item in batch_water:
260
+ if hasattr(item, '__array__'):
261
+ water_arrays.append(np.array(item))
262
+ elif isinstance(item, NIfTI_MRS_Plus):
263
+ water_arrays.append(item.numpy())
264
+ else:
265
+ water_arrays.append(item[:])
266
+ water_arrays = np.array(water_arrays) if water_arrays else None
267
+
268
+ return np.array(data_arrays), water_arrays
269
+
270
+ elif backend == Backend.PYTORCH:
271
+ import torch
272
+ # Convert to torch tensors
273
+ data_np, water_np = convert_batch_to_backend(batch_data, batch_water, Backend.NUMPY)
274
+ data_torch = torch.from_numpy(data_np) if data_np is not None else None
275
+ water_torch = torch.from_numpy(water_np) if water_np is not None else None
276
+ return data_torch, water_torch
277
+
278
+ elif backend in [Backend.TENSORFLOW, Backend.KERAS]:
279
+ import tensorflow as tf
280
+ # Convert to tensorflow tensors
281
+ data_np, water_np = convert_batch_to_backend(batch_data, batch_water, Backend.NUMPY)
282
+ data_tf = tf.convert_to_tensor(data_np) if data_np is not None else None
283
+ water_tf = tf.convert_to_tensor(water_np) if water_np is not None else None
284
+ return data_tf, water_tf
285
+
286
+ elif backend == Backend.JAX:
287
+ import jax.numpy as jnp
288
+ # Convert to jax arrays
289
+ data_np, water_np = convert_batch_to_backend(batch_data, batch_water, Backend.NUMPY)
290
+ data_jax = jnp.array(data_np) if data_np is not None else None
291
+ water_jax = jnp.array(water_np) if water_np is not None else None
292
+ return data_jax, water_jax
293
+
294
+ else:
295
+ raise ValueError(f"Unknown backend: {backend}")
296
+
297
+
298
+ #**************************************************************************************************#
299
+ # Framework DataLoader Wrappers #
300
+ #**************************************************************************************************#
301
+
302
+ def wrap_generator_for_framework(generator: Callable,
303
+ backend: Backend,
304
+ framework: str = None):
305
+ """
306
+ Wrap a generator for a specific framework (PyTorch DataLoader, tf.data, etc.).
307
+
308
+ Args:
309
+ generator: Generator (iterator) yielding (data, water) batches
310
+ backend: Target backend for data conversion
311
+ framework: 'pytorch', 'tensorflow', 'keras', 'jax', 'numpy', None (raw)
312
+
313
+ Returns:
314
+ Framework-specific dataloader/dataset/generator
315
+ """
316
+ # Default framework from backend
317
+ if framework is None:
318
+ backend_name = backend.name.lower()
319
+ # nifti_list backend should default to raw python iteration
320
+ if backend_name == 'nifti_list':
321
+ framework = 'python'
322
+ else:
323
+ framework = backend_name
324
+
325
+ # For nifti_list or raw python: unwrap NIfTI_MRS_Plus to raw NIFTI_MRS lists
326
+ if framework in ['python', 'nifti_list']:
327
+ def unwrapped_gen():
328
+ for batch_data, batch_water in generator:
329
+ # Unwrap NIfTI_MRS_Plus objects to raw NIFTI_MRS lists
330
+ if isinstance(batch_data, list):
331
+ # Batch is a list of NIfTI_MRS_Plus objects, unwrap each
332
+ data_list = []
333
+ for item in batch_data:
334
+ if hasattr(item, 'list'):
335
+ # It's a NIfTI_MRS_Plus, get the internal list
336
+ data_list.extend(item.list())
337
+ else:
338
+ # Already a NIFTI_MRS object
339
+ data_list.append(item)
340
+ batch_data = data_list
341
+ elif hasattr(batch_data, 'list'):
342
+ # Single NIfTI_MRS_Plus object
343
+ batch_data = batch_data.list()
344
+
345
+ if batch_water is not None:
346
+ if isinstance(batch_water, list):
347
+ water_list = []
348
+ for item in batch_water:
349
+ if hasattr(item, 'list'):
350
+ water_list.extend(item.list())
351
+ else:
352
+ water_list.append(item)
353
+ batch_water = water_list
354
+ elif hasattr(batch_water, 'list'):
355
+ batch_water = batch_water.list()
356
+
357
+ yield batch_data, batch_water
358
+ return unwrapped_gen()
359
+
360
+ # For numpy/jax: return generator directly with backend conversion
361
+ if framework in ['numpy', 'jax']:
362
+ def converted_gen():
363
+ for batch_data, batch_water in generator: # generator is already iterator
364
+ yield convert_batch_to_backend(batch_data, batch_water, backend)
365
+ return converted_gen()
366
+
367
+ # For PyTorch: wrap in DataLoader-like iterator
368
+ elif framework == 'pytorch':
369
+ def converted_gen():
370
+ for batch_data, batch_water in generator: # generator is already iterator
371
+ yield convert_batch_to_backend(batch_data, batch_water, Backend.PYTORCH)
372
+ return converted_gen()
373
+
374
+ # For TensorFlow/Keras: wrap in tf.data.Dataset
375
+ elif framework in ['tensorflow', 'keras']:
376
+ try:
377
+ import tensorflow as tf
378
+
379
+ def tf_gen():
380
+ for batch_data, batch_water in generator: # generator is already iterator
381
+ data_tf, water_tf = convert_batch_to_backend(
382
+ batch_data, batch_water, Backend.TENSORFLOW
383
+ )
384
+ yield data_tf, water_tf
385
+
386
+ # Infer output signature from first batch
387
+ # We need to peek at the generator - convert to list first
388
+ batch_list = list(generator)
389
+ if not batch_list:
390
+ raise ValueError("Empty generator")
391
+
392
+ first_data, first_water = batch_list[0]
393
+ data_shape = (None,) + first_data[0][:].shape if hasattr(first_data[0], 'shape') else (None, None)
394
+
395
+ output_signature = (
396
+ tf.TensorSpec(shape=data_shape, dtype=tf.complex64),
397
+ tf.TensorSpec(shape=data_shape, dtype=tf.complex64) if first_water else None
398
+ )
399
+
400
+ # Create generator from list
401
+ def list_gen():
402
+ for batch_data, batch_water in batch_list:
403
+ data_tf, water_tf = convert_batch_to_backend(
404
+ batch_data, batch_water, Backend.TENSORFLOW
405
+ )
406
+ yield data_tf, water_tf
407
+
408
+ return tf.data.Dataset.from_generator(
409
+ list_gen,
410
+ output_signature=output_signature
411
+ )
412
+ except ImportError:
413
+ print("Warning: TensorFlow not installed, returning numpy generator")
414
+ return wrap_generator_for_framework(generator, backend, 'numpy')
415
+
416
+ else:
417
+ raise ValueError(f"Unknown framework: {framework}")