From fe0898f079018626a6932efee8c1816b833d7bf7 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Thu, 4 Jul 2019 13:45:34 +0100 Subject: [PATCH 1/9] ENH: raise error if CIFTI-2 header file has different shape as data --- nibabel/cifti2/cifti2.py | 39 +++++++++++++++++-- nibabel/cifti2/tests/test_cifti2.py | 8 +++- nibabel/cifti2/tests/test_new_cifti2.py | 51 ++++++++++++++++++------- 3 files changed, 80 insertions(+), 18 deletions(-) diff --git a/nibabel/cifti2/cifti2.py b/nibabel/cifti2/cifti2.py index 0f8108b235..5f9fc6dac4 100644 --- a/nibabel/cifti2/cifti2.py +++ b/nibabel/cifti2/cifti2.py @@ -1209,6 +1209,36 @@ def _to_xml_element(self): mat.append(mim._to_xml_element()) return mat + def get_axis(self, index): + ''' + Generates the Cifti2 axis for a given dimension + + Parameters + ---------- + index : int + Dimension for which we want to obtain the mapping. + + Returns + ------- + axis : :class:`.cifti2_axes.Axis` + ''' + from . import cifti2_axes + return cifti2_axes.from_index_mapping(self.get_index_map(index)) + + def get_data_shape(self): + """ + Returns data shape expected based on the CIFTI-2 header + """ + from . import cifti2_axes + if len(self.mapped_indices) == 0: + return () + base_shape = [-1 for _ in range(max(self.mapped_indices) + 1)] + for mim in self: + size = len(cifti2_axes.from_index_mapping(mim)) + for idx in mim.applies_to_matrix_dimension: + base_shape[idx] = size + return tuple(base_shape) + class Cifti2Header(FileBasedHeader, xml.XmlSerializable): ''' Class for CIFTI-2 header extension ''' @@ -1279,8 +1309,7 @@ def get_axis(self, index): ------- axis : :class:`.cifti2_axes.Axis` ''' - from . import cifti2_axes - return cifti2_axes.from_index_mapping(self.matrix.get_index_map(index)) + return self.matrix.get_axis(index) @classmethod def from_axes(cls, axes): @@ -1426,6 +1455,10 @@ def to_file_map(self, file_map=None): header = self._nifti_header extension = Cifti2Extension(content=self.header.to_xml()) header.extensions.append(extension) + if header.get_data_shape() != self.header.matrix.get_data_shape(): + raise ValueError("Dataobj shape {} does not match shape expected from CIFTI-2 header {}".format( + self._dataobj.shape, self.header.matrix.get_data_shape() + )) # if intent code is not set, default to unknown CIFTI if header.get_intent()[0] == 'none': header.set_intent('NIFTI_INTENT_CONNECTIVITY_UNKNOWN') @@ -1438,7 +1471,7 @@ def to_file_map(self, file_map=None): img.to_file_map(file_map or self.file_map) def update_headers(self): - ''' Harmonize CIFTI-2 and NIfTI headers with image data + ''' Harmonize NIfTI headers with image data >>> import numpy as np >>> data = np.zeros((2,3,4)) diff --git a/nibabel/cifti2/tests/test_cifti2.py b/nibabel/cifti2/tests/test_cifti2.py index 6054c126b0..8a8ffd6fc2 100644 --- a/nibabel/cifti2/tests/test_cifti2.py +++ b/nibabel/cifti2/tests/test_cifti2.py @@ -7,7 +7,7 @@ from nibabel import cifti2 as ci from nibabel.nifti2 import Nifti2Header -from nibabel.cifti2.cifti2 import _float_01, _value_if_klass, Cifti2HeaderError +from nibabel.cifti2.cifti2 import _float_01, _value_if_klass, Cifti2HeaderError, Cifti2NamedMap, Cifti2MatrixIndicesMap from nose.tools import assert_true, assert_equal, assert_raises, assert_is_none @@ -358,4 +358,10 @@ class TestCifti2ImageAPI(_TDA): standard_extension = '.nii' def make_imaker(self, arr, header=None, ni_header=None): + for idx, sz in enumerate(arr.shape): + maps = [Cifti2NamedMap(str(value)) for value in range(sz)] + mim = ci.Cifti2MatrixIndicesMap( + (idx, ), 'CIFTI_INDEX_TYPE_SCALARS', maps=maps + ) + header.matrix.append(mim) return lambda: self.image_maker(arr.copy(), header, ni_header) diff --git a/nibabel/cifti2/tests/test_new_cifti2.py b/nibabel/cifti2/tests/test_new_cifti2.py index 01bc742a22..79e79ba2d2 100644 --- a/nibabel/cifti2/tests/test_new_cifti2.py +++ b/nibabel/cifti2/tests/test_new_cifti2.py @@ -12,11 +12,12 @@ from nibabel import cifti2 as ci from nibabel.tmpdirs import InTemporaryDirectory -from nose.tools import assert_true, assert_equal +from nose.tools import assert_true, assert_equal, assert_raises affine = [[-1.5, 0, 0, 90], [0, 1.5, 0, -85], - [0, 0, 1.5, -71]] + [0, 0, 1.5, -71], + [0, 0, 0, 1.]] dimensions = (120, 83, 78) @@ -234,7 +235,7 @@ def test_dtseries(): matrix.append(series_map) matrix.append(geometry_map) hdr = ci.Cifti2Header(matrix) - data = np.random.randn(13, 9) + data = np.random.randn(13, 10) img = ci.Cifti2Image(data, hdr) img.nifti_header.set_intent('NIFTI_INTENT_CONNECTIVITY_DENSE_SERIES') @@ -257,7 +258,7 @@ def test_dscalar(): matrix.append(scalar_map) matrix.append(geometry_map) hdr = ci.Cifti2Header(matrix) - data = np.random.randn(2, 9) + data = np.random.randn(2, 10) img = ci.Cifti2Image(data, hdr) img.nifti_header.set_intent('NIFTI_INTENT_CONNECTIVITY_DENSE_SCALARS') @@ -279,7 +280,7 @@ def test_dlabel(): matrix.append(label_map) matrix.append(geometry_map) hdr = ci.Cifti2Header(matrix) - data = np.random.randn(2, 9) + data = np.random.randn(2, 10) img = ci.Cifti2Image(data, hdr) img.nifti_header.set_intent('NIFTI_INTENT_CONNECTIVITY_DENSE_LABELS') @@ -299,7 +300,7 @@ def test_dconn(): matrix = ci.Cifti2Matrix() matrix.append(mapping) hdr = ci.Cifti2Header(matrix) - data = np.random.randn(9, 9) + data = np.random.randn(10, 10) img = ci.Cifti2Image(data, hdr) img.nifti_header.set_intent('NIFTI_INTENT_CONNECTIVITY_DENSE') @@ -322,7 +323,7 @@ def test_ptseries(): matrix.append(series_map) matrix.append(parcel_map) hdr = ci.Cifti2Header(matrix) - data = np.random.randn(13, 3) + data = np.random.randn(13, 4) img = ci.Cifti2Image(data, hdr) img.nifti_header.set_intent('NIFTI_INTENT_CONNECTIVITY_PARCELLATED_SERIES') @@ -344,7 +345,7 @@ def test_pscalar(): matrix.append(scalar_map) matrix.append(parcel_map) hdr = ci.Cifti2Header(matrix) - data = np.random.randn(2, 3) + data = np.random.randn(2, 4) img = ci.Cifti2Image(data, hdr) img.nifti_header.set_intent('NIFTI_INTENT_CONNECTIVITY_PARCELLATED_SCALAR') @@ -366,7 +367,7 @@ def test_pdconn(): matrix.append(geometry_map) matrix.append(parcel_map) hdr = ci.Cifti2Header(matrix) - data = np.random.randn(2, 3) + data = np.random.randn(10, 4) img = ci.Cifti2Image(data, hdr) img.nifti_header.set_intent('NIFTI_INTENT_CONNECTIVITY_PARCELLATED_DENSE') @@ -388,7 +389,7 @@ def test_dpconn(): matrix.append(parcel_map) matrix.append(geometry_map) hdr = ci.Cifti2Header(matrix) - data = np.random.randn(2, 3) + data = np.random.randn(4, 10) img = ci.Cifti2Image(data, hdr) img.nifti_header.set_intent('NIFTI_INTENT_CONNECTIVITY_DENSE_PARCELLATED') @@ -410,7 +411,7 @@ def test_plabel(): matrix.append(label_map) matrix.append(parcel_map) hdr = ci.Cifti2Header(matrix) - data = np.random.randn(2, 3) + data = np.random.randn(2, 4) img = ci.Cifti2Image(data, hdr) with InTemporaryDirectory(): @@ -429,7 +430,7 @@ def test_pconn(): matrix = ci.Cifti2Matrix() matrix.append(mapping) hdr = ci.Cifti2Header(matrix) - data = np.random.randn(3, 3) + data = np.random.randn(4, 4) img = ci.Cifti2Image(data, hdr) img.nifti_header.set_intent('NIFTI_INTENT_CONNECTIVITY_PARCELLATED') @@ -453,7 +454,7 @@ def test_pconnseries(): matrix.append(parcel_map) matrix.append(series_map) hdr = ci.Cifti2Header(matrix) - data = np.random.randn(3, 3, 13) + data = np.random.randn(4, 4, 13) img = ci.Cifti2Image(data, hdr) img.nifti_header.set_intent('NIFTI_INTENT_CONNECTIVITY_PARCELLATED_' 'PARCELLATED_SERIES') @@ -479,7 +480,7 @@ def test_pconnscalar(): matrix.append(parcel_map) matrix.append(scalar_map) hdr = ci.Cifti2Header(matrix) - data = np.random.randn(3, 3, 13) + data = np.random.randn(4, 4, 2) img = ci.Cifti2Image(data, hdr) img.nifti_header.set_intent('NIFTI_INTENT_CONNECTIVITY_PARCELLATED_' 'PARCELLATED_SCALAR') @@ -496,3 +497,25 @@ def test_pconnscalar(): check_parcel_map(img2.header.matrix.get_index_map(0)) check_scalar_map(img2.header.matrix.get_index_map(2)) del img2 + + +def test_wrong_shape(): + scalar_map = create_scalar_map((0, )) + brain_model_map = create_geometry_map((1, )) + + matrix = ci.Cifti2Matrix() + matrix.append(scalar_map) + matrix.append(brain_model_map) + hdr = ci.Cifti2Header(matrix) + + # correct shape is (2, 10) + for data in ( + np.random.randn(1, 11), + np.random.randn(2, 10, 1), + np.random.randn(1, 2, 10), + np.random.randn(3, 10), + np.random.randn(2, 9), + ): + img = ci.Cifti2Image(data, hdr) + assert_raises(ValueError, img.to_file_map) + From 61e6b6a9c4a1a791528416da251591cd6a164c7b Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Thu, 4 Jul 2019 13:45:44 +0100 Subject: [PATCH 2/9] correct typo --- nibabel/batteryrunners.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nibabel/batteryrunners.py b/nibabel/batteryrunners.py index be3977111a..b77c8b8858 100644 --- a/nibabel/batteryrunners.py +++ b/nibabel/batteryrunners.py @@ -141,7 +141,7 @@ def check_only(self, obj): ------- reports : sequence sequence of report objects reporting on result of running - checks (withou fixes) on `obj` + checks (without fixes) on `obj` ''' reports = [] for check in self._checks: From aba96da7f2b9e903934faa1e2b134805debd8ef2 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Thu, 4 Jul 2019 13:53:38 +0100 Subject: [PATCH 3/9] ENH: raise warning if creating Cifti2Image with incorrect shape --- nibabel/cifti2/cifti2.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/nibabel/cifti2/cifti2.py b/nibabel/cifti2/cifti2.py index 5f9fc6dac4..a26d120f87 100644 --- a/nibabel/cifti2/cifti2.py +++ b/nibabel/cifti2/cifti2.py @@ -30,6 +30,7 @@ from ..nifti2 import Nifti2Image, Nifti2Header from ..arrayproxy import reshape_dataobj from ..keywordonly import kw_only_meth +from warnings import warn def _float_01(val): @@ -1374,12 +1375,19 @@ def __init__(self, super(Cifti2Image, self).__init__(dataobj, header=header, extra=extra, file_map=file_map) self._nifti_header = Nifti2Header.from_header(nifti_header) + # if NIfTI header not specified, get data type from input array if nifti_header is None: if hasattr(dataobj, 'dtype'): self._nifti_header.set_data_dtype(dataobj.dtype) self.update_headers() + if self._nifti_header.get_data_shape() != self.header.matrix.get_data_shape(): + warn("Dataobj shape {} does not match shape expected from CIFTI-2 header {}".format( + self._dataobj.shape, self.header.matrix.get_data_shape() + )) + + @property def nifti_header(self): return self._nifti_header From 5888a12a969a76fadb6994ef1929aae39c47e326 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Thu, 4 Jul 2019 13:56:28 +0100 Subject: [PATCH 4/9] STYLE: removed extra empty line --- nibabel/cifti2/cifti2.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nibabel/cifti2/cifti2.py b/nibabel/cifti2/cifti2.py index a26d120f87..52d3a92415 100644 --- a/nibabel/cifti2/cifti2.py +++ b/nibabel/cifti2/cifti2.py @@ -1387,7 +1387,6 @@ def __init__(self, self._dataobj.shape, self.header.matrix.get_data_shape() )) - @property def nifti_header(self): return self._nifti_header From a1bfa76c838450254183239fabe5ae280eea4668 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Thu, 4 Jul 2019 14:07:12 +0100 Subject: [PATCH 5/9] TEST: add tests to check that warnings are raised --- nibabel/cifti2/tests/test_cifti2.py | 4 ++-- nibabel/cifti2/tests/test_new_cifti2.py | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/nibabel/cifti2/tests/test_cifti2.py b/nibabel/cifti2/tests/test_cifti2.py index 8a8ffd6fc2..8f85b62041 100644 --- a/nibabel/cifti2/tests/test_cifti2.py +++ b/nibabel/cifti2/tests/test_cifti2.py @@ -7,7 +7,7 @@ from nibabel import cifti2 as ci from nibabel.nifti2 import Nifti2Header -from nibabel.cifti2.cifti2 import _float_01, _value_if_klass, Cifti2HeaderError, Cifti2NamedMap, Cifti2MatrixIndicesMap +from nibabel.cifti2.cifti2 import _float_01, _value_if_klass, Cifti2HeaderError from nose.tools import assert_true, assert_equal, assert_raises, assert_is_none @@ -359,7 +359,7 @@ class TestCifti2ImageAPI(_TDA): def make_imaker(self, arr, header=None, ni_header=None): for idx, sz in enumerate(arr.shape): - maps = [Cifti2NamedMap(str(value)) for value in range(sz)] + maps = [ci.Cifti2NamedMap(str(value)) for value in range(sz)] mim = ci.Cifti2MatrixIndicesMap( (idx, ), 'CIFTI_INDEX_TYPE_SCALARS', maps=maps ) diff --git a/nibabel/cifti2/tests/test_new_cifti2.py b/nibabel/cifti2/tests/test_new_cifti2.py index 79e79ba2d2..2a157ca7fb 100644 --- a/nibabel/cifti2/tests/test_new_cifti2.py +++ b/nibabel/cifti2/tests/test_new_cifti2.py @@ -13,6 +13,7 @@ from nibabel.tmpdirs import InTemporaryDirectory from nose.tools import assert_true, assert_equal, assert_raises +from nibabel.testing import clear_and_catch_warnings, error_warnings, suppress_warnings affine = [[-1.5, 0, 0, 90], [0, 1.5, 0, -85], @@ -516,6 +517,10 @@ def test_wrong_shape(): np.random.randn(3, 10), np.random.randn(2, 9), ): - img = ci.Cifti2Image(data, hdr) + with clear_and_catch_warnings(): + with error_warnings(): + assert_raises(UserWarning, ci.Cifti2Image, data, hdr) + with suppress_warnings(): + img = ci.Cifti2Image(data, hdr) assert_raises(ValueError, img.to_file_map) From 907231367c1812fc57a798e119f3303f6226ee91 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Fri, 5 Jul 2019 11:46:46 +0100 Subject: [PATCH 6/9] Fixed style errors --- nibabel/cifti2/cifti2.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/nibabel/cifti2/cifti2.py b/nibabel/cifti2/cifti2.py index 52d3a92415..d2448c8939 100644 --- a/nibabel/cifti2/cifti2.py +++ b/nibabel/cifti2/cifti2.py @@ -1463,9 +1463,10 @@ def to_file_map(self, file_map=None): extension = Cifti2Extension(content=self.header.to_xml()) header.extensions.append(extension) if header.get_data_shape() != self.header.matrix.get_data_shape(): - raise ValueError("Dataobj shape {} does not match shape expected from CIFTI-2 header {}".format( - self._dataobj.shape, self.header.matrix.get_data_shape() - )) + raise ValueError( + "Dataobj shape {} does not match shape expected from CIFTI-2 header {}".format( + self._dataobj.shape, self.header.matrix.get_data_shape() + )) # if intent code is not set, default to unknown CIFTI if header.get_intent()[0] == 'none': header.set_intent('NIFTI_INTENT_CONNECTIVITY_UNKNOWN') From 875f93c4e5a6b1cedb34cebfadcd76dcaa1c99bf Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Fri, 12 Jul 2019 09:26:48 -0700 Subject: [PATCH 7/9] Apply suggestions from code review Co-Authored-By: Chris Markiewicz --- nibabel/cifti2/cifti2.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nibabel/cifti2/cifti2.py b/nibabel/cifti2/cifti2.py index d2448c8939..c68208cfee 100644 --- a/nibabel/cifti2/cifti2.py +++ b/nibabel/cifti2/cifti2.py @@ -1233,7 +1233,7 @@ def get_data_shape(self): from . import cifti2_axes if len(self.mapped_indices) == 0: return () - base_shape = [-1 for _ in range(max(self.mapped_indices) + 1)] + base_shape = [-1] * (max(self.mapped_indices) + 1) for mim in self: size = len(cifti2_axes.from_index_mapping(mim)) for idx in mim.applies_to_matrix_dimension: @@ -1382,7 +1382,7 @@ def __init__(self, self._nifti_header.set_data_dtype(dataobj.dtype) self.update_headers() - if self._nifti_header.get_data_shape() != self.header.matrix.get_data_shape(): + if self._dataobj.shape != self.header.matrix.get_data_shape(): warn("Dataobj shape {} does not match shape expected from CIFTI-2 header {}".format( self._dataobj.shape, self.header.matrix.get_data_shape() )) @@ -1462,7 +1462,7 @@ def to_file_map(self, file_map=None): header = self._nifti_header extension = Cifti2Extension(content=self.header.to_xml()) header.extensions.append(extension) - if header.get_data_shape() != self.header.matrix.get_data_shape(): + if self._dataobj.shape != self.header.matrix.get_data_shape(): raise ValueError( "Dataobj shape {} does not match shape expected from CIFTI-2 header {}".format( self._dataobj.shape, self.header.matrix.get_data_shape() From d0a484b15be8bf1b398eb9fcc1b6241205fa66d3 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Fri, 12 Jul 2019 09:34:30 -0700 Subject: [PATCH 8/9] Set undefined dimensions to size None --- nibabel/cifti2/cifti2.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nibabel/cifti2/cifti2.py b/nibabel/cifti2/cifti2.py index c68208cfee..fdd52b871f 100644 --- a/nibabel/cifti2/cifti2.py +++ b/nibabel/cifti2/cifti2.py @@ -1229,11 +1229,13 @@ def get_axis(self, index): def get_data_shape(self): """ Returns data shape expected based on the CIFTI-2 header + + Any dimensions omitted in the CIFIT-2 header will be given a default size of None. """ from . import cifti2_axes if len(self.mapped_indices) == 0: return () - base_shape = [-1] * (max(self.mapped_indices) + 1) + base_shape = [None] * (max(self.mapped_indices) + 1) for mim in self: size = len(cifti2_axes.from_index_mapping(mim)) for idx in mim.applies_to_matrix_dimension: From c531421827f81b627fcafa5184d0d5ced2549744 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Fri, 12 Jul 2019 09:47:01 -0700 Subject: [PATCH 9/9] Update nibabel/cifti2/cifti2.py Co-Authored-By: Chris Markiewicz --- nibabel/cifti2/cifti2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nibabel/cifti2/cifti2.py b/nibabel/cifti2/cifti2.py index fdd52b871f..104c9396cd 100644 --- a/nibabel/cifti2/cifti2.py +++ b/nibabel/cifti2/cifti2.py @@ -1230,7 +1230,7 @@ def get_data_shape(self): """ Returns data shape expected based on the CIFTI-2 header - Any dimensions omitted in the CIFIT-2 header will be given a default size of None. + Any dimensions omitted in the CIFTI-2 header will be given a default size of None. """ from . import cifti2_axes if len(self.mapped_indices) == 0: