Repository: scikit-image/scikit-image

Issue:
Wrong order in hessian_matrix?
## Description

In `hessian_matrix`, when `order='rc'`, the elements are supposed to be returned as `(Hrr, Hrc, Hcc)`. But if I'm understanding the code correctly, it instead returns `(Hcc, Hrc, Hrr)`:
```python
    gradients = np.gradient(gaussian_filtered)
    axes = range(image.ndim)

    if order == 'rc':  # I think this line is wrong -- should be if order == 'xy'
        axes = reversed(axes)

    H_elems = [np.gradient(gradients[ax0], axis=ax1)
               for ax0, ax1 in combinations_with_replacement(axes, 2)]
```

Note that when `order == 'rc'`, then
```python
axes = reversed(range(2))
combinations = list(combinations_with_replacement(axes, 2))
combinations
```
which yeilds `[(1, 1), (1, 0), (0, 0)]`, putting columns first instead of rows.


Am I correct that this isn't the intended behavior?


Discussion:

Resolving patch:
diff --git a/skimage/feature/corner.py b/skimage/feature/corner.py
--- a/skimage/feature/corner.py
+++ b/skimage/feature/corner.py
@@ -175,6 +175,10 @@ def _hessian_matrix_with_gaussian(image, sigma=1, mode='reflect', cval=0,
     image = img_as_float(image)
     float_dtype = _supported_float_type(image.dtype)
     image = image.astype(float_dtype, copy=False)
+    if image.ndim > 2 and order == "xy":
+        raise ValueError("order='xy' is only supported for 2D images.")
+    if order not in ["rc", "xy"]:
+        raise ValueError(f"unrecognized order: {order}")
 
     if np.isscalar(sigma):
         sigma = (sigma,) * image.ndim
@@ -211,7 +215,7 @@ def _hessian_matrix_with_gaussian(image, sigma=1, mode='reflect', cval=0,
 
     # 2.) apply the derivative along another axis as well
     axes = range(ndim)
-    if order == 'rc':
+    if order == 'xy':
         axes = reversed(axes)
     H_elems = [gaussian_(gradients[ax0], order=orders[ax1])
                for ax0, ax1 in combinations_with_replacement(axes, 2)]
@@ -245,10 +249,11 @@ def hessian_matrix(image, sigma=1, mode='constant', cval=0, order='rc',
         Used in conjunction with mode 'constant', the value outside
         the image boundaries.
     order : {'rc', 'xy'}, optional
-        This parameter allows for the use of reverse or forward order of
-        the image axes in gradient computation. 'rc' indicates the use of
-        the first axis initially (Hrr, Hrc, Hcc), whilst 'xy' indicates the
-        usage of the last axis initially (Hxx, Hxy, Hyy)
+        For 2D images, this parameter allows for the use of reverse or forward
+        order of the image axes in gradient computation. 'rc' indicates the use
+        of the first axis initially (Hrr, Hrc, Hcc), whilst 'xy' indicates the
+        usage of the last axis initially (Hxx, Hxy, Hyy). Images with higher
+        dimension must always use 'rc' order.
     use_gaussian_derivatives : boolean, optional
         Indicates whether the Hessian is computed by convolving with Gaussian
         derivatives, or by a simple finite-difference operation.
@@ -297,6 +302,10 @@ def hessian_matrix(image, sigma=1, mode='constant', cval=0, order='rc',
     image = img_as_float(image)
     float_dtype = _supported_float_type(image.dtype)
     image = image.astype(float_dtype, copy=False)
+    if image.ndim > 2 and order == "xy":
+        raise ValueError("order='xy' is only supported for 2D images.")
+    if order not in ["rc", "xy"]:
+        raise ValueError(f"unrecognized order: {order}")
 
     if use_gaussian_derivatives is None:
         use_gaussian_derivatives = False
@@ -314,7 +323,7 @@ def hessian_matrix(image, sigma=1, mode='constant', cval=0, order='rc',
     gradients = np.gradient(gaussian_filtered)
     axes = range(image.ndim)
 
-    if order == 'rc':
+    if order == 'xy':
         axes = reversed(axes)
 
     H_elems = [np.gradient(gradients[ax0], axis=ax1)


Test patch:
diff --git a/skimage/feature/tests/test_corner.py b/skimage/feature/tests/test_corner.py
--- a/skimage/feature/tests/test_corner.py
+++ b/skimage/feature/tests/test_corner.py
@@ -114,13 +114,14 @@ def test_hessian_matrix(dtype):
     square[2, 2] = 4
     Hrr, Hrc, Hcc = hessian_matrix(square, sigma=0.1, order='rc',
                                    use_gaussian_derivatives=False)
+
     out_dtype = _supported_float_type(dtype)
     assert all(a.dtype == out_dtype for a in (Hrr, Hrc, Hcc))
-    assert_almost_equal(Hrr, np.array([[0, 0,  0, 0, 0],
+    assert_almost_equal(Hrr, np.array([[0, 0,  2, 0, 0],
                                        [0, 0,  0, 0, 0],
-                                       [2, 0, -2, 0, 2],
+                                       [0, 0, -2, 0, 0],
                                        [0, 0,  0, 0, 0],
-                                       [0, 0,  0, 0, 0]]))
+                                       [0, 0,  2, 0, 0]]))
 
     assert_almost_equal(Hrc, np.array([[0,  0, 0,  0, 0],
                                        [0,  1, 0, -1, 0],
@@ -128,11 +129,11 @@ def test_hessian_matrix(dtype):
                                        [0, -1, 0,  1, 0],
                                        [0,  0, 0,  0, 0]]))
 
-    assert_almost_equal(Hcc, np.array([[0, 0,  2, 0, 0],
+    assert_almost_equal(Hcc, np.array([[0, 0,  0, 0, 0],
                                        [0, 0,  0, 0, 0],
-                                       [0, 0, -2, 0, 0],
+                                       [2, 0, -2, 0, 2],
                                        [0, 0,  0, 0, 0],
-                                       [0, 0,  2, 0, 0]]))
+                                       [0, 0,  0, 0, 0]]))
 
     with expected_warnings(["use_gaussian_derivatives currently defaults"]):
         # FutureWarning warning when use_gaussian_derivatives is not
@@ -140,17 +141,58 @@ def test_hessian_matrix(dtype):
         hessian_matrix(square, sigma=0.1, order='rc')
 
 
+@pytest.mark.parametrize('use_gaussian_derivatives', [False, True])
+def test_hessian_matrix_order(use_gaussian_derivatives):
+    square = np.zeros((5, 5), dtype=float)
+    square[2, 2] = 4
+
+    Hxx, Hxy, Hyy = hessian_matrix(
+        square, sigma=0.1, order="xy",
+        use_gaussian_derivatives=use_gaussian_derivatives)
+
+    Hrr, Hrc, Hcc = hessian_matrix(
+        square, sigma=0.1, order="rc",
+        use_gaussian_derivatives=use_gaussian_derivatives)
+
+    # verify results are equivalent, just reversed in order
+    assert_array_equal(Hxx, Hcc)
+    assert_array_equal(Hxy, Hrc)
+    assert_array_equal(Hyy, Hrr)
+
+
 def test_hessian_matrix_3d():
     cube = np.zeros((5, 5, 5))
     cube[2, 2, 2] = 4
     Hs = hessian_matrix(cube, sigma=0.1, order='rc',
                         use_gaussian_derivatives=False)
     assert len(Hs) == 6, (f"incorrect number of Hessian images ({len(Hs)}) for 3D")
+    # This test didn't catch the fix in gh-6624 (passes with and without) ...
     assert_almost_equal(Hs[2][:, 2, :], np.array([[0,  0,  0,  0,  0],
                                                   [0,  1,  0, -1,  0],
                                                   [0,  0,  0,  0,  0],
                                                   [0, -1,  0,  1,  0],
                                                   [0,  0,  0,  0,  0]]))
+    # ... so we add another test that fails for the not-fixed hessian_matrix
+    assert_almost_equal(Hs[0][:, 2, :], np.array([[0,  0,  2,  0,  0],
+                                                  [0,  0,  0,  0,  0],
+                                                  [0,  0, -2,  0,  0],
+                                                  [0,  0,  0,  0,  0],
+                                                  [0,  0,  2,  0,  0]]))
+
+
+@pytest.mark.parametrize('use_gaussian_derivatives', [False, True])
+def test_hessian_matrix_3d_xy(use_gaussian_derivatives):
+
+    img = np.ones((5, 5, 5))
+
+    # order="xy" is only permitted for 2D
+    with pytest.raises(ValueError):
+        hessian_matrix(img, sigma=0.1, order="xy",
+                       use_gaussian_derivatives=use_gaussian_derivatives)
+
+    with pytest.raises(ValueError):
+        hessian_matrix(img, sigma=0.1, order='nonexistant',
+                       use_gaussian_derivatives=use_gaussian_derivatives)
 
 
 @pytest.mark.parametrize('dtype', [np.float16, np.float32, np.float64])