Repository: scikit-image/scikit-image

Issue:
filters.farid should be generalised to n-dimensions
## Description

In #4347 we generalised Sobel, Prewitt, and Scharr filters to n dimensions. I think the Farid & Simoncelli filter, which has given me very good results in 2D, can also be generalised to nD, so we should do that.



Discussion:

Resolving patch:
diff --git a/skimage/filters/edges.py b/skimage/filters/edges.py
--- a/skimage/filters/edges.py
+++ b/skimage/filters/edges.py
@@ -43,11 +43,17 @@
 # These filter weights can be found in Farid & Simoncelli (2004),
 # Table 1 (3rd and 4th row). Additional decimal places were computed
 # using the code found at https://www.cs.dartmouth.edu/farid/
-p = np.array([[0.0376593171958126, 0.249153396177344, 0.426374573253687,
-               0.249153396177344, 0.0376593171958126]])
-d1 = np.array([[0.109603762960254, 0.276690988455557, 0, -0.276690988455557,
-                -0.109603762960254]])
-HFARID_WEIGHTS = d1.T * p
+farid_smooth = np.array([[0.0376593171958126,
+                          0.249153396177344,
+                          0.426374573253687,
+                          0.249153396177344,
+                          0.0376593171958126]])
+farid_edge = np.array([[0.109603762960254,
+                        0.276690988455557,
+                        0,
+                        -0.276690988455557,
+                        -0.109603762960254]])
+HFARID_WEIGHTS = farid_edge.T * farid_smooth
 VFARID_WEIGHTS = np.copy(HFARID_WEIGHTS.T)
 
 
@@ -706,27 +712,41 @@ def laplace(image, ksize=3, mask=None):
     return _mask_filter_result(result, mask)
 
 
-def farid(image, *, mask=None):
+def farid(image, mask=None, *, axis=None, mode='reflect', cval=0.0):
     """Find the edge magnitude using the Farid transform.
 
     Parameters
     ----------
-    image : 2-D array
-        Image to process.
-    mask : 2-D array, optional
-        An optional mask to limit the application to a certain area.
-        Note that pixels surrounding masked regions are also masked to
-        prevent masked regions from affecting the result.
+    image : array
+        The input image.
+    mask : array of bool, optional
+        Clip the output image to this mask. (Values where mask=0 will be set
+        to 0.)
+    axis : int or sequence of int, optional
+        Compute the edge filter along this axis. If not provided, the edge
+        magnitude is computed. This is defined as::
+
+            farid_mag = np.sqrt(sum([farid(image, axis=i)**2
+                                     for i in range(image.ndim)]) / image.ndim)
+
+        The magnitude is also computed if axis is a sequence.
+    mode : str or sequence of str, optional
+        The boundary mode for the convolution. See `scipy.ndimage.convolve`
+        for a description of the modes. This can be either a single boundary
+        mode or one boundary mode per axis.
+    cval : float, optional
+        When `mode` is ``'constant'``, this is the constant used in values
+        outside the boundary of the image data.
 
     Returns
     -------
-    output : 2-D array
+    output : array of float
         The Farid edge map.
 
     See also
     --------
     farid_h, farid_v : horizontal and vertical edge detection.
-    sobel, prewitt, farid, skimage.feature.canny
+    scharr, sobel, prewitt, skimage.feature.canny
 
     Notes
     -----
@@ -750,11 +770,11 @@ def farid(image, *, mask=None):
     >>> from skimage import filters
     >>> edges = filters.farid(camera)
     """
-    check_nD(image, 2)
-    out = np.sqrt(farid_h(image, mask=mask) ** 2
-                  + farid_v(image, mask=mask) ** 2)
-    out /= np.sqrt(2)
-    return out
+    output = _generic_edge_filter(image, smooth_weights=farid_smooth,
+                                  edge_weights=farid_edge, axis=axis,
+                                  mode=mode, cval=cval)
+    output = _mask_filter_result(output, mask)
+    return output
 
 
 def farid_h(image, *, mask=None):


Test patch:
diff --git a/skimage/filters/tests/test_edges.py b/skimage/filters/tests/test_edges.py
--- a/skimage/filters/tests/test_edges.py
+++ b/skimage/filters/tests/test_edges.py
@@ -531,8 +531,8 @@ def test_vertical_mask_line(grad_func):
      [0, 0, 0]],
     [[1, 1, 1],
      [1, 1, 1],
-     [1, 1, 1]],
-]).astype(float)
+     [1, 1, 1]]],
+    dtype=float)
 
 # maximum Sobel 3D edge in magnitude
 MAX_SOBEL_ND = np.array([
@@ -546,8 +546,8 @@ def test_vertical_mask_line(grad_func):
 
     [[1, 1, 0],
      [1, 1, 0],
-     [1, 1, 0]]
-]).astype(float)
+     [1, 1, 0]]],
+    dtype=float)
 
 # maximum Scharr 3D edge in magnitude. This illustrates the better rotation
 # invariance of the Scharr filter!
@@ -560,29 +560,83 @@ def test_vertical_mask_line(grad_func):
      [0, 1, 1]],
     [[0, 0, 1],
      [0, 1, 1],
-     [1, 1, 1]]
-]).astype(float)
+     [1, 1, 1]]],
+    dtype=float)
+
+# maximum Farid 3D edge on axis 0
+MAX_FARID_0 = np.zeros((5, 5, 5), dtype=float)
+MAX_FARID_0[2:, :, :] = 1
+
+# maximum Farid 3D edge in magnitude (not necessarily the true maximum,
+# but this was the empirical max over the blobs image as in the test below).
+MAX_FARID_ND = np.array([
+    [[1, 0, 0, 0, 0],
+     [1, 1, 1, 1, 0],
+     [1, 1, 1, 1, 1],
+     [1, 1, 1, 1, 1],
+     [1, 1, 1, 1, 1]],
+    [[0, 0, 0, 0, 0],
+     [1, 1, 0, 0, 0],
+     [1, 1, 1, 1, 0],
+     [1, 1, 1, 1, 1],
+     [1, 1, 1, 1, 1]],
+    [[0, 0, 0, 0, 0],
+     [1, 0, 0, 0, 0],
+     [1, 1, 1, 0, 0],
+     [1, 1, 1, 1, 0],
+     [1, 1, 1, 1, 1]],
+    [[0, 0, 0, 0, 0],
+     [0, 0, 0, 0, 0],
+     [1, 0, 0, 0, 0],
+     [1, 1, 1, 0, 0],
+     [1, 1, 1, 1, 1]],
+    [[0, 0, 0, 0, 0],
+     [0, 0, 0, 0, 0],
+     [0, 0, 0, 0, 0],
+     [0, 0, 0, 0, 0],
+     [1, 1, 1, 1, 1]]],
+    dtype=float)
 
 
 @pytest.mark.parametrize(
     ('func', 'max_edge'),
     [(filters.prewitt, MAX_SOBEL_ND),
      (filters.sobel, MAX_SOBEL_ND),
-     (filters.scharr, MAX_SCHARR_ND)]
+     (filters.scharr, MAX_SCHARR_ND),
+     (filters.farid, MAX_FARID_ND)]
 )
 def test_3d_edge_filters(func, max_edge):
-    blobs = data.binary_blobs(length=128, n_dim=3)
+    blobs = data.binary_blobs(length=128, n_dim=3, seed=5)
     edges = func(blobs)
-    assert_allclose(np.max(edges), func(max_edge)[1, 1, 1])
+    center = max_edge.shape[0] // 2
+    if center == 2:
+        # exact edge as defined in MAX_FARID_0 not present in blobs data
+        rtol = 1e-3
+    else:
+        rtol = 1e-7
+    assert_allclose(
+        np.max(edges), func(max_edge)[center, center, center], rtol=rtol)
 
 
 @pytest.mark.parametrize(
-    'func', (filters.prewitt, filters.sobel, filters.scharr)
+    ('func', 'max_edge'),
+    [(filters.prewitt, MAX_SOBEL_0),
+     (filters.sobel, MAX_SOBEL_0),
+     (filters.scharr, MAX_SOBEL_0),
+     (filters.farid, MAX_FARID_0)]
 )
-def test_3d_edge_filters_single_axis(func):
-    blobs = data.binary_blobs(length=128, n_dim=3)
+def test_3d_edge_filters_single_axis(func, max_edge):
+    blobs = data.binary_blobs(length=128, n_dim=3, seed=5)
     edges0 = func(blobs, axis=0)
-    assert_allclose(np.max(edges0), func(MAX_SOBEL_0, axis=0)[1, 1, 1])
+    center = max_edge.shape[0] // 2
+    if center == 2:
+        # exact edge as defined in MAX_FARID_0 not present in blobs data
+        rtol = 1e-3
+    else:
+        rtol = 1e-7
+    assert_allclose(np.max(edges0),
+                    func(max_edge, axis=0)[center, center, center],
+                    rtol=rtol)
 
 
 @pytest.mark.parametrize(