Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 39 additions & 7 deletions nibabel/spatialimages.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,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 @@ -448,21 +448,53 @@ def __str__(self):
'metadata:',
'%s' % self._header))

def get_data(self):
def get_data(self, caching='fill'):
""" Return image data from image with any necessary scalng applied

If the image data is a array proxy (data not yet read from disk) then
read the data, and store in an internal cache. Future calls to
``get_data`` will return the cached copy.
the default behavior (`caching` == "fill") is to read the data, and
store in an internal cache. Future calls to ``get_data`` will return
the cached copy.

Once the data has been cached and returned from a proxy array, the
cached array can be modified by modifying the returned array, because
the returned array is a reference to the array in the cache. Regardless
of the `caching` flag, this is always true of an in-memory image (where
the image data is an array rather than an array proxy).

Parameters
----------
caching : {'fill', 'unchanged'}, optional
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@matthew-brett Reading this explanatory paragraph, I think we failed to converge on the optimal API.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aha - could you say more? What were you thinking would be different?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I could just be a very naive user, but I cannot figure out what this paragraph is telling me--it seems terribly complicated! I have no idea how I would go about using this flag correctly.

This argument has no effect in the case where the image data is an
array, or the image data has already been cached. If the image data
is an array proxy, and the image data has not yet been cached, then
'fill' (the default) will read the data from the array proxy, and
store in an internal cache, so that future calls to ``get_data``
will return the cached copy. If 'unchanged' then leave the current
state of caching unchanged; return the cached copy if it exists, if
not, load the data from disk and return that, but without filling
the cache.

Returns
-------
data : array
array of image data
"""
if self._data_cache is None:
self._data_cache = np.asanyarray(self._dataobj)
return self._data_cache
if caching not in ('fill', 'unchanged'):
raise ValueError('caching value should be "fill" or "unchanged"')
if self._data_cache is not None:
return self._data_cache
data = np.asanyarray(self._dataobj)
if caching == 'fill':
self._data_cache = data
return data

@property
def in_memory(self):
""" True when array data is in memory
"""
return (isinstance(self._dataobj, np.ndarray)
or self._data_cache is not None)

def uncache(self):
""" Delete any cached read of data from proxied data
Expand Down
62 changes: 49 additions & 13 deletions nibabel/tests/test_image_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@
array creation. If it does, this call empties that cache. Implement this
as a no-op if ``get_data()`` does not cache.
* ``img[something]`` generates an informative TypeError
* ``img.in_memory`` is True for an array image, and for a proxy image that is
cached, but False otherwise.
"""
from __future__ import division, print_function, absolute_import

import warnings
from functools import partial

import numpy as np

Expand Down Expand Up @@ -172,8 +175,23 @@ def validate_data(self, imaker, params):
assert_false(isinstance(img.dataobj, np.ndarray))
proxy_data = np.asarray(img.dataobj)
proxy_copy = proxy_data.copy()
# Not yet cached, proxy image: in_memory is False
assert_false(img.in_memory)
# Load with caching='unchanged'
data = img.get_data(caching='unchanged')
# Still not cached
assert_false(img.in_memory)
# Default load, does caching
data = img.get_data()
# Data now cached
assert_true(img.in_memory)
assert_false(proxy_data is data)
# Now caching='unchanged' does nothing, returns cached version
data_again = img.get_data(caching='unchanged')
assert_true(data is data_again)
# caching='fill' does nothing because the cache is already full
data_yet_again = img.get_data(caching='fill')
assert_true(data is data_yet_again)
# changing array data does not change proxy data, or reloaded data
data[:] = 42
assert_array_equal(proxy_data, proxy_copy)
Expand All @@ -182,23 +200,41 @@ def validate_data(self, imaker, params):
assert_array_equal(img.get_data(), 42)
# until we uncache
img.uncache()
# Which unsets in_memory
assert_false(img.in_memory)
assert_array_equal(img.get_data(), proxy_copy)
# Check caching='fill' does cache data
img = imaker()
assert_false(img.in_memory)
data = img.get_data(caching='fill')
assert_true(img.in_memory)
data_again = img.get_data()
assert_true(data is data_again)
else: # not proxy
assert_true(isinstance(img.dataobj, np.ndarray))
non_proxy_data = np.asarray(img.dataobj)
data = img.get_data()
assert_true(non_proxy_data is data)
# changing array data does change proxy data, and reloaded data
data[:] = 42
assert_array_equal(np.asarray(img.dataobj), 42)
# It does change the result of get_data
assert_array_equal(img.get_data(), 42)
# Unache has no effect
img.uncache()
assert_array_equal(img.get_data(), 42)
# Read only
for caching in (None, 'fill', 'unchanged'):
img = imaker()
get_data_func = (img.get_data if caching is None else
partial(img.get_data, caching=caching))
assert_true(isinstance(img.dataobj, np.ndarray))
assert_true(img.in_memory)
data = get_data_func()
assert_true(data is img.dataobj)
# changing array data does change proxy data, and reloaded data
data[:] = 42
assert_array_equal(np.asarray(img.dataobj), 42)
# It does change the result of get_data
assert_array_equal(get_data_func(), 42)
# Unache has no effect
img.uncache()
assert_array_equal(get_data_func(), 42)
assert_true(img.in_memory)
# dataobj is read only
fake_data = np.zeros(img.shape).astype(img.get_data_dtype())
assert_raises(AttributeError, setattr, img, 'dataobj', fake_data)
# So is in_memory
assert_raises(AttributeError, setattr, img, 'in_memory', False)
# Values to get_data caching parameter must be 'fill' or 'unchanged'
assert_raises(ValueError, img.get_data, caching='something')

def validate_data_deprecated(self, imaker, params):
# Check _data property still exists, but raises warning
Expand Down