Commit 04adb4db authored by William Clarke's avatar William Clarke
Browse files

Merge branch 'master' into 'master'

Merge for version 1.1.7 - pre FSL Course

Closes #6, #7, #11, #13, and #15

See merge request !25
parents baf4b0cf 8c75ef8f
Pipeline #11097 passed with stages
in 4 minutes and 13 seconds
...@@ -105,7 +105,7 @@ pages: ...@@ -105,7 +105,7 @@ pages:
stage: doc stage: doc
script: script:
- git describe --tag --dirty - git describe --tag --dirty
- pip install -U sphinx sphinx_rtd_theme - pip install -U sphinx sphinx_rtd_theme==0.5.2
- pip install . - pip install .
- sphinx-build -b html ./docs/user_docs public - sphinx-build -b html ./docs/user_docs public
artifacts: artifacts:
......
This document contains the FSL-MRS release history in reverse chronological order. This document contains the FSL-MRS release history in reverse chronological order.
1.1.7 (Monday 4th October 2021)
-------------------------------
- Fixed commandline arguments for mrs_tools.
- mrs_tools now handles files with passed without extension.
- Fixed plotting orientation for preprocessing reports.
- CRLB are now reported in scaled absolute and percentage units.
- mrs_tools vis now handles DIM_COIL dimension appropriately with --display_dim command.
- Added a --no_mean command to mrs_tools vis to remove the average signal in multi dimensional data.
1.1.6 (Monday 20th September 2021) 1.1.6 (Monday 20th September 2021)
----------- ----------------------------------
- Updates to dynamic MRS fitting in prep for 2021 dwMRS workshop. - Updates to dynamic MRS fitting in prep for 2021 dwMRS workshop.
- Dynamic MRS fitting beta: pending tests, documentation, and final features. - Dynamic MRS fitting beta: pending tests, documentation, and final features.
......
...@@ -88,7 +88,7 @@ def main(): ...@@ -88,7 +88,7 @@ def main():
'Or "all" to align over all spectra in higer dimensions.' 'Or "all" to align over all spectra in higer dimensions.'
'Default = DIM_DYN') 'Default = DIM_DYN')
align_group.add_argument('--ppm', type=float, nargs=2, align_group.add_argument('--ppm', type=float, nargs=2,
metavar='<lower-limit upper-limit>', metavar=('<lower-limit>', '<upper-limit>'),
default=(0.2, 4.2), default=(0.2, 4.2),
help='ppm limits of alignment window' help='ppm limits of alignment window'
' (default=0.2->4.2)') ' (default=0.2->4.2)')
......
...@@ -49,18 +49,20 @@ def main(): ...@@ -49,18 +49,20 @@ def main():
visparser.add_argument('--save', default=None, type=str, help='Save fig to path') visparser.add_argument('--save', default=None, type=str, help='Save fig to path')
visparser.add_argument('--display_dim', default=None, type=str, visparser.add_argument('--display_dim', default=None, type=str,
help='NIFTI-MRS tag. Do not average across this dimension.') help='NIFTI-MRS tag. Do not average across this dimension.')
visparser.add_argument('--no_mean', action="store_false",
help='Do not plot the mean signal line in the case of multiple spectra.')
visparser.set_defaults(func=vis) visparser.set_defaults(func=vis)
# Merge tool - Merge NIfTI MRS along higher dimensions # Merge tool - Merge NIfTI MRS along higher dimensions
mergeparser = sp.add_parser( mergeparser = sp.add_parser(
'merge', 'merge',
help='Merge NIfTI-MRS along higher dimensions.') help='Merge NIfTI-MRS along higher dimensions.')
mergeparser.add_argument('--files', type=Path, required=True, nargs='+', merge_req = mergeparser.add_argument_group('required arguments')
merge_req.add_argument('--files', type=Path, nargs='+', required=True,
help='List of files to merge') help='List of files to merge')
mergeparser.add_argument('--dim', type=str, required=True, merge_req.add_argument('--dim', type=str, required=True,
help='NIFTI-MRS dimension tag to merge across.') help='NIFTI-MRS dimension tag to merge across.')
mergeparser.add_argument('--output', mergeparser.add_argument('--output', type=Path, default=Path('.'),
required=True, type=Path, default=Path('.'),
help='output folder (defaults to current directory)') help='output folder (defaults to current directory)')
mergeparser.add_argument('--filename', type=str, mergeparser.add_argument('--filename', type=str,
help='Override output file name.') help='Override output file name.')
...@@ -70,18 +72,19 @@ def main(): ...@@ -70,18 +72,19 @@ def main():
splitparser = sp.add_parser( splitparser = sp.add_parser(
'split', 'split',
help='Split NIfTI-MRS along higher dimensions.') help='Split NIfTI-MRS along higher dimensions.')
splitparser.add_argument('--file', type=Path, required=True, split_req = splitparser.add_argument_group('required arguments')
split_req.add_argument('--file', type=Path, required=True,
help='File to split') help='File to split')
splitparser.add_argument('--dim', type=str, required=True, split_req.add_argument('--dim', type=str, required=True,
help='NIFTI-MRS dimension tag to split across.') help='NIFTI-MRS dimension tag to split across.')
group = splitparser.add_mutually_exclusive_group(required=True) group = splitparser.add_mutually_exclusive_group(required=True)
group.add_argument('--indices', type=int, nargs='+', group.add_argument('--indices', type=int, nargs='+',
help='List of indices to extract into second file.' help='List of indices to extract into second file.'
'All indices are zero-indexed.') ' All indices are zero-indexed.')
group.add_argument('--index', type=int, group.add_argument('--index', type=int,
help='Index to split at (split after index, zero-indexed).') help='Index to split at (split after index, zero-indexed).')
splitparser.add_argument('--output', splitparser.add_argument('--output',
required=True, type=Path, default=Path('.'), required=False, type=Path, default=Path('.'),
help='output folder (defaults to current directory)') help='output folder (defaults to current directory)')
splitparser.add_argument('--filename', type=str, splitparser.add_argument('--filename', type=str,
help='Override output file names.') help='Override output file names.')
...@@ -91,14 +94,15 @@ def main(): ...@@ -91,14 +94,15 @@ def main():
reorderparser = sp.add_parser( reorderparser = sp.add_parser(
'reorder', 'reorder',
help='Reorder higher dimensions of NIfTI-MRS.') help='Reorder higher dimensions of NIfTI-MRS.')
reorderparser.add_argument('--file', type=Path, required=True, reord_req = reorderparser.add_argument_group('required arguments')
reord_req.add_argument('--file', type=Path, required=True,
help='File to reorder') help='File to reorder')
reorderparser.add_argument('--dim_order', type=str, nargs='+', required=True, reord_req.add_argument('--dim_order', type=str, nargs='+', required=True,
help='NIFTI-MRS dimension tags in desired order. ' help='NIFTI-MRS dimension tags in desired order. '
'Enter as strings (min:1, max:3). ' 'Enter as strings (min:1, max:3). '
'Can create singleton dimension at end.') 'Can create singleton dimension at end.')
reorderparser.add_argument('--output', reorderparser.add_argument('--output',
required=True, type=Path, default=Path('.'), required=False, type=Path, default=Path('.'),
help='output folder (defaults to current directory)') help='output folder (defaults to current directory)')
reorderparser.add_argument('--filename', type=str, reorderparser.add_argument('--filename', type=str,
help='Override output file names.') help='Override output file names.')
...@@ -108,10 +112,11 @@ def main(): ...@@ -108,10 +112,11 @@ def main():
conjparser = sp.add_parser( conjparser = sp.add_parser(
'conjugate', 'conjugate',
help='Conjugate data to correct phase/frequency convention in a NIfTI-MRS file.') help='Conjugate data to correct phase/frequency convention in a NIfTI-MRS file.')
conjparser.add_argument('--file', type=Path, required=True, conj_req = conjparser.add_argument_group('required arguments')
conj_req.add_argument('--file', type=Path, required=True,
help='File to conjugate') help='File to conjugate')
conjparser.add_argument('--output', conjparser.add_argument('--output',
required=True, type=Path, default=Path('.'), required=False, type=Path, default=Path('.'),
help='output folder (defaults to current directory)') help='output folder (defaults to current directory)')
conjparser.add_argument('--filename', type=str, conjparser.add_argument('--filename', type=str,
help='Override output file names.') help='Override output file names.')
...@@ -156,42 +161,18 @@ def vis(args): ...@@ -156,42 +161,18 @@ def vis(args):
""" """
from fsl_mrs.utils.plotting import plot_spectrum, plot_spectra from fsl_mrs.utils.plotting import plot_spectrum, plot_spectra
from fsl_mrs.utils.mrs_io import read_FID, read_basis from fsl_mrs.utils.mrs_io import read_FID, read_basis
from fsl_mrs.utils.mrs_io.main import FileNotRecognisedError
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import numpy as np import numpy as np
from fsl_mrs.utils.preproc import nifti_mrs_proc from fsl_mrs.utils.preproc import nifti_mrs_proc
import nibabel as nib import nibabel as nib
# 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)
fig = 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).')
# Single nifti file # Single nifti file
elif p.is_file(): def vis_nifti_mrs(file):
data = read_FID(args.file) data = read_FID(file)
if data.ndim > 4 and 'DIM_COIL' in data.dim_tags: if data.ndim > 4 \
and 'DIM_COIL' in data.dim_tags\
and args.display_dim != 'DIM_COIL':
print('Performing coil combination') print('Performing coil combination')
data = nifti_mrs_proc.coilcombine(data) data = nifti_mrs_proc.coilcombine(data)
...@@ -203,7 +184,7 @@ def vis(args): ...@@ -203,7 +184,7 @@ def vis(args):
print(f'Averaging {data.dim_tags[idx]}') print(f'Averaging {data.dim_tags[idx]}')
data = nifti_mrs_proc.average(data, data.dim_tags[idx]) data = nifti_mrs_proc.average(data, data.dim_tags[idx])
fig = plot_spectra(data.mrs(), ppmlim=args.ppmlim) fig = plot_spectra(data.mrs(), ppmlim=args.ppmlim, plot_avg=args.no_mean)
else: else:
while data.ndim > 4: while data.ndim > 4:
print(f'Averaging {data.dim_tags[0]}') print(f'Averaging {data.dim_tags[0]}')
...@@ -228,6 +209,45 @@ def vis(args): ...@@ -228,6 +209,45 @@ def vis(args):
mrsi.set_mask(mask) mrsi.set_mask(mask)
mrsi.plot() mrsi.plot()
# 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
def merge(args): def merge(args):
"""Merges one or more NIfTI-MRS files along a specified dimension """Merges one or more NIfTI-MRS files along a specified dimension
......
...@@ -14,6 +14,7 @@ testsPath = Path(__file__).parent ...@@ -14,6 +14,7 @@ testsPath = Path(__file__).parent
# Testing vis option # Testing vis option
svs = testsPath / 'testdata/fsl_mrs/metab.nii.gz' svs = testsPath / 'testdata/fsl_mrs/metab.nii.gz'
svs_raw = testsPath / 'testdata/fsl_mrs_preproc/metab_raw.nii.gz'
basis = testsPath / 'testdata/fsl_mrs/steam_basis' basis = testsPath / 'testdata/fsl_mrs/steam_basis'
...@@ -25,6 +26,30 @@ def test_vis_svs(tmp_path): ...@@ -25,6 +26,30 @@ def test_vis_svs(tmp_path):
assert (tmp_path / 'svs.png').exists() assert (tmp_path / 'svs.png').exists()
subprocess.check_call(['mrs_tools', 'vis',
'--ppmlim', '0.2', '4.2',
'--save', str(tmp_path / 'svs2.png'),
svs.with_suffix('').with_suffix('')])
assert (tmp_path / 'svs2.png').exists()
subprocess.check_call(['mrs_tools', 'vis',
'--ppmlim', '0.2', '4.2',
'--display_dim', 'DIM_DYN',
'--save', str(tmp_path / 'svs3.png'),
svs_raw])
assert (tmp_path / 'svs3.png').exists()
subprocess.check_call(['mrs_tools', 'vis',
'--ppmlim', '0.2', '4.2',
'--display_dim', 'DIM_COIL',
'--no_mean',
'--save', str(tmp_path / 'svs4.png'),
svs_raw])
assert (tmp_path / 'svs4.png').exists()
def test_vis_basis(tmp_path): def test_vis_basis(tmp_path):
subprocess.check_call(['mrs_tools', 'vis', subprocess.check_call(['mrs_tools', 'vis',
......
...@@ -44,7 +44,7 @@ def _check_datatype(filename): ...@@ -44,7 +44,7 @@ def _check_datatype(filename):
identify the file type (.nii(.gz),.RAW/.H2O,.txt) identify the file type (.nii(.gz),.RAW/.H2O,.txt)
Returns one of: 'NIFTI', 'RAW', 'TXT', 'Unknown' Returns one of: 'NIFTI', 'RAW', 'TXT', 'Unknown'
""" """
_, ext = filename.split(os.extsep, 1) ext = filename.split(os.extsep, 1)[-1]
if 'nii' in ext.lower() or 'nii.gz' in ext.lower(): if 'nii' in ext.lower() or 'nii.gz' in ext.lower():
return 'NIFTI' return 'NIFTI'
elif ext.lower() == 'raw' or ext.lower() == 'h2o': elif ext.lower() == 'raw' or ext.lower() == 'h2o':
......
...@@ -553,8 +553,10 @@ def plotly_fit(mrs, res, ppmlim=(.2, 4.2), proj='real', metabs=None, phs=(0, 0)) ...@@ -553,8 +553,10 @@ def plotly_fit(mrs, res, ppmlim=(.2, 4.2), proj='real', metabs=None, phs=(0, 0))
df['Metab'] = res.metabs df['Metab'] = res.metabs
if res.concScalings['molality'] is not None: if res.concScalings['molality'] is not None:
df['mMol/kg'] = np.round(res.getConc(scaling='molality'), decimals=2) df['mMol/kg'] = np.round(res.getConc(scaling='molality'), decimals=2)
df['CRLB'] = np.round(res.getUncertainties(type='molality'), decimals=2)
else: else:
df['unscaled'] = np.round(res.getConc(), decimals=2) df['unscaled'] = np.round(res.getConc(), decimals=2)
df['CRLB'] = np.round(res.getUncertainties(type='raw'), decimals=3)
df['%CRLB'] = np.round(res.getUncertainties(), decimals=1) df['%CRLB'] = np.round(res.getUncertainties(), decimals=1)
if res.concScalings['internal'] is not None: if res.concScalings['internal'] is not None:
concstr = f'/{res.concScalings["internalRef"]}' concstr = f'/{res.concScalings["internalRef"]}'
......
...@@ -54,7 +54,8 @@ def plotAxesStyle(fig, ppmlim, title=None): ...@@ -54,7 +54,8 @@ def plotAxesStyle(fig, ppmlim, title=None):
range=[ppmlim[1], ppmlim[0]]) range=[ppmlim[1], ppmlim[0]])
else: else:
fig.layout.xaxis.update(title_text='Chemical shift (ppm)', fig.layout.xaxis.update(title_text='Chemical shift (ppm)',
tick0=2, dtick=.5) tick0=2, dtick=.5,
autorange="reversed")
fig.layout.yaxis.update(zeroline=True, fig.layout.yaxis.update(zeroline=True,
zerolinewidth=1, zerolinewidth=1,
zerolinecolor='Gray', zerolinecolor='Gray',
......
...@@ -312,12 +312,16 @@ class FitRes(object): ...@@ -312,12 +312,16 @@ class FitRes(object):
if self.concScalings['internal'] is not None: if self.concScalings['internal'] is not None:
concstr = f'/{self.concScalings["internalRef"]}' concstr = f'/{self.concScalings["internalRef"]}'
df[concstr] = self.getConc(scaling='internal') df[concstr] = self.getConc(scaling='internal')
df[concstr + ' CRLB'] = self.getUncertainties(type='internal')
else: else:
df['Raw conc'] = self.getConc() df['Raw conc'] = self.getConc()
df['Raw CRLB'] = self.getUncertainties(type='raw')
if self.concScalings['molality'] is not None: if self.concScalings['molality'] is not None:
df['mMol/kg'] = self.getConc(scaling='molality') df['mMol/kg'] = self.getConc(scaling='molality')
df['mMol/kg CRLB'] = self.getUncertainties(type='molality')
if self.concScalings['molarity'] is not None: if self.concScalings['molarity'] is not None:
df['mM'] = self.getConc(scaling='molarity') df['mM'] = self.getConc(scaling='molarity')
df['mM CRLB'] = self.getUncertainties(type='molarity')
df['%CRLB'] = self.getUncertainties() df['%CRLB'] = self.getUncertainties()
SNR = self.getQCParams()[0] SNR = self.getQCParams()[0]
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment