@@ -1799,6 +1799,10 @@ class Nifti1Pair(analyze.AnalyzeImage):
1799
1799
_meta_sniff_len = header_class .sizeof_hdr
1800
1800
rw = True
1801
1801
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
+
1802
1806
def __init__ (self , dataobj , affine , header = None ,
1803
1807
extra = None , file_map = None , dtype = None ):
1804
1808
# Special carve-out for 64 bit integers
@@ -2043,6 +2047,127 @@ def set_sform(self, affine, code=None, **kwargs):
2043
2047
else :
2044
2048
self ._affine [:] = self ._header .get_best_affine ()
2045
2049
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
+
2046
2171
def as_reoriented (self , ornt ):
2047
2172
"""Apply an orientation change and return a new image
2048
2173
@@ -2136,3 +2261,52 @@ def save(img, filename):
2136
2261
Nifti1Image .instance_to_filename (img , filename )
2137
2262
except ImageFileError :
2138
2263
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