Commit 75fc2ef4 authored by William Clarke's avatar William Clarke
Browse files

Merge branch 'master' into 'master'

Master

See merge request !14
parents 69b1442e 086ab398
Pipeline #6972 passed with stages
in 5 minutes and 8 seconds
###########################################################################
# This file defines the build process for fsl-mrs, as hosted at:
#
# https://git.fmrib.ox.ac.uk/fsl/fsl_mrs
#
# The build pipeline currently comprises the following stages:
#
# 1. style: Check coding style - allowed to fail
#
# 2. test: Unit tests
#
# 3. doc: Building user documentation which appears at:
# https://open.win.ox.ac.uk/pages/fsl/fsl_mrs/
#
# 4. build
# & deploy: Build in three stages fsl-ci-pre, fsl-ci-build,
# sl-ci-deploy
#
# A custom docker image is used for the test job - images are
# available at:
#
# https://hub.docker.com/u/wtclarke/
#
# Stage run conditions:
# Style is run in all cases, but allowed to fail.
# Test is run in all cases.
# Doc is only run on master branches.
# Build stages are run according to the rules associated
# with https://git.fmrib.ox.ac.uk/fsl/fsl-ci-rules
#
###########################################################################
include:
- project: fsl/fsl-ci-rules
file: .gitlab-ci.yml
stages:
- Static Analysis
- Test
- style
- test
- doc
- fsl-ci-pre
- fsl-ci-build
- fsl-ci-deploy
####################################
# These anchors are used to restrict
# when and where jobs are executed.
####################################
.only_upstream: &only_upstream
only:
- branches@fsl/fsl_mrs
.only_master: &only_master
only:
- master
.only_releases: &only_releases
only:
- tags@fsl/fsl_mrs
.except_releases: &except_releases
except:
- tags
# ############
# # 1. style
# ############
flake8:
image: python:3.7-slim-buster
stage: Static Analysis
stage: style
before_script:
- python --version
- pip install flake8
script:
- flake8 --max-line-length=120 fsl_mrs
- flake8 fsl_mrs
allow_failure: true
############
# 2. test
############
pytest:
image: wtclarke/fsl_mrs_tests:1.0
stage: Test
stage: test
variables:
GIT_SUBMODULE_STRATEGY: normal
before_script:
......@@ -36,14 +100,18 @@ pytest:
script:
- pytest fsl_mrs/tests
############
# 3. doc
############
pages:
<<: *only_master
image: python:3.7
stage: doc
script:
- pip install -U sphinx sphinx_rtd_theme
- pip install .
- sphinx-build -b html ./docs/user_docs public
artifacts:
paths:
- public
rules:
- if: '$CI_COMMIT_BRANCH == "master"'
This document contains the FSL-MRS release history in reverse chronological order.
1.0.6 (Tuesday 12th January 2021)
---------------------------------
- Internal changes to core MRS class.
- New plotting functions added, utility functions for plotting added to MRS class.
- fsl_mrs/aux folder renamed for Windows compatibility.
- Moved online documentation to open.win.ox.ac.uk/pages/fsl/fsl_mrs/.
- Fixed small bugs in preprocessing display.
- Synthetic spectra now use fitting model directly.
- Bug fixes in the fsl_Mrs commandline interface. Thanks to Alex Craig-Craven.
- WIP: Dynamic fitting model and dynamic experiment simulation.
- spec2nii requirement pinned to 0.2.11 during NIFTI-MRS development.
1.0.5 (Friday 9th October 2020)
-------------------------------
- Extended documentation of hardcoded constants, including MCMC priors.
......
......@@ -9,7 +9,7 @@ FSL-MRS is a collection of python modules and wrapper scripts for pre-processing
### Installation
#### Conda package
The primary installation method is via _conda_. After installing conda and creating or activating a suitable [enviroment](https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html) you can install FSL-MRS from the FSL conda channel.
The primary installation method is via _conda_. After installing conda and creating or activating a suitable [environment](https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html) you can install FSL-MRS from the FSL conda channel.
conda install -c conda-forge \
-c https://fsl.fmrib.ox.ac.uk/fsldownloads/fslconda/channel/ \
......@@ -32,7 +32,7 @@ or
pip install spec2nii
After installation see the [quick start guide](https://users.fmrib.ox.ac.uk/~saad/fsl_mrs/html/quick_start.html).
After installation see the [quick start guide](https://open.win.ox.ac.uk/pages/fsl/fsl_mrs/quick_start.html).
---
......@@ -59,7 +59,7 @@ After installation see the [quick start guide](https://users.fmrib.ox.ac.uk/~saa
### Documentation
Documentation can be found online [here](https://users.fmrib.ox.ac.uk/~saad/fsl_mrs/html/index.html).
Documentation can be found online on the [WIN open science website](https://open.win.ox.ac.uk/pages/fsl/fsl_mrs/).
For each of the wrapper scripts above, simply type `<name_of_script> --help` to get the usage.
......
import datetime
import fsl_mrs
date = datetime.date.today()
# Configuration file for the Sphinx documentation builder.
......@@ -25,7 +26,7 @@ copyright = f'{date.year}, Will Clarke & Saad Jbabdi, University of Oxford, Oxfo
author = 'William Clarke'
# The full version, including alpha/beta/rc tags
version = '1.0.5'
version = fsl_mrs.__version__
release = version
# From PM's fsleyes doc
......
......@@ -30,7 +30,7 @@ By default this option will add the following basis spectra (in separate metabol
Additional peaks may be added int he interactive environment by calling :code:`add_MM_peaks` with optional arguments to override the defaults.
References
==========
~~~~~~~~~~
.. [CUDA12] Cudalbu C, Mlynárik V, Gruetter R. Handling Macromolecule Signals in the Quantification of the Neurochemical Profile. Journal of Alzheimer’s Disease 2012;31:S101–S115 doi: 10.3233/JAD-2012-120100.
......
This diff is collapsed.
......@@ -13,25 +13,32 @@ from fsl_mrs.core import MRS
from fsl_mrs.utils import mrs_io, misc
import matplotlib.pyplot as plt
import nibabel as nib
from fsl_mrs.utils.mrs_io.fsl_io import saveNIFTI
from fsl_mrs.utils.mrs_io.fsl_io import saveNIFTI, readNIFTI
class MRSI(object):
def __init__(self,FID,header,mask=None,basis=None,names=None,basis_hdr=None,H2O=None):
def __init__(self, FID, header,
mask=None, basis=None, names=None,
basis_hdr=None, H2O=None):
# process mask
if mask is None:
mask = np.full(FID.shape,True)
elif mask.shape[0:3]==FID.shape[0:3]:
mask = mask!=0.0
mask = np.full(FID.shape, True)
elif mask.shape[0:3] == FID.shape[0:3]:
mask = mask != 0.0
else:
raise ValueError(f'Mask must be None or numpy array of the same shape as FID. Mask {mask.shape[0:3]}, FID {FID.shape[0:3]}.')
raise ValueError(f'Mask must be None or numpy'
f' array of the same shape'
f' as FID. Mask {mask.shape[0:3]},'
f' FID {FID.shape[0:3]}.')
# process H2O
if H2O is None:
H2O = np.full(FID.shape,None)
elif H2O.shape!=FID.shape:
raise ValueError('H2O must be None or numpy array of the same shape as FID.')
H2O = np.full(FID.shape, None)
elif H2O.shape != FID.shape:
raise ValueError('H2O must be None or numpy array '
'of the same shape as FID.')
# Load into properties
self.data = FID
......@@ -86,13 +93,15 @@ class MRSI(object):
self._store_scalings.append(mrs_out.scaling)
if self.tissue_seg_loaded:
tissue_seg = {'CSF':self.csf[idx],'WM':self.wm[idx],'GM':self.gm[idx]}
tissue_seg = {'CSF': self.csf[idx],
'WM': self.wm[idx],
'GM': self.gm[idx]}
else:
tissue_seg = None
yield mrs_out,idx,tissue_seg
yield mrs_out, idx, tissue_seg
def get_indicies_in_order(self,mask=True):
def get_indicies_in_order(self, mask=True):
"""Return a list of iteration indicies in order"""
out = []
shape = self.data.shape
......@@ -104,29 +113,33 @@ class MRSI(object):
out.append(idx)
return out
def get_scalings_in_order(self,mask=True):
def get_scalings_in_order(self, mask=True):
"""Return a list of MRS object scalings in order"""
if self._store_scalings is None:
raise ValueError('Fetch mrs by iterable first.')
else:
return self._store_scalings
def mrs_by_index(self,index):
mrs_out = MRS(FID=self.data[index[0],index[1],index[2],:],
def mrs_by_index(self, index):
''' Return MRS object by index (tuple - x,y,z).'''
mrs_out = MRS(FID=self.data[index[0], index[1], index[2], :],
header=self.header,
basis=self.basis,
names=self.names,
basis_hdr=self.basis_hdr,
H2O=self.H2O[index[0],index[1],index[2],:])
H2O=self.H2O[index[0], index[1], index[2], :])
self._process_mrs(mrs_out)
return mrs_out
def mrs_from_average(self):
FID = misc.volume_to_list(self.data,self.mask)
H2O = misc.volume_to_list(self.H2O,self.mask)
FID = sum(FID)/len(FID)
H2O = sum(H2O)/len(H2O)
'''
Return average of all masked voxels
as a single MRS object.
'''
FID = misc.volume_to_list(self.data, self.mask)
H2O = misc.volume_to_list(self.H2O, self.mask)
FID = sum(FID) / len(FID)
H2O = sum(H2O) / len(H2O)
mrs_out = MRS(FID=FID,
header=self.header,
......@@ -137,15 +150,20 @@ class MRSI(object):
self._process_mrs(mrs_out)
return mrs_out
def seg_by_index(self,index):
def seg_by_index(self, index):
'''Return segmentation information by index.'''
if self.tissue_seg_loaded:
return {'CSF':self.csf[index],'WM':self.wm[index],'GM':self.gm[index]}
return {'CSF': self.csf[index],
'WM': self.wm[index],
'GM': self.gm[index]}
else:
raise ValueError('Load tissue segmentation first.')
def _process_mrs(self,mrs):
def _process_mrs(self, mrs):
''' Process (conjugate, rescale)
basis and FID and apply basis operations
to all voxels.
'''
if self.basis is not None:
if self.conj_basis:
mrs.conj_Basis()
......@@ -167,122 +185,145 @@ class MRSI(object):
if self.rescale:
mrs.rescaleForFitting(ind_scaling=self.ind_scaling)
def plot(self,mask=True,ppmlim=(0.2,4.2)):
def plot(self, mask=True, ppmlim=(0.2, 4.2)):
'''Plot (masked) grid of spectra.'''
if mask:
mask_indicies = np.where(self.mask)
else:
mask_indicies = np.where(np.full(self.mask.shape,True))
dim1 = np.asarray((np.min(mask_indicies[0]),np.max(mask_indicies[0])))
dim2 = np.asarray((np.min(mask_indicies[1]),np.max(mask_indicies[1])))
dim3 = np.asarray((np.min(mask_indicies[2]),np.max(mask_indicies[2])))
mask_indicies = np.where(np.full(self.mask.shape, True))
dim1 = np.asarray((np.min(mask_indicies[0]), np.max(mask_indicies[0])))
dim2 = np.asarray((np.min(mask_indicies[1]), np.max(mask_indicies[1])))
dim3 = np.asarray((np.min(mask_indicies[2]), np.max(mask_indicies[2])))
size1 = 1+ dim1[1]-dim1[0]
size2 = 1+ dim2[1]-dim2[0]
size3 = 1+ dim3[1]-dim3[0]
size1 = 1 + dim1[1] - dim1[0]
size2 = 1 + dim2[1] - dim2[0]
size3 = 1 + dim3[1] - dim3[0]
ar1 = size1/(size1+size2)
ar2 = size2/(size1+size2)
ar1 = size1 / (size1 + size2)
ar2 = size2 / (size1 + size2)
for sDx in range(size3):
fig,axes = plt.subplots(size1,size2,figsize=(20*ar2,20*ar1))
for i,j,k in zip(*mask_indicies):
if (not self.mask[i,j,k]) and mask:
fig, axes = plt.subplots(size1, size2, figsize=(20 * ar2, 20 * ar1))
for i, j, k in zip(*mask_indicies):
if (not self.mask[i, j, k]) and mask:
continue
ii = i - dim1[0]
jj = j - dim2[0]
ax = axes[ii,jj]
mrs = self.mrs_by_index([i,j,k])
ax.plot(mrs.getAxes(ppmlim=ppmlim),np.real(mrs.getSpectrum(ppmlim=ppmlim)))
ax = axes[ii, jj]
mrs = self.mrs_by_index([i, j, k])
ax.plot(mrs.getAxes(ppmlim=ppmlim), np.real(mrs.get_spec(ppmlim=ppmlim)))
ax.invert_xaxis()
ax.set_xticks([])
ax.set_yticks([])
plt.subplots_adjust(left = 0.03, # the left side of the subplots of the figure
right = 0.97, # the right side of the subplots of the figure
bottom = 0.01, # the bottom of the subplots of the figure
top = 0.95, # the top of the subplots of the figure
wspace = 0, # the amount of width reserved for space between subplots,
hspace = 0)
fig.suptitle(f'Slice {k}')
plt.subplots_adjust(left=0.03, # the left side of the subplots of the figure
right=0.97, # the right side of the subplots of the figure
bottom=0.01, # the bottom of the subplots of the figure
top=0.95, # the top of the subplots of the figure
wspace=0, # the amount of width reserved for space between subplots,
hspace=0)
fig.suptitle(f'Slice {sDx}')
plt.show()
def __str__(self):
return f'MRSI with shape {self.data.shape}\nNumber of voxels = {self.num_voxels}\nNumber of masked voxels = {self.num_masked_voxels}'
return f'MRSI with shape {self.data.shape}\n' \
f'Number of voxels = {self.num_voxels}\n' \
f'Number of masked voxels = {self.num_masked_voxels}'
def __repr__(self):
return str(self)
def set_mask(self,mask):
def set_mask(self, mask):
""" Load mask as numpy array."""
if mask is None:
mask = np.full(self.data.shape,True)
elif mask.shape[0:3]==self.data.shape[0:3]:
mask = mask!=0.0
mask = np.full(self.data.shape, True)
elif mask.shape[0:3] == self.data.shape[0:3]:
mask = mask != 0.0
else:
raise ValueError(f'Mask must be None or numpy array of the same shape as FID. Mask {mask.shape[0:3]}, FID {self.data.shape[0:3]}.')
raise ValueError(f'Mask must be None or numpy array of the same shape as FID.'
f' Mask {mask.shape[0:3]}, FID {self.data.shape[0:3]}.')
self.mask = mask
self.num_masked_voxels = np.sum(self.mask)
def set_tissue_seg(self,csf,wm,gm):
def set_tissue_seg(self, csf, wm, gm):
""" Load tissue segmentation as numpy arrays."""
if (csf.shape != self.spatial_shape) or (wm.shape != self.spatial_shape) or (gm.shape != self.spatial_shape):
raise ValueError(f'Tissue segmentation arrays have wrong shape (CSF:{csf.shape}, GM:{gm.shape}, WM:{wm.shape}). Must match FID ({self.spatial_shape}).')
raise ValueError(f'Tissue segmentation arrays have wrong shape '
f'(CSF:{csf.shape}, GM:{gm.shape}, WM:{wm.shape}).'
f' Must match FID ({self.spatial_shape}).')
self.csf = csf
self.wm = wm
self.gm = gm
self.tissue_seg_loaded = True
def write_output(self,data_list,file_path_name,indicies=None,cleanup=True,dtype=float):
if indicies==None:
def write_output(self, data_list, file_path_name, indicies=None, cleanup=True, dtype=float):
'''Write 3D or 4D array of data to nifti file with current orientation.'''
if indicies is None:
indicies = self.get_indicies_in_order()
nt = data_list[0].size
if nt>1:
data = np.zeros(self.spatial_shape+(nt,),dtype=dtype)
if nt > 1:
data = np.zeros(self.spatial_shape + (nt,), dtype=dtype)
else:
data = np.zeros(self.spatial_shape,dtype=dtype)
data = np.zeros(self.spatial_shape, dtype=dtype)
for d,ind in zip(data_list,indicies):
for d, ind in zip(data_list, indicies):
data[ind] = d
if cleanup:
data[np.isnan(data)] = 0
data[np.isinf(data)] = 0
data[data<1e-10] = 0
data[data>1e10] = 0
data[data < 1e-10] = 0
data[data > 1e10] = 0
if nt == self.FID_points:
saveNIFTI(file_path_name, data, self.header)
else:
img = nib.Nifti1Image(data,self.header['nifti'].affine)
img = nib.Nifti1Image(data, self.header['nifti'].affine)
nib.save(img, file_path_name)
@classmethod
def from_files(cls,data_file,mask_file=None,basis_file=None,H2O_file=None,csf_file=None,gm_file=None,wm_file=None):
data,hdr = mrs_io.read_FID(data_file)
def from_files(cls, data_file,
mask_file=None,
basis_file=None,
H2O_file=None,
csf_file=None,
gm_file=None,
wm_file=None):
""" Load MRSI data directly from files """
data, hdr = mrs_io.read_FID(data_file)
if mask_file is not None:
mask,_ = mrs_io.fsl_io.readNIFTI(mask_file)
mask, _ = readNIFTI(mask_file)
else:
mask = None
if basis_file is not None:
basis,names,basisHdr = mrs_io.read_basis(basis_file)
basis, names, basisHdr = mrs_io.read_basis(basis_file)
else:
basis,names,basisHdr = None,None,[None,]
basis, names, basisHdr = None, None, [None, ]
if H2O_file is not None:
data_w,hdr_w = mrs_io.read_FID(H2O_file)
data_w, hdr_w = mrs_io.read_FID(H2O_file)
else:
data_w = None
out = cls(data,hdr,mask=mask,basis=basis,names=names,basis_hdr=basisHdr[0],H2O=data_w)
out = cls(data, hdr,
mask=mask,
basis=basis,
names=names,
basis_hdr=basisHdr[0],
H2O=data_w)
def loadNii(f):
nii = np.asanyarray(nib.load(f).dataobj)
if nii.ndim == 2:
nii = np.expand_dims(nii, 2)
return nii
if (csf_file is not None) and (gm_file is not None) and (wm_file is not None):
csf,_ = mrs_io.fsl_io.readNIFTI(csf_file)
gm,_ = mrs_io.fsl_io.readNIFTI(gm_file)
wm,_ = mrs_io.fsl_io.readNIFTI(wm_file)
out.set_tissue_seg(csf,wm,gm)
csf = loadNii(csf_file)
gm = loadNii(gm_file)
wm = loadNii(wm_file)
out.set_tissue_seg(csf, wm, gm)
return out
......@@ -9,7 +9,7 @@
# SHBASECOPYRIGHT
# Quick imports
from fsl_mrs.aux import configargparse
from fsl_mrs.auxiliary import configargparse
from fsl_mrs import __version__
from fsl_mrs.utils.splash import splash
......@@ -99,6 +99,8 @@ def main():
nargs='+',
help='Metabolite(s) used as an internal reference.'
' Defaults to tCr (Cr+PCr).')
optional.add_argument('--h2o_scale', type=float, default=1.0,
help='Additional scaling modifier for external water referencing.')
optional.add_argument('--central_frequency', default=None, type=float,
help='central frequency in Hz')
optional.add_argument('--dwell_time', default=None, type=float,
......@@ -204,7 +206,7 @@ def main():
'Please either set it or include it in data header'))
if args.dwell_time is not None:
bw = 1/args.dwell_time
bw = 1 / args.dwell_time
elif dataheader['bandwidth'] is not None:
bw = dataheader['bandwidth']
if args.verbose:
......@@ -216,7 +218,7 @@ def main():
# Fix case where basis file contains no header info (e.g. .RAW)
if basisheader is None:
basisheader = {'bandwidth': bw,
'dwelltime': 1/bw,
'dwelltime': 1 / bw,
'centralFrequency': cf}
else:
basisheader = basisheader[0]
......@@ -257,7 +259,6 @@ def main():
if args.verbose:
print('--->> Phase correction\n')
mrs.FID = misc.phase_correct(mrs, mrs.FID)
mrs.Spec = misc.FIDToSpec(mrs.FID)
# Keep/Ignore metabolites
mrs.keep(args.keep)
......@@ -281,7 +282,7 @@ def main():
if not args.verbose:
print('Adding macromolecules')
nMM = mrs.add_MM_peaks(gamma=10, sigma=20)
G = [i+max(metab_groups)+1 for i in range(nMM)]
G = [i + max(metab_groups) + 1 for i in range(nMM)]
metab_groups += G
# Choose fitting lineshape model.
......@@ -310,7 +311,7 @@ def main():
# Quantification
# Echo time
if args.TE is not None:
echotime = args.TE*1E-3
echotime = args.TE * 1E-3
elif 'meta' in basisheader:
if 'TE' in basisheader['meta']:
echotime = basisheader['meta']['TE']
......@@ -330,20 +331,23 @@ def main():
' no absolute quantification will be performed.',
UserWarning)
res.calculateConcScaling(mrs, referenceMetab=args.internal_ref)
elif args.tissue_frac is None:
elif args.tissue_frac is not None:
res.calculateConcScaling(mrs,
referenceMetab=args.internal_ref,
waterRefFID=mrs.H2O,
tissueFractions=None,
tissueFractions=args.tissue_frac,
TE=echotime,
verbose=args.verbose)
verbose=args.verbose,
add_scale=args.h2o_scale)
else:
res.calculateConcScaling(mrs,