From 3f3b450313ece2c65a74db7891777934d0e025cb Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Tue, 24 Oct 2017 09:54:04 -0400 Subject: [PATCH 01/24] ENH: Add get_norm_zooms for zooms in mm/s units --- nibabel/analyze.py | 4 ++++ nibabel/nifti1.py | 33 +++++++++++++++++++++++++++++++++ nibabel/spatialimages.py | 4 ++++ 3 files changed, 41 insertions(+) diff --git a/nibabel/analyze.py b/nibabel/analyze.py index a1c1cf1d2f..8f0d56cade 100644 --- a/nibabel/analyze.py +++ b/nibabel/analyze.py @@ -706,6 +706,10 @@ def set_zooms(self, zooms): pixdims = hdr['pixdim'] pixdims[1:ndim + 1] = zooms[:] + def get_norm_zooms(self, raise_unknown=False): + ''' Get zooms in mm/s units ''' + return self.get_zooms() + def as_analyze_map(self): """ Return header as mapping for conversion to Analyze types diff --git a/nibabel/nifti1.py b/nibabel/nifti1.py index 1bffac10ce..64dac785ad 100644 --- a/nibabel/nifti1.py +++ b/nibabel/nifti1.py @@ -1677,6 +1677,39 @@ def set_xyzt_units(self, xyz=None, t=None): t_code = unit_codes[t] self.structarr['xyzt_units'] = xyz_code + t_code + def get_norm_zooms(self, raise_unknown=False): + ''' Get zooms in mm/s units ''' + raw_zooms = self.get_zooms() + xyz_zooms = raw_zooms[:3] + t_zoom = raw_zooms[3] if len(raw_zooms) > 3 else None + + xyz_code, t_code = self.get_xyzt_units() + xyz_msg = t_msg = '' + if xyz_code == 'unknown': + xyz_msg = 'Unknown spatial units' + xyz_code = 'mm' + if t_code == 'unknown' and t_zoom is not None: + t_msg = 'Unknown time units' + t_code = 'sec' + if raise_unknown and (xyz_msg, t_msg) != ('', ''): + if xyz_msg and t_msg: + msg = 'Unknown spatial and time units' + else: + msg = xyz_msg or t_msg + raise ValueError("Error: {}".format(msg)) + if xyz_msg: + warnings.warn('{} - assuming mm'.format(xyz_msg)) + if t_msg: + warnings.warn('{} - assuming sec'.format(t_msg)) + + xyz_factor = {'meter': 0.001, 'mm': 1, 'usec': 1000}[xyz_code] + t_factor = {'sec': 1, 'msec': 1000, 'usec': 1000000}[t_code] + + xyz_zooms = tuple(np.array(xyz_zooms) / xyz_factor) + t_zoom = (t_zoom / t_factor,) if t_zoom is not None else () + + return xyz_zooms + t_zoom + def _clean_after_mapping(self): """ Set format-specific stuff after converting header from mapping diff --git a/nibabel/spatialimages.py b/nibabel/spatialimages.py index 09744d0149..ece704f373 100644 --- a/nibabel/spatialimages.py +++ b/nibabel/spatialimages.py @@ -238,6 +238,10 @@ def set_zooms(self, zooms): raise HeaderDataError('zooms must be positive') self._zooms = zooms + def get_norm_zooms(self, raise_unknown=False): + ''' Get zooms in mm/s units ''' + return self.get_zooms() + def get_base_affine(self): shape = self.get_data_shape() zooms = self.get_zooms() From d26acfbec1c6ea117834c5df6d68656e675aa142 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Mon, 18 Dec 2017 12:41:41 -0500 Subject: [PATCH 02/24] ENH: Add get_norm_zooms for MGHHeader --- nibabel/freesurfer/mghformat.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index 9d2cdb905b..ea9cf48d2a 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -285,6 +285,15 @@ def set_zooms(self, zooms): raise HeaderDataError(f'TR must be non-negative; got {zooms[3]}') hdr['tr'] = zooms[3] + def get_norm_zooms(self, raise_unknown=False): + ''' Get zooms in mm/s units ''' + zooms = self.get_zooms() + + if len(zooms) == 4: + zooms = zooms[:3] + (zooms[3] / 1000,) + + return zooms + def get_data_shape(self): """ Get shape of data """ From de9323c42ff9e7cc6ef90abcee69eaebd8deb060 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Thu, 21 Dec 2017 14:00:01 -0600 Subject: [PATCH 03/24] ENH: Add set_norm_zooms --- nibabel/analyze.py | 4 ++++ nibabel/freesurfer/mghformat.py | 6 ++++++ nibabel/nifti1.py | 5 +++++ nibabel/spatialimages.py | 4 ++++ 4 files changed, 19 insertions(+) diff --git a/nibabel/analyze.py b/nibabel/analyze.py index 8f0d56cade..3f1a730850 100644 --- a/nibabel/analyze.py +++ b/nibabel/analyze.py @@ -710,6 +710,10 @@ def get_norm_zooms(self, raise_unknown=False): ''' Get zooms in mm/s units ''' return self.get_zooms() + def set_norm_zooms(self, zooms): + ''' Set zooms in mm/s units ''' + return self.set_zooms(zooms) + def as_analyze_map(self): """ Return header as mapping for conversion to Analyze types diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index ea9cf48d2a..bf5a844368 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -294,6 +294,12 @@ def get_norm_zooms(self, raise_unknown=False): return zooms + def set_norm_zooms(self, zooms): + if len(zooms) == 4: + zooms = zooms[:3] + (zooms[3] * 1000,) + + self.set_zooms(zooms) + def get_data_shape(self): """ Get shape of data """ diff --git a/nibabel/nifti1.py b/nibabel/nifti1.py index 64dac785ad..0524b47698 100644 --- a/nibabel/nifti1.py +++ b/nibabel/nifti1.py @@ -1710,6 +1710,11 @@ def get_norm_zooms(self, raise_unknown=False): return xyz_zooms + t_zoom + def set_norm_zooms(self, zooms): + ''' Set zooms in mm/s units ''' + self.set_zooms(zooms) + self.set_xyzt_units('mm', 'sec') + def _clean_after_mapping(self): """ Set format-specific stuff after converting header from mapping diff --git a/nibabel/spatialimages.py b/nibabel/spatialimages.py index ece704f373..e2be3a47dc 100644 --- a/nibabel/spatialimages.py +++ b/nibabel/spatialimages.py @@ -242,6 +242,10 @@ def get_norm_zooms(self, raise_unknown=False): ''' Get zooms in mm/s units ''' return self.get_zooms() + def set_norm_zooms(self, zooms): + ''' Get zooms in mm/s units ''' + return self.set_zooms(zooms) + def get_base_affine(self): shape = self.get_data_shape() zooms = self.get_zooms() From 5c3036622697b3c64ef4d125299620419abe30c4 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Thu, 21 Dec 2017 14:00:27 -0600 Subject: [PATCH 04/24] FIX: Various unit issues --- nibabel/nifti1.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/nibabel/nifti1.py b/nibabel/nifti1.py index 0524b47698..df7997d486 100644 --- a/nibabel/nifti1.py +++ b/nibabel/nifti1.py @@ -1671,8 +1671,6 @@ def set_xyzt_units(self, xyz=None, t=None): xyz = 0 if t is None: t = 0 - xyz_code = self.structarr['xyzt_units'] % 8 - t_code = self.structarr['xyzt_units'] - xyz_code xyz_code = unit_codes[xyz] t_code = unit_codes[t] self.structarr['xyzt_units'] = xyz_code + t_code @@ -1702,11 +1700,11 @@ def get_norm_zooms(self, raise_unknown=False): if t_msg: warnings.warn('{} - assuming sec'.format(t_msg)) - xyz_factor = {'meter': 0.001, 'mm': 1, 'usec': 1000}[xyz_code] - t_factor = {'sec': 1, 'msec': 1000, 'usec': 1000000}[t_code] + xyz_factor = {'meter': 1000, 'mm': 1, 'micron': 0.001}[xyz_code] + t_factor = {'sec': 1, 'msec': 0.001, 'usec': 0.000001}[t_code] - xyz_zooms = tuple(np.array(xyz_zooms) / xyz_factor) - t_zoom = (t_zoom / t_factor,) if t_zoom is not None else () + xyz_zooms = tuple(np.array(xyz_zooms) * xyz_factor) + t_zoom = (t_zoom * t_factor,) if t_zoom is not None else () return xyz_zooms + t_zoom From 4b86d3e020f4be63a81ba6a213da0bc87fdd8a08 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Thu, 21 Dec 2017 14:19:09 -0600 Subject: [PATCH 05/24] TEST: Test get/set_norm_zooms --- nibabel/freesurfer/tests/test_mghformat.py | 48 +++++++++++++++++++++ nibabel/tests/test_nifti1.py | 49 ++++++++++++++++++++++ nibabel/tests/test_spatialimages.py | 22 ++++++++++ 3 files changed, 119 insertions(+) diff --git a/nibabel/freesurfer/tests/test_mghformat.py b/nibabel/freesurfer/tests/test_mghformat.py index 4c812087c2..886b06528e 100644 --- a/nibabel/freesurfer/tests/test_mghformat.py +++ b/nibabel/freesurfer/tests/test_mghformat.py @@ -351,6 +351,54 @@ def check_dtypes(self, expected, actual): # MGH requires the actual to be a big endian version of expected assert expected.newbyteorder('>') == actual + def test_norm_zooms_edge_cases(self): + img_klass = self.image_class + aff = np.eye(4) + arr = np.arange(120, dtype=np.int16).reshape((2, 3, 4, 5)) + img = img_klass(arr, aff) + + assert_array_almost_equal(img.header.get_zooms(), + (1, 1, 1, 0)) + assert_array_almost_equal(img.header.get_norm_zooms(), + (1, 1, 1, 0)) + + img.header.set_zooms((1, 1, 1, 2000)) + assert_array_almost_equal(img.header.get_zooms(), + (1, 1, 1, 2000)) + assert_array_almost_equal(img.header.get_norm_zooms(), + (1, 1, 1, 2)) + + img.header.set_norm_zooms((2, 2, 2, 3)) + assert_array_almost_equal(img.header.get_zooms(), + (2, 2, 2, 3000)) + assert_array_almost_equal(img.header.get_norm_zooms(), + (2, 2, 2, 3)) + + # It's legal to set zooms for spatial dimensions only + img.header.set_norm_zooms((3, 3, 3)) + assert_array_almost_equal(img.header.get_zooms(), + (3, 3, 3, 3000)) + assert_array_almost_equal(img.header.get_norm_zooms(), + (3, 3, 3, 3)) + + arr = np.arange(24, dtype=np.int16).reshape((2, 3, 4)) + img = img_klass(arr, aff) + + assert_array_almost_equal(img.header.get_zooms(), (1, 1, 1)) + assert_array_almost_equal(img.header.get_norm_zooms(), (1, 1, 1)) + + img.header.set_zooms((2, 2, 2)) + assert_array_almost_equal(img.header.get_zooms(), (2, 2, 2)) + assert_array_almost_equal(img.header.get_norm_zooms(), (2, 2, 2)) + + img.header.set_norm_zooms((3, 3, 3)) + assert_array_almost_equal(img.header.get_zooms(), (3, 3, 3)) + assert_array_almost_equal(img.header.get_norm_zooms(), (3, 3, 3)) + + # Cannot set TR as zoom for 3D image + assert_raises(HeaderDataError, img.header.set_zooms, (4, 4, 4, 5)) + assert_raises(HeaderDataError, img.header.set_norm_zooms, (4, 4, 4, 5)) + class TestMGHHeader(tws._TestLabeledWrapStruct): header_class = MGHHeader diff --git a/nibabel/tests/test_nifti1.py b/nibabel/tests/test_nifti1.py index 63cf13c103..e5fd6406cd 100644 --- a/nibabel/tests/test_nifti1.py +++ b/nibabel/tests/test_nifti1.py @@ -1176,6 +1176,55 @@ def test_static_dtype_aliases(self): img_rt = bytesio_round_trip(img) assert img_rt.get_data_dtype() == effective_dt + def test_norm_zooms_edge_cases(self): + img_klass = self.image_class + arr = np.arange(120, dtype=np.int16).reshape((2, 3, 4, 5)) + aff = np.eye(4) + img = img_klass(arr, aff) + + # Unknown units = 2 warnings + with warnings.catch_warnings(record=True) as warns: + assert_array_almost_equal(img.header.get_norm_zooms(), + (1, 1, 1, 1)) + assert_equal(len(warns), 2) + assert_raises(ValueError, img.header.get_norm_zooms, True) + + img.header.set_xyzt_units(xyz='meter') + with warnings.catch_warnings(record=True) as warns: + assert_array_almost_equal(img.header.get_norm_zooms(), + (1000, 1000, 1000, 1)) + assert_equal(len(warns), 1) + assert_raises(ValueError, img.header.get_norm_zooms, True) + + img.header.set_xyzt_units(xyz='mm', t='sec') + assert_array_almost_equal(img.header.get_norm_zooms(), + (1, 1, 1, 1)) + img.header.set_xyzt_units(xyz='micron', t='sec') + assert_array_almost_equal(img.header.get_norm_zooms(), + (0.001, 0.001, 0.001, 1)) + + img.header.set_xyzt_units(t='sec') + with warnings.catch_warnings(record=True) as warns: + assert_array_equal(img.header.get_norm_zooms(), (1, 1, 1, 1)) + assert_equal(len(warns), 1) + assert_raises(ValueError, img.header.get_norm_zooms, True) + + img.header.set_xyzt_units(xyz='mm', t='msec') + assert_array_almost_equal(img.header.get_norm_zooms(), + (1, 1, 1, 0.001)) + + img.header.set_xyzt_units(xyz='mm', t='usec') + assert_array_almost_equal(img.header.get_norm_zooms(), + (1, 1, 1, 0.000001)) + + # Verify `set_norm_zooms` resets units + img.header.set_xyzt_units(xyz='meter', t='usec') + assert_equal(img.header.get_xyzt_units(), ('meter', 'usec')) + img.header.set_norm_zooms((2, 2, 2, 2.5)) + assert_array_almost_equal(img.header.get_norm_zooms(), (2, 2, 2, 2.5)) + assert_array_almost_equal(img.header.get_zooms(), (2, 2, 2, 2.5)) + assert_equal(img.header.get_xyzt_units(), ('mm', 'sec')) + class TestNifti1Image(TestNifti1Pair): # Run analyze-flavor spatialimage tests diff --git a/nibabel/tests/test_spatialimages.py b/nibabel/tests/test_spatialimages.py index fc11452151..5bca2b9030 100644 --- a/nibabel/tests/test_spatialimages.py +++ b/nibabel/tests/test_spatialimages.py @@ -528,6 +528,28 @@ def test_slicer(self): assert (sliced_data == img.get_data()[sliceobj]).all() assert (sliced_data == img.get_fdata()[sliceobj]).all() + def test_norm_zooms(self): + ''' Should be true for all images ''' + img_klass = self.image_class + arr = np.arange(120, dtype=np.int16).reshape((2, 3, 4, 5)) + aff = np.eye(4) + img = img_klass(arr, aff) + img.header.set_norm_zooms((2, 2, 2, 2.5)) + assert_array_equal(img.header.get_norm_zooms(), (2, 2, 2, 2.5)) + + def test_norm_zooms_edge_cases(self): + ''' Override for classes where *_norm_zooms != *_zooms ''' + img_klass = self.image_class + arr = np.arange(120, dtype=np.int16).reshape((2, 3, 4, 5)) + aff = np.eye(4) + img = img_klass(arr, aff) + img.header.set_zooms((2, 2, 2, 2.5)) + assert_array_equal(img.header.get_zooms(), (2, 2, 2, 2.5)) + assert_array_equal(img.header.get_norm_zooms(), (2, 2, 2, 2.5)) + img.header.set_norm_zooms((2, 2, 2, 2.5)) + assert_array_equal(img.header.get_zooms(), (2, 2, 2, 2.5)) + assert_array_equal(img.header.get_norm_zooms(), (2, 2, 2, 2.5)) + class MmapImageMixin: """ Mixin for testing images that may return memory maps """ From eb1d8ffc71eeaf40d641e02c5283d1fe69e8ac0f Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Thu, 21 Dec 2017 14:37:58 -0600 Subject: [PATCH 06/24] TEST: Fix warnings --- nibabel/tests/test_nifti1.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nibabel/tests/test_nifti1.py b/nibabel/tests/test_nifti1.py index e5fd6406cd..9a8d89046c 100644 --- a/nibabel/tests/test_nifti1.py +++ b/nibabel/tests/test_nifti1.py @@ -1182,8 +1182,10 @@ def test_norm_zooms_edge_cases(self): aff = np.eye(4) img = img_klass(arr, aff) + # Unknown units = 2 warnings with warnings.catch_warnings(record=True) as warns: + warnings.simplefilter('always') assert_array_almost_equal(img.header.get_norm_zooms(), (1, 1, 1, 1)) assert_equal(len(warns), 2) @@ -1191,6 +1193,7 @@ def test_norm_zooms_edge_cases(self): img.header.set_xyzt_units(xyz='meter') with warnings.catch_warnings(record=True) as warns: + warnings.simplefilter('always') assert_array_almost_equal(img.header.get_norm_zooms(), (1000, 1000, 1000, 1)) assert_equal(len(warns), 1) @@ -1205,6 +1208,7 @@ def test_norm_zooms_edge_cases(self): img.header.set_xyzt_units(t='sec') with warnings.catch_warnings(record=True) as warns: + warnings.simplefilter('always') assert_array_equal(img.header.get_norm_zooms(), (1, 1, 1, 1)) assert_equal(len(warns), 1) assert_raises(ValueError, img.header.get_norm_zooms, True) From 6ba8b66a7ac83c0657e572dde274f0ea449cc385 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Fri, 12 Jan 2018 17:11:10 -0500 Subject: [PATCH 07/24] RF: get_norm_zooms -> get_zooms(units="canonical") --- nibabel/analyze.py | 21 ++++++++----- nibabel/freesurfer/mghformat.py | 41 +++++++++++++++---------- nibabel/nifti1.py | 53 +++++++++++++++++++++++++++++++-- nibabel/spatialimages.py | 22 ++++++++++---- 4 files changed, 106 insertions(+), 31 deletions(-) diff --git a/nibabel/analyze.py b/nibabel/analyze.py index 3f1a730850..e059f5f3c9 100644 --- a/nibabel/analyze.py +++ b/nibabel/analyze.py @@ -661,13 +661,22 @@ def get_base_affine(self): get_best_affine = get_base_affine - def get_zooms(self): - """ Get zooms from header + def get_zooms(self, units='canonical', raise_unknown=False): + """ Get zooms (spacing between voxels along each axis) from header + + Parameters + ---------- + units : {'canonical', 'raw'}, optional + Return zooms in "canonical" units of mm/sec for spatial/temporal or + as raw values stored in header. + raise_unkown : bool, optional + If canonical units are requested and the units are ambiguous, raise + a ``ValueError`` Returns ------- - z : tuple - tuple of header zoom values + zooms : tuple + tuple of header zoom values Examples -------- @@ -706,10 +715,6 @@ def set_zooms(self, zooms): pixdims = hdr['pixdim'] pixdims[1:ndim + 1] = zooms[:] - def get_norm_zooms(self, raise_unknown=False): - ''' Get zooms in mm/s units ''' - return self.get_zooms() - def set_norm_zooms(self, zooms): ''' Set zooms in mm/s units ''' return self.set_zooms(zooms) diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index bf5a844368..d8232ca423 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -237,25 +237,45 @@ def _ndims(self): """ return 3 + (self._structarr['dims'][3] > 1) - def get_zooms(self): + def get_zooms(self, units='canonical', raise_unknown=False): """ 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 (see `The MGH/MGZ Volume Format - `_). + repetition time (TR). + TR is stored in milliseconds (see `The MGH/MGZ Volume Format + `_), + so if ``units == 'raw'``, the fourth zoom will be in ms. + If ``units == 'canonical'`` (default), the fourth zoom will be in + seconds. To access only the spatial zooms, use `hdr['delta']`. + Parameters + ---------- + units : {'canonical', 'raw'}, optional + Return zooms in "canonical" units of mm/sec for spatial/temporal or + as raw values stored in header. + raise_unkown : bool, optional + If canonical units are requested and the units are ambiguous, raise + a ``ValueError`` + Returns ------- - z : tuple - tuple of header zoom values + zooms : tuple + tuple of header zoom values .. _mghformat: https://surfer.nmr.mgh.harvard.edu/fswiki/FsTutorial/MghFormat#line-82 """ + if units == 'canonical': + tfactor = 0.001 + elif units == 'raw': + tfactor = 1 + else: + raise ValueError("`units` parameter must be 'canonical' or 'raw'") + # Do not return time zoom (TR) if 3D image - tzoom = (self['tr'],) if self._ndims() > 3 else () + tzoom = (self['tr'] * tfactor,) if self._ndims() > 3 else () return tuple(self._structarr['delta']) + tzoom def set_zooms(self, zooms): @@ -285,15 +305,6 @@ def set_zooms(self, zooms): raise HeaderDataError(f'TR must be non-negative; got {zooms[3]}') hdr['tr'] = zooms[3] - def get_norm_zooms(self, raise_unknown=False): - ''' Get zooms in mm/s units ''' - zooms = self.get_zooms() - - if len(zooms) == 4: - zooms = zooms[:3] + (zooms[3] / 1000,) - - return zooms - def set_norm_zooms(self, zooms): if len(zooms) == 4: zooms = zooms[:3] + (zooms[3] * 1000,) diff --git a/nibabel/nifti1.py b/nibabel/nifti1.py index df7997d486..1ce261d1da 100644 --- a/nibabel/nifti1.py +++ b/nibabel/nifti1.py @@ -1675,9 +1675,56 @@ def set_xyzt_units(self, xyz=None, t=None): t_code = unit_codes[t] self.structarr['xyzt_units'] = xyz_code + t_code - def get_norm_zooms(self, raise_unknown=False): - ''' Get zooms in mm/s units ''' - raw_zooms = self.get_zooms() + def get_zooms(self, units=None, raise_unknown=False): + ''' Get zooms (spacing between voxels along each axis) from header + + NIfTI1 headers may specify that zooms are encoded in units other than + mm and sec (see ``get_xyzt_units``). + Default behavior has been to return the raw zooms, and leave it to the + programmer to handle non-standard units. + However, most files indicate mm/sec units or have unspecified units, + and it is common practice to neglect specified units and assume all + files will be in mm/sec. + + The default behavior for ``get_zooms`` will remain to return the raw + zooms until version 4.0, when it will change to return zooms in + canonical mm/sec units. + Because the default behavior will change, a warning will be given to + prompt programmers to specify whether they intend to retrieve raw + values, or values coerced into canonical units. + + Parameters + ---------- + units : {'canonical', 'raw'} + Return zooms in "canonical" units of mm/sec for spatial/temporal or + as raw values stored in header. + raise_unkown : bool, optional + If canonical units are requested and the units are ambiguous, raise + a ``ValueError`` + + Returns + ------- + zooms : tuple + tuple of header zoom values + + ''' + if units is None: + units = 'raw' + warnings.warn('Units not specified in `{}.get_zooms`. Returning ' + 'raw zooms, but default will change to canonical.\n' + 'Please explicitly specify units parameter.' + ''.format(self.__class__.__name__), + FutureWarning, stacklevel=2) + + raw_zooms = super(Nifti1Header, self).get_zooms(units='raw', + raise_unknown=False) + + if units == 'raw': + return raw_zooms + + elif units != 'canonical': + raise ValueError("`units` parameter must be 'canonical' or 'raw'") + xyz_zooms = raw_zooms[:3] t_zoom = raw_zooms[3] if len(raw_zooms) > 3 else None diff --git a/nibabel/spatialimages.py b/nibabel/spatialimages.py index e2be3a47dc..b50d085ab2 100644 --- a/nibabel/spatialimages.py +++ b/nibabel/spatialimages.py @@ -224,7 +224,23 @@ def set_data_shape(self, shape): nzs = min(len(self._zooms), ndim) self._zooms = self._zooms[:nzs] + (1.0,) * (ndim - nzs) - def get_zooms(self): + def get_zooms(self, units='canonical', raise_unknown=False): + ''' Get zooms (spacing between voxels along each axis) from header + + Parameters + ---------- + units : {'canonical', 'raw'}, optional + Return zooms in "canonical" units of mm/sec for spatial/temporal or + as raw values stored in header. + raise_unkown : bool, optional + If canonical units are requested and the units are ambiguous, raise + a ``ValueError`` + + Returns + ------- + zooms : tuple + Spacing between voxels along each axis + ''' return self._zooms def set_zooms(self, zooms): @@ -238,10 +254,6 @@ def set_zooms(self, zooms): raise HeaderDataError('zooms must be positive') self._zooms = zooms - def get_norm_zooms(self, raise_unknown=False): - ''' Get zooms in mm/s units ''' - return self.get_zooms() - def set_norm_zooms(self, zooms): ''' Get zooms in mm/s units ''' return self.set_zooms(zooms) From 1357a3b8bd231e81f0e9a27ba466e85f53e742e4 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Fri, 12 Jan 2018 22:00:09 -0500 Subject: [PATCH 08/24] TEST: Update get_zooms tests --- nibabel/freesurfer/tests/test_mghformat.py | 39 +++++++++++++--------- nibabel/tests/test_nifti1.py | 32 ++++++++++-------- nibabel/tests/test_spatialimages.py | 14 ++++---- 3 files changed, 50 insertions(+), 35 deletions(-) diff --git a/nibabel/freesurfer/tests/test_mghformat.py b/nibabel/freesurfer/tests/test_mghformat.py index 886b06528e..9486add0c8 100644 --- a/nibabel/freesurfer/tests/test_mghformat.py +++ b/nibabel/freesurfer/tests/test_mghformat.py @@ -351,49 +351,58 @@ def check_dtypes(self, expected, actual): # MGH requires the actual to be a big endian version of expected assert expected.newbyteorder('>') == actual - def test_norm_zooms_edge_cases(self): + def test_zooms_edge_cases(self): img_klass = self.image_class aff = np.eye(4) arr = np.arange(120, dtype=np.int16).reshape((2, 3, 4, 5)) img = img_klass(arr, aff) - assert_array_almost_equal(img.header.get_zooms(), + assert_array_almost_equal(img.header.get_zooms(units='raw'), (1, 1, 1, 0)) - assert_array_almost_equal(img.header.get_norm_zooms(), + assert_array_almost_equal(img.header.get_zooms(units='canonical'), (1, 1, 1, 0)) img.header.set_zooms((1, 1, 1, 2000)) - assert_array_almost_equal(img.header.get_zooms(), + assert_array_almost_equal(img.header.get_zooms(units='raw'), (1, 1, 1, 2000)) - assert_array_almost_equal(img.header.get_norm_zooms(), + assert_array_almost_equal(img.header.get_zooms(units='canonical'), (1, 1, 1, 2)) + assert_array_almost_equal(img.header.get_zooms(), (1, 1, 1, 2)) img.header.set_norm_zooms((2, 2, 2, 3)) - assert_array_almost_equal(img.header.get_zooms(), + assert_array_almost_equal(img.header.get_zooms(units='raw'), (2, 2, 2, 3000)) - assert_array_almost_equal(img.header.get_norm_zooms(), + assert_array_almost_equal(img.header.get_zooms(units='canonical'), (2, 2, 2, 3)) + assert_array_almost_equal(img.header.get_zooms(), (2, 2, 2, 3)) # It's legal to set zooms for spatial dimensions only img.header.set_norm_zooms((3, 3, 3)) - assert_array_almost_equal(img.header.get_zooms(), + assert_array_almost_equal(img.header.get_zooms(units='raw'), (3, 3, 3, 3000)) - assert_array_almost_equal(img.header.get_norm_zooms(), + assert_array_almost_equal(img.header.get_zooms(units='canonical'), (3, 3, 3, 3)) + assert_array_almost_equal(img.header.get_zooms(), (3, 3, 3, 3)) arr = np.arange(24, dtype=np.int16).reshape((2, 3, 4)) img = img_klass(arr, aff) - assert_array_almost_equal(img.header.get_zooms(), (1, 1, 1)) - assert_array_almost_equal(img.header.get_norm_zooms(), (1, 1, 1)) + assert_array_almost_equal(img.header.get_zooms(units='raw'), + (1, 1, 1)) + assert_array_almost_equal(img.header.get_zooms(units='canonical'), + (1, 1, 1)) img.header.set_zooms((2, 2, 2)) - assert_array_almost_equal(img.header.get_zooms(), (2, 2, 2)) - assert_array_almost_equal(img.header.get_norm_zooms(), (2, 2, 2)) + assert_array_almost_equal(img.header.get_zooms(units='raw'), + (2, 2, 2)) + assert_array_almost_equal(img.header.get_zooms(units='canonical'), + (2, 2, 2)) img.header.set_norm_zooms((3, 3, 3)) - assert_array_almost_equal(img.header.get_zooms(), (3, 3, 3)) - assert_array_almost_equal(img.header.get_norm_zooms(), (3, 3, 3)) + assert_array_almost_equal(img.header.get_zooms(units='raw'), + (3, 3, 3)) + assert_array_almost_equal(img.header.get_zooms(units='canonical'), + (3, 3, 3)) # Cannot set TR as zoom for 3D image assert_raises(HeaderDataError, img.header.set_zooms, (4, 4, 4, 5)) diff --git a/nibabel/tests/test_nifti1.py b/nibabel/tests/test_nifti1.py index 9a8d89046c..f39e76d2af 100644 --- a/nibabel/tests/test_nifti1.py +++ b/nibabel/tests/test_nifti1.py @@ -1176,7 +1176,7 @@ def test_static_dtype_aliases(self): img_rt = bytesio_round_trip(img) assert img_rt.get_data_dtype() == effective_dt - def test_norm_zooms_edge_cases(self): + def test_zooms_edge_cases(self): img_klass = self.image_class arr = np.arange(120, dtype=np.int16).reshape((2, 3, 4, 5)) aff = np.eye(4) @@ -1186,47 +1186,53 @@ def test_norm_zooms_edge_cases(self): # Unknown units = 2 warnings with warnings.catch_warnings(record=True) as warns: warnings.simplefilter('always') - assert_array_almost_equal(img.header.get_norm_zooms(), + assert_array_almost_equal(img.header.get_zooms(units='canonical'), (1, 1, 1, 1)) assert_equal(len(warns), 2) - assert_raises(ValueError, img.header.get_norm_zooms, True) + assert_raises(ValueError, img.header.get_zooms, + units='canonical', raise_unknown=True) img.header.set_xyzt_units(xyz='meter') with warnings.catch_warnings(record=True) as warns: warnings.simplefilter('always') - assert_array_almost_equal(img.header.get_norm_zooms(), + assert_array_almost_equal(img.header.get_zooms(units='canonical'), (1000, 1000, 1000, 1)) assert_equal(len(warns), 1) - assert_raises(ValueError, img.header.get_norm_zooms, True) + assert_raises(ValueError, img.header.get_zooms, + units='canonical', raise_unknown=True) img.header.set_xyzt_units(xyz='mm', t='sec') - assert_array_almost_equal(img.header.get_norm_zooms(), + assert_array_almost_equal(img.header.get_zooms(units='canonical'), (1, 1, 1, 1)) img.header.set_xyzt_units(xyz='micron', t='sec') - assert_array_almost_equal(img.header.get_norm_zooms(), + assert_array_almost_equal(img.header.get_zooms(units='canonical'), (0.001, 0.001, 0.001, 1)) img.header.set_xyzt_units(t='sec') with warnings.catch_warnings(record=True) as warns: warnings.simplefilter('always') - assert_array_equal(img.header.get_norm_zooms(), (1, 1, 1, 1)) + assert_array_equal(img.header.get_zooms(units='canonical'), + (1, 1, 1, 1)) assert_equal(len(warns), 1) - assert_raises(ValueError, img.header.get_norm_zooms, True) + assert_raises(ValueError, img.header.get_zooms, + units='canonical', raise_unknown=True) img.header.set_xyzt_units(xyz='mm', t='msec') - assert_array_almost_equal(img.header.get_norm_zooms(), + assert_array_almost_equal(img.header.get_zooms(units='canonical'), (1, 1, 1, 0.001)) img.header.set_xyzt_units(xyz='mm', t='usec') - assert_array_almost_equal(img.header.get_norm_zooms(), + assert_array_almost_equal(img.header.get_zooms(units='canonical'), (1, 1, 1, 0.000001)) # Verify `set_norm_zooms` resets units img.header.set_xyzt_units(xyz='meter', t='usec') assert_equal(img.header.get_xyzt_units(), ('meter', 'usec')) img.header.set_norm_zooms((2, 2, 2, 2.5)) - assert_array_almost_equal(img.header.get_norm_zooms(), (2, 2, 2, 2.5)) - assert_array_almost_equal(img.header.get_zooms(), (2, 2, 2, 2.5)) + assert_array_almost_equal(img.header.get_zooms(units='canonical'), + (2, 2, 2, 2.5)) + assert_array_almost_equal(img.header.get_zooms(units='raw'), + (2, 2, 2, 2.5)) assert_equal(img.header.get_xyzt_units(), ('mm', 'sec')) diff --git a/nibabel/tests/test_spatialimages.py b/nibabel/tests/test_spatialimages.py index 5bca2b9030..b94947f6ee 100644 --- a/nibabel/tests/test_spatialimages.py +++ b/nibabel/tests/test_spatialimages.py @@ -528,27 +528,27 @@ def test_slicer(self): assert (sliced_data == img.get_data()[sliceobj]).all() assert (sliced_data == img.get_fdata()[sliceobj]).all() - def test_norm_zooms(self): + def test_zooms(self): ''' Should be true for all images ''' img_klass = self.image_class arr = np.arange(120, dtype=np.int16).reshape((2, 3, 4, 5)) aff = np.eye(4) img = img_klass(arr, aff) img.header.set_norm_zooms((2, 2, 2, 2.5)) - assert_array_equal(img.header.get_norm_zooms(), (2, 2, 2, 2.5)) + assert_array_equal(img.header.get_zooms(units='canonical'), (2, 2, 2, 2.5)) - def test_norm_zooms_edge_cases(self): + def test_zooms_edge_cases(self): ''' Override for classes where *_norm_zooms != *_zooms ''' img_klass = self.image_class arr = np.arange(120, dtype=np.int16).reshape((2, 3, 4, 5)) aff = np.eye(4) img = img_klass(arr, aff) img.header.set_zooms((2, 2, 2, 2.5)) - assert_array_equal(img.header.get_zooms(), (2, 2, 2, 2.5)) - assert_array_equal(img.header.get_norm_zooms(), (2, 2, 2, 2.5)) + assert_array_equal(img.header.get_zooms(units='raw'), (2, 2, 2, 2.5)) + assert_array_equal(img.header.get_zooms(units='canonical'), (2, 2, 2, 2.5)) img.header.set_norm_zooms((2, 2, 2, 2.5)) - assert_array_equal(img.header.get_zooms(), (2, 2, 2, 2.5)) - assert_array_equal(img.header.get_norm_zooms(), (2, 2, 2, 2.5)) + assert_array_equal(img.header.get_zooms(units='raw'), (2, 2, 2, 2.5)) + assert_array_equal(img.header.get_zooms(units='canonical'), (2, 2, 2, 2.5)) class MmapImageMixin: From 096da887a0701baa52c768d8141e1fef170be213 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Fri, 12 Jan 2018 22:28:41 -0500 Subject: [PATCH 09/24] FIX: Set default t_code even if no t_zoom --- nibabel/nifti1.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nibabel/nifti1.py b/nibabel/nifti1.py index 1ce261d1da..3632029962 100644 --- a/nibabel/nifti1.py +++ b/nibabel/nifti1.py @@ -1733,8 +1733,9 @@ def get_zooms(self, units=None, raise_unknown=False): if xyz_code == 'unknown': xyz_msg = 'Unknown spatial units' xyz_code = 'mm' - if t_code == 'unknown' and t_zoom is not None: - t_msg = 'Unknown time units' + if t_code == 'unknown': + if t_zoom is not None: + t_msg = 'Unknown time units' t_code = 'sec' if raise_unknown and (xyz_msg, t_msg) != ('', ''): if xyz_msg and t_msg: From 798fc4e655c4740c31660ccda6e0737a17e8f35c Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Mon, 19 Feb 2018 10:24:03 -0500 Subject: [PATCH 10/24] TEST: Add units specification to tests --- nibabel/analyze.py | 2 +- nibabel/nifti1.py | 2 +- nibabel/tests/test_analyze.py | 22 ++++++++++++++-------- nibabel/tests/test_nifti1.py | 22 +++++++++++----------- 4 files changed, 27 insertions(+), 21 deletions(-) diff --git a/nibabel/analyze.py b/nibabel/analyze.py index e059f5f3c9..2cb445328f 100644 --- a/nibabel/analyze.py +++ b/nibabel/analyze.py @@ -399,7 +399,7 @@ def from_header(klass, header=None, check=True): f"but output header {klass} does not support it") obj.set_data_dtype(header.get_data_dtype()) obj.set_data_shape(header.get_data_shape()) - obj.set_zooms(header.get_zooms()) + obj.set_zooms(header.get_zooms(units='raw')) if check: obj.check_fix() return obj diff --git a/nibabel/nifti1.py b/nibabel/nifti1.py index 3632029962..1f3125696e 100644 --- a/nibabel/nifti1.py +++ b/nibabel/nifti1.py @@ -784,7 +784,7 @@ def get_data_shape(self): Expanding number of dimensions gets default zooms - >>> hdr.get_zooms() + >>> hdr.get_zooms(units='canonical') (1.0, 1.0, 1.0) Notes diff --git a/nibabel/tests/test_analyze.py b/nibabel/tests/test_analyze.py index d91769bc73..10a3396e96 100644 --- a/nibabel/tests/test_analyze.py +++ b/nibabel/tests/test_analyze.py @@ -91,7 +91,8 @@ def test_general_init(self): assert_array_equal(np.diag(hdr.get_base_affine()), [-1, 1, 1, 1]) # But zooms only go with number of dimensions - assert hdr.get_zooms() == (1.0,) + assert hdr.get_zooms(units='raw') == (1.0,) + assert hdr.get_zooms(units='canonical') == (1.0,) def test_header_size(self): assert self.header_class.template_dtype.itemsize == self.sizeof_hdr @@ -437,7 +438,8 @@ def test_data_shape_zooms_affine(self): else: assert hdr.get_data_shape() == (0,) # Default zoom - for 3D - is 1(()) - assert hdr.get_zooms() == (1,) * L + assert hdr.get_zooms(units='raw') == (1,) * L + assert hdr.get_zooms(units='canonical') == (1,) * L # errors if zooms do not match shape if len(shape): with pytest.raises(HeaderDataError): @@ -455,11 +457,14 @@ def test_data_shape_zooms_affine(self): hdr = self.header_class() hdr.set_data_shape((1, 2, 3)) hdr.set_zooms((4, 5, 6)) - assert_array_equal(hdr.get_zooms(), (4, 5, 6)) + assert_array_equal(hdr.get_zooms(units='raw'), (4, 5, 6)) + assert_array_equal(hdr.get_zooms(units='canonical'), (4, 5, 6)) hdr.set_data_shape((1, 2)) - assert_array_equal(hdr.get_zooms(), (4, 5)) + assert_array_equal(hdr.get_zooms(units='raw'), (4, 5)) + assert_array_equal(hdr.get_zooms(units='canonical'), (4, 5)) hdr.set_data_shape((1, 2, 3)) - assert_array_equal(hdr.get_zooms(), (4, 5, 1)) + assert_array_equal(hdr.get_zooms(units='raw'), (4, 5, 1)) + assert_array_equal(hdr.get_zooms(units='canonical'), (4, 5, 1)) # Setting zooms changes affine assert_array_equal(np.diag(hdr.get_base_affine()), [-4, 5, 1, 1]) @@ -529,12 +534,13 @@ def get_data_dtype(self): return np.dtype('i2') def get_data_shape(self): return (5, 4, 3) - def get_zooms(self): return (10.0, 9.0, 8.0) + def get_zooms(self, units=None, raise_unknown=None): return (10.0, 9.0, 8.0) converted = klass.from_header(C()) assert isinstance(converted, klass) assert converted.get_data_dtype() == np.dtype('i2') assert converted.get_data_shape() == (5, 4, 3) - assert converted.get_zooms() == (10.0, 9.0, 8.0) + assert converted.get_zooms(units='raw') == (10.0, 9.0, 8.0) + assert converted.get_zooms(units='canonical') == (10.0, 9.0, 8.0) def test_base_affine(self): klass = self.header_class @@ -640,7 +646,7 @@ def get_data_shape(self): class H4(H3): - def get_zooms(self): + def get_zooms(self, units=None, raise_unknown=None): return 4., 5., 6. exp_hdr = klass() exp_hdr.set_data_dtype(np.dtype('u1')) diff --git a/nibabel/tests/test_nifti1.py b/nibabel/tests/test_nifti1.py index f39e76d2af..1eba32e833 100644 --- a/nibabel/tests/test_nifti1.py +++ b/nibabel/tests/test_nifti1.py @@ -881,11 +881,11 @@ def test_set_qform(self): img.set_qform(new_affine, 1, update_affine=False) assert_array_almost_equal(img.affine, aff_affine) # Clear qform using None, zooms unchanged - assert_array_almost_equal(hdr.get_zooms(), [1.1, 1.1, 1.1]) + assert_array_almost_equal(hdr.get_zooms(units='raw'), [1.1, 1.1, 1.1]) img.set_qform(None) qaff, code = img.get_qform(coded=True) assert (qaff, code) == (None, 0) - assert_array_almost_equal(hdr.get_zooms(), [1.1, 1.1, 1.1]) + assert_array_almost_equal(hdr.get_zooms(units='raw'), [1.1, 1.1, 1.1]) # Best affine similarly assert_array_almost_equal(img.affine, hdr.get_best_affine()) # If sform is not set, qform should update affine @@ -942,9 +942,9 @@ def test_set_sform(self): assert_array_almost_equal(img.affine, aff_affine) # zooms do not get updated when qform is 0 assert_array_almost_equal(img.get_qform(), orig_aff) - assert_array_almost_equal(hdr.get_zooms(), [2.2, 3.3, 4.3]) + assert_array_almost_equal(hdr.get_zooms(units='raw'), [2.2, 3.3, 4.3]) img.set_qform(None) - assert_array_almost_equal(hdr.get_zooms(), [2.2, 3.3, 4.3]) + assert_array_almost_equal(hdr.get_zooms(units='raw'), [2.2, 3.3, 4.3]) # Set sform using new_affine when qform is set img.set_qform(qform_affine, 1) img.set_sform(new_affine, 1) @@ -953,7 +953,7 @@ def test_set_sform(self): assert_array_almost_equal(saff, new_affine) assert_array_almost_equal(img.affine, new_affine) # zooms follow qform - assert_array_almost_equal(hdr.get_zooms(), [1.2, 1.2, 1.2]) + assert_array_almost_equal(hdr.get_zooms(units='raw'), [1.2, 1.2, 1.2]) # Clear sform using None, best_affine should fall back on qform img.set_sform(None) assert hdr['sform_code'] == 0 @@ -1042,7 +1042,7 @@ def test_load_pixdims(self): # Check qform, sform, pixdims are the same assert_array_equal(img_hdr.get_qform(), qaff) assert_array_equal(img_hdr.get_sform(), saff) - assert_array_equal(img_hdr.get_zooms(), [2, 3, 4]) + assert_array_equal(img_hdr.get_zooms(units='raw'), [2, 3, 4]) # Save to stringio re_simg = bytesio_round_trip(simg) assert_array_equal(re_simg.get_fdata(), arr) @@ -1050,7 +1050,7 @@ def test_load_pixdims(self): rimg_hdr = re_simg.header assert_array_equal(rimg_hdr.get_qform(), qaff) assert_array_equal(rimg_hdr.get_sform(), saff) - assert_array_equal(rimg_hdr.get_zooms(), [2, 3, 4]) + assert_array_equal(rimg_hdr.get_zooms(units='raw'), [2, 3, 4]) def test_affines_init(self): # Test we are doing vaguely spec-related qform things. The 'spec' here @@ -1064,20 +1064,20 @@ def test_affines_init(self): hdr = img.header assert hdr['qform_code'] == 0 assert hdr['sform_code'] == 2 - assert_array_equal(hdr.get_zooms(), [2, 3, 4]) + assert_array_equal(hdr.get_zooms(units='raw'), [2, 3, 4]) # This is also true for affines with header passed qaff = np.diag([3, 4, 5, 1]) saff = np.diag([6, 7, 8, 1]) hdr.set_qform(qaff, code='scanner') hdr.set_sform(saff, code='talairach') - assert_array_equal(hdr.get_zooms(), [3, 4, 5]) + assert_array_equal(hdr.get_zooms(units='raw'), [3, 4, 5]) img = IC(arr, aff, hdr) new_hdr = img.header # Again affine is sort of anonymous space assert new_hdr['qform_code'] == 0 assert new_hdr['sform_code'] == 2 assert_array_equal(new_hdr.get_sform(), aff) - assert_array_equal(new_hdr.get_zooms(), [2, 3, 4]) + assert_array_equal(new_hdr.get_zooms(units='raw'), [2, 3, 4]) # But if no affine passed, codes and matrices stay the same img = IC(arr, None, hdr) new_hdr = img.header @@ -1086,7 +1086,7 @@ def test_affines_init(self): assert new_hdr['sform_code'] == 3 # Still talairach assert_array_equal(new_hdr.get_sform(), saff) # Pixdims as in the original header - assert_array_equal(new_hdr.get_zooms(), [3, 4, 5]) + assert_array_equal(new_hdr.get_zooms(units='raw'), [3, 4, 5]) def test_read_no_extensions(self): IC = self.image_class From f31aba5bc58ca7f84560c6874fb058ae3b755bc8 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Mon, 19 Feb 2018 10:24:47 -0500 Subject: [PATCH 11/24] TEST: Add setup/teardown to try to catch warnings properly [WIP] --- nibabel/tests/test_nifti1.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/nibabel/tests/test_nifti1.py b/nibabel/tests/test_nifti1.py index 1eba32e833..7a8f133d6d 100644 --- a/nibabel/tests/test_nifti1.py +++ b/nibabel/tests/test_nifti1.py @@ -83,6 +83,18 @@ class TestNifti1PairHeader(tana.TestAnalyzeHeader, tspm.HeaderScalingMixin): np.longcomplex)) tana.add_intp(supported_np_types) + def setUp(self): + # Add warning filters for duration of test case + self._wctx = warnings.catch_warnings() + self._wctx.__enter__() + warnings.filterwarnings('ignore', 'get_zooms', FutureWarning) + warnings.filterwarnings('ignore', 'Unknown (spatial|time) units', + UserWarning) + + def tearDown(self): + # Restore warning filters + self._wctx.__exit__() + def test_empty(self): tana.TestAnalyzeHeader.test_empty(self) hdr = self.header_class() @@ -765,6 +777,18 @@ class TestNifti1Pair(tana.TestAnalyzeImage, tspm.ImageScalingMixin): image_class = Nifti1Pair supported_np_types = TestNifti1PairHeader.supported_np_types + def setUp(self): + # Add warning filters for duration of test case + self._wctx = warnings.catch_warnings() + self._wctx.__enter__() + warnings.filterwarnings('ignore', 'get_zooms', FutureWarning) + warnings.filterwarnings('ignore', 'Unknown (spatial|time) units', + UserWarning) + + def tearDown(self): + # Restore warning filters + self._wctx.__exit__() + def test_int64_warning(self): # Verify that initializing with (u)int64 data and no # header/dtype info produces a warning From 27234c3d637e3ad591bd929c43199bad616f63cf Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Mon, 19 Feb 2018 10:25:03 -0500 Subject: [PATCH 12/24] TEST: Try using clear_and_catch_warnings --- nibabel/tests/test_nifti1.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nibabel/tests/test_nifti1.py b/nibabel/tests/test_nifti1.py index 7a8f133d6d..3fbba449a0 100644 --- a/nibabel/tests/test_nifti1.py +++ b/nibabel/tests/test_nifti1.py @@ -1208,7 +1208,7 @@ def test_zooms_edge_cases(self): # Unknown units = 2 warnings - with warnings.catch_warnings(record=True) as warns: + with clear_and_catch_warnings() as warns: warnings.simplefilter('always') assert_array_almost_equal(img.header.get_zooms(units='canonical'), (1, 1, 1, 1)) @@ -1217,7 +1217,7 @@ def test_zooms_edge_cases(self): units='canonical', raise_unknown=True) img.header.set_xyzt_units(xyz='meter') - with warnings.catch_warnings(record=True) as warns: + with clear_and_catch_warnings() as warns: warnings.simplefilter('always') assert_array_almost_equal(img.header.get_zooms(units='canonical'), (1000, 1000, 1000, 1)) @@ -1233,7 +1233,7 @@ def test_zooms_edge_cases(self): (0.001, 0.001, 0.001, 1)) img.header.set_xyzt_units(t='sec') - with warnings.catch_warnings(record=True) as warns: + with clear_and_catch_warnings() as warns: warnings.simplefilter('always') assert_array_equal(img.header.get_zooms(units='canonical'), (1, 1, 1, 1)) From 8899bc71745d69daa2d9ec3e941796af04f6a121 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Sun, 11 Mar 2018 14:06:44 -1000 Subject: [PATCH 13/24] RF: Add units parameter to set_zooms - Switch 'canonical' to 'norm' for brevity --- nibabel/analyze.py | 25 +++-- nibabel/freesurfer/mghformat.py | 45 +++++---- nibabel/freesurfer/tests/test_mghformat.py | 35 +++---- nibabel/nifti1.py | 107 +++++++++++++++++---- nibabel/spatialimages.py | 29 +++--- nibabel/tests/test_analyze.py | 36 +++---- nibabel/tests/test_nifti1.py | 26 ++--- nibabel/tests/test_spatialimages.py | 14 +-- 8 files changed, 203 insertions(+), 114 deletions(-) diff --git a/nibabel/analyze.py b/nibabel/analyze.py index 2cb445328f..40a1971a86 100644 --- a/nibabel/analyze.py +++ b/nibabel/analyze.py @@ -399,7 +399,7 @@ def from_header(klass, header=None, check=True): f"but output header {klass} does not support it") obj.set_data_dtype(header.get_data_dtype()) obj.set_data_shape(header.get_data_shape()) - obj.set_zooms(header.get_zooms(units='raw')) + obj.set_zooms(header.get_zooms(units='raw'), units='raw') if check: obj.check_fix() return obj @@ -661,16 +661,16 @@ def get_base_affine(self): get_best_affine = get_base_affine - def get_zooms(self, units='canonical', raise_unknown=False): + def get_zooms(self, units='norm', raise_unknown=False): """ Get zooms (spacing between voxels along each axis) from header Parameters ---------- - units : {'canonical', 'raw'}, optional - Return zooms in "canonical" units of mm/sec for spatial/temporal or + units : {'norm', 'raw'}, optional + Return zooms in normalized units of mm/sec for spatial/temporal or as raw values stored in header. raise_unkown : bool, optional - If canonical units are requested and the units are ambiguous, raise + If normalized units are requested and the units are ambiguous, raise a ``ValueError`` Returns @@ -698,10 +698,19 @@ def get_zooms(self, units='canonical', raise_unknown=False): pixdims = hdr['pixdim'] return tuple(pixdims[1:ndim + 1]) - def set_zooms(self, zooms): + def set_zooms(self, zooms, units='norm'): """ Set zooms into header fields See docstring for ``get_zooms`` for examples + + Parameters + ---------- + zooms : sequence of floats + Zoom values to set in header + units : {'norm', 'raw'}, optional + Zooms are specified in normalized units of mm/sec for + spatial/temporal or as raw values to be interpreted according to + format specification. """ hdr = self._structarr dims = hdr['dim'] @@ -715,10 +724,6 @@ def set_zooms(self, zooms): pixdims = hdr['pixdim'] pixdims[1:ndim + 1] = zooms[:] - def set_norm_zooms(self, zooms): - ''' Set zooms in mm/s units ''' - return self.set_zooms(zooms) - def as_analyze_map(self): """ Return header as mapping for conversion to Analyze types diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index d8232ca423..b277411486 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -237,7 +237,7 @@ def _ndims(self): """ return 3 + (self._structarr['dims'][3] > 1) - def get_zooms(self, units='canonical', raise_unknown=False): + def get_zooms(self, units='norm', raise_unknown=False): """ Get zooms from header Returns the spacing of voxels in the x, y, and z dimensions. @@ -246,18 +246,18 @@ def get_zooms(self, units='canonical', raise_unknown=False): TR is stored in milliseconds (see `The MGH/MGZ Volume Format `_), so if ``units == 'raw'``, the fourth zoom will be in ms. - If ``units == 'canonical'`` (default), the fourth zoom will be in + If ``units == 'norm'`` (default), the fourth zoom will be in seconds. To access only the spatial zooms, use `hdr['delta']`. Parameters ---------- - units : {'canonical', 'raw'}, optional - Return zooms in "canonical" units of mm/sec for spatial/temporal or + units : {'norm', 'raw'}, optional + Return zooms in normalized units of mm/sec for spatial/temporal or as raw values stored in header. raise_unkown : bool, optional - If canonical units are requested and the units are ambiguous, raise + If normalized units are requested and the units are ambiguous, raise a ``ValueError`` Returns @@ -267,29 +267,40 @@ def get_zooms(self, units='canonical', raise_unknown=False): .. _mghformat: https://surfer.nmr.mgh.harvard.edu/fswiki/FsTutorial/MghFormat#line-82 """ - if units == 'canonical': + if units == 'norm': tfactor = 0.001 elif units == 'raw': tfactor = 1 else: - raise ValueError("`units` parameter must be 'canonical' or 'raw'") + raise ValueError("`units` parameter must be 'norm' or 'raw'") # Do not return time zoom (TR) if 3D image tzoom = (self['tr'] * tfactor,) if self._ndims() > 3 else () return tuple(self._structarr['delta']) + tzoom - def set_zooms(self, zooms): + def set_zooms(self, zooms, units='norm'): """ Set zooms into header fields 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. + For four-dimensional files, a temporal zoom (repetition time, or TR) + may be provided as a fourth sequence element. + + TR is stored in milliseconds (see `The MGH/MGZ Volume Format + `_), + so if ``units == 'raw'``, the fourth zoom will be interpreted as ms + and stored unmodified. + If ``units == 'norm'`` (default), the fourth zoom will be interpreted + as seconds, and converted to ms before storing in the header. Parameters ---------- zooms : sequence sequence of floats specifying spatial and (optionally) temporal zooms + units : {'norm', 'raw'}, optional + Zooms are specified in normalized units of mm/sec for + spatial/temporal dimensions or as raw values to be stored in + header. """ hdr = self._structarr zooms = np.asarray(zooms) @@ -303,13 +314,13 @@ def set_zooms(self, zooms): if len(zooms) == 4: if zooms[3] < 0: raise HeaderDataError(f'TR must be non-negative; got {zooms[3]}') - hdr['tr'] = zooms[3] - - def set_norm_zooms(self, zooms): - if len(zooms) == 4: - zooms = zooms[:3] + (zooms[3] * 1000,) - - self.set_zooms(zooms) + if units == 'norm': + tfactor = 1000 + elif units == 'raw': + tfactor = 1 + else: + raise ValueError("`units` parameter must be 'norm' or 'raw'") + hdr['tr'] = zooms[3] * tfactor def get_data_shape(self): """ Get shape of data diff --git a/nibabel/freesurfer/tests/test_mghformat.py b/nibabel/freesurfer/tests/test_mghformat.py index 9486add0c8..f2addd26a7 100644 --- a/nibabel/freesurfer/tests/test_mghformat.py +++ b/nibabel/freesurfer/tests/test_mghformat.py @@ -70,11 +70,12 @@ def test_read_mgh(): assert h['dof'] == 0 assert h['goodRASFlag'] == 1 assert_array_equal(h['dims'], [3, 4, 5, 2]) - assert_almost_equal(h['tr'], 2.0) + assert_almost_equal(h['tr'], 2) 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_zooms(units='raw'), [1, 1, 1, 2]) + assert_array_almost_equal(h.get_zooms(units='norm'), [1, 1, 1, 0.002]) assert_array_almost_equal(h.get_vox2ras(), v2r) assert_array_almost_equal(h.get_vox2ras_tkr(), v2rtkr) @@ -147,7 +148,7 @@ def test_write_noaffine_mgh(): def test_set_zooms(): mgz = load(MGZ_FNAME) h = mgz.header - assert_array_almost_equal(h.get_zooms(), [1, 1, 1, 2]) + assert_array_almost_equal(h.get_zooms(), [1, 1, 1, 0.002]) 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), @@ -359,28 +360,28 @@ def test_zooms_edge_cases(self): assert_array_almost_equal(img.header.get_zooms(units='raw'), (1, 1, 1, 0)) - assert_array_almost_equal(img.header.get_zooms(units='canonical'), + assert_array_almost_equal(img.header.get_zooms(units='norm'), (1, 1, 1, 0)) - img.header.set_zooms((1, 1, 1, 2000)) + img.header.set_zooms((1, 1, 1, 2000), units='raw') assert_array_almost_equal(img.header.get_zooms(units='raw'), (1, 1, 1, 2000)) - assert_array_almost_equal(img.header.get_zooms(units='canonical'), + assert_array_almost_equal(img.header.get_zooms(units='norm'), (1, 1, 1, 2)) assert_array_almost_equal(img.header.get_zooms(), (1, 1, 1, 2)) - img.header.set_norm_zooms((2, 2, 2, 3)) + img.header.set_zooms((2, 2, 2, 3), units='norm') assert_array_almost_equal(img.header.get_zooms(units='raw'), (2, 2, 2, 3000)) - assert_array_almost_equal(img.header.get_zooms(units='canonical'), + assert_array_almost_equal(img.header.get_zooms(units='norm'), (2, 2, 2, 3)) assert_array_almost_equal(img.header.get_zooms(), (2, 2, 2, 3)) # It's legal to set zooms for spatial dimensions only - img.header.set_norm_zooms((3, 3, 3)) + img.header.set_zooms((3, 3, 3), units='norm') assert_array_almost_equal(img.header.get_zooms(units='raw'), (3, 3, 3, 3000)) - assert_array_almost_equal(img.header.get_zooms(units='canonical'), + assert_array_almost_equal(img.header.get_zooms(units='norm'), (3, 3, 3, 3)) assert_array_almost_equal(img.header.get_zooms(), (3, 3, 3, 3)) @@ -389,24 +390,24 @@ def test_zooms_edge_cases(self): assert_array_almost_equal(img.header.get_zooms(units='raw'), (1, 1, 1)) - assert_array_almost_equal(img.header.get_zooms(units='canonical'), + assert_array_almost_equal(img.header.get_zooms(units='norm'), (1, 1, 1)) - img.header.set_zooms((2, 2, 2)) + img.header.set_zooms((2, 2, 2), units='raw') assert_array_almost_equal(img.header.get_zooms(units='raw'), (2, 2, 2)) - assert_array_almost_equal(img.header.get_zooms(units='canonical'), + assert_array_almost_equal(img.header.get_zooms(units='norm'), (2, 2, 2)) - img.header.set_norm_zooms((3, 3, 3)) + img.header.set_zooms((3, 3, 3), units='norm') assert_array_almost_equal(img.header.get_zooms(units='raw'), (3, 3, 3)) - assert_array_almost_equal(img.header.get_zooms(units='canonical'), + assert_array_almost_equal(img.header.get_zooms(units='norm'), (3, 3, 3)) # Cannot set TR as zoom for 3D image - assert_raises(HeaderDataError, img.header.set_zooms, (4, 4, 4, 5)) - assert_raises(HeaderDataError, img.header.set_norm_zooms, (4, 4, 4, 5)) + assert_raises(HeaderDataError, img.header.set_zooms, (4, 4, 4, 5), 'raw') + assert_raises(HeaderDataError, img.header.set_zooms, (4, 4, 4, 5), 'norm') class TestMGHHeader(tws._TestLabeledWrapStruct): diff --git a/nibabel/nifti1.py b/nibabel/nifti1.py index 1f3125696e..738aa079da 100644 --- a/nibabel/nifti1.py +++ b/nibabel/nifti1.py @@ -784,7 +784,7 @@ def get_data_shape(self): Expanding number of dimensions gets default zooms - >>> hdr.get_zooms(units='canonical') + >>> hdr.get_zooms(units='norm') (1.0, 1.0, 1.0) Notes @@ -1678,7 +1678,7 @@ def set_xyzt_units(self, xyz=None, t=None): def get_zooms(self, units=None, raise_unknown=False): ''' Get zooms (spacing between voxels along each axis) from header - NIfTI1 headers may specify that zooms are encoded in units other than + NIfTI-1 headers may specify that zooms are encoded in units other than mm and sec (see ``get_xyzt_units``). Default behavior has been to return the raw zooms, and leave it to the programmer to handle non-standard units. @@ -1688,18 +1688,18 @@ def get_zooms(self, units=None, raise_unknown=False): The default behavior for ``get_zooms`` will remain to return the raw zooms until version 4.0, when it will change to return zooms in - canonical mm/sec units. + normalized mm/sec units. Because the default behavior will change, a warning will be given to prompt programmers to specify whether they intend to retrieve raw - values, or values coerced into canonical units. + values, or values coerced into normalized units. Parameters ---------- - units : {'canonical', 'raw'} - Return zooms in "canonical" units of mm/sec for spatial/temporal or + units : {'norm', 'raw'} + Return zooms in normalized units of mm/sec for spatial/temporal or as raw values stored in header. raise_unkown : bool, optional - If canonical units are requested and the units are ambiguous, raise + If normalized units are requested and the units are ambiguous, raise a ``ValueError`` Returns @@ -1711,7 +1711,7 @@ def get_zooms(self, units=None, raise_unknown=False): if units is None: units = 'raw' warnings.warn('Units not specified in `{}.get_zooms`. Returning ' - 'raw zooms, but default will change to canonical.\n' + 'raw zooms, but default will change to normalized.\n' 'Please explicitly specify units parameter.' ''.format(self.__class__.__name__), FutureWarning, stacklevel=2) @@ -1722,8 +1722,8 @@ def get_zooms(self, units=None, raise_unknown=False): if units == 'raw': return raw_zooms - elif units != 'canonical': - raise ValueError("`units` parameter must be 'canonical' or 'raw'") + elif units != 'norm': + raise ValueError("`units` parameter must be 'norm' or 'raw'") xyz_zooms = raw_zooms[:3] t_zoom = raw_zooms[3] if len(raw_zooms) > 3 else None @@ -1733,10 +1733,13 @@ def get_zooms(self, units=None, raise_unknown=False): if xyz_code == 'unknown': xyz_msg = 'Unknown spatial units' xyz_code = 'mm' - if t_code == 'unknown': - if t_zoom is not None: + if t_zoom is not None: + if t_code == 'unknown': t_msg = 'Unknown time units' - t_code = 'sec' + t_code = 'sec' + elif t_code in ('hz', 'ppm', 'rads'): + t_msg = 'Unconvertible temporal units: {}'.format(t_code) + if raise_unknown and (xyz_msg, t_msg) != ('', ''): if xyz_msg and t_msg: msg = 'Unknown spatial and time units' @@ -1746,20 +1749,82 @@ def get_zooms(self, units=None, raise_unknown=False): if xyz_msg: warnings.warn('{} - assuming mm'.format(xyz_msg)) if t_msg: - warnings.warn('{} - assuming sec'.format(t_msg)) + if t_code == 'sec': + warnings.warn('{} - assuming sec'.format(t_msg)) + else: + warnings.warn(t_msg) xyz_factor = {'meter': 1000, 'mm': 1, 'micron': 0.001}[xyz_code] - t_factor = {'sec': 1, 'msec': 0.001, 'usec': 0.000001}[t_code] - xyz_zooms = tuple(np.array(xyz_zooms) * xyz_factor) - t_zoom = (t_zoom * t_factor,) if t_zoom is not None else () + + if t_zoom is not None: + t_factor = {'sec': 1, 'msec': 0.001, 'usec': 0.000001, + 'hz': 1, 'ppm': 1, 'rads': 1}[t_code] + t_zoom = (t_zoom * t_factor,) + else: + t_zoom = () return xyz_zooms + t_zoom - def set_norm_zooms(self, zooms): - ''' Set zooms in mm/s units ''' - self.set_zooms(zooms) - self.set_xyzt_units('mm', 'sec') + def set_zooms(self, zooms, units=None): + ''' Set zooms into header fields + + NIfTI-1 headers may specify that zooms are encoded in units other than + mm and sec. + Default behavior has been to set the raw zooms, and leave unit handling + to a separate method (``set_xyzt_units``). + However, most files indicate mm/sec units or have unspecified units, + and it is common practice to neglect specified units and assume all + files will be in mm/sec. + + The default behavior for ``set_zooms`` will remain to update only the + zooms until version 4.0, when it will change to setting units in + normalized mm/sec units. + Because the default behavior will change, a warning will be given to + prompt programmers to specify whether they intend to set only the raw + values, or set normalized units, as well. + + For setting normalized units, the following rules apply: + Spatial units will be set to mm. + The temporal code for a <4D image is considered "unknown". + If the current temporal units are Hz, PPM or Rads, these are left + unchanged. + Otherwise, the fourth zoom is considered to be in seconds. + + Parameters + ---------- + zooms : sequence of floats + Zoom values to set in header + units : {'norm', 'raw', tuple of unit codes}, optional + Zooms are specified in normalized units of mm/sec for + spatial/temporal, as raw values to be interpreted according to + format specification, or according to a tuple of NIFTI unit + codes. + + ''' + if units is None: + units = 'raw' + warnings.warn('Units not specified in `{}.set_zooms`. Setting ' + 'raw zooms, but default will change to normalized.\n' + 'Please explicitly specify units parameter.' + ''.format(self.__class__.__name__), + FutureWarning, stacklevel=2) + + super(Nifti1Header, self).set_zooms(zooms, units=units) + + if isinstance(units, tuple): + self.set_xyzt_units(*units) + elif units == 'norm': + _, t_code = self.get_xyzt_units() + xyz_code = 'mm' + if len(zooms) == 3: + t_code = 'unknown' + elif len(zooms) > 3 and t_code in ('unknown', 'sec', 'msec', 'usec'): + t_code = 'sec' + self.set_xyzt_units(xyz_code, t_code) + elif units != 'raw': + raise ValueError("`units` parameter must be 'norm', 'raw'," + " or a tuple of unit codes (see set_xyzt_units)") def _clean_after_mapping(self): """ Set format-specific stuff after converting header from mapping diff --git a/nibabel/spatialimages.py b/nibabel/spatialimages.py index b50d085ab2..f7fe66d231 100644 --- a/nibabel/spatialimages.py +++ b/nibabel/spatialimages.py @@ -224,15 +224,15 @@ def set_data_shape(self, shape): nzs = min(len(self._zooms), ndim) self._zooms = self._zooms[:nzs] + (1.0,) * (ndim - nzs) - def get_zooms(self, units='canonical', raise_unknown=False): + def get_zooms(self, units='norm', raise_unknown=False): ''' Get zooms (spacing between voxels along each axis) from header Parameters ---------- - units : {'canonical', 'raw'}, optional - Return zooms in "canonical" units of mm/sec for spatial/temporal or + units : {'norm', 'raw'}, optional + Return zooms in normalized units of mm/sec for spatial/temporal or as raw values stored in header. - raise_unkown : bool, optional + raise_unknown : bool, optional If canonical units are requested and the units are ambiguous, raise a ``ValueError`` @@ -243,21 +243,28 @@ def get_zooms(self, units='canonical', raise_unknown=False): ''' return self._zooms - def set_zooms(self, zooms): - zooms = tuple([float(z) for z in zooms]) + def set_zooms(self, zooms, units='norm'): + ''' Set zooms (spacing between voxels along each axis) in header + + Parameters + ---------- + zooms : sequence of floats + Spacing between voxels along each axis + units : {'norm', 'raw'}, optional + Zooms are specified in normalized units of mm/sec for + spatial/temporal or as raw values to be interpreted according to + format specification. + ''' + zooms = tuple(float(z) for z in zooms) shape = self.get_data_shape() ndim = len(shape) if len(zooms) != ndim: raise HeaderDataError('Expecting %d zoom values for ndim %d' % (ndim, ndim)) - if len([z for z in zooms if z < 0]): + if any(z < 0 for z in zooms): raise HeaderDataError('zooms must be positive') self._zooms = zooms - def set_norm_zooms(self, zooms): - ''' Get zooms in mm/s units ''' - return self.set_zooms(zooms) - def get_base_affine(self): shape = self.get_data_shape() zooms = self.get_zooms() diff --git a/nibabel/tests/test_analyze.py b/nibabel/tests/test_analyze.py index 10a3396e96..de02f3f3aa 100644 --- a/nibabel/tests/test_analyze.py +++ b/nibabel/tests/test_analyze.py @@ -92,7 +92,7 @@ def test_general_init(self): [-1, 1, 1, 1]) # But zooms only go with number of dimensions assert hdr.get_zooms(units='raw') == (1.0,) - assert hdr.get_zooms(units='canonical') == (1.0,) + assert hdr.get_zooms(units='norm') == (1.0,) def test_header_size(self): assert self.header_class.template_dtype.itemsize == self.sizeof_hdr @@ -439,7 +439,7 @@ def test_data_shape_zooms_affine(self): assert hdr.get_data_shape() == (0,) # Default zoom - for 3D - is 1(()) assert hdr.get_zooms(units='raw') == (1,) * L - assert hdr.get_zooms(units='canonical') == (1,) * L + assert hdr.get_zooms(units='norm') == (1,) * L # errors if zooms do not match shape if len(shape): with pytest.raises(HeaderDataError): @@ -456,19 +456,19 @@ def test_data_shape_zooms_affine(self): # it again reverts the previously set zoom values to 1.0 hdr = self.header_class() hdr.set_data_shape((1, 2, 3)) - hdr.set_zooms((4, 5, 6)) + hdr.set_zooms((4, 5, 6), units='norm') assert_array_equal(hdr.get_zooms(units='raw'), (4, 5, 6)) - assert_array_equal(hdr.get_zooms(units='canonical'), (4, 5, 6)) + assert_array_equal(hdr.get_zooms(units='norm'), (4, 5, 6)) hdr.set_data_shape((1, 2)) assert_array_equal(hdr.get_zooms(units='raw'), (4, 5)) - assert_array_equal(hdr.get_zooms(units='canonical'), (4, 5)) + assert_array_equal(hdr.get_zooms(units='norm'), (4, 5)) hdr.set_data_shape((1, 2, 3)) assert_array_equal(hdr.get_zooms(units='raw'), (4, 5, 1)) - assert_array_equal(hdr.get_zooms(units='canonical'), (4, 5, 1)) + assert_array_equal(hdr.get_zooms(units='norm'), (4, 5, 1)) # Setting zooms changes affine assert_array_equal(np.diag(hdr.get_base_affine()), [-4, 5, 1, 1]) - hdr.set_zooms((1, 1, 1)) + hdr.set_zooms((1, 1, 1), units='norm') assert_array_equal(np.diag(hdr.get_base_affine()), [-1, 1, 1, 1]) @@ -476,7 +476,7 @@ def test_default_x_flip(self): hdr = self.header_class() hdr.default_x_flip = True hdr.set_data_shape((1, 2, 3)) - hdr.set_zooms((1, 1, 1)) + hdr.set_zooms((1, 1, 1), units='norm') assert_array_equal(np.diag(hdr.get_base_affine()), [-1, 1, 1, 1]) hdr.default_x_flip = False @@ -495,7 +495,7 @@ def test_orientation(self): hdr = self.header_class() assert hdr.default_x_flip hdr.set_data_shape((3, 5, 7)) - hdr.set_zooms((4, 5, 6)) + hdr.set_zooms((4, 5, 6), units='norm') aff = np.diag((-4, 5, 6, 1)) aff[:3, 3] = np.array([1, 2, 3]) * np.array([-4, 5, 6]) * -1 assert_array_equal(hdr.get_base_affine(), aff) @@ -522,7 +522,7 @@ def test_from_header(self): hdr = klass() hdr.set_data_dtype(np.float64) hdr.set_data_shape((1, 2, 3)) - hdr.set_zooms((3.0, 2.0, 1.0)) + hdr.set_zooms((3.0, 2.0, 1.0), units='norm') for check in (True, False): copy = klass.from_header(hdr, check=check) assert hdr == copy @@ -540,13 +540,13 @@ def get_zooms(self, units=None, raise_unknown=None): return (10.0, 9.0, 8.0) assert converted.get_data_dtype() == np.dtype('i2') assert converted.get_data_shape() == (5, 4, 3) assert converted.get_zooms(units='raw') == (10.0, 9.0, 8.0) - assert converted.get_zooms(units='canonical') == (10.0, 9.0, 8.0) + assert converted.get_zooms(units='norm') == (10.0, 9.0, 8.0) def test_base_affine(self): klass = self.header_class hdr = klass() hdr.set_data_shape((3, 5, 7)) - hdr.set_zooms((3, 2, 1)) + hdr.set_zooms((3, 2, 1), units='norm') assert hdr.default_x_flip assert_array_almost_equal( hdr.get_base_affine(), @@ -651,7 +651,7 @@ def get_zooms(self, units=None, raise_unknown=None): exp_hdr = klass() exp_hdr.set_data_dtype(np.dtype('u1')) exp_hdr.set_data_shape((2, 3, 4)) - exp_hdr.set_zooms((4, 5, 6)) + exp_hdr.set_zooms((4, 5, 6), units='raw') assert klass.from_header(H4()) == exp_hdr # cal_max, cal_min get properly set from ``as_analyze_map`` @@ -688,7 +688,7 @@ def as_analyze_map(self): def test_best_affine(): hdr = AnalyzeHeader() hdr.set_data_shape((3, 5, 7)) - hdr.set_zooms((4, 5, 6)) + hdr.set_zooms((4, 5, 6), units='norm') assert_array_equal(hdr.get_base_affine(), hdr.get_best_affine()) @@ -836,24 +836,24 @@ def test_header_updating(self): # With a None affine - don't overwrite zooms img = img_klass(np.zeros((2, 3, 4)), None) hdr = img.header - hdr.set_zooms((4, 5, 6)) + hdr.set_zooms((4, 5, 6), units='norm') # Save / reload using bytes IO objects for key, value in img.file_map.items(): value.fileobj = BytesIO() img.to_file_map() hdr_back = img.from_file_map(img.file_map).header - assert_array_equal(hdr_back.get_zooms(), (4, 5, 6)) + assert_array_equal(hdr_back.get_zooms(units='norm'), (4, 5, 6)) # With a real affine, update zooms img = img_klass(np.zeros((2, 3, 4)), np.diag([2, 3, 4, 1]), hdr) hdr = img.header - assert_array_equal(hdr.get_zooms(), (2, 3, 4)) + assert_array_equal(hdr.get_zooms(units='norm'), (2, 3, 4)) # Modify affine in-place? Update on save. img.affine[0, 0] = 9 for key, value in img.file_map.items(): value.fileobj = BytesIO() img.to_file_map() hdr_back = img.from_file_map(img.file_map).header - assert_array_equal(hdr.get_zooms(), (9, 3, 4)) + assert_array_equal(hdr.get_zooms(units='norm'), (9, 3, 4)) # Modify data in-place? Update on save data = img.get_fdata() data.shape = (3, 2, 4) diff --git a/nibabel/tests/test_nifti1.py b/nibabel/tests/test_nifti1.py index 3fbba449a0..61629ede4b 100644 --- a/nibabel/tests/test_nifti1.py +++ b/nibabel/tests/test_nifti1.py @@ -1210,50 +1210,50 @@ def test_zooms_edge_cases(self): # Unknown units = 2 warnings with clear_and_catch_warnings() as warns: warnings.simplefilter('always') - assert_array_almost_equal(img.header.get_zooms(units='canonical'), + assert_array_almost_equal(img.header.get_zooms(units='norm'), (1, 1, 1, 1)) assert_equal(len(warns), 2) assert_raises(ValueError, img.header.get_zooms, - units='canonical', raise_unknown=True) + units='norm', raise_unknown=True) img.header.set_xyzt_units(xyz='meter') with clear_and_catch_warnings() as warns: warnings.simplefilter('always') - assert_array_almost_equal(img.header.get_zooms(units='canonical'), + assert_array_almost_equal(img.header.get_zooms(units='norm'), (1000, 1000, 1000, 1)) assert_equal(len(warns), 1) assert_raises(ValueError, img.header.get_zooms, - units='canonical', raise_unknown=True) + units='norm', raise_unknown=True) img.header.set_xyzt_units(xyz='mm', t='sec') - assert_array_almost_equal(img.header.get_zooms(units='canonical'), + assert_array_almost_equal(img.header.get_zooms(units='norm'), (1, 1, 1, 1)) img.header.set_xyzt_units(xyz='micron', t='sec') - assert_array_almost_equal(img.header.get_zooms(units='canonical'), + assert_array_almost_equal(img.header.get_zooms(units='norm'), (0.001, 0.001, 0.001, 1)) img.header.set_xyzt_units(t='sec') with clear_and_catch_warnings() as warns: warnings.simplefilter('always') - assert_array_equal(img.header.get_zooms(units='canonical'), + assert_array_equal(img.header.get_zooms(units='norm'), (1, 1, 1, 1)) assert_equal(len(warns), 1) assert_raises(ValueError, img.header.get_zooms, - units='canonical', raise_unknown=True) + units='norm', raise_unknown=True) img.header.set_xyzt_units(xyz='mm', t='msec') - assert_array_almost_equal(img.header.get_zooms(units='canonical'), + assert_array_almost_equal(img.header.get_zooms(units='norm'), (1, 1, 1, 0.001)) img.header.set_xyzt_units(xyz='mm', t='usec') - assert_array_almost_equal(img.header.get_zooms(units='canonical'), + assert_array_almost_equal(img.header.get_zooms(units='norm'), (1, 1, 1, 0.000001)) - # Verify `set_norm_zooms` resets units + # Verify `set_zooms(units='norm')` resets units img.header.set_xyzt_units(xyz='meter', t='usec') assert_equal(img.header.get_xyzt_units(), ('meter', 'usec')) - img.header.set_norm_zooms((2, 2, 2, 2.5)) - assert_array_almost_equal(img.header.get_zooms(units='canonical'), + img.header.set_zooms((2, 2, 2, 2.5), units='norm') + assert_array_almost_equal(img.header.get_zooms(units='norm'), (2, 2, 2, 2.5)) assert_array_almost_equal(img.header.get_zooms(units='raw'), (2, 2, 2, 2.5)) diff --git a/nibabel/tests/test_spatialimages.py b/nibabel/tests/test_spatialimages.py index b94947f6ee..b55cc16ee0 100644 --- a/nibabel/tests/test_spatialimages.py +++ b/nibabel/tests/test_spatialimages.py @@ -214,7 +214,7 @@ def test_isolation(self): # Pass it back in img = img_klass(arr, aff, ihdr) # Check modifying header outside does not modify image - ihdr.set_zooms((4, 5, 6)) + ihdr.set_zooms((4, 5, 6), units='norm') assert img.header != ihdr def test_float_affine(self): @@ -534,8 +534,8 @@ def test_zooms(self): arr = np.arange(120, dtype=np.int16).reshape((2, 3, 4, 5)) aff = np.eye(4) img = img_klass(arr, aff) - img.header.set_norm_zooms((2, 2, 2, 2.5)) - assert_array_equal(img.header.get_zooms(units='canonical'), (2, 2, 2, 2.5)) + img.header.set_zooms((2, 2, 2, 2.5), units='norm') + assert_array_equal(img.header.get_zooms(units='norm'), (2, 2, 2, 2.5)) def test_zooms_edge_cases(self): ''' Override for classes where *_norm_zooms != *_zooms ''' @@ -543,12 +543,12 @@ def test_zooms_edge_cases(self): arr = np.arange(120, dtype=np.int16).reshape((2, 3, 4, 5)) aff = np.eye(4) img = img_klass(arr, aff) - img.header.set_zooms((2, 2, 2, 2.5)) + img.header.set_zooms((2, 2, 2, 2.5), units='raw') assert_array_equal(img.header.get_zooms(units='raw'), (2, 2, 2, 2.5)) - assert_array_equal(img.header.get_zooms(units='canonical'), (2, 2, 2, 2.5)) - img.header.set_norm_zooms((2, 2, 2, 2.5)) + assert_array_equal(img.header.get_zooms(units='norm'), (2, 2, 2, 2.5)) + img.header.set_zooms((2, 2, 2, 2.5), units='norm') assert_array_equal(img.header.get_zooms(units='raw'), (2, 2, 2, 2.5)) - assert_array_equal(img.header.get_zooms(units='canonical'), (2, 2, 2, 2.5)) + assert_array_equal(img.header.get_zooms(units='norm'), (2, 2, 2, 2.5)) class MmapImageMixin: From da546657e3561e782a68224591964caafe199a10 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Sun, 11 Mar 2018 14:18:49 -1000 Subject: [PATCH 14/24] TEST: More complete zoom testing, revert unnecessary change --- nibabel/freesurfer/tests/test_mghformat.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/nibabel/freesurfer/tests/test_mghformat.py b/nibabel/freesurfer/tests/test_mghformat.py index f2addd26a7..5e7c61a887 100644 --- a/nibabel/freesurfer/tests/test_mghformat.py +++ b/nibabel/freesurfer/tests/test_mghformat.py @@ -70,7 +70,7 @@ def test_read_mgh(): assert h['dof'] == 0 assert h['goodRASFlag'] == 1 assert_array_equal(h['dims'], [3, 4, 5, 2]) - assert_almost_equal(h['tr'], 2) + 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) @@ -148,9 +148,14 @@ def test_write_noaffine_mgh(): def test_set_zooms(): mgz = load(MGZ_FNAME) h = mgz.header - assert_array_almost_equal(h.get_zooms(), [1, 1, 1, 0.002]) - h.set_zooms([1, 1, 1, 3]) - assert_array_almost_equal(h.get_zooms(), [1, 1, 1, 3]) + assert_array_almost_equal(h.get_zooms(units='raw'), [1, 1, 1, 2]) + assert_array_almost_equal(h.get_zooms(units='norm'), [1, 1, 1, 0.002]) + h.set_zooms([1, 1, 1, 3], units='raw') + assert_array_almost_equal(h.get_zooms(units='raw'), [1, 1, 1, 3]) + assert_array_almost_equal(h.get_zooms(units='norm'), [1, 1, 1, 0.003]) + h.set_zooms([1, 1, 1, 3], units='norm') + assert_array_almost_equal(h.get_zooms(units='raw'), [1, 1, 1, 3000]) + assert_array_almost_equal(h.get_zooms(units='norm'), [1, 1, 1, 3]) for zooms in ((-1, 1, 1, 1), (1, -1, 1, 1), (1, 1, -1, 1), From 1620f31d27ae1e4b3561950b1cfbe2c0c76d2d80 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Sun, 11 Mar 2018 14:42:26 -1000 Subject: [PATCH 15/24] TEST: Improve warning filters, check set_zooms behavior more thoroughly --- nibabel/tests/test_nifti1.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/nibabel/tests/test_nifti1.py b/nibabel/tests/test_nifti1.py index 61629ede4b..5e6ea7e75f 100644 --- a/nibabel/tests/test_nifti1.py +++ b/nibabel/tests/test_nifti1.py @@ -88,6 +88,7 @@ def setUp(self): self._wctx = warnings.catch_warnings() self._wctx.__enter__() warnings.filterwarnings('ignore', 'get_zooms', FutureWarning) + warnings.filterwarnings('ignore', 'set_zooms', FutureWarning) warnings.filterwarnings('ignore', 'Unknown (spatial|time) units', UserWarning) @@ -782,6 +783,7 @@ def setUp(self): self._wctx = warnings.catch_warnings() self._wctx.__enter__() warnings.filterwarnings('ignore', 'get_zooms', FutureWarning) + warnings.filterwarnings('ignore', 'set_zooms', FutureWarning) warnings.filterwarnings('ignore', 'Unknown (spatial|time) units', UserWarning) @@ -1206,7 +1208,6 @@ def test_zooms_edge_cases(self): aff = np.eye(4) img = img_klass(arr, aff) - # Unknown units = 2 warnings with clear_and_catch_warnings() as warns: warnings.simplefilter('always') @@ -1249,9 +1250,26 @@ def test_zooms_edge_cases(self): assert_array_almost_equal(img.header.get_zooms(units='norm'), (1, 1, 1, 0.000001)) - # Verify `set_zooms(units='norm')` resets units img.header.set_xyzt_units(xyz='meter', t='usec') assert_equal(img.header.get_xyzt_units(), ('meter', 'usec')) + + # Verify `set_zooms(units='raw')` leaves units unchanged + img.header.set_zooms((2, 2, 2, 2.5), units='raw') + assert_array_almost_equal(img.header.get_zooms(units='norm'), + (2000, 2000, 2000, 0.0000025)) + assert_array_almost_equal(img.header.get_zooms(units='raw'), + (2, 2, 2, 2.5)) + assert_equal(img.header.get_xyzt_units(), ('meter', 'usec')) + + # Verify `set_zooms(units=)` sets units explicitly + img.header.set_zooms((2, 2, 2, 2.5), units=('micron', 'msec')) + assert_array_almost_equal(img.header.get_zooms(units='norm'), + (0.002, 0.002, 0.002, 0.0025)) + assert_array_almost_equal(img.header.get_zooms(units='raw'), + (2, 2, 2, 2.5)) + assert_equal(img.header.get_xyzt_units(), ('micron', 'msec')) + + # Verify `set_zooms(units='norm')` resets units img.header.set_zooms((2, 2, 2, 2.5), units='norm') assert_array_almost_equal(img.header.get_zooms(units='norm'), (2, 2, 2, 2.5)) @@ -1259,6 +1277,10 @@ def test_zooms_edge_cases(self): (2, 2, 2, 2.5)) assert_equal(img.header.get_xyzt_units(), ('mm', 'sec')) + assert_raises(ValueError, img.header.get_zooms, units='badparam') + assert_raises(ValueError, img.header.set_zooms, (3, 3, 3, 3.5), + units='badparam') + class TestNifti1Image(TestNifti1Pair): # Run analyze-flavor spatialimage tests From 5d96f5c1aa10ffa78b1d12262e4761173becad7c Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Sun, 11 Mar 2018 14:54:18 -1000 Subject: [PATCH 16/24] TEST: Explicitly test non-temporal t_units during set_zooms(units="norm") --- nibabel/tests/test_nifti1.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/nibabel/tests/test_nifti1.py b/nibabel/tests/test_nifti1.py index 5e6ea7e75f..a26050d11f 100644 --- a/nibabel/tests/test_nifti1.py +++ b/nibabel/tests/test_nifti1.py @@ -1277,6 +1277,36 @@ def test_zooms_edge_cases(self): (2, 2, 2, 2.5)) assert_equal(img.header.get_xyzt_units(), ('mm', 'sec')) + # Non-temporal t units are not transformed + img.header.set_zooms((1, 1, 1, 1.5), units=('mm', 'ppm')) + with clear_and_catch_warnings() as warns: + warnings.simplefilter('always') + assert_array_almost_equal(img.header.get_zooms(units='norm'), + (1, 1, 1, 1.5)) + assert_equal(len(warns), 1) + assert_array_almost_equal(img.header.get_zooms(units='raw'), + (1, 1, 1, 1.5)) + + # Non-temporal t units are not normalized + img.header.set_zooms((2, 2, 2, 3.5), units='norm') + with clear_and_catch_warnings() as warns: + warnings.simplefilter('always') + assert_array_almost_equal(img.header.get_zooms(units='norm'), + (2, 2, 2, 3.5)) + assert_equal(len(warns), 1) + assert_array_almost_equal(img.header.get_zooms(units='raw'), + (2, 2, 2, 3.5)) + assert_equal(img.header.get_xyzt_units(), ('mm', 'ppm')) + + # Unknown t units are normalized to seconds + img.header.set_xyzt_units(xyz='mm', t='unknown') + img.header.set_zooms((2, 2, 2, 3.5), units='norm') + assert_array_almost_equal(img.header.get_zooms(units='norm'), + (2, 2, 2, 3.5)) + assert_array_almost_equal(img.header.get_zooms(units='raw'), + (2, 2, 2, 3.5)) + assert_equal(img.header.get_xyzt_units(), ('mm', 'sec')) + assert_raises(ValueError, img.header.get_zooms, units='badparam') assert_raises(ValueError, img.header.set_zooms, (3, 3, 3, 3.5), units='badparam') From 233f7f07f596b7a5d4c1190e294dc8cdc9f2ab95 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Sun, 11 Mar 2018 15:17:14 -1000 Subject: [PATCH 17/24] TEST: Filter warnings for unmodified superclass tests only --- nibabel/tests/test_nifti1.py | 58 ++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/nibabel/tests/test_nifti1.py b/nibabel/tests/test_nifti1.py index a26050d11f..266bf2936c 100644 --- a/nibabel/tests/test_nifti1.py +++ b/nibabel/tests/test_nifti1.py @@ -83,19 +83,6 @@ class TestNifti1PairHeader(tana.TestAnalyzeHeader, tspm.HeaderScalingMixin): np.longcomplex)) tana.add_intp(supported_np_types) - def setUp(self): - # Add warning filters for duration of test case - self._wctx = warnings.catch_warnings() - self._wctx.__enter__() - warnings.filterwarnings('ignore', 'get_zooms', FutureWarning) - warnings.filterwarnings('ignore', 'set_zooms', FutureWarning) - warnings.filterwarnings('ignore', 'Unknown (spatial|time) units', - UserWarning) - - def tearDown(self): - # Restore warning filters - self._wctx.__exit__() - def test_empty(self): tana.TestAnalyzeHeader.test_empty(self) hdr = self.header_class() @@ -731,6 +718,30 @@ def test_recoded_fields(self): hdr['slice_code'] = 4 # alternating decreasing assert hdr.get_value_label('slice_code') == 'alternating decreasing' + def test_general_init(self): + with clear_and_catch_warnings() as warns: + warnings.filterwarnings('ignore', 'get_zooms', FutureWarning) + warnings.filterwarnings('ignore', 'set_zooms', FutureWarning) + warnings.filterwarnings('ignore', 'Unknown (spatial|time) units', + UserWarning) + super(TestNifti1PairHeader, self).test_general_init() + + def test_from_header(self): + with clear_and_catch_warnings() as warns: + warnings.filterwarnings('ignore', 'get_zooms', FutureWarning) + warnings.filterwarnings('ignore', 'set_zooms', FutureWarning) + warnings.filterwarnings('ignore', 'Unknown (spatial|time) units', + UserWarning) + super(TestNifti1PairHeader, self).test_from_header() + + def test_data_shape_zooms_affine(self): + with clear_and_catch_warnings() as warns: + warnings.filterwarnings('ignore', 'get_zooms', FutureWarning) + warnings.filterwarnings('ignore', 'set_zooms', FutureWarning) + warnings.filterwarnings('ignore', 'Unknown (spatial|time) units', + UserWarning) + super(TestNifti1PairHeader, self).test_data_shape_zooms_affine() + def unshear_44(affine): RZS = affine[:3, :3] @@ -778,19 +789,6 @@ class TestNifti1Pair(tana.TestAnalyzeImage, tspm.ImageScalingMixin): image_class = Nifti1Pair supported_np_types = TestNifti1PairHeader.supported_np_types - def setUp(self): - # Add warning filters for duration of test case - self._wctx = warnings.catch_warnings() - self._wctx.__enter__() - warnings.filterwarnings('ignore', 'get_zooms', FutureWarning) - warnings.filterwarnings('ignore', 'set_zooms', FutureWarning) - warnings.filterwarnings('ignore', 'Unknown (spatial|time) units', - UserWarning) - - def tearDown(self): - # Restore warning filters - self._wctx.__exit__() - def test_int64_warning(self): # Verify that initializing with (u)int64 data and no # header/dtype info produces a warning @@ -1311,6 +1309,14 @@ def test_zooms_edge_cases(self): assert_raises(ValueError, img.header.set_zooms, (3, 3, 3, 3.5), units='badparam') + def test_no_finite_values(self): + with clear_and_catch_warnings() as warns: + warnings.filterwarnings('ignore', 'get_zooms', FutureWarning) + warnings.filterwarnings('ignore', 'set_zooms', FutureWarning) + warnings.filterwarnings('ignore', 'Unknown (spatial|time) units', + UserWarning) + super(TestNifti1Pair, self).test_no_finite_values() + class TestNifti1Image(TestNifti1Pair): # Run analyze-flavor spatialimage tests From 4babf08d17232b393b3fc019a23159f99c4ce4ef Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Sun, 11 Mar 2018 16:59:57 -1000 Subject: [PATCH 18/24] ENH: Add units/raise_unknown to MINC/ECAT get_zooms --- nibabel/ecat.py | 2 +- nibabel/minc1.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nibabel/ecat.py b/nibabel/ecat.py index 54f600f147..20dcefb128 100644 --- a/nibabel/ecat.py +++ b/nibabel/ecat.py @@ -578,7 +578,7 @@ def get_frame_affine(self, frame=0): z_off]) return aff - def get_zooms(self, frame=0): + def get_zooms(self, frame=0, units='norm', raise_unknown=False): """returns zooms ...pixdims""" subhdr = self.subheaders[frame] x_zoom = subhdr['x_pixel_size'] * 10 diff --git a/nibabel/minc1.py b/nibabel/minc1.py index c0ae95bd7b..c76f3d6daa 100644 --- a/nibabel/minc1.py +++ b/nibabel/minc1.py @@ -90,7 +90,7 @@ def get_data_dtype(self): def get_data_shape(self): return self._image.data.shape - def get_zooms(self): + def get_zooms(self, units='norm', raise_unknown=False): """ Get real-world sizes of voxels """ # zooms must be positive; but steps in MINC can be negative return tuple([abs(float(dim.step)) if hasattr(dim, 'step') else 1.0 From 6cd83f7fa0476daaed2a69bfdfeb9ace08b01cfd Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Mon, 12 Mar 2018 14:02:23 -1000 Subject: [PATCH 19/24] ENH: Validate units parameter in all get/set_zooms --- nibabel/analyze.py | 4 ++++ nibabel/ecat.py | 2 ++ nibabel/freesurfer/mghformat.py | 4 ++++ nibabel/minc1.py | 2 ++ nibabel/nifti1.py | 6 +++--- nibabel/spatialimages.py | 4 ++++ 6 files changed, 19 insertions(+), 3 deletions(-) diff --git a/nibabel/analyze.py b/nibabel/analyze.py index 40a1971a86..fb07443ebc 100644 --- a/nibabel/analyze.py +++ b/nibabel/analyze.py @@ -690,6 +690,8 @@ def get_zooms(self, units='norm', raise_unknown=False): >>> hdr.get_zooms() (3.0, 4.0) """ + if units not in ('norm', 'raw'): + raise ValueError("`units` parameter must be 'norm' or 'raw'") hdr = self._structarr dims = hdr['dim'] ndim = dims[0] @@ -712,6 +714,8 @@ def set_zooms(self, zooms, units='norm'): spatial/temporal or as raw values to be interpreted according to format specification. """ + if units not in ('norm', 'raw'): + raise ValueError("`units` parameter must be 'norm' or 'raw'") hdr = self._structarr dims = hdr['dim'] ndim = dims[0] diff --git a/nibabel/ecat.py b/nibabel/ecat.py index 20dcefb128..afb85e78bd 100644 --- a/nibabel/ecat.py +++ b/nibabel/ecat.py @@ -580,6 +580,8 @@ def get_frame_affine(self, frame=0): def get_zooms(self, frame=0, units='norm', raise_unknown=False): """returns zooms ...pixdims""" + if units not in ('norm', 'raw'): + raise ValueError("`units` parameter must be 'norm' or 'raw'") subhdr = self.subheaders[frame] x_zoom = subhdr['x_pixel_size'] * 10 y_zoom = subhdr['y_pixel_size'] * 10 diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index b277411486..6bc91db7a3 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -301,7 +301,11 @@ def set_zooms(self, zooms, units='norm'): Zooms are specified in normalized units of mm/sec for spatial/temporal dimensions or as raw values to be stored in header. + + .. _mghformat: https://surfer.nmr.mgh.harvard.edu/fswiki/FsTutorial/MghFormat#line-82 """ + if units not in ('norm', 'raw'): + raise ValueError("`units` parameter must be 'norm' or 'raw'") hdr = self._structarr zooms = np.asarray(zooms) ndims = self._ndims() diff --git a/nibabel/minc1.py b/nibabel/minc1.py index c76f3d6daa..1525ddd004 100644 --- a/nibabel/minc1.py +++ b/nibabel/minc1.py @@ -92,6 +92,8 @@ def get_data_shape(self): def get_zooms(self, units='norm', raise_unknown=False): """ Get real-world sizes of voxels """ + if units not in ('norm', 'raw'): + raise ValueError("`units` parameter must be 'norm' or 'raw'") # zooms must be positive; but steps in MINC can be negative return tuple([abs(float(dim.step)) if hasattr(dim, 'step') else 1.0 for dim in self._dims]) diff --git a/nibabel/nifti1.py b/nibabel/nifti1.py index 738aa079da..d948f477e6 100644 --- a/nibabel/nifti1.py +++ b/nibabel/nifti1.py @@ -1810,6 +1810,9 @@ def set_zooms(self, zooms, units=None): ''.format(self.__class__.__name__), FutureWarning, stacklevel=2) + if units not in ('norm', 'raw') and not isinstance(units, tuple): + raise ValueError("`units` parameter must be 'norm', 'raw'," + " or a tuple of unit codes (see set_xyzt_units)") super(Nifti1Header, self).set_zooms(zooms, units=units) if isinstance(units, tuple): @@ -1822,9 +1825,6 @@ def set_zooms(self, zooms, units=None): elif len(zooms) > 3 and t_code in ('unknown', 'sec', 'msec', 'usec'): t_code = 'sec' self.set_xyzt_units(xyz_code, t_code) - elif units != 'raw': - raise ValueError("`units` parameter must be 'norm', 'raw'," - " or a tuple of unit codes (see set_xyzt_units)") def _clean_after_mapping(self): """ Set format-specific stuff after converting header from mapping diff --git a/nibabel/spatialimages.py b/nibabel/spatialimages.py index f7fe66d231..93877e947c 100644 --- a/nibabel/spatialimages.py +++ b/nibabel/spatialimages.py @@ -241,6 +241,8 @@ def get_zooms(self, units='norm', raise_unknown=False): zooms : tuple Spacing between voxels along each axis ''' + if units not in ('norm', 'raw'): + raise ValueError("`units` parameter must be 'norm' or 'raw'") return self._zooms def set_zooms(self, zooms, units='norm'): @@ -255,6 +257,8 @@ def set_zooms(self, zooms, units='norm'): spatial/temporal or as raw values to be interpreted according to format specification. ''' + if units not in ('norm', 'raw'): + raise ValueError("`units` parameter must be 'norm' or 'raw'") zooms = tuple(float(z) for z in zooms) shape = self.get_data_shape() ndim = len(shape) From 94cbd0dfd3ac02b4e57fa76f2a0991177482aa59 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Tue, 13 Mar 2018 19:37:52 -1000 Subject: [PATCH 20/24] FIX: Correct and simplify NIFTI logic --- nibabel/nifti1.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/nibabel/nifti1.py b/nibabel/nifti1.py index d948f477e6..4efa3bbc28 100644 --- a/nibabel/nifti1.py +++ b/nibabel/nifti1.py @@ -1726,22 +1726,22 @@ def get_zooms(self, units=None, raise_unknown=False): raise ValueError("`units` parameter must be 'norm' or 'raw'") xyz_zooms = raw_zooms[:3] - t_zoom = raw_zooms[3] if len(raw_zooms) > 3 else None + t_zoom = raw_zooms[3:4] # Tuple of length 0 or 1 xyz_code, t_code = self.get_xyzt_units() xyz_msg = t_msg = '' if xyz_code == 'unknown': xyz_msg = 'Unknown spatial units' xyz_code = 'mm' - if t_zoom is not None: + if t_zoom: if t_code == 'unknown': t_msg = 'Unknown time units' t_code = 'sec' elif t_code in ('hz', 'ppm', 'rads'): t_msg = 'Unconvertible temporal units: {}'.format(t_code) - if raise_unknown and (xyz_msg, t_msg) != ('', ''): - if xyz_msg and t_msg: + if raise_unknown and (xyz_msg or t_msg.startswith('Unknown')): + if xyz_msg and t_msg.startswith('Unknown'): msg = 'Unknown spatial and time units' else: msg = xyz_msg or t_msg @@ -1757,12 +1757,10 @@ def get_zooms(self, units=None, raise_unknown=False): xyz_factor = {'meter': 1000, 'mm': 1, 'micron': 0.001}[xyz_code] xyz_zooms = tuple(np.array(xyz_zooms) * xyz_factor) - if t_zoom is not None: + if t_zoom: t_factor = {'sec': 1, 'msec': 0.001, 'usec': 0.000001, 'hz': 1, 'ppm': 1, 'rads': 1}[t_code] - t_zoom = (t_zoom * t_factor,) - else: - t_zoom = () + t_zoom = (t_zoom[0] * t_factor,) return xyz_zooms + t_zoom @@ -1813,7 +1811,7 @@ def set_zooms(self, zooms, units=None): if units not in ('norm', 'raw') and not isinstance(units, tuple): raise ValueError("`units` parameter must be 'norm', 'raw'," " or a tuple of unit codes (see set_xyzt_units)") - super(Nifti1Header, self).set_zooms(zooms, units=units) + super(Nifti1Header, self).set_zooms(zooms, units='raw') if isinstance(units, tuple): self.set_xyzt_units(*units) From cfc5abe65c032ef70938e14a9d6966b04cf02cf6 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Mon, 19 Mar 2018 21:04:57 -0400 Subject: [PATCH 21/24] TEST: Clear nifti1 module warnings --- nibabel/tests/test_nifti1.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/nibabel/tests/test_nifti1.py b/nibabel/tests/test_nifti1.py index 266bf2936c..9993c52b75 100644 --- a/nibabel/tests/test_nifti1.py +++ b/nibabel/tests/test_nifti1.py @@ -719,7 +719,7 @@ def test_recoded_fields(self): assert hdr.get_value_label('slice_code') == 'alternating decreasing' def test_general_init(self): - with clear_and_catch_warnings() as warns: + with clear_and_catch_warnings(modules=(nifti1,)) as warns: warnings.filterwarnings('ignore', 'get_zooms', FutureWarning) warnings.filterwarnings('ignore', 'set_zooms', FutureWarning) warnings.filterwarnings('ignore', 'Unknown (spatial|time) units', @@ -727,7 +727,7 @@ def test_general_init(self): super(TestNifti1PairHeader, self).test_general_init() def test_from_header(self): - with clear_and_catch_warnings() as warns: + with clear_and_catch_warnings(modules=(nifti1,)) as warns: warnings.filterwarnings('ignore', 'get_zooms', FutureWarning) warnings.filterwarnings('ignore', 'set_zooms', FutureWarning) warnings.filterwarnings('ignore', 'Unknown (spatial|time) units', @@ -735,7 +735,7 @@ def test_from_header(self): super(TestNifti1PairHeader, self).test_from_header() def test_data_shape_zooms_affine(self): - with clear_and_catch_warnings() as warns: + with clear_and_catch_warnings(modules=(nifti1,)) as warns: warnings.filterwarnings('ignore', 'get_zooms', FutureWarning) warnings.filterwarnings('ignore', 'set_zooms', FutureWarning) warnings.filterwarnings('ignore', 'Unknown (spatial|time) units', @@ -1207,7 +1207,7 @@ def test_zooms_edge_cases(self): img = img_klass(arr, aff) # Unknown units = 2 warnings - with clear_and_catch_warnings() as warns: + with clear_and_catch_warnings(modules=(nifti1,)) as warns: warnings.simplefilter('always') assert_array_almost_equal(img.header.get_zooms(units='norm'), (1, 1, 1, 1)) @@ -1216,7 +1216,7 @@ def test_zooms_edge_cases(self): units='norm', raise_unknown=True) img.header.set_xyzt_units(xyz='meter') - with clear_and_catch_warnings() as warns: + with clear_and_catch_warnings(modules=(nifti1,)) as warns: warnings.simplefilter('always') assert_array_almost_equal(img.header.get_zooms(units='norm'), (1000, 1000, 1000, 1)) @@ -1232,7 +1232,7 @@ def test_zooms_edge_cases(self): (0.001, 0.001, 0.001, 1)) img.header.set_xyzt_units(t='sec') - with clear_and_catch_warnings() as warns: + with clear_and_catch_warnings(modules=(nifti1,)) as warns: warnings.simplefilter('always') assert_array_equal(img.header.get_zooms(units='norm'), (1, 1, 1, 1)) @@ -1277,7 +1277,7 @@ def test_zooms_edge_cases(self): # Non-temporal t units are not transformed img.header.set_zooms((1, 1, 1, 1.5), units=('mm', 'ppm')) - with clear_and_catch_warnings() as warns: + with clear_and_catch_warnings(modules=(nifti1,)) as warns: warnings.simplefilter('always') assert_array_almost_equal(img.header.get_zooms(units='norm'), (1, 1, 1, 1.5)) @@ -1287,7 +1287,7 @@ def test_zooms_edge_cases(self): # Non-temporal t units are not normalized img.header.set_zooms((2, 2, 2, 3.5), units='norm') - with clear_and_catch_warnings() as warns: + with clear_and_catch_warnings(modules=(nifti1,)) as warns: warnings.simplefilter('always') assert_array_almost_equal(img.header.get_zooms(units='norm'), (2, 2, 2, 3.5)) @@ -1310,7 +1310,7 @@ def test_zooms_edge_cases(self): units='badparam') def test_no_finite_values(self): - with clear_and_catch_warnings() as warns: + with clear_and_catch_warnings(modules=(nifti1,)) as warns: warnings.filterwarnings('ignore', 'get_zooms', FutureWarning) warnings.filterwarnings('ignore', 'set_zooms', FutureWarning) warnings.filterwarnings('ignore', 'Unknown (spatial|time) units', From 3562921c73f13017c0ae7814de2357079c2de03b Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Mon, 19 Mar 2018 21:28:07 -0400 Subject: [PATCH 22/24] TEST: Check edge cases --- nibabel/tests/test_spatialimages.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/nibabel/tests/test_spatialimages.py b/nibabel/tests/test_spatialimages.py index b55cc16ee0..4282794e9c 100644 --- a/nibabel/tests/test_spatialimages.py +++ b/nibabel/tests/test_spatialimages.py @@ -536,6 +536,12 @@ def test_zooms(self): img = img_klass(arr, aff) img.header.set_zooms((2, 2, 2, 2.5), units='norm') assert_array_equal(img.header.get_zooms(units='norm'), (2, 2, 2, 2.5)) + with assert_raises(ValueError): + img.header.set_zooms((1, 1, 1, 1), units='badarg') + with assert_raises(ValueError): + img.header.get_zooms(units='badarg') + with assert_raises(HeaderDataError): + img.header.set_zooms((-1, 1, 1, 1)) def test_zooms_edge_cases(self): ''' Override for classes where *_norm_zooms != *_zooms ''' From d9199caa29ba759b78bfaf3764d961962f4e01c5 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Mon, 19 Mar 2018 21:35:46 -0400 Subject: [PATCH 23/24] TEST: Add unit to bad set_zoom call --- nibabel/tests/test_spatialimages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nibabel/tests/test_spatialimages.py b/nibabel/tests/test_spatialimages.py index 4282794e9c..246f92b712 100644 --- a/nibabel/tests/test_spatialimages.py +++ b/nibabel/tests/test_spatialimages.py @@ -541,7 +541,7 @@ def test_zooms(self): with assert_raises(ValueError): img.header.get_zooms(units='badarg') with assert_raises(HeaderDataError): - img.header.set_zooms((-1, 1, 1, 1)) + img.header.set_zooms((-1, 1, 1, 1), units='norm') def test_zooms_edge_cases(self): ''' Override for classes where *_norm_zooms != *_zooms ''' From 98e43a0806d8f95488ed796c4ad491bdff820216 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Mon, 19 Mar 2018 21:44:21 -0400 Subject: [PATCH 24/24] TEST: Check get_zooms units arg in MINC/ECAT --- nibabel/tests/test_ecat.py | 4 ++++ nibabel/tests/test_minc1.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/nibabel/tests/test_ecat.py b/nibabel/tests/test_ecat.py index 9e56fd73c7..d15d098ecd 100644 --- a/nibabel/tests/test_ecat.py +++ b/nibabel/tests/test_ecat.py @@ -169,6 +169,10 @@ def test_subheader(self): np.array([2.20241979, 2.20241979, 3.125, 1.])) assert self.subhdr.get_zooms()[0] == 2.20241978764534 assert self.subhdr.get_zooms()[2] == 3.125 + assert_array_equal(self.subhdr.get_zooms(units='raw'), + self.subhdr.get_zooms(units='norm')) + with pytest.raises(ValueError): + self.subhdr.get_zooms(units='badarg') assert self.subhdr._get_data_dtype(0) == np.int16 #assert_equal(self.subhdr._get_frame_offset(), 1024) assert self.subhdr._get_frame_offset() == 1536 diff --git a/nibabel/tests/test_minc1.py b/nibabel/tests/test_minc1.py index 4fecf5782e..1bbe7c8746 100644 --- a/nibabel/tests/test_minc1.py +++ b/nibabel/tests/test_minc1.py @@ -117,6 +117,10 @@ def test_mincfile(self): assert mnc.get_data_dtype().type == tp['dtype'] assert mnc.get_data_shape() == tp['shape'] assert mnc.get_zooms() == tp['zooms'] + assert mnc.get_zooms(units='raw') == tp['zooms'] + assert mnc.get_zooms(units='norm') == tp['zooms'] + with pytest.raises(ValueError): + mnc.get_zooms(units='badarg') assert_array_equal(mnc.get_affine(), tp['affine']) data = mnc.get_scaled_data() assert data.shape == tp['shape']