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''' Very simple spatial image class 10 11The image class maintains the association between a 3D (or greater) 12array, and an affine transform that maps voxel coordinates to some real 13world space. It also has a ``header`` - some standard set of meta-data 14that is specific to the image format - and ``extra`` - a dictionary 15container for any other metadata. 16 17It has attributes: 18 19 * extra 20 21methods: 22 23 * .get_data() 24 * .get_affine() 25 * .get_header() 26 * .set_shape(shape) 27 * .to_filename(fname) - writes data to filename(s) derived from 28 ``fname``, where the derivation may differ between formats. 29 * to_file_map() - save image to files with which the image is already 30 associated. 31 * .get_shape() (Deprecated) 32 33properties: 34 35 * shape 36 37classmethods: 38 39 * from_filename(fname) - make instance by loading from filename 40 * instance_to_filename(img, fname) - save ``img`` instance to 41 filename ``fname``. 42 43There are several ways of writing data. 44======================================= 45 46There is the usual way, which is the default:: 47 48 img.to_filename(fname) 49 50and that is, to take the data encapsulated by the image and cast it to 51the datatype the header expects, setting any available header scaling 52into the header to help the data match. 53 54You can load the data into an image from file with:: 55 56 img.from_filename(fname) 57 58The image stores its associated files in its ``files`` attribute. In 59order to just save an image, for which you know there is an associated 60filename, or other storage, you can do:: 61 62 img.to_file_map() 63 64You can get the data out again with of:: 65 66 img.get_data() 67 68Less commonly, for some image types that support it, you might want to 69fetch out the unscaled array via the header:: 70 71 unscaled_data = img.get_unscaled_data() 72 73Analyze-type images (including nifti) support this, but others may not 74(MINC, for example). 75 76Sometimes you might to avoid any loss of precision by making the 77data type the same as the input:: 78 79 hdr = img.get_header() 80 hdr.set_data_dtype(data.dtype) 81 img.to_filename(fname) 82 83Files interface 84=============== 85 86The image has an attribute ``file_map``. This is a mapping, that has keys 87corresponding to the file types that an image needs for storage. For 88example, the Analyze data format needs an ``image`` and a ``header`` 89file type for storage: 90 91 >>> import nibabel as nib 92 >>> data = np.arange(24, dtype='f4').reshape((2,3,4)) 93 >>> img = nib.AnalyzeImage(data, np.eye(4)) 94 >>> sorted(img.file_map) 95 ['header', 'image'] 96 97The values of ``file_map`` are not in fact files but objects with 98attributes ``filename``, ``fileobj`` and ``pos``. 99 100The reason for this interface, is that the contents of files has to 101contain enough information so that an existing image instance can save 102itself back to the files pointed to in ``file_map``. When a file holder 103holds active file-like objects, then these may be affected by the 104initial file read; in this case, the contains file-like objects need to 105carry the position at which a write (with ``to_files``) should place the 106data. The ``file_map`` contents should therefore be such, that this will 107work: 108 109 >>> # write an image to files 110 >>> from StringIO import StringIO #23dt : BytesIO 111 >>> file_map = nib.AnalyzeImage.make_file_map() 112 >>> file_map['image'].fileobj = StringIO() #23dt : BytesIO 113 >>> file_map['header'].fileobj = StringIO() #23dt : BytesIO 114 >>> img = nib.AnalyzeImage(data, np.eye(4)) 115 >>> img.file_map = file_map 116 >>> img.to_file_map() 117 >>> # read it back again from the written files 118 >>> img2 = nib.AnalyzeImage.from_file_map(file_map) 119 >>> np.all(img2.get_data() == data) 120 True 121 >>> # write, read it again 122 >>> img2.to_file_map() 123 >>> img3 = nib.AnalyzeImage.from_file_map(file_map) 124 >>> np.all(img3.get_data() == data) 125 True 126 127''' 128 129import warnings 130 131import numpy as np 132 133from .filename_parser import types_filenames, TypesFilenamesError 134from .fileholders import FileHolder 135from .volumeutils import shape_zoom_affine 136 137 138class HeaderDataError(Exception): 139 ''' Class to indicate error in getting or setting header data ''' 140 pass 141 142 143class HeaderTypeError(Exception): 144 ''' Class to indicate error in parameters into header functions ''' 145 pass 146 147 148class Header(object): 149 ''' Template class to implement header protocol ''' 150 default_x_flip = True 151 152 def __init__(self, 153 data_dtype=np.float32, 154 shape=(0,), 155 zooms=None): 156 self.set_data_dtype(data_dtype) 157 self._zooms = () 158 self.set_data_shape(shape) 159 if not zooms is None: 160 self.set_zooms(zooms) 161 162 @classmethod 163 def from_header(klass, header=None): 164 if header is None: 165 return klass() 166 # I can't do isinstance here because it is not necessarily true 167 # that a subclass has exactly the same interface as it's parent 168 # - for example Nifti1Images inherit from Analyze, but have 169 # different field names 170 if type(header) == klass: 171 return header.copy() 172 return klass(header.get_data_dtype(), 173 header.get_data_shape(), 174 header.get_zooms()) 175 176 @classmethod 177 def from_fileobj(klass, fileobj): 178 raise NotImplementedError 179 180 def write_to(self, fileobj): 181 raise NotImplementedError 182 183 def __eq__(self, other): 184 return ((self.get_data_dtype(), 185 self.get_data_shape(), 186 self.get_zooms()) == 187 (other.get_data_dtype(), 188 other.get_data_shape(), 189 other.get_zooms())) 190 191 def __ne__(self, other): 192 return not self == other 193 194 def copy(self): 195 ''' Copy object to independent representation 196 197 The copy should not be affected by any changes to the original 198 object. 199 ''' 200 return self.__class__(self._dtype, self._shape, self._zooms) 201 202 def get_data_dtype(self): 203 return self._dtype 204 205 def set_data_dtype(self, dtype): 206 self._dtype = np.dtype(dtype) 207 208 def get_data_shape(self): 209 return self._shape 210 211 def set_data_shape(self, shape): 212 ndim = len(shape) 213 if ndim == 0: 214 self._shape = (0,) 215 self._zooms = (1.0,) 216 return 217 self._shape = tuple([int(s) for s in shape]) 218 # set any unset zooms to 1.0 219 nzs = min(len(self._zooms), ndim) 220 self._zooms = self._zooms[:nzs] + (1.0,) * (ndim-nzs) 221 222 def get_zooms(self): 223 return self._zooms 224 225 def set_zooms(self, zooms): 226 zooms = tuple([float(z) for z in zooms]) 227 shape = self.get_data_shape() 228 ndim = len(shape) 229 if len(zooms) != ndim: 230 raise HeaderDataError('Expecting %d zoom values for ndim %d' 231 % (ndim, ndim)) 232 if len([z for z in zooms if z < 0]): 233 raise HeaderDataError('zooms must be positive') 234 self._zooms = zooms 235 236 def get_base_affine(self): 237 shape = self.get_data_shape() 238 zooms = self.get_zooms() 239 return shape_zoom_affine(shape, zooms, 240 self.default_x_flip) 241 242 get_default_affine = get_base_affine 243 244 def data_to_fileobj(self, data, fileobj): 245 ''' Write image data to file in fortran order ''' 246 dtype = self.get_data_dtype() 247 fileobj.write(data.astype(dtype).tostring(order='F')) 248 249 def data_from_fileobj(self, fileobj): 250 ''' Read data in fortran order ''' 251 dtype = self.get_data_dtype() 252 shape = self.get_data_shape() 253 data_size = int(np.prod(shape) * dtype.itemsize) 254 data_bytes = fileobj.read(data_size) 255 return np.ndarray(shape, dtype, data_bytes, order='F') 256 257 258class ImageDataError(Exception): 259 pass 260 261 262class ImageFileError(Exception): 263 pass 264 265 266class SpatialImage(object): 267 header_class = Header 268 files_types = (('image', None),) 269 _compressed_exts = () 270 271 ''' Template class for images ''' 272 def __init__(self, data, affine, header=None, 273 extra=None, file_map=None): 274 ''' Initialize image 275 276 The image is a combination of (array, affine matrix, header), with 277 optional metadata in `extra`, and filename / file-like objects contained 278 in the `file_map` mapping. 279 280 Parameters 281 ---------- 282 data : object 283 image data. It should be some object that retuns an array 284 from ``np.asanyarray``. It should have a ``shape`` attribute or 285 property 286 affine : None or (4,4) array-like 287 homogenous affine giving relationship between voxel coordinates and 288 world coordinates. Affine can also be None. In this case, 289 ``obj.get_affine()`` also returns None, and the affine as written to 290 disk will depend on the file format. 291 header : None or mapping or header instance, optional 292 metadata for this image format 293 extra : None or mapping, optional 294 metadata to associate with image that cannot be stored in the 295 metadata of this image type 296 file_map : mapping, optional 297 mapping giving file information for this image format 298 ''' 299 self._data = data 300 if not affine is None: 301 # Check that affine is array-like 4,4. Maybe this is too strict at 302 # this abstract level, but so far I think all image formats we know 303 # do need 4,4. 304 # Copy affine to isolate from environment. Specify float type to 305 # avoid surprising integer rounding when setting values into affine 306 affine = np.array(affine, dtype=np.float64, copy=True) 307 if not affine.shape == (4,4): 308 raise ValueError('Affine should be shape 4,4') 309 self._affine = affine 310 if extra is None: 311 extra = {} 312 self.extra = extra 313 self._header = self.header_class.from_header(header) 314 # if header not specified, get data type from input array 315 if header is None: 316 if hasattr(data, 'dtype'): 317 self._header.set_data_dtype(data.dtype) 318 # make header correspond with image and affine 319 self.update_header() 320 if file_map is None: 321 file_map = self.__class__.make_file_map() 322 self.file_map = file_map 323 self._load_cache = None 324 325 def update_header(self): 326 ''' Update header from information in image''' 327 self._header.set_data_shape(self._data.shape) 328 329 def __str__(self): 330 shape = self.shape 331 affine = self.get_affine() 332 return '\n'.join(( 333 str(self.__class__), 334 'data shape %s' % (shape,), 335 'affine: ', 336 '%s' % affine, 337 'metadata:', 338 '%s' % self._header)) 339 340 def get_data(self): 341 return np.asanyarray(self._data) 342 343 @property 344 def shape(self): 345 return self._data.shape 346 347 def get_shape(self): 348 """ Return shape for image 349 350 This function deprecated; please use the ``shape`` property instead 351 """ 352 warnings.warn('Please use the shape property instead of get_shape', 353 DeprecationWarning, 354 stacklevel=2) 355 return self.shape 356 357 def get_data_dtype(self): 358 return self._header.get_data_dtype() 359 360 def set_data_dtype(self, dtype): 361 self._header.set_data_dtype(dtype) 362 363 def get_affine(self): 364 return self._affine 365 366 def get_header(self): 367 return self._header 368 369 def get_filename(self): 370 ''' Fetch the image filename 371 372 Parameters 373 ---------- 374 None 375 376 Returns 377 ------- 378 fname : None or str 379 Returns None if there is no filename, or a filename string. 380 If an image may have several filenames assoctiated with it 381 (e.g Analyze ``.img, .hdr`` pair) then we return the more 382 characteristic filename (the ``.img`` filename in the case of 383 Analyze') 384 ''' 385 # which filename is returned depends on the ordering of the 386 # 'files_types' class attribute - we return the name 387 # corresponding to the first in that tuple 388 characteristic_type = self.files_types[0][0] 389 return self.file_map[characteristic_type].filename 390 391 def set_filename(self, filename): 392 ''' Sets the files in the object from a given filename 393 394 The different image formats may check whether the filename has 395 an extension characteristic of the format, and raise an error if 396 not. 397 398 Parameters 399 ---------- 400 filename : str 401 If the image format only has one file associated with it, 402 this will be the only filename set into the image 403 ``.file_map`` attribute. Otherwise, the image instance will 404 try and guess the other filenames from this given filename. 405 ''' 406 self.file_map = self.__class__.filespec_to_file_map(filename) 407 408 @classmethod 409 def from_filename(klass, filename): 410 file_map = klass.filespec_to_file_map(filename) 411 return klass.from_file_map(file_map) 412 413 @classmethod 414 def from_filespec(klass, filespec): 415 warnings.warn('``from_filespec`` class method is deprecated\n' 416 'Please use the ``from_filename`` class method ' 417 'instead', 418 DeprecationWarning, stacklevel=2) 419 klass.from_filename(filespec) 420 421 @classmethod 422 def from_file_map(klass, file_map): 423 raise NotImplementedError 424 425 @classmethod 426 def from_files(klass, file_map): 427 warnings.warn('``from_files`` class method is deprecated\n' 428 'Please use the ``from_file_map`` class method ' 429 'instead', 430 DeprecationWarning, stacklevel=2) 431 return klass.from_file_map(file_map) 432 433 @classmethod 434 def filespec_to_file_map(klass, filespec): 435 try: 436 filenames = types_filenames(filespec, 437 klass.files_types, 438 trailing_suffixes=klass._compressed_exts) 439 except TypesFilenamesError: 440 raise ImageFileError('Filespec "%s" does not look right for ' 441 'class %s ' % (filespec, klass)) 442 file_map = {} 443 for key, fname in filenames.items(): 444 file_map[key] = FileHolder(filename=fname) 445 return file_map 446 447 @classmethod 448 def filespec_to_files(klass, filespec): 449 warnings.warn('``filespec_to_files`` class method is deprecated\n' 450 'Please use the ``filespec_to_file_map`` class method ' 451 'instead', 452 DeprecationWarning, stacklevel=2) 453 return klass.filespec_to_file_map(filespec) 454 455 def to_filename(self, filename): 456 ''' Write image to files implied by filename string 457 458 Parameters 459 ---------- 460 filename : str 461 filename to which to save image. We will parse `filename` 462 with ``filespec_to_file_map`` to work out names for image, 463 header etc. 464 465 Returns 466 ------- 467 None 468 ''' 469 self.file_map = self.filespec_to_file_map(filename) 470 self.to_file_map() 471 472 def to_filespec(self, filename): 473 warnings.warn('``to_filespec`` is deprecated, please ' 474 'use ``to_filename`` instead', 475 DeprecationWarning, stacklevel=2) 476 self.to_filename(filename) 477 478 def to_file_map(self, file_map=None): 479 raise NotImplementedError 480 481 def to_files(self, file_map=None): 482 warnings.warn('``to_files`` method is deprecated\n' 483 'Please use the ``to_file_map`` method ' 484 'instead', 485 DeprecationWarning, stacklevel=2) 486 self.to_file_map(file_map) 487 488 @classmethod 489 def make_file_map(klass, mapping=None): 490 ''' Class method to make files holder for this image type 491 492 Parameters 493 ---------- 494 mapping : None or mapping, optional 495 mapping with keys corresponding to image file types (such as 496 'image', 'header' etc, depending on image class) and values 497 that are filenames or file-like. Default is None 498 499 Returns 500 ------- 501 file_map : dict 502 dict with string keys given by first entry in tuples in 503 sequence klass.files_types, and values of type FileHolder, 504 where FileHolder objects have default values, other than 505 those given by `mapping` 506 ''' 507 if mapping is None: 508 mapping = {} 509 file_map = {} 510 for key, ext in klass.files_types: 511 file_map[key] = FileHolder() 512 mapval = mapping.get(key, None) 513 if isinstance(mapval, basestring): 514 file_map[key].filename = mapval 515 elif hasattr(mapval, 'tell'): 516 file_map[key].fileobj = mapval 517 return file_map 518 519 @classmethod 520 def load(klass, filename): 521 return klass.from_filename(filename) 522 523 @classmethod 524 def instance_to_filename(klass, img, filename): 525 ''' Save `img` in our own format, to name implied by `filename` 526 527 This is a class method 528 529 Parameters 530 ---------- 531 img : ``spatialimage`` instance 532 In fact, an object with the API of ``spatialimage`` - 533 specifically ``get_data``, ``get_affine``, ``get_header`` and 534 ``extra``. 535 filename : str 536 Filename, implying name to which to save image. 537 ''' 538 img = klass.from_image(img) 539 img.to_filename(filename) 540 541 @classmethod 542 def from_image(klass, img): 543 ''' Class method to create new instance of own class from `img` 544 545 Parameters 546 ---------- 547 img : ``spatialimage`` instance 548 In fact, an object with the API of ``spatialimage`` - 549 specifically ``get_data``, ``get_affine``, ``get_header`` and 550 ``extra``. 551 552 Returns 553 ------- 554 cimg : ``spatialimage`` instance 555 Image, of our own class 556 ''' 557 return klass(img.get_data(), 558 img.get_affine(), 559 klass.header_class.from_header(img.get_header()), 560 extra=img.extra.copy()) 561 562