Repository: scikit-image/scikit-image

Issue:
meijering filter artefacts around the image
## Description

`maijering` filter suffers from the same artifacts as `frangi` described in #4249 .

## Way to reproduce

```python
import pylab as plt
from skimage import color, data
from skimage.filters import meijering
img = color.rgb2gray(data.retina())[300:500, 700:900]
out = meijering(img, sigmas=[1])
plt.imshow(out)
```

![meijerin](https://user-images.githubusercontent.com/3438227/72299913-8d16d480-3662-11ea-9edb-6b95bed769c6.png)

Managing border conditions for the ridges filters as suggested by @shenker https://github.com/scikit-image/scikit-image/pull/4343#pullrequestreview-337754448 may solve the problem.

## Version information
```
3.7.5 (default, Oct 25 2019, 15:51:11) 
[GCC 7.3.0]
Linux-4.4.0-171-generic-x86_64-with-debian-stretch-sid
scikit-image version: 0.17.dev0
numpy version: 1.17.4
```



Discussion:

Resolving patch:
diff --git a/doc/examples/edges/plot_ridge_filter.py b/doc/examples/edges/plot_ridge_filter.py
--- a/doc/examples/edges/plot_ridge_filter.py
+++ b/doc/examples/edges/plot_ridge_filter.py
@@ -57,17 +57,13 @@ def identity(image, **kwargs):
 image = color.rgb2gray(data.retina())[300:700, 700:900]
 cmap = plt.cm.gray
 
-kwargs = {}
-kwargs['sigmas'] = [1]
+kwargs = {'sigmas': [1], 'mode': 'reflect'}
 
 fig, axes = plt.subplots(2, 5)
 for i, black_ridges in enumerate([1, 0]):
     for j, func in enumerate([identity, meijering, sato, frangi, hessian]):
         kwargs['black_ridges'] = black_ridges
         result = func(image, **kwargs)
-        if func is meijering:
-            # Crop by 4 pixels for rendering purpose.
-            result = result[4:-4, 4:-4]
         axes[i, j].imshow(result, cmap=cmap, aspect='auto')
         if i == 0:
             axes[i, j].set_title(['Original\nimage', 'Meijering\nneuriteness',
diff --git a/skimage/filters/ridges.py b/skimage/filters/ridges.py
--- a/skimage/filters/ridges.py
+++ b/skimage/filters/ridges.py
@@ -81,6 +81,31 @@ def _sortbyabs(array, axis=0):
     return array[tuple(index)]
 
 
+def _check_sigmas(sigmas):
+    """Check sigma values for ridges filters.
+
+    Parameters
+    ----------
+    sigmas : iterable of floats
+        Sigmas argument to be checked
+
+    Returns
+    -------
+    sigmas : ndarray
+        input iterable converted to ndarray
+
+    Raises
+    ------
+    ValueError if any input value is negative
+
+    """
+    sigmas = np.asarray(sigmas).ravel()
+    if np.any(sigmas < 0.0):
+        raise ValueError('Sigma values should be equal to or greater '
+                         'than zero.')
+    return sigmas
+
+
 def compute_hessian_eigenvalues(image, sigma, sorting='none',
                                 mode='constant', cval=0):
     """
@@ -140,7 +165,7 @@ def compute_hessian_eigenvalues(image, sigma, sorting='none',
 
 
 def meijering(image, sigmas=range(1, 10, 2), alpha=None,
-              black_ridges=True):
+              black_ridges=True, mode='reflect', cval=0):
     """
     Filter an image with the Meijering neuriteness filter.
 
@@ -163,6 +188,11 @@ def meijering(image, sigmas=range(1, 10, 2), alpha=None,
     black_ridges : boolean, optional
         When True (the default), the filter detects black ridges; when
         False, it detects white ridges.
+    mode : {'constant', 'reflect', 'wrap', 'nearest', 'mirror'}, optional
+        How to handle values outside the image borders.
+    cval : float, optional
+        Used in conjunction with mode 'constant', the value outside
+        the image boundaries.
 
     Returns
     -------
@@ -185,9 +215,7 @@ def meijering(image, sigmas=range(1, 10, 2), alpha=None,
     """
 
     # Check (sigma) scales
-    sigmas = np.asarray(sigmas)
-    if np.any(sigmas < 0.0):
-        raise ValueError('Sigma values less than zero are not valid')
+    sigmas = _check_sigmas(sigmas)
 
     # Get image dimensions
     ndim = image.ndim
@@ -208,7 +236,8 @@ def meijering(image, sigmas=range(1, 10, 2), alpha=None,
     for i, sigma in enumerate(sigmas):
 
         # Calculate (sorted) eigenvalues
-        eigenvalues = compute_hessian_eigenvalues(image, sigma, sorting='abs')
+        eigenvalues = compute_hessian_eigenvalues(image, sigma, sorting='abs',
+                                                  mode=mode, cval=cval)
 
         if ndim > 1:
 
@@ -236,7 +265,8 @@ def meijering(image, sigmas=range(1, 10, 2), alpha=None,
     return np.max(filtered_array, axis=0)
 
 
-def sato(image, sigmas=range(1, 10, 2), black_ridges=True):
+def sato(image, sigmas=range(1, 10, 2), black_ridges=True,
+         mode=None, cval=0):
     """
     Filter an image with the Sato tubeness filter.
 
@@ -257,6 +287,11 @@ def sato(image, sigmas=range(1, 10, 2), black_ridges=True):
     black_ridges : boolean, optional
         When True (the default), the filter detects black ridges; when
         False, it detects white ridges.
+    mode : {'constant', 'reflect', 'wrap', 'nearest', 'mirror'}, optional
+        How to handle values outside the image borders.
+    cval : float, optional
+        Used in conjunction with mode 'constant', the value outside
+        the image boundaries.
 
     Returns
     -------
@@ -282,9 +317,15 @@ def sato(image, sigmas=range(1, 10, 2), black_ridges=True):
     check_nD(image, [2, 3])
 
     # Check (sigma) scales
-    sigmas = np.asarray(sigmas)
-    if np.any(sigmas < 0.0):
-        raise ValueError('Sigma values less than zero are not valid')
+    sigmas = _check_sigmas(sigmas)
+
+    if mode is None:
+        warn("Previously, sato implicitly used 'constant' as the "
+             "border mode when dealing with the edge of the array. The new "
+             "behavior is 'reflect'. To recover the old behavior, use "
+             "mode='constant'. To avoid this warning, please explicitly "
+             "set the mode.", category=FutureWarning, stacklevel=2)
+        mode = 'reflect'
 
     # Invert image to detect bright ridges on dark background
     if not black_ridges:
@@ -299,7 +340,8 @@ def sato(image, sigmas=range(1, 10, 2), black_ridges=True):
 
         # Calculate (sorted) eigenvalues
         lamba1, *lambdas = compute_hessian_eigenvalues(image, sigma,
-                                                       sorting='val')
+                                                       sorting='val',
+                                                       mode=mode, cval=cval)
 
         # Compute tubeness, see  equation (9) in reference [1]_.
         # np.abs(lambda2) in 2D, np.sqrt(np.abs(lambda2 * lambda3)) in 3D
@@ -405,9 +447,7 @@ def frangi(image, sigmas=range(1, 10, 2), scale_range=None,
     check_nD(image, [2, 3])
 
     # Check (sigma) scales
-    sigmas = np.asarray(sigmas).ravel()
-    if np.any(sigmas < 0.0):
-        raise ValueError('Sigma values less than zero are not valid')
+    sigmas = _check_sigmas(sigmas)
 
     # Rescale filter parameters
     alpha_sq = 2 * alpha ** 2
@@ -465,7 +505,7 @@ def frangi(image, sigmas=range(1, 10, 2), scale_range=None,
 
 def hessian(image, sigmas=range(1, 10, 2), scale_range=None, scale_step=None,
             beta1=None, beta2=None, alpha=0.5, beta=0.5, gamma=15,
-            black_ridges=True):
+            black_ridges=True, mode=None, cval=0):
     """Filter an image with the Hybrid Hessian filter.
 
     This filter can be used to detect continuous edges, e.g. vessels,
@@ -496,6 +536,11 @@ def hessian(image, sigmas=range(1, 10, 2), scale_range=None, scale_step=None,
     black_ridges : boolean, optional
         When True (the default), the filter detects black ridges; when
         False, it detects white ridges.
+    mode : {'constant', 'reflect', 'wrap', 'nearest', 'mirror'}, optional
+        How to handle values outside the image borders.
+    cval : float, optional
+        Used in conjunction with mode 'constant', the value outside
+        the image boundaries.
 
     Returns
     -------
@@ -522,10 +567,18 @@ def hessian(image, sigmas=range(1, 10, 2), scale_range=None, scale_step=None,
     .. [2] Kroon, D. J.: Hessian based Frangi vesselness filter.
     """
 
+    if mode is None:
+        warn("Previously, hessian implicitly used 'constant' as the "
+             "border mode when dealing with the edge of the array. The new "
+             "behavior is 'reflect'. To recover the old behavior, use "
+             "mode='constant'. To avoid this warning, please explicitly "
+             "set the mode.", category=FutureWarning, stacklevel=2)
+        mode = 'reflect'
+
     filtered = frangi(image, sigmas=sigmas, scale_range=scale_range,
                       scale_step=scale_step, beta1=beta1, beta2=beta2,
                       alpha=alpha, beta=beta, gamma=gamma,
-                      black_ridges=black_ridges)
+                      black_ridges=black_ridges, mode=mode, cval=cval)
 
     filtered[filtered <= 0] = 1
     return filtered


Test patch:
diff --git a/skimage/filters/tests/test_ridges.py b/skimage/filters/tests/test_ridges.py
--- a/skimage/filters/tests/test_ridges.py
+++ b/skimage/filters/tests/test_ridges.py
@@ -1,10 +1,11 @@
-
+import pytest
 import numpy as np
 from numpy.testing import assert_allclose, assert_array_less, assert_equal
 from skimage.filters import meijering, sato, frangi, hessian
 from skimage.data import camera, retina
 from skimage.util import crop, invert
 from skimage.color import rgb2gray
+from skimage._shared._warnings import expected_warnings
 
 
 def test_2d_null_matrix():
@@ -15,17 +16,17 @@ def test_2d_null_matrix():
     zeros = np.zeros((3, 3))
     ones = np.ones((3, 3))
 
-    assert_equal(meijering(a_black, black_ridges=True), ones)
-    assert_equal(meijering(a_white, black_ridges=False), ones)
+    assert_equal(meijering(a_black, black_ridges=True), zeros)
+    assert_equal(meijering(a_white, black_ridges=False), zeros)
 
-    assert_equal(sato(a_black, black_ridges=True), zeros)
-    assert_equal(sato(a_white, black_ridges=False), zeros)
+    assert_equal(sato(a_black, black_ridges=True, mode='reflect'), zeros)
+    assert_equal(sato(a_white, black_ridges=False, mode='reflect'), zeros)
 
     assert_allclose(frangi(a_black, black_ridges=True), zeros, atol=1e-3)
     assert_allclose(frangi(a_white, black_ridges=False), zeros, atol=1e-3)
 
-    assert_equal(hessian(a_black, black_ridges=False), ones)
-    assert_equal(hessian(a_white, black_ridges=True), ones)
+    assert_equal(hessian(a_black, black_ridges=False, mode='reflect'), ones)
+    assert_equal(hessian(a_white, black_ridges=True, mode='reflect'), ones)
 
 
 def test_3d_null_matrix():
@@ -36,17 +37,17 @@ def test_3d_null_matrix():
     zeros = np.zeros((3, 3, 3))
     ones = np.ones((3, 3, 3))
 
-    assert_allclose(meijering(a_black, black_ridges=True), ones, atol=1e-1)
-    assert_allclose(meijering(a_white, black_ridges=False), ones, atol=1e-1)
+    assert_allclose(meijering(a_black, black_ridges=True), zeros, atol=1e-1)
+    assert_allclose(meijering(a_white, black_ridges=False), zeros, atol=1e-1)
 
-    assert_equal(sato(a_black, black_ridges=True), zeros)
-    assert_equal(sato(a_white, black_ridges=False), zeros)
+    assert_equal(sato(a_black, black_ridges=True, mode='reflect'), zeros)
+    assert_equal(sato(a_white, black_ridges=False, mode='reflect'), zeros)
 
     assert_allclose(frangi(a_black, black_ridges=True), zeros, atol=1e-3)
     assert_allclose(frangi(a_white, black_ridges=False), zeros, atol=1e-3)
 
-    assert_equal(hessian(a_black, black_ridges=False), ones)
-    assert_equal(hessian(a_white, black_ridges=True), ones)
+    assert_equal(hessian(a_black, black_ridges=False, mode='reflect'), ones)
+    assert_equal(hessian(a_white, black_ridges=True, mode='reflect'), ones)
 
 
 def test_2d_energy_decrease():
@@ -60,18 +61,20 @@ def test_2d_energy_decrease():
     assert_array_less(meijering(a_white, black_ridges=False).std(),
                       a_white.std())
 
-    assert_array_less(sato(a_black, black_ridges=True).std(), a_black.std())
-    assert_array_less(sato(a_white, black_ridges=False).std(), a_white.std())
+    assert_array_less(sato(a_black, black_ridges=True, mode='reflect').std(),
+                      a_black.std())
+    assert_array_less(sato(a_white, black_ridges=False, mode='reflect').std(),
+                      a_white.std())
 
     assert_array_less(frangi(a_black, black_ridges=True).std(),
                       a_black.std())
     assert_array_less(frangi(a_white, black_ridges=False).std(),
                       a_white.std())
 
-    assert_array_less(hessian(a_black, black_ridges=True).std(),
-                      a_black.std())
-    assert_array_less(hessian(a_white, black_ridges=False).std(),
-                      a_white.std())
+    assert_array_less(hessian(a_black, black_ridges=True,
+                              mode='reflect').std(), a_black.std())
+    assert_array_less(hessian(a_white, black_ridges=False,
+                              mode='reflect').std(), a_white.std())
 
 
 def test_3d_energy_decrease():
@@ -85,18 +88,20 @@ def test_3d_energy_decrease():
     assert_array_less(meijering(a_white, black_ridges=False).std(),
                       a_white.std())
 
-    assert_array_less(sato(a_black, black_ridges=True).std(), a_black.std())
-    assert_array_less(sato(a_white, black_ridges=False).std(), a_white.std())
+    assert_array_less(sato(a_black, black_ridges=True, mode='reflect').std(),
+                      a_black.std())
+    assert_array_less(sato(a_white, black_ridges=False, mode='reflect').std(),
+                      a_white.std())
 
     assert_array_less(frangi(a_black, black_ridges=True).std(),
                       a_black.std())
     assert_array_less(frangi(a_white, black_ridges=False).std(),
                       a_white.std())
 
-    assert_array_less(hessian(a_black, black_ridges=True).std(),
-                      a_black.std())
-    assert_array_less(hessian(a_white, black_ridges=False).std(),
-                      a_white.std())
+    assert_array_less(hessian(a_black, black_ridges=True,
+                              mode='reflect').std(), a_black.std())
+    assert_array_less(hessian(a_white, black_ridges=False,
+                              mode='reflect').std(), a_white.std())
 
 
 def test_2d_linearity():
@@ -109,20 +114,24 @@ def test_2d_linearity():
     assert_allclose(meijering(1 * a_white, black_ridges=False),
                     meijering(10 * a_white, black_ridges=False), atol=1e-3)
 
-    assert_allclose(sato(1 * a_black, black_ridges=True),
-                    sato(10 * a_black, black_ridges=True), atol=1e-3)
-    assert_allclose(sato(1 * a_white, black_ridges=False),
-                    sato(10 * a_white, black_ridges=False), atol=1e-3)
+    assert_allclose(sato(1 * a_black, black_ridges=True, mode='reflect'),
+                    sato(10 * a_black, black_ridges=True, mode='reflect'),
+                    atol=1e-3)
+    assert_allclose(sato(1 * a_white, black_ridges=False, mode='reflect'),
+                    sato(10 * a_white, black_ridges=False, mode='reflect'),
+                    atol=1e-3)
 
     assert_allclose(frangi(1 * a_black, black_ridges=True),
                     frangi(10 * a_black, black_ridges=True), atol=1e-3)
     assert_allclose(frangi(1 * a_white, black_ridges=False),
                     frangi(10 * a_white, black_ridges=False), atol=1e-3)
 
-    assert_allclose(hessian(1 * a_black, black_ridges=True),
-                    hessian(10 * a_black, black_ridges=True), atol=1e-3)
-    assert_allclose(hessian(1 * a_white, black_ridges=False),
-                    hessian(10 * a_white, black_ridges=False), atol=1e-3)
+    assert_allclose(hessian(1 * a_black, black_ridges=True, mode='reflect'),
+                    hessian(10 * a_black, black_ridges=True, mode='reflect'),
+                    atol=1e-3)
+    assert_allclose(hessian(1 * a_white, black_ridges=False, mode='reflect'),
+                    hessian(10 * a_white, black_ridges=False, mode='reflect'),
+                    atol=1e-3)
 
 
 def test_3d_linearity():
@@ -135,20 +144,24 @@ def test_3d_linearity():
     assert_allclose(meijering(1 * a_white, black_ridges=False),
                     meijering(10 * a_white, black_ridges=False), atol=1e-3)
 
-    assert_allclose(sato(1 * a_black, black_ridges=True),
-                    sato(10 * a_black, black_ridges=True), atol=1e-3)
-    assert_allclose(sato(1 * a_white, black_ridges=False),
-                    sato(10 * a_white, black_ridges=False), atol=1e-3)
+    assert_allclose(sato(1 * a_black, black_ridges=True, mode='reflect'),
+                    sato(10 * a_black, black_ridges=True, mode='reflect'),
+                    atol=1e-3)
+    assert_allclose(sato(1 * a_white, black_ridges=False, mode='reflect'),
+                    sato(10 * a_white, black_ridges=False, mode='reflect'),
+                    atol=1e-3)
 
     assert_allclose(frangi(1 * a_black, black_ridges=True),
                     frangi(10 * a_black, black_ridges=True), atol=1e-3)
     assert_allclose(frangi(1 * a_white, black_ridges=False),
                     frangi(10 * a_white, black_ridges=False), atol=1e-3)
 
-    assert_allclose(hessian(1 * a_black, black_ridges=True),
-                    hessian(10 * a_black, black_ridges=True), atol=1e-3)
-    assert_allclose(hessian(1 * a_white, black_ridges=False),
-                    hessian(10 * a_white, black_ridges=False), atol=1e-3)
+    assert_allclose(hessian(1 * a_black, black_ridges=True, mode='reflect'),
+                    hessian(10 * a_black, black_ridges=True, mode='reflect'),
+                    atol=1e-3)
+    assert_allclose(hessian(1 * a_white, black_ridges=False, mode='reflect'),
+                    hessian(10 * a_white, black_ridges=False, mode='reflect'),
+                    atol=1e-3)
 
 
 def test_2d_cropped_camera_image():
@@ -162,14 +175,16 @@ def test_2d_cropped_camera_image():
     assert_allclose(meijering(a_black, black_ridges=True),
                     meijering(a_white, black_ridges=False))
 
-    assert_allclose(sato(a_black, black_ridges=True),
-                    sato(a_white, black_ridges=False))
+    assert_allclose(sato(a_black, black_ridges=True, mode='reflect'),
+                    sato(a_white, black_ridges=False, mode='reflect'))
 
     assert_allclose(frangi(a_black, black_ridges=True), zeros, atol=1e-3)
     assert_allclose(frangi(a_white, black_ridges=False), zeros, atol=1e-3)
 
-    assert_allclose(hessian(a_black, black_ridges=True), ones, atol=1 - 1e-7)
-    assert_allclose(hessian(a_white, black_ridges=False), ones, atol=1 - 1e-7)
+    assert_allclose(hessian(a_black, black_ridges=True, mode='reflect'),
+                    ones, atol=1 - 1e-7)
+    assert_allclose(hessian(a_white, black_ridges=False, mode='reflect'),
+                    ones, atol=1 - 1e-7)
 
 
 def test_3d_cropped_camera_image():
@@ -184,19 +199,25 @@ def test_3d_cropped_camera_image():
     assert_allclose(meijering(a_black, black_ridges=True),
                     meijering(a_white, black_ridges=False))
 
-    assert_allclose(sato(a_black, black_ridges=True),
-                    sato(a_white, black_ridges=False))
+    assert_allclose(sato(a_black, black_ridges=True, mode='reflect'),
+                    sato(a_white, black_ridges=False, mode='reflect'))
 
     assert_allclose(frangi(a_black, black_ridges=True), zeros, atol=1e-3)
     assert_allclose(frangi(a_white, black_ridges=False), zeros, atol=1e-3)
 
-    assert_allclose(hessian(a_black, black_ridges=True), ones, atol=1 - 1e-7)
-    assert_allclose(hessian(a_white, black_ridges=False), ones, atol=1 - 1e-7)
+    assert_allclose(hessian(a_black, black_ridges=True, mode='reflect'),
+                    ones, atol=1 - 1e-7)
+    assert_allclose(hessian(a_white, black_ridges=False, mode='reflect'),
+                    ones, atol=1 - 1e-7)
 
 
-def test_border_management():
+@pytest.mark.parametrize('func, tol', [(frangi, 1e-7),
+                                       (meijering, 1e-2),
+                                       (sato, 1e-3),
+                                       (hessian, 2e-2)])
+def test_border_management(func, tol):
     img = rgb2gray(retina()[300:500, 700:900])
-    out = frangi(img, sigmas=[1])
+    out = func(img, sigmas=[1], mode='reflect')
 
     full_std = out.std()
     full_mean = out.mean()
@@ -207,12 +228,20 @@ def test_border_management():
     border_mean = np.stack([out[:4, :], out[-4:, :],
                             out[:, :4].T, out[:, -4:].T]).mean()
 
-    assert abs(full_std - inside_std) < 1e-7
-    assert abs(full_std - border_std) < 1e-7
-    assert abs(inside_std - border_std) < 1e-7
-    assert abs(full_mean - inside_mean) < 1e-7
-    assert abs(full_mean - border_mean) < 1e-7
-    assert abs(inside_mean - border_mean) < 1e-7
+    assert abs(full_std - inside_std) < tol
+    assert abs(full_std - border_std) < tol
+    assert abs(inside_std - border_std) < tol
+    assert abs(full_mean - inside_mean) < tol
+    assert abs(full_mean - border_mean) < tol
+    assert abs(inside_mean - border_mean) < tol
+
+
+@pytest.mark.parametrize('func', [sato, hessian])
+def test_border_warning(func):
+    img = rgb2gray(retina()[300:500, 700:900])
+
+    with expected_warnings(["implicitly used 'constant' as the border mode"]):
+        func(img, sigmas=[1])
 
 
 if __name__ == "__main__":