kwimage.transform

Objects for representing and manipulating image transforms.

Module Contents

Classes

Transform

Inherit from this class and define __nice__ to “nicely” print your

Matrix

Base class for matrix-based transform.

Linear

Base class for matrix-based transform.

Projective

Currently just a stub class that may be used to implement projective /

Affine

Helper for making affine transform matrices.

Functions

_ensure_iterablen(scalar, n)

class kwimage.transform.Transform[source]

Bases: 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 NiceRepr should redefine __nice__. If the inheriting class has a __len__, method then the default __nice__ method will return its length.

Example

>>> import ubelt as ub
>>> class Foo(ub.NiceRepr):
...    def __nice__(self):
...        return 'info'
>>> foo = Foo()
>>> assert str(foo) == '<Foo(info)>'
>>> assert repr(foo).startswith('<Foo(info) at ')

Example

>>> 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)

Example

>>> import ubelt as ub
>>> class Baz(ub.NiceRepr):
...    def __len__(self):
...        return 5
>>> baz = Baz()
>>> assert str(baz) == '<Baz(5)>'

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 ...>
class kwimage.transform.Matrix(matrix)[source]

Bases: Transform

Base class for matrix-based transform.

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()}')
__nice__(self)[source]
__repr__(self)[source]

Return repr(self).

property shape(self)[source]
__json__(self)[source]
classmethod coerce(cls, data=None, **kwargs)[source]

Example

>>> Matrix.coerce({'type': 'matrix', 'matrix': [[1, 0, 0], [0, 1, 0]]})
>>> Matrix.coerce(np.eye(3))
>>> Matrix.coerce(None)
__array__(self)[source]

Allow this object to be passed to np.asarray

References

https://numpy.org/doc/stable/user/basics.dispatch.html

__imatmul__(self, other)[source]
__matmul__(self, other)[source]

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)))
inv(self)[source]

Returns the inverse of this matrix

Returns

Matrix

property T(self)[source]

Transpose the underlying matrix

det(self)[source]

Compute the determinant of the underlying matrix

Returns

float

classmethod eye(cls, shape=None, rng=None)[source]

Construct an identity

classmethod random(cls, shape=None, rng=None)[source]
class kwimage.transform.Linear(matrix)[source]

Bases: Matrix

Base class for matrix-based transform.

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()}')
class kwimage.transform.Projective(matrix)[source]

Bases: Linear

Currently just a stub class that may be used to implement projective / homography transforms in the future.

class kwimage.transform.Affine(matrix)[source]

Bases: Projective

Helper for making affine transform matrices.

Example

>>> self = Affine(np.eye(3))
>>> m1 = np.eye(3) @ self
>>> m2 = self @ np.eye(3)

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)))
property shape(self)[source]
__getitem__(self, index)[source]
__json__(self)[source]
concise(self)[source]

Return a concise coercable dictionary representation of this matrix

Returns

a small serializable dict that can be passed

to Affine.coerce() to reconstruct this object.

Return type

Dict[str, object]

Returns

dictionary with consise parameters

Return type

Dict

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',
}

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',
}
classmethod coerce(cls, data=None, **kwargs)[source]

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

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))
decompose(self)[source]

Decompose the affine matrix into its individual scale, translation, rotation, and skew parameters.

Returns

decomposed offset, scale, theta, and shear params

Return type

Dict

References

https://math.stackexchange.com/questions/612006/decompose-affine

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()
classmethod scale(cls, scale)[source]

Create a scale Affine object

Parameters

scale (float | Tuple[float, float]) – x, y scale factor

Returns

Affine

classmethod translate(cls, offset)[source]

Create a translation Affine object

Parameters

offset (float | Tuple[float, float]) – x, y translation factor

Returns

Affine

classmethod rotate(cls, theta)[source]

Create a rotation Affine object

Parameters

theta (float) – counter-clockwise rotation angle in radians

Returns

Affine

classmethod random(cls, rng=None, **kw)[source]

Create a random Affine object

Parameters
  • rng – random number generator

  • **kw – passed to Affine.random_params(). can contain coercable random distributions for scale, offset, about, theta, and shear.

Returns

Affine

classmethod random_params(cls, rng=None, **kw)[source]
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

Return type

Dict

Todo

  • [ ] improve kwargs parameterization

classmethod affine(cls, scale=None, offset=None, theta=None, shear=None, about=None)[source]

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

Return type

Affine

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)))
kwimage.transform._ensure_iterablen(scalar, n)[source]