diff --git a/doc/source/old/examples.txt b/doc/source/old/examples.txt index 3a472d9a47..b84f5441bf 100644 --- a/doc/source/old/examples.txt +++ b/doc/source/old/examples.txt @@ -32,7 +32,7 @@ and we have made a temporary directory for the files we are going to write: and we've got the path to the nifti example data: >>> from nibabel.testing import data_path as example_data_path - + Loading and saving NIfTI files ============================== @@ -77,7 +77,7 @@ The relevant header information is extracted from the NumPy array. If you query the header information about the dimensionality of the image, it returns the desired values: - >>> print nim.get_header()['dim'] + >>> print nim.header['dim'] [ 4 32 32 16 100 1 1 1] First value shows the number of dimensions in the datset: 4 (good, that's what @@ -110,8 +110,8 @@ preserving as much header information as possible >>> nim2 = nib.Nifti1Image(nim.get_data()[..., :10], ... nim.get_affine(), - ... nim.get_header()) - >>> print nim2.get_header()['dim'] + ... nim.header) + >>> print nim2.header['dim'] [ 4 32 32 16 10 1 1 1] >>> # a filename in our temporary directory >>> fname = pjoin(tmpdir, 'part.hdr.gz') @@ -136,7 +136,7 @@ will first create a NIfTI image with just a single voxel and 50 timepoints >>> nim = nib.Nifti1Image( ... (np.linspace(0,100) + np.random.randn(50)).reshape(1,1,1,50), ... np.eye(4)) - >>> print nim.get_header()['dim'] + >>> print nim.header['dim'] [ 4 1 1 1 50 1 1 1] Depending on the datatype of the input image the detrending process might @@ -154,4 +154,4 @@ source image. >>> nim_detrended = nib.Nifti1Image(data_detrended, ... nim.get_affine(), - ... nim.get_header()) + ... nim.header) diff --git a/doc/source/old/orientation.txt b/doc/source/old/orientation.txt index e7b14e0e33..4efbe73db1 100644 --- a/doc/source/old/orientation.txt +++ b/doc/source/old/orientation.txt @@ -6,7 +6,7 @@ Every image in ``nibabel`` has an orientation. The orientation is the relationship between the voxels in the image array, and millimeters in -some space. +some space. Affines as orientation ---------------------- @@ -85,7 +85,7 @@ the affine after loading, as in:: img = nibabel.load('some_image.img') aff = img.get_affine() x_flipper = np.diag([-1,1,1,1]) - lr_img = nibabel.Nifti1Image(img.get_data, np.dot(x_flipper, aff), img.get_header()) + lr_img = nibabel.Nifti1Image(img.get_data, np.dot(x_flipper, aff), img.header) Affines for Analyze, SPM analyze, and NIFTI ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/nibabel/ecat.py b/nibabel/ecat.py index 495d9ab6c5..7b8efc4656 100644 --- a/nibabel/ecat.py +++ b/nibabel/ecat.py @@ -929,7 +929,7 @@ def to_file_map(self, file_map=None): # It appears to be necessary to load the data before saving even if the # data itself is not used. self.get_data() - hdr = self.get_header() + hdr = self.header mlist = self._mlist subheaders = self.get_subheaders() dir_pos = 512 @@ -944,7 +944,7 @@ def to_file_map(self, file_map=None): hdr.write_to(hdrf) # Write every frames - for index in range(0, self.get_header()['num_frames']): + for index in range(0, self.header['num_frames']): # Move to subheader offset frame_offset = subheaders._get_frame_offset(index) - 512 imgf.seek(frame_offset) diff --git a/nibabel/filebasedimages.py b/nibabel/filebasedimages.py new file mode 100644 index 0000000000..3d73c3cdf5 --- /dev/null +++ b/nibabel/filebasedimages.py @@ -0,0 +1,531 @@ +# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +# +# See COPYING file distributed along with the NiBabel package for the +# copyright and license terms. +# +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +''' Common interface for any image format--volume or surface, binary or xml.''' + +import warnings + +from .externals.six import string_types +from .fileholders import FileHolder +from .filename_parser import (types_filenames, TypesFilenamesError, + splitext_addext) +from .openers import ImageOpener + + +class ImageFileError(Exception): + pass + + +class FileBasedHeader(object): + ''' Template class to implement header protocol ''' + + @classmethod + def from_header(klass, header=None): + if header is None: + return klass() + # I can't do isinstance here because it is not necessarily true + # that a subclass has exactly the same interface as its parent + # - for example Nifti1Images inherit from Analyze, but have + # different field names + if type(header) == klass: + return header.copy() + raise NotImplementedError("Header class requires a conversion" + " from %s to %s" % (klass, type(header))) + + @classmethod + def from_fileobj(klass, fileobj): + raise NotImplementedError + + def write_to(self, fileobj): + raise NotImplementedError + + def __eq__(self, other): + raise NotImplementedError + + def __ne__(self, other): + return not self == other + + def copy(self): + ''' Copy object to independent representation + + The copy should not be affected by any changes to the original + object. + ''' + raise NotImplementedError + + +class FileBasedImage(object): + ''' + This abstract image class defines an interface for loading/saving images + from disk. It doesn't define any image properties. + + It has: + + attributes: + + * extra + + properties: + + * shape + * header + + methods: + * .get_header() (deprecated, use header property instead) + * .to_filename(fname) - writes data to filename(s) derived from + ``fname``, where the derivation may differ between formats. + * to_file_map() - save image to files with which the image is already + associated. + + classmethods: + + * from_filename(fname) - make instance by loading from filename + * from_file_map(fmap) - make instance from file map + * instance_to_filename(img, fname) - save ``img`` instance to + filename ``fname``. + + It also has a ``header`` - some standard set of meta-data that is specific + to the image format, and ``extra`` - a dictionary container for any other + metadata. + + You cannot slice an image, and trying to slice an image generates an + informative TypeError. + + + There are several ways of writing data. + ======================================= + + There is the usual way, which is the default:: + + img.to_filename(fname) + + and that is, to take the data encapsulated by the image and cast it to + the datatype the header expects, setting any available header scaling + into the header to help the data match. + + You can load the data into an image from file with:: + + img.from_filename(fname) + + The image stores its associated files in its ``file_map`` attribute. In + order to just save an image, for which you know there is an associated + filename, or other storage, you can do:: + + img.to_file_map() + + You can get the data out again with:: + + img.get_data() + + Less commonly, for some image types that support it, you might want to + fetch out the unscaled array via the object containing the data:: + + unscaled_data = img.dataoobj.get_unscaled() + + Analyze-type images (including nifti) support this, but others may not + (MINC, for example). + + Sometimes you might to avoid any loss of precision by making the + data type the same as the input:: + + hdr = img.header + hdr.set_data_dtype(data.dtype) + img.to_filename(fname) + + Files interface + =============== + + The image has an attribute ``file_map``. This is a mapping, that has keys + corresponding to the file types that an image needs for storage. For + example, the Analyze data format needs an ``image`` and a ``header`` + file type for storage: + + >>> import numpy as np + >>> import nibabel as nib + >>> data = np.arange(24, dtype='f4').reshape((2,3,4)) + >>> img = nib.AnalyzeImage(data, np.eye(4)) + >>> sorted(img.file_map) + ['header', 'image'] + + The values of ``file_map`` are not in fact files but objects with + attributes ``filename``, ``fileobj`` and ``pos``. + + The reason for this interface, is that the contents of files has to + contain enough information so that an existing image instance can save + itself back to the files pointed to in ``file_map``. When a file holder + holds active file-like objects, then these may be affected by the + initial file read; in this case, the contains file-like objects need to + carry the position at which a write (with ``to_files``) should place the + data. The ``file_map`` contents should therefore be such, that this will + work: + ''' + header_class = FileBasedHeader + _meta_sniff_len = 0 + files_types = (('image', None),) + valid_exts = () + _compressed_suffixes = () + + makeable = True # Used in test code + rw = True # Used in test code + + def __init__(self, header=None, extra=None, file_map=None): + ''' Initialize image + + The image is a combination of (header), with + optional metadata in `extra`, and filename / file-like objects + contained in the `file_map` mapping. + + Parameters + ---------- + header : None or mapping or header instance, optional + metadata for this image format + extra : None or mapping, optional + metadata to associate with image that cannot be stored in the + metadata of this image type + file_map : mapping, optional + mapping giving file information for this image format + ''' + + if header or self.header_class: + self._header = self.header_class.from_header(header) + else: + self._header = None + if extra is None: + extra = {} + self.extra = extra + + if file_map is None: + file_map = self.__class__.make_file_map() + self.file_map = file_map + + @property + def header(self): + return self._header + + def __getitem__(self): + ''' No slicing or dictionary interface for images + ''' + raise TypeError("Cannot slice image objects.") + + def get_header(self): + """ Get header from image + + Please use the `header` property instead of `get_header`; we will + deprecate this method in future versions of nibabel. + """ + warnings.warn('``get_header`` is deprecated.\n' + 'Please use the ``img.header`` property ' + 'instead', + DeprecationWarning, stacklevel=2) + return self.header + + def get_filename(self): + ''' Fetch the image filename + + Parameters + ---------- + None + + Returns + ------- + fname : None or str + Returns None if there is no filename, or a filename string. + If an image may have several filenames assoctiated with it + (e.g Analyze ``.img, .hdr`` pair) then we return the more + characteristic filename (the ``.img`` filename in the case of + Analyze') + ''' + # which filename is returned depends on the ordering of the + # 'files_types' class attribute - we return the name + # corresponding to the first in that tuple + characteristic_type = self.files_types[0][0] + return self.file_map[characteristic_type].filename + + def set_filename(self, filename): + ''' Sets the files in the object from a given filename + + The different image formats may check whether the filename has + an extension characteristic of the format, and raise an error if + not. + + Parameters + ---------- + filename : str + If the image format only has one file associated with it, + this will be the only filename set into the image + ``.file_map`` attribute. Otherwise, the image instance will + try and guess the other filenames from this given filename. + ''' + self.file_map = self.__class__.filespec_to_file_map(filename) + + @classmethod + def from_filename(klass, filename): + file_map = klass.filespec_to_file_map(filename) + return klass.from_file_map(file_map) + + @classmethod + def from_filespec(klass, filespec): + warnings.warn('``from_filespec`` class method is deprecated\n' + 'Please use the ``from_filename`` class method ' + 'instead', + DeprecationWarning, stacklevel=2) + klass.from_filename(filespec) + + @classmethod + def from_file_map(klass, file_map): + raise NotImplementedError + + @classmethod + def from_files(klass, file_map): + warnings.warn('``from_files`` class method is deprecated\n' + 'Please use the ``from_file_map`` class method ' + 'instead', + DeprecationWarning, stacklevel=2) + return klass.from_file_map(file_map) + + @classmethod + def filespec_to_file_map(klass, filespec): + """ Make `file_map` for this class from filename `filespec` + + Class method + + Parameters + ---------- + filespec : str + Filename that might be for this image file type. + + Returns + ------- + file_map : dict + `file_map` dict with (key, value) pairs of (``file_type``, + FileHolder instance), where ``file_type`` is a string giving the + type of the contained file. + + Raises + ------ + ImageFileError + if `filespec` is not recognizable as being a filename for this + image type. + """ + try: + filenames = types_filenames( + filespec, klass.files_types, + trailing_suffixes=klass._compressed_suffixes) + except TypesFilenamesError: + raise ImageFileError( + 'Filespec "{0}" does not look right for class {1}'.format( + filespec, klass)) + file_map = {} + for key, fname in filenames.items(): + file_map[key] = FileHolder(filename=fname) + return file_map + + @classmethod + def filespec_to_files(klass, filespec): + warnings.warn('``filespec_to_files`` class method is deprecated\n' + 'Please use the ``filespec_to_file_map`` class method ' + 'instead', + DeprecationWarning, stacklevel=2) + return klass.filespec_to_file_map(filespec) + + def to_filename(self, filename): + ''' Write image to files implied by filename string + + Parameters + ---------- + filename : str + filename to which to save image. We will parse `filename` + with ``filespec_to_file_map`` to work out names for image, + header etc. + + Returns + ------- + None + ''' + self.file_map = self.filespec_to_file_map(filename) + self.to_file_map() + + def to_filespec(self, filename): + warnings.warn('``to_filespec`` is deprecated, please ' + 'use ``to_filename`` instead', + DeprecationWarning, stacklevel=2) + self.to_filename(filename) + + def to_file_map(self, file_map=None): + raise NotImplementedError + + def to_files(self, file_map=None): + warnings.warn('``to_files`` method is deprecated\n' + 'Please use the ``to_file_map`` method ' + 'instead', + DeprecationWarning, stacklevel=2) + self.to_file_map(file_map) + + @classmethod + def make_file_map(klass, mapping=None): + ''' Class method to make files holder for this image type + + Parameters + ---------- + mapping : None or mapping, optional + mapping with keys corresponding to image file types (such as + 'image', 'header' etc, depending on image class) and values + that are filenames or file-like. Default is None + + Returns + ------- + file_map : dict + dict with string keys given by first entry in tuples in + sequence klass.files_types, and values of type FileHolder, + where FileHolder objects have default values, other than + those given by `mapping` + ''' + if mapping is None: + mapping = {} + file_map = {} + for key, ext in klass.files_types: + file_map[key] = FileHolder() + mapval = mapping.get(key, None) + if isinstance(mapval, string_types): + file_map[key].filename = mapval + elif hasattr(mapval, 'tell'): + file_map[key].fileobj = mapval + return file_map + + load = from_filename + + @classmethod + def instance_to_filename(klass, img, filename): + ''' Save `img` in our own format, to name implied by `filename` + + This is a class method + + Parameters + ---------- + img : ``any FileBasedImage`` instance + + filename : str + Filename, implying name to which to save image. + ''' + img = klass.from_image(img) + img.to_filename(filename) + + @classmethod + def from_image(klass, img): + ''' Class method to create new instance of own class from `img` + + Parameters + ---------- + img : ``spatialimage`` instance + In fact, an object with the API of ``FileBasedImage``. + + Returns + ------- + cimg : ``spatialimage`` instance + Image, of our own class + ''' + raise NotImplementedError() + + @classmethod + def _sniff_meta_for(klass, filename, sniff_nbytes, sniff=None): + """ Sniff metadata for image represented by `filename` + + Parameters + ---------- + filename : str + Filename for an image, or an image header (metadata) file. + If `filename` points to an image data file, and the image type has + a separate "header" file, we work out the name of the header file, + and read from that instead of `filename`. + sniff_nbytes : int + Number of bytes to read from the image or metadata file + sniff : (bytes, fname), optional + The result of a previous call to `_sniff_meta_for`. If fname + matches the computed header file name, `sniff` is returned without + rereading the file. + + Returns + ------- + sniff : None or (bytes, fname) + None if we could not read the image or metadata file. `sniff[0]` + is either length `sniff_nbytes` or the length of the image / + metadata file, whichever is the shorter. `fname` is the name of + the sniffed file. + """ + froot, ext, trailing = splitext_addext(filename, + klass._compressed_suffixes) + # Determine the metadata location + t_fnames = types_filenames( + filename, + klass.files_types, + trailing_suffixes=klass._compressed_suffixes) + meta_fname = t_fnames.get('header', filename) + + # Do not re-sniff if it would be from the same file + if sniff is not None and sniff[1] == meta_fname: + return sniff + + # Attempt to sniff from metadata location + try: + with ImageOpener(meta_fname, 'rb') as fobj: + binaryblock = fobj.read(sniff_nbytes) + except IOError: + return None + return (binaryblock, meta_fname) + + @classmethod + def path_maybe_image(klass, filename, sniff=None, sniff_max=1024): + """ Return True if `filename` may be image matching this class + + Parameters + ---------- + filename : str + Filename for an image, or an image header (metadata) file. + If `filename` points to an image data file, and the image type has + a separate "header" file, we work out the name of the header file, + and read from that instead of `filename`. + sniff : None or (bytes, filename), optional + Bytes content read from a previous call to this method, on another + class, with metadata filename. This allows us to read metadata + bytes once from the image or header, and pass this read set of + bytes to other image classes, therefore saving a repeat read of the + metadata. `filename` is used to validate that metadata would be + read from the same file, re-reading if not. None forces this + method to read the metadata. + sniff_max : int, optional + The maximum number of bytes to read from the metadata. If the + metadata file is long enough, we read this many bytes from the + file, otherwise we read to the end of the file. Longer values + sniff more of the metadata / image file, making it more likely that + the returned sniff will be useful for later calls to + ``path_maybe_image`` for other image classes. + + Returns + ------- + maybe_image : bool + True if `filename` may be valid for an image of this class. + sniff : None or (bytes, filename) + Read bytes content from found metadata. May be None if the file + does not appear to have useful metadata. + """ + froot, ext, trailing = splitext_addext(filename, + klass._compressed_suffixes) + if ext.lower() not in klass.valid_exts: + return False, sniff + if not hasattr(klass.header_class, 'may_contain_header'): + return True, sniff + + # Force re-sniff on too-short sniff + if sniff is not None and len(sniff[0]) < klass._meta_sniff_len: + sniff = None + sniff = klass._sniff_meta_for(filename, + max(klass._meta_sniff_len, sniff_max), + sniff) + if sniff is None or len(sniff[0]) < klass._meta_sniff_len: + return False, sniff + return klass.header_class.may_contain_header(sniff[0]), sniff diff --git a/nibabel/loadsave.py b/nibabel/loadsave.py index f4ce286f61..3333fb9735 100644 --- a/nibabel/loadsave.py +++ b/nibabel/loadsave.py @@ -14,7 +14,7 @@ from .filename_parser import splitext_addext from .openers import ImageOpener -from .spatialimages import ImageFileError +from .filebasedimages import ImageFileError from .imageclasses import all_image_classes from .arrayproxy import is_proxy diff --git a/nibabel/minc1.py b/nibabel/minc1.py index 8a155712df..f5fc0ac918 100644 --- a/nibabel/minc1.py +++ b/nibabel/minc1.py @@ -14,7 +14,7 @@ from .externals.netcdf import netcdf_file -from .spatialimages import Header, SpatialImage +from .spatialimages import SpatialHeader, SpatialImage from .fileslice import canonical_slicers from .deprecated import FutureWarningMixin @@ -264,7 +264,7 @@ def __getitem__(self, sliceobj): return self.minc_file.get_scaled_data(sliceobj) -class MincHeader(Header): +class MincHeader(SpatialHeader): """ Class to contain header for MINC formats """ # We don't use the data layout - this just in case we do later diff --git a/nibabel/parrec.py b/nibabel/parrec.py index cfbc77b1db..af3a5ede5f 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -97,7 +97,7 @@ from locale import getpreferredencoding from .keywordonly import kw_only_meth -from .spatialimages import SpatialImage, Header +from .spatialimages import SpatialHeader, SpatialImage from .eulerangles import euler2mat from .volumeutils import Recoder, array_from_file from .affines import from_matvec, dot_reduce, apply_affine @@ -615,7 +615,7 @@ def __getitem__(self, slicer): return raw_data * slopes[slicer] + inters[slicer] -class PARRECHeader(Header): +class PARRECHeader(SpatialHeader): """PAR/REC header""" def __init__(self, info, image_defs, permit_truncated=False): """ @@ -645,10 +645,9 @@ def __init__(self, info, image_defs, permit_truncated=False): % bitpix) # REC data always little endian dt = np.dtype('uint' + str(bitpix)).newbyteorder('<') - Header.__init__(self, - data_dtype=dt, - shape=self._calc_data_shape(), - zooms=self._calc_zooms()) + super(PARRECHeader, self).__init__(data_dtype=dt, + shape=self._calc_data_shape(), + zooms=self._calc_zooms()) @classmethod def from_header(klass, header=None): diff --git a/nibabel/spatialimages.py b/nibabel/spatialimages.py index 521f8cf307..72bf7dbc58 100644 --- a/nibabel/spatialimages.py +++ b/nibabel/spatialimages.py @@ -112,39 +112,33 @@ data. The ``file_map`` contents should therefore be such, that this will work: - >>> # write an image to files - >>> from io import BytesIO - >>> file_map = nib.AnalyzeImage.make_file_map() - >>> file_map['image'].fileobj = BytesIO() - >>> file_map['header'].fileobj = BytesIO() - >>> img = nib.AnalyzeImage(data, np.eye(4)) - >>> img.file_map = file_map - >>> img.to_file_map() - >>> # read it back again from the written files - >>> img2 = nib.AnalyzeImage.from_file_map(file_map) - >>> np.all(img2.get_data() == data) - True - >>> # write, read it again - >>> img2.to_file_map() - >>> img3 = nib.AnalyzeImage.from_file_map(file_map) - >>> np.all(img3.get_data() == data) - True + >>> # write an image to files + >>> from io import BytesIO + >>> import nibabel as nib + >>> file_map = nib.AnalyzeImage.make_file_map() + >>> file_map['image'].fileobj = BytesIO() + >>> file_map['header'].fileobj = BytesIO() + >>> img = nib.AnalyzeImage(data, np.eye(4)) + >>> img.file_map = file_map + >>> img.to_file_map() + >>> # read it back again from the written files + >>> img2 = nib.AnalyzeImage.from_file_map(file_map) + >>> np.all(img2.get_data() == data) + True + >>> # write, read it again + >>> img2.to_file_map() + >>> img3 = nib.AnalyzeImage.from_file_map(file_map) + >>> np.all(img3.get_data() == data) + True ''' -try: - basestring -except NameError: # python 3 - basestring = str - import warnings import numpy as np -from .filename_parser import (types_filenames, TypesFilenamesError, - splitext_addext) -from .fileholders import FileHolder -from .openers import ImageOpener +from .filebasedimages import FileBasedHeader, FileBasedImage +from .filebasedimages import ImageFileError # needed for back-compat. from .volumeutils import shape_zoom_affine @@ -158,7 +152,7 @@ class HeaderTypeError(Exception): pass -class Header(object): +class SpatialHeader(FileBasedHeader): ''' Template class to implement header protocol ''' default_x_flip = True data_layout = 'F' @@ -312,24 +306,21 @@ def supported_np_types(obj): return set(supported) -class ImageDataError(Exception): - pass +class Header(SpatialHeader): + '''Alias for SpatialHeader; kept for backwards compatibility.''' + def __init__(self, *args, **kwargs): + warnings.warn('Header is deprecated, use SpatialHeader', + DeprecationWarning, stacklevel=2) + super(Header, self).__init__(*args, **kwargs) -class ImageFileError(Exception): +class ImageDataError(Exception): pass -class SpatialImage(object): - ''' Template class for images ''' - header_class = Header - _meta_sniff_len = 0 - files_types = (('image', None),) - valid_exts = () - _compressed_suffixes = () - - makeable = True # Used in test code - rw = True # Used in test code +class SpatialImage(FileBasedImage): + ''' Template class for volumetric (3D/4D) images ''' + header_class = SpatialHeader def __init__(self, dataobj, affine, header=None, extra=None, file_map=None): @@ -358,6 +349,8 @@ def __init__(self, dataobj, affine, header=None, file_map : mapping, optional mapping giving file information for this image format ''' + super(SpatialImage, self).__init__(header=header, extra=extra, + file_map=file_map) self._dataobj = dataobj if not affine is None: # Check that affine is array-like 4,4. Maybe this is too strict at @@ -369,19 +362,13 @@ def __init__(self, dataobj, affine, header=None, if not affine.shape == (4, 4): raise ValueError('Affine should be shape 4,4') self._affine = affine - if extra is None: - extra = {} - self.extra = extra - self._header = self.header_class.from_header(header) + # if header not specified, get data type from input array if header is None: if hasattr(dataobj, 'dtype'): self._header.set_data_dtype(dataobj.dtype) # make header correspond with image and affine self.update_header() - if file_map is None: - file_map = self.__class__.make_file_map() - self.file_map = file_map self._load_cache = None self._data_cache = None @@ -401,10 +388,6 @@ def dataobj(self): def affine(self): return self._affine - @property - def header(self): - return self._header - def update_header(self): ''' Harmonize header with image data and affine @@ -653,206 +636,6 @@ def get_affine(self): """ return self.affine - def get_header(self): - """ Get header from image - - Please use the `header` property instead of `get_header`; we will - deprecate this method in future versions of nibabel. - """ - return self.header - - def get_filename(self): - ''' Fetch the image filename - - Parameters - ---------- - None - - Returns - ------- - fname : None or str - Returns None if there is no filename, or a filename string. - If an image may have several filenames assoctiated with it - (e.g Analyze ``.img, .hdr`` pair) then we return the more - characteristic filename (the ``.img`` filename in the case of - Analyze') - ''' - # which filename is returned depends on the ordering of the - # 'files_types' class attribute - we return the name - # corresponding to the first in that tuple - characteristic_type = self.files_types[0][0] - return self.file_map[characteristic_type].filename - - def set_filename(self, filename): - ''' Sets the files in the object from a given filename - - The different image formats may check whether the filename has - an extension characteristic of the format, and raise an error if - not. - - Parameters - ---------- - filename : str - If the image format only has one file associated with it, - this will be the only filename set into the image - ``.file_map`` attribute. Otherwise, the image instance will - try and guess the other filenames from this given filename. - ''' - self.file_map = self.__class__.filespec_to_file_map(filename) - - @classmethod - def from_filename(klass, filename): - file_map = klass.filespec_to_file_map(filename) - return klass.from_file_map(file_map) - - @classmethod - def from_filespec(klass, filespec): - warnings.warn('``from_filespec`` class method is deprecated\n' - 'Please use the ``from_filename`` class method ' - 'instead', - DeprecationWarning, stacklevel=2) - klass.from_filename(filespec) - - @classmethod - def from_file_map(klass, file_map): - raise NotImplementedError - - @classmethod - def from_files(klass, file_map): - warnings.warn('``from_files`` class method is deprecated\n' - 'Please use the ``from_file_map`` class method ' - 'instead', - DeprecationWarning, stacklevel=2) - return klass.from_file_map(file_map) - - @classmethod - def filespec_to_file_map(klass, filespec): - """ Make `file_map` for this class from filename `filespec` - - Class method - - Parameters - ---------- - filespec : str - Filename that might be for this image file type. - - Returns - ------- - file_map : dict - `file_map` dict with (key, value) pairs of (``file_type``, - FileHolder instance), where ``file_type`` is a string giving the - type of the contained file. - - Raises - ------ - ImageFileError - if `filespec` is not recognizable as being a filename for this - image type. - """ - try: - filenames = types_filenames( - filespec, klass.files_types, - trailing_suffixes=klass._compressed_suffixes) - except TypesFilenamesError: - raise ImageFileError( - 'Filespec "{0}" does not look right for class {1}'.format( - filespec, klass)) - file_map = {} - for key, fname in filenames.items(): - file_map[key] = FileHolder(filename=fname) - return file_map - - @classmethod - def filespec_to_files(klass, filespec): - warnings.warn('``filespec_to_files`` class method is deprecated\n' - 'Please use the ``filespec_to_file_map`` class method ' - 'instead', - DeprecationWarning, stacklevel=2) - return klass.filespec_to_file_map(filespec) - - def to_filename(self, filename): - ''' Write image to files implied by filename string - - Parameters - ---------- - filename : str - filename to which to save image. We will parse `filename` - with ``filespec_to_file_map`` to work out names for image, - header etc. - - Returns - ------- - None - ''' - self.file_map = self.filespec_to_file_map(filename) - self.to_file_map() - - def to_filespec(self, filename): - warnings.warn('``to_filespec`` is deprecated, please ' - 'use ``to_filename`` instead', - DeprecationWarning, stacklevel=2) - self.to_filename(filename) - - def to_file_map(self, file_map=None): - raise NotImplementedError - - def to_files(self, file_map=None): - warnings.warn('``to_files`` method is deprecated\n' - 'Please use the ``to_file_map`` method ' - 'instead', - DeprecationWarning, stacklevel=2) - self.to_file_map(file_map) - - @classmethod - def make_file_map(klass, mapping=None): - ''' Class method to make files holder for this image type - - Parameters - ---------- - mapping : None or mapping, optional - mapping with keys corresponding to image file types (such as - 'image', 'header' etc, depending on image class) and values - that are filenames or file-like. Default is None - - Returns - ------- - file_map : dict - dict with string keys given by first entry in tuples in - sequence klass.files_types, and values of type FileHolder, - where FileHolder objects have default values, other than - those given by `mapping` - ''' - if mapping is None: - mapping = {} - file_map = {} - for key, ext in klass.files_types: - file_map[key] = FileHolder() - mapval = mapping.get(key, None) - if isinstance(mapval, basestring): - file_map[key].filename = mapval - elif hasattr(mapval, 'tell'): - file_map[key].fileobj = mapval - return file_map - - load = from_filename - - @classmethod - def instance_to_filename(klass, img, filename): - ''' Save `img` in our own format, to name implied by `filename` - - This is a class method - - Parameters - ---------- - img : ``spatialimage`` instance - In fact, an object with the API of ``spatialimage`` - specifically - ``dataobj``, ``affine``, ``header`` and ``extra``. - filename : str - Filename, implying name to which to save image. - ''' - img = klass.from_image(img) - img.to_filename(filename) - @classmethod def from_image(klass, img): ''' Class method to create new instance of own class from `img` @@ -873,105 +656,6 @@ def from_image(klass, img): klass.header_class.from_header(img.header), extra=img.extra.copy()) - @classmethod - def _sniff_meta_for(klass, filename, sniff_nbytes, sniff=None): - """ Sniff metadata for image represented by `filename` - - Parameters - ---------- - filename : str - Filename for an image, or an image header (metadata) file. - If `filename` points to an image data file, and the image type has - a separate "header" file, we work out the name of the header file, - and read from that instead of `filename`. - sniff_nbytes : int - Number of bytes to read from the image or metadata file - sniff : (bytes, fname), optional - The result of a previous call to `_sniff_meta_for`. If fname - matches the computed header file name, `sniff` is returned without - rereading the file. - - Returns - ------- - sniff : None or (bytes, fname) - None if we could not read the image or metadata file. `sniff[0]` - is either length `sniff_nbytes` or the length of the image / - metadata file, whichever is the shorter. `fname` is the name of - the sniffed file. - """ - froot, ext, trailing = splitext_addext(filename, - klass._compressed_suffixes) - # Determine the metadata location - t_fnames = types_filenames( - filename, - klass.files_types, - trailing_suffixes=klass._compressed_suffixes) - meta_fname = t_fnames.get('header', filename) - - # Do not re-sniff if it would be from the same file - if sniff is not None and sniff[1] == meta_fname: - return sniff - - # Attempt to sniff from metadata location - try: - with ImageOpener(meta_fname, 'rb') as fobj: - binaryblock = fobj.read(sniff_nbytes) - except IOError: - return None - return (binaryblock, meta_fname) - - @classmethod - def path_maybe_image(klass, filename, sniff=None, sniff_max=1024): - """ Return True if `filename` may be image matching this class - - Parameters - ---------- - filename : str - Filename for an image, or an image header (metadata) file. - If `filename` points to an image data file, and the image type has - a separate "header" file, we work out the name of the header file, - and read from that instead of `filename`. - sniff : None or (bytes, filename), optional - Bytes content read from a previous call to this method, on another - class, with metadata filename. This allows us to read metadata - bytes once from the image or header, and pass this read set of - bytes to other image classes, therefore saving a repeat read of the - metadata. `filename` is used to validate that metadata would be - read from the same file, re-reading if not. None forces this - method to read the metadata. - sniff_max : int, optional - The maximum number of bytes to read from the metadata. If the - metadata file is long enough, we read this many bytes from the - file, otherwise we read to the end of the file. Longer values - sniff more of the metadata / image file, making it more likely that - the returned sniff will be useful for later calls to - ``path_maybe_image`` for other image classes. - - Returns - ------- - maybe_image : bool - True if `filename` may be valid for an image of this class. - sniff : None or (bytes, filename) - Read bytes content from found metadata. May be None if the file - does not appear to have useful metadata. - """ - froot, ext, trailing = splitext_addext(filename, - klass._compressed_suffixes) - if ext.lower() not in klass.valid_exts: - return False, sniff - if not hasattr(klass.header_class, 'may_contain_header'): - return True, sniff - - # Force re-sniff on too-short sniff - if sniff is not None and len(sniff[0]) < klass._meta_sniff_len: - sniff = None - sniff = klass._sniff_meta_for(filename, - max(klass._meta_sniff_len, sniff_max), - sniff) - if sniff is None or len(sniff[0]) < klass._meta_sniff_len: - return False, sniff - return klass.header_class.may_contain_header(sniff[0]), sniff - def __getitem__(self): ''' No slicing or dictionary interface for images ''' diff --git a/nibabel/tests/test_image_api.py b/nibabel/tests/test_image_api.py index f5a081fd8b..46fbe123c3 100644 --- a/nibabel/tests/test_image_api.py +++ b/nibabel/tests/test_image_api.py @@ -43,7 +43,7 @@ assert_equal, assert_not_equal) from numpy.testing import (assert_almost_equal, assert_array_equal) - +from ..testing import clear_and_catch_warnings from ..tmpdirs import InTemporaryDirectory from .test_api_validators import ValidateAPI @@ -134,9 +134,12 @@ def validate_header(self, imaker, params): def validate_header_deprecated(self, imaker, params): # Check deprecated header API - img = imaker() - hdr = img.get_header() - assert_true(hdr is img.get_header()) + with clear_and_catch_warnings() as w: + warnings.simplefilter('always', DeprecationWarning) + img = imaker() + hdr = img.get_header() + assert_equal(len(w), 1) + assert_true(hdr is img.header) def validate_shape(self, imaker, params): # Validate shape diff --git a/nibabel/tests/test_loadsave.py b/nibabel/tests/test_loadsave.py index 567c079968..4a40ccd344 100644 --- a/nibabel/tests/test_loadsave.py +++ b/nibabel/tests/test_loadsave.py @@ -11,7 +11,7 @@ Nifti1Pair, Nifti1Image, Nifti2Pair, Nifti2Image) from ..loadsave import load, read_img_data -from ..spatialimages import ImageFileError +from ..filebasedimages import ImageFileError from ..tmpdirs import InTemporaryDirectory, TemporaryDirectory from ..optpkg import optional_package diff --git a/nibabel/tests/test_spatialimages.py b/nibabel/tests/test_spatialimages.py index 7b684d1fd2..919fc9f846 100644 --- a/nibabel/tests/test_spatialimages.py +++ b/nibabel/tests/test_spatialimages.py @@ -9,21 +9,22 @@ """ Testing spatialimages """ -from ..externals.six import BytesIO + +import warnings + import numpy as np -from ..spatialimages import (Header, SpatialImage, HeaderDataError, - ImageDataError) +from ..externals.six import BytesIO +from ..spatialimages import (SpatialHeader, SpatialImage, HeaderDataError, + Header, ImageDataError) from unittest import TestCase - from nose.tools import (assert_true, assert_false, assert_equal, assert_not_equal, assert_raises) - from numpy.testing import assert_array_equal, assert_array_almost_equal from .test_helpers import bytesio_round_trip -from ..testing import suppress_warnings +from ..testing import clear_and_catch_warnings, suppress_warnings from ..tmpdirs import InTemporaryDirectory from .. import load as top_load @@ -110,7 +111,7 @@ def test_shape_zooms(): assert_equal(hdr.get_data_shape(), (1,2,3)) assert_equal(hdr.get_zooms(), (1.0,1.0,1.0)) hdr.set_zooms((4, 3, 2)) - assert_equal(hdr.get_zooms(), (4.0,3.0,2.0)) + assert_equal(hdr.get_zooms(), (4.0,3.0,2.0)) hdr.set_data_shape((1, 2)) assert_equal(hdr.get_data_shape(), (1,2)) assert_equal(hdr.get_zooms(), (4.0,3.0)) @@ -159,9 +160,9 @@ def test_affine(): def test_read_data(): - class CHeader(Header): - data_layout='C' - for klass, order in ((Header, 'F'), (CHeader, 'C')): + class CHeader(SpatialHeader): + data_layout = 'C' + for klass, order in ((SpatialHeader, 'F'), (CHeader, 'C')): hdr = klass(np.int32, shape=(1,2,3), zooms=(3.0, 2.0, 1.0)) fobj = BytesIO() data = np.arange(6).reshape((1,2,3)) @@ -187,6 +188,7 @@ class CHeader(Header): class DataLike(object): # Minimal class implementing 'data' API shape = (3,) + def __array__(self): return np.arange(3) @@ -384,3 +386,15 @@ def test_load_mmap(self): # Check invalid values raise error assert_raises(ValueError, func, param1, mmap='rw') assert_raises(ValueError, func, param1, mmap='r+') + + +def test_header_deprecated(): + with clear_and_catch_warnings() as w: + warnings.simplefilter('always', DeprecationWarning) + + class MyHeader(Header): + pass + assert_equal(len(w), 0) + + MyHeader() + assert_equal(len(w), 1)