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.
- augmentrum/__init__.py +23 -0
- augmentrum/augmentation/__init__.py +32 -0
- augmentrum/augmentation/amplitude_scaling.py +170 -0
- augmentrum/augmentation/apodization.py +275 -0
- augmentrum/augmentation/artificial_peaks.py +334 -0
- augmentrum/augmentation/baseline_augmentation.py +544 -0
- augmentrum/augmentation/eddy_current.py +278 -0
- augmentrum/augmentation/gaussian_noise.py +211 -0
- augmentrum/augmentation/line_broadening.py +179 -0
- augmentrum/augmentation/phase_frequency.py +259 -0
- augmentrum/augmentation/residual_water.py +264 -0
- augmentrum/augmentation/spatial_augmentations.py +859 -0
- augmentrum/augmentation/spurious_echoes.py +378 -0
- augmentrum/augmentation/zero_fill.py +163 -0
- augmentrum/core/__init__.py +10 -0
- augmentrum/core/augmentrum.py +1172 -0
- augmentrum/core/base_module.py +423 -0
- augmentrum/core/dataset_utils.py +417 -0
- augmentrum/core/nifti_mrs_plus.py +1079 -0
- augmentrum/core/pipeline.py +358 -0
- augmentrum/dataset/brainbeats.py +537 -0
- augmentrum/dataset/cows.py +302 -0
- augmentrum/dataset/fmrsinpain.py +208 -0
- augmentrum/processing/bicubic_interpolator.py +296 -0
- augmentrum/processing/nifti_raw_processor.py +389 -0
- augmentrum/processing/tricubic_interpolator.py +293 -0
- augmentrum/processing/utils.py +414 -0
- augmentrum/resources/__init__.py +14 -0
- augmentrum/resources/scans/__init__.py +0 -0
- augmentrum/sampling/coil_average_sampler.py +274 -0
- augmentrum/sampling/kspace_reconstructor.py +275 -0
- augmentrum/sampling/kspace_sampling.py +2562 -0
- augmentrum/sampling/subject_splitter.py +131 -0
- augmentrum/utils/__init__.py +10 -0
- augmentrum/utils/philips.py +90 -0
- augmentrum/utils/plotting.py +651 -0
- augmentrum/utils/tensor_ops.py +203 -0
- augmentrum-0.0.1.dist-info/METADATA +189 -0
- augmentrum-0.0.1.dist-info/RECORD +42 -0
- augmentrum-0.0.1.dist-info/WHEEL +5 -0
- augmentrum-0.0.1.dist-info/licenses/LICENSE +202 -0
- augmentrum-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
####################################################################################################
|
|
2
|
+
# plotting.py #
|
|
3
|
+
####################################################################################################
|
|
4
|
+
# #
|
|
5
|
+
# Authors: J. P. Merkofer (j.p.merkofer@tue.nl) #
|
|
6
|
+
# #
|
|
7
|
+
# Created: 2025-02-13 #
|
|
8
|
+
# #
|
|
9
|
+
# Purpose: State-of-the-art plotting utilities for NIfTI_MRS_Plus objects #
|
|
10
|
+
# Implements batch-aware plotting following NIfTI-MRS conventions #
|
|
11
|
+
# #
|
|
12
|
+
####################################################################################################
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
import matplotlib.pyplot as plt
|
|
18
|
+
from typing import Optional, Union, List, Tuple
|
|
19
|
+
import warnings
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
23
|
+
# Simple spectrum / fit plots (MRS paper style)
|
|
24
|
+
# Ported from scripts/visualizations.py so there is one canonical location.
|
|
25
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
26
|
+
|
|
27
|
+
def _crop_ppm(arr, ppmAxis, ppmLim):
|
|
28
|
+
"""Return (arr_cropped, ppmAxis_cropped) within *ppmLim* = (lo, hi)."""
|
|
29
|
+
beg = int((np.abs(ppmAxis - ppmLim[0])).argmin())
|
|
30
|
+
end = int((np.abs(ppmAxis - ppmLim[1])).argmin())
|
|
31
|
+
if beg > end:
|
|
32
|
+
beg, end = end, beg
|
|
33
|
+
return arr[beg:end], ppmAxis[beg:end]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _pick_part(arr, real):
|
|
37
|
+
"""Select real or imaginary part of a (possibly already real) array."""
|
|
38
|
+
return np.real(arr) if real else np.imag(arr)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _style_mrs_ax(ax, clean):
|
|
42
|
+
"""Canonical MRS axis style (inverted ppm, y-hidden); everything hidden when *clean*."""
|
|
43
|
+
for spine in ('right', 'top', 'left'):
|
|
44
|
+
ax.spines[spine].set_visible(False)
|
|
45
|
+
ax.yaxis.set_visible(False)
|
|
46
|
+
ax.yaxis.set_ticks_position('none')
|
|
47
|
+
ax.invert_xaxis()
|
|
48
|
+
if clean:
|
|
49
|
+
ax.spines['bottom'].set_visible(False)
|
|
50
|
+
ax.xaxis.set_visible(False)
|
|
51
|
+
ax.xaxis.set_ticks_position('none')
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _save_fig(fig, save_path, name):
|
|
55
|
+
"""Save *fig* to ``save_path/name.png``, close it, and return the path."""
|
|
56
|
+
if save_path is not None:
|
|
57
|
+
os.makedirs(save_path, exist_ok=True)
|
|
58
|
+
out = os.path.join(save_path, f'{name}.png')
|
|
59
|
+
fig.savefig(out, dpi=300, bbox_inches='tight', transparent=True)
|
|
60
|
+
plt.close(fig)
|
|
61
|
+
return out
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def plot_spec_and_fit(spec, fit, true=None, ppmAxis=None, ppmLim=None, name='',
|
|
66
|
+
save_path=None, real=True, clean=False):
|
|
67
|
+
"""Plot a measured spectrum, the model fit, and the residual on a ppm axis.
|
|
68
|
+
|
|
69
|
+
Visual style: Data (black) / Fit (red, α=0.6) / Residual offset above (dimgrey) /
|
|
70
|
+
optional True spectrum (green dashed). ppm-axis inverted, y-axis hidden.
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
spec : np.ndarray (complex) measured spectrum (1-D).
|
|
75
|
+
fit : np.ndarray (complex) forward-modelled fit (1-D).
|
|
76
|
+
true : np.ndarray (complex), optional ground-truth spectrum.
|
|
77
|
+
ppmAxis : np.ndarray, optional chemical-shift axis matching *spec*. Defaults to
|
|
78
|
+
``linspace(0.5, 4.0, len(spec))``.
|
|
79
|
+
ppmLim : tuple(float, float), optional (lo, hi) ppm window to display.
|
|
80
|
+
name : str filename stem (without extension).
|
|
81
|
+
save_path : str, optional directory to save PNG into; created if missing.
|
|
82
|
+
real : bool if True plot real part, else imaginary.
|
|
83
|
+
clean : bool if True hide all axes/labels (publication snippet style).
|
|
84
|
+
"""
|
|
85
|
+
if ppmAxis is None:
|
|
86
|
+
ppmAxis = np.linspace(0.5, 4.0, spec.shape[0])
|
|
87
|
+
|
|
88
|
+
if ppmLim is not None:
|
|
89
|
+
orig_ppm = ppmAxis # save BEFORE first crop
|
|
90
|
+
spec, ppmAxis = _crop_ppm(spec, orig_ppm, ppmLim)
|
|
91
|
+
fit_ppm = orig_ppm if fit.shape[0] == orig_ppm.shape[0] \
|
|
92
|
+
else np.linspace(orig_ppm[0], orig_ppm[-1], fit.shape[0])
|
|
93
|
+
fit, _ = _crop_ppm(fit, fit_ppm, ppmLim)
|
|
94
|
+
if true is not None:
|
|
95
|
+
true_ppm = orig_ppm if true.shape[0] == orig_ppm.shape[0] \
|
|
96
|
+
else np.linspace(orig_ppm[0], orig_ppm[-1], true.shape[0])
|
|
97
|
+
true, _ = _crop_ppm(true, true_ppm, ppmLim)
|
|
98
|
+
|
|
99
|
+
spec = _pick_part(spec, real)
|
|
100
|
+
fit = _pick_part(fit, real)
|
|
101
|
+
residual = spec - fit + 1.1 * spec.max()
|
|
102
|
+
|
|
103
|
+
fig, ax = plt.subplots(figsize=(4, 3.5))
|
|
104
|
+
ax.plot(ppmAxis, spec, 'k', label='Data', linewidth=1)
|
|
105
|
+
ax.plot(ppmAxis, fit, 'r', label='Fit', alpha=0.6, linewidth=2)
|
|
106
|
+
ax.plot(ppmAxis, residual, 'dimgrey', label='Residual', alpha=0.8, linewidth=1)
|
|
107
|
+
if true is not None:
|
|
108
|
+
true = _pick_part(true, real)
|
|
109
|
+
ax.plot(ppmAxis, true, 'g', label='True Spectrum',
|
|
110
|
+
alpha=0.8, linewidth=1, linestyle='--')
|
|
111
|
+
|
|
112
|
+
if not clean:
|
|
113
|
+
ax.set_xlabel('Chemical Shift [ppm]')
|
|
114
|
+
ax.legend(frameon=False, fontsize=8, loc='upper left')
|
|
115
|
+
_style_mrs_ax(ax, clean)
|
|
116
|
+
return _save_fig(fig, save_path, name)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def plot_spec(spec, ppmAxis=None, ppmLim=None, name='', save_path=None,
|
|
120
|
+
real=True, color='k', title=None, clean=False):
|
|
121
|
+
"""Plot a single complex spectrum on a ppm axis (no fit / residual).
|
|
122
|
+
|
|
123
|
+
Same visual style as :func:`plot_spec_and_fit` — useful for sanity-checking
|
|
124
|
+
the simulated input in isolation.
|
|
125
|
+
|
|
126
|
+
Parameters
|
|
127
|
+
----------
|
|
128
|
+
spec : np.ndarray (complex) spectrum to display (1-D).
|
|
129
|
+
ppmAxis : np.ndarray, optional chemical-shift axis matching *spec*.
|
|
130
|
+
ppmLim : tuple(float, float), optional (lo, hi) ppm window to display.
|
|
131
|
+
name : str filename stem (without extension).
|
|
132
|
+
save_path : str, optional directory to save PNG into; created if missing.
|
|
133
|
+
real : bool if True plot real part, else imaginary.
|
|
134
|
+
color : str matplotlib colour string for the spectrum line.
|
|
135
|
+
title : str, optional axes title (small font, publication-friendly).
|
|
136
|
+
clean : bool if True hide all axes/labels (publication snippet style).
|
|
137
|
+
"""
|
|
138
|
+
if ppmAxis is None:
|
|
139
|
+
ppmAxis = np.linspace(0.5, 4.0, spec.shape[0])
|
|
140
|
+
|
|
141
|
+
if ppmLim is not None:
|
|
142
|
+
spec, ppmAxis = _crop_ppm(spec, ppmAxis, ppmLim)
|
|
143
|
+
|
|
144
|
+
spec = _pick_part(spec, real)
|
|
145
|
+
|
|
146
|
+
fig, ax = plt.subplots(figsize=(4, 3.5))
|
|
147
|
+
ax.plot(ppmAxis, spec, color, label='Data', linewidth=1)
|
|
148
|
+
if not clean:
|
|
149
|
+
ax.set_xlabel('Chemical Shift [ppm]')
|
|
150
|
+
if title:
|
|
151
|
+
ax.set_title(title, fontsize=9)
|
|
152
|
+
ax.legend(frameon=False, fontsize=8, loc='upper left')
|
|
153
|
+
_style_mrs_ax(ax, clean)
|
|
154
|
+
return _save_fig(fig, save_path, name)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
158
|
+
# NIfTI-MRS batch plotting (existing code below, unchanged)
|
|
159
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
from fsl_mrs.utils.plotting import plot_spectrum, plot_spectra
|
|
163
|
+
from fsl_mrs.core.mrs import MRS
|
|
164
|
+
from fsl_mrs.utils import constants
|
|
165
|
+
FSL_MRS_AVAILABLE = True
|
|
166
|
+
except ImportError:
|
|
167
|
+
FSL_MRS_AVAILABLE = False
|
|
168
|
+
warnings.warn(
|
|
169
|
+
"FSL-MRS not available. Some plotting features will be limited. "
|
|
170
|
+
"Install FSL-MRS for full functionality: pip install fsl-mrs"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def vis_nifti_mrs_plus(
|
|
175
|
+
data,
|
|
176
|
+
display_dim=None,
|
|
177
|
+
ppmlim=None,
|
|
178
|
+
plot_avg=False,
|
|
179
|
+
batch_index=None,
|
|
180
|
+
max_batch_display=6,
|
|
181
|
+
grid_layout=None,
|
|
182
|
+
legend=True,
|
|
183
|
+
figsize=None,
|
|
184
|
+
title=None
|
|
185
|
+
):
|
|
186
|
+
"""
|
|
187
|
+
Visualize NIfTI_MRS_Plus objects with batch-aware plotting.
|
|
188
|
+
|
|
189
|
+
This function mirrors the NIfTI-MRS plot() method but adds batch support.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
data: NIfTI_MRS_Plus object
|
|
193
|
+
display_dim: Dimension to display (if multiple dims exist)
|
|
194
|
+
ppmlim: Tuple of (min_ppm, max_ppm) for x-axis limits
|
|
195
|
+
plot_avg: If True, plot average spectrum
|
|
196
|
+
batch_index: Which batch element to plot (None = plot all up to max_batch_display)
|
|
197
|
+
max_batch_display: Maximum number of batch elements to display
|
|
198
|
+
grid_layout: Tuple (rows, cols) for grid layout. Auto if None
|
|
199
|
+
legend: Whether to show legend
|
|
200
|
+
figsize: Figure size (width, height)
|
|
201
|
+
title: Plot title
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
matplotlib figure object
|
|
205
|
+
|
|
206
|
+
Example:
|
|
207
|
+
>>> nifti_plus.plot() # Plot first spectrum
|
|
208
|
+
>>> nifti_plus.plot(batch_index=0) # Plot first batch element
|
|
209
|
+
>>> nifti_plus.plot(batch_index=None, max_batch_display=4) # Plot first 4
|
|
210
|
+
>>> nifti_plus.plot(ppmlim=(0.5, 4.2)) # Custom ppm range
|
|
211
|
+
"""
|
|
212
|
+
from augmentrum.core.nifti_mrs_plus import NIfTI_MRS_Plus
|
|
213
|
+
|
|
214
|
+
if not isinstance(data, NIfTI_MRS_Plus):
|
|
215
|
+
raise TypeError("data must be a NIfTI_MRS_Plus object")
|
|
216
|
+
|
|
217
|
+
# Get list of NIFTI_MRS objects
|
|
218
|
+
nifti_list = data.list()
|
|
219
|
+
|
|
220
|
+
if len(nifti_list) == 0:
|
|
221
|
+
raise ValueError("NIfTI_MRS_Plus object is empty")
|
|
222
|
+
|
|
223
|
+
# Determine ppm limits
|
|
224
|
+
if ppmlim is None and len(nifti_list) > 0:
|
|
225
|
+
first_nifti = nifti_list[0]
|
|
226
|
+
if hasattr(first_nifti, 'nucleus') and FSL_MRS_AVAILABLE:
|
|
227
|
+
nuc_info = constants.nucleus_constants(first_nifti.nucleus[0])
|
|
228
|
+
if nuc_info.ppm_range:
|
|
229
|
+
ppmlim = nuc_info.ppm_range
|
|
230
|
+
else:
|
|
231
|
+
ppmlim = (0.2, 4.2)
|
|
232
|
+
else:
|
|
233
|
+
ppmlim = (0.2, 4.2)
|
|
234
|
+
|
|
235
|
+
# Handle batch indexing
|
|
236
|
+
if batch_index is not None:
|
|
237
|
+
# Plot single batch element
|
|
238
|
+
if batch_index < 0 or batch_index >= len(nifti_list):
|
|
239
|
+
raise IndexError(f"batch_index {batch_index} out of range [0, {len(nifti_list)})")
|
|
240
|
+
|
|
241
|
+
nifti_to_plot = nifti_list[batch_index]
|
|
242
|
+
|
|
243
|
+
if FSL_MRS_AVAILABLE and hasattr(nifti_to_plot, 'plot'):
|
|
244
|
+
# Use native NIfTI-MRS plotting
|
|
245
|
+
fig = nifti_to_plot.plot(
|
|
246
|
+
display_dim=display_dim,
|
|
247
|
+
ppmlim=ppmlim,
|
|
248
|
+
plot_avg=plot_avg,
|
|
249
|
+
legend=legend
|
|
250
|
+
)
|
|
251
|
+
if title:
|
|
252
|
+
fig.suptitle(title, fontsize=14, fontweight='bold')
|
|
253
|
+
elif not title and len(nifti_list) > 1:
|
|
254
|
+
fig.suptitle(f"Batch Element {batch_index+1}/{len(nifti_list)}", fontsize=12)
|
|
255
|
+
return fig
|
|
256
|
+
else:
|
|
257
|
+
# Fallback plotting
|
|
258
|
+
return _plot_single_spectrum(
|
|
259
|
+
nifti_to_plot,
|
|
260
|
+
ppmlim=ppmlim,
|
|
261
|
+
title=title or f"Batch Element {batch_index+1}/{len(nifti_list)}"
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
else:
|
|
265
|
+
# Plot multiple batch elements
|
|
266
|
+
n_to_plot = min(len(nifti_list), max_batch_display)
|
|
267
|
+
|
|
268
|
+
if n_to_plot == 1:
|
|
269
|
+
# Just plot the single element
|
|
270
|
+
return vis_nifti_mrs_plus(
|
|
271
|
+
data,
|
|
272
|
+
display_dim=display_dim,
|
|
273
|
+
ppmlim=ppmlim,
|
|
274
|
+
plot_avg=plot_avg,
|
|
275
|
+
batch_index=0,
|
|
276
|
+
legend=legend,
|
|
277
|
+
title=title
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# Determine grid layout
|
|
281
|
+
if grid_layout is None:
|
|
282
|
+
if n_to_plot <= 2:
|
|
283
|
+
rows, cols = 1, n_to_plot
|
|
284
|
+
elif n_to_plot <= 4:
|
|
285
|
+
rows, cols = 2, 2
|
|
286
|
+
elif n_to_plot <= 6:
|
|
287
|
+
rows, cols = 2, 3
|
|
288
|
+
elif n_to_plot <= 9:
|
|
289
|
+
rows, cols = 3, 3
|
|
290
|
+
else:
|
|
291
|
+
rows, cols = 4, 3
|
|
292
|
+
else:
|
|
293
|
+
rows, cols = grid_layout
|
|
294
|
+
|
|
295
|
+
# Create figure
|
|
296
|
+
if figsize is None:
|
|
297
|
+
figsize = (5 * cols, 3.5 * rows)
|
|
298
|
+
|
|
299
|
+
fig, axes = plt.subplots(rows, cols, figsize=figsize, squeeze=False)
|
|
300
|
+
axes = axes.flatten()
|
|
301
|
+
|
|
302
|
+
# Plot each batch element
|
|
303
|
+
for i in range(n_to_plot):
|
|
304
|
+
nifti = nifti_list[i]
|
|
305
|
+
ax = axes[i]
|
|
306
|
+
|
|
307
|
+
# Plot spectrum on this axis
|
|
308
|
+
_plot_spectrum_on_axis(
|
|
309
|
+
nifti,
|
|
310
|
+
ax=ax,
|
|
311
|
+
ppmlim=ppmlim,
|
|
312
|
+
title=f"Batch {i+1}",
|
|
313
|
+
legend=False
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Hide unused subplots
|
|
317
|
+
for i in range(n_to_plot, len(axes)):
|
|
318
|
+
axes[i].axis('off')
|
|
319
|
+
|
|
320
|
+
# Overall title
|
|
321
|
+
if title:
|
|
322
|
+
fig.suptitle(title, fontsize=16, fontweight='bold', y=0.995)
|
|
323
|
+
else:
|
|
324
|
+
fig.suptitle(
|
|
325
|
+
f"NIfTI_MRS_Plus Batch ({n_to_plot}/{len(nifti_list)} displayed)",
|
|
326
|
+
fontsize=14,
|
|
327
|
+
fontweight='bold',
|
|
328
|
+
y=0.995
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
plt.tight_layout()
|
|
332
|
+
return fig
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _plot_single_spectrum(nifti, ppmlim=(0.2, 4.2), title="MRS Spectrum"):
|
|
336
|
+
"""Plot a single NIfTI-MRS spectrum using basic matplotlib."""
|
|
337
|
+
fig, ax = plt.subplots(1, 1, figsize=(10, 4))
|
|
338
|
+
_plot_spectrum_on_axis(nifti, ax, ppmlim, title)
|
|
339
|
+
plt.tight_layout()
|
|
340
|
+
return fig
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _plot_spectrum_on_axis(nifti, ax, ppmlim=(0.2, 4.2), title="", legend=True):
|
|
344
|
+
"""Plot spectrum on axis, properly handling raw multi-coil/average data."""
|
|
345
|
+
from fsl_mrs.utils.preproc import nifti_mrs_proc
|
|
346
|
+
from fsl_mrs.core.mrs import MRS
|
|
347
|
+
|
|
348
|
+
# Process data like vis_nifti_mrs does
|
|
349
|
+
processed = nifti
|
|
350
|
+
|
|
351
|
+
# Handle coil combination
|
|
352
|
+
dim_tags = getattr(processed, 'dim_tags', None)
|
|
353
|
+
# Check if dim_tags is iterable (not None, not Mock)
|
|
354
|
+
try:
|
|
355
|
+
has_coil_dim = dim_tags is not None and 'DIM_COIL' in dim_tags
|
|
356
|
+
except TypeError:
|
|
357
|
+
has_coil_dim = False
|
|
358
|
+
|
|
359
|
+
if has_coil_dim and processed.shape[processed.dim_position('DIM_COIL')] > 1:
|
|
360
|
+
processed = nifti_mrs_proc.coilcombine(processed)
|
|
361
|
+
|
|
362
|
+
# Handle all other dimensions
|
|
363
|
+
if dim_tags:
|
|
364
|
+
try:
|
|
365
|
+
for dim in dim_tags:
|
|
366
|
+
if dim is None:
|
|
367
|
+
continue
|
|
368
|
+
if processed.shape[processed.dim_position(dim)] > 1:
|
|
369
|
+
if dim in ('DIM_EDIT', 'DIM_METCYCLE', 'DIM_ISIS'):
|
|
370
|
+
processed = nifti_mrs_proc.subtract(processed, dim=dim)
|
|
371
|
+
else:
|
|
372
|
+
processed = nifti_mrs_proc.average(processed, dim)
|
|
373
|
+
except TypeError:
|
|
374
|
+
# dim_tags is not iterable (e.g., Mock object)
|
|
375
|
+
pass
|
|
376
|
+
|
|
377
|
+
# Create MRS object
|
|
378
|
+
mrs = MRS(
|
|
379
|
+
processed[:].squeeze(),
|
|
380
|
+
bw=processed.bandwidth,
|
|
381
|
+
cf=processed.spectrometer_frequency[0],
|
|
382
|
+
nucleus=processed.nucleus[0]
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
# Get spectrum
|
|
386
|
+
ppm = mrs.getAxes(ppmlim=ppmlim)
|
|
387
|
+
spec = mrs.get_spec(ppmlim=ppmlim)
|
|
388
|
+
|
|
389
|
+
# Plot
|
|
390
|
+
ax.plot(ppm, np.real(spec), 'k-', linewidth=1.0, label='Real' if legend else None)
|
|
391
|
+
|
|
392
|
+
# Formatting
|
|
393
|
+
ax.invert_xaxis()
|
|
394
|
+
if ppmlim is not None:
|
|
395
|
+
ax.set_xlim(ppmlim[1], ppmlim[0])
|
|
396
|
+
ax.set_xlabel("Chemical Shift (ppm)", fontsize=10)
|
|
397
|
+
ax.set_ylabel("Amplitude (a.u.)", fontsize=10)
|
|
398
|
+
if title:
|
|
399
|
+
ax.set_title(title, fontsize=11, fontweight='bold')
|
|
400
|
+
ax.grid(alpha=0.3, linestyle='--', linewidth=0.5)
|
|
401
|
+
|
|
402
|
+
if legend:
|
|
403
|
+
ax.legend(loc='best', fontsize=8)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def plot_batch_comparison(
|
|
407
|
+
nifti_plus,
|
|
408
|
+
indices=None,
|
|
409
|
+
ppmlim=(0.2, 4.2),
|
|
410
|
+
labels=None,
|
|
411
|
+
title="Batch Comparison",
|
|
412
|
+
colors=None,
|
|
413
|
+
alpha=0.7,
|
|
414
|
+
figsize=(12, 5)
|
|
415
|
+
):
|
|
416
|
+
"""
|
|
417
|
+
Plot multiple spectra from a batch overlaid for comparison.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
nifti_plus: NIfTI_MRS_Plus object
|
|
421
|
+
indices: List of batch indices to plot (None = all up to 6)
|
|
422
|
+
ppmlim: PPM limits (min, max)
|
|
423
|
+
labels: List of labels for each spectrum
|
|
424
|
+
title: Plot title
|
|
425
|
+
colors: List of colors for each spectrum
|
|
426
|
+
alpha: Transparency for overlaid spectra
|
|
427
|
+
figsize: Figure size
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
matplotlib figure
|
|
431
|
+
|
|
432
|
+
Example:
|
|
433
|
+
>>> plot_batch_comparison(nifti_plus, indices=[0, 1, 2])
|
|
434
|
+
>>> plot_batch_comparison(nifti_plus, labels=['Original', 'Augmented 1', 'Augmented 2'])
|
|
435
|
+
"""
|
|
436
|
+
from augmentrum.core.nifti_mrs_plus import NIfTI_MRS_Plus
|
|
437
|
+
|
|
438
|
+
if not isinstance(nifti_plus, NIfTI_MRS_Plus):
|
|
439
|
+
raise TypeError("nifti_plus must be a NIfTI_MRS_Plus object")
|
|
440
|
+
|
|
441
|
+
nifti_list = nifti_plus.list()
|
|
442
|
+
|
|
443
|
+
if indices is None:
|
|
444
|
+
indices = list(range(min(6, len(nifti_list))))
|
|
445
|
+
|
|
446
|
+
if labels is None:
|
|
447
|
+
labels = [f"Spectrum {i+1}" for i in indices]
|
|
448
|
+
|
|
449
|
+
if colors is None:
|
|
450
|
+
colors = plt.cm.tab10(np.linspace(0, 1, len(indices)))
|
|
451
|
+
|
|
452
|
+
from fsl_mrs.utils.preproc import nifti_mrs_proc
|
|
453
|
+
from fsl_mrs.core.mrs import MRS
|
|
454
|
+
from fsl_mrs.utils.plotting import plot_spectra
|
|
455
|
+
|
|
456
|
+
# Process each spectrum properly
|
|
457
|
+
mrs_list = []
|
|
458
|
+
for idx in indices:
|
|
459
|
+
if idx >= len(nifti_list):
|
|
460
|
+
warnings.warn(f"Index {idx} out of range, skipping")
|
|
461
|
+
continue
|
|
462
|
+
|
|
463
|
+
nifti = nifti_list[idx]
|
|
464
|
+
|
|
465
|
+
# Process like vis_nifti_mrs
|
|
466
|
+
processed = nifti
|
|
467
|
+
dim_tags = getattr(processed, 'dim_tags', None)
|
|
468
|
+
try:
|
|
469
|
+
has_coil_dim = dim_tags is not None and 'DIM_COIL' in dim_tags
|
|
470
|
+
except TypeError:
|
|
471
|
+
has_coil_dim = False
|
|
472
|
+
|
|
473
|
+
if has_coil_dim and processed.shape[processed.dim_position('DIM_COIL')] > 1:
|
|
474
|
+
processed = nifti_mrs_proc.coilcombine(processed)
|
|
475
|
+
|
|
476
|
+
if dim_tags:
|
|
477
|
+
try:
|
|
478
|
+
for dim in dim_tags:
|
|
479
|
+
if dim is None:
|
|
480
|
+
continue
|
|
481
|
+
if processed.shape[processed.dim_position(dim)] > 1:
|
|
482
|
+
if dim in ('DIM_EDIT', 'DIM_METCYCLE', 'DIM_ISIS'):
|
|
483
|
+
processed = nifti_mrs_proc.subtract(processed, dim=dim)
|
|
484
|
+
else:
|
|
485
|
+
processed = nifti_mrs_proc.average(processed, dim)
|
|
486
|
+
except TypeError:
|
|
487
|
+
pass
|
|
488
|
+
|
|
489
|
+
# Create MRS object
|
|
490
|
+
mrs = MRS(
|
|
491
|
+
processed[:].squeeze(),
|
|
492
|
+
bw=processed.bandwidth,
|
|
493
|
+
cf=processed.spectrometer_frequency[0],
|
|
494
|
+
nucleus=processed.nucleus[0]
|
|
495
|
+
)
|
|
496
|
+
mrs_list.append(mrs)
|
|
497
|
+
|
|
498
|
+
# Use FSL-MRS plot_spectra for overlay in ONE figure
|
|
499
|
+
fig = plot_spectra(mrs_list, ppmlim=ppmlim, legend=True)
|
|
500
|
+
|
|
501
|
+
# Customize
|
|
502
|
+
if fig and len(fig.axes) > 0:
|
|
503
|
+
ax = fig.axes[0]
|
|
504
|
+
ax.set_title(title, fontsize=14, fontweight='bold')
|
|
505
|
+
if len(labels) == len(mrs_list):
|
|
506
|
+
ax.legend(labels, loc='best', fontsize=10)
|
|
507
|
+
|
|
508
|
+
plt.tight_layout()
|
|
509
|
+
plt.show()
|
|
510
|
+
return fig
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def plot_batch_grid_detailed(
|
|
514
|
+
nifti_plus,
|
|
515
|
+
max_display=9,
|
|
516
|
+
ppmlim=(0.2, 4.2),
|
|
517
|
+
title="Batch Grid",
|
|
518
|
+
show_metabolites=False,
|
|
519
|
+
figsize=None
|
|
520
|
+
):
|
|
521
|
+
"""
|
|
522
|
+
Plot batch elements in a detailed grid with optional metabolite regions.
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
nifti_plus: NIfTI_MRS_Plus object
|
|
526
|
+
max_display: Maximum number of spectra to display
|
|
527
|
+
ppmlim: PPM limits
|
|
528
|
+
title: Overall title
|
|
529
|
+
show_metabolites: If True, highlight common metabolite regions
|
|
530
|
+
figsize: Figure size (auto if None)
|
|
531
|
+
|
|
532
|
+
Returns:
|
|
533
|
+
matplotlib figure
|
|
534
|
+
"""
|
|
535
|
+
from augmentrum.core.nifti_mrs_plus import NIfTI_MRS_Plus
|
|
536
|
+
|
|
537
|
+
if not isinstance(nifti_plus, NIfTI_MRS_Plus):
|
|
538
|
+
raise TypeError("nifti_plus must be a NIfTI_MRS_Plus object")
|
|
539
|
+
|
|
540
|
+
nifti_list = nifti_plus.list()
|
|
541
|
+
n_to_plot = min(len(nifti_list), max_display)
|
|
542
|
+
|
|
543
|
+
# Grid layout
|
|
544
|
+
if n_to_plot <= 3:
|
|
545
|
+
rows, cols = 1, n_to_plot
|
|
546
|
+
elif n_to_plot <= 6:
|
|
547
|
+
rows, cols = 2, 3
|
|
548
|
+
else:
|
|
549
|
+
rows, cols = 3, 3
|
|
550
|
+
|
|
551
|
+
if figsize is None:
|
|
552
|
+
figsize = (5 * cols, 3.5 * rows)
|
|
553
|
+
|
|
554
|
+
fig, axes = plt.subplots(rows, cols, figsize=figsize, squeeze=False)
|
|
555
|
+
axes = axes.flatten()
|
|
556
|
+
|
|
557
|
+
# Metabolite regions (1H MRS)
|
|
558
|
+
metabolite_regions = {
|
|
559
|
+
'NAA': (1.9, 2.1, '#E74C3C'),
|
|
560
|
+
'Cr': (2.9, 3.1, '#3498DB'),
|
|
561
|
+
'Cho': (3.1, 3.3, '#2ECC71'),
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
from fsl_mrs.utils.preproc import nifti_mrs_proc
|
|
565
|
+
from fsl_mrs.core.mrs import MRS
|
|
566
|
+
|
|
567
|
+
for i in range(n_to_plot):
|
|
568
|
+
ax = axes[i]
|
|
569
|
+
nifti = nifti_list[i]
|
|
570
|
+
|
|
571
|
+
# Process like vis_nifti_mrs
|
|
572
|
+
processed = nifti
|
|
573
|
+
dim_tags = getattr(processed, 'dim_tags', None)
|
|
574
|
+
try:
|
|
575
|
+
has_coil_dim = dim_tags is not None and 'DIM_COIL' in dim_tags
|
|
576
|
+
except TypeError:
|
|
577
|
+
has_coil_dim = False
|
|
578
|
+
|
|
579
|
+
if has_coil_dim and processed.shape[processed.dim_position('DIM_COIL')] > 1:
|
|
580
|
+
processed = nifti_mrs_proc.coilcombine(processed)
|
|
581
|
+
|
|
582
|
+
if dim_tags:
|
|
583
|
+
try:
|
|
584
|
+
for dim in dim_tags:
|
|
585
|
+
if dim is None:
|
|
586
|
+
continue
|
|
587
|
+
if processed.shape[processed.dim_position(dim)] > 1:
|
|
588
|
+
if dim in ('DIM_EDIT', 'DIM_METCYCLE', 'DIM_ISIS'):
|
|
589
|
+
processed = nifti_mrs_proc.subtract(processed, dim=dim)
|
|
590
|
+
else:
|
|
591
|
+
processed = nifti_mrs_proc.average(processed, dim)
|
|
592
|
+
except TypeError:
|
|
593
|
+
pass
|
|
594
|
+
|
|
595
|
+
# Create MRS object and get spectrum
|
|
596
|
+
mrs = MRS(
|
|
597
|
+
processed[:].squeeze(),
|
|
598
|
+
bw=processed.bandwidth,
|
|
599
|
+
cf=processed.spectrometer_frequency[0],
|
|
600
|
+
nucleus=processed.nucleus[0]
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
ppm = mrs.getAxes(ppmlim=ppmlim)
|
|
604
|
+
spec = mrs.get_spec(ppmlim=ppmlim)
|
|
605
|
+
|
|
606
|
+
# Plot
|
|
607
|
+
ax.plot(ppm, np.real(spec), 'k-', linewidth=1.0)
|
|
608
|
+
|
|
609
|
+
# Highlight metabolites
|
|
610
|
+
if show_metabolites:
|
|
611
|
+
for name, (ppm_min, ppm_max, color) in metabolite_regions.items():
|
|
612
|
+
ax.axvspan(ppm_min, ppm_max, alpha=0.15, color=color)
|
|
613
|
+
|
|
614
|
+
ax.invert_xaxis()
|
|
615
|
+
if ppmlim:
|
|
616
|
+
ax.set_xlim(ppmlim[1], ppmlim[0])
|
|
617
|
+
ax.set_ylabel("Amplitude", fontsize=9)
|
|
618
|
+
ax.set_title(f"Sample {i+1}", fontsize=10, fontweight='bold')
|
|
619
|
+
ax.grid(alpha=0.3, linestyle='--', linewidth=0.5)
|
|
620
|
+
|
|
621
|
+
# Only show x-label on bottom row
|
|
622
|
+
if i >= (rows - 1) * cols:
|
|
623
|
+
ax.set_xlabel("Chemical Shift (ppm)", fontsize=9)
|
|
624
|
+
|
|
625
|
+
# Hide unused subplots
|
|
626
|
+
for i in range(n_to_plot, len(axes)):
|
|
627
|
+
axes[i].axis('off')
|
|
628
|
+
|
|
629
|
+
fig.suptitle(title, fontsize=14, fontweight='bold', y=0.995)
|
|
630
|
+
plt.tight_layout()
|
|
631
|
+
plt.show()
|
|
632
|
+
return fig
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
# Convenience function for interactive use
|
|
636
|
+
def quick_plot(nifti_plus, index=0, ppmlim=(0.2, 4.2)):
|
|
637
|
+
"""
|
|
638
|
+
Quick plot of a single spectrum from batch.
|
|
639
|
+
|
|
640
|
+
Args:
|
|
641
|
+
nifti_plus: NIfTI_MRS_Plus object
|
|
642
|
+
index: Batch index to plot
|
|
643
|
+
ppmlim: PPM range
|
|
644
|
+
|
|
645
|
+
Example:
|
|
646
|
+
>>> quick_plot(nifti_plus, 0) # Plot first spectrum
|
|
647
|
+
"""
|
|
648
|
+
return vis_nifti_mrs_plus(nifti_plus, batch_index=index, ppmlim=ppmlim)
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
|