mrs_tools 13 KB
Newer Older
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
#!/usr/bin/env python

""" mrs_tools - top level script for calling general mrs handling tools

Author: William Clarke <william.clarke@ndcn.ox.ac.uk>

Copyright (C) 2021 University of Oxford
SHBASECOPYRIGHT
"""

import argparse
from pathlib import Path

from fsl_mrs import __version__


def main():
    # Parse command-line arguments
    p = argparse.ArgumentParser(description="FSL Magnetic Resonance Spectroscopy - Tools")

    p.add_argument('-v', '--version', action='version', version=__version__)

    sp = p.add_subparsers(title='subcommands',
                          description='Availible tools',
                          required=True,
                          dest='subcommand')

    # Info tool
    infoparser = sp.add_parser(
        'info',
        help='Information about the NIfTI-MRS file.')
    infoparser.add_argument(
        'file',
        type=Path,
        metavar='FILE or list of FILEs',
        help='NIfTI MRS file(s)', nargs='+')
    infoparser.set_defaults(func=info)

    # Vis tool
    visparser = sp.add_parser(
        'vis',
        help='Quick visualisation of a NIfTI-MRS file or FSL-MRS basis set.')
    visparser.add_argument('file', type=Path, metavar='FILE or DIR',
                           help='NIfTI file or directory of basis sets')
    visparser.add_argument('--ppmlim', default=(.2, 4.2), type=float,
                           nargs=2, metavar=('LOW', 'HIGH'),
                           help='limit the fit to a freq range (default=(.2,4.2))')
    visparser.add_argument('--mask', default=None, type=str, help='Mask for MRSI')
    visparser.add_argument('--save', default=None, type=str, help='Save fig to path')
    visparser.add_argument('--display_dim', default=None, type=str,
                           help='NIFTI-MRS tag. Do not average across this dimension.')
52
53
    visparser.add_argument('--no_mean', action="store_false",
                           help='Do not plot the mean signal line in the case of multiple spectra.')
54
55
    visparser.set_defaults(func=vis)

56
57
58
59
    # Merge tool - Merge NIfTI MRS along higher dimensions
    mergeparser = sp.add_parser(
        'merge',
        help='Merge NIfTI-MRS along higher dimensions.')
60
61
62
63
64
65
    merge_req = mergeparser.add_argument_group('required arguments')
    merge_req.add_argument('--files', type=Path, nargs='+', required=True,
                           help='List of files to merge')
    merge_req.add_argument('--dim', type=str, required=True,
                           help='NIFTI-MRS dimension tag to merge across.')
    mergeparser.add_argument('--output', type=Path, default=Path('.'),
66
67
68
69
70
71
72
73
74
                             help='output folder (defaults to current directory)')
    mergeparser.add_argument('--filename', type=str,
                             help='Override output file name.')
    mergeparser.set_defaults(func=merge)

    # Split tool
    splitparser = sp.add_parser(
        'split',
        help='Split NIfTI-MRS along higher dimensions.')
75
76
77
78
79
    split_req = splitparser.add_argument_group('required arguments')
    split_req.add_argument('--file', type=Path, required=True,
                           help='File to split')
    split_req.add_argument('--dim', type=str, required=True,
                           help='NIFTI-MRS dimension tag to split across.')
80
81
82
    group = splitparser.add_mutually_exclusive_group(required=True)
    group.add_argument('--indices', type=int, nargs='+',
                       help='List of indices to extract into second file.'
83
                            ' All indices are zero-indexed.')
William Clarke's avatar
William Clarke committed
84
    group.add_argument('--index', type=int,
85
86
                       help='Index to split at (split after index, zero-indexed).')
    splitparser.add_argument('--output',
87
                             required=False, type=Path, default=Path('.'),
88
89
90
91
92
93
94
95
                             help='output folder (defaults to current directory)')
    splitparser.add_argument('--filename', type=str,
                             help='Override output file names.')
    splitparser.set_defaults(func=split)

    # Reorder tool
    reorderparser = sp.add_parser(
        'reorder',
96
        help='Reorder higher dimensions of NIfTI-MRS.')
97
98
99
100
101
102
103
    reord_req = reorderparser.add_argument_group('required arguments')
    reord_req.add_argument('--file', type=Path, required=True,
                           help='File to reorder')
    reord_req.add_argument('--dim_order', type=str, nargs='+', required=True,
                           help='NIFTI-MRS dimension tags in desired order. '
                                'Enter as strings (min:1, max:3). '
                                'Can create singleton dimension at end.')
104
    reorderparser.add_argument('--output',
105
                               required=False, type=Path, default=Path('.'),
106
107
108
109
110
                               help='output folder (defaults to current directory)')
    reorderparser.add_argument('--filename', type=str,
                               help='Override output file names.')
    reorderparser.set_defaults(func=reorder)

111
112
113
114
    # conjugate tool
    conjparser = sp.add_parser(
        'conjugate',
        help='Conjugate data to correct phase/frequency convention in a NIfTI-MRS file.')
115
116
117
    conj_req = conjparser.add_argument_group('required arguments')
    conj_req.add_argument('--file', type=Path, required=True,
                          help='File to conjugate')
118
    conjparser.add_argument('--output',
119
                            required=False, type=Path, default=Path('.'),
120
121
122
123
124
                            help='output folder (defaults to current directory)')
    conjparser.add_argument('--filename', type=str,
                            help='Override output file names.')
    conjparser.set_defaults(func=conj)

125
126
127
128
129
130
131
132
    # Parse command-line arguments
    args = p.parse_args()

    # Call function
    args.func(args)


def info(args):
133
134
135
136
    """Prints basic information about NIfTI-MRS files
    :param args: Argparse interpreted arguments
    :type args: Namespace
    """
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
    from fsl_mrs.utils.mrs_io import read_FID
    from fsl_mrs.utils.constants import GYRO_MAG_RATIO

    for file in args.file:
        data = read_FID(str(file))

        print(f'\nRead file {file.name} ({file.parent.resolve()}).')
        print(f'NIfTI-MRS version {data.mrs_nifti_version}')
        print(f'Data shape {data.shape}')
        print(f'Dimension tags: {data.dim_tags}')

        print(f'Spectrometer Frequency: {data.spectrometer_frequency[0]} MHz')
        print(f'Dwelltime (Bandwidth): {data.dwelltime:0.3E}s ({data.bandwidth:0.0f} Hz)')
        print(f'Nucleus: {data.nucleus[0]}')
        if data.nucleus[0] in GYRO_MAG_RATIO:
            field_strength = data.spectrometer_frequency[0] / GYRO_MAG_RATIO[data.nucleus[0]]
            print(f'Field Strength: {field_strength:0.2f} T')
        print()


def vis(args):
158
159
160
161
    """Visualiser for NIfTI-MRS files
    :param args: Argparse interpreted arguments
    :type args: Namespace
    """
162
    from fsl_mrs.utils.plotting import plot_spectrum, plot_spectra
163
    from fsl_mrs.utils.mrs_io import read_FID, read_basis
164
    from fsl_mrs.utils.mrs_io.main import FileNotRecognisedError
165
166
167
168
169
170
    import matplotlib.pyplot as plt
    import numpy as np
    from fsl_mrs.utils.preproc import nifti_mrs_proc
    import nibabel as nib

    # Single nifti file
171
172
173
174
175
    def vis_nifti_mrs(file):
        data = read_FID(file)
        if data.ndim > 4 \
                and 'DIM_COIL' in data.dim_tags\
                and args.display_dim != 'DIM_COIL':
176
177
178
179
180
181
182
183
184
185
186
            print('Performing coil combination')
            data = nifti_mrs_proc.coilcombine(data)

        if np.prod(data.shape[:3]) == 1:
            # SVS
            if args.display_dim:
                for idx in range(data.ndim - 4):
                    if data.dim_tags[idx] != args.display_dim:
                        print(f'Averaging {data.dim_tags[idx]}')
                        data = nifti_mrs_proc.average(data, data.dim_tags[idx])

187
                fig = plot_spectra(data.mrs(), ppmlim=args.ppmlim, plot_avg=args.no_mean)
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
            else:
                while data.ndim > 4:
                    print(f'Averaging {data.dim_tags[0]}')
                    data = nifti_mrs_proc.average(data, data.dim_tags[0])
                fig = plot_spectrum(data.mrs(), ppmlim=args.ppmlim)
            if args.save is not None:
                fig.savefig(args.save)
            else:
                plt.show()

        else:
            while data.ndim > 4:
                print(f'Averaging {data.dim_tags[0]}')
                data = nifti_mrs_proc.average(data, data.dim_tags[0])

            mrsi = data.mrs()
            if args.mask is not None:
                mask_hdr = nib.load(args.mask)
                mask = np.asanyarray(mask_hdr.dataobj)
                if mask.ndim == 2:
                    mask = np.expand_dims(mask, 2)
                mrsi.set_mask(mask)
            mrsi.plot()

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
    # Some logic to figure out what we are dealing with
    p = args.file
    nifti_files = list(p.glob('*.nii*'))

    # Identify BASIS
    if (p.is_dir() and len(nifti_files) == 0)\
            or p.suffix.upper() == '.BASIS':

        # Some heuristics
        if p.is_dir():
            conj = True
        else:
            conj = False

        basis = read_basis(args.file)
        _ = basis.plot(ppmlim=args.ppmlim, conjugate=conj)
        if args.save is not None:
            plt.savefig(args.save)
        else:
            plt.show()

    # Identify directory of nifti files
    elif p.is_dir() and len(nifti_files) > 0:
        raise ValueError('mrs_tools vis should be called on a single'
                         ' NIFTI-MRS file, not a directory (unless'
                         ' it contains basis files).')

    elif p.is_file():
        vis_nifti_mrs(p)

    else:
        try:
            vis_nifti_mrs(p)
        except FileNotRecognisedError as exc:
            raise FileNotFoundError(
                f"No file or directory '{p}' found."
                " Please specify correct file extension (e.g. nii.gz) if there is one.")\
                from exc

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
288
289
290
291
def merge(args):
    """Merges one or more NIfTI-MRS files along a specified dimension
    :param args: Argparse interpreted arguments
    :type args: Namespace
    """
    from fsl_mrs.utils import nifti_mrs_tools as nmrs_tools
    from fsl_mrs.utils import mrs_io
    # 1. Load the files
    if len(args.files) < 2:
        raise ValueError('Files argument must provide two or more files to merge.')

    to_concat = []
    concat_names = []
    for fp in args.files:
        concat_names.append(fp.with_suffix('').with_suffix('').name)
        to_concat.append(mrs_io.read_FID(str(fp)))

    # 2. Merge the files
    merged = nmrs_tools.merge(to_concat, args.dim)

    # 3. Save the output file
    if args.filename:
        file_out = args.output / args.filename
    else:
        file_out = args.output / ('_'.join(concat_names) + '_merged')
    merged.save(file_out)


def split(args):
    """Splits a NIfTI-MRS file into two along a specified dimension
    :param args: Argparse interpreted arguments
    :type args: Namespace
    """
    from fsl_mrs.utils import nifti_mrs_tools as nmrs_tools
    from fsl_mrs.utils import mrs_io
    # 1. Load the file
    to_split = mrs_io.read_FID(str(args.file))
    split_name = args.file.with_suffix('').with_suffix('').name

    # 2. Merge the files
William Clarke's avatar
William Clarke committed
292
    if args.index is not None:
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
        split_1, split_2 = nmrs_tools.split(to_split, args.dim, args.index)
    elif args.indices:
        split_1, split_2 = nmrs_tools.split(to_split, args.dim, args.indices)

    # 3. Save the output file
    if args.filename:
        file_out_1 = args.output / (args.filename + '_1')
        file_out_2 = args.output / (args.filename + '_2')
    else:
        file_out_1 = args.output / (split_name + '_1')
        file_out_2 = args.output / (split_name + '_2')
    split_1.save(file_out_1)
    split_2.save(file_out_2)


def reorder(args):
    """Reorders the higher dimensions of a NIfTI-MRS file
    :param args: Argparse interpreted arguments
    :type args: Namespace
    """
    from fsl_mrs.utils import nifti_mrs_tools as nmrs_tools
    from fsl_mrs.utils import mrs_io
    # 1. Load the file
    to_reorder = mrs_io.read_FID(str(args.file))
    reorder_name = args.file.with_suffix('').with_suffix('').name

    # 2. Merge the files
    dim_order = args.dim_order
    while len(dim_order) < 3:
        dim_order.append(None)
    reordered = nmrs_tools.reorder(to_reorder, args.dim_order)

    # 3. Save the output file
    if args.filename:
        file_out = args.output / args.filename
    else:
        file_out = args.output / (reorder_name + '_reordered')
    reordered.save(file_out)


333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
def conj(args):
    """Conjugate the data in a nifti-mrs file

    :param args: Argparse interpreted arguments
    :type args: Namespace
    """
    from fsl_mrs.utils import nifti_mrs_tools as nmrs_tools
    from fsl_mrs.utils import mrs_io
    # 1. Load the file
    infile = mrs_io.read_FID(str(args.file))
    name = args.file.with_suffix('').with_suffix('').name

    # 2. conjugate the file
    outfile = nmrs_tools.conjugate(infile)

    # 3. Save the output file
    if args.filename:
        file_out = args.output / args.filename
    else:
        file_out = args.output / name
    outfile.save(file_out)


356
357
if __name__ == '__main__':
    main()