Commit 53cfec37 authored by William Clarke's avatar William Clarke
Browse files

Add reshape nifti-mrs tool. Modify storage of dim tags in NIFTI_MRS class.

parent 731c7a45
......@@ -19,6 +19,24 @@ import fsl_mrs.core as core
from fsl_mrs.utils.misc import checkCFUnits
dimension_tags = {"DIM_COIL": "For storage of data from each individual receiver coil element.",
"DIM_DYN": "For storage of each individual acquisition transient."
" E.g. for post-acquisition B0 drift correction.",
"DIM_INDIRECT_0": "The indirect detection dimension - necessary for 2D"
" (and greater) MRS acquisitions.",
"DIM_INDIRECT_1": "The indirect detection dimension - necessary for 2D"
" (and greater) MRS acquisitions.",
"DIM_INDIRECT_2": "The indirect detection dimension - necessary for 2D"
" (and greater) MRS acquisitions.",
"DIM_PHASE_CYCLE": "Used for the time-proportional phase incrementation method.",
"DIM_EDIT": "Used for edited MRS techniques such as MEGA or HERMES.",
"DIM_MEAS": "Used to indicate multiple repeats of the full sequence"
" contained within the same original data file.",
"DIM_USER_0": "User defined dimension.",
"DIM_USER_1": "User defined dimension.",
"DIM_USER_2": "User defined dimension."}
def gen_new_nifti_mrs(data, dwelltime, spec_freq, nucleus='1H', affine=None, dim_tags=[None, None, None]):
'''Generate a NIFTI_MRS object from a np array and header info.
......@@ -107,20 +125,6 @@ class NIFTI_MRS(Image):
except KeyError:
raise NotNIFTI_MRS('NIFTI-MRS header extension must have nucleus and spectrometerFrequency keys.')
# Extract key parameters from the header extension
self._set_dim_tags()
def _set_dim_tags(self):
self.dim_tags = [None, None, None]
std_tags = ['DIM_COIL', 'DIM_DYN', 'DIM_INDIRECT_0']
for idx in range(3):
curr_dim = idx + 5
curr_tag = f'dim_{curr_dim}'
if curr_tag in self.hdr_ext:
self.dim_tags[idx] = self.hdr_ext[curr_tag]
elif curr_dim < self.ndim:
self.dim_tags[idx] = std_tags[idx]
def __getitem__(self, sliceobj):
'''Apply conjugation at use. This swaps from the
NIFTI-MRS and Levvit inspired right-handed reference frame
......@@ -179,7 +183,6 @@ class NIFTI_MRS(Image):
extension = Nifti1Extension(44, json_s.encode('UTF-8'))
self.header.extensions.clear()
self.header.extensions.append(extension)
self._set_dim_tags()
@property
def filename(self):
......@@ -190,6 +193,24 @@ class NIFTI_MRS(Image):
else:
return ''
@property
def dim_tags(self):
"""Return the three higher dimension tags"""
return self._read_dim_tags()
def _read_dim_tags(self):
"""Read dim tags from current header extension"""
dim_tags = [None, None, None]
std_tags = ['DIM_COIL', 'DIM_DYN', 'DIM_INDIRECT_0']
for idx in range(3):
curr_dim = idx + 5
curr_tag = f'dim_{curr_dim}'
if curr_tag in self.hdr_ext:
dim_tags[idx] = self.hdr_ext[curr_tag]
elif curr_dim < self.ndim:
dim_tags[idx] = std_tags[idx]
return dim_tags
def dim_position(self, dim_tag):
'''Return position of dim if it exists.'''
if dim_tag in self.dim_tags:
......@@ -237,6 +258,24 @@ class NIFTI_MRS(Image):
current_hdr_ext.pop(key, None)
self.hdr_ext = current_hdr_ext
def set_dim_tag(self, dim, tag):
"""Set or update the 'dim_N' fields
Tag must be one of the standard-defined tags (e.g. DIM_DYN)
:param dim: The existing dim tag or python dimension index (i.e. N-1)
:type dim: str or int
:param tag: New tag
:type tag: str
"""
if tag not in dimension_tags.keys():
raise ValueError(f'Tag must be one of: {", ".join(list(dimension_tags.keys()))}.')
dim = self._dim_tag_to_index(dim)
current_hdr_ext = self.hdr_ext
current_hdr_ext[f'dim_{dim + 1}'] = tag
self.hdr_ext = current_hdr_ext
def set_dim_info(self, dim, info_str):
"""Set or update the 'dim_N_info' field
......@@ -309,8 +348,6 @@ class NIFTI_MRS(Image):
hdr_ext[f'dim_{dd}_info'] = hdr_ext[f'dim_{dd + 1}_info']
new_obj.hdr_ext = hdr_ext
new_obj._set_dim_tags()
return new_obj
else:
return NIFTI_MRS(self.data, header=self.header)
......
......@@ -177,6 +177,25 @@ def test_add_remove_field():
assert 'RepetitionTime' not in nmrs.hdr_ext
def test_set_dim_tag():
nmrs = NIFTI_MRS(data['unprocessed'])
with pytest.raises(ValueError) as exc_info:
nmrs.set_dim_tag('DIM_DYN', 'DIM_FOO')
assert exc_info.type is ValueError
assert exc_info.value.args[0] == \
'Tag must be one of: DIM_COIL, DIM_DYN, DIM_INDIRECT_0, DIM_INDIRECT_1, DIM_INDIRECT_2,'\
' DIM_PHASE_CYCLE, DIM_EDIT, DIM_MEAS, DIM_USER_0, DIM_USER_1, DIM_USER_2.'
nmrs.set_dim_tag('DIM_DYN', 'DIM_USER_0')
assert nmrs.hdr_ext['dim_6'] == 'DIM_USER_0'
assert nmrs.dim_tags == ['DIM_COIL', 'DIM_USER_0', None]
nmrs.set_dim_tag(4, 'DIM_USER_1')
assert nmrs.hdr_ext['dim_5'] == 'DIM_USER_1'
assert nmrs.dim_tags == ['DIM_USER_1', 'DIM_USER_0', None]
def test_set_dim_info():
nmrs = NIFTI_MRS(data['unprocessed'])
nmrs.set_dim_info('DIM_DYN', 'my info')
......
from pathlib import Path
import pytest
from fsl_mrs.utils import mrs_io
from fsl_mrs.utils import nifti_mrs_tools as nmrs_tools
testsPath = Path(__file__).parent
test_data = testsPath / 'testdata' / 'fsl_mrs_preproc' / 'metab_raw.nii.gz'
def test_reshape():
# Data is (1, 1, 1, 4096, 32, 64) ['DIM_COIL', 'DIM_DYN', None]
nmrs = mrs_io.read_FID(test_data)
new_shape = (16, 2, 64)
with pytest.raises(TypeError) as exc_info:
reshaped = nmrs_tools.reshape(nmrs, new_shape, d6='DIM_USER_0')
assert exc_info.type is TypeError
assert exc_info.value.args[0] == 'An appropriate d7 dim tag must be given as ndim = 7.'
reshaped = nmrs_tools.reshape(nmrs, new_shape, d6='DIM_USER_0', d7='DIM_DYN')
assert reshaped.data.shape == (1, 1, 1, 4096, 16, 2, 64)
assert reshaped.dim_tags == ['DIM_COIL', 'DIM_USER_0', 'DIM_DYN']
new_shape = (16, -1, 64)
reshaped = nmrs_tools.reshape(nmrs, new_shape, d7='DIM_USER_0')
assert reshaped.data.shape == (1, 1, 1, 4096, 16, 2, 64)
assert reshaped.dim_tags == ['DIM_COIL', 'DIM_DYN', 'DIM_USER_0']
from .split_merge import split, merge, reorder
from .reshape import reshape
"""Tools for reshaping the higher dimensions of NIfTI-MRS
Author: Will Clarke <william.clarke@ndcn.ox.ac.uk>
Copyright (C) 2021 University of Oxford
"""
from fsl_mrs.core import NIFTI_MRS
import numpy as np
def reshape(nmrs, reshape, d5=None, d6=None, d7=None):
"""Reshape the higher dimensions (5-7) of an nifti-mrs file.
Uses numpy reshape syntax to reshape. Use -1 for automatic sizing.
If the dimension exists after reshaping a tag is required. If None is passed
but one already exists no change will be made. If no value exists then an
exception will be raised.
:param nmrs: Input NIfTI-MRS file
:type nmrs: NIFTI_MRS
:param reshape: Tuple of target sizes in style of numpy.reshape, higher dimensions only.
:type reshape: tuple
:param d5: Dimension tag to set dim_5, defaults to None
:type d5: str, optional
:param d6: Dimension tag to set dim_6, defaults to None
:type d6: str, optional
:param d7: Dimension tag to set dim_7, defaults to None
:type d7: str, optional
"""
shape = nmrs.data.shape[0:4]
shape += reshape
nmrs_reshaped = NIFTI_MRS(np.reshape(nmrs.data, shape), header=nmrs.header)
# Note numerical index is N-1
if d5:
nmrs_reshaped.set_dim_tag(4, d5)
elif nmrs_reshaped.ndim > 4\
and nmrs_reshaped.dim_tags[0] is None:
raise TypeError(f'An appropriate d5 dim tag must be given as ndim = {nmrs_reshaped.ndim}.')
if d6:
nmrs_reshaped.set_dim_tag(5, d6)
elif nmrs_reshaped.ndim > 5\
and nmrs_reshaped.dim_tags[1] is None:
raise TypeError(f'An appropriate d6 dim tag must be given as ndim = {nmrs_reshaped.ndim}.')
if d7:
nmrs_reshaped.set_dim_tag(6, d7)
elif nmrs_reshaped.ndim > 6\
and nmrs_reshaped.dim_tags[2] is None:
raise TypeError(f'An appropriate d7 dim tag must be given as ndim = {nmrs_reshaped.ndim}.')
return nmrs_reshaped
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