Repository: nilearn/nilearn

Issue:
index_img / iter_img
Preparing my course on nilearn, it is becoming clearer and clearer that we need something to index iter niimgs in the 4th direction.

I would suggest the following semantics:

<pre>
def index_img(niimgs, index):
     niimgs = niimgs.check_niimgs(niimgs)
     return nibabel.Nifti1Image(niimgs.get_data()[:, :, :, index], niimgs.get_affine(), header=niimgs.get_header())

def iter_img(niimgs):
     niimgs = niimgs.check_niimgs(niimgs)
     for i in range(_utils.get_shape(niimgs)[3]):
         yield nibabel.Nifti1Image(niimgs.get_data()[:, :, :, i], niimgs.get_affine(), header=niimgs.get_header())
</pre>


What do people think?

I am thinking that this may be something that we should prioritize before the 1.0 release.



Discussion:
I have these functions in my toolbox and use them all the time.

How about a cool nifti generator? :-P

https://github.com/nilearn/nilearn/pull/272

> How about a cool nifti generator? :-P
> 
> https://github.com/nilearn/nilearn/pull/272

In essence, it is the same idea, which is probably a good thing: if the
same idea keeps coming up, it is probably useful.

One minor difference is the vocabulary in which it is formulated. I think
that a function (technicaly and generator function) rather than an
object, and avoiding the term "generator" in the name, makes things a bit
more friendly to Joe user.

@AlexandreAbraham : want to do a PR?


Resolving patch:
diff --git a/nilearn/_utils/niimg_conversions.py b/nilearn/_utils/niimg_conversions.py
--- a/nilearn/_utils/niimg_conversions.py
+++ b/nilearn/_utils/niimg_conversions.py
@@ -302,7 +302,7 @@ def concat_niimgs(niimgs, dtype=np.float32, accept_4d=False,
     return nibabel.Nifti1Image(data, target_affine)
 
 
-def check_niimgs(niimgs, accept_3d=False):
+def check_niimgs(niimgs, accept_3d=False, return_iterator=False):
     """ Check that an object is a list of niimgs and load it if necessary
 
     Parameters
@@ -315,15 +315,19 @@ def check_niimgs(niimgs, accept_3d=False):
         call nibabel.load on it. If it is an object, check if get_data
         and get_affine methods are present, raise an Exception otherwise.
 
-    accept_3d (boolean)
+    accept_3d: boolean
        If True, consider a 3D image as a 4D one with last dimension equals
        to 1.
 
+    return_iterator: boolean
+        If False, a single 4D image is returned. When `niimgs` contains 3D
+        images they are concatenated together.
+        If True, an iterator of 3D images is returned. This reduces the memory
+        usage when `niimgs` contains 3D images.
+
     Returns
     -------
-    niimg: nibabel.Nifti1Image
-        One 4D image. If 3D images were provided as input, this is the
-        concatenation of all of them.
+    niimg: 4D nibabel.Nifti1Image or iterator of 3D nibabel.Nifti1Image
 
     Notes
     -----
@@ -372,7 +376,21 @@ def check_niimgs(niimgs, accept_3d=False):
 
     # Now, we load data as we know its format
     if dim == 4:
-        niimg = check_niimg(niimgs)
+        if return_iterator:
+            result = (_index_niimgs(niimgs, i)
+                      for i in range(_get_shape(niimgs)[3]))
+        else:
+            result = check_niimg(niimgs)
     else:
-        niimg = concat_niimgs(niimgs)
-    return niimg
+        if return_iterator:
+            result = (check_niimg(img) for img in niimgs)
+        else:
+            result = concat_niimgs(niimgs)
+    return result
+
+
+def _index_niimgs(niimgs, index):
+    """Helper function for check_niimgs."""
+    return nibabel.Nifti1Image(niimgs.get_data()[:, :, :, index],
+                               niimgs.get_affine(),
+                               header=niimgs.get_header())
diff --git a/nilearn/image/__init__.py b/nilearn/image/__init__.py
--- a/nilearn/image/__init__.py
+++ b/nilearn/image/__init__.py
@@ -4,11 +4,10 @@
 """
 from .resampling import resample_img, reorder_img
 from .image import high_variance_confounds, smooth_img, crop_img, \
-            mean_img, swap_img_hemispheres
+    mean_img, swap_img_hemispheres, index_img, iter_img
 from .._utils.niimg_conversions import concat_niimgs as concat_imgs
 
 __all__ = ['resample_img', 'high_variance_confounds', 'smooth_img',
            'crop_img', 'mean_img', 'reorder_img',
-           'swap_img_hemispheres', 'concat_imgs']
-
-
+           'swap_img_hemispheres', 'concat_imgs',
+           'index_img', 'iter_img']
diff --git a/nilearn/image/image.py b/nilearn/image/image.py
--- a/nilearn/image/image.py
+++ b/nilearn/image/image.py
@@ -15,7 +15,8 @@
 
 from .. import signal
 from .._utils import check_niimgs, check_niimg, as_ndarray, _repr_niimgs
-from .._utils.niimg_conversions import _safe_get_data
+from .._utils.niimg_conversions import (_safe_get_data, check_niimgs,
+                                        _index_niimgs)
 from .. import masking
 from nilearn.image import reorder_img
 
@@ -502,6 +503,44 @@ def swap_img_hemispheres(img):
 
     # create swapped nifti object
     out_img = nibabel.Nifti1Image(img.get_data()[::-1], img.get_affine(),
-        header=img.get_header())
+                                  header=img.get_header())
 
     return out_img
+
+
+def index_img(imgs, index):
+    """Indexes into a 4D Niimg-like object in the fourth dimension.
+
+    Common use cases include extracting a 3D image out of `img` or
+    creating a 4D image whose data is a subset of `img` data.
+
+    Parameters
+    ----------
+    imgs: 4D Niimg-like object
+        See http://nilearn.github.io/building_blocks/manipulating_mr_images.html#niimg.
+
+    index: Any type compatible with numpy array indexing
+        Used for indexing the 4D data array in the fourth dimension.
+
+    Returns
+    -------
+    output: nibabel.Nifti1Image
+
+    """
+    imgs = check_niimgs(imgs)
+    return _index_niimgs(imgs, index)
+
+
+def iter_img(imgs):
+    """Iterates over a 4D Niimg-like object in the fourth dimension.
+
+    Parameters
+    ----------
+    imgs: 4D Niimg-like object
+        See http://nilearn.github.io/building_blocks/manipulating_mr_images.html#niimg.
+
+    Returns
+    -------
+    output: iterator of 3D nibabel.Nifti1Image
+    """
+    return check_niimgs(imgs, return_iterator=True)


Test patch:
diff --git a/nilearn/image/tests/test_image.py b/nilearn/image/tests/test_image.py
--- a/nilearn/image/tests/test_image.py
+++ b/nilearn/image/tests/test_image.py
@@ -1,7 +1,7 @@
 """
 Test image pre-processing functions
 """
-from nose.tools import assert_true, assert_false, assert_raises
+from nose.tools import assert_true, assert_false
 
 import nibabel
 import numpy as np
@@ -12,6 +12,7 @@
 from .. import concat_imgs
 from ..._utils import testing, niimg_conversions
 
+
 def test_high_variance_confounds():
     # See also test_signals.test_high_variance_confounds()
     # There is only tests on what is added by image.high_variance_confounds()
@@ -274,3 +275,60 @@ def test_swap_img_hemispheres():
 
 def test_concat_imgs():
     assert_true(concat_imgs is niimg_conversions.concat_niimgs)
+
+
+def test_index_img():
+    img_3d = nibabel.Nifti1Image(np.ones((3, 4, 5)), np.eye(4))
+    testing.assert_raises_regexp(TypeError, '4D Nifti image',
+                                 image.index_img, img_3d, 0)
+
+    affine = np.array([[1., 2., 3., 4.],
+                       [5., 6., 7., 8.],
+                       [9., 10., 11., 12.],
+                       [0., 0., 0., 1.]])
+    img_4d, _ = testing.generate_fake_fmri(affine=affine)
+
+    fourth_dim_size = niimg_conversions._get_shape(img_4d)[3]
+    tested_indices = (range(fourth_dim_size) +
+                      [slice(2, 8, 2), [1, 2, 3, 2], [],
+                       (np.arange(fourth_dim_size) % 3) == 1])
+    for i in tested_indices:
+        this_img = image.index_img(img_4d, i)
+        expected_data_3d = img_4d.get_data()[..., i]
+        assert_array_equal(this_img.get_data(),
+                           expected_data_3d)
+        assert_array_equal(this_img.get_affine(),
+                           img_4d.get_affine())
+
+    for i in [fourth_dim_size, - fourth_dim_size - 1,
+              [0, fourth_dim_size],
+              np.repeat(True, fourth_dim_size + 1)]:
+        testing.assert_raises_regexp(IndexError, 'out of bounds',
+                                     image.index_img, img_4d, i)
+
+
+def test_iter_img():
+    img_3d = nibabel.Nifti1Image(np.ones((3, 4, 5)), np.eye(4))
+    testing.assert_raises_regexp(TypeError, '4D Nifti image',
+                                 image.iter_img, img_3d)
+
+    affine = np.array([[1., 2., 3., 4.],
+                       [5., 6., 7., 8.],
+                       [9., 10., 11., 12.],
+                       [0., 0., 0., 1.]])
+    img_4d, _ = testing.generate_fake_fmri(affine=affine)
+
+    for i, img in enumerate(image.iter_img(img_4d)):
+        expected_data_3d = img_4d.get_data()[..., i]
+        assert_array_equal(img.get_data(),
+                           expected_data_3d)
+        assert_array_equal(img.get_affine(),
+                           img_4d.get_affine())
+
+    img_3d_list = list(image.iter_img(img_4d))
+    for i, img in enumerate(image.iter_img(img_3d_list)):
+        expected_data_3d = img_4d.get_data()[..., i]
+        assert_array_equal(img.get_data(),
+                           expected_data_3d)
+        assert_array_equal(img.get_affine(),
+                           img_4d.get_affine())
diff --git a/nilearn/tests/test_niimg_conversions.py b/nilearn/tests/test_niimg_conversions.py
--- a/nilearn/tests/test_niimg_conversions.py
+++ b/nilearn/tests/test_niimg_conversions.py
@@ -7,9 +7,10 @@
 # Author: Gael Varoquaux, Alexandre Abraham
 # License: simplified BSD
 
-import nose
 import os
 import tempfile
+import itertools
+
 from nose.tools import assert_equal, assert_true
 
 import numpy as np
@@ -80,14 +81,46 @@ def test_check_niimgs():
                          _utils.check_niimgs, [])
 
     affine = np.eye(4)
-    img = Nifti1Image(np.ones((10, 10, 10)), affine)
+    img_3d = Nifti1Image(np.ones((10, 10, 10)), affine)
+
+    # Tests with return_iterator=False
+    img_4d_1 = _utils.check_niimgs([img_3d, img_3d])
+    assert_true(img_4d_1.get_data().shape == (10, 10, 10, 2))
+    assert_array_equal(img_4d_1.get_affine(), affine)
+
+    img_4d_2 = _utils.check_niimgs(img_4d_1)
+    assert_array_equal(img_4d_2.get_data(), img_4d_2.get_data())
+    assert_array_equal(img_4d_2.get_affine(), img_4d_2.get_affine())
+
+    # Tests with return_iterator=True
+    img_3d_iterator = _utils.check_niimgs([img_3d, img_3d],
+                                          return_iterator=True)
+    img_3d_iterator_length = sum(1 for _ in img_3d_iterator)
+    assert_true(img_3d_iterator_length == 2)
+
+    img_3d_iterator_1 = _utils.check_niimgs([img_3d, img_3d],
+                                            return_iterator=True)
+    img_3d_iterator_2 = _utils.check_niimgs(img_3d_iterator_1,
+                                            return_iterator=True)
+    for img_1, img_2 in itertools.izip(img_3d_iterator_1, img_3d_iterator_2):
+        assert_true(img_1.get_data().shape == (10, 10, 10))
+        assert_array_equal(img_1.get_data(), img_2.get_data())
+        assert_array_equal(img_1.get_affine(), img_2.get_affine())
+
+    img_3d_iterator_1 = _utils.check_niimgs([img_3d, img_3d],
+                                            return_iterator=True)
+    img_3d_iterator_2 = _utils.check_niimgs(img_4d_1,
+                                            return_iterator=True)
+    for img_1, img_2 in itertools.izip(img_3d_iterator_1, img_3d_iterator_2):
+        assert_true(img_1.get_data().shape == (10, 10, 10))
+        assert_array_equal(img_1.get_data(), img_2.get_data())
+        assert_array_equal(img_1.get_affine(), img_2.get_affine())
 
-    _utils.check_niimgs([img, img])
     # This should raise an error: a 3D img is given and we want a 4D
     assert_raises_regexp(TypeError, 'image',
-                         _utils.check_niimgs, img)
+                         _utils.check_niimgs, img_3d)
     # This shouldn't raise an error
-    _utils.check_niimgs(img, accept_3d=True)
+    _utils.check_niimgs(img_3d, accept_3d=True)
 
     # Test a Niimage that does not hold a shape attribute
     phony_img = PhonyNiimage()