Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Signatures #75

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
33 changes: 18 additions & 15 deletions nibabel/analyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,22 @@ def scaling_from_data(self, data):
out_dtype,
self.has_data_intercept)

def state_stamper(self, caller):
""" Return stamp for current state of `self`

Parameters
----------
caller : callable
May be object from which this method was called. Not used by
analyze headers, but may be used by subclasses

Returns
-------
stamp : object
object unique to this state of `self`
"""
return self.__class__, self.binaryblock

@classmethod
def _get_checks(klass):
''' Return sequence of check functions for this class '''
Expand Down Expand Up @@ -899,18 +915,7 @@ class AnalyzeImage(SpatialImage):
files_types = (('image','.img'), ('header','.hdr'))
_compressed_exts = ('.gz', '.bz2')

class ImageArrayProxy(ArrayProxy):
''' Analyze-type implemention of array proxy protocol

The array proxy allows us to freeze the passed fileobj and
header such that it returns the expected data array.
'''
def _read_data(self):
fileobj = allopen(self.file_like)
data = self.header.data_from_fileobj(fileobj)
if isinstance(self.file_like, basestring): # filename
fileobj.close()
return data
ImageArrayProxy = ArrayProxy

def get_header(self):
''' Return header
Expand Down Expand Up @@ -941,9 +946,7 @@ def from_file_map(klass, file_map):
img = klass(data, None, header, file_map=file_map)
# set affine from header though
img._affine = header.get_best_affine()
img._load_cache = {'header': hdr_copy,
'affine': img._affine.copy(),
'file_map': copy_file_map(file_map)}
img._stored_state['affine'] = img.stamper(img._affine)
return img

@staticmethod
Expand Down
75 changes: 72 additions & 3 deletions nibabel/arrayproxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,48 @@
# copyright and license terms.
#
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
''' Array proxy base class '''
""" Array proxy base class

The API is - at minimum:

* The object has an attribute ``shape``
* that the object returns the data array from ``np.asarray(obj)``
* that modifying no object outside ``obj`` will affect the result of
``np.asarray(obj)``. Specifically, if you pass a header into the the
__init__, then modifying the original header will not affect the result of the
array return.

You might also want to implement ``state_stamper``
"""

from .volumeutils import allopen


class ArrayProxy(object):
"""
The array proxy allows us to freeze the passed fileobj and header such that
it returns the expected data array.

This fairly generic implementation allows us to deal with Analyze and its
variants, including Nifti1, and with the MGH format, apparently.

It requires a ``header`` object with methods:
* copy
* get_data_shape
* data_from_fileobj

Other image types might need to implement their own implementation of this
API. See :mod:`minc` for an example.
"""
def __init__(self, file_like, header):
self.file_like = file_like
self.header = header.copy()
self._data = None
self.shape = header.get_data_shape()
self._shape = header.get_data_shape()

@property
def shape(self):
return self._shape

def __array__(self):
''' Cached read of data from file '''
Expand All @@ -22,6 +56,41 @@ def __array__(self):
return self._data

def _read_data(self):
raise NotImplementedError
fileobj = allopen(self.file_like)
data = self.header.data_from_fileobj(fileobj)
if isinstance(self.file_like, basestring): # filename
fileobj.close()
return data

def state_stamper(self, caller):
""" Return stamp for current state of `self`

The result somewhat uniquely identifies the state of the array proxy.
It assumes that the underlying ``self.file_like`` does not get modified.
Specifically, if you open a file-like object, pass into an arrayproxy
(call it ``ap``) and get the stamp (say with ``Stamper()(ap)``, then
this stamp will uniquely identify the result of ``np.asarry(ap)`` only
if the file-like object has not changed.

Parameters
----------
caller : callable
callable object from which this method was called.

Returns
-------
stamp : object
object unique to this state of `self`

Notes
-----
The stamp changes if the array to be returned has been cached
(``_data`` attribute). This is because this makes it possible to change
the array outside the proxy object, because further calls to
``__array__`` returns a refernce to ``self._data``, and the reference
allows the caller to modify the array in-place.
"""
return (self.__class__,
self.file_like,
caller(self.header),
caller(self._data))
25 changes: 24 additions & 1 deletion nibabel/fileholders.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,30 @@ def same_file_as(self, other):
return ((self.filename == other.filename) and
(self.fileobj == other.fileobj))

def state_stamper(self, caller):
""" Get record of state of fileholder

See: :mod:`stampers`

Parameters
----------
caller : object
Passed from stamper object, but not used by us

Returns
-------
stamp : tuple
state stamp

Notes
-----
We can get state stamp for these file objects assuming that the same
filename corresponds to the same file. We can let pass the position of
reading in the file because we are recording the position with
``self.pos``.
"""
return (self.filename, self.fileobj, self.pos)


def copy_file_map(file_map):
''' Copy mapping of fileholders given by `file_map`
Expand All @@ -109,4 +133,3 @@ def copy_file_map(file_map):
for key, fh in file_map.items():
fm_copy[key] = copy(fh)
return fm_copy

16 changes: 1 addition & 15 deletions nibabel/freesurfer/mghformat.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,18 +429,7 @@ class MGHImage(SpatialImage):
files_types = (('image', '.mgh'),)
_compressed_exts = ('.mgz',)

class ImageArrayProxy(ArrayProxy):
''' Analyze-type implemention of array proxy protocol

The array proxy allows us to freeze the passed fileobj and
header such that it returns the expected data array.
'''
def _read_data(self):
fileobj = allopen(self.file_like)
data = self.header.data_from_fileobj(fileobj)
if isinstance(self.file_like, basestring): # filename
fileobj.close()
return data
ImageArrayProxy = ArrayProxy

def get_header(self):
''' Return the MGH header given the MGHImage'''
Expand Down Expand Up @@ -483,9 +472,6 @@ def from_file_map(klass, file_map):
hdr_copy = header.copy()
data = klass.ImageArrayProxy(mghf, hdr_copy)
img = klass(data, affine, header, file_map=file_map)
img._load_cache = {'header': hdr_copy,
'affine': affine.copy(),
'file_map': copy_file_map(file_map)}
return img

def to_file_map(self, file_map=None):
Expand Down
30 changes: 30 additions & 0 deletions nibabel/nifti1.py
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,21 @@ def from_fileobj(klass, fileobj, size, byteswap):
extensions.append(ext)
return extensions

def state_stamper(self, caller):
""" Return stamp for current state of `self`

Parameters
----------
caller : callable
callable with which we can process our state

Returns
-------
stamp : object
object unique to this state of `self`
"""
return self.__class__, caller(list(self))


class Nifti1Header(SpmAnalyzeHeader):
''' Class for NIFTI1 header
Expand Down Expand Up @@ -620,6 +635,21 @@ def default_structarr(klass, endianness=None):
hdr_data['vox_offset'] = 0
return hdr_data

def state_stamper(self, caller):
""" Return stamp for current state of `self`

Parameters
----------
caller : None or callable
May be object from which this method was called.

Returns
-------
stamp : object
object unique to this state of `self`
"""
return self.__class__, self.binaryblock, caller(self.extensions)

def get_qform_quaternion(self):
''' Compute quaternion from b, c, d of quaternion

Expand Down
96 changes: 94 additions & 2 deletions nibabel/spatialimages.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@

import numpy as np

from .stampers import NdaStamper
from .filename_parser import types_filenames, TypesFilenamesError
from .fileholders import FileHolder
from .volumeutils import shape_zoom_affine
Expand Down Expand Up @@ -164,7 +165,7 @@ 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 it's parent
# 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:
Expand Down Expand Up @@ -254,6 +255,24 @@ def data_from_fileobj(self, fileobj):
data_bytes = fileobj.read(data_size)
return np.ndarray(shape, dtype, data_bytes, order='F')

def state_stamper(self, caller):
""" Return stamp for current state of `self`

Parameters
----------
caller : callable
May be object from which this method was called.

Returns
-------
stamp : object
object unique to this state of `self`
"""
return (self.__class__,
np.dtype(self._dtype),
tuple(self._shape),
tuple(self._zooms))


class ImageDataError(Exception):
pass
Expand All @@ -266,6 +285,8 @@ class ImageFileError(Exception):
class SpatialImage(object):
header_class = Header
files_types = (('image', None),)
# Object with which to get state stamps for components
stamper = NdaStamper()
_compressed_exts = ()

''' Template class for images '''
Expand Down Expand Up @@ -319,7 +340,7 @@ def __init__(self, data, affine, header=None,
if file_map is None:
file_map = self.__class__.make_file_map()
self.file_map = file_map
self._load_cache = None
self._stored_state = self.current_state()

def update_header(self):
''' Update header from information in image'''
Expand Down Expand Up @@ -558,3 +579,74 @@ def from_image(klass, img):
klass.header_class.from_header(img.get_header()),
extra=img.extra.copy())

def current_state(self, stamper=None):
""" Return dictionary unique to current state of the image

The state of an image is defined by all of:
* data
* affine
* header
* file_map

Note we ignore ``extra`` in defining state.

Parameters
----------
stamper : None or callable
Object with which to create state stamps for components of image.
Defaults to an ndarray-aware stamper

Returns
-------
state : dict
dictionary with key, value pairs for each image component
"""
if stamper is None:
stamper = self.stamper
return dict(data=stamper(self._data),
affine=stamper(self._affine),
header=stamper(self._header),
file_map=stamper(self.file_map))

def reset_changed(self):
""" Reset stored state so that changes are relative to current state

Checkpoints image stored state so that ``maybe_changed`` is relative to
state as of this call. See ``maybe_changed``.
"""
self._stored_state = self.current_state()

def maybe_changed(self):
""" True if image might have changed relative to last checkpoint

We record the image state when you create the image object, and when you
call ``reset_changed`` explicitly. We return True from this method if
the recorded image state may be different from the current image state.
We also return True if the image state is too time-consuming to
calculate.
"""
return self._stored_state != self.current_state()

def state_stamper(self, caller):
""" Return stamp for current state of `self`

The state of an image is defined by all of:
* data
* affine
* header
* file_map

Note we ignore ``extra`` in defining state.

Parameters
----------
caller : callable
May be object from which this method was called.

Returns
-------
stamp : object
object unique to this state of `self`
"""
cstate = self.current_state(caller)
return self.__class__, tuple(cstate.items())
Loading