Skip to content

Commit 840cf24

Browse files
committed
ENH: Add aliases for set/get_data_dtype on NIfTI images
1 parent fa66516 commit 840cf24

File tree

1 file changed

+174
-0
lines changed

1 file changed

+174
-0
lines changed

nibabel/nifti1.py

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1799,6 +1799,10 @@ class Nifti1Pair(analyze.AnalyzeImage):
17991799
_meta_sniff_len = header_class.sizeof_hdr
18001800
rw = True
18011801

1802+
# If a _dtype_alias has been set, it can only be resolved by inspecting
1803+
# the data at serialization time
1804+
_dtype_alias = None
1805+
18021806
def __init__(self, dataobj, affine, header=None,
18031807
extra=None, file_map=None, dtype=None):
18041808
# Special carve-out for 64 bit integers
@@ -2043,6 +2047,127 @@ def set_sform(self, affine, code=None, **kwargs):
20432047
else:
20442048
self._affine[:] = self._header.get_best_affine()
20452049

2050+
def set_data_dtype(self, datatype):
2051+
""" Set numpy dtype for data from code, dtype, type or alias
2052+
2053+
Using :py:class:`int` or ``"int"`` is disallowed, as these types
2054+
will be interpreted as ``np.int64``, which is almost never desired.
2055+
``np.int64`` is permitted for those intent on making poor choices.
2056+
2057+
The following aliases are defined to allow for flexible specification:
2058+
2059+
* ``'mask'`` - Alias for ``uint8``
2060+
* ``'compat'`` - The smallest Analyze-compatible datatype
2061+
(``uint8``, ``int16``, ``int32``, ``float32``)
2062+
* ``'smallest'`` - The smallest Analyze-compatible integer
2063+
(``uint8``, ``int16``, ``int32``)
2064+
2065+
Dynamic aliases are resolved when ``get_data_dtype()`` is called
2066+
with a ``finalize=True`` flag. Until then, these aliases are not
2067+
written to the header and will not persist to new images.
2068+
2069+
Examples
2070+
--------
2071+
>>> ints = np.arange(24, dtype='i4').reshape((2,3,4))
2072+
2073+
>>> img = Nifti1Image(ints, np.eye(4))
2074+
>>> img.set_data_dtype(np.uint8)
2075+
>>> img.get_data_dtype()
2076+
dtype('uint8')
2077+
>>> img.set_data_dtype('mask')
2078+
>>> img.get_data_dtype()
2079+
dtype('uint8')
2080+
>>> img.set_data_dtype('compat')
2081+
>>> img.get_data_dtype()
2082+
'compat'
2083+
>>> img.get_data_dtype(finalize=True)
2084+
dtype('uint8')
2085+
>>> img.get_data_dtype()
2086+
dtype('uint8')
2087+
>>> img.set_data_dtype('smallest')
2088+
>>> img.get_data_dtype()
2089+
'smallest'
2090+
>>> img.get_data_dtype(finalize=True)
2091+
dtype('uint8')
2092+
>>> img.get_data_dtype()
2093+
dtype('uint8')
2094+
2095+
Note that floating point values will not be coerced to ``int``
2096+
2097+
>>> floats = np.arange(24, dtype='f4').reshape((2,3,4))
2098+
>>> img = Nifti1Image(ints, np.eye(4))
2099+
2100+
>>> arr = np.arange(1000, 1024, dtype='i4').reshape((2,3,4))
2101+
>>> img = Nifti1Image(arr, np.eye(4))
2102+
>>> img.set_data_dtype('smallest')
2103+
>>> img.set_data_dtype('implausible') #doctest: +IGNORE_EXCEPTION_DETAIL
2104+
Traceback (most recent call last):
2105+
...
2106+
HeaderDataError: data dtype "implausible" not recognized
2107+
>>> img.set_data_dtype('none') #doctest: +IGNORE_EXCEPTION_DETAIL
2108+
Traceback (most recent call last):
2109+
...
2110+
HeaderDataError: data dtype "none" known but not supported
2111+
>>> img.set_data_dtype(np.void) #doctest: +IGNORE_EXCEPTION_DETAIL
2112+
Traceback (most recent call last):
2113+
...
2114+
HeaderDataError: data dtype "<type 'numpy.void'>" known but not supported
2115+
>>> img.set_data_dtype('int') #doctest: +IGNORE_EXCEPTION_DETAIL
2116+
Traceback (most recent call last):
2117+
...
2118+
ValueError: Invalid data type 'int'. Specify a sized integer, e.g., 'uint8' or numpy.int16.
2119+
>>> img.set_data_dtype(int) #doctest: +IGNORE_EXCEPTION_DETAIL
2120+
Traceback (most recent call last):
2121+
...
2122+
ValueError: Invalid data type 'int'. Specify a sized integer, e.g., 'uint8' or numpy.int16.
2123+
>>> img.set_data_dtype('int64')
2124+
>>> img.get_data_dtype() == np.dtype('int64')
2125+
True
2126+
"""
2127+
# Numpy dtype comparison can fail in odd ways, check for aliases only if str
2128+
if isinstance(datatype, str):
2129+
# Static aliases
2130+
if datatype == 'mask':
2131+
datatype = 'u1'
2132+
# Dynamic aliases
2133+
elif datatype in ('compat', 'smallest'):
2134+
self._dtype_alias = datatype
2135+
return
2136+
2137+
self._dtype_alias = None
2138+
super().set_data_dtype(datatype)
2139+
2140+
def get_data_dtype(self, finalize=False):
2141+
""" Get numpy dtype for data
2142+
2143+
If ``set_data_dtype()`` has been called with an alias
2144+
and ``finalize`` is ``False``, return the alias.
2145+
If ``finalize`` is ``True``, determine the appropriate dtype
2146+
from the image data object and set the final dtype in the
2147+
header before returning it.
2148+
"""
2149+
if self._dtype_alias is None:
2150+
return super().get_data_dtype()
2151+
if not finalize:
2152+
return self._dtype_alias
2153+
2154+
datatype = None
2155+
if self._dtype_alias == 'compat':
2156+
datatype = _get_smallest_dtype(self._dataobj)
2157+
descrip = "an Analyze-compatible dtype"
2158+
elif self._dtype_alias == 'smallest':
2159+
datatype = _get_smallest_dtype(self._dataobj, ftypes=())
2160+
descrip = "an integer type with fewer than 64 bits"
2161+
else:
2162+
raise ValueError(f"Unknown dtype alias {self._dtype_alias}.")
2163+
if datatype is None:
2164+
dt = get_obj_dtype(self._dataobj)
2165+
raise ValueError(f"Cannot automatically cast array (of type {dt}) to {descrip}."
2166+
" Please set_data_dtype() to an explicit data type.")
2167+
2168+
self.set_data_dtype(datatype) # Clears the alias
2169+
return super().get_data_dtype()
2170+
20462171
def as_reoriented(self, ornt):
20472172
"""Apply an orientation change and return a new image
20482173
@@ -2136,3 +2261,52 @@ def save(img, filename):
21362261
Nifti1Image.instance_to_filename(img, filename)
21372262
except ImageFileError:
21382263
Nifti1Pair.instance_to_filename(img, filename)
2264+
2265+
2266+
def _get_smallest_dtype(
2267+
arr,
2268+
itypes=(np.uint8, np.int16, np.int32),
2269+
ftypes=(np.float32,),
2270+
):
2271+
""" Return the smallest "sensible" dtype that will hold the array data
2272+
2273+
The purpose of this function is to support automatic type selection
2274+
for serialization, so "sensible" here means well-supported in the NIfTI-1 world.
2275+
2276+
For floating point data, select between single- and double-precision.
2277+
For integer data, select among uint8, int16 and int32.
2278+
2279+
The test is for min/max range, so float64 is pretty unlikely to be hit.
2280+
2281+
Returns ``None`` if these dtypes do not suffice.
2282+
2283+
>>> _get_smallest_dtype(np.array([0, 1]))
2284+
dtype('uint8')
2285+
>>> _get_smallest_dtype(np.array([-1, 1]))
2286+
dtype('int16')
2287+
>>> _get_smallest_dtype(np.array([0, 256]))
2288+
dtype('int16')
2289+
>>> _get_smallest_dtype(np.array([-65536, 65536]))
2290+
dtype('int32')
2291+
>>> _get_smallest_dtype(np.array([-2147483648, 2147483648]))
2292+
>>> _get_smallest_dtype(np.array([1.]))
2293+
dtype('float32')
2294+
>>> _get_smallest_dtype(np.array([2. ** 1000]))
2295+
>>> _get_smallest_dtype(np.float128(2) ** 2000)
2296+
>>> _get_smallest_dtype(np.array([1+0j]))
2297+
"""
2298+
arr = np.asanyarray(arr)
2299+
if np.issubdtype(arr.dtype, np.floating):
2300+
test_dts = ftypes
2301+
info = np.finfo
2302+
elif np.issubdtype(arr.dtype, np.integer):
2303+
test_dts = itypes
2304+
info = np.iinfo
2305+
else:
2306+
return None
2307+
2308+
mn, mx = np.min(arr), np.max(arr)
2309+
for dt in test_dts:
2310+
dtinfo = info(dt)
2311+
if dtinfo.min <= mn and mx <= dtinfo.max:
2312+
return np.dtype(dt)

0 commit comments

Comments
 (0)