Skip to content

Commit 3f783a6

Browse files
committed
RF - fill out arrayproxy object and add tests
Arrayproxy was previously a partly-specified API. Fill out the API with the analyze implementation and add tests.
1 parent ba18b33 commit 3f783a6

File tree

2 files changed

+129
-3
lines changed

2 files changed

+129
-3
lines changed

nibabel/arrayproxy.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,48 @@
66
# copyright and license terms.
77
#
88
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
9-
''' Array proxy base class '''
9+
""" Array proxy base class
10+
11+
The API is - at minimum:
12+
13+
* The object has an attribute ``shape``
14+
* that the object returns the data array from ``np.asarray(obj)``
15+
* that modifying no object outside ``obj`` will affect the result of
16+
``np.asarray(obj)``. Specifically, if you pass a header into the the
17+
__init__, then modifying the original header will not affect the result of the
18+
array return.
19+
20+
You might also want to implement ``state_stamper``
21+
"""
22+
23+
from .volumeutils import allopen
24+
1025

1126
class ArrayProxy(object):
27+
"""
28+
The array proxy allows us to freeze the passed fileobj and header such that
29+
it returns the expected data array.
30+
31+
This fairly generic implementation allows us to deal with Analyze and its
32+
variants, including Nifti1, and with the MGH format, apparently.
33+
34+
It requires a ``header`` object with methods:
35+
* copy
36+
* get_data_shape
37+
* data_from_fileobj
38+
39+
Other image types might need to implement their own implementation of this
40+
API. See :mod:`minc` for an example.
41+
"""
1242
def __init__(self, file_like, header):
1343
self.file_like = file_like
1444
self.header = header.copy()
1545
self._data = None
16-
self.shape = header.get_data_shape()
46+
self._shape = header.get_data_shape()
47+
48+
@property
49+
def shape(self):
50+
return self._shape
1751

1852
def __array__(self):
1953
''' Cached read of data from file '''
@@ -22,6 +56,10 @@ def __array__(self):
2256
return self._data
2357

2458
def _read_data(self):
25-
raise NotImplementedError
59+
fileobj = allopen(self.file_like)
60+
data = self.header.data_from_fileobj(fileobj)
61+
if isinstance(self.file_like, basestring): # filename
62+
fileobj.close()
63+
return data
2664

2765

nibabel/tests/test_arrayproxy.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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+
""" Tests for arrayproxy module
10+
"""
11+
from __future__ import with_statement
12+
13+
from copy import deepcopy
14+
15+
from ..py3k import BytesIO, ZEROB, asbytes
16+
from ..tmpdirs import InTemporaryDirectory
17+
18+
import numpy as np
19+
20+
from ..arrayproxy import ArrayProxy
21+
from ..nifti1 import Nifti1Header
22+
23+
from numpy.testing import assert_array_equal, assert_array_almost_equal
24+
from nose.tools import (assert_true, assert_false, assert_equal,
25+
assert_not_equal, assert_raises)
26+
27+
28+
class FunkyHeader(object):
29+
def __init__(self, shape):
30+
self.shape = shape
31+
32+
def copy(self):
33+
return self.__class__(self.shape[:])
34+
35+
def get_data_shape(self):
36+
return self.shape[:]
37+
38+
def data_from_fileobj(self, fileobj):
39+
return np.arange(np.prod(self.shape)).reshape(self.shape)
40+
41+
42+
def test_init():
43+
bio = BytesIO()
44+
shape = [2,3,4]
45+
hdr = FunkyHeader(shape)
46+
ap = ArrayProxy(bio, hdr)
47+
assert_true(ap.file_like is bio)
48+
assert_equal(ap.shape, shape)
49+
# shape should be read only
50+
assert_raises(AttributeError, setattr, ap, 'shape', shape)
51+
# Check there has been a copy of the header
52+
assert_false(ap.header is hdr)
53+
# Check we can modify the original header without changing the ap version
54+
hdr.shape[0] = 6
55+
assert_not_equal(ap.shape, shape)
56+
# Get the data
57+
assert_array_equal(np.asarray(ap), np.arange(24).reshape((2,3,4)))
58+
59+
60+
def write_raw_data(arr, hdr, fileobj):
61+
hdr.set_data_shape(arr.shape)
62+
hdr.set_data_dtype(arr.dtype)
63+
fileobj.write(ZEROB * hdr.get_data_offset())
64+
fileobj.write(arr.tostring(order='F'))
65+
66+
67+
def test_nifti1_init():
68+
bio = BytesIO()
69+
shape = (2,3,4)
70+
hdr = Nifti1Header()
71+
arr = np.arange(24, dtype=np.int16).reshape(shape)
72+
write_raw_data(arr, hdr, bio)
73+
hdr.set_slope_inter(2, 10)
74+
ap = ArrayProxy(bio, hdr)
75+
assert_true(ap.file_like == bio)
76+
assert_equal(ap.shape, shape)
77+
# Check there has been a copy of the header
78+
assert_false(ap.header is hdr)
79+
# Get the data
80+
assert_array_equal(np.asarray(ap), arr * 2.0 + 10)
81+
with InTemporaryDirectory():
82+
f = open('test.nii', 'wb')
83+
write_raw_data(arr, hdr, f)
84+
f.close()
85+
ap = ArrayProxy('test.nii', hdr)
86+
assert_true(ap.file_like == 'test.nii')
87+
assert_equal(ap.shape, shape)
88+
assert_array_equal(np.asarray(ap), arr * 2.0 + 10)

0 commit comments

Comments
 (0)