Repository: scikit-image/scikit-image

Issue:
Frangi artefacts around the image

[img_raw.zip](https://github.com/scikit-image/scikit-image/files/3738350/img_raw.zip)

## Description
![frangi problem](https://user-images.githubusercontent.com/32536010/66994646-e4da8980-f0cd-11e9-950f-e154f3f1baf3.jpg)

Hi,
I upgraded Scikit from 0.14.2 to 0.16.1 and now I experience some problems. As illustrated in the picture, with the new Frangi I get a a bright square all across the image which I did not get using the old version (and also not present in the raw image)
I used Frangi in 2D and I still use the beta1 and beta2 parameters (instead of beta and gamma). Switching over to the new parameters does not solve it

                              'scale_range'= [5,6]
                              'scale_step' = 1
                              'beta1'=1         
                              'beta2'=0.0005
                              'black_ridges':True
I don't pass on the alpha parameter (because I work in 2D), but I do notice that this alpha is still used in calculations (maybe ok, I don't know)

I will have to revert to 14.01 for now, but if this could be fixed that would be great

thanks !
Wim

## Way to reproduce
from tifffile import TiffFile,imsave
from skimage.filters import frangi
from skimage import img_as_uint

with TiffFile("img_raw.tif") as tif:
    img_raw= tif.asarray()
frangi_img = frangi(img_raw,scale_range=[5,6],
                    scale_step=1,
                    beta1 = 1, 
                    beta2 = 0.0005,
                    black_ridges=True)

imsave("frangi.tif", img_as_uint(frangi_img))


## Version information
```python 3.7.3 

Running the previous version scikit : 
Linux-4.4.0-18362-Microsoft-x86_64-with-debian-buster-sid
scikit-image version: 0.14.2
numpy version: 1.16.2

running  the new version scikit
Linux-4.4.0-18362-Microsoft-x86_64-with-debian-buster-sid
scikit-image version: 0.16.1
numpy version: 1.16.2
frangi_issue_recreate.py:18: UserWarning: Use keyword parameter `beta` instead of `beta1` which will be removed in version 0.17.
  black_ridges=True)
frangi_issue_recreate.py:18: UserWarning: Use keyword parameter `gamma` instead of `beta2` which will be removed in version 0.17.
  black_ridges=True)
frangi_issue_recreate.py:18: UserWarning: Use keyword parameter `sigmas` instead of `scale_range` and `scale_range` which will be removed in version 0.17.
  black_ridges=True)




Discussion:
Yikes, thank you for the report @wimthiels! We'll chase it down. I suspect it was accidentally introduced in #3515...

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
@@ -65,7 +65,7 @@ def identity(image, **kwargs):
     for j, func in enumerate([identity, meijering, sato, frangi, hessian]):
         kwargs['black_ridges'] = black_ridges
         result = func(image, **kwargs)
-        if func in (meijering, frangi):
+        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')
diff --git a/skimage/feature/corner.py b/skimage/feature/corner.py
--- a/skimage/feature/corner.py
+++ b/skimage/feature/corner.py
@@ -5,9 +5,9 @@
 from scipy import stats
 
 from ..util import img_as_float
-from ..feature import peak_local_max
-from ..feature.util import _prepare_grayscale_input_2D
-from ..feature.corner_cy import _corner_fast
+from .peak import peak_local_max
+from .util import _prepare_grayscale_input_2D
+from .corner_cy import _corner_fast
 from ._hessian_det_appx import _hessian_matrix_det
 from ..transform import integral_image
 from .._shared.utils import safe_as_int
diff --git a/skimage/filters/ridges.py b/skimage/filters/ridges.py
--- a/skimage/filters/ridges.py
+++ b/skimage/filters/ridges.py
@@ -15,6 +15,7 @@ class of ridge filters relies on the eigenvalues of the Hessian matrix of
 
 from ..util import img_as_float, invert
 from .._shared.utils import check_nD
+from ..feature.corner import hessian_matrix, hessian_matrix_eigvals
 
 
 def _divide_nonzero(array1, array2, cval=1e-10):
@@ -80,7 +81,8 @@ def _sortbyabs(array, axis=0):
     return array[tuple(index)]
 
 
-def compute_hessian_eigenvalues(image, sigma, sorting='none'):
+def compute_hessian_eigenvalues(image, sigma, sorting='none',
+                                mode='constant', cval=0):
     """
     Compute Hessian eigenvalues of nD images.
 
@@ -97,6 +99,11 @@ def compute_hessian_eigenvalues(image, sigma, sorting='none'):
     sorting : {'val', 'abs', 'none'}, optional
         Sorting of eigenvalues by values ('val') or absolute values ('abs'),
         or without sorting ('none'). Default is 'none'.
+    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
     -------
@@ -105,20 +112,18 @@ def compute_hessian_eigenvalues(image, sigma, sorting='none'):
         of the input image.
     """
 
-    # Import has to be here due to circular import error
-    from ..feature import hessian_matrix, hessian_matrix_eigvals
-
     # Convert image to float
     image = img_as_float(image)
 
     # Make nD hessian
-    hessian_elements = hessian_matrix(image, sigma=sigma, order='rc')
+    hessian_elements = hessian_matrix(image, sigma=sigma, order='rc',
+                                      mode=mode, cval=cval)
 
     # Correct for scale
     hessian_elements = [(sigma ** 2) * e for e in hessian_elements]
 
     # Compute Hessian eigenvalues
-    hessian_eigenvalues = np.array(hessian_matrix_eigvals(hessian_elements))
+    hessian_eigenvalues = hessian_matrix_eigvals(hessian_elements)
 
     if sorting == 'abs':
 
@@ -307,9 +312,10 @@ def sato(image, sigmas=range(1, 10, 2), black_ridges=True):
     return np.max(filtered_array, axis=0)
 
 
-def frangi(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):
+def frangi(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, mode='reflect',
+           cval=0):
     """
     Filter an image with the Frangi vesselness filter.
 
@@ -344,6 +350,11 @@ def frangi(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
     -------
@@ -374,17 +385,17 @@ def frangi(image, sigmas=range(1, 10, 2), scale_range=None, scale_step=None,
     """
 
     # Check deprecated keyword parameters
-    if beta1:
+    if beta1 is not None:
         warn('Use keyword parameter `beta` instead of `beta1` which '
              'will be removed in version 0.17.', stacklevel=2)
         beta = beta1
 
-    if beta2:
+    if beta2 is not None:
         warn('Use keyword parameter `gamma` instead of `beta2` which '
              'will be removed in version 0.17.', stacklevel=2)
         gamma = beta2
 
-    if scale_range and scale_step:
+    if scale_range is not None and scale_step is not None:
         warn('Use keyword parameter `sigmas` instead of `scale_range` and '
              '`scale_range` which will be removed in version 0.17.',
              stacklevel=2)
@@ -394,7 +405,7 @@ def frangi(image, sigmas=range(1, 10, 2), scale_range=None, scale_step=None,
     check_nD(image, [2, 3])
 
     # Check (sigma) scales
-    sigmas = np.asarray(sigmas)
+    sigmas = np.asarray(sigmas).ravel()
     if np.any(sigmas < 0.0):
         raise ValueError('Sigma values less than zero are not valid')
 
@@ -413,17 +424,18 @@ def frangi(image, sigmas=range(1, 10, 2), scale_range=None, scale_step=None,
     # Generate empty (n+1)D arrays for storing auxiliary images filtered
     # at different (sigma) scales
     filtered_array = np.zeros(sigmas.shape + image.shape)
-    lambdas_array = np.zeros(sigmas.shape + image.shape)
+    lambdas_array = np.zeros_like(filtered_array)
 
     # Filtering for all (sigma) scales
     for i, sigma in enumerate(sigmas):
 
         # Calculate (abs sorted) eigenvalues
         lambda1, *lambdas = compute_hessian_eigenvalues(image, sigma,
-                                                        sorting='abs')
+                                                        sorting='abs',
+                                                        mode=mode, cval=cval)
 
-        # Compute sensitivity to deviation from a plate-like structure
-        # see equations (11) and (15) in reference [1]_
+        # Compute sensitivity to deviation from a plate-like
+        # structure see equations (11) and (15) in reference [1]_
         r_a = np.inf if ndim == 2 else _divide_nonzero(*lambdas) ** 2
 
         # Compute sensitivity to deviation from a blob-like structure,
@@ -441,6 +453,7 @@ def frangi(image, sigmas=range(1, 10, 2), scale_range=None, scale_step=None,
         filtered_array[i] = ((1 - np.exp(-r_a / alpha_sq))
                              * np.exp(-r_b / beta_sq)
                              * (1 - np.exp(-r_g / gamma_sq)))
+
         lambdas_array[i] = np.max(lambdas, axis=0)
 
     # Remove background


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
@@ -2,8 +2,9 @@
 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
+from skimage.data import camera, retina
 from skimage.util import crop, invert
+from skimage.color import rgb2gray
 
 
 def test_2d_null_matrix():
@@ -193,6 +194,27 @@ def test_3d_cropped_camera_image():
     assert_allclose(hessian(a_white, black_ridges=False), ones, atol=1 - 1e-7)
 
 
+def test_border_management():
+    img = rgb2gray(retina()[300:500, 700:900])
+    out = frangi(img, sigmas=[1])
+
+    full_std = out.std()
+    full_mean = out.mean()
+    inside_std = out[4:-4, 4:-4].std()
+    inside_mean = out[4:-4, 4:-4].mean()
+    border_std = np.stack([out[:4, :], out[-4:, :],
+                           out[:, :4].T, out[:, -4:].T]).std()
+    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
+
+
 if __name__ == "__main__":
     from numpy import testing
     testing.run_module_suite()