1# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- 2# vi: set ft=python sts=4 ts=4 sw=4 et: 3### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4# 5# See COPYING file distributed along with the NiBabel package for the 6# copyright and license terms. 7# 8### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9# module imports 10""" Utilities to load and save image objects """ 11 12import os 13import numpy as np 14 15from .filename_parser import splitext_addext, _stringify_path 16from .openers import ImageOpener 17from .filebasedimages import ImageFileError 18from .imageclasses import all_image_classes 19from .arrayproxy import is_proxy 20from .deprecated import deprecate_with_version 21 22 23def load(filename, **kwargs): 24 r""" Load file given filename, guessing at file type 25 26 Parameters 27 ---------- 28 filename : str or os.PathLike 29 specification of file to load 30 \*\*kwargs : keyword arguments 31 Keyword arguments to format-specific load 32 33 Returns 34 ------- 35 img : ``SpatialImage`` 36 Image of guessed type 37 """ 38 filename = _stringify_path(filename) 39 40 # Check file exists and is not empty 41 try: 42 stat_result = os.stat(filename) 43 except OSError: 44 raise FileNotFoundError(f"No such file or no access: '{filename}'") 45 if stat_result.st_size <= 0: 46 raise ImageFileError(f"Empty file: '{filename}'") 47 48 sniff = None 49 for image_klass in all_image_classes: 50 is_valid, sniff = image_klass.path_maybe_image(filename, sniff) 51 if is_valid: 52 img = image_klass.from_filename(filename, **kwargs) 53 return img 54 55 raise ImageFileError(f'Cannot work out file type of "{filename}"') 56 57 58@deprecate_with_version('guessed_image_type deprecated.', '3.2', '5.0') 59def guessed_image_type(filename): 60 """ Guess image type from file `filename` 61 62 Parameters 63 ---------- 64 filename : str 65 File name containing an image 66 67 Returns 68 ------- 69 image_class : class 70 Class corresponding to guessed image type 71 """ 72 sniff = None 73 for image_klass in all_image_classes: 74 is_valid, sniff = image_klass.path_maybe_image(filename, sniff) 75 if is_valid: 76 return image_klass 77 78 raise ImageFileError(f'Cannot work out file type of "{filename}"') 79 80 81def save(img, filename): 82 """ Save an image to file adapting format to `filename` 83 84 Parameters 85 ---------- 86 img : ``SpatialImage`` 87 image to save 88 filename : str or os.PathLike 89 filename (often implying filenames) to which to save `img`. 90 91 Returns 92 ------- 93 None 94 """ 95 filename = _stringify_path(filename) 96 97 # Save the type as expected 98 try: 99 img.to_filename(filename) 100 except ImageFileError: 101 pass 102 else: 103 return 104 105 # Be nice to users by making common implicit conversions 106 froot, ext, trailing = splitext_addext(filename, ('.gz', '.bz2')) 107 lext = ext.lower() 108 109 # Special-case Nifti singles and Pairs 110 # Inline imports, as this module really shouldn't reference any image type 111 from .nifti1 import Nifti1Image, Nifti1Pair 112 from .nifti2 import Nifti2Image, Nifti2Pair 113 114 klass = None 115 converted = None 116 117 if type(img) == Nifti1Image and lext in ('.img', '.hdr'): 118 klass = Nifti1Pair 119 elif type(img) == Nifti2Image and lext in ('.img', '.hdr'): 120 klass = Nifti2Pair 121 elif type(img) == Nifti1Pair and lext == '.nii': 122 klass = Nifti1Image 123 elif type(img) == Nifti2Pair and lext == '.nii': 124 klass = Nifti2Image 125 else: # arbitrary conversion 126 valid_klasses = [klass for klass in all_image_classes 127 if ext in klass.valid_exts] 128 if not valid_klasses: # if list is empty 129 raise ImageFileError(f'Cannot work out file type of "{filename}"') 130 131 # Got a list of valid extensions, but that's no guarantee 132 # the file conversion will work. So, try each image 133 # in order... 134 for klass in valid_klasses: 135 try: 136 converted = klass.from_image(img) 137 break 138 except Exception as e: 139 err = e 140 # ... and if none of them work, raise an error. 141 if converted is None: 142 raise err 143 144 # Here, we either have a klass or a converted image. 145 if converted is None: 146 converted = klass.from_image(img) 147 converted.to_filename(filename) 148 149 150@deprecate_with_version('read_img_data deprecated. ' 151 'Please use ``img.dataobj.get_unscaled()`` instead.', 152 '3.2', 153 '5.0') 154def read_img_data(img, prefer='scaled'): 155 """ Read data from image associated with files 156 157 If you want unscaled data, please use ``img.dataobj.get_unscaled()`` 158 instead. If you want scaled data, use ``img.get_fdata()`` (which will cache 159 the loaded array) or ``np.array(img.dataobj)`` (which won't cache the 160 array). If you want to load the data as for a modified header, save the 161 image with the modified header, and reload. 162 163 Parameters 164 ---------- 165 img : ``SpatialImage`` 166 Image with valid image file in ``img.file_map``. Unlike the 167 ``img.get_fdata()`` method, this function returns the data read 168 from the image file, as specified by the *current* image header 169 and *current* image files. 170 prefer : str, optional 171 Can be 'scaled' - in which case we return the data with the 172 scaling suggested by the format, or 'unscaled', in which case we 173 return, if we can, the raw data from the image file, without the 174 scaling applied. 175 176 Returns 177 ------- 178 arr : ndarray 179 array as read from file, given parameters in header 180 181 Notes 182 ----- 183 Summary: please use the ``get_data`` method of `img` instead of this 184 function unless you are sure what you are doing. 185 186 In general, you will probably prefer ``prefer='scaled'``, because 187 this gives the data as the image format expects to return it. 188 189 Use `prefer` == 'unscaled' with care; the modified Analyze-type 190 formats such as SPM formats, and nifti1, specify that the image data 191 array is given by the raw data on disk, multiplied by a scalefactor 192 and maybe with the addition of a constant. This function, with 193 ``unscaled`` returns the data on the disk, without these 194 format-specific scalings applied. Please use this funciton only if 195 you absolutely need the unscaled data, and the magnitude of the 196 data, as given by the scalefactor, is not relevant to your 197 application. The Analyze-type formats have a single scalefactor +/- 198 offset per image on disk. If you do not care about the absolute 199 values, and will be removing the mean from the data, then the 200 unscaled values will have preserved intensity ratios compared to the 201 mean-centered scaled data. However, this is not necessarily true of 202 other formats with more complicated scaling - such as MINC. 203 """ 204 if prefer not in ('scaled', 'unscaled'): 205 raise ValueError(f'Invalid string "{prefer}" for "prefer"') 206 hdr = img.header 207 if not hasattr(hdr, 'raw_data_from_fileobj'): 208 # We can only do scaled 209 if prefer == 'unscaled': 210 raise ValueError("Can only do unscaled for Analyze types") 211 return np.array(img.dataobj) 212 # Analyze types 213 img_fh = img.file_map['image'] 214 img_file_like = (img_fh.filename if img_fh.fileobj is None 215 else img_fh.fileobj) 216 if img_file_like is None: 217 raise ImageFileError('No image file specified for this image') 218 # Check the consumable values in the header 219 hdr = img.header 220 dao = img.dataobj 221 default_offset = hdr.get_data_offset() == 0 222 default_scaling = hdr.get_slope_inter() == (None, None) 223 # If we have a proxy object and the header has any consumed fields, we load 224 # the consumed values back from the proxy 225 if is_proxy(dao) and (default_offset or default_scaling): 226 hdr = hdr.copy() 227 if default_offset and dao.offset != 0: 228 hdr.set_data_offset(dao.offset) 229 if default_scaling and (dao.slope, dao.inter) != (1, 0): 230 hdr.set_slope_inter(dao.slope, dao.inter) 231 with ImageOpener(img_file_like) as fileobj: 232 if prefer == 'scaled': 233 return hdr.data_from_fileobj(fileobj) 234 return hdr.raw_data_from_fileobj(fileobj) 235 236 237@deprecate_with_version('which_analyze_type deprecated.', '3.2', '4.0') 238def which_analyze_type(binaryblock): 239 """ Is `binaryblock` from NIfTI1, NIfTI2 or Analyze header? 240 241 Parameters 242 ---------- 243 binaryblock : bytes 244 The `binaryblock` is 348 bytes that might be NIfTI1, NIfTI2, Analyze, 245 or None of the the above. 246 247 Returns 248 ------- 249 hdr_type : str 250 * a nifti1 header (pair or single) -> return 'nifti1' 251 * a nifti2 header (pair or single) -> return 'nifti2' 252 * an Analyze header -> return 'analyze' 253 * None of the above -> return None 254 255 Notes 256 ----- 257 Algorithm: 258 259 * read in the first 4 bytes from the file as 32-bit int ``sizeof_hdr`` 260 * if ``sizeof_hdr`` is 540 or byteswapped 540 -> assume nifti2 261 * Check for 'ni1', 'n+1' magic -> assume nifti1 262 * if ``sizeof_hdr`` is 348 or byteswapped 348 assume Analyze 263 * Return None 264 """ 265 from .nifti1 import header_dtype 266 hdr_struct = np.ndarray(shape=(), dtype=header_dtype, buffer=binaryblock) 267 bs_hdr_struct = hdr_struct.byteswap() 268 sizeof_hdr = hdr_struct['sizeof_hdr'] 269 bs_sizeof_hdr = bs_hdr_struct['sizeof_hdr'] 270 if 540 in (sizeof_hdr, bs_sizeof_hdr): 271 return 'nifti2' 272 if hdr_struct['magic'] in (b'ni1', b'n+1'): 273 return 'nifti1' 274 if 348 in (sizeof_hdr, bs_sizeof_hdr): 275 return 'analyze' 276 return None 277