Source code for nipy.core.image.image

# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
# vi: set ft=python sts=4 ts=4 sw=4 et:
""" Define the Image class and functions to work with Image instances

* fromarray : create an Image instance from an ndarray (deprecated in favor of
  using the Image constructor)
* subsample : slice an Image instance (deprecated in favor of image slicing)
* rollimg : roll an image axis to given location
* synchronized_order : match coordinate systems between images
* iter_axis : make iterator to iterate over an image axis
* is_image : test for an object obeying the Image API
"""
from __future__ import print_function
from __future__ import absolute_import

import warnings
from copy import copy
from itertools import chain

import numpy as np

from nibabel.onetime import setattr_on_read

# These imports are used in the fromarray and subsample functions only, not in
# Image
from ..reference.coordinate_map import (AffineTransform, CoordinateSystem,
                                       input_axis_index)
from ..reference.array_coords import ArrayCoordMap

# Legacy repr printing from numpy.
from nipy.testing import legacy_printing as setup_module  # noqa


[docs]class Image(object): """ The `Image` class provides the core object type used in nipy. An `Image` represents a volumetric brain image and provides means for manipulating the image data. Most functions in the image module operate on `Image` objects. Notes ----- Images can be created through the module functions. See nipy.io for image IO such as ``load`` and ``save`` Examples -------- Load an image from disk >>> from nipy.testing import anatfile >>> from nipy.io.api import load_image >>> img = load_image(anatfile) Make an image from an array. We need to make a meaningful coordinate map for the image. >>> arr = np.zeros((21,64,64), dtype=np.int16) >>> cmap = AffineTransform('kji', 'zxy', np.eye(4)) >>> img = Image(arr, cmap) """ _doc = {} # Dictionary to store docs for attributes that are properties. We # want these docs to conform with our documentation standard, but # they need to be passed into the property function. Defining # them separately allows us to do this without a lot of clutter # in the property line. ################################################################### # # Attributes # ################################################################### metadata = {} _doc['metadata'] = "Dictionary containing additional information." coordmap = AffineTransform(CoordinateSystem('ijk'), CoordinateSystem('xyz'), np.diag([3,5,7,1])) _doc['coordmap'] = "Affine transform mapping from axes coordinates to reference coordinates."
[docs] @setattr_on_read def shape(self): return self._data.shape
_doc['shape'] = "Shape of data array."
[docs] @setattr_on_read def ndim(self): return len(self._data.shape)
_doc['ndim'] = "Number of data dimensions."
[docs] @setattr_on_read def reference(self): return self.coordmap.function_range
_doc['reference'] = "Reference coordinate system."
[docs] @setattr_on_read def axes(self): return self.coordmap.function_domain
_doc['axes'] = "Axes of image."
[docs] @setattr_on_read def affine(self): if hasattr(self.coordmap, "affine"): return self.coordmap.affine raise AttributeError('Nonlinear transform does not have an affine.')
_doc['affine'] = "Affine transformation if one exists." ################################################################### # # Properties # ################################################################### def _getheader(self): # data loaded from a file may have a header warnings.warn("Please don't use ``img.header``; use" "``img.metadata['header'] instead", DeprecationWarning, stacklevel=2) hdr = self.metadata.get('header') if hdr is None: raise AttributeError('Image created from arrays ' 'may not have headers.') return hdr def _setheader(self, header): warnings.warn("Please don't use ``img.header``; use" "``img.metadata['header'] instead", DeprecationWarning, stacklevel=2) self.metadata['header'] = header _doc['header'] = \ """The file header structure for this image, if available. This interface will soon go away - you should use ``img.metadata['header'] instead. """ header = property(_getheader, _setheader, doc=_doc['header']) ################################################################### # # Constructor # ###################################################################
[docs] def __init__(self, data, coordmap, metadata=None): """Create an `Image` object from array and `CoordinateMap` object. Images are often created through the ``load_image`` function in the nipy base namespace. Parameters ---------- data : array-like object that as attribute ``shape`` and returns an array from ``np.asarray(data)`` coordmap : `AffineTransform` object coordmap mapping the domain (input) voxel axes of the image to the range (reference, output) axes - usually mm in real world space metadata : dict, optional Freeform metadata for image. Most common contents is ``header`` from nifti etc loaded images. See Also -------- load_image : load ``Image`` from a file save_image : save ``Image`` to a file """ if metadata is None: metadata = {} else: # Shallow copy metadata = copy(metadata) ndim = len(data.shape) if not isinstance(coordmap, AffineTransform): raise ValueError('coordmap must be an AffineTransform') # self._data is an array-like object. It must have a shape attribute # (see above) and return an array from np.array(data) self._data = data self.coordmap = coordmap if coordmap.function_domain.ndim != ndim: raise ValueError('the number of axes implied by the coordmap do ' 'not match the number of axes of the data') self.metadata = metadata
################################################################### # # Methods # ###################################################################
[docs] def reordered_reference(self, order=None): """ Return new Image with reordered output coordinates New Image coordmap has reordered output coordinates. This does not transpose the data. Parameters ---------- order : None, sequence, optional sequence of int (giving indices) or str (giving names) - expressing new order of coordmap output coordinates. None (the default) results in reversed ordering. Returns ------- r_img : object Image of same class as `self`, with reordered output coordinates. Examples -------- >>> cmap = AffineTransform.from_start_step( ... 'ijk', 'xyz', [1, 2, 3], [4, 5, 6], 'domain', 'range') >>> im = Image(np.empty((30,40,50)), cmap) >>> im_reordered = im.reordered_reference([2,0,1]) >>> im_reordered.shape (30, 40, 50) >>> im_reordered.coordmap AffineTransform( function_domain=CoordinateSystem(coord_names=('i', 'j', 'k'), name='domain', coord_dtype=float64), function_range=CoordinateSystem(coord_names=('z', 'x', 'y'), name='range', coord_dtype=float64), affine=array([[ 0., 0., 6., 3.], [ 4., 0., 0., 1.], [ 0., 5., 0., 2.], [ 0., 0., 0., 1.]]) ) """ if order is None: order = list(range(self.ndim))[::-1] elif type(order[0]) == type(''): order = [self.reference.index(s) for s in order] new_cmap = self.coordmap.reordered_range(order) return self.__class__.from_image(self, coordmap=new_cmap)
[docs] def reordered_axes(self, order=None): """ Return a new Image with reordered input coordinates. This transposes the data as well. Parameters ---------- order : None, sequence, optional Sequence of int (giving indices) or str (giving names) - expressing new order of coordmap output coordinates. None (the default) results in reversed ordering. Returns ------- r_img : object Image of same class as `self`, with reordered output coordinates. Examples -------- >>> cmap = AffineTransform.from_start_step( ... 'ijk', 'xyz', [1, 2, 3], [4, 5, 6], 'domain', 'range') >>> cmap AffineTransform( function_domain=CoordinateSystem(coord_names=('i', 'j', 'k'), name='domain', coord_dtype=float64), function_range=CoordinateSystem(coord_names=('x', 'y', 'z'), name='range', coord_dtype=float64), affine=array([[ 4., 0., 0., 1.], [ 0., 5., 0., 2.], [ 0., 0., 6., 3.], [ 0., 0., 0., 1.]]) ) >>> im = Image(np.empty((30,40,50)), cmap) >>> im_reordered = im.reordered_axes([2,0,1]) >>> im_reordered.shape (50, 30, 40) >>> im_reordered.coordmap AffineTransform( function_domain=CoordinateSystem(coord_names=('k', 'i', 'j'), name='domain', coord_dtype=float64), function_range=CoordinateSystem(coord_names=('x', 'y', 'z'), name='range', coord_dtype=float64), affine=array([[ 0., 4., 0., 1.], [ 0., 0., 5., 2.], [ 6., 0., 0., 3.], [ 0., 0., 0., 1.]]) ) """ if order is None: order = list(range(self.ndim))[::-1] elif type(order[0]) == type(''): order = [self.axes.index(s) for s in order] new_cmap = self.coordmap.reordered_domain(order) # Only transpose if we have to so as to avoid calling # self.get_data if order != list(range(self.ndim)): new_data = np.transpose(self.get_data(), order) else: new_data = self._data return self.__class__.from_image(self, data=new_data, coordmap=new_cmap)
[docs] def renamed_axes(self, **names_dict): r""" Return a new image with input (domain) axes renamed Axes renamed according to the input dictionary. Parameters ---------- \*\*names_dict : dict with keys being old names, and values being new names Returns ------- newimg : Image An Image with the same data, having its axes renamed. Examples -------- >>> data = np.random.standard_normal((11,9,4)) >>> im = Image(data, AffineTransform.from_params('ijk', 'xyz', np.identity(4), 'domain', 'range')) >>> im_renamed = im.renamed_axes(i='slice') >>> print(im_renamed.axes) CoordinateSystem(coord_names=('slice', 'j', 'k'), name='domain', coord_dtype=float64) """ new_cmap = self.coordmap.renamed_domain(names_dict) return self.__class__.from_image(self, coordmap=new_cmap)
[docs] def renamed_reference(self, **names_dict): r""" Return new image with renamed output (range) coordinates Coordinates renamed according to the dictionary Parameters ---------- \*\*names_dict : dict with keys being old names, and values being new names Returns ------- newimg : Image An Image with the same data, having its output coordinates renamed. Examples -------- >>> data = np.random.standard_normal((11,9,4)) >>> im = Image(data, AffineTransform.from_params('ijk', 'xyz', np.identity(4), 'domain', 'range')) >>> im_renamed_reference = im.renamed_reference(x='newx', y='newy') >>> print(im_renamed_reference.reference) CoordinateSystem(coord_names=('newx', 'newy', 'z'), name='range', coord_dtype=float64) """ new_cmap = self.coordmap.renamed_range(names_dict) return self.__class__.from_image(self, coordmap=new_cmap)
def __setitem__(self, index, value): """Setting values of an image, set values in the data array.""" warnings.warn("Please don't use ``img[x] = y``; use " "``img.get_data()[x] = y`` instead", DeprecationWarning, stacklevel=2) self._data[index] = value def __array__(self): """Return data as a numpy array.""" warnings.warn('Please use get_data instead - will be deprecated', DeprecationWarning, stacklevel=2) return self.get_data()
[docs] def get_data(self): """Return data as a numpy array.""" return np.asanyarray(self._data)
def __getitem__(self, slice_object): """ Slicing an image returns an Image. Parameters ---------- slice_object: int, slice or sequence of slice An object representing a numpy 'slice'. Returns ------- img_subsampled: Image An Image with data self.get_data()[slice_object] and an appropriately corrected CoordinateMap. Examples -------- >>> from nipy.io.api import load_image >>> from nipy.testing import funcfile >>> im = load_image(funcfile) >>> frame3 = im[:,:,:,3] >>> np.allclose(frame3.get_data(), im.get_data()[:,:,:,3]) True """ data = self.get_data()[slice_object] g = ArrayCoordMap(self.coordmap, self.shape)[slice_object] coordmap = g.coordmap if coordmap.function_domain.ndim > 0: return self.__class__.from_image(self, data=data, coordmap=coordmap) else: return data def __iter__(self): """ Images do not have default iteration This is because it's not obvious that axis 0 is the right axis to iterate over. For example, we often want to iterate over the time or volume axis, and this is more likely to be axis 3 """ raise TypeError("Images do not have default iteration; " "you can use ``iter_axis(img, axis)`` instead.") def __eq__(self, other): return (isinstance(other, self.__class__) and self.shape == other.shape and np.all(self.get_data() == other.get_data()) and np.all(self.affine == other.affine) and (self.axes.coord_names == other.axes.coord_names)) def __ne__(self, other): return not self.__eq__(other) def __repr__(self): options = np.get_printoptions() np.set_printoptions(precision=6, threshold=64, edgeitems=2) representation = \ 'Image(\n data=%s,\n coordmap=%s)' % ( '\n '.join(repr(self._data).split('\n')), '\n '.join(repr(self.coordmap).split('\n'))) np.set_printoptions(**options) return representation
[docs] @classmethod def from_image(klass, img, data=None, coordmap=None, metadata=None): """ Classmethod makes new instance of this `klass` from instance `img` Parameters ---------- data : array-like object that as attribute ``shape`` and returns an array from ``np.asarray(data)`` coordmap : `AffineTransform` object coordmap mapping the domain (input) voxel axes of the image to the range (reference, output) axes - usually mm in real world space metadata : dict, optional Freeform metadata for image. Most common contents is ``header`` from nifti etc loaded images. Returns ------- img : `klass` instance New image with data from `data`, coordmap from `coordmap` maybe metadata from `metadata` Notes ----- Subclasses of ``Image`` with different semantics for ``__init__`` will need to override this classmethod. Examples -------- >>> from nipy import load_image >>> from nipy.core.api import Image >>> from nipy.testing import anatfile >>> aimg = load_image(anatfile) >>> arr = np.arange(24).reshape((2,3,4)) >>> img = Image.from_image(aimg, data=arr) """ if data is None: data = img._data if coordmap is None: coordmap = copy(img.coordmap) if metadata is None: metadata = copy(img.metadata) return klass(data, coordmap, metadata)
[docs]class SliceMaker(object): """ This class just creates slice objects for image resampling It only has a __getitem__ method that returns its argument. XXX Wouldn't need this if there was a way XXX to do this XXX subsample(img, [::2,::3,10:1:-1]) XXX XXX Could be something like this Subsample(img)[::2,::3,10:1:-1] """ def __getitem__(self, index): return index
slice_maker = SliceMaker()
[docs]def subsample(img, slice_object): """ Subsample an image Please don't use this function, but use direct image slicing instead. That is, replace:: frame3 = subsample(im, slice_maker[:,:,:,3]) with:: frame3 = im[:,:,:,3] Parameters ---------- img : Image slice_object: int, slice or sequence of slice An object representing a numpy 'slice'. Returns ------- img_subsampled: Image An Image with data img.get_data()[slice_object] and an appropriately corrected CoordinateMap. Examples -------- >>> from nipy.io.api import load_image >>> from nipy.testing import funcfile >>> from nipy.core.api import subsample, slice_maker >>> im = load_image(funcfile) >>> frame3 = subsample(im, slice_maker[:,:,:,3]) >>> np.allclose(frame3.get_data(), im.get_data()[:,:,:,3]) True """ warnings.warn('subsample is deprecated, please use image ' 'slicing instead (e.g. img[:,:,1]', DeprecationWarning, stacklevel=2) return img.__getitem__(slice_object)
[docs]def fromarray(data, innames, outnames): """Create an image from array `data`, and input/output coordinate names The mapping between the input and output coordinate names is the identity matrix. Please don't use this routine, but instead prefer:: from nipy.core.api import Image, AffineTransform img = Image(data, AffineTransform(innames, outnames, np.eye(4))) where ``4`` is ``len(innames) + 1``. Parameters ---------- data : numpy array A numpy array of three dimensions. innames : sequence a list of input axis names innames : sequence a list of output axis names Returns ------- image : An `Image` object See Also -------- load : function for loading images save : function for saving images Examples -------- >>> img = fromarray(np.zeros((2,3,4)), 'ijk', 'xyz') >>> img.coordmap AffineTransform( function_domain=CoordinateSystem(coord_names=('i', 'j', 'k'), name='', coord_dtype=float64), function_range=CoordinateSystem(coord_names=('x', 'y', 'z'), name='', coord_dtype=float64), affine=array([[ 1., 0., 0., 0.], [ 0., 1., 0., 0.], [ 0., 0., 1., 0.], [ 0., 0., 0., 1.]]) ) """ warnings.warn('fromarray is deprecated, please use the Image ' 'constructor instead', DeprecationWarning, stacklevel=2) ndim = len(data.shape) coordmap = AffineTransform.from_start_step(innames, outnames, (0.,)*ndim, (1.,)*ndim) return Image(data, coordmap)
@np.deprecate_with_doc('Please use rollimg instead') def rollaxis(img, axis, inverse=False): """ Roll `axis` backwards, until it lies in the first position. It also reorders the reference coordinates by the same ordering. This is done to preserve a diagonal affine matrix if image.affine is diagonal. It also makes it possible to unambiguously specify an axis to roll along in terms of either a reference name (i.e. 'z') or an axis name (i.e. 'slice'). This function is deprecated; please use ``rollimg`` instead. Parameters ---------- img : Image Image whose axes and reference coordinates are to be reordered by rolling. axis : str or int Axis to be rolled, can be specified by name or as an integer. inverse : bool, optional If inverse is True, then axis must be an integer and the first axis is returned to the position axis. This keyword is deprecated and we'll remove it in a future version of nipy. Returns ------- newimg : Image Image with reordered axes and reference coordinates. Examples -------- >>> data = np.zeros((30,40,50,5)) >>> affine_transform = AffineTransform.from_params('ijkl', 'xyzt', np.diag([1,2,3,4,1])) >>> im = Image(data, affine_transform) >>> im.coordmap AffineTransform( function_domain=CoordinateSystem(coord_names=('i', 'j', 'k', 'l'), name='', coord_dtype=float64), function_range=CoordinateSystem(coord_names=('x', 'y', 'z', 't'), name='', coord_dtype=float64), affine=array([[ 1., 0., 0., 0., 0.], [ 0., 2., 0., 0., 0.], [ 0., 0., 3., 0., 0.], [ 0., 0., 0., 4., 0.], [ 0., 0., 0., 0., 1.]]) ) >>> im_t_first = rollaxis(im, 't') >>> np.diag(im_t_first.affine) array([ 4., 1., 2., 3., 1.]) >>> im_t_first.shape (5, 30, 40, 50) >>> im_t_first.coordmap AffineTransform( function_domain=CoordinateSystem(coord_names=('l', 'i', 'j', 'k'), name='', coord_dtype=float64), function_range=CoordinateSystem(coord_names=('t', 'x', 'y', 'z'), name='', coord_dtype=float64), affine=array([[ 4., 0., 0., 0., 0.], [ 0., 1., 0., 0., 0.], [ 0., 0., 2., 0., 0.], [ 0., 0., 0., 3., 0.], [ 0., 0., 0., 0., 1.]]) ) """ if inverse not in (True, False): raise ValueError('Inverse should be True or False; did you mean to ' 'use the ``rollimg` function instead?') if isinstance(axis, int) and axis < 0: axis = img.ndim + axis if inverse: if type(axis) != type(0): raise ValueError('If carrying out inverse rolling, ' 'axis must be an integer') order = list(range(1, img.ndim)) order.insert(axis, 0) return img.reordered_axes(order).reordered_reference(order) if axis not in chain(range(img.axes.ndim), img.axes.coord_names, img.reference.coord_names): raise ValueError('axis must be an axis number,' 'an axis name or a reference name') # Find out which index axis corresonds to in_index = out_index = -1 if type(axis) == type(''): try: in_index = img.axes.index(axis) except: pass try: out_index = img.reference.index(axis) except: pass if in_index > 0 and out_index > 0 and in_index != out_index: raise ValueError('ambiguous choice of axis -- it exists ' 'both in as an axis name and a ' 'reference name') if in_index >= 0: axis = in_index else: axis = out_index if axis == -1: axis += img.axes.ndim order = list(range(img.ndim)) order.remove(axis) order.insert(0, axis) return img.reordered_axes(order).reordered_reference(order)
[docs]def rollimg(img, axis, start=0, fix0=True): """ Roll `axis` backwards in the inputs, until it lies before `start` Parameters ---------- img : Image Image whose axes and reference coordinates are to be reordered by rollimg. axis : str or int Axis to be rolled, can be specified by name or as an integer. If an integer, axis is an input axis. If a name, can be name of input or output axis. If an output axis, we search for the closest matching input axis, and raise an AxisError if this fails. start : str or int, optional position before which to roll axis `axis`. Default to 0. Can again be an integer (input axis) or name of input or output axis. fix0 : bool, optional Whether to allow for zero scaling when searching for an input axis matching an output axis. Useful for images where time scaling is 0. Returns ------- newimg : Image Image with reordered input axes and corresponding data. Examples -------- >>> data = np.zeros((30,40,50,5)) >>> affine_transform = AffineTransform('ijkl', 'xyzt', np.diag([1,2,3,4,1])) >>> im = Image(data, affine_transform) >>> im.coordmap AffineTransform( function_domain=CoordinateSystem(coord_names=('i', 'j', 'k', 'l'), name='', coord_dtype=float64), function_range=CoordinateSystem(coord_names=('x', 'y', 'z', 't'), name='', coord_dtype=float64), affine=array([[ 1., 0., 0., 0., 0.], [ 0., 2., 0., 0., 0.], [ 0., 0., 3., 0., 0.], [ 0., 0., 0., 4., 0.], [ 0., 0., 0., 0., 1.]]) ) >>> im_t_first = rollimg(im, 't') >>> im_t_first.shape (5, 30, 40, 50) >>> im_t_first.coordmap AffineTransform( function_domain=CoordinateSystem(coord_names=('l', 'i', 'j', 'k'), name='', coord_dtype=float64), function_range=CoordinateSystem(coord_names=('x', 'y', 'z', 't'), name='', coord_dtype=float64), affine=array([[ 0., 1., 0., 0., 0.], [ 0., 0., 2., 0., 0.], [ 0., 0., 0., 3., 0.], [ 4., 0., 0., 0., 0.], [ 0., 0., 0., 0., 1.]]) ) """ axis = input_axis_index(img.coordmap, axis, fix0) start = input_axis_index(img.coordmap, start, fix0) order = list(range(img.ndim)) order.remove(axis) if axis < start: start -= 1 order.insert(start, axis) return img.reordered_axes(order)
[docs]def iter_axis(img, axis, asarray=False): """ Return generator to slice an image `img` over `axis` Parameters ---------- img : ``Image`` instance axis : int or str axis identifier, either name or axis number asarray : {False, True}, optional Returns ------- g : generator such that list(g) returns a list of slices over `axis`. If `asarray` is `False` the slices are images. If `asarray` is True, slices are the data from the images. Examples -------- >>> data = np.arange(24).reshape((4,3,2)) >>> img = Image(data, AffineTransform('ijk', 'xyz', np.eye(4))) >>> slices = list(iter_axis(img, 'j')) >>> len(slices) 3 >>> slices[0].shape (4, 2) >>> slices = list(iter_axis(img, 'k', asarray=True)) >>> slices[1].sum() == data[:,:,1].sum() True """ rimg = rollimg(img, axis) for i in range(rimg.shape[0]): if asarray: yield rimg[i].get_data() else: yield rimg[i]
[docs]def synchronized_order(img, target_img, axes=True, reference=True): """ Reorder reference and axes of `img` to match target_img. Parameters ---------- img : Image target_img : Image axes : bool, optional If True, synchronize the order of the axes. reference : bool, optional If True, synchronize the order of the reference coordinates. Returns ------- newimg : Image An Image satisfying newimg.axes == target.axes (if axes == True), newimg.reference == target.reference (if reference == True). Examples -------- >>> data = np.random.standard_normal((3,4,7,5)) >>> im = Image(data, AffineTransform.from_params('ijkl', 'xyzt', np.diag([1,2,3,4,1]))) >>> im_scrambled = im.reordered_axes('iljk').reordered_reference('txyz') >>> im == im_scrambled False >>> im_unscrambled = synchronized_order(im_scrambled, im) >>> im == im_unscrambled True The images don't have to be the same shape >>> data2 = np.random.standard_normal((3,11,9,4)) >>> im2 = Image(data, AffineTransform.from_params('ijkl', 'xyzt', np.diag([1,2,3,4,1]))) >>> im_scrambled2 = im2.reordered_axes('iljk').reordered_reference('xtyz') >>> im_unscrambled2 = synchronized_order(im_scrambled2, im) >>> im_unscrambled2.coordmap == im.coordmap True or have the same coordmap >>> data3 = np.random.standard_normal((3,11,9,4)) >>> im3 = Image(data3, AffineTransform.from_params('ijkl', 'xyzt', np.diag([1,9,3,-2,1]))) >>> im_scrambled3 = im3.reordered_axes('iljk').reordered_reference('xtyz') >>> im_unscrambled3 = synchronized_order(im_scrambled3, im) >>> im_unscrambled3.axes == im.axes True >>> im_unscrambled3.reference == im.reference True >>> im_unscrambled4 = synchronized_order(im_scrambled3, im, axes=False) >>> im_unscrambled4.axes == im.axes False >>> im_unscrambled4.axes == im_scrambled3.axes True >>> im_unscrambled4.reference == im.reference True """ # Caution, we can't just use target_img.reference because other subclasses # of Image may not have all axes in the .reference attribute. target_axes = target_img.axes # = target_img.coordmap.function_domain # the below not necessarily == target_image.reference target_reference = target_img.coordmap.function_range if axes: img = img.reordered_axes(target_axes.coord_names) if reference: img = img.reordered_reference(target_reference.coord_names) return img
[docs]def is_image(obj): ''' Returns true if this object obeys the Image API This allows us to test for something that is duck-typing an image. For now an array must have a 'coordmap' attribute, and a callable 'get_data' attribute. Parameters ---------- obj : object object for which to test API Returns ------- is_img : bool True if object obeys image API Examples -------- >>> from nipy.testing import anatfile >>> from nipy.io.api import load_image >>> img = load_image(anatfile) >>> is_image(img) True >>> class C(object): pass >>> c = C() >>> is_image(c) False ''' if not hasattr(obj, 'coordmap') or not hasattr(obj, 'metadata'): return False return callable(getattr(obj, 'get_data'))