Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
6feb581
add gifti image class function agg_data
htwangtw Aug 18, 2019
72471a7
PEP8
htwangtw Aug 18, 2019
98c68af
limit stacking to timeseries data only
htwangtw Aug 31, 2019
448e073
Update nibabel/gifti/gifti.py
htwangtw Sep 10, 2019
15d1dd7
Update nibabel/gifti/gifti.py
htwangtw Sep 10, 2019
85f65a4
Update nibabel/gifti/gifti.py
htwangtw Sep 10, 2019
de8f028
Update nibabel/gifti/gifti.py
htwangtw Sep 10, 2019
154321c
doc string draft 1, need to finish the examples
htwangtw Sep 12, 2019
0530484
ENH: Add general test data retriever
effigies Sep 12, 2019
8c6cd7d
DOCTEST: Retreive a surface file using test_data
effigies Sep 12, 2019
c54b696
DATA: Add 10 time point time series GIFTI in fsaverage3 space
effigies Sep 12, 2019
641feaa
TEST: Test new test_data function
effigies Sep 12, 2019
bee6065
Merge pull request #1 from effigies/enh/gifti_test_data
htwangtw Oct 8, 2019
bd95ce8
add doc and example for surface gii files
htwangtw Oct 8, 2019
ad3316f
Merge branch 'master' of https://github.com/htwangtw/nibabel into gif…
htwangtw Oct 10, 2019
aef5a4f
fix formatting; need timeseries example
htwangtw Oct 11, 2019
18501c4
add time series example
htwangtw Oct 11, 2019
51ff298
Correct more formatting usses
htwangtw Oct 11, 2019
b3adb15
docstring passed nosetests
htwangtw Oct 16, 2019
785a8d3
fix: trailing white space and os separator
htwangtw Oct 16, 2019
995d834
fix docstring output
htwangtw Oct 16, 2019
7ea7dec
fix os separater in the test
htwangtw Oct 16, 2019
f633676
Rename example file
htwangtw Oct 16, 2019
bf6eb97
Changing numpy float print style to 1.13
htwangtw Oct 16, 2019
a9471a0
Revert "Rename example file"
htwangtw Oct 21, 2019
9a52b78
Revert "Changing numpy float print style to 1.13"
htwangtw Oct 21, 2019
0447bbc
Revert "Revert "Rename example file""
htwangtw Oct 21, 2019
1ecaa26
Move the docstring to test
htwangtw Oct 21, 2019
384475b
Remove the actual docstring to prevent errors
htwangtw Oct 21, 2019
3fb7003
Revert "Remove the actual docstring to prevent errors"
htwangtw Oct 21, 2019
08a8752
Remove docstring in agg_data
htwangtw Oct 21, 2019
bb7517d
Remove docstring in test
htwangtw Oct 21, 2019
033ca51
add minimum example to docstring
htwangtw Oct 22, 2019
76efe61
add shape gifti
htwangtw Oct 22, 2019
c8c2c43
fix the test with shape gii
htwangtw Oct 23, 2019
654ee5b
delete trailing whitespace
htwangtw Oct 23, 2019
64b019a
DOC: More comprehensive agg_data examples
effigies Oct 27, 2019
5ea1e88
Merge pull request #2 from effigies/doc/agg_data
htwangtw Oct 28, 2019
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
135 changes: 135 additions & 0 deletions nibabel/gifti/gifti.py
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,141 @@ def get_arrays_from_intent(self, intent):
it = intent_codes.code[intent]
return [x for x in self.darrays if x.intent == it]

def agg_data(self, intent_code=None):
"""
Aggregate GIFTI data arrays into an ndarray or tuple of ndarray

In the general case, the numpy data array is extracted from each ``GiftiDataArray``
object and returned in a ``tuple``, in the order they are found in the GIFTI image.

If all ``GiftiDataArray`` s have ``intent`` of 2001 (``NIFTI_INTENT_TIME_SERIES``),
then the data arrays are concatenated as columns, producing a vertex-by-time array.
If an ``intent_code`` is passed, data arrays are filtered by the selected intents,
before being aggregated.
This may be useful for images containing several intents, or ensuring an expected
data type in an image of uncertain provenance.
If ``intent_code`` is a ``tuple``, then a ``tuple`` will be returned with the result of
``agg_data`` for each element, in order.
This may be useful for ensuring that expected data arrives in a consistent order.

Copy link
Member

Choose a reason for hiding this comment

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

It would be good to add some doctests that demonstrate how to use this. If we include a time series and a surface file, we could follow the examples of get_fdata():

The cache can effect the behavior of the image, because if the cache is
full, or you have an array image, then modifying the returned array
will modify the result of future calls to ``get_fdata()``. For example
you might do this:
>>> import os
>>> import nibabel as nib
>>> from nibabel.testing import data_path
>>> img_fname = os.path.join(data_path, 'example4d.nii.gz')
>>> img = nib.load(img_fname) # This is a proxy image
>>> nib.is_proxy(img.dataobj)
True
The array is not yet cached by a call to "get_fdata", so:
>>> img.in_memory
False
After we call ``get_fdata`` using the default `caching` == 'fill', the
cache contains a reference to the returned array ``data``:
>>> data = img.get_fdata()
>>> img.in_memory
True
We modify an element in the returned data array:
>>> data[0, 0, 0, 0]
0.0
>>> data[0, 0, 0, 0] = 99
>>> data[0, 0, 0, 0]
99.0
The next time we call 'get_fdata', the method returns the cached
reference to the (modified) array:
>>> data_again = img.get_fdata()
>>> data_again is data
True
>>> data_again[0, 0, 0, 0]
99.0
If you had *initially* used `caching` == 'unchanged' then the returned
``data`` array would have been loaded from file, but not cached, and:
>>> img = nib.load(img_fname) # a proxy image again
>>> data = img.get_fdata(caching='unchanged')
>>> img.in_memory
False
>>> data[0, 0, 0] = 99
>>> data_again = img.get_fdata(caching='unchanged')
>>> data_again is data
False
>>> data_again[0, 0, 0, 0]
0.0

I would show aggregating data:

  • without intent code
  • with matching intent codes
  • with mismatching intent codes (should return ())
  • with tuple intent codes

Parameters
----------
intent_code : None, string, integer or tuple of strings or integers, optional
code(s) specifying nifti intent

Returns
-------
tuple of ndarrays or ndarray
If the input is a tuple, the returned tuple will match the order.

Examples
--------

Consider a surface GIFTI file:

>>> import nibabel as nib
>>> from nibabel.testing import test_data
>>> surf_img = nib.load(test_data('gifti', 'ascii.gii'))

The coordinate data, which is indicated by the ``NIFTI_INTENT_POINTSET``
intent code, may be retrieved using any of the following equivalent
calls:

>>> coords = surf_img.agg_data('NIFTI_INTENT_POINTSET')
>>> coords_2 = surf_img.agg_data('pointset')
>>> coords_3 = surf_img.agg_data(1008) # Numeric code for pointset
>>> print(np.array2string(coords, precision=3))
[[-16.072 -66.188 21.267]
[-16.706 -66.054 21.233]
[-17.614 -65.402 21.071]]
>>> np.array_equal(coords, coords_2)
True
>>> np.array_equal(coords, coords_3)
True

Similarly, the triangle mesh can be retrieved using various intent
specifiers:

>>> triangles = surf_img.agg_data('NIFTI_INTENT_TRIANGLE')
>>> triangles_2 = surf_img.agg_data('triangle')
>>> triangles_3 = surf_img.agg_data(1009) # Numeric code for pointset
>>> print(np.array2string(triangles))
[0 1 2]
>>> np.array_equal(triangles, triangles_2)
True
>>> np.array_equal(triangles, triangles_3)
True

All arrays can be retrieved as a ``tuple`` by omitting the intent
code:

>>> coords_4, triangles_4 = surf_img.agg_data()
>>> np.array_equal(coords, coords_4)
True
>>> np.array_equal(triangles, triangles_4)
True

Finally, a tuple of intent codes may be passed in order to select
the arrays in a specific order:

>>> triangles_5, coords_5 = surf_img.agg_data(('triangle', 'pointset'))
>>> np.array_equal(triangles, triangles_5)
True
>>> np.array_equal(coords, coords_5)
True

The following image is a GIFTI file with ten (10) data arrays of the same
size, and with intent code 2001 (``NIFTI_INTENT_TIME_SERIES``):

>>> func_img = nib.load(test_data('gifti', 'task.func.gii'))

When aggregating time series data, these arrays are concatenated into
a single, vertex-by-timestep array:

>>> series = func_img.agg_data()
>>> series.shape
(642, 10)

In the case of a GIFTI file with unknown data arrays, it may be preferable
to specify the intent code, so that a time series array is always returned:

>>> series_2 = func_img.agg_data('NIFTI_INTENT_TIME_SERIES')
>>> series_3 = func_img.agg_data('time series')
>>> series_4 = func_img.agg_data(2001)
>>> np.array_equal(series, series_2)
True
>>> np.array_equal(series, series_3)
True
>>> np.array_equal(series, series_4)
True

Requesting a data array from a GIFTI file with no matching intent codes
will result in an empty tuple:

>>> surf_img.agg_data('time series')
()
>>> func_img.agg_data('triangle')
()
"""

# Allow multiple intents to specify the order
# e.g., agg_data(('pointset', 'triangle')) ensures consistent order

if isinstance(intent_code, tuple):
return tuple(self.agg_data(intent_code=code) for code in intent_code)

darrays = self.darrays if intent_code is None else self.get_arrays_from_intent(intent_code)
all_data = tuple(da.data for da in darrays)
all_intent = {intent_codes.niistring[da.intent] for da in darrays}

if all_intent == {'NIFTI_INTENT_TIME_SERIES'}: # stack when the gifti is a timeseries
return np.column_stack(all_data)
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure about this one. It's not clear to me that:

  1. We can assume multiple arrays of the same intent will be stackable. The shapes might not work out. For example, suppose I have a GIFTI with multiple pointsets of different lengths. (I don't know why you'd want this, but it's definitely not ruled out by the standard.)
  2. A user will prefer this stacking for any arbitrary intent. For instance, if I have a two-parameter statistical distribution like NIFTI_INTENT_NORMAL, I might get alternating columns (mu, sigma, mu, sigma, ...). In that case, perhaps concatenating on a new axis would be preferred. The nice thing about time series is that the spec is pretty clear that each time step is a 1-D array. And if we see NIFTI_INTENT_NONE, all bets are off.

I'm hesitant to make concatenation contingent on the shapes working out, as the output type will then be an even more complex function of the input data than we're already proposing. I see a few options:

  1. Try to np.column_stack, and if it fails, raise an error saying agg_data failed, so please munge the data arrays yourself.
  2. np.column_stack for time series only (I didn't see any others that seem obvious candidates), tuple for everything else. This should be pretty safe.
  3. Take option (2) by default, but allow it to be parameterized with an aggregator parameter:
    def agg_data(self, intent_code=None, aggregator=None):
        if aggregator is None:
            if intent_code == 'NIFTI_INTENT_TIME_SERIES':
                aggregator = np.column_stack
            else:
                aggregator = tuple
  1. Require an aggregator parameter, using tuple by default. This would make time series a less special case, but would make the return type much more predictable.

I'm inclined toward (2), with an option to move to (3) if people actually want control over that. Not sure they will, as they can always do np.column_stack(img.agg_data()). WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wrote something but for got to hit reply.....

Anyway, the TLDR of my original comment was, these are great suggestions! I didn't think about those weird but totally legit use of gifti format. I agreed to go for 2 for now and move to 3. I think we will need more people to use these functions to know what is common for users.
Option 1 is just cruel. Option 4 is going to be confusing for users that don't really know the data structure.


if len(all_data) == 1:
all_data = all_data[0]

return all_data

@deprecate_with_version(
'getArraysFromIntent method deprecated. '
"Use get_arrays_from_intent instead.",
Expand Down
Loading