Repository: nilearn/nilearn

Issue:
Should smooth slightly the maps in the find_cuts.py code
No use detecting peaks on high-frequency noise.

We should use a cheap smoothing function, not ndimage.gaussian_filter, which is too expensive, but something like:

<pre>
        X_smooth = X.copy()
        X_smooth[:-1] += .2 * X[1:]
        X_smooth[1:]  += .2 * X[:-1]
        X_smooth[:, :-1] += .2 * X[:, 1:]
        X_smooth[:, 1:]  += .2 * X[:, :-1]
        X_smooth[:, :, :-1] += .2 * X[:, :, 1:]
        X_smooth[:, :, 1:]  += .2 * X[:, :, :-1]
</pre>



Discussion:

Resolving patch:
diff --git a/nilearn/image/image.py b/nilearn/image/image.py
--- a/nilearn/image/image.py
+++ b/nilearn/image/image.py
@@ -82,6 +82,55 @@ def high_variance_confounds(niimgs, n_confounds=5, percentile=2.,
                                            detrend=detrend)
 
 
+def _fast_smooth_array(arr):
+    """Simple smoothing which is less computationally expensive than
+    applying a gaussian filter.
+
+    Only the first three dimensions of the array will be smoothed. The
+    filter uses [0.2, 1, 0.2] weights in each direction and use a
+    normalisation to preserve the local average value.
+
+    Parameters
+    ==========
+    arr: numpy.ndarray
+        4D array, with image number as last dimension. 3D arrays are
+        also accepted.
+
+    Returns
+    =======
+    smoothed_arr: numpy.ndarray
+        Smoothed array.
+
+    Note
+    ====
+    Rather than calling this function directly, users are encouraged
+    to call the high-level function :func:`smooth_img` with
+    fwhm='fast'.
+
+    """
+    neighbor_weight = 0.2
+    # 6 neighbors in 3D if not on an edge
+    nb_neighbors = 6
+    # This scale ensures that a uniform array stays uniform
+    # except on the array edges
+    scale = 1 + nb_neighbors * neighbor_weight
+
+    # Need to copy because the smoothing is done in multiple statements
+    # and there does not seem to be an easy way to do it in place
+    smoothed_arr = arr.copy()
+    weighted_arr = neighbor_weight * arr
+
+    smoothed_arr[:-1] += weighted_arr[1:]
+    smoothed_arr[1:] += weighted_arr[:-1]
+    smoothed_arr[:, :-1] += weighted_arr[:, 1:]
+    smoothed_arr[:, 1:] += weighted_arr[:, :-1]
+    smoothed_arr[:, :, :-1] += weighted_arr[:, :, 1:]
+    smoothed_arr[:, :, 1:] += weighted_arr[:, :, :-1]
+    smoothed_arr /= scale
+
+    return smoothed_arr
+
+
 def _smooth_array(arr, affine, fwhm=None, ensure_finite=True, copy=True):
     """Smooth images by applying a Gaussian filter.
 
@@ -97,12 +146,16 @@ def _smooth_array(arr, affine, fwhm=None, ensure_finite=True, copy=True):
         (4, 4) matrix, giving affine transformation for image. (3, 3) matrices
         are also accepted (only these coefficients are used).
 
-    fwhm: scalar or numpy.ndarray
+    fwhm: scalar, numpy.ndarray, 'fast' or None
         Smoothing strength, as a full-width at half maximum, in millimeters.
         If a scalar is given, width is identical on all three directions.
         A numpy.ndarray must have 3 elements, giving the FWHM along each axis.
+        If fwhm == 'fast', a fast smoothing will be performed with
+        a filter [0.2, 1, 0.2] in each direction and a normalisation
+        to preserve the local average value.
         If fwhm is None, no filtering is performed (useful when just removal
-        of non-finite values is needed)
+        of non-finite values is needed).
+
 
     ensure_finite: bool
         if True, replace every non-finite values (like NaNs) by zero before
@@ -138,7 +191,9 @@ def _smooth_array(arr, affine, fwhm=None, ensure_finite=True, copy=True):
         # SPM tends to put NaNs in the data outside the brain
         arr[np.logical_not(np.isfinite(arr))] = 0
 
-    if fwhm is not None:
+    if fwhm == 'fast':
+        arr = _fast_smooth_array(arr)
+    elif fwhm is not None:
         # Convert from a FWHM to a sigma:
         # Do not use /=, fwhm may be a numpy scalar
         fwhm = fwhm / np.sqrt(8 * np.log(2))
@@ -161,10 +216,13 @@ def smooth_img(niimgs, fwhm):
     niimgs: niimgs or iterable of niimgs
         One or several niimage(s), either 3D or 4D.
 
-    fwhm: scalar or numpy.ndarray
+    fwhm: scalar, numpy.ndarray, 'fast' or None
         Smoothing strength, as a Full-Width at Half Maximum, in millimeters.
         If a scalar is given, width is identical on all three directions.
         A numpy.ndarray must have 3 elements, giving the FWHM along each axis.
+        If fwhm == 'fast', a fast smoothing will be performed with
+        a filter [0.2, 1, 0.2] in each direction and a normalisation
+        to preserve the scale.
         If fwhm is None, no filtering is performed (useful when just removal
         of non-finite values is needed)
 
diff --git a/nilearn/plotting/find_cuts.py b/nilearn/plotting/find_cuts.py
--- a/nilearn/plotting/find_cuts.py
+++ b/nilearn/plotting/find_cuts.py
@@ -15,6 +15,7 @@
 from .._utils.fast_maths import fast_abs_percentile
 from .._utils.numpy_conversions import as_ndarray
 from ..image.resampling import get_mask_bounds, coord_transform
+from ..image.image import _smooth_array
 
 ################################################################################
 # Functions for automatic choice of cuts coordinates
@@ -119,7 +120,6 @@ def _get_auto_mask_bounds(img):
             get_mask_bounds(nibabel.Nifti1Image(mask, affine))
     return (xmin, xmax), (ymin, ymax), (zmin, zmax)
 
-
 def find_cut_slices(img, direction='z', n_cuts=12, spacing='auto'):
     """ Find 'good' cross-section slicing positions along a given axis.
 
@@ -152,7 +152,6 @@ def find_cut_slices(img, direction='z', n_cuts=12, spacing='auto'):
 
     axis = 'xyz'.index(direction)
 
-
     affine = img.get_affine()
     orig_data = np.abs(img.get_data())
     this_shape = orig_data.shape[axis]
@@ -163,9 +162,8 @@ def find_cut_slices(img, direction='z', n_cuts=12, spacing='auto'):
     data = orig_data.copy()
     if data.dtype.kind == 'i':
         data = data.astype(np.float)
-        # We have discreete values: we smooth them in order to have
-        # maxima located at the center of peaks
-        data = ndimage.gaussian_filter(data, 3)
+
+    data = _smooth_array(data, affine, fwhm='fast')
 
     if spacing == 'auto':
         spacing = max(int(.5 / n_cuts * data.shape[axis]), 1)


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
@@ -33,6 +33,28 @@ def test_high_variance_confounds():
     assert_true(confounds2.shape == (length, n_confounds))
 
 
+def test__fast_smooth_array():
+    N = 4
+    shape = (N, N, N)
+    # hardcoded in _fast_smooth_array
+    neighbor_weight = 0.2
+    # 6 neighbors in 3D if you are not on an edge
+    nb_neighbors_max = 6
+
+    data = np.ones(shape)
+    smooth_data = image._fast_smooth_array(data)
+
+    # this contains the number of neighbors for each cell in the array
+    nb_neighbors_arr = np.empty(shape)
+    for (i, j, k), __ in np.ndenumerate(nb_neighbors_arr):
+        nb_neighbors_arr[i, j, k] = (3 + (0 < i < N - 1) +
+                                     (0 < j < N - 1) + (0 < k < N - 1))
+
+    expected = ((1 + neighbor_weight * nb_neighbors_arr) /
+                (1 + neighbor_weight * nb_neighbors_max))
+    np.testing.assert_allclose(smooth_data, expected)
+
+
 def test__smooth_array():
     """Test smoothing of images: _smooth_array()"""
     # Impulse in 3D
@@ -81,6 +103,11 @@ def test__smooth_array():
             np.testing.assert_equal(proj.sum(),
                                     fwhm / np.abs(affine[axis, axis]))
 
+    # Check fwhm='fast'
+    for affine in test_affines:
+        np.testing.assert_equal(image._smooth_array(data, affine, fwhm='fast'),
+                                image._fast_smooth_array(data))
+
 
 def test_smooth_img():
     # This function only checks added functionalities compared