From d41b35355c225d11e3e3495a85c87871e8e4cab9 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Thu, 10 Feb 2022 21:48:20 -0500 Subject: [PATCH 01/10] API: Warn on creation of Nifti images with 64-bit ints --- nibabel/analyze.py | 5 ++++- nibabel/nifti1.py | 12 ++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/nibabel/analyze.py b/nibabel/analyze.py index 3daaaf1175..dcdeb0844b 100644 --- a/nibabel/analyze.py +++ b/nibabel/analyze.py @@ -914,12 +914,15 @@ class AnalyzeImage(SpatialImage): ImageArrayProxy = ArrayProxy def __init__(self, dataobj, affine, header=None, - extra=None, file_map=None): + extra=None, file_map=None, dtype=None): super(AnalyzeImage, self).__init__( dataobj, affine, header, extra, file_map) # Reset consumable values self._header.set_data_offset(0) self._header.set_slope_inter(None, None) + + if dtype is not None: + self.set_data_dtype(dtype) __init__.__doc__ = SpatialImage.__init__.__doc__ def get_data_dtype(self): diff --git a/nibabel/nifti1.py b/nibabel/nifti1.py index b9df63a450..5a93d5eaad 100644 --- a/nibabel/nifti1.py +++ b/nibabel/nifti1.py @@ -1754,12 +1754,20 @@ class Nifti1Pair(analyze.AnalyzeImage): rw = True def __init__(self, dataobj, affine, header=None, - extra=None, file_map=None): + extra=None, file_map=None, dtype=None): + danger_dts = (np.dtype("int64"), np.dtype("uint64")) + if header is None and dtype is None and dataobj.dtype in danger_dts: + msg = (f"Image data has type {dataobj.dtype}, which may cause " + "incompatibilities with other tools. This will error in " + "NiBabel 5.0. This warning can be silenced " + f"by passing the dtype argument to {self.__class__.__name__}().") + warnings.warn(msg, FutureWarning, stacklevel=2) super(Nifti1Pair, self).__init__(dataobj, affine, header, extra, - file_map) + file_map, + dtype) # Force set of s/q form when header is None unless affine is also None if header is None and affine is not None: self._affine2header() From c40a18b5ee0ea359224df163387d8c53a2a07842 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Thu, 10 Feb 2022 22:18:03 -0500 Subject: [PATCH 02/10] ENH: Allow save-time passing of on-disk dtype to to_* methods --- nibabel/analyze.py | 11 +++++++++-- nibabel/cifti2/cifti2.py | 4 ++-- nibabel/filebasedimages.py | 19 +++++++++++-------- nibabel/spm99analyze.py | 4 ++-- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/nibabel/analyze.py b/nibabel/analyze.py index dcdeb0844b..ddc9d29805 100644 --- a/nibabel/analyze.py +++ b/nibabel/analyze.py @@ -992,7 +992,7 @@ def _get_fileholders(file_map): """ return file_map['header'], file_map['image'] - def to_file_map(self, file_map=None): + def to_file_map(self, file_map=None, dtype=None): """ Write image to `file_map` or contained ``self.file_map`` Parameters @@ -1000,15 +1000,21 @@ def to_file_map(self, file_map=None): file_map : None or mapping, optional files mapping. If None (default) use object's ``file_map`` attribute instead + dtype : dtype-like, optional + The on-disk data type to coerce the data array. """ if file_map is None: file_map = self.file_map data = np.asanyarray(self.dataobj) self.update_header() hdr = self._header - out_dtype = self.get_data_dtype() # Store consumable values for later restore offset = hdr.get_data_offset() + data_dtype = hdr.get_data_dtype() + # Override dtype conditionally + if dtype is not None: + hdr.set_data_dtype(dtype) + out_dtype = hdr.get_data_dtype() # Scalars of slope, offset to get immutable values slope = hdr['scl_slope'].item() if hdr.has_data_slope else np.nan inter = hdr['scl_inter'].item() if hdr.has_data_intercept else np.nan @@ -1048,6 +1054,7 @@ def to_file_map(self, file_map=None): self.file_map = file_map # Restore any changed consumable values hdr.set_data_offset(offset) + hdr.set_data_dtype(data_dtype) if hdr.has_data_slope: hdr['scl_slope'] = slope if hdr.has_data_intercept: diff --git a/nibabel/cifti2/cifti2.py b/nibabel/cifti2/cifti2.py index 85d93129f3..32b7a7d8d3 100644 --- a/nibabel/cifti2/cifti2.py +++ b/nibabel/cifti2/cifti2.py @@ -1460,7 +1460,7 @@ def from_image(klass, img): return img raise NotImplementedError - def to_file_map(self, file_map=None): + def to_file_map(self, file_map=None, dtype=None): """ Write image to `file_map` or contained ``self.file_map`` Parameters @@ -1493,7 +1493,7 @@ def to_file_map(self, file_map=None): # If qform not set, reset pixdim values so Nifti2 does not complain if header['qform_code'] == 0: header['pixdim'][:4] = 1 - img = Nifti2Image(data, None, header) + img = Nifti2Image(data, None, header, dtype=dtype) img.to_file_map(file_map or self.file_map) def update_headers(self): diff --git a/nibabel/filebasedimages.py b/nibabel/filebasedimages.py index 3fd5b5fc8f..21fe754edf 100644 --- a/nibabel/filebasedimages.py +++ b/nibabel/filebasedimages.py @@ -299,8 +299,8 @@ def filespec_to_file_map(klass, filespec): file_map[key] = FileHolder(filename=fname) return file_map - def to_filename(self, filename): - """ Write image to files implied by filename string + def to_filename(self, filename, **kwargs): + r""" Write image to files implied by filename string Parameters ---------- @@ -308,15 +308,17 @@ def to_filename(self, filename): filename to which to save image. We will parse `filename` with ``filespec_to_file_map`` to work out names for image, header etc. + \*\*kwargs : keyword arguments + Keyword arguments to format-specific save Returns ------- None """ self.file_map = self.filespec_to_file_map(filename) - self.to_file_map() + self.to_file_map(**kwargs) - def to_file_map(self, file_map=None): + def to_file_map(self, file_map=None, **kwargs): raise NotImplementedError @classmethod @@ -552,13 +554,14 @@ def from_bytes(klass, bytestring): file_map = klass.make_file_map({'image': bio, 'header': bio}) return klass.from_file_map(file_map) - def to_bytes(self): - """ Return a ``bytes`` object with the contents of the file that would + def to_bytes(self, **kwargs): + r""" Return a ``bytes`` object with the contents of the file that would be written if the image were saved. Parameters ---------- - None + \*\*kwargs : keyword arguments + Keyword arguments that may be passed to ``img.to_file_map()`` Returns ------- @@ -569,5 +572,5 @@ def to_bytes(self): raise NotImplementedError("to_bytes() is undefined for multi-file images") bio = io.BytesIO() file_map = self.make_file_map({'image': bio, 'header': bio}) - self.to_file_map(file_map) + self.to_file_map(file_map, **kwargs) return bio.getvalue() diff --git a/nibabel/spm99analyze.py b/nibabel/spm99analyze.py index bfa169ebe3..687a94da5a 100644 --- a/nibabel/spm99analyze.py +++ b/nibabel/spm99analyze.py @@ -308,7 +308,7 @@ def from_file_map(klass, file_map, *, mmap=True, keep_file_open=None): ret._affine = np.dot(ret._affine, to_111) return ret - def to_file_map(self, file_map=None): + def to_file_map(self, file_map=None, dtype=None): """ Write image to `file_map` or contained ``self.file_map`` Extends Analyze ``to_file_map`` method by writing ``mat`` file @@ -321,7 +321,7 @@ def to_file_map(self, file_map=None): """ if file_map is None: file_map = self.file_map - super(Spm99AnalyzeImage, self).to_file_map(file_map) + super(Spm99AnalyzeImage, self).to_file_map(file_map, dtype=dtype) mat = self._affine if mat is None: return From e4bd1a7eac4656029107437b7198223b8b1ea464 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Thu, 10 Feb 2022 22:20:15 -0500 Subject: [PATCH 03/10] ENH: Allow nibabel.save() to accept kwargs to pass to img.to_filename() --- nibabel/loadsave.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/nibabel/loadsave.py b/nibabel/loadsave.py index 1f736409dd..04fee7b6a2 100644 --- a/nibabel/loadsave.py +++ b/nibabel/loadsave.py @@ -130,8 +130,8 @@ def guessed_image_type(filename): raise ImageFileError(f'Cannot work out file type of "{filename}"') -def save(img, filename): - """ Save an image to file adapting format to `filename` +def save(img, filename, **kwargs): + r""" Save an image to file adapting format to `filename` Parameters ---------- @@ -139,6 +139,8 @@ def save(img, filename): image to save filename : str or os.PathLike filename (often implying filenames) to which to save `img`. + \*\*kwargs : keyword arguments + Keyword arguments to format-specific save Returns ------- @@ -148,7 +150,7 @@ def save(img, filename): # Save the type as expected try: - img.to_filename(filename) + img.to_filename(filename, **kwargs) except ImageFileError: pass else: @@ -196,7 +198,7 @@ def save(img, filename): # Here, we either have a klass or a converted image. if converted is None: converted = klass.from_image(img) - converted.to_filename(filename) + converted.to_filename(filename, **kwargs) @deprecate_with_version('read_img_data deprecated. ' From 6b51f0f74d8f02907553ecce3b91d68ebd9f875f Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Fri, 18 Feb 2022 16:03:38 -0500 Subject: [PATCH 04/10] ENH: Black-list int/"int" dtypes, add clarifying comment --- nibabel/nifti1.py | 52 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/nibabel/nifti1.py b/nibabel/nifti1.py index 5a93d5eaad..d6d11811b0 100644 --- a/nibabel/nifti1.py +++ b/nibabel/nifti1.py @@ -881,6 +881,51 @@ def set_data_shape(self, shape): shape = (-1, 1, 1) + shape[3:] super(Nifti1Header, self).set_data_shape(shape) + def set_data_dtype(self, datatype): + """ Set numpy dtype for data from code or dtype or type + + Using :py:class:`int` or ``"int"`` is disallowed, as these types + will be interpreted as ``np.int64``, which is almost never desired. + ``np.int64`` is permitted for those intent on making poor choices. + + Examples + -------- + >>> hdr = Nifti1Header() + >>> hdr.set_data_dtype(np.uint8) + >>> hdr.get_data_dtype() + dtype('uint8') + >>> hdr.set_data_dtype(np.dtype(np.uint8)) + >>> hdr.get_data_dtype() + dtype('uint8') + >>> hdr.set_data_dtype('implausible') #doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + HeaderDataError: data dtype "implausible" not recognized + >>> hdr.set_data_dtype('none') #doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + HeaderDataError: data dtype "none" known but not supported + >>> hdr.set_data_dtype(np.void) #doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + HeaderDataError: data dtype "" known but not supported + >>> hdr.set_data_dtype('int') + Traceback (most recent call last): + ... + ValueError: Invalid data type 'int'. Specify a sized integer, e.g., 'uint8' or numpy.int16. + >>> hdr.set_data_dtype(int) + Traceback (most recent call last): + ... + ValueError: Invalid data type 'int'. Specify a sized integer, e.g., 'uint8' or numpy.int16. + >>> hdr.set_data_dtype('int64') + >>> hdr.get_data_dtype() + dtype('int64') + """ + if not isinstance(datatype, np.dtype) and datatype in (int, "int"): + raise ValueError(f"Invalid data type {datatype!r}. Specify a sized integer, " + "e.g., 'uint8' or numpy.int16.") + super().set_data_dtype(datatype) + def get_qform_quaternion(self): """ Compute quaternion from b, c, d of quaternion @@ -1755,6 +1800,13 @@ class Nifti1Pair(analyze.AnalyzeImage): def __init__(self, dataobj, affine, header=None, extra=None, file_map=None, dtype=None): + # Special carve-out for 64 bit integers + # See GitHub issues + # * https://github.com/nipy/nibabel/issues/1046 + # * https://github.com/nipy/nibabel/issues/1089 + # This only applies to NIfTI because the parent Analyze formats did + # not support 64-bit integer data, so `set_data_dtype(int64)` would + # already fail. danger_dts = (np.dtype("int64"), np.dtype("uint64")) if header is None and dtype is None and dataobj.dtype in danger_dts: msg = (f"Image data has type {dataobj.dtype}, which may cause " From 61c5fc7d92e644ebfee4d14808959cc6a4032381 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Fri, 18 Feb 2022 16:09:42 -0500 Subject: [PATCH 05/10] RF: Add a more resilient method for fetching data dtype --- nibabel/arrayproxy.py | 14 ++++++++++++++ nibabel/nifti1.py | 3 ++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/nibabel/arrayproxy.py b/nibabel/arrayproxy.py index 0ec6da0ca5..6cda3a206a 100644 --- a/nibabel/arrayproxy.py +++ b/nibabel/arrayproxy.py @@ -412,3 +412,17 @@ def reshape_dataobj(obj, shape): """ return (obj.reshape(shape) if hasattr(obj, 'reshape') else np.reshape(obj, shape)) + + +def get_obj_dtype(obj): + """ Get the effective dtype of an array-like object """ + if is_proxy(obj): + # Read and potentially apply scaling to one value + idx = (0,) * len(obj.shape) + return obj[idx].dtype + elif hasattr(obj, "dtype"): + # Trust the dtype (probably an ndarray) + return obj.dtype + else: + # Coerce; this could be expensive but we don't know what we can do with it + return np.asanyarray(obj).dtype diff --git a/nibabel/nifti1.py b/nibabel/nifti1.py index d6d11811b0..94ca721946 100644 --- a/nibabel/nifti1.py +++ b/nibabel/nifti1.py @@ -17,6 +17,7 @@ import numpy.linalg as npl from numpy.compat.py3k import asstr +from .arrayproxy import get_obj_dtype from .optpkg import optional_package from .filebasedimages import SerializableImage from .volumeutils import Recoder, make_dt_codes, endian_codes @@ -1808,7 +1809,7 @@ def __init__(self, dataobj, affine, header=None, # not support 64-bit integer data, so `set_data_dtype(int64)` would # already fail. danger_dts = (np.dtype("int64"), np.dtype("uint64")) - if header is None and dtype is None and dataobj.dtype in danger_dts: + if header is None and dtype is None and get_obj_dtype(dataobj) in danger_dts: msg = (f"Image data has type {dataobj.dtype}, which may cause " "incompatibilities with other tools. This will error in " "NiBabel 5.0. This warning can be silenced " From f36470c422cc738b6443544481d4c272adf42e56 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Wed, 2 Mar 2022 19:31:14 -0500 Subject: [PATCH 06/10] TEST: Clean up tests to avoid triggering warnings/exceptions --- nibabel/nifti1.py | 12 ++++++------ nibabel/tests/test_analyze.py | 7 ++++++- nibabel/tests/test_nifti1.py | 12 ++++++------ 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/nibabel/nifti1.py b/nibabel/nifti1.py index 94ca721946..c41b9a8ed3 100644 --- a/nibabel/nifti1.py +++ b/nibabel/nifti1.py @@ -910,17 +910,17 @@ def set_data_dtype(self, datatype): Traceback (most recent call last): ... HeaderDataError: data dtype "" known but not supported - >>> hdr.set_data_dtype('int') + >>> hdr.set_data_dtype('int') #doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... ValueError: Invalid data type 'int'. Specify a sized integer, e.g., 'uint8' or numpy.int16. - >>> hdr.set_data_dtype(int) + >>> hdr.set_data_dtype(int) #doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... ValueError: Invalid data type 'int'. Specify a sized integer, e.g., 'uint8' or numpy.int16. >>> hdr.set_data_dtype('int64') - >>> hdr.get_data_dtype() - dtype('int64') + >>> hdr.get_data_dtype() == np.dtype('int64') + True """ if not isinstance(datatype, np.dtype) and datatype in (int, "int"): raise ValueError(f"Invalid data type {datatype!r}. Specify a sized integer, " @@ -1926,7 +1926,7 @@ def set_qform(self, affine, code=None, strip_shears=True, **kwargs): Examples -------- - >>> data = np.arange(24).reshape((2,3,4)) + >>> data = np.arange(24, dtype='f4').reshape((2,3,4)) >>> aff = np.diag([2, 3, 4, 1]) >>> img = Nifti1Pair(data, aff) >>> img.get_qform() @@ -2009,7 +2009,7 @@ def set_sform(self, affine, code=None, **kwargs): Examples -------- - >>> data = np.arange(24).reshape((2,3,4)) + >>> data = np.arange(24, dtype='f4').reshape((2,3,4)) >>> aff = np.diag([2, 3, 4, 1]) >>> img = Nifti1Pair(data, aff) >>> img.get_sform() diff --git a/nibabel/tests/test_analyze.py b/nibabel/tests/test_analyze.py index 95b33f5069..78b96669a5 100644 --- a/nibabel/tests/test_analyze.py +++ b/nibabel/tests/test_analyze.py @@ -290,7 +290,12 @@ def assert_set_dtype(dt_spec, np_dtype): # Test aliases to Python types assert_set_dtype(float, np.float64) # float64 always supported np_sys_int = np.dtype(int).type # int could be 32 or 64 bit - if np_sys_int in self.supported_np_types: # no int64 for Analyze + if issubclass(self.header_class, Nifti1Header): + # We don't allow int aliases in Nifti + with pytest.raises(ValueError): + hdr = self.header_class() + hdr.set_data_dtype(int) + elif np_sys_int in self.supported_np_types: # no int64 for Analyze assert_set_dtype(int, np_sys_int) hdr = self.header_class() for inp in all_unsupported_types: diff --git a/nibabel/tests/test_nifti1.py b/nibabel/tests/test_nifti1.py index 96a3e3c387..91957a4897 100644 --- a/nibabel/tests/test_nifti1.py +++ b/nibabel/tests/test_nifti1.py @@ -770,7 +770,7 @@ def test_none_qsform(self): img_klass = self.image_class hdr_klass = img_klass.header_class shape = (2, 3, 4) - data = np.arange(24).reshape(shape) + data = np.arange(24, dtype='f4').reshape((2, 3, 4)) # With specified affine aff = from_matvec(euler2mat(0.1, 0.2, 0.3), [11, 12, 13]) for hdr in (None, hdr_klass()): @@ -1040,7 +1040,7 @@ def test_affines_init(self): # is some thoughts by Mark Jenkinson: # http://nifti.nimh.nih.gov/nifti-1/documentation/nifti1fields/nifti1fields_pages/qsform_brief_usage IC = self.image_class - arr = np.arange(24).reshape((2, 3, 4)) + arr = np.arange(24, dtype='f4').reshape((2, 3, 4)) aff = np.diag([2, 3, 4, 1]) # Default is sform set, qform not set img = IC(arr, aff) @@ -1073,7 +1073,7 @@ def test_affines_init(self): def test_read_no_extensions(self): IC = self.image_class - arr = np.arange(24).reshape((2, 3, 4)) + arr = np.arange(24, dtype='f4').reshape((2, 3, 4)) img = IC(arr, np.eye(4)) assert len(img.header.extensions) == 0 img_rt = bytesio_round_trip(img) @@ -1110,7 +1110,7 @@ class TestNifti1Image(TestNifti1Pair): def test_offset_errors(self): # Test that explicit offset too low raises error IC = self.image_class - arr = np.arange(24).reshape((2, 3, 4)) + arr = np.arange(24, dtype='f4').reshape((2, 3, 4)) img = IC(arr, np.eye(4)) assert img.header.get_data_offset() == 0 # Saving with zero offset is OK @@ -1365,7 +1365,7 @@ def test_loadsave_cycle(self): def test_load(self): # test module level load. We try to load a nii and an .img and a .hdr # and expect to get a nifti back of single or pair type - arr = np.arange(24).reshape((2, 3, 4)) + arr = np.arange(24, dtype='f4').reshape((2, 3, 4)) aff = np.diag([2, 3, 4, 1]) simg = self.single_class(arr, aff) pimg = self.pair_class(arr, aff) @@ -1439,7 +1439,7 @@ def test_rt_bias(self): def test_reoriented_dim_info(self): # Check that dim_info is reoriented correctly - arr = np.arange(24).reshape((2, 3, 4)) + arr = np.arange(24, dtype='f4').reshape((2, 3, 4)) # Start as RAS aff = np.diag([2, 3, 4, 1]) simg = self.single_class(arr, aff) From e0cb73ae2ac734dd84e5e941193f3f146997be03 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Wed, 2 Mar 2022 19:42:47 -0500 Subject: [PATCH 07/10] TEST: Check behavior of get_obj_dtype --- nibabel/tests/test_arrayproxy.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/nibabel/tests/test_arrayproxy.py b/nibabel/tests/test_arrayproxy.py index 887b231464..80806cae3a 100644 --- a/nibabel/tests/test_arrayproxy.py +++ b/nibabel/tests/test_arrayproxy.py @@ -19,7 +19,7 @@ import numpy as np -from ..arrayproxy import (ArrayProxy, is_proxy, reshape_dataobj) +from ..arrayproxy import (ArrayProxy, is_proxy, reshape_dataobj, get_obj_dtype) from ..openers import ImageOpener from ..nifti1 import Nifti1Header @@ -240,6 +240,28 @@ def test_reshaped_is_proxy(): prox.reshape((2, -1, 5)) +def test_get_obj_dtype(): + # Check get_obj_dtype(obj) returns same result as array(obj).dtype + bio = BytesIO() + shape = (2, 3, 4) + hdr = Nifti1Header() + arr = np.arange(24, dtype=np.int16).reshape(shape) + write_raw_data(arr, hdr, bio) + hdr.set_slope_inter(2, 10) + prox = ArrayProxy(bio, hdr) + assert get_obj_dtype(prox) == np.dtype('float64') + assert get_obj_dtype(np.array(prox)) == np.dtype('float64') + hdr.set_slope_inter(1, 0) + prox = ArrayProxy(bio, hdr) + assert get_obj_dtype(prox) == np.dtype('int16') + assert get_obj_dtype(np.array(prox)) == np.dtype('int16') + + class ArrGiver: + def __array__(self): + return arr + assert get_obj_dtype(ArrGiver()) == np.dtype('int16') + + def test_get_unscaled(): # Test fetch of raw array class FunkyHeader2(FunkyHeader): From 39a2f26d6d9b49ca29e07a89dd6b7f6baeebd977 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Wed, 2 Mar 2022 20:05:07 -0500 Subject: [PATCH 08/10] TEST: Add tests for __init__ and to_filename --- nibabel/tests/test_analyze.py | 29 +++++++++++++++++++++++++++++ nibabel/tests/test_spm99analyze.py | 1 + 2 files changed, 30 insertions(+) diff --git a/nibabel/tests/test_analyze.py b/nibabel/tests/test_analyze.py index 78b96669a5..0477da718a 100644 --- a/nibabel/tests/test_analyze.py +++ b/nibabel/tests/test_analyze.py @@ -764,6 +764,20 @@ def test_affine_44(self): with pytest.raises(ValueError): IC(data, np.diag([2, 3, 4])) + def test_dtype_init_arg(self): + # data_dtype can be set by argument in absence of header + img_klass = self.image_class + arr = np.arange(24, dtype=np.int16).reshape((2, 3, 4)) + aff = np.eye(4) + for dtype in self.supported_np_types: + img = img_klass(arr, aff, dtype=dtype) + assert img.get_data_dtype() == dtype + # It can also override the header dtype + hdr = img.header + for dtype in self.supported_np_types: + img = img_klass(arr, aff, hdr, dtype=dtype) + assert img.get_data_dtype() == dtype + def test_offset_to_zero(self): # Check offset is always set to zero when creating images img_klass = self.image_class @@ -878,6 +892,21 @@ def test_no_finite_values(self): img_back = self.image_class.from_file_map(fm) assert_array_equal(img_back.dataobj, 0) + def test_dtype_to_filename_arg(self): + # data_dtype can be set by argument in absence of header + img_klass = self.image_class + arr = np.arange(24, dtype=np.int16).reshape((2, 3, 4)) + aff = np.eye(4) + img = img_klass(arr, aff) + fname = 'test' + img_klass.files_types[0][1] + with InTemporaryDirectory(): + for dtype in self.supported_np_types: + img.to_filename(fname, dtype=dtype) + new_img = img_klass.from_filename(fname) + assert new_img.get_data_dtype() == dtype + # data_type is reset after write + assert img.get_data_dtype() == np.int16 + def test_unsupported(): # analyze does not support uint32 diff --git a/nibabel/tests/test_spm99analyze.py b/nibabel/tests/test_spm99analyze.py index e84a18ea4f..492faf1d51 100644 --- a/nibabel/tests/test_spm99analyze.py +++ b/nibabel/tests/test_spm99analyze.py @@ -414,6 +414,7 @@ class TestSpm99AnalyzeImage(test_analyze.TestAnalyzeImage, ImageScalingMixin): test_header_updating = needs_scipy(test_analyze.TestAnalyzeImage.test_header_updating) test_offset_to_zero = needs_scipy(test_analyze.TestAnalyzeImage.test_offset_to_zero) test_big_offset_exts = needs_scipy(test_analyze.TestAnalyzeImage.test_big_offset_exts) + test_dtype_to_filename_arg = needs_scipy(test_analyze.TestAnalyzeImage.test_dtype_to_filename_arg) test_header_scaling = needs_scipy(ImageScalingMixin.test_header_scaling) test_int_int_scaling = needs_scipy(ImageScalingMixin.test_int_int_scaling) test_write_scaling = needs_scipy(ImageScalingMixin.test_write_scaling) From a32491e3e64b443b3e905db46c6eccc9895fd1ab Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Wed, 2 Mar 2022 20:45:07 -0500 Subject: [PATCH 09/10] ENH: Suppress warnings by passing more reasonable data types --- nibabel/funcs.py | 4 ++-- nibabel/tests/test_filehandles.py | 2 +- nibabel/tests/test_funcs.py | 4 ++-- nibabel/tests/test_processing.py | 10 +++++----- nibabel/tests/test_round_trip.py | 3 +-- 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/nibabel/funcs.py b/nibabel/funcs.py index df5eb0e96f..e5db0477b0 100644 --- a/nibabel/funcs.py +++ b/nibabel/funcs.py @@ -35,7 +35,7 @@ def squeeze_image(img): -------- >>> import nibabel as nf >>> shape = (10,20,30,1,1) - >>> data = np.arange(np.prod(shape)).reshape(shape) + >>> data = np.arange(np.prod(shape), dtype='int32').reshape(shape) >>> affine = np.eye(4) >>> img = nf.Nifti1Image(data, affine) >>> img.shape == (10, 20, 30, 1, 1) @@ -47,7 +47,7 @@ def squeeze_image(img): If the data are 3D then last dimensions of 1 are ignored >>> shape = (10,1,1) - >>> data = np.arange(np.prod(shape)).reshape(shape) + >>> data = np.arange(np.prod(shape), dtype='int32').reshape(shape) >>> img = nf.ni1.Nifti1Image(data, affine) >>> img.shape == (10, 1, 1) True diff --git a/nibabel/tests/test_filehandles.py b/nibabel/tests/test_filehandles.py index 5f08af95ec..ed1e80e70a 100644 --- a/nibabel/tests/test_filehandles.py +++ b/nibabel/tests/test_filehandles.py @@ -26,7 +26,7 @@ def test_multiload(): # Make a tiny image, save, load many times. If we are leaking filehandles, # this will cause us to run out and generate an error N = SOFT_LIMIT + 100 - arr = np.arange(24).reshape((2, 3, 4)) + arr = np.arange(24, dtype='int32').reshape((2, 3, 4)) img = Nifti1Image(arr, np.eye(4)) imgs = [] try: diff --git a/nibabel/tests/test_funcs.py b/nibabel/tests/test_funcs.py index f6f7b59d34..44266f25fd 100644 --- a/nibabel/tests/test_funcs.py +++ b/nibabel/tests/test_funcs.py @@ -51,12 +51,12 @@ def test_concat(): # second position. for data0_shape in all_shapes: data0_numel = np.asarray(data0_shape).prod() - data0 = np.arange(data0_numel).reshape(data0_shape) + data0 = np.arange(data0_numel, dtype='int32').reshape(data0_shape) img0_mem = Nifti1Image(data0, affine) for data1_shape in all_shapes: data1_numel = np.asarray(data1_shape).prod() - data1 = np.arange(data1_numel).reshape(data1_shape) + data1 = np.arange(data1_numel, dtype='int32').reshape(data1_shape) img1_mem = Nifti1Image(data1, affine) img2_mem = Nifti1Image(data1, affine + 1) # bad affine diff --git a/nibabel/tests/test_processing.py b/nibabel/tests/test_processing.py index 633502ffd9..401966faa9 100644 --- a/nibabel/tests/test_processing.py +++ b/nibabel/tests/test_processing.py @@ -97,7 +97,7 @@ def test_adapt_affine(): @needs_scipy def test_resample_from_to(caplog): # Test resampling from image to image / image space - data = np.arange(24).reshape((2, 3, 4)) + data = np.arange(24, dtype='int32').reshape((2, 3, 4)) affine = np.diag([-4, 5, 6, 1]) img = Nifti1Image(data, affine) img.header['descrip'] = 'red shirt image' @@ -186,7 +186,7 @@ def test_resample_from_to(caplog): out = resample_from_to(img, (img_2d.shape, img_2d.affine)) assert_array_equal(out.dataobj, data[:, :, 0]) # 4D input and output also OK - data_4d = np.arange(24 * 5).reshape((2, 3, 4, 5)) + data_4d = np.arange(24 * 5, dtype='int32').reshape((2, 3, 4, 5)) img_4d = Nifti1Image(data_4d, affine) out = resample_from_to(img_4d, img_4d) assert_almost_equal(data_4d, out.dataobj) @@ -202,7 +202,7 @@ def test_resample_from_to(caplog): def test_resample_to_output(caplog): # Test routine to sample iamges to output space # Image aligned to output axes - no-op - data = np.arange(24).reshape((2, 3, 4)) + data = np.arange(24, dtype='int32').reshape((2, 3, 4)) img = Nifti1Image(data, np.eye(4)) # Check default resampling img2 = resample_to_output(img) @@ -305,7 +305,7 @@ def test_resample_to_output(caplog): @needs_scipy def test_smooth_image(caplog): # Test image smoothing - data = np.arange(24).reshape((2, 3, 4)) + data = np.arange(24, dtype='int32').reshape((2, 3, 4)) aff = np.diag([-4, 5, 6, 1]) img = Nifti1Image(data, aff) # Zero smoothing is no-op @@ -332,7 +332,7 @@ def test_smooth_image(caplog): with pytest.raises(ValueError): smooth_image(img_2d, [8, 8, 8]) # Isotropic in 4D has zero for last dimension in scalar case - data_4d = np.arange(24 * 5).reshape((2, 3, 4, 5)) + data_4d = np.arange(24 * 5, dtype='int32').reshape((2, 3, 4, 5)) img_4d = Nifti1Image(data_4d, aff) exp_out = spnd.gaussian_filter(data_4d, list(sd) + [0], mode='nearest') assert_array_equal(smooth_image(img_4d, 8).dataobj, exp_out) diff --git a/nibabel/tests/test_round_trip.py b/nibabel/tests/test_round_trip.py index 24e53c05c8..dfc53a2bdb 100644 --- a/nibabel/tests/test_round_trip.py +++ b/nibabel/tests/test_round_trip.py @@ -17,9 +17,8 @@ def round_trip(arr, out_dtype): - img = Nifti1Image(arr, np.eye(4)) + img = Nifti1Image(arr, np.eye(4), dtype=out_dtype) img.file_map['image'].fileobj = BytesIO() - img.set_data_dtype(out_dtype) img.to_file_map() back = Nifti1Image.from_file_map(img.file_map) # Recover array and calculated scaling from array proxy object From fa6651685350f8908f79e9167e4d37f4254cb4dd Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Wed, 2 Mar 2022 21:16:51 -0500 Subject: [PATCH 10/10] TEST: Test int64 warnings for header-less NIfTIs --- nibabel/tests/test_nifti1.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/nibabel/tests/test_nifti1.py b/nibabel/tests/test_nifti1.py index 91957a4897..7652f77e42 100644 --- a/nibabel/tests/test_nifti1.py +++ b/nibabel/tests/test_nifti1.py @@ -765,6 +765,23 @@ class TestNifti1Pair(tana.TestAnalyzeImage, tspm.ImageScalingMixin): image_class = Nifti1Pair supported_np_types = TestNifti1PairHeader.supported_np_types + def test_int64_warning(self): + # Verify that initializing with (u)int64 data and no + # header/dtype info produces a warning + img_klass = self.image_class + hdr_klass = img_klass.header_class + for dtype in (np.int64, np.uint64): + data = np.arange(24, dtype=dtype).reshape((2, 3, 4)) + with pytest.warns(FutureWarning): + img_klass(data, np.eye(4)) + # No warnings if we're explicit, though + with clear_and_catch_warnings(): + warnings.simplefilter("error") + img_klass(data, np.eye(4), dtype=dtype) + hdr = hdr_klass() + hdr.set_data_dtype(dtype) + img_klass(data, np.eye(4), hdr) + def test_none_qsform(self): # Check that affine gets set to q/sform if header is None img_klass = self.image_class