@@ -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