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.
......@@ -5,7 +5,7 @@
# Author: Saad Jbabdi <saad@fmrib.ox.ac.uk>
# Will Clarke <william.clarke@ndcn.ox.ac.uk>
#
# Copyright (C) 2020 University of Oxford
# Copyright (C) 2020 University of Oxford
# SHBASECOPYRIGHT
import numpy as np
......@@ -13,42 +13,49 @@ 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
self.H2O = H2O
self.mask = mask
self.data = FID
self.H2O = H2O
self.mask = mask
self.header = header
# Basis
self.basis = basis
self.names = names
self.basis_hdr = basis_hdr
# Basis
self.basis = basis
self.names = names
self.basis_hdr = basis_hdr
# tissue segmentation
self.csf = None
self.wm = None
self.gm = None
self.tissue_seg_loaded = False
self.csf = None
self.wm = None
self.gm = None
self.tissue_seg_loaded = False
# Helpful properties
self.spatial_shape = self.data.shape[:3]
......@@ -69,31 +76,33 @@ class MRSI(object):
self.ind_scaling = None
self._store_scalings = None
def __iter__(self):
shape = self.data.shape
self._store_scalings = []
for idx in np.ndindex(shape[:3]):
if self.mask[idx]:
mrs_out = MRS(FID=self.data[idx],
header=self.header,
basis=self.basis,
names=self.names,
basis_hdr=self.basis_hdr,
H2O=self.H2O[idx])
header=self.header,
basis=self.basis,
names=self.names,
basis_hdr=self.basis_hdr,
H2O=self.H2O[idx])
self._process_mrs(mrs_out)
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
def get_indicies_in_order(self,mask=True):
"""Return a list of iteration indicies in order"""
yield mrs_out, idx, tissue_seg
def get_indicies_in_order(self, mask=True):
"""Return a list of iteration indicies in order"""
out = []
shape = self.data.shape
for idx in np.ndindex(shape[:3]):
......@@ -104,30 +113,34 @@ class MRSI(object):
out.append(idx)
return out
def get_scalings_in_order(self,mask=True):
"""Return a list of MRS object scalings in order"""
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],:],
header=self.header,
basis=self.basis,
names=self.names,
basis_hdr=self.basis_hdr,
H2O=self.H2O[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], :])
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,
basis=self.basis,
......@@ -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()
......@@ -153,7 +171,7 @@ class MRSI(object):
pass
else:
mrs.check_Basis(repair=True)
mrs.keep(self.keep)
mrs.ignore(self.ignore)
......@@ -165,124 +183,147 @@ class MRSI(object):
mrs.check_FID(repair=True)
if self.rescale:
mrs.rescaleForFitting(ind_scaling=self.ind_scaling)
def plot(self,mask=True,ppmlim=(0.2,4.2)):
mrs.rescaleForFitting(ind_scaling=self.ind_scaling)
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.show()
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)
nt = data_list[0].size
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)
else:
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):