Repository: scikit-image/scikit-image

Issue:
SimilarityTransform.scale implementation sometimes returns incorrect scale factor
## Description

The [current implementation](https://github.com/scikit-image/scikit-image/blob/e8dd5e0204e912931437d8da0f4f0ea353fe1950/skimage/transform/_geometric.py#L1169) used to obtain the scale factor relies on dividing out the cos(rot) or sin(rot) from the (0,0) or (1,0) element of the affine matrix, respectively, depending on if cos(rot) is close to 0. The initial check has the potential to fail, resulting in the wrong scale factor being returned.

A simpler, more stable implementation would be to use the determinant of the matrix to obtain the scale factor:

```python
# det = scale**(# of dimensions), therefore scale = det**(1/2)
np.sqrt(np.linalg.det(self.params))
```

## Way to reproduce

The following code defines an affine transformation with a rotation of 90 degrees and a scale factor of 1 (you can tell because element 1,0 is 1). 

```python
>>> import numpy as np; import skimage.transform
>>> m = np.array([[0,-1,1.33226763e-15],[1,2.22044605e-16,-1.33226763e-15],[0,0,1.00000000e+00]])
>>> skimage.transform.SimilarityTransform(m).scale
0.0
```

## Version information
```python
>>> import platform; print(platform.platform())
Linux-3.10.0-514.21.1.el7.x86_64-x86_64-with-redhat-7.3-Maipo
>>> import skimage; print("scikit-image version: {}".format(skimage.__version__))
scikit-image version: 0.16.2
>>> import numpy; print("numpy version: {}".format(numpy.__version__))
numpy version: 1.17.3
```



Discussion:
@84danie Thanks for reporting! Your suggestion looks great. Would you like to open a PR with a new test, ideally failing with the current implementation and passing with yours?
Sure! I should have the PR open sometime later today

Resolving patch:
diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py
--- a/skimage/transform/_geometric.py
+++ b/skimage/transform/_geometric.py
@@ -1167,12 +1167,8 @@ def estimate(self, src, dst):
 
     @property
     def scale(self):
-        if abs(math.cos(self.rotation)) < np.spacing(1):
-            # sin(self.rotation) == 1
-            scale = self.params[1, 0]
-        else:
-            scale = self.params[0, 0] / math.cos(self.rotation)
-        return scale
+        # det = scale**(# of dimensions), therefore scale = det**(1/2)
+        return np.sqrt(np.linalg.det(self.params))
 
 
 class PolynomialTransform(GeometricTransform):


Test patch:
diff --git a/skimage/transform/tests/test_geometric.py b/skimage/transform/tests/test_geometric.py
--- a/skimage/transform/tests/test_geometric.py
+++ b/skimage/transform/tests/test_geometric.py
@@ -150,6 +150,19 @@ def test_similarity_init():
     assert_almost_equal(tform.rotation, rotation)
     assert_almost_equal(tform.translation, translation)
 
+    # test special case for scale where the rotation isn't exactly 90deg,
+    # but very close
+    scale = 1.0
+    rotation = np.pi / 2
+    translation = (0, 0)
+    params = np.array([[0, -1, 1.33226763e-15],
+                       [1, 2.22044605e-16, -1.33226763e-15],
+                       [0, 0, 1]])
+    tform = SimilarityTransform(params)
+    assert_almost_equal(tform.scale, scale)
+    assert_almost_equal(tform.rotation, rotation)
+    assert_almost_equal(tform.translation, translation)
+
 
 def test_affine_estimation():
     # exact solution