diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 70584664..a47b51e4 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -30,11 +30,6 @@ def _stackcopy(a, b): class GeometricTransform(object): """Perform geometric transformations on a set of coordinates. - Parameters - ---------- - matrix : 3x3 array, optional - Homogeneous transformation matrix. - """ def __call__(self, coords): """Apply forward transformation. @@ -99,6 +94,11 @@ class ProjectiveTransform(GeometricTransform): [0 1 20] [0 0 1 ]]. + Parameters + ---------- + matrix : 3x3 array, optional + Homogeneous transformation matrix. + """ _coefs = range(8) @@ -201,22 +201,30 @@ class AffineTransform(ProjectiveTransform): Parameters ---------- - scale : (sx, sy), floats - Scale factors. - rotation : float - Rotation angle in radians, counter-clockwise direction. - shear : float - Shear angle in radians, counter-clockwise direction. - translation : (tx, ty), floats - Translation in x and y. + matrix : 3x3 array, optional + Homogeneous transformation matrix. """ _coefs = range(6) - def __init__(self, scale=None, rotation=None, shear=None, translation=None): - ProjectiveTransform.__init__(self) + def compose_implicit(self, scale=None, rotation=None, shear=None, + translation=None): + """Set the transformation matrix with the implicit transformation + parameters. + Parameters + ---------- + scale : (sx, sy) as array, list or tuple + scale factors + rotation : float + rotation angle in counter-clockwise direction + shear : float + shear angle in counter-clockwise direction + translation : (tx, ty) as array, list or tuple + translation parameters + + """ if scale is None: scale = (1, 1) if rotation is None: @@ -226,18 +234,35 @@ class AffineTransform(ProjectiveTransform): if translation is None: translation = (0, 0) - a = rotation sx, sy = scale - tx, ty = translation - self._matrix = np.array([ - [sx * math.cos(a), - sy * math.sin(a + shear), tx], - [sx * math.sin(a), sy * math.cos(a + shear), ty], - [0, 0, 1] + [sx * math.cos(rotation), - sy * math.sin(rotation + shear), 0], + [sx * math.sin(rotation), sy * math.cos(rotation + shear), 0], + [ 0, 0, 1] ]) + self._matrix[0:2, 2] = translation + + @property + def scale(self): + sx = math.sqrt(self._matrix[0, 0] ** 2 + self._matrix[1, 0] ** 2) + sy = math.sqrt(self._matrix[0, 1] ** 2 + self._matrix[1, 1] ** 2) + return sx, sy + + @property + def rotation(self): + return math.atan2(self._matrix[1, 0], self._matrix[0, 0]) + + @property + def shear(self): + beta = math.atan2(- self._matrix[0, 1], self._matrix[1, 1]) + return beta - self.rotation + + @property + def translation(self): + return self._matrix[0:2, 2] -class SimilarityTransform(AffineTransform): +class SimilarityTransform(ProjectiveTransform): """2D similarity transformation of the form:: X = a0*x + b0*y + a1 = @@ -254,23 +279,11 @@ class SimilarityTransform(AffineTransform): Parameters ---------- - scale : float, optional - Scale / zoom factor. - rotation : float, optional - Rotation angle, counter-clockwise, in radians. - translation : (tx, ty) of float - x, y translation parameters + matrix : 3x3 array, optional + Homogeneous transformation matrix. """ - def __init__(self, scale=None, rotation=None, translation=None): - if scale is not None: - scale = (scale, scale) - AffineTransform.__init__(self, scale=scale, - rotation=rotation, - shear=0, - translation=translation) - def estimate(self, src, dst): """Set the transformation matrix with the explicit transformation parameters. @@ -305,6 +318,52 @@ class SimilarityTransform(AffineTransform): [b0, a0, b1], [ 0, 0, 1]]) + def compose_implicit(self, scale=None, rotation=None, translation=None): + """Set the transformation matrix with the implicit transformation + parameters. + + Parameters + ---------- + scale : float, optional + scale factor + rotation : float, optional + rotation angle in counter-clockwise direction + translation : (tx, ty) as array, list or tuple, optional + x, y translation parameters + + """ + if scale is None: + scale = (1, 1) + if rotation is None: + rotation = 0 + if translation is None: + translation = (0, 0) + + self._matrix = np.array([ + [math.cos(rotation), - math.sin(rotation), 0], + [math.sin(rotation), math.cos(rotation), 0], + [ 0, 0, 1] + ]) + self._matrix *= scale + self._matrix[0:2, 2] = translation + + @property + def scale(self): + if math.cos(self.rotation) == 0: + # sin(self.rotation) == 1 + scale = self._matrix[0, 1] + else: + scale = self._matrix[0, 0] / math.cos(self.rotation) + return scale + + @property + def rotation(self): + return math.atan2(self._matrix[1, 0], self._matrix[1, 1]) + + @property + def translation(self): + return self._matrix[0:2, 2] + class PolynomialTransform(GeometricTransform): """2D transformation of the form:: @@ -449,7 +508,7 @@ def estimate_transform(ttype, src, dst, **kwargs): >>> tform = tf.estimate_transform('similarity', src, dst) - >>> tform.inverse(tform.forward(src)) # == src + >>> tform.inverse(tform(src)) # == src >>> # warp image using the estimated transformation >>> from skimage import data @@ -458,15 +517,12 @@ def estimate_transform(ttype, src, dst, **kwargs): >>> warp(image, inverse_map=tform.inverse) >>> # create transformation with explicit parameters - >>> scale = 1.1 - >>> rotation = 1 - >>> translation = (10, 20) - >>> - >>> tform2 = tf.SimilarityTransform(scale, rotation, translation) + >>> tform2 = tf.SimilarityTransform() + >>> tform2.compose_implicit(scale=1.1, rotation=1, translation=(10, 20)) >>> # unite transformations, applied in order from left to right >>> tform3 = tform + tform2 - >>> tform3.forward(src) # == tform2.forward(tform.forward(src)) + >>> tform3(src) # == tform2(tform(src)) """ ttype = ttype.lower() diff --git a/skimage/transform/tests/test_geometric.py b/skimage/transform/tests/test_geometric.py index 4430673c..f99bab7a 100644 --- a/skimage/transform/tests/test_geometric.py +++ b/skimage/transform/tests/test_geometric.py @@ -1,5 +1,5 @@ import numpy as np -from numpy.testing import assert_array_almost_equal +from numpy.testing import assert_equal, assert_array_almost_equal from skimage.transform._geometric import _stackcopy from skimage.transform import (estimate_transform, SimilarityTransform, @@ -43,12 +43,26 @@ def test_similarity_estimation(): tform = estimate_transform('similarity', SRC[:2, :], DST[:2, :]) assert_array_almost_equal(tform(SRC[:2, :]), DST[:2, :]) assert_array_almost_equal(tform.inverse(tform(SRC)), SRC) + assert_equal(tform._matrix[0, 0], tform._matrix[1, 1]) + assert_equal(tform._matrix[0, 1], - tform._matrix[1, 0]) #: over-determined tform = estimate_transform('similarity', SRC, DST) assert_array_almost_equal(tform.inverse(tform(SRC)), SRC) + assert_equal(tform._matrix[0, 0], tform._matrix[1, 1]) + assert_equal(tform._matrix[0, 1], - tform._matrix[1, 0]) +def test_similarity_implicit(): + tform = SimilarityTransform() + scale = 0.1 + rotation = 1 + translation = (1, 1) + tform.compose_implicit(scale, rotation, translation) + assert_array_almost_equal(tform.scale, scale) + assert_array_almost_equal(tform.rotation, rotation) + assert_array_almost_equal(tform.translation, translation) + def test_affine_estimation(): #: exact solution @@ -61,6 +75,19 @@ def test_affine_estimation(): assert_array_almost_equal(tform.inverse(tform(SRC)), SRC) +def test_affine_implicit(): + tform = AffineTransform() + scale = (0.1, 0.13) + rotation = 1 + shear = 0.1 + translation = (1, 1) + tform.compose_implicit(scale, rotation, shear, translation) + assert_array_almost_equal(tform.scale, scale) + assert_array_almost_equal(tform.rotation, rotation) + assert_array_almost_equal(tform.shear, shear) + assert_array_almost_equal(tform.translation, translation) + + def test_projective(): #: exact solution tform = estimate_transform('projective', SRC[:4, :], DST[:4, :]) @@ -77,9 +104,13 @@ def test_polynomial(): def test_union(): - tform1 = SimilarityTransform(1, 0.3) - tform2 = SimilarityTransform(1, 0.6) - tform3 = SimilarityTransform(1, 0.9) + tform1 = SimilarityTransform() + tform1.compose_implicit(scale=0.1, rotation=0.3) + tform2 = SimilarityTransform() + tform2.compose_implicit(scale=0.1, rotation=0.9) + tform3 = SimilarityTransform() + tform3.compose_implicit(scale=0.1**2, rotation=0.3+0.9) + tform = tform1 + tform2