:py:mod:`kwimage.transform` =========================== .. py:module:: kwimage.transform .. autoapi-nested-parse:: Objects for representing and manipulating image transforms. Module Contents --------------- Classes ~~~~~~~ .. autoapisummary:: kwimage.transform.Transform kwimage.transform.Matrix kwimage.transform.Linear kwimage.transform.Projective kwimage.transform.Affine Functions ~~~~~~~~~ .. autoapisummary:: kwimage.transform._ensure_iterablen .. py:class:: Transform Bases: :py:obj:`ubelt.NiceRepr` Inherit from this class and define ``__nice__`` to "nicely" print your objects. Defines ``__str__`` and ``__repr__`` in terms of ``__nice__`` function Classes that inherit from :class:`NiceRepr` should redefine ``__nice__``. If the inheriting class has a ``__len__``, method then the default ``__nice__`` method will return its length. .. rubric:: Example >>> import ubelt as ub >>> class Foo(ub.NiceRepr): ... def __nice__(self): ... return 'info' >>> foo = Foo() >>> assert str(foo) == '' >>> assert repr(foo).startswith('>> import ubelt as ub >>> class Bar(ub.NiceRepr): ... pass >>> bar = Bar() >>> import pytest >>> with pytest.warns(RuntimeWarning) as record: >>> assert 'object at' in str(bar) >>> assert 'object at' in repr(bar) .. rubric:: Example >>> import ubelt as ub >>> class Baz(ub.NiceRepr): ... def __len__(self): ... return 5 >>> baz = Baz() >>> assert str(baz) == '' .. rubric:: Example >>> import ubelt as ub >>> # If your nice message has a bug, it shouldn't bring down the house >>> class Foo(ub.NiceRepr): ... def __nice__(self): ... assert False >>> foo = Foo() >>> import pytest >>> with pytest.warns(RuntimeWarning) as record: >>> print('foo = {!r}'.format(foo)) foo = <...Foo ...> .. py:class:: Matrix(matrix) Bases: :py:obj:`Transform` Base class for matrix-based transform. .. rubric:: Example >>> from kwimage.transform import * # NOQA >>> ms = {} >>> ms['random()'] = Matrix.random() >>> ms['eye()'] = Matrix.eye() >>> ms['random(3)'] = Matrix.random(3) >>> ms['random(4, 4)'] = Matrix.random(4, 4) >>> ms['eye(3)'] = Matrix.eye(3) >>> ms['explicit'] = Matrix(np.array([[1.618]])) >>> for k, m in ms.items(): >>> print('----') >>> print(f'{k} = {m}') >>> print(f'{k}.inv() = {m.inv()}') >>> print(f'{k}.T = {m.T}') >>> print(f'{k}.det() = {m.det()}') .. py:method:: __nice__(self) .. py:method:: __repr__(self) Return repr(self). .. py:method:: shape(self) :property: .. py:method:: __json__(self) .. py:method:: coerce(cls, data=None, **kwargs) :classmethod: .. rubric:: Example >>> Matrix.coerce({'type': 'matrix', 'matrix': [[1, 0, 0], [0, 1, 0]]}) >>> Matrix.coerce(np.eye(3)) >>> Matrix.coerce(None) .. py:method:: __array__(self) Allow this object to be passed to np.asarray .. rubric:: References https://numpy.org/doc/stable/user/basics.dispatch.html .. py:method:: __imatmul__(self, other) .. py:method:: __matmul__(self, other) .. rubric:: Example >>> m = {} >>> # Works, and returns a Matrix >>> m[len(m)] = x = Matrix.random() @ np.eye(2) >>> assert isinstance(x, Matrix) >>> m[len(m)] = x = Matrix.random() @ None >>> assert isinstance(x, Matrix) >>> # Works, and returns an ndarray >>> m[len(m)] = x = np.eye(3) @ Matrix.random(3) >>> assert isinstance(x, np.ndarray) >>> # These do not work >>> # m[len(m)] = None @ Matrix.random() >>> # m[len(m)] = np.eye(3) @ None >>> print('m = {}'.format(ub.repr2(m))) .. py:method:: inv(self) Returns the inverse of this matrix :returns: Matrix .. py:method:: T(self) :property: Transpose the underlying matrix .. py:method:: det(self) Compute the determinant of the underlying matrix :returns: float .. py:method:: eye(cls, shape=None, rng=None) :classmethod: Construct an identity .. py:method:: random(cls, shape=None, rng=None) :classmethod: .. py:class:: Linear(matrix) Bases: :py:obj:`Matrix` Base class for matrix-based transform. .. rubric:: Example >>> from kwimage.transform import * # NOQA >>> ms = {} >>> ms['random()'] = Matrix.random() >>> ms['eye()'] = Matrix.eye() >>> ms['random(3)'] = Matrix.random(3) >>> ms['random(4, 4)'] = Matrix.random(4, 4) >>> ms['eye(3)'] = Matrix.eye(3) >>> ms['explicit'] = Matrix(np.array([[1.618]])) >>> for k, m in ms.items(): >>> print('----') >>> print(f'{k} = {m}') >>> print(f'{k}.inv() = {m.inv()}') >>> print(f'{k}.T = {m.T}') >>> print(f'{k}.det() = {m.det()}') .. py:class:: Projective(matrix) Bases: :py:obj:`Linear` Currently just a stub class that may be used to implement projective / homography transforms in the future. .. py:class:: Affine(matrix) Bases: :py:obj:`Projective` Helper for making affine transform matrices. .. rubric:: Example >>> self = Affine(np.eye(3)) >>> m1 = np.eye(3) @ self >>> m2 = self @ np.eye(3) .. rubric:: Example >>> from kwimage.transform import * # NOQA >>> m = {} >>> # Works, and returns a Affine >>> m[len(m)] = x = Affine.random() @ np.eye(3) >>> assert isinstance(x, Affine) >>> m[len(m)] = x = Affine.random() @ None >>> assert isinstance(x, Affine) >>> # Works, and returns an ndarray >>> m[len(m)] = x = np.eye(3) @ Affine.random(3) >>> assert isinstance(x, np.ndarray) >>> # Works, and returns an Matrix >>> m[len(m)] = x = Affine.random() @ Matrix.random(3) >>> assert isinstance(x, Matrix) >>> m[len(m)] = x = Matrix.random(3) @ Affine.random() >>> assert isinstance(x, Matrix) >>> print('m = {}'.format(ub.repr2(m))) .. py:method:: shape(self) :property: .. py:method:: __getitem__(self, index) .. py:method:: __json__(self) .. py:method:: concise(self) Return a concise coercable dictionary representation of this matrix :returns: a small serializable dict that can be passed to :func:`Affine.coerce` to reconstruct this object. :rtype: Dict[str, object] :returns: dictionary with consise parameters :rtype: Dict .. rubric:: Example >>> self = Affine.random(rng=0, scale=1) >>> params = self.concise() >>> assert np.allclose(Affine.coerce(params).matrix, self.matrix) >>> print('params = {}'.format(ub.repr2(params, nl=1, precision=2))) params = { 'offset': (0.08, 0.38), 'theta': 0.08, 'type': 'affine', } .. rubric:: Example >>> self = Affine.random(rng=0, scale=2, offset=0) >>> params = self.concise() >>> assert np.allclose(Affine.coerce(params).matrix, self.matrix) >>> print('params = {}'.format(ub.repr2(params, nl=1, precision=2))) params = { 'scale': 2.00, 'theta': 0.04, 'type': 'affine', } .. py:method:: coerce(cls, data=None, **kwargs) :classmethod: Attempt to coerce the data into an affine object :Parameters: * **data** -- some data we attempt to coerce to an Affine matrix * **\*\*kwargs** -- some data we attempt to coerce to an Affine matrix, mutually exclusive with `data`. :returns: Affine .. rubric:: Example >>> import kwimage >>> kwimage.Affine.coerce({'type': 'affine', 'matrix': [[1, 0, 0], [0, 1, 0]]}) >>> kwimage.Affine.coerce({'scale': 2}) >>> kwimage.Affine.coerce({'offset': 3}) >>> kwimage.Affine.coerce(np.eye(3)) >>> kwimage.Affine.coerce(None) >>> kwimage.Affine.coerce(skimage.transform.AffineTransform(scale=30)) .. py:method:: decompose(self) Decompose the affine matrix into its individual scale, translation, rotation, and skew parameters. :returns: decomposed offset, scale, theta, and shear params :rtype: Dict .. rubric:: References https://math.stackexchange.com/questions/612006/decompose-affine .. rubric:: Example >>> self = Affine.random() >>> params = self.decompose() >>> recon = Affine.coerce(**params) >>> params2 = recon.decompose() >>> pt = np.vstack([np.random.rand(2, 1), [1]]) >>> result1 = self.matrix[0:2] @ pt >>> result2 = recon.matrix[0:2] @ pt >>> assert np.allclose(result1, result2) >>> self = Affine.scale(0.001) @ Affine.random() >>> params = self.decompose() >>> self.det() .. py:method:: scale(cls, scale) :classmethod: Create a scale Affine object :Parameters: **scale** (*float | Tuple[float, float]*) -- x, y scale factor :returns: Affine .. py:method:: translate(cls, offset) :classmethod: Create a translation Affine object :Parameters: **offset** (*float | Tuple[float, float]*) -- x, y translation factor :returns: Affine .. py:method:: rotate(cls, theta) :classmethod: Create a rotation Affine object :Parameters: **theta** (*float*) -- counter-clockwise rotation angle in radians :returns: Affine .. py:method:: random(cls, rng=None, **kw) :classmethod: Create a random Affine object :Parameters: * **rng** -- random number generator * **\*\*kw** -- passed to :func:`Affine.random_params`. can contain coercable random distributions for scale, offset, about, theta, and shear. :returns: Affine .. py:method:: random_params(cls, rng=None, **kw) :classmethod: :Parameters: * **rng** -- random number generator * **\*\*kw** -- can contain coercable random distributions for scale, offset, about, theta, and shear. :returns: affine parameters suitable to be passed to Affine.affine :rtype: Dict .. todo:: - [ ] improve kwargs parameterization .. py:method:: affine(cls, scale=None, offset=None, theta=None, shear=None, about=None) :classmethod: Create an affine matrix from high-level parameters :Parameters: * **scale** (*float | Tuple[float, float]*) -- x, y scale factor * **offset** (*float | Tuple[float, float]*) -- x, y translation factor * **theta** (*float*) -- counter-clockwise rotation angle in radians * **shear** (*float*) -- counter-clockwise shear angle in radians * **about** (*float | Tuple[float, float]*) -- x, y location of the origin :returns: the constructed Affine object :rtype: Affine .. rubric:: Example >>> rng = kwarray.ensure_rng(None) >>> scale = rng.randn(2) * 10 >>> offset = rng.randn(2) * 10 >>> about = rng.randn(2) * 10 >>> theta = rng.randn() * 10 >>> shear = rng.randn() * 10 >>> # Create combined matrix from all params >>> F = Affine.affine( >>> scale=scale, offset=offset, theta=theta, shear=shear, >>> about=about) >>> # Test that combining components matches >>> S = Affine.affine(scale=scale) >>> T = Affine.affine(offset=offset) >>> R = Affine.affine(theta=theta) >>> H = Affine.affine(shear=shear) >>> O = Affine.affine(offset=about) >>> # combine (note shear must be on the RHS of rotation) >>> alt = O @ T @ R @ H @ S @ O.inv() >>> print('F = {}'.format(ub.repr2(F.matrix.tolist(), nl=1))) >>> print('alt = {}'.format(ub.repr2(alt.matrix.tolist(), nl=1))) >>> assert np.all(np.isclose(alt.matrix, F.matrix)) >>> pt = np.vstack([np.random.rand(2, 1), [[1]]]) >>> warp_pt1 = (F.matrix @ pt) >>> warp_pt2 = (alt.matrix @ pt) >>> assert np.allclose(warp_pt2, warp_pt1) Sympy: >>> # xdoctest: +SKIP >>> import sympy >>> # Shows the symbolic construction of the code >>> # https://groups.google.com/forum/#!topic/sympy/k1HnZK_bNNA >>> from sympy.abc import theta >>> x0, y0, sx, sy, theta, shear, tx, ty = sympy.symbols( >>> 'x0, y0, sx, sy, theta, shear, tx, ty') >>> # move the center to 0, 0 >>> tr1_ = np.array([[1, 0, -x0], >>> [0, 1, -y0], >>> [0, 0, 1]]) >>> # Define core components of the affine transform >>> S = np.array([ # scale >>> [sx, 0, 0], >>> [ 0, sy, 0], >>> [ 0, 0, 1]]) >>> H = np.array([ # shear >>> [1, -sympy.sin(shear), 0], >>> [0, sympy.cos(shear), 0], >>> [0, 0, 1]]) >>> R = np.array([ # rotation >>> [sympy.cos(theta), -sympy.sin(theta), 0], >>> [sympy.sin(theta), sympy.cos(theta), 0], >>> [ 0, 0, 1]]) >>> T = np.array([ # translation >>> [ 1, 0, tx], >>> [ 0, 1, ty], >>> [ 0, 0, 1]]) >>> # Contruct the affine 3x3 about the origin >>> aff0 = np.array(sympy.simplify(T @ R @ H @ S)) >>> # move 0, 0 back to the specified origin >>> tr2_ = np.array([[1, 0, x0], >>> [0, 1, y0], >>> [0, 0, 1]]) >>> # combine transformations >>> aff = tr2_ @ aff0 @ tr1_ >>> print('aff = {}'.format(ub.repr2(aff.tolist(), nl=1))) .. py:function:: _ensure_iterablen(scalar, n)