Repository: scikit-image/scikit-image

Issue:
Sign error in `measure.regionprops` for orientations of 45 degrees
### Description:

There seems to be a sign error for the orientation property of `measure.regionprops` for orientations of +/- 45 degrees. For the three objects shown in the image below, the orientation is given as  -41, +45, and -49 degrees respectively.

![image](https://user-images.githubusercontent.com/125406467/224400042-b5767e62-e524-49e3-a8c0-614781d532bd.png)


### Way to reproduce:

```Python
import numpy as np
from skimage.measure import regionprops_table
import pandas as pd

#nearly diagonal array
arr1 = np.array([[0, 0, 1, 1],[0, 0, 1, 0],[0, 1, 0, 0],[1, 0, 0, 0]])
#diagonal array 
arr2 = np.array([[0, 0, 0, 2],[0, 0, 2, 0],[0, 2, 0, 0],[2, 0, 0, 0]])
#nearly diagonal array 
arr3 = np.array([[0, 0, 0, 3],[0, 0, 3, 3],[0, 3, 0, 0],[3, 0, 0, 0]])

arr = np.hstack((arr1,arr2,arr3))
out = pd.DataFrame(regionprops_table(arr, properties=["label","orientation"]))
print(out)
```


### Traceback or output:

```Shell
label  orientation
0      1    -0.714450
1      2     0.785398
2      3    -0.856347
```


### Version information:

```Shell
3.8.3 (default, Jul  2 2020, 16:21:59) 
[GCC 7.3.0]
Linux-5.8.0-7642-generic-x86_64-with-glibc2.10
scikit-image version: 0.20.0
numpy version: 1.23.3
```



Discussion:
Wow, that's insane @lcordonnier, thank you for reporting!
Hi,
I Im fairly sure I fixed this issue. 
For the input:
```python
import numpy as np
from skimage.measure import regionprops_table
import pandas as pd

#nearly diagonal array
arr1 = np.array([[1, 0, 0, 0],[1, 1, 0, 0],[0, 0, 1, 0],[0, 0, 0, 1]])
#diagonal array 
arr2 = np.array([[0, 0, 0, 2],[0, 0, 2, 0],[0, 2, 0, 0],[2, 0, 0, 0]])
#nearly diagonal array 
arr3 = np.array([[0, 0, 0, 3],[0, 0, 3, 3],[0, 3, 0, 0],[3, 0, 0, 0]])

arr = np.hstack((arr1,arr2,arr3))
out = pd.DataFrame(regionprops_table(arr, properties=["label","orientation"]))
print(out)
```
I got the output:
```
   label  orientation
0      1    -0.714450
1      2     0.785398
2      3     0.714450
```
Check out my merge request at this link: https://github.com/scikit-image/scikit-image/pull/6834. 

Hi @jakeMartin1234, thanks for looking into this! The output you provided still looks a bit dodgy to me - I'd expect it to be -0.714450, -0.785398,  -0.856347, respectively. I did a bit of digging and it looks like the issue could be fixed by just flipping the inequality that handles the 45 degree cases, that way the atan doesn't need to be changed:
https://github.com/scikit-image/scikit-image/blob/907bfb71d2c8e4d58c2dffc6056760c1e00d070b/skimage/measure/_regionprops.py#L606-L609

Ah, I changed the first input which is why one of them was negative. I was presuming that orientation was measuring the angle of the coloured pixels with the value of 0 set to if the line was horizontal. Should I revert my change and switch lines 607 and 609?
The rotation is from axis 0 (ie the rows of the image) to axis 1 (the columns), meaning a vertical line is zero and a positive angles are in the counter-clockwise direction. 

Resolving patch:
diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py
--- a/skimage/measure/_regionprops.py
+++ b/skimage/measure/_regionprops.py
@@ -604,9 +604,9 @@ def orientation(self):
         a, b, b, c = self.inertia_tensor.flat
         if a - c == 0:
             if b < 0:
-                return -PI / 4.
-            else:
                 return PI / 4.
+            else:
+                return -PI / 4.
         else:
             return 0.5 * atan2(-2 * b, c - a)
 


Test patch:
diff --git a/skimage/measure/tests/test_regionprops.py b/skimage/measure/tests/test_regionprops.py
--- a/skimage/measure/tests/test_regionprops.py
+++ b/skimage/measure/tests/test_regionprops.py
@@ -645,22 +645,37 @@ def test_orientation():
     # test diagonal regions
     diag = np.eye(10, dtype=int)
     orient_diag = regionprops(diag)[0].orientation
-    assert_almost_equal(orient_diag, -math.pi / 4)
+    assert_almost_equal(orient_diag, math.pi / 4)
     orient_diag = regionprops(diag, spacing=(1, 2))[0].orientation
     assert_almost_equal(orient_diag, np.arccos(0.5 / np.sqrt(1 + 0.5 ** 2)))
     orient_diag = regionprops(np.flipud(diag))[0].orientation
-    assert_almost_equal(orient_diag, math.pi / 4)
+    assert_almost_equal(orient_diag, -math.pi / 4)
     orient_diag = regionprops(np.flipud(diag), spacing=(1, 2))[0].orientation
     assert_almost_equal(orient_diag, -np.arccos(0.5 / np.sqrt(1 + 0.5 ** 2)))
     orient_diag = regionprops(np.fliplr(diag))[0].orientation
-    assert_almost_equal(orient_diag, math.pi / 4)
+    assert_almost_equal(orient_diag, -math.pi / 4)
     orient_diag = regionprops(np.fliplr(diag), spacing=(1, 2))[0].orientation
     assert_almost_equal(orient_diag, -np.arccos(0.5 / np.sqrt(1 + 0.5 ** 2)))
     orient_diag = regionprops(np.fliplr(np.flipud(diag)))[0].orientation
-    assert_almost_equal(orient_diag, -math.pi / 4)
+    assert_almost_equal(orient_diag, math.pi / 4)
     orient_diag = regionprops(np.fliplr(np.flipud(diag)), spacing=(1, 2))[0].orientation
     assert_almost_equal(orient_diag, np.arccos(0.5 / np.sqrt(1 + 0.5 ** 2)))
 
+def test_orientation_continuity():
+    # nearly diagonal array
+    arr1 = np.array([[0, 0, 1, 1],[0, 0, 1, 0],[0, 1, 0, 0],[1, 0, 0, 0]])
+    # diagonal array
+    arr2 = np.array([[0, 0, 0, 2],[0, 0, 2, 0],[0, 2, 0, 0],[2, 0, 0, 0]])
+    # nearly diagonal array
+    arr3 = np.array([[0, 0, 0, 3],[0, 0, 3, 3],[0, 3, 0, 0],[3, 0, 0, 0]])
+    image = np.hstack((arr1,arr2,arr3))
+    props = regionprops(image)
+    orientations = [prop.orientation for prop in props]
+    np.testing.assert_allclose(orientations, orientations[1], rtol=0, atol=0.08)
+    assert_almost_equal(orientations[0], -0.7144496360953664)
+    assert_almost_equal(orientations[1], -0.7853981633974483)
+    assert_almost_equal(orientations[2], -0.8563466906995303)
+
 
 def test_perimeter():
     per = regionprops(SAMPLE)[0].perimeter