From bb56000e852ef092096be43fa6852132cf380e47 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Wed, 25 Oct 2017 15:14:32 -0400 Subject: [PATCH 01/34] FIX/ENH: get/set_data_shape more robustly --- nibabel/freesurfer/mghformat.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index 1bc9071538..1ce0335c2c 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -314,12 +314,12 @@ def set_zooms(self, zooms): def get_data_shape(self): ''' Get shape of data ''' - dims = self._header_data['dims'][:] + shape = tuple(self._header_data['dims']) # If last dimension (nframes) is 1, remove it because # we want to maintain 3D and it's redundant - if int(dims[-1]) == 1: - dims = dims[:-1] - return tuple(int(d) for d in dims) + if shape[3] == 1: + shape = shape[:3] + return shape def set_data_shape(self, shape): ''' Set shape of data @@ -329,15 +329,10 @@ def set_data_shape(self, shape): shape : sequence sequence of integers specifying data array shape ''' - dims = self._header_data['dims'] - # If len(dims) is 3, add a dimension. MGH header always - # needs 4 dimensions. - if len(shape) == 3: - shape = list(shape) - shape.append(1) - shape = tuple(shape) - dims[:] = shape - self._header_data['delta'][:] = 1.0 + shape = tuple(shape) + if len(shape) > 4: + raise ValueError("Shape may be at most 4 dimensional") + self._header_data['dims'] = shape + (1,) * (4 - len(shape)) def get_data_bytespervox(self): ''' Get the number of bytes per voxel of the data From d7b472a9b3f64050a8138f71fbfe314b7e5cb007 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Tue, 24 Oct 2017 21:16:41 -0400 Subject: [PATCH 02/34] RF: Subclass MGHHeader from LabeledWrapStruct --- nibabel/freesurfer/mghformat.py | 153 ++++++++------------------------ 1 file changed, 39 insertions(+), 114 deletions(-) diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index 1ce0335c2c..ecb69db2ff 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -19,6 +19,7 @@ from ..arrayproxy import ArrayProxy from ..keywordonly import kw_only_meth from ..openers import ImageOpener +from ..wrapstruct import LabeledWrapStruct # mgh header # See https://surfer.nmr.mgh.harvard.edu/fswiki/FsTutorial/MghFormat @@ -70,7 +71,7 @@ class MGHError(Exception): """ -class MGHHeader(object): +class MGHHeader(LabeledWrapStruct): ''' Class for MGH format header The header also consists of the footer data which MGH places after the data @@ -84,6 +85,7 @@ class MGHHeader(object): def __init__(self, binaryblock=None, + endianness='>', check=True): ''' Initialize header from binary data block @@ -96,64 +98,16 @@ def __init__(self, Whether to check content of header in initialization. Default is True. ''' - if binaryblock is None: - self._header_data = self._empty_headerdata() - return - # check size - if len(binaryblock) != self.template_dtype.itemsize: - raise HeaderDataError('Binary block is wrong size') - hdr = np.ndarray(shape=(), - dtype=self.template_dtype, - buffer=binaryblock) - # if goodRASFlag, discard delta, Mdc and c_ras stuff - if int(hdr['goodRASFlag']) < 0: - hdr = self._set_affine_default(hdr) - self._header_data = hdr.copy() + if endianness != '>': + raise ValueError("MGHHeader is big-endian") + + super(MGHHeader, self).__init__(binaryblock=binaryblock, + endianness=endianness, + check=False) + if int(self._structarr['goodRASFlag']) < 0: + self._set_affine_default() if check: self.check_fix() - return - - def __str__(self): - ''' Print the MGH header object information - ''' - txt = [] - txt.append(str(self.__class__)) - txt.append('Dims: ' + str(self.get_data_shape())) - code = int(self._header_data['type']) - txt.append('MRI Type: ' + self._data_type_codes.mritype[code]) - txt.append('goodRASFlag: ' + str(self._header_data['goodRASFlag'])) - txt.append('delta: ' + str(self._header_data['delta'])) - txt.append('Mdc: ') - txt.append(str(self._header_data['Mdc'])) - txt.append('Pxyz_c: ' + str(self._header_data['Pxyz_c'])) - txt.append('mrparms: ' + str(self._header_data['mrparms'])) - return '\n'.join(txt) - - def __getitem__(self, item): - ''' Return values from header data - ''' - return self._header_data[item] - - def __setitem__(self, item, value): - ''' Set values in header data - ''' - self._header_data[item] = value - - def __iter__(self): - return iter(self.keys()) - - def keys(self): - ''' Return keys from header data''' - return list(self.template_dtype.names) - - def values(self): - ''' Return values from header data''' - data = self._header_data - return [data[key] for key in self.template_dtype.names] - - def items(self): - ''' Return items from header data''' - return zip(self.keys(), self.values()) @classmethod def from_header(klass, header=None, check=True): @@ -188,42 +142,7 @@ def from_fileobj(klass, fileobj, check=True): int(klass._data_type_codes.bytespervox[tp]) * np.prod(hdr_str_to_np['dims'])) ftr_str = fileobj.read(klass._ftrdtype.itemsize) - return klass(hdr_str + ftr_str, check) - - @property - def binaryblock(self): - ''' binary block of data as string - - Returns - ------- - binaryblock : string - string giving binary data block - - ''' - return self._header_data.tostring() - - def copy(self): - ''' Return copy of header - ''' - return self.__class__(self.binaryblock, check=False) - - def __eq__(self, other): - ''' equality between two MGH format headers - - Examples - -------- - >>> wstr = MGHHeader() - >>> wstr2 = MGHHeader() - >>> wstr == wstr2 - True - ''' - return self.binaryblock == other.binaryblock - - def __ne__(self, other): - return not self == other - - def check_fix(self): - ''' Pass. maybe for now''' + return klass(hdr_str + ftr_str, check=check) def get_affine(self): ''' Get the affine transform from the header information. @@ -231,7 +150,7 @@ def get_affine(self): from the zooms ( delta ), direction cosines ( Mdc ), RAS centers ( Pxyz_c ) and the dimensions. ''' - hdr = self._header_data + hdr = self._structarr d = np.diag(hdr['delta']) pcrs_c = hdr['dims'][:3] / 2.0 Mdc = hdr['Mdc'].T @@ -253,8 +172,8 @@ def get_vox2ras_tkr(self): ''' Get the vox2ras-tkr transform. See "Torig" here: https://surfer.nmr.mgh.harvard.edu/fswiki/CoordinateSystems ''' - ds = np.array(self._header_data['delta']) - ns = (np.array(self._header_data['dims'][:3]) * ds) / 2.0 + ds = np.array(self._structarr['delta']) + ns = (np.array(self._structarr['dims'][:3]) * ds) / 2.0 v2rtkr = np.array([[-ds[0], 0, 0, ns[0]], [0, 0, ds[2], -ns[2]], [0, -ds[1], 0, ns[1]], @@ -271,7 +190,7 @@ def get_data_dtype(self): For examples see ``set_data_dtype`` ''' - code = int(self._header_data['type']) + code = int(self._structarr['type']) dtype = self._data_type_codes.numpy_dtype[code] return dtype @@ -282,7 +201,7 @@ def set_data_dtype(self, datatype): code = self._data_type_codes[datatype] except KeyError: raise MGHError('datatype dtype "%s" not recognized' % datatype) - self._header_data['type'] = code + self._structarr['type'] = code def get_zooms(self): ''' Get zooms from header @@ -292,7 +211,7 @@ def get_zooms(self): z : tuple tuple of header zoom values ''' - hdr = self._header_data + hdr = self._structarr zooms = hdr['delta'] return tuple(zooms[:]) @@ -301,7 +220,7 @@ def set_zooms(self, zooms): See docstring for ``get_zooms`` for examples ''' - hdr = self._header_data + hdr = self._structarr zooms = np.asarray(zooms) if len(zooms) != len(hdr['delta']): raise HeaderDataError('Expecting %d zoom values for ndim' @@ -314,7 +233,7 @@ def set_zooms(self, zooms): def get_data_shape(self): ''' Get shape of data ''' - shape = tuple(self._header_data['dims']) + shape = tuple(self._structarr['dims']) # If last dimension (nframes) is 1, remove it because # we want to maintain 3D and it's redundant if shape[3] == 1: @@ -332,18 +251,18 @@ def set_data_shape(self, shape): shape = tuple(shape) if len(shape) > 4: raise ValueError("Shape may be at most 4 dimensional") - self._header_data['dims'] = shape + (1,) * (4 - len(shape)) + self._structarr['dims'] = shape + (1,) * (4 - len(shape)) def get_data_bytespervox(self): ''' Get the number of bytes per voxel of the data ''' return int(self._data_type_codes.bytespervox[ - int(self._header_data['type'])]) + int(self._structarr['type'])]) def get_data_size(self): ''' Get the number of bytes the data chunk occupies. ''' - return self.get_data_bytespervox() * np.prod(self._header_data['dims']) + return self.get_data_bytespervox() * np.prod(self._structarr['dims']) def get_data_offset(self): ''' Return offset into data file to read data @@ -379,11 +298,18 @@ def get_slope_inter(self): """ return None, None - def _empty_headerdata(self): + @classmethod + def guessed_endian(klass, mapping): + """ MGHHeader data must be big-endian """ + return '>' + + @classmethod + def default_structarr(klass, endianness=None): ''' Return header data for empty header + + Ignores byte order; always big endian ''' - dt = self.template_dtype - hdr_data = np.zeros((), dtype=dt) + hdr_data = super(MGHHeader, klass).default_structarr() hdr_data['version'] = 1 hdr_data['dims'][:] = np.array([1, 1, 1, 1]) hdr_data['type'] = 3 @@ -396,15 +322,14 @@ def _empty_headerdata(self): hdr_data['mrparms'] = np.array([0, 0, 0, 0]) return hdr_data - def _set_affine_default(self, hdr): + def _set_affine_default(self): ''' If goodRASFlag is 0, return the default delta, Mdc and Pxyz_c ''' - hdr['delta'][:] = np.array([1, 1, 1]) - hdr['Mdc'][0][:] = np.array([-1, 0, 0]) # x_ras - hdr['Mdc'][1][:] = np.array([0, 0, -1]) # y_ras - hdr['Mdc'][2][:] = np.array([0, 1, 0]) # z_ras - hdr['Pxyz_c'][:] = np.array([0, 0, 0]) # c_ras - return hdr + self._structarr['delta'][:] = np.array([1, 1, 1]) + self._structarr['Mdc'][0][:] = np.array([-1, 0, 0]) # x_ras + self._structarr['Mdc'][1][:] = np.array([0, 0, -1]) # y_ras + self._structarr['Mdc'][2][:] = np.array([0, 1, 0]) # z_ras + self._structarr['Pxyz_c'][:] = np.array([0, 0, 0]) # c_ras def writehdr_to(self, fileobj): ''' Write header to fileobj From 415ee1012e06958aa83dede2ab299d5322b456af Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Tue, 24 Oct 2017 21:18:46 -0400 Subject: [PATCH 03/34] RF: Remove one-line private method --- nibabel/freesurfer/mghformat.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index ecb69db2ff..31cd7247d1 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -499,7 +499,7 @@ def to_file_map(self, file_map=None): with file_map['image'].get_prepare_fileobj('wb') as mghf: hdr.writehdr_to(mghf) self._write_data(mghf, data, hdr) - self._write_footer(mghf, hdr) + hdr.writeftr_to(mghf) self._header = hdr self.file_map = file_map @@ -524,18 +524,6 @@ def _write_data(self, mghfile, data, header): out_dtype = header.get_data_dtype() array_to_file(data, mghfile, out_dtype, offset) - def _write_footer(self, mghfile, header): - ''' Utility routine to write header. This write the footer data - which occurs after the data chunk in mgh file - - Parameters - ---------- - mghfile : file-like - file-like object implementing ``write``, open for writing - header : header object - ''' - header.writeftr_to(mghfile) - def _affine2header(self): """ Unconditionally set affine into the header """ hdr = self._header From 4e520ac5b1f4f14d246c04481ae69132689bb18d Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Tue, 24 Oct 2017 22:03:02 -0400 Subject: [PATCH 04/34] FIX: goodRASFlag check, update --- nibabel/freesurfer/mghformat.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index 31cd7247d1..ddf0dc8c25 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -104,7 +104,7 @@ def __init__(self, super(MGHHeader, self).__init__(binaryblock=binaryblock, endianness=endianness, check=False) - if int(self._structarr['goodRASFlag']) < 0: + if int(self._structarr['goodRASFlag']) < 1: self._set_affine_default() if check: self.check_fix() @@ -325,6 +325,7 @@ def default_structarr(klass, endianness=None): def _set_affine_default(self): ''' If goodRASFlag is 0, return the default delta, Mdc and Pxyz_c ''' + self._structarr['goodRASFlag'] = 1 self._structarr['delta'][:] = np.array([1, 1, 1]) self._structarr['Mdc'][0][:] = np.array([-1, 0, 0]) # x_ras self._structarr['Mdc'][1][:] = np.array([0, 0, -1]) # y_ras From a83f80bffe00a27f156f4b1e920ebb30c5e55e7d Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Tue, 24 Oct 2017 22:09:26 -0400 Subject: [PATCH 05/34] RF: Remove unused _load_cache --- nibabel/freesurfer/mghformat.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index ddf0dc8c25..7d212e0d15 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -15,7 +15,7 @@ from ..volumeutils import (array_to_file, array_from_file, Recoder) from ..spatialimages import HeaderDataError, SpatialImage -from ..fileholders import FileHolder, copy_file_map +from ..fileholders import FileHolder from ..arrayproxy import ArrayProxy from ..keywordonly import kw_only_meth from ..openers import ImageOpener @@ -437,9 +437,6 @@ def from_file_map(klass, file_map, mmap=True, keep_file_open=None): data = klass.ImageArrayProxy(img_fh.file_like, hdr_copy, mmap=mmap, keep_file_open=keep_file_open) img = klass(data, affine, header, file_map=file_map) - img._load_cache = {'header': hdr_copy, - 'affine': affine.copy(), - 'file_map': copy_file_map(file_map)} return img @classmethod From ae5c085d795ceb74dbff99917a3bfd77171680c3 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Tue, 24 Oct 2017 22:28:28 -0400 Subject: [PATCH 06/34] FIX: Label x/y/z_ras correctly --- nibabel/freesurfer/mghformat.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index 7d212e0d15..9244bb46ce 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -315,9 +315,9 @@ def default_structarr(klass, endianness=None): hdr_data['type'] = 3 hdr_data['goodRASFlag'] = 1 hdr_data['delta'][:] = np.array([1, 1, 1]) - hdr_data['Mdc'][0][:] = np.array([-1, 0, 0]) # x_ras - hdr_data['Mdc'][1][:] = np.array([0, 0, -1]) # y_ras - hdr_data['Mdc'][2][:] = np.array([0, 1, 0]) # z_ras + hdr_data['Mdc'][:, 0] = np.array([-1, 0, 0]) # x_ras + hdr_data['Mdc'][:, 1] = np.array([0, 0, 1]) # y_ras + hdr_data['Mdc'][:, 2] = np.array([0, -1, 0]) # z_ras hdr_data['Pxyz_c'] = np.array([0, 0, 0]) # c_ras hdr_data['mrparms'] = np.array([0, 0, 0, 0]) return hdr_data @@ -327,9 +327,9 @@ def _set_affine_default(self): ''' self._structarr['goodRASFlag'] = 1 self._structarr['delta'][:] = np.array([1, 1, 1]) - self._structarr['Mdc'][0][:] = np.array([-1, 0, 0]) # x_ras - self._structarr['Mdc'][1][:] = np.array([0, 0, -1]) # y_ras - self._structarr['Mdc'][2][:] = np.array([0, 1, 0]) # z_ras + hdr_data['Mdc'][:, 0] = np.array([-1, 0, 0]) # x_ras + hdr_data['Mdc'][:, 1] = np.array([0, 0, 1]) # y_ras + hdr_data['Mdc'][:, 2] = np.array([0, -1, 0]) # z_ras self._structarr['Pxyz_c'][:] = np.array([0, 0, 0]) # c_ras def writehdr_to(self, fileobj): From 6b6e512a0a1222d398c6ec86fbd3f77ce9adc471 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Tue, 24 Oct 2017 22:35:45 -0400 Subject: [PATCH 07/34] RF: Make direction cosines explicit, not transposed --- nibabel/freesurfer/mghformat.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index 9244bb46ce..4897996d0d 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -32,7 +32,9 @@ ('dof', '>i4'), ('goodRASFlag', '>i2'), ('delta', '>f4', (3,)), - ('Mdc', '>f4', (3, 3)), + ('x_ras', '>f4', (3, 1)), + ('y_ras', '>f4', (3, 1)), + ('z_ras', '>f4', (3, 1)), ('Pxyz_c', '>f4', (3,)) ] # Optional footer. Also has more stuff after this, optionally @@ -153,7 +155,7 @@ def get_affine(self): hdr = self._structarr d = np.diag(hdr['delta']) pcrs_c = hdr['dims'][:3] / 2.0 - Mdc = hdr['Mdc'].T + Mdc = np.vstack((hdr['x_ras'], hdr['y_ras'], hdr['z_ras'])) pxyz_0 = hdr['Pxyz_c'] - np.dot(Mdc, np.dot(d, pcrs_c)) M = np.eye(4, 4) M[0:3, 0:3] = np.dot(Mdc, d) @@ -315,9 +317,9 @@ def default_structarr(klass, endianness=None): hdr_data['type'] = 3 hdr_data['goodRASFlag'] = 1 hdr_data['delta'][:] = np.array([1, 1, 1]) - hdr_data['Mdc'][:, 0] = np.array([-1, 0, 0]) # x_ras - hdr_data['Mdc'][:, 1] = np.array([0, 0, 1]) # y_ras - hdr_data['Mdc'][:, 2] = np.array([0, -1, 0]) # z_ras + hdr_data['x_ras'] = np.array([[-1], [0], [0]]) + hdr_data['y_ras'] = np.array([[0], [0], [1]]) + hdr_data['z_ras'] = np.array([[0], [-1], [0]]) hdr_data['Pxyz_c'] = np.array([0, 0, 0]) # c_ras hdr_data['mrparms'] = np.array([0, 0, 0, 0]) return hdr_data @@ -327,9 +329,9 @@ def _set_affine_default(self): ''' self._structarr['goodRASFlag'] = 1 self._structarr['delta'][:] = np.array([1, 1, 1]) - hdr_data['Mdc'][:, 0] = np.array([-1, 0, 0]) # x_ras - hdr_data['Mdc'][:, 1] = np.array([0, 0, 1]) # y_ras - hdr_data['Mdc'][:, 2] = np.array([0, -1, 0]) # z_ras + self._structarr['x_ras'] = np.array([[-1], [0], [0]]) + self._structarr['y_ras'] = np.array([[0], [0], [1]]) + self._structarr['z_ras'] = np.array([[0], [-1], [0]]) self._structarr['Pxyz_c'][:] = np.array([0, 0, 0]) # c_ras def writehdr_to(self, fileobj): @@ -535,7 +537,9 @@ def _affine2header(self): Pxyz_c = np.dot(self._affine, Pcrs_c) hdr['delta'][:] = delta - hdr['Mdc'][:, :] = Mdc.T + hdr['x_ras'] = Mdc[:, [0]] + hdr['y_ras'] = Mdc[:, [1]] + hdr['z_ras'] = Mdc[:, [2]] hdr['Pxyz_c'][:] = Pxyz_c[:3] From 971c98a64101d68ff4cf7bd5adf1210346028c1c Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Tue, 24 Oct 2017 22:46:32 -0400 Subject: [PATCH 08/34] RF: Pxyz_c to c_ras --- nibabel/freesurfer/mghformat.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index 4897996d0d..321791a647 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -35,7 +35,7 @@ ('x_ras', '>f4', (3, 1)), ('y_ras', '>f4', (3, 1)), ('z_ras', '>f4', (3, 1)), - ('Pxyz_c', '>f4', (3,)) + ('c_ras', '>f4', (3, 1)) ] # Optional footer. Also has more stuff after this, optionally footer_dtd = [ @@ -150,16 +150,16 @@ def get_affine(self): ''' Get the affine transform from the header information. MGH format doesn't store the transform directly. Instead it's gleaned from the zooms ( delta ), direction cosines ( Mdc ), RAS centers ( - Pxyz_c ) and the dimensions. + c_ras ) and the dimensions. ''' hdr = self._structarr d = np.diag(hdr['delta']) pcrs_c = hdr['dims'][:3] / 2.0 - Mdc = np.vstack((hdr['x_ras'], hdr['y_ras'], hdr['z_ras'])) - pxyz_0 = hdr['Pxyz_c'] - np.dot(Mdc, np.dot(d, pcrs_c)) + Mdc = np.hstack((hdr['x_ras'], hdr['y_ras'], hdr['z_ras'])) + pxyz_0 = hdr['c_ras'] - Mdc.dot(d).dot(pcrs_c.reshape(3, 1)) M = np.eye(4, 4) - M[0:3, 0:3] = np.dot(Mdc, d) - M[0:3, 3] = pxyz_0.T + M[:3, :3] = np.dot(Mdc, d) + M[:3, [3]] = pxyz_0 return M # For compatibility with nifti (multiple affines) @@ -320,19 +320,19 @@ def default_structarr(klass, endianness=None): hdr_data['x_ras'] = np.array([[-1], [0], [0]]) hdr_data['y_ras'] = np.array([[0], [0], [1]]) hdr_data['z_ras'] = np.array([[0], [-1], [0]]) - hdr_data['Pxyz_c'] = np.array([0, 0, 0]) # c_ras + hdr_data['c_ras'] = 0 hdr_data['mrparms'] = np.array([0, 0, 0, 0]) return hdr_data def _set_affine_default(self): - ''' If goodRASFlag is 0, return the default delta, Mdc and Pxyz_c + ''' If goodRASFlag is 0, return the default delta, Mdc and c_ras ''' self._structarr['goodRASFlag'] = 1 self._structarr['delta'][:] = np.array([1, 1, 1]) self._structarr['x_ras'] = np.array([[-1], [0], [0]]) self._structarr['y_ras'] = np.array([[0], [0], [1]]) self._structarr['z_ras'] = np.array([[0], [-1], [0]]) - self._structarr['Pxyz_c'][:] = np.array([0, 0, 0]) # c_ras + self._structarr['c_ras'] = 0 def writehdr_to(self, fileobj): ''' Write header to fileobj @@ -534,13 +534,13 @@ def _affine2header(self): Mdc = MdcD / np.tile(delta, (3, 1)) Pcrs_c = np.array([0, 0, 0, 1], dtype=np.float) Pcrs_c[:3] = np.array(shape[:3]) / 2.0 - Pxyz_c = np.dot(self._affine, Pcrs_c) + c_ras = self._affine.dot(Pcrs_c.reshape(4, 1)) hdr['delta'][:] = delta hdr['x_ras'] = Mdc[:, [0]] hdr['y_ras'] = Mdc[:, [1]] hdr['z_ras'] = Mdc[:, [2]] - hdr['Pxyz_c'][:] = Pxyz_c[:3] + hdr['c_ras'] = c_ras[:3] load = MGHImage.load From 3823788cf9f67dec47fb2850c2c2359a89c0604b Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Tue, 24 Oct 2017 23:05:36 -0400 Subject: [PATCH 09/34] RF: Simplify to/from affine --- nibabel/freesurfer/mghformat.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index 321791a647..ae608ea0a3 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -13,6 +13,7 @@ from os.path import splitext import numpy as np +from ..affines import voxel_sizes from ..volumeutils import (array_to_file, array_from_file, Recoder) from ..spatialimages import HeaderDataError, SpatialImage from ..fileholders import FileHolder @@ -152,15 +153,14 @@ def get_affine(self): from the zooms ( delta ), direction cosines ( Mdc ), RAS centers ( c_ras ) and the dimensions. ''' + affine = np.eye(4) hdr = self._structarr d = np.diag(hdr['delta']) pcrs_c = hdr['dims'][:3] / 2.0 - Mdc = np.hstack((hdr['x_ras'], hdr['y_ras'], hdr['z_ras'])) - pxyz_0 = hdr['c_ras'] - Mdc.dot(d).dot(pcrs_c.reshape(3, 1)) - M = np.eye(4, 4) - M[:3, :3] = np.dot(Mdc, d) - M[:3, [3]] = pxyz_0 - return M + MdcD = np.hstack((hdr['x_ras'], hdr['y_ras'], hdr['z_ras'])).dot(d) + affine[:3, :3] = MdcD + affine[:3, [3]] = hdr['c_ras'] - MdcD.dot(pcrs_c.reshape(3, 1)) + return affine # For compatibility with nifti (multiple affines) get_best_affine = get_affine @@ -527,16 +527,14 @@ def _write_data(self, mghfile, data, header): def _affine2header(self): """ Unconditionally set affine into the header """ hdr = self._header - shape = self._dataobj.shape + shape = np.array(self._dataobj.shape[:3]).reshape(3, 1) # for more information, go through save_mgh.m in FreeSurfer dist MdcD = self._affine[:3, :3] - delta = np.sqrt(np.sum(MdcD * MdcD, axis=0)) + delta = voxel_sizes(self._affine) Mdc = MdcD / np.tile(delta, (3, 1)) - Pcrs_c = np.array([0, 0, 0, 1], dtype=np.float) - Pcrs_c[:3] = np.array(shape[:3]) / 2.0 - c_ras = self._affine.dot(Pcrs_c.reshape(4, 1)) + c_ras = self._affine.dot(np.vstack((shape, [1]))) - hdr['delta'][:] = delta + hdr['delta'] = delta hdr['x_ras'] = Mdc[:, [0]] hdr['y_ras'] = Mdc[:, [1]] hdr['z_ras'] = Mdc[:, [2]] From ef86052d653bc8d05d04e3aa71303323de60300e Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Wed, 25 Oct 2017 19:34:25 -0400 Subject: [PATCH 10/34] Rename goodRASFlag -> ras_good --- nibabel/freesurfer/mghformat.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index ae608ea0a3..b97561b16e 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -31,7 +31,7 @@ ('dims', '>i4', (4,)), ('type', '>i4'), ('dof', '>i4'), - ('goodRASFlag', '>i2'), + ('ras_good', '>i2'), ('delta', '>f4', (3,)), ('x_ras', '>f4', (3, 1)), ('y_ras', '>f4', (3, 1)), @@ -107,7 +107,7 @@ def __init__(self, super(MGHHeader, self).__init__(binaryblock=binaryblock, endianness=endianness, check=False) - if int(self._structarr['goodRASFlag']) < 1: + if not self._structarr['ras_good']: self._set_affine_default() if check: self.check_fix() @@ -315,7 +315,7 @@ def default_structarr(klass, endianness=None): hdr_data['version'] = 1 hdr_data['dims'][:] = np.array([1, 1, 1, 1]) hdr_data['type'] = 3 - hdr_data['goodRASFlag'] = 1 + hdr_data['ras_good'] = 1 hdr_data['delta'][:] = np.array([1, 1, 1]) hdr_data['x_ras'] = np.array([[-1], [0], [0]]) hdr_data['y_ras'] = np.array([[0], [0], [1]]) @@ -325,9 +325,9 @@ def default_structarr(klass, endianness=None): return hdr_data def _set_affine_default(self): - ''' If goodRASFlag is 0, return the default delta, Mdc and c_ras + ''' If ras_good flag is 0, set the default affine ''' - self._structarr['goodRASFlag'] = 1 + self._structarr['ras_good'] = 1 self._structarr['delta'][:] = np.array([1, 1, 1]) self._structarr['x_ras'] = np.array([[-1], [0], [0]]) self._structarr['y_ras'] = np.array([[0], [0], [1]]) From f742d4c4d188d34a6895dc0bd29af91b65d3b685 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Fri, 27 Oct 2017 17:34:37 -0400 Subject: [PATCH 11/34] FIX/TEST: MGHImages must be 3D or 4D --- nibabel/freesurfer/mghformat.py | 10 +++++++++- nibabel/spatialimages.py | 2 +- nibabel/tests/test_spatialimages.py | 24 ++++++++++++++++++------ 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index b97561b16e..80dbd26d59 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -17,7 +17,7 @@ from ..volumeutils import (array_to_file, array_from_file, Recoder) from ..spatialimages import HeaderDataError, SpatialImage from ..fileholders import FileHolder -from ..arrayproxy import ArrayProxy +from ..arrayproxy import ArrayProxy, reshape_dataobj from ..keywordonly import kw_only_meth from ..openers import ImageOpener from ..wrapstruct import LabeledWrapStruct @@ -390,6 +390,14 @@ class MGHImage(SpatialImage): ImageArrayProxy = ArrayProxy + def __init__(self, dataobj, affine, header=None, + extra=None, file_map=None): + shape = dataobj.shape + if len(shape) < 3: + dataobj = reshape_dataobj(dataobj, shape + (1,) * (3 - len(shape))) + super(MGHImage, self).__init__(dataobj, affine, header=header, + extra=extra, file_map=file_map) + @classmethod def filespec_to_file_map(klass, filespec): """ Check for compressed .mgz format, then .mgh format """ diff --git a/nibabel/spatialimages.py b/nibabel/spatialimages.py index 2554600649..b88b3e8538 100644 --- a/nibabel/spatialimages.py +++ b/nibabel/spatialimages.py @@ -354,7 +354,7 @@ def __init__(self, dataobj, affine, header=None, ''' super(SpatialImage, self).__init__(dataobj, header=header, extra=extra, file_map=file_map) - if not affine is None: + if affine is not None: # Check that affine is array-like 4,4. Maybe this is too strict at # this abstract level, but so far I think all image formats we know # do need 4,4. diff --git a/nibabel/tests/test_spatialimages.py b/nibabel/tests/test_spatialimages.py index 9cb4759f50..bd8b834b84 100644 --- a/nibabel/tests/test_spatialimages.py +++ b/nibabel/tests/test_spatialimages.py @@ -195,7 +195,7 @@ class DataLike(object): shape = (3,) def __array__(self): - return np.arange(3) + return np.arange(3, dtype=np.int16) class TestSpatialImage(TestCase): @@ -249,8 +249,11 @@ def test_default_header(self): def test_data_api(self): # Test minimal api data object can initialize img = self.image_class(DataLike(), None) - assert_array_equal(img.get_data(), np.arange(3)) - assert_equal(img.shape, (3,)) + # Shape may be promoted to higher dimension, but may not reorder or + # change size + assert_array_equal(img.get_data().flatten(), np.arange(3)) + assert_equal(img.get_shape()[:1], (3,)) + assert_equal(np.prod(img.get_shape()), 3) def check_dtypes(self, expected, actual): # Some images will want dtypes to be equal including endianness, @@ -278,7 +281,10 @@ def test_data_shape(self): # See https://github.com/nipy/nibabel/issues/58 arr = np.arange(4, dtype=np.int16) img = img_klass(arr, np.eye(4)) - assert_equal(img.shape, (4,)) + # Shape may be promoted to higher dimension, but may not reorder or + # change size + assert_equal(img.get_shape()[:1], (4,)) + assert_equal(np.prod(img.get_shape()), 4) img = img_klass(np.zeros((2, 3, 4), dtype=np.float32), np.eye(4)) assert_equal(img.shape, (2, 3, 4)) @@ -290,7 +296,10 @@ def test_str(self): arr = np.arange(5, dtype=np.int16) img = img_klass(arr, np.eye(4)) assert_true(len(str(img)) > 0) - assert_equal(img.shape, (5,)) + # Shape may be promoted to higher dimension, but may not reorder or + # change size + assert_equal(img.shape[:1], (5,)) + assert_equal(np.prod(img.shape), 5) img = img_klass(np.zeros((2, 3, 4), dtype=np.int16), np.eye(4)) assert_true(len(str(img)) > 0) @@ -302,7 +311,10 @@ def test_get_shape(self): # See https://github.com/nipy/nibabel/issues/58 img = img_klass(np.arange(1, dtype=np.int16), np.eye(4)) with suppress_warnings(): - assert_equal(img.get_shape(), (1,)) + # Shape may be promoted to higher dimension, but may not reorder or + # change size + assert_equal(img.get_shape()[:1], (1,)) + assert_equal(np.prod(img.get_shape()), 1) img = img_klass(np.zeros((2, 3, 4), np.int16), np.eye(4)) assert_equal(img.get_shape(), (2, 3, 4)) From d0b77886ec303d8a31652ef5dc7b29dfd997e52f Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Fri, 27 Oct 2017 20:57:01 -0400 Subject: [PATCH 12/34] RF: delta -> voxelsize; simplify conversion code --- nibabel/freesurfer/mghformat.py | 71 ++++++++++++++++----------------- 1 file changed, 34 insertions(+), 37 deletions(-) diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index 80dbd26d59..56ffddd6c1 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -32,7 +32,7 @@ ('type', '>i4'), ('dof', '>i4'), ('ras_good', '>i2'), - ('delta', '>f4', (3,)), + ('voxelsize', '>f4', (3,)), ('x_ras', '>f4', (3, 1)), ('y_ras', '>f4', (3, 1)), ('z_ras', '>f4', (3, 1)), @@ -155,11 +155,10 @@ def get_affine(self): ''' affine = np.eye(4) hdr = self._structarr - d = np.diag(hdr['delta']) - pcrs_c = hdr['dims'][:3] / 2.0 - MdcD = np.hstack((hdr['x_ras'], hdr['y_ras'], hdr['z_ras'])).dot(d) + MdcD = np.hstack((hdr['x_ras'], hdr['y_ras'], hdr['z_ras'])) * hdr['voxelsize'] + vol_center = MdcD.dot(hdr['dims'][:3].reshape(-1, 1)) / 2 affine[:3, :3] = MdcD - affine[:3, [3]] = hdr['c_ras'] - MdcD.dot(pcrs_c.reshape(3, 1)) + affine[:3, [3]] = hdr['c_ras'] - vol_center return affine # For compatibility with nifti (multiple affines) @@ -174,8 +173,8 @@ def get_vox2ras_tkr(self): ''' Get the vox2ras-tkr transform. See "Torig" here: https://surfer.nmr.mgh.harvard.edu/fswiki/CoordinateSystems ''' - ds = np.array(self._structarr['delta']) - ns = (np.array(self._structarr['dims'][:3]) * ds) / 2.0 + ds = self._structarr['voxelsize'] + ns = self._structarr['dims'][:3] * ds / 2.0 v2rtkr = np.array([[-ds[0], 0, 0, ns[0]], [0, 0, ds[2], -ns[2]], [0, -ds[1], 0, ns[1]], @@ -213,9 +212,7 @@ def get_zooms(self): z : tuple tuple of header zoom values ''' - hdr = self._structarr - zooms = hdr['delta'] - return tuple(zooms[:]) + return tuple(self._structarr['voxelsize']) def set_zooms(self, zooms): ''' Set zooms into header fields @@ -224,13 +221,12 @@ def set_zooms(self, zooms): ''' hdr = self._structarr zooms = np.asarray(zooms) - if len(zooms) != len(hdr['delta']): + if len(zooms) != len(hdr['voxelsize']): raise HeaderDataError('Expecting %d zoom values for ndim' - % hdr['delta']) + % hdr['voxelsize']) if np.any(zooms < 0): raise HeaderDataError('zooms must be positive') - delta = hdr['delta'] - delta[:] = zooms[:] + hdr['voxelsize'] = zooms def get_data_shape(self): ''' Get shape of data @@ -311,27 +307,27 @@ def default_structarr(klass, endianness=None): Ignores byte order; always big endian ''' - hdr_data = super(MGHHeader, klass).default_structarr() - hdr_data['version'] = 1 - hdr_data['dims'][:] = np.array([1, 1, 1, 1]) - hdr_data['type'] = 3 - hdr_data['ras_good'] = 1 - hdr_data['delta'][:] = np.array([1, 1, 1]) - hdr_data['x_ras'] = np.array([[-1], [0], [0]]) - hdr_data['y_ras'] = np.array([[0], [0], [1]]) - hdr_data['z_ras'] = np.array([[0], [-1], [0]]) - hdr_data['c_ras'] = 0 - hdr_data['mrparms'] = np.array([0, 0, 0, 0]) - return hdr_data + structarr = super(MGHHeader, klass).default_structarr() + structarr['version'] = 1 + structarr['dims'] = 1 + structarr['type'] = 3 + structarr['ras_good'] = 1 + structarr['voxelsize'] = 1 + structarr['x_ras'] = [[-1], [0], [0]] + structarr['y_ras'] = [[0], [0], [1]] + structarr['z_ras'] = [[0], [-1], [0]] + structarr['c_ras'] = 0 + structarr['mrparms'] = 0 + return structarr def _set_affine_default(self): ''' If ras_good flag is 0, set the default affine ''' self._structarr['ras_good'] = 1 - self._structarr['delta'][:] = np.array([1, 1, 1]) - self._structarr['x_ras'] = np.array([[-1], [0], [0]]) - self._structarr['y_ras'] = np.array([[0], [0], [1]]) - self._structarr['z_ras'] = np.array([[0], [-1], [0]]) + self._structarr['voxelsize'] = 1 + self._structarr['x_ras'] = [[-1], [0], [0]] + self._structarr['y_ras'] = [[0], [0], [1]] + self._structarr['z_ras'] = [[0], [-1], [0]] self._structarr['c_ras'] = 0 def writehdr_to(self, fileobj): @@ -535,18 +531,19 @@ def _write_data(self, mghfile, data, header): def _affine2header(self): """ Unconditionally set affine into the header """ hdr = self._header - shape = np.array(self._dataobj.shape[:3]).reshape(3, 1) + shape = np.array(self._dataobj.shape[:3]).reshape(-1, 1) + # for more information, go through save_mgh.m in FreeSurfer dist - MdcD = self._affine[:3, :3] - delta = voxel_sizes(self._affine) - Mdc = MdcD / np.tile(delta, (3, 1)) - c_ras = self._affine.dot(np.vstack((shape, [1]))) + voxelsize = voxel_sizes(self._affine) + Mdc = self._affine[:3, :3] / voxelsize + c_ras = self._affine.dot(np.vstack((shape / 2, [1])))[:3] - hdr['delta'] = delta + # Assign after we've had a chance to raise exceptions + hdr['voxelsize'] = voxelsize hdr['x_ras'] = Mdc[:, [0]] hdr['y_ras'] = Mdc[:, [1]] hdr['z_ras'] = Mdc[:, [2]] - hdr['c_ras'] = c_ras[:3] + hdr['c_ras'] = c_ras load = MGHImage.load From 8c9a14118a5bbf6dd0a39fa0a08c3cdc6e47b5a5 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Fri, 27 Oct 2017 20:57:19 -0400 Subject: [PATCH 13/34] TEST: Update tests for refactor --- nibabel/freesurfer/tests/test_mghformat.py | 35 ++++++++++++---------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/nibabel/freesurfer/tests/test_mghformat.py b/nibabel/freesurfer/tests/test_mghformat.py index b6a2e071ac..5818c63c9f 100644 --- a/nibabel/freesurfer/tests/test_mghformat.py +++ b/nibabel/freesurfer/tests/test_mghformat.py @@ -53,7 +53,7 @@ def test_read_mgh(): assert_equal(h['version'], 1) assert_equal(h['type'], 3) assert_equal(h['dof'], 0) - assert_equal(h['goodRASFlag'], 1) + assert_equal(h['ras_good'], 1) assert_array_equal(h['dims'], [3, 4, 5, 2]) assert_array_almost_equal(h['mrparms'], [2.0, 0.0, 0.0, 0.0]) assert_array_almost_equal(h.get_zooms(), 1) @@ -84,7 +84,7 @@ def test_write_mgh(): assert_equal(h['version'], 1) assert_equal(h['type'], 3) assert_equal(h['dof'], 0) - assert_equal(h['goodRASFlag'], 1) + assert_equal(h['ras_good'], 1) assert_array_equal(h['dims'], [5, 4, 3, 2]) assert_array_almost_equal(h['mrparms'], [0.0, 0.0, 0.0, 0.0]) assert_array_almost_equal(h.get_vox2ras(), v2r) @@ -110,17 +110,15 @@ def test_write_noaffine_mgh(): assert_equal(h['version'], 1) assert_equal(h['type'], 0) # uint8 for mgh assert_equal(h['dof'], 0) - assert_equal(h['goodRASFlag'], 1) + assert_equal(h['ras_good'], 1) assert_array_equal(h['dims'], [7, 13, 3, 22]) assert_array_almost_equal(h['mrparms'], [0.0, 0.0, 0.0, 0.0]) # important part -- whether default affine info is stored - ex_mdc = np.array([[-1, 0, 0], - [0, 0, -1], - [0, 1, 0]], dtype=np.float32) - assert_array_almost_equal(h['Mdc'], ex_mdc) + assert_array_almost_equal(h['x_ras'].T, [[-1, 0, 0]]) + assert_array_almost_equal(h['y_ras'].T, [[0, 0, 1]]) + assert_array_almost_equal(h['z_ras'].T, [[0, -1, 0]]) - ex_pxyzc = np.array([0, 0, 0], dtype=np.float32) - assert_array_almost_equal(h['Pxyz_c'], ex_pxyzc) + assert_array_almost_equal(h['c_ras'].T, [[0, 0, 0]]) def bad_dtype_mgh(): @@ -178,14 +176,15 @@ def test_header_updating(): assert_almost_equal(mgz.affine, exp_aff, 6) assert_almost_equal(hdr.get_affine(), exp_aff, 6) # Test that initial wonky header elements have not changed - assert_equal(hdr['delta'], 1) - assert_almost_equal(hdr['Mdc'], exp_aff[:3, :3].T) + assert_equal(hdr['voxelsize'], 1) + assert_almost_equal(np.hstack((hdr['x_ras'], hdr['y_ras'], hdr['z_ras'])), + exp_aff[:3, :3]) # Save, reload, same thing img_fobj = io.BytesIO() mgz2 = _mgh_rt(mgz, img_fobj) hdr2 = mgz2.header assert_almost_equal(hdr2.get_affine(), exp_aff, 6) - assert_equal(hdr2['delta'], 1) + assert_equal(hdr2['voxelsize'], 1) # Change affine, change underlying header info exp_aff_d = exp_aff.copy() exp_aff_d[0, -1] = -14 @@ -194,8 +193,10 @@ def test_header_updating(): mgz2.update_header() assert_almost_equal(hdr2.get_affine(), exp_aff_d, 6) RZS = exp_aff_d[:3, :3] - assert_almost_equal(hdr2['delta'], np.sqrt(np.sum(RZS ** 2, axis=0))) - assert_almost_equal(hdr2['Mdc'], (RZS / hdr2['delta']).T) + assert_almost_equal(hdr2['voxelsize'], np.sqrt(np.sum(RZS ** 2, axis=0))) + assert_almost_equal( + np.hstack((hdr2['x_ras'], hdr2['y_ras'], hdr2['z_ras'])), + RZS / hdr2['voxelsize']) def test_cosine_order(): @@ -210,8 +211,10 @@ def test_cosine_order(): hdr2 = img2.header RZS = aff[:3, :3] zooms = np.sqrt(np.sum(RZS ** 2, axis=0)) - assert_almost_equal(hdr2['Mdc'], (RZS / zooms).T) - assert_almost_equal(hdr2['delta'], zooms) + assert_almost_equal( + np.hstack((hdr2['x_ras'], hdr2['y_ras'], hdr2['z_ras'])), + RZS / zooms) + assert_almost_equal(hdr2['voxelsize'], zooms) def test_eq(): From ef831af9b7438f48059ff301526d633204718591 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Fri, 27 Oct 2017 21:02:47 -0400 Subject: [PATCH 14/34] DOC: Annotate header fields --- nibabel/freesurfer/mghformat.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index 56ffddd6c1..3eb97206e4 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -27,16 +27,16 @@ DATA_OFFSET = 284 # Note that mgh data is strictly big endian ( hence the > sign ) header_dtd = [ - ('version', '>i4'), - ('dims', '>i4', (4,)), - ('type', '>i4'), - ('dof', '>i4'), - ('ras_good', '>i2'), - ('voxelsize', '>f4', (3,)), - ('x_ras', '>f4', (3, 1)), - ('y_ras', '>f4', (3, 1)), - ('z_ras', '>f4', (3, 1)), - ('c_ras', '>f4', (3, 1)) + ('version', '>i4'), # 0; must be 1 + ('dims', '>i4', (4,)), # 4; width, height, depth, nframes + ('type', '>i4'), # 20; data type + ('dof', '>i4'), # 24; degrees of freedom + ('ras_good', '>i2'), # 28; *_ras fields valid + ('voxelsize', '>f4', (3,)), # 30; zooms (X, Y, Z) + ('x_ras', '>f4', (3, 1)), # 42; X direction cosine column + ('y_ras', '>f4', (3, 1)), # 54; Y direction cosine column + ('z_ras', '>f4', (3, 1)), # 66; Z direction cosine column + ('c_ras', '>f4', (3, 1)), # 78; mm from (0, 0, 0) RAS to vol center ] # Optional footer. Also has more stuff after this, optionally footer_dtd = [ From 5ce50fd0fe392192851abf168fee3ceb96154c9c Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Fri, 27 Oct 2017 23:43:01 -0400 Subject: [PATCH 15/34] RF: Improve footer handling --- nibabel/freesurfer/mghformat.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index 3eb97206e4..63b8a367de 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -40,7 +40,11 @@ ] # Optional footer. Also has more stuff after this, optionally footer_dtd = [ - ('mrparms', '>f4', (4,)) + ('tr', '>f4'), # 0; repetition time + ('flip_angle', '>f4'), # 4; flip angle + ('te', '>f4'), # 8; echo time + ('ti', '>f4'), # 12; inversion time + ('fov', '>f4'), # 16; field of view (unused) ] header_dtype = np.dtype(header_dtd) @@ -104,6 +108,14 @@ def __init__(self, if endianness != '>': raise ValueError("MGHHeader is big-endian") + min_size = self._hdrdtype.itemsize + full_size = self.template_dtype.itemsize + if binaryblock is not None and len(binaryblock) >= min_size: + # Right zero-pad or truncate binaryblock to appropriate size + # Footer is optional and may contain variable-length text fields, + # so limit to fixed fields + binaryblock = (binaryblock[:full_size] + + b'\x00' * (full_size - len(binaryblock))) super(MGHHeader, self).__init__(binaryblock=binaryblock, endianness=endianness, check=False) From f3811bacbfef4b2672c19db244d650f48b8ca1d8 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Fri, 27 Oct 2017 23:54:57 -0400 Subject: [PATCH 16/34] ENH: Return TR as 4th zoom for 4D images --- nibabel/freesurfer/mghformat.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index 63b8a367de..03e2cdf6bc 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -224,7 +224,9 @@ def get_zooms(self): z : tuple tuple of header zoom values ''' - return tuple(self._structarr['voxelsize']) + # Do not return time zoom (TR) if 3D image + tzoom = () if self._structarr['dims'][3] == 1 else (self['tr'],) + return tuple(self._structarr['voxelsize']) + tzoom def set_zooms(self, zooms): ''' Set zooms into header fields From 76210cd310e53b3259e056b5ec87517ffc740c11 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Fri, 27 Oct 2017 23:55:19 -0400 Subject: [PATCH 17/34] RF: Reset zooms to 1 on reshape --- nibabel/freesurfer/mghformat.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index 03e2cdf6bc..86d578a596 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -264,6 +264,7 @@ def set_data_shape(self, shape): if len(shape) > 4: raise ValueError("Shape may be at most 4 dimensional") self._structarr['dims'] = shape + (1,) * (4 - len(shape)) + self._structarr['voxelsize'] = 1 def get_data_bytespervox(self): ''' Get the number of bytes per voxel of the data From ae912c3f66f6d634836bbb433d2fd00e2454f684 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Sat, 28 Oct 2017 10:43:12 -0400 Subject: [PATCH 18/34] TEST: Footer; FIX: Simplify defaults --- nibabel/freesurfer/mghformat.py | 8 +++----- nibabel/freesurfer/tests/test_mghformat.py | 19 +++++++++++++++---- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index 86d578a596..2ef3f57628 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -328,11 +328,9 @@ def default_structarr(klass, endianness=None): structarr['type'] = 3 structarr['ras_good'] = 1 structarr['voxelsize'] = 1 - structarr['x_ras'] = [[-1], [0], [0]] - structarr['y_ras'] = [[0], [0], [1]] - structarr['z_ras'] = [[0], [-1], [0]] - structarr['c_ras'] = 0 - structarr['mrparms'] = 0 + structarr['x_ras'][0] = -1 + structarr['y_ras'][2] = 1 + structarr['z_ras'][1] = -1 return structarr def _set_affine_default(self): diff --git a/nibabel/freesurfer/tests/test_mghformat.py b/nibabel/freesurfer/tests/test_mghformat.py index 5818c63c9f..33be7b4599 100644 --- a/nibabel/freesurfer/tests/test_mghformat.py +++ b/nibabel/freesurfer/tests/test_mghformat.py @@ -55,8 +55,11 @@ def test_read_mgh(): assert_equal(h['dof'], 0) assert_equal(h['ras_good'], 1) assert_array_equal(h['dims'], [3, 4, 5, 2]) - assert_array_almost_equal(h['mrparms'], [2.0, 0.0, 0.0, 0.0]) - assert_array_almost_equal(h.get_zooms(), 1) + assert_almost_equal(h['tr'], 2.0) + assert_almost_equal(h['flip_angle'], 0.0) + assert_almost_equal(h['te'], 0.0) + assert_almost_equal(h['ti'], 0.0) + assert_array_almost_equal(h.get_zooms(), [1, 1, 1, 2]) assert_array_almost_equal(h.get_vox2ras(), v2r) assert_array_almost_equal(h.get_vox2ras_tkr(), v2rtkr) @@ -86,7 +89,11 @@ def test_write_mgh(): assert_equal(h['dof'], 0) assert_equal(h['ras_good'], 1) assert_array_equal(h['dims'], [5, 4, 3, 2]) - assert_array_almost_equal(h['mrparms'], [0.0, 0.0, 0.0, 0.0]) + assert_almost_equal(h['tr'], 0.0) + assert_almost_equal(h['flip_angle'], 0.0) + assert_almost_equal(h['te'], 0.0) + assert_almost_equal(h['ti'], 0.0) + assert_almost_equal(h['fov'], 0.0) assert_array_almost_equal(h.get_vox2ras(), v2r) # data assert_almost_equal(dat, v, 7) @@ -112,7 +119,11 @@ def test_write_noaffine_mgh(): assert_equal(h['dof'], 0) assert_equal(h['ras_good'], 1) assert_array_equal(h['dims'], [7, 13, 3, 22]) - assert_array_almost_equal(h['mrparms'], [0.0, 0.0, 0.0, 0.0]) + assert_almost_equal(h['tr'], 0.0) + assert_almost_equal(h['flip_angle'], 0.0) + assert_almost_equal(h['te'], 0.0) + assert_almost_equal(h['ti'], 0.0) + assert_almost_equal(h['fov'], 0.0) # important part -- whether default affine info is stored assert_array_almost_equal(h['x_ras'].T, [[-1, 0, 0]]) assert_array_almost_equal(h['y_ras'].T, [[0, 0, 1]]) From 024e190e2e4e4bb074969a1de06aa827896abe0e Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Sat, 28 Oct 2017 11:18:06 -0400 Subject: [PATCH 19/34] FIX: Python 2 division --- nibabel/freesurfer/mghformat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index 2ef3f57628..f823203359 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -549,7 +549,7 @@ def _affine2header(self): # for more information, go through save_mgh.m in FreeSurfer dist voxelsize = voxel_sizes(self._affine) Mdc = self._affine[:3, :3] / voxelsize - c_ras = self._affine.dot(np.vstack((shape / 2, [1])))[:3] + c_ras = self._affine.dot(np.vstack((shape / 2.0, [1])))[:3] # Assign after we've had a chance to raise exceptions hdr['voxelsize'] = voxelsize From fce43fa90f596108cf8c3c9dc10614996e8f683b Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Mon, 30 Oct 2017 09:58:42 -0400 Subject: [PATCH 20/34] TEST: Missing functionality tests --- nibabel/freesurfer/tests/test_mghformat.py | 31 ++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/nibabel/freesurfer/tests/test_mghformat.py b/nibabel/freesurfer/tests/test_mghformat.py index 33be7b4599..2cc4e8b33e 100644 --- a/nibabel/freesurfer/tests/test_mghformat.py +++ b/nibabel/freesurfer/tests/test_mghformat.py @@ -265,6 +265,37 @@ def test_mgh_load_fileobj(): assert_array_equal(img.get_data(), img2.get_data()) +def test_mgh_reject_little_endian(): + bblock = b'\x00' * MGHHeader.template_dtype.itemsize + with assert_raises(ValueError): + MGHHeader(bblock, endianness='<') + + +def test_mgh_affine_default(): + hdr = MGHHeader() + hdr['ras_good'] = 0 + hdr2 = MGHHeader(hdr.binaryblock) + assert_equal(hdr2['ras_good'], 1) + assert_array_equal(hdr['x_ras'], hdr2['x_ras']) + assert_array_equal(hdr['y_ras'], hdr2['y_ras']) + assert_array_equal(hdr['z_ras'], hdr2['z_ras']) + assert_array_equal(hdr['c_ras'], hdr2['c_ras']) + + +def test_mgh_set_data_shape(): + hdr = MGHHeader() + hdr.set_data_shape((5,)) + assert_array_equal(hdr.get_data_shape(), (5, 1, 1)) + hdr.set_data_shape((5, 4)) + assert_array_equal(hdr.get_data_shape(), (5, 4, 1)) + hdr.set_data_shape((5, 4, 3)) + assert_array_equal(hdr.get_data_shape(), (5, 4, 3)) + hdr.set_data_shape((5, 4, 3, 2)) + assert_array_equal(hdr.get_data_shape(), (5, 4, 3, 2)) + with assert_raises(ValueError): + hdr.set_data_shape((5, 4, 3, 2, 1)) + + class TestMGHImage(tsi.TestSpatialImage, tsi.MmapImageMixin): """ Apply general image tests to MGHImage """ From 98fc992d1ceebdcb98e0d87c7d2cf38381f20236 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Mon, 30 Oct 2017 22:13:19 -0400 Subject: [PATCH 21/34] ENH/DOC: Update and document get/set_zooms --- nibabel/freesurfer/mghformat.py | 42 +++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index f823203359..1a27758efc 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -216,31 +216,59 @@ def set_data_dtype(self, datatype): raise MGHError('datatype dtype "%s" not recognized' % datatype) self._structarr['type'] = code + def _ndims(self): + ''' Get dimensionality of data + + MGH does not encode dimensionality explicitly, so an image where the + fourth dimension is 1 is treated as three-dimensional. + + Returns + ------- + ndims : 3 or 4 + ''' + return 3 + (self._structarr['dims'][3] > 1) + def get_zooms(self): ''' Get zooms from header + Returns the spacing of voxels in the x, y, and z dimensions. + For four-dimensional files, a fourth zoom is included, equal to the + repetition time (TR) in ms. + + To access only the spatial zooms, use `hdr['voxelsize']`. + Returns ------- z : tuple tuple of header zoom values ''' # Do not return time zoom (TR) if 3D image - tzoom = () if self._structarr['dims'][3] == 1 else (self['tr'],) + tzoom = (self['tr'],)[:self._ndims() > 3] return tuple(self._structarr['voxelsize']) + tzoom def set_zooms(self, zooms): ''' Set zooms into header fields - See docstring for ``get_zooms`` for examples + Sets the spaing of voxels in the x, y, and z dimensions. + For four-dimensional files, a temporal zoom (repetition time, or TR, in + ms) may be provided as a fourth sequence element. + + Parameters + ---------- + zooms : sequence + sequence of floats specifying spatial and (optionally) temporal + zooms ''' hdr = self._structarr zooms = np.asarray(zooms) - if len(zooms) != len(hdr['voxelsize']): - raise HeaderDataError('Expecting %d zoom values for ndim' - % hdr['voxelsize']) - if np.any(zooms < 0): + ndims = self._ndims() + if len(zooms) > ndims: + raise HeaderDataError('Expecting %d zoom values' % ndims) + if np.any(zooms <= 0): raise HeaderDataError('zooms must be positive') - hdr['voxelsize'] = zooms + hdr['voxelsize'] = zooms[:3] + if len(zooms) == 4: + hdr['tr'] = zooms[3] def get_data_shape(self): ''' Get shape of data From d193d7b12bd9fa182c33ab2b065c9a07af04fb09 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Mon, 30 Oct 2017 22:15:24 -0400 Subject: [PATCH 22/34] RF: Simplify affine conversion code --- nibabel/freesurfer/mghformat.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index 1a27758efc..1f6f724fe5 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -13,7 +13,7 @@ from os.path import splitext import numpy as np -from ..affines import voxel_sizes +from ..affines import voxel_sizes, from_matvec from ..volumeutils import (array_to_file, array_from_file, Recoder) from ..spatialimages import HeaderDataError, SpatialImage from ..fileholders import FileHolder @@ -168,10 +168,8 @@ def get_affine(self): affine = np.eye(4) hdr = self._structarr MdcD = np.hstack((hdr['x_ras'], hdr['y_ras'], hdr['z_ras'])) * hdr['voxelsize'] - vol_center = MdcD.dot(hdr['dims'][:3].reshape(-1, 1)) / 2 - affine[:3, :3] = MdcD - affine[:3, [3]] = hdr['c_ras'] - vol_center - return affine + vol_center = MdcD.dot(hdr['dims'][:3]) / 2 + return from_matvec(MdcD, hdr['c_ras'].T - vol_center) # For compatibility with nifti (multiple affines) get_best_affine = get_affine @@ -581,9 +579,7 @@ def _affine2header(self): # Assign after we've had a chance to raise exceptions hdr['voxelsize'] = voxelsize - hdr['x_ras'] = Mdc[:, [0]] - hdr['y_ras'] = Mdc[:, [1]] - hdr['z_ras'] = Mdc[:, [2]] + hdr['x_ras'][:, 0], hdr['y_ras'][:, 0], hdr['z_ras'][:, 0] = Mdc.T hdr['c_ras'] = c_ras From 16c8bb66c80d91c91670f9ceead3b672f11c5156 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Mon, 30 Oct 2017 22:16:04 -0400 Subject: [PATCH 23/34] ENH: Check for expected byte order --- nibabel/freesurfer/mghformat.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index 1f6f724fe5..6f08246fad 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -161,6 +161,7 @@ def from_fileobj(klass, fileobj, check=True): def get_affine(self): ''' Get the affine transform from the header information. + MGH format doesn't store the transform directly. Instead it's gleaned from the zooms ( delta ), direction cosines ( Mdc ), RAS centers ( c_ras ) and the dimensions. @@ -349,6 +350,9 @@ def default_structarr(klass, endianness=None): Ignores byte order; always big endian ''' structarr = super(MGHHeader, klass).default_structarr() + # This should not be reachable even to test + if structarr.newbyteorder('>') != structarr: + raise ValueError("Default structarr is not big-endian") structarr['version'] = 1 structarr['dims'] = 1 structarr['type'] = 3 From 743db3b4202812864a7ee6383a9628e3ac457924 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Mon, 30 Oct 2017 23:03:11 -0400 Subject: [PATCH 24/34] ENH: Re-add _header_data with deprecation warning --- nibabel/freesurfer/mghformat.py | 53 +++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index 6f08246fad..91237e39ed 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -21,6 +21,7 @@ from ..keywordonly import kw_only_meth from ..openers import ImageOpener from ..wrapstruct import LabeledWrapStruct +from ..deprecated import deprecate_with_version # mgh header # See https://surfer.nmr.mgh.harvard.edu/fswiki/FsTutorial/MghFormat @@ -413,6 +414,58 @@ def writeftr_to(self, fileobj): fileobj.seek(self.get_footer_offset()) fileobj.write(ftr_nd.tostring()) + class _HeaderData: + """ Provide interface to deprecated MGHHeader fields""" + renamed = {'goodRASFlag': 'ras_good', + 'delta': 'voxelsize'} + def __init__(self, structarr): + self._structarr = structarr + + def __getitem__(self, item): + sa = self._structarr + if item == 'Mdc': + return np.hstack((sa['x_ras'], sa['y_ras'], sa['z_ras'])).T + elif item == 'Pxyz_c': + return sa['c_ras'][:, 0] + elif item == 'mrparams': + return np.hstack((sa['tr'], sa['flip_angle'], sa['te'], sa['ti'])) + elif item in self.renamed: + item = self.renamed[item] + return sa[item] + + def __setitem__(self, item, val): + sa = self._structarr + if item == 'Mdc': + sa['x_ras'][:, 0], sa['y_ras'][:, 0], sa['z_ras'][:, 0] = val + elif item == 'Pxyz_c': + sa['c_ras'][:, 0] = val + elif item == 'mrparams': + return sa['tr'], sa['flip_angle'], sa['te'], sa['ti'] = val + else: + if item in self.renamed.values(): + item = {v: k for k, v in self.renamed.items()}[item] + sa[item] = val + + @property + @deprecate_with_version('_header_data is deprecated.\n' + 'Please use the _structarr interface instead.\n' + 'Note also that some fields have changed name and ' + 'shape.', + '2.3', '4.0') + def _header_data(self): + """ Deprecated field-access interface """ + return self._HeaderData(self._structarr) + + def __getitem__(self, item): + if item in ('goodRASFlag', 'delta', 'Mdc', 'Pxyz_c', 'mrparams'): + return self._header_data[item] + super(MGHHeader, self).__getitem__(item) + + def __setitem__(self, item, value): + if item in ('goodRASFlag', 'delta', 'Mdc', 'Pxyz_c', 'mrparams'): + return self._header_data[item] = value + super(MGHHeader, self).__setitem__(item, value) + class MGHImage(SpatialImage): """ Class for MGH format image From 495bbce653ef685e8029cd2e50ebdb349fc83ac3 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Thu, 9 Nov 2017 08:01:29 -0800 Subject: [PATCH 25/34] FIX: Return on get, not on set --- nibabel/freesurfer/mghformat.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index 91237e39ed..521e70c7a6 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -459,11 +459,11 @@ def _header_data(self): def __getitem__(self, item): if item in ('goodRASFlag', 'delta', 'Mdc', 'Pxyz_c', 'mrparams'): return self._header_data[item] - super(MGHHeader, self).__getitem__(item) + return super(MGHHeader, self).__getitem__(item) def __setitem__(self, item, value): if item in ('goodRASFlag', 'delta', 'Mdc', 'Pxyz_c', 'mrparams'): - return self._header_data[item] = value + self._header_data[item] = value super(MGHHeader, self).__setitem__(item, value) From 821b6b650f4a606f8c12122047e4b225d6bf8866 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Thu, 9 Nov 2017 08:02:02 -0800 Subject: [PATCH 26/34] REVERT renamed/reshaped header fields --- nibabel/freesurfer/mghformat.py | 86 ++++++++-------------- nibabel/freesurfer/tests/test_mghformat.py | 42 ++++------- 2 files changed, 48 insertions(+), 80 deletions(-) diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index 521e70c7a6..85da5e1970 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -32,12 +32,10 @@ ('dims', '>i4', (4,)), # 4; width, height, depth, nframes ('type', '>i4'), # 20; data type ('dof', '>i4'), # 24; degrees of freedom - ('ras_good', '>i2'), # 28; *_ras fields valid - ('voxelsize', '>f4', (3,)), # 30; zooms (X, Y, Z) - ('x_ras', '>f4', (3, 1)), # 42; X direction cosine column - ('y_ras', '>f4', (3, 1)), # 54; Y direction cosine column - ('z_ras', '>f4', (3, 1)), # 66; Z direction cosine column - ('c_ras', '>f4', (3, 1)), # 78; mm from (0, 0, 0) RAS to vol center + ('goodRASFlag', '>i2'), # 28; Mdc, Pxyz_c fields valid + ('delta', '>f4', (3,)), # 30; zooms (X, Y, Z) + ('Mdc', '>f4', (3, 3)), # 42; TRANSPOSE of direction cosine matrix + ('Pxyz_c', '>f4', (3,)), # 78; mm from (0, 0, 0) RAS to vol center ] # Optional footer. Also has more stuff after this, optionally footer_dtd = [ @@ -120,7 +118,7 @@ def __init__(self, super(MGHHeader, self).__init__(binaryblock=binaryblock, endianness=endianness, check=False) - if not self._structarr['ras_good']: + if not self._structarr['goodRASFlag']: self._set_affine_default() if check: self.check_fix() @@ -165,13 +163,12 @@ def get_affine(self): MGH format doesn't store the transform directly. Instead it's gleaned from the zooms ( delta ), direction cosines ( Mdc ), RAS centers ( - c_ras ) and the dimensions. + Pxyz_c ) and the dimensions. ''' - affine = np.eye(4) hdr = self._structarr - MdcD = np.hstack((hdr['x_ras'], hdr['y_ras'], hdr['z_ras'])) * hdr['voxelsize'] + MdcD = hdr['Mdc'].T * hdr['delta'] vol_center = MdcD.dot(hdr['dims'][:3]) / 2 - return from_matvec(MdcD, hdr['c_ras'].T - vol_center) + return from_matvec(MdcD, hdr['Pxyz_c'] - vol_center) # For compatibility with nifti (multiple affines) get_best_affine = get_affine @@ -185,7 +182,7 @@ def get_vox2ras_tkr(self): ''' Get the vox2ras-tkr transform. See "Torig" here: https://surfer.nmr.mgh.harvard.edu/fswiki/CoordinateSystems ''' - ds = self._structarr['voxelsize'] + ds = self._structarr['delta'] ns = self._structarr['dims'][:3] * ds / 2.0 v2rtkr = np.array([[-ds[0], 0, 0, ns[0]], [0, 0, ds[2], -ns[2]], @@ -235,7 +232,7 @@ def get_zooms(self): For four-dimensional files, a fourth zoom is included, equal to the repetition time (TR) in ms. - To access only the spatial zooms, use `hdr['voxelsize']`. + To access only the spatial zooms, use `hdr['delta']`. Returns ------- @@ -244,7 +241,7 @@ def get_zooms(self): ''' # Do not return time zoom (TR) if 3D image tzoom = (self['tr'],)[:self._ndims() > 3] - return tuple(self._structarr['voxelsize']) + tzoom + return tuple(self._structarr['delta']) + tzoom def set_zooms(self, zooms): ''' Set zooms into header fields @@ -266,7 +263,7 @@ def set_zooms(self, zooms): raise HeaderDataError('Expecting %d zoom values' % ndims) if np.any(zooms <= 0): raise HeaderDataError('zooms must be positive') - hdr['voxelsize'] = zooms[:3] + hdr['delta'] = zooms[:3] if len(zooms) == 4: hdr['tr'] = zooms[3] @@ -292,7 +289,7 @@ def set_data_shape(self, shape): if len(shape) > 4: raise ValueError("Shape may be at most 4 dimensional") self._structarr['dims'] = shape + (1,) * (4 - len(shape)) - self._structarr['voxelsize'] = 1 + self._structarr['delta'] = 1 def get_data_bytespervox(self): ''' Get the number of bytes per voxel of the data @@ -357,22 +354,18 @@ def default_structarr(klass, endianness=None): structarr['version'] = 1 structarr['dims'] = 1 structarr['type'] = 3 - structarr['ras_good'] = 1 - structarr['voxelsize'] = 1 - structarr['x_ras'][0] = -1 - structarr['y_ras'][2] = 1 - structarr['z_ras'][1] = -1 + structarr['goodRASFlag'] = 1 + structarr['delta'] = 1 + structarr['Mdc'] = [[-1, 0, 0], [0, 0, 1], [0, -1, 0]] return structarr def _set_affine_default(self): - ''' If ras_good flag is 0, set the default affine + ''' If goodRASFlag is 0, set the default affine ''' - self._structarr['ras_good'] = 1 - self._structarr['voxelsize'] = 1 - self._structarr['x_ras'] = [[-1], [0], [0]] - self._structarr['y_ras'] = [[0], [0], [1]] - self._structarr['z_ras'] = [[0], [-1], [0]] - self._structarr['c_ras'] = 0 + self._structarr['goodRASFlag'] = 1 + self._structarr['delta'] = 1 + self._structarr['Mdc'] = [[-1, 0, 0], [0, 0, 1], [0, -1, 0]] + self._structarr['Pxyz_c'] = 0 def writehdr_to(self, fileobj): ''' Write header to fileobj @@ -416,35 +409,20 @@ def writeftr_to(self, fileobj): class _HeaderData: """ Provide interface to deprecated MGHHeader fields""" - renamed = {'goodRASFlag': 'ras_good', - 'delta': 'voxelsize'} def __init__(self, structarr): self._structarr = structarr def __getitem__(self, item): sa = self._structarr - if item == 'Mdc': - return np.hstack((sa['x_ras'], sa['y_ras'], sa['z_ras'])).T - elif item == 'Pxyz_c': - return sa['c_ras'][:, 0] - elif item == 'mrparams': + if item == 'mrparams': return np.hstack((sa['tr'], sa['flip_angle'], sa['te'], sa['ti'])) - elif item in self.renamed: - item = self.renamed[item] return sa[item] def __setitem__(self, item, val): sa = self._structarr - if item == 'Mdc': - sa['x_ras'][:, 0], sa['y_ras'][:, 0], sa['z_ras'][:, 0] = val - elif item == 'Pxyz_c': - sa['c_ras'][:, 0] = val - elif item == 'mrparams': - return sa['tr'], sa['flip_angle'], sa['te'], sa['ti'] = val - else: - if item in self.renamed.values(): - item = {v: k for k, v in self.renamed.items()}[item] - sa[item] = val + if item == 'mrparams': + sa['tr'], sa['flip_angle'], sa['te'], sa['ti'] = val + sa[item] = val @property @deprecate_with_version('_header_data is deprecated.\n' @@ -457,12 +435,12 @@ def _header_data(self): return self._HeaderData(self._structarr) def __getitem__(self, item): - if item in ('goodRASFlag', 'delta', 'Mdc', 'Pxyz_c', 'mrparams'): + if item == 'mrparams': return self._header_data[item] return super(MGHHeader, self).__getitem__(item) def __setitem__(self, item, value): - if item in ('goodRASFlag', 'delta', 'Mdc', 'Pxyz_c', 'mrparams'): + if item == 'mrparams': self._header_data[item] = value super(MGHHeader, self).__setitem__(item, value) @@ -627,17 +605,17 @@ def _write_data(self, mghfile, data, header): def _affine2header(self): """ Unconditionally set affine into the header """ hdr = self._header - shape = np.array(self._dataobj.shape[:3]).reshape(-1, 1) + shape = np.array(self._dataobj.shape[:3]) # for more information, go through save_mgh.m in FreeSurfer dist voxelsize = voxel_sizes(self._affine) Mdc = self._affine[:3, :3] / voxelsize - c_ras = self._affine.dot(np.vstack((shape / 2.0, [1])))[:3] + c_ras = self._affine.dot(np.hstack((shape / 2.0, [1])))[:3] # Assign after we've had a chance to raise exceptions - hdr['voxelsize'] = voxelsize - hdr['x_ras'][:, 0], hdr['y_ras'][:, 0], hdr['z_ras'][:, 0] = Mdc.T - hdr['c_ras'] = c_ras + hdr['delta'] = voxelsize + hdr['Mdc'] = Mdc.T + hdr['Pxyz_c'] = c_ras load = MGHImage.load diff --git a/nibabel/freesurfer/tests/test_mghformat.py b/nibabel/freesurfer/tests/test_mghformat.py index 2cc4e8b33e..9a0f1fa347 100644 --- a/nibabel/freesurfer/tests/test_mghformat.py +++ b/nibabel/freesurfer/tests/test_mghformat.py @@ -53,7 +53,7 @@ def test_read_mgh(): assert_equal(h['version'], 1) assert_equal(h['type'], 3) assert_equal(h['dof'], 0) - assert_equal(h['ras_good'], 1) + assert_equal(h['goodRASFlag'], 1) assert_array_equal(h['dims'], [3, 4, 5, 2]) assert_almost_equal(h['tr'], 2.0) assert_almost_equal(h['flip_angle'], 0.0) @@ -87,7 +87,7 @@ def test_write_mgh(): assert_equal(h['version'], 1) assert_equal(h['type'], 3) assert_equal(h['dof'], 0) - assert_equal(h['ras_good'], 1) + assert_equal(h['goodRASFlag'], 1) assert_array_equal(h['dims'], [5, 4, 3, 2]) assert_almost_equal(h['tr'], 0.0) assert_almost_equal(h['flip_angle'], 0.0) @@ -117,7 +117,7 @@ def test_write_noaffine_mgh(): assert_equal(h['version'], 1) assert_equal(h['type'], 0) # uint8 for mgh assert_equal(h['dof'], 0) - assert_equal(h['ras_good'], 1) + assert_equal(h['goodRASFlag'], 1) assert_array_equal(h['dims'], [7, 13, 3, 22]) assert_almost_equal(h['tr'], 0.0) assert_almost_equal(h['flip_angle'], 0.0) @@ -125,11 +125,8 @@ def test_write_noaffine_mgh(): assert_almost_equal(h['ti'], 0.0) assert_almost_equal(h['fov'], 0.0) # important part -- whether default affine info is stored - assert_array_almost_equal(h['x_ras'].T, [[-1, 0, 0]]) - assert_array_almost_equal(h['y_ras'].T, [[0, 0, 1]]) - assert_array_almost_equal(h['z_ras'].T, [[0, -1, 0]]) - - assert_array_almost_equal(h['c_ras'].T, [[0, 0, 0]]) + assert_array_almost_equal(h['Mdc'], [[-1, 0, 0], [0, 0, 1], [0, -1, 0]]) + assert_array_almost_equal(h['Pxyz_c'], [0, 0, 0]) def bad_dtype_mgh(): @@ -187,15 +184,14 @@ def test_header_updating(): assert_almost_equal(mgz.affine, exp_aff, 6) assert_almost_equal(hdr.get_affine(), exp_aff, 6) # Test that initial wonky header elements have not changed - assert_equal(hdr['voxelsize'], 1) - assert_almost_equal(np.hstack((hdr['x_ras'], hdr['y_ras'], hdr['z_ras'])), - exp_aff[:3, :3]) + assert_equal(hdr['delta'], 1) + assert_almost_equal(hdr['Mdc'].T, exp_aff[:3, :3]) # Save, reload, same thing img_fobj = io.BytesIO() mgz2 = _mgh_rt(mgz, img_fobj) hdr2 = mgz2.header assert_almost_equal(hdr2.get_affine(), exp_aff, 6) - assert_equal(hdr2['voxelsize'], 1) + assert_equal(hdr2['delta'], 1) # Change affine, change underlying header info exp_aff_d = exp_aff.copy() exp_aff_d[0, -1] = -14 @@ -204,10 +200,8 @@ def test_header_updating(): mgz2.update_header() assert_almost_equal(hdr2.get_affine(), exp_aff_d, 6) RZS = exp_aff_d[:3, :3] - assert_almost_equal(hdr2['voxelsize'], np.sqrt(np.sum(RZS ** 2, axis=0))) - assert_almost_equal( - np.hstack((hdr2['x_ras'], hdr2['y_ras'], hdr2['z_ras'])), - RZS / hdr2['voxelsize']) + assert_almost_equal(hdr2['delta'], np.sqrt(np.sum(RZS ** 2, axis=0))) + assert_almost_equal(hdr2['Mdc'].T, RZS / hdr2['delta']) def test_cosine_order(): @@ -222,10 +216,8 @@ def test_cosine_order(): hdr2 = img2.header RZS = aff[:3, :3] zooms = np.sqrt(np.sum(RZS ** 2, axis=0)) - assert_almost_equal( - np.hstack((hdr2['x_ras'], hdr2['y_ras'], hdr2['z_ras'])), - RZS / zooms) - assert_almost_equal(hdr2['voxelsize'], zooms) + assert_almost_equal(hdr2['Mdc'].T, RZS / zooms) + assert_almost_equal(hdr2['delta'], zooms) def test_eq(): @@ -273,13 +265,11 @@ def test_mgh_reject_little_endian(): def test_mgh_affine_default(): hdr = MGHHeader() - hdr['ras_good'] = 0 + hdr['goodRASFlag'] = 0 hdr2 = MGHHeader(hdr.binaryblock) - assert_equal(hdr2['ras_good'], 1) - assert_array_equal(hdr['x_ras'], hdr2['x_ras']) - assert_array_equal(hdr['y_ras'], hdr2['y_ras']) - assert_array_equal(hdr['z_ras'], hdr2['z_ras']) - assert_array_equal(hdr['c_ras'], hdr2['c_ras']) + assert_equal(hdr2['goodRASFlag'], 1) + assert_array_equal(hdr['Mdc'], hdr2['Mdc']) + assert_array_equal(hdr['Pxyz_c'], hdr2['Pxyz_c']) def test_mgh_set_data_shape(): From fede660739f4d3aec0860507e41a926409f4eda8 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Sat, 9 Dec 2017 09:28:02 -0500 Subject: [PATCH 27/34] RF/TEST: Check endianness rather than ignoring --- nibabel/freesurfer/mghformat.py | 11 ++++--- nibabel/freesurfer/tests/test_mghformat.py | 37 ++++++++++++++++++++++ 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index 85da5e1970..d823bd13a4 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -14,7 +14,8 @@ import numpy as np from ..affines import voxel_sizes, from_matvec -from ..volumeutils import (array_to_file, array_from_file, Recoder) +from ..volumeutils import (array_to_file, array_from_file, endian_codes, + Recoder) from ..spatialimages import HeaderDataError, SpatialImage from ..fileholders import FileHolder from ..arrayproxy import ArrayProxy, reshape_dataobj @@ -347,10 +348,10 @@ def default_structarr(klass, endianness=None): Ignores byte order; always big endian ''' - structarr = super(MGHHeader, klass).default_structarr() - # This should not be reachable even to test - if structarr.newbyteorder('>') != structarr: - raise ValueError("Default structarr is not big-endian") + if endianness is not None and endian_codes[endianness] != '>': + raise ValueError('MGHHeader must always be big endian') + structarr = super(MGHHeader, + klass).default_structarr(endianness=endianness) structarr['version'] = 1 structarr['dims'] = 1 structarr['type'] = 3 diff --git a/nibabel/freesurfer/tests/test_mghformat.py b/nibabel/freesurfer/tests/test_mghformat.py index 9a0f1fa347..09437039b4 100644 --- a/nibabel/freesurfer/tests/test_mghformat.py +++ b/nibabel/freesurfer/tests/test_mghformat.py @@ -18,6 +18,7 @@ from ..mghformat import MGHHeader, MGHError, MGHImage from ...tmpdirs import InTemporaryDirectory from ...fileholders import FileHolder +from ...volumeutils import sys_is_le from nose.tools import assert_true, assert_false @@ -286,6 +287,42 @@ def test_mgh_set_data_shape(): hdr.set_data_shape((5, 4, 3, 2, 1)) +def test_mghheader_default_structarr(): + hdr = MGHHeader.default_structarr() + assert_equal(hdr['version'], 1) + assert_array_equal(hdr['dims'], 1) + assert_equal(hdr['type'], 3) + assert_equal(hdr['dof'], 0) + assert_equal(hdr['goodRASFlag'], 1) + assert_array_equal(hdr['delta'], 1) + assert_array_equal(hdr['Mdc'], [[-1, 0, 0], [0, 0, 1], [0, -1, 0]]) + assert_array_equal(hdr['Pxyz_c'], 0) + assert_equal(hdr['tr'], 0) + assert_equal(hdr['flip_angle'], 0) + assert_equal(hdr['te'], 0) + assert_equal(hdr['ti'], 0) + assert_equal(hdr['fov'], 0) + + big_codes = ('>', 'big', 'BIG', 'b', 'be', 'B', 'BE') + little_codes = ('<', 'little', 'l', 'le', 'L', 'LE') + + if sys_is_le: + big_codes += ('swapped', 's', 'S', '!') + little_codes += ('native', 'n', 'N', '=', '|', 'i', 'I') + else: + big_codes += ('native', 'n', 'N', '=', '|', 'i', 'I') + little_codes += ('swapped', 's', 'S', '!') + + for endianness in big_codes: + hdr2 = MGHHeader.default_structarr(endianness=endianness) + assert_equal(hdr2, hdr) + assert_equal(hdr2.newbyteorder('>'), hdr) + + for endianness in little_codes: + with assert_raises(ValueError): + MGHHeader.default_structarr(endianness=endianness) + + class TestMGHImage(tsi.TestSpatialImage, tsi.MmapImageMixin): """ Apply general image tests to MGHImage """ From 0d4cdd055456e35504e148be78b35fa561c5eebe Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Sat, 9 Dec 2017 10:08:19 -0500 Subject: [PATCH 28/34] RF: Remove endianness, override copy, as_byteswapped, diagnose_binaryblock --- nibabel/freesurfer/mghformat.py | 42 ++++++++++++++++++--- nibabel/freesurfer/tests/test_mghformat.py | 44 +++++++++++++--------- 2 files changed, 63 insertions(+), 23 deletions(-) diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index d823bd13a4..9fc828a567 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -92,7 +92,6 @@ class MGHHeader(LabeledWrapStruct): def __init__(self, binaryblock=None, - endianness='>', check=True): ''' Initialize header from binary data block @@ -105,9 +104,6 @@ def __init__(self, Whether to check content of header in initialization. Default is True. ''' - if endianness != '>': - raise ValueError("MGHHeader is big-endian") - min_size = self._hdrdtype.itemsize full_size = self.template_dtype.itemsize if binaryblock is not None and len(binaryblock) >= min_size: @@ -117,7 +113,7 @@ def __init__(self, binaryblock = (binaryblock[:full_size] + b'\x00' * (full_size - len(binaryblock))) super(MGHHeader, self).__init__(binaryblock=binaryblock, - endianness=endianness, + endianness='big', check=False) if not self._structarr['goodRASFlag']: self._set_affine_default() @@ -408,6 +404,42 @@ def writeftr_to(self, fileobj): fileobj.seek(self.get_footer_offset()) fileobj.write(ftr_nd.tostring()) + def copy(self): + ''' Return copy of structure ''' + return self.__class__(self.binaryblock, check=False) + + def as_byteswapped(self, endianness=None): + ''' Return new object with given ``endianness`` + + If big endian, returns a copy of the object. Otherwise raises ValueError. + + Parameters + ---------- + endianness : None or string, optional + endian code to which to swap. None means swap from current + endianness, and is the default + + Returns + ------- + wstr : ``MGHHeader`` + ``MGHHeader`` object + + ''' + if endianness is None or endian_codes[endianness] != '>': + raise ValueError('Cannot byteswap MGHHeader - ' + 'must always be big endian') + return self.copy() + + @classmethod + def diagnose_binaryblock(klass, binaryblock, endianness=None): + if endianness is not None and endian_codes[endianness] != '>': + raise ValueError('MGHHeader must always be big endian') + wstr = klass(binaryblock, check=False) + battrun = BatteryRunner(klass._get_checks()) + reports = battrun.check_only(wstr) + return '\n'.join([report.message + for report in reports if report.message]) + class _HeaderData: """ Provide interface to deprecated MGHHeader fields""" def __init__(self, structarr): diff --git a/nibabel/freesurfer/tests/test_mghformat.py b/nibabel/freesurfer/tests/test_mghformat.py index 09437039b4..76b3b0830d 100644 --- a/nibabel/freesurfer/tests/test_mghformat.py +++ b/nibabel/freesurfer/tests/test_mghformat.py @@ -41,6 +41,17 @@ [0.0, -1.0, 0.0, 2.0], [0.0, 0.0, 0.0, 1.0]], dtype=np.float32) +BIG_CODES = ('>', 'big', 'BIG', 'b', 'be', 'B', 'BE') +LITTLE_CODES = ('<', 'little', 'l', 'le', 'L', 'LE') + +if sys_is_le: + BIG_CODES += ('swapped', 's', 'S', '!') + LITTLE_CODES += ('native', 'n', 'N', '=', '|', 'i', 'I') +else: + BIG_CODES += ('native', 'n', 'N', '=', '|', 'i', 'I') + LITTLE_CODES += ('swapped', 's', 'S', '!') + + def test_read_mgh(): # test.mgz was generated by the following command @@ -258,12 +269,6 @@ def test_mgh_load_fileobj(): assert_array_equal(img.get_data(), img2.get_data()) -def test_mgh_reject_little_endian(): - bblock = b'\x00' * MGHHeader.template_dtype.itemsize - with assert_raises(ValueError): - MGHHeader(bblock, endianness='<') - - def test_mgh_affine_default(): hdr = MGHHeader() hdr['goodRASFlag'] = 0 @@ -303,26 +308,29 @@ def test_mghheader_default_structarr(): assert_equal(hdr['ti'], 0) assert_equal(hdr['fov'], 0) - big_codes = ('>', 'big', 'BIG', 'b', 'be', 'B', 'BE') - little_codes = ('<', 'little', 'l', 'le', 'L', 'LE') - - if sys_is_le: - big_codes += ('swapped', 's', 'S', '!') - little_codes += ('native', 'n', 'N', '=', '|', 'i', 'I') - else: - big_codes += ('native', 'n', 'N', '=', '|', 'i', 'I') - little_codes += ('swapped', 's', 'S', '!') - - for endianness in big_codes: + for endianness in (None,) + BIG_CODES: hdr2 = MGHHeader.default_structarr(endianness=endianness) assert_equal(hdr2, hdr) assert_equal(hdr2.newbyteorder('>'), hdr) - for endianness in little_codes: + for endianness in LITTLE_CODES: with assert_raises(ValueError): MGHHeader.default_structarr(endianness=endianness) +def test_byteswap(): + hdr = MGHHeader() + + for endianness in BIG_CODES: + hdr2 = hdr.as_byteswapped(endianness) + assert_true(hdr2 is not hdr) + assert_equal(hdr2, hdr) + + for endianness in (None,) + LITTLE_CODES: + with assert_raises(ValueError): + hdr.as_byteswapped(endianness) + + class TestMGHImage(tsi.TestSpatialImage, tsi.MmapImageMixin): """ Apply general image tests to MGHImage """ From 93f3c70536c8cb8604e3e6d8401aef2c53973852 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Sat, 9 Dec 2017 11:21:45 -0500 Subject: [PATCH 29/34] FIX: Do not double-set attributes --- nibabel/freesurfer/mghformat.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index 9fc828a567..a8ab58f51a 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -455,7 +455,8 @@ def __setitem__(self, item, val): sa = self._structarr if item == 'mrparams': sa['tr'], sa['flip_angle'], sa['te'], sa['ti'] = val - sa[item] = val + else: + sa[item] = val @property @deprecate_with_version('_header_data is deprecated.\n' @@ -475,7 +476,8 @@ def __getitem__(self, item): def __setitem__(self, item, value): if item == 'mrparams': self._header_data[item] = value - super(MGHHeader, self).__setitem__(item, value) + else: + super(MGHHeader, self).__setitem__(item, value) class MGHImage(SpatialImage): From 664f11ceffc9cf621c9039fbf334e17731435d41 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Sat, 9 Dec 2017 11:22:09 -0500 Subject: [PATCH 30/34] TEST: TestWrapStruct features, set_zooms, deprecated fields --- nibabel/freesurfer/tests/test_mghformat.py | 144 +++++++++++++++++++-- 1 file changed, 136 insertions(+), 8 deletions(-) diff --git a/nibabel/freesurfer/tests/test_mghformat.py b/nibabel/freesurfer/tests/test_mghformat.py index 76b3b0830d..ec2d96dc96 100644 --- a/nibabel/freesurfer/tests/test_mghformat.py +++ b/nibabel/freesurfer/tests/test_mghformat.py @@ -18,17 +18,21 @@ from ..mghformat import MGHHeader, MGHError, MGHImage from ...tmpdirs import InTemporaryDirectory from ...fileholders import FileHolder +from ...spatialimages import HeaderDataError from ...volumeutils import sys_is_le +from ...wrapstruct import WrapStructError from nose.tools import assert_true, assert_false from numpy.testing import (assert_equal, assert_array_equal, assert_array_almost_equal, assert_almost_equal, assert_raises) +from ...testing import assert_not_equal from ...testing import data_path from ...tests import test_spatialimages as tsi +from ...tests.test_wrapstruct import _TestWrapStructBase MGZ_FNAME = os.path.join(data_path, 'test.mgz') @@ -141,6 +145,21 @@ def test_write_noaffine_mgh(): assert_array_almost_equal(h['Pxyz_c'], [0, 0, 0]) +def test_set_zooms(): + mgz = load(MGZ_FNAME) + h = mgz.header + assert_array_almost_equal(h.get_zooms(), [1, 1, 1, 2]) + h.set_zooms([1, 1, 1, 3]) + assert_array_almost_equal(h.get_zooms(), [1, 1, 1, 3]) + for zooms in ((-1, 1, 1, 1), + (1, -1, 1, 1), + (1, 1, -1, 1), + (1, 1, 1, -1), + (1, 1, 1, 1, 5)): + with assert_raises(HeaderDataError): + h.set_zooms(zooms) + + def bad_dtype_mgh(): ''' This function raises an MGHError exception because uint16 is not a valid MGH datatype. @@ -318,17 +337,25 @@ def test_mghheader_default_structarr(): MGHHeader.default_structarr(endianness=endianness) -def test_byteswap(): +def test_deprecated_fields(): hdr = MGHHeader() - for endianness in BIG_CODES: - hdr2 = hdr.as_byteswapped(endianness) - assert_true(hdr2 is not hdr) - assert_equal(hdr2, hdr) + # mrparams is the only deprecated field at the moment + assert_array_equal(hdr['mrparams'], 0) - for endianness in (None,) + LITTLE_CODES: - with assert_raises(ValueError): - hdr.as_byteswapped(endianness) + hdr['mrparams'] = [1, 2, 3, 4] + assert_array_almost_equal(hdr['mrparams'], [1, 2, 3, 4]) + assert_equal(hdr['tr'], 1) + assert_equal(hdr['flip_angle'], 2) + assert_equal(hdr['te'], 3) + assert_equal(hdr['ti'], 4) + assert_equal(hdr['fov'], 0) + + hdr['tr'] = 5 + hdr['flip_angle'] = 6 + hdr['te'] = 7 + hdr['ti'] = 8 + assert_array_almost_equal(hdr['mrparams'], [5, 6, 7, 8]) class TestMGHImage(tsi.TestSpatialImage, tsi.MmapImageMixin): @@ -342,3 +369,104 @@ def check_dtypes(self, expected, actual): # others may only require the same type # MGH requires the actual to be a big endian version of expected assert_equal(expected.newbyteorder('>'), actual) + + +class TestMGHHeader(_TestWrapStructBase): + header_class = MGHHeader + + def test_general_init(self): + hdr = self.header_class() + # binaryblock has length given by header data dtype + binblock = hdr.binaryblock + assert_equal(len(binblock), hdr.structarr.dtype.itemsize) + # Endianness will always be big, and cannot be set + assert_equal(hdr.endianness, '>') + # You can also pass in a check flag, without data this has no + # effect + hdr = self.header_class(check=False) + + def _set_something_into_hdr(self, hdr): + hdr['dims'] = [4, 3, 2, 1] + + # Update tests to account for big-endian requirement + def test__eq__(self): + # Test equal and not equal + hdr1 = self.header_class() + hdr2 = self.header_class() + assert_equal(hdr1, hdr2) + self._set_something_into_hdr(hdr1) + assert_not_equal(hdr1, hdr2) + self._set_something_into_hdr(hdr2) + assert_equal(hdr1, hdr2) + # REMOVED as_byteswapped() test + # Check comparing to funny thing says no + assert_not_equal(hdr1, None) + assert_not_equal(hdr1, 1) + + def test_to_from_fileobj(self): + # Successful write using write_to + hdr = self.header_class() + str_io = io.BytesIO() + hdr.write_to(str_io) + str_io.seek(0) + hdr2 = self.header_class.from_fileobj(str_io) + assert_equal(hdr2.endianness, '>') + assert_equal(hdr2.binaryblock, hdr.binaryblock) + + def test_endian_guess(self): + # Check guesses of endian + eh = self.header_class() + assert_equal(eh.endianness, '>') + assert_equal(self.header_class.guessed_endian(eh), '>') + + def test_bytes(self): + # Test get of bytes + hdr1 = self.header_class() + bb = hdr1.binaryblock + hdr2 = self.header_class(hdr1.binaryblock) + assert_equal(hdr1, hdr2) + assert_equal(hdr1.binaryblock, hdr2.binaryblock) + # Do a set into the header, and try again. The specifics of 'setting + # something' will depend on the nature of the bytes object + self._set_something_into_hdr(hdr1) + hdr2 = self.header_class(hdr1.binaryblock) + assert_equal(hdr1, hdr2) + assert_equal(hdr1.binaryblock, hdr2.binaryblock) + # Short binaryblocks give errors (here set through init) + # Long binaryblocks are truncated + assert_raises(WrapStructError, + self.header_class, + bb[:self.header_class._hdrdtype.itemsize - 1]) + # Checking set to true by default, and prevents nonsense being + # set into the header. + bb_bad = self.get_bad_bb() + if bb_bad is None: + return + with imageglobals.LoggingOutputSuppressor(): + assert_raises(HeaderDataError, self.header_class, bb_bad) + # now slips past without check + _ = self.header_class(bb_bad, check=False) + + def test_as_byteswapped(self): + # Check byte swapping + hdr = self.header_class() + assert_equal(hdr.endianness, '>') + # same code just returns a copy + for endianness in BIG_CODES: + hdr2 = hdr.as_byteswapped(endianness) + assert_false(hdr2 is hdr) + assert_equal(hdr2, hdr) + + # Different code raises error + for endianness in (None,) + LITTLE_CODES: + with assert_raises(ValueError): + hdr.as_byteswapped(endianness) + # Note that contents is not rechecked on swap / copy + class DC(self.header_class): + def check_fix(self, *args, **kwargs): + raise Exception + + # Assumes check=True default + assert_raises(Exception, DC, hdr.binaryblock) + hdr = DC(hdr.binaryblock, check=False) + hdr2 = hdr.as_byteswapped('>') From 3f6244574c8f6408d879dc32dd09c37d363dc21e Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Sat, 9 Dec 2017 13:00:28 -0500 Subject: [PATCH 31/34] TEST: Add and test version check --- nibabel/freesurfer/mghformat.py | 15 ++++++++++++ nibabel/freesurfer/tests/test_mghformat.py | 27 +++++++++++++++++----- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index a8ab58f51a..b55e1b7cc6 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -21,6 +21,7 @@ from ..arrayproxy import ArrayProxy, reshape_dataobj from ..keywordonly import kw_only_meth from ..openers import ImageOpener +from ..batteryrunners import BatteryRunner, Report from ..wrapstruct import LabeledWrapStruct from ..deprecated import deprecate_with_version @@ -120,6 +121,20 @@ def __init__(self, if check: self.check_fix() + @staticmethod + def chk_version(hdr, fix=False): + rep = Report() + if hdr['version'] != 1: + rep = Report(HeaderDataError, 40) + rep.problem_msg = 'Unknown MGH format version' + if fix: + hdr['version'] = 1 + return hdr, rep + + @classmethod + def _get_checks(klass): + return (klass.chk_version,) + @classmethod def from_header(klass, header=None, check=True): ''' Class method to create MGH header from another MGH header diff --git a/nibabel/freesurfer/tests/test_mghformat.py b/nibabel/freesurfer/tests/test_mghformat.py index ec2d96dc96..b9bfa53228 100644 --- a/nibabel/freesurfer/tests/test_mghformat.py +++ b/nibabel/freesurfer/tests/test_mghformat.py @@ -21,6 +21,7 @@ from ...spatialimages import HeaderDataError from ...volumeutils import sys_is_le from ...wrapstruct import WrapStructError +from ... import imageglobals from nose.tools import assert_true, assert_false @@ -32,7 +33,7 @@ from ...testing import data_path from ...tests import test_spatialimages as tsi -from ...tests.test_wrapstruct import _TestWrapStructBase +from ...tests.test_wrapstruct import _TestLabeledWrapStruct MGZ_FNAME = os.path.join(data_path, 'test.mgz') @@ -371,9 +372,16 @@ def check_dtypes(self, expected, actual): assert_equal(expected.newbyteorder('>'), actual) -class TestMGHHeader(_TestWrapStructBase): +class TestMGHHeader(_TestLabeledWrapStruct): header_class = MGHHeader + def _set_something_into_hdr(self, hdr): + hdr['dims'] = [4, 3, 2, 1] + + def get_bad_bb(self): + return b'\xff' + bytes(self.header_class._hdrdtype.itemsize) + + # Update tests to account for big-endian requirement def test_general_init(self): hdr = self.header_class() # binaryblock has length given by header data dtype @@ -385,10 +393,6 @@ def test_general_init(self): # effect hdr = self.header_class(check=False) - def _set_something_into_hdr(self, hdr): - hdr['dims'] = [4, 3, 2, 1] - - # Update tests to account for big-endian requirement def test__eq__(self): # Test equal and not equal hdr1 = self.header_class() @@ -470,3 +474,14 @@ def check_fix(self, *args, **kwargs): assert_raises(Exception, DC, hdr.binaryblock) hdr = DC(hdr.binaryblock, check=False) hdr2 = hdr.as_byteswapped('>') + + def test_checks(self): + # Test header checks + hdr_t = self.header_class() + # _dxer just returns the diagnostics as a string + # Default hdr is OK + assert_equal(self._dxer(hdr_t), '') + # Version should be 1 + hdr = hdr_t.copy() + hdr['version'] = 2 + assert_equal(self._dxer(hdr), 'Unknown MGH format version') From fa73ad4b09e9d9c7b0e87b75b79ca8bc4f2df994 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Sat, 9 Dec 2017 13:06:27 -0500 Subject: [PATCH 32/34] TEST: Test MGHHeader._HeaderData fully --- nibabel/freesurfer/tests/test_mghformat.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/nibabel/freesurfer/tests/test_mghformat.py b/nibabel/freesurfer/tests/test_mghformat.py index b9bfa53228..776c461e18 100644 --- a/nibabel/freesurfer/tests/test_mghformat.py +++ b/nibabel/freesurfer/tests/test_mghformat.py @@ -340,9 +340,12 @@ def test_mghheader_default_structarr(): def test_deprecated_fields(): hdr = MGHHeader() + hdr_data = MGHHeader._HeaderData(hdr.structarr) # mrparams is the only deprecated field at the moment + # Accessing hdr_data is equivalent to accessing hdr, so double all checks assert_array_equal(hdr['mrparams'], 0) + assert_array_equal(hdr_data['mrparams'], 0) hdr['mrparams'] = [1, 2, 3, 4] assert_array_almost_equal(hdr['mrparams'], [1, 2, 3, 4]) @@ -351,12 +354,26 @@ def test_deprecated_fields(): assert_equal(hdr['te'], 3) assert_equal(hdr['ti'], 4) assert_equal(hdr['fov'], 0) + assert_array_almost_equal(hdr_data['mrparams'], [1, 2, 3, 4]) + assert_equal(hdr_data['tr'], 1) + assert_equal(hdr_data['flip_angle'], 2) + assert_equal(hdr_data['te'], 3) + assert_equal(hdr_data['ti'], 4) + assert_equal(hdr_data['fov'], 0) hdr['tr'] = 5 hdr['flip_angle'] = 6 hdr['te'] = 7 hdr['ti'] = 8 assert_array_almost_equal(hdr['mrparams'], [5, 6, 7, 8]) + assert_array_almost_equal(hdr_data['mrparams'], [5, 6, 7, 8]) + + hdr_data['tr'] = 9 + hdr_data['flip_angle'] = 10 + hdr_data['te'] = 11 + hdr_data['ti'] = 12 + assert_array_almost_equal(hdr['mrparams'], [9, 10, 11, 12]) + assert_array_almost_equal(hdr_data['mrparams'], [9, 10, 11, 12]) class TestMGHImage(tsi.TestSpatialImage, tsi.MmapImageMixin): @@ -379,7 +396,7 @@ def _set_something_into_hdr(self, hdr): hdr['dims'] = [4, 3, 2, 1] def get_bad_bb(self): - return b'\xff' + bytes(self.header_class._hdrdtype.itemsize) + return b'\xff' + b'\x00' * self.header_class._hdrdtype.itemsize # Update tests to account for big-endian requirement def test_general_init(self): From c066ceaa0ab11556a92f331e7f53462276bec821 Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Mon, 18 Dec 2017 11:37:37 -0500 Subject: [PATCH 33/34] STY: Typo --- nibabel/freesurfer/mghformat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index b55e1b7cc6..d975f0a9b4 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -258,7 +258,7 @@ def get_zooms(self): def set_zooms(self, zooms): ''' Set zooms into header fields - Sets the spaing of voxels in the x, y, and z dimensions. + Sets the spacing of voxels in the x, y, and z dimensions. For four-dimensional files, a temporal zoom (repetition time, or TR, in ms) may be provided as a fourth sequence element. From 8d720fe7572478f33fb21005c37a7ee1a9bc19c8 Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Mon, 18 Dec 2017 12:12:15 -0500 Subject: [PATCH 34/34] DOC: Add reference to FreeSurfer wiki --- nibabel/freesurfer/mghformat.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index d975f0a9b4..c35acc7cf5 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -242,7 +242,8 @@ def get_zooms(self): Returns the spacing of voxels in the x, y, and z dimensions. For four-dimensional files, a fourth zoom is included, equal to the - repetition time (TR) in ms. + repetition time (TR) in ms (see `The MGH/MGZ Volume Format + `_). To access only the spatial zooms, use `hdr['delta']`. @@ -250,6 +251,8 @@ def get_zooms(self): ------- z : tuple tuple of header zoom values + + .. _mghformat: https://surfer.nmr.mgh.harvard.edu/fswiki/FsTutorial/MghFormat#line-82 ''' # Do not return time zoom (TR) if 3D image tzoom = (self['tr'],)[:self._ndims() > 3]