Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port upstream fixes to skimage.transform estimate methods #207

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions python/cucim/src/cucim/skimage/transform/_geometric.py
Original file line number Diff line number Diff line change
Expand Up @@ -763,7 +763,7 @@ def estimate(self, src, dst, weights=None):
src_matrix, src, has_nan1 = _center_and_normalize_points(src)
dst_matrix, dst, has_nan2 = _center_and_normalize_points(dst)
if has_nan1 or has_nan2:
self.params = xp.full((d, d), np.nan)
self.params = xp.full((d + 1, d + 1), xp.nan)
return False
# params: a0, a1, a2, b0, b1, b2, c0, c1
A = xp.zeros((n * d, (d + 1) ** 2))
Expand Down Expand Up @@ -792,6 +792,7 @@ def estimate(self, src, dst, weights=None):
# because it is a rank-defective transform, which would map points
# to a line rather than a plane.
if xp.isclose(V[-1, -1], 0):
self.params = xp.full((d + 1, d + 1), xp.nan)
return False

H = np.zeros(
Expand Down Expand Up @@ -1050,7 +1051,7 @@ def estimate(self, src, dst):
Returns
-------
success : bool
True, if model estimation succeeds.
True, if all pieces of the model are successfully estimated.

"""

Expand All @@ -1065,11 +1066,14 @@ def estimate(self, src, dst):
dst = cp.asnumpy(dst)

self._tesselation = spatial.Delaunay(src)

ok = 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, :])
ok &= affine.estimate(src[tri, :], dst[tri, :])
self.affines.append(affine)

# inverse piecewise affine
Expand All @@ -1079,10 +1083,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, :])
ok &= affine.estimate(dst[tri, :], src[tri, :])
self.inverse_affines.append(affine)

return True
return ok

def __call__(self, coords):
"""Apply forward transformation.
Expand Down Expand Up @@ -1460,7 +1464,8 @@ def estimate(self, src, dst):

self.params = _umeyama(src, dst, estimate_scale=True)

return True
# _umeyama will return nan if the problem is not well-conditioned.
return not cp.any(cp.isnan(self.params))

@property
def scale(self):
Expand Down
48 changes: 38 additions & 10 deletions python/cucim/src/cucim/skimage/transform/tests/test_geometric.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def test_euclidean_estimation():

# via estimate method
tform3 = EuclideanTransform()
tform3.estimate(SRC, DST)
assert tform3.estimate(SRC, DST)
assert_array_almost_equal(tform3.params, tform2.params)


Expand Down Expand Up @@ -117,7 +117,7 @@ def test_similarity_estimation():

# via estimate method
tform3 = SimilarityTransform()
tform3.estimate(SRC, DST)
assert tform3.estimate(SRC, DST)
assert_array_almost_equal(tform3.params, tform2.params)


Expand Down Expand Up @@ -183,7 +183,7 @@ def test_affine_estimation():

# via estimate method
tform3 = AffineTransform()
tform3.estimate(SRC, DST)
assert tform3.estimate(SRC, DST)
assert_array_almost_equal(tform3.params, tform2.params)


Expand Down Expand Up @@ -214,7 +214,7 @@ def test_affine_init():

def test_piecewise_affine():
tform = PiecewiseAffineTransform()
tform.estimate(SRC, DST)
assert tform.estimate(SRC, DST)
# make sure each single affine transform is exactly estimated
assert_array_almost_equal(tform(SRC), DST)
assert_array_almost_equal(tform.inverse(DST), SRC)
Expand Down Expand Up @@ -357,7 +357,7 @@ def test_projective_estimation():

# via estimate method
tform3 = ProjectiveTransform()
tform3.estimate(SRC, DST)
assert tform3.estimate(SRC, DST)
assert_array_almost_equal(tform3.params, tform2.params)


Expand Down Expand Up @@ -401,7 +401,7 @@ def test_polynomial_estimation():

# via estimate method
tform2 = PolynomialTransform()
tform2.estimate(SRC, DST, order=10)
assert tform2.estimate(SRC, DST, order=10)
assert_array_almost_equal(tform2.params, tform.params)


Expand Down Expand Up @@ -557,15 +557,15 @@ def test_degenerate(xp=cp):
src = dst = xp.zeros((10, 2))

tform = SimilarityTransform()
tform.estimate(src, dst)
assert not tform.estimate(src, dst)
assert xp.all(xp.isnan(tform.params))

tform = AffineTransform()
tform.estimate(src, dst)
assert not tform.estimate(src, dst)
assert xp.all(xp.isnan(tform.params))

tform = ProjectiveTransform()
tform.estimate(src, dst)
assert not tform.estimate(src, dst)
assert xp.all(xp.isnan(tform.params))

# See gh-3926 for discussion details
Expand All @@ -581,6 +581,34 @@ def test_degenerate(xp=cp):
# a transform could be returned with nan values.
assert not tform.estimate(src, dst) or xp.isfinite(tform.params).all()

src = xp.array([[0, 2, 0], [0, 2, 0], [0, 4, 0]])
dst = xp.array([[0, 1, 0], [0, 1, 0], [0, 3, 0]])
tform = AffineTransform()
assert not tform.estimate(src, dst)
# Prior to gh-6207, the above would set the parameters as the identity.
assert xp.all(xp.isnan(tform.params))

# The tesselation on the following points produces one degenerate affine
# warp within PiecewiseAffineTransform.
src = xp.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 = xp.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)
xp = np # PiecewiseAffine implemented in CPU
assert np.all(xp.isnan(tform.affines[4].params)) # degenerate affine
for idx, affine in enumerate(tform.affines):
if idx != 4:
assert not xp.all(xp.isnan(affine.params))
for affine in tform.inverse_affines:
assert not xp.all(xp.isnan(affine.params))


@pytest.mark.parametrize('xp', [np, cp])
def test_normalize_degenerate_points(xp):
Expand Down Expand Up @@ -659,7 +687,7 @@ def test_estimate_affine_3d(xp):
dst = tf(src)
dst_noisy = dst + xp.asarray(np.random.random((25, ndim)))
tf2 = AffineTransform(dimensionality=ndim)
tf2.estimate(src, dst_noisy)
assert tf2.estimate(src, dst_noisy)
# we check rot/scale/etc more tightly than translation because translation
# estimation is on the 1 pixel scale
assert_array_almost_equal(tf2.params[:, :-1], matrix[:, :-1], decimal=2)
Expand Down