Repository: scikit-image/scikit-image

Issue:
Propagate AffineTransform estimate return value in PiecewiseAffineTransform
## Description

During `PiecewiseAffineTransform.estimate`, an individual forward or inverse AffineTransform could be degenerate and its estimate returns `False`.  This value is not used or checked, e.g.:

https://github.com/scikit-image/scikit-image/blob/1544b32de29440f787d3d038ca912ad85e54d616/skimage/transform/_geometric.py#L982

Now, if this happens, the transform can be partially valid in some ranges.  But I believe `False` should be returned.  Happy to make a PR, if this sounds acceptable.






Discussion:

Resolving patch:
diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py
--- a/skimage/transform/_geometric.py
+++ b/skimage/transform/_geometric.py
@@ -968,7 +968,7 @@ def estimate(self, src, dst):
         Returns
         -------
         success : bool
-            True, if model estimation succeeds.
+            True, if all pieces of the model are successfully estimated.
 
         """
 
@@ -976,11 +976,14 @@ def estimate(self, src, dst):
         # forward piecewise affine
         # triangulate input positions into mesh
         self._tesselation = spatial.Delaunay(src)
+
+        success = True
+
         # find affine mapping from source positions to destination
         self.affines = []
         for tri in self._tesselation.vertices:
             affine = AffineTransform(dimensionality=ndim)
-            affine.estimate(src[tri, :], dst[tri, :])
+            success &= affine.estimate(src[tri, :], dst[tri, :])
             self.affines.append(affine)
 
         # inverse piecewise affine
@@ -990,10 +993,10 @@ def estimate(self, src, dst):
         self.inverse_affines = []
         for tri in self._inverse_tesselation.vertices:
             affine = AffineTransform(dimensionality=ndim)
-            affine.estimate(dst[tri, :], src[tri, :])
+            success &= affine.estimate(dst[tri, :], src[tri, :])
             self.inverse_affines.append(affine)
 
-        return True
+        return success
 
     def __call__(self, coords):
         """Apply forward transformation.


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
@@ -550,6 +550,26 @@ def test_degenerate():
     # Prior to gh-6207, the above would set the parameters as the identity.
     assert np.all(np.isnan(tform.params))
 
+    # The tesselation on the following points produces one degenerate affine
+    # warp within PiecewiseAffineTransform.
+    src = np.asarray([
+        [0, 192, 256], [0, 256, 256], [5, 0, 192], [5, 64, 0], [5, 64, 64],
+        [5, 64, 256], [5, 192, 192], [5, 256, 256], [0, 192, 256],
+    ])
+
+    dst = np.asarray([
+        [0, 142, 206], [0, 206, 206], [5, -50, 142], [5, 14, 0], [5, 14, 64],
+        [5, 14, 206], [5, 142, 142], [5, 206, 206], [0, 142, 206],
+    ])
+    tform = PiecewiseAffineTransform()
+    assert not tform.estimate(src, dst)
+    assert np.all(np.isnan(tform.affines[4].params))  # degenerate affine
+    for idx, affine in enumerate(tform.affines):
+        if idx != 4:
+            assert not np.all(np.isnan(affine.params))
+    for affine in tform.inverse_affines:
+        assert not np.all(np.isnan(affine.params))
+
 
 def test_normalize_degenerate_points():
     """Return nan matrix *of appropriate size* when point is repeated."""