From 232189c4ce02b2f184bd2fdf4353582754763af6 Mon Sep 17 00:00:00 2001 From: William T Clarke Date: Fri, 7 Jul 2023 15:08:44 +0100 Subject: [PATCH] Rel/0.6.9 (#79) * Messy fixes for new nifti-mrs-tools version. Adds handling of GE sequences. * Prepare for release. * new pymapvbvd not yet available --- CHANGELOG.md | 7 +++ requirements.yml | 2 +- setup.py | 9 +--- spec2nii/GE/ge_pfile.py | 62 ++++++++++++++++++++++++--- spec2nii/GE/ge_read_pfile.py | 5 ++- spec2nii/Philips/philips_data_list.py | 17 +++++++- spec2nii/other.py | 2 +- 7 files changed, 86 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea1f69e..b129e9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ This document contains the Spec2nii release history in reverse chronological order. +0.6.9 (Friday 7th July 2023) +---------------------------- +- Add handling for the `fidall` sequence in GE. +- Add handling for the `slaser` sequence in GE. +- Update to nifti-mrs-tools 1.0.0 API. +- Minor code fixes contributed by the community. + 0.6.8 (Wednesday 22nd March 2023) --------------------------------- - Added handling for the GE jpress sequence (for JH HURCULES sequence) diff --git a/requirements.yml b/requirements.yml index 7f5bb67..3fd85f2 100644 --- a/requirements.yml +++ b/requirements.yml @@ -6,4 +6,4 @@ dependencies: - scipy - brukerapi>=0.1.8 - pandas - - nifti-mrs>=0.1.3 + - nifti-mrs>=1.0.0 diff --git a/setup.py b/setup.py index 837380c..c1a124a 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -from setuptools import setup +from setuptools import setup, find_packages import versioneer import yaml @@ -23,12 +23,7 @@ url='https://github.com/wtclarke/spec2nii', long_description=long_description, long_description_content_type="text/markdown", - packages=['spec2nii', - 'spec2nii.Siemens', - 'spec2nii.GSL', - 'spec2nii.dcm2niiOrientation', - 'spec2nii.Philips', - 'spec2nii.GE'], + packages=find_packages(), install_requires=install_requires, classifiers=[ "Programming Language :: Python :: 3", diff --git a/spec2nii/GE/ge_pfile.py b/spec2nii/GE/ge_pfile.py index c7e868c..74a9c00 100644 --- a/spec2nii/GE/ge_pfile.py +++ b/spec2nii/GE/ge_pfile.py @@ -95,6 +95,8 @@ def _process_svs_pfile(pfile): data, meta, dwelltime, fname_suffix = _process_probe_p(pfile) elif psd in ('oslaser', 'slaser_cni'): data, meta, dwelltime, fname_suffix = _process_oslaser(pfile) + elif psd in ('slaser'): + data, meta, dwelltime, fname_suffix = _process_slaser(pfile) elif psd == 'gaba': data, meta, dwelltime, fname_suffix = _process_gaba(pfile) elif 'jpress_ac' in psd: # Bergen patch @@ -132,6 +134,12 @@ def _process_probe_p(pfile): meta = _populate_metadata(pfile, water_suppressed=True) meta_ref = _populate_metadata(pfile, water_suppressed=False) + meta.set_dim_info(0, 'DIM_COIL') + meta.set_dim_info(1, 'DIM_DYN') + + meta_ref.set_dim_info(0, 'DIM_COIL') + meta_ref.set_dim_info(1, 'DIM_DYN') + return [metab, water], [meta, meta_ref], dwelltime, ['', '_ref'] @@ -154,16 +162,15 @@ def _process_oslaser(pfile): data = [] meta = [] fname_suffix = [] - data.append(water[:, :, :, :, :, [0, 1, 4, 5]]) - meta.append(_populate_metadata(pfile, water_suppressed=False)) + meta.append(_populate_metadata(pfile, water_suppressed=False, data_dimensions=data[0].ndim)) fname_suffix.append('_quant') data.append(water[:, :, :, :, :, [2, 3, 6, 7]]) - meta.append(_populate_metadata(pfile, water_suppressed=False)) + meta.append(_populate_metadata(pfile, water_suppressed=False, data_dimensions=data[1].ndim)) fname_suffix.append('_ecc') data.append(metab) - meta.append(_populate_metadata(pfile, water_suppressed=True)) + meta.append(_populate_metadata(pfile, water_suppressed=True, data_dimensions=metab.ndim)) fname_suffix.append('') dwelltime = 1 / pfile.hdr.rhr_spectral_width @@ -171,6 +178,30 @@ def _process_oslaser(pfile): return data, meta, dwelltime, fname_suffix +def _process_slaser(pfile): + '''Extract metabolite and reference data from a slaser format pfile + + This seems to be like a standard probe-p. Maybe slaser is the canonical vendor implementation. + + :param Pfile pfile: Pfile object + :return: List numpy data arrays + :return: List of file name suffixes + ''' + + metab = pfile.map.raw_suppressed # typically (1,1,1,navg,ncoil,npts) + metab = np.transpose(metab, [0, 1, 2, 5, 4, 3]) # swap to (1,1,1,npts,ncoil,navg) + + water = pfile.map.raw_unsuppressed # typically (1,1,1,navg,ncoil,npts) + water = np.transpose(water, [0, 1, 2, 5, 4, 3]) # swap to (1,1,1,npts,ncoil,navg) + + dwelltime = 1 / pfile.hdr.rhr_spectral_width + + meta = _populate_metadata(pfile, water_suppressed=True, data_dimensions=metab.ndim) + meta_ref = _populate_metadata(pfile, water_suppressed=False, data_dimensions=water.ndim) + + return [metab, water], [meta, meta_ref], dwelltime, ['', '_ref'] + + def _process_gaba(pfile): '''Extract metabolite and reference data from a gaba (MPRESS) format pfile @@ -231,6 +262,8 @@ def fft_and_shift(x, axis): dwelltime = 1 / pfile.hdr.rhr_spectral_width meta = _populate_metadata(pfile) + meta.set_dim_info(0, 'DIM_COIL') + orientation = NIFTIOrient(_calculate_affine_mrsi(pfile)) return [gen_nifti_mrs_hdr_ext(data, dwelltime, meta, orientation.Q44, no_conj=True), ], ['', ] @@ -277,8 +310,22 @@ def _calculate_affine(pfile): return affine -def _populate_metadata(pfile, water_suppressed=True): - ''' Populate a nifti-mrs header extension with the requisite information''' +def _populate_metadata(pfile, water_suppressed=True, data_dimensions=None): + """Populate a nifti-mrs header extension with metadata from the pfile + + If (up to 7) data_dimensions are specified then default dimension tags + (coil, dyn, indirect) will be included. Otherwise manually specify + outside this function. + + :param pfile: pfile object + :type pfile: pfile map object + :param water_suppressed: Set water suppression header field, defaults to True + :type water_suppressed: bool, optional + :param data_dimensions: If set to 5,6, or 7 will inlcude default dim tags for those diemnsions, defaults to None + :type data_dimensions: int, optional + :return: Header extension object + :rtype: nifti_mrs.hdr_ext + """ hdr = pfile.hdr spec_frequency = float(pfile.hdr.rhr_rh_ps_mps_freq) / 1e7 @@ -304,7 +351,8 @@ def _populate_metadata(pfile, water_suppressed=True): meta = Hdr_Ext( spec_frequency, - nucleus) + nucleus, + dimensions=data_dimensions) # Standard defined metadata # # 5.1 MRS specific Tags diff --git a/spec2nii/GE/ge_read_pfile.py b/spec2nii/GE/ge_read_pfile.py index 7222ffc..9038f8b 100644 --- a/spec2nii/GE/ge_read_pfile.py +++ b/spec2nii/GE/ge_read_pfile.py @@ -180,7 +180,7 @@ def get_mapper(self): if psd in ('probe-p', 'probe-s'): mapper = PfileMapper - elif psd in ('oslaser', 'slaser_cni'): + elif psd in ('oslaser', 'slaser_cni', 'slaser'): mapper = PfileMapperSlaser elif psd == 'presscsi': mapper = PfileMapper @@ -208,6 +208,9 @@ def get_mapper(self): elif psd == 'jpress': # wtc - Added for HURCULES data. mapper = PfileMapperGaba + elif psd == 'fidall': + # WTC - added for JG's Hyperpolarised 13C data + mapper = PfileMapper else: raise UnknownPfile("No Pfile mapper for pulse sequence = %s" % psd) diff --git a/spec2nii/Philips/philips_data_list.py b/spec2nii/Philips/philips_data_list.py index 78d0392..ede8bfa 100644 --- a/spec2nii/Philips/philips_data_list.py +++ b/spec2nii/Philips/philips_data_list.py @@ -95,7 +95,7 @@ def read_data_list_pair(data_file, list_file, aux_file, special_case=None): if data_type == 'STD_0'\ and (special_case == 'hyper' or 'hyper' in meta['ProtocolName'].lower()): out_hyper, meta_hyper = _special_case_hyper(out_data, meta) - # Handle the main acqusition of the HYPER (short TE + editing) sequence + # Handle the main acquisition of the HYPER (short TE + editing) sequence # Insert spatial dimensions out_shortte = out_hyper[0].reshape((1, 1, 1) + out_hyper[0].shape) @@ -119,6 +119,10 @@ def read_data_list_pair(data_file, list_file, aux_file, special_case=None): and (special_case == 'hyper' or 'hyper' in meta['ProtocolName'].lower()): # Handle the water ref acqusition of the HYPER sequence + meta.set_dim_info( + 0, + 'DIM_COIL') + meta.set_dim_info( 1, 'DIM_USER_0', @@ -285,6 +289,13 @@ def _special_case_hyper(data, meta): meta_short_te = meta.copy() meta_edited = meta.copy() + meta_short_te.set_dim_info( + 0, + 'DIM_COIL') + meta_short_te.set_dim_info( + 1, + 'DIM_DYN') + edit_pulse_1 = 1.9 edit_pulse_2 = 4.58 edit_pulse_off = 4.18 @@ -296,6 +307,10 @@ def _special_case_hyper(data, meta): "C": {"PulseOffset": edit_pulse_1, "PulseDuration": 0.02}, "D": {"PulseOffset": edit_pulse_off, "PulseDuration": 0.02}} + meta_edited.set_dim_info( + 0, + 'DIM_COIL') + meta_edited.set_dim_info( 1, 'DIM_EDIT', diff --git a/spec2nii/other.py b/spec2nii/other.py index 7a1010e..760a09b 100644 --- a/spec2nii/other.py +++ b/spec2nii/other.py @@ -68,7 +68,7 @@ def insert_hdr_ext(args): from nifti_mrs.hdr_ext import Hdr_Ext nimg = Image(args.file) - hdr_ext = Hdr_Ext.from_header_ext(new_hdr, dimensions=nimg.ndim) + hdr_ext = Hdr_Ext.from_header_ext(new_hdr) nifti_mrs_img = gen_nifti_mrs_hdr_ext( nimg[:], nimg.header['pixdim'][4],