kwimage.transform module

Objects for representing and manipulating image transforms.

XDEV_PROFILE=1 xdoctest ~/code/kwimage/kwimage/transform.py

class kwimage.transform.Transform[source]

Bases: NiceRepr

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

Example

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

TODO: rename to “is_symbolic”

inv()[source]

Returns the inverse of this matrix

Returns:

Matrix

Example

>>> # Test with rationals
>>> # xdoctest: +REQUIRES(module:sympy)
>>> import kwimage
>>> self = kwimage.Matrix.random((3, 3)).rationalize()
>>> inv = self.inv()
>>> eye = self @ inv
>>> eye.isclose_identity(0, 0)
property T

Transpose the underlying matrix

det()[source]

Compute the determinant of the underlying matrix

Returns:

float

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

Construct an identity

classmethod random(shape=None, rng=None)[source]
rationalize()[source]

Convert the underlying matrix to a rational type to avoid floating point errors. This does decrease efficiency.

Todo

  • [ ] mpmath for arbitrary precision? It doesn’t seem to do

inverses correct whereas sympy does.

Example

>>> # xdoctest: +REQUIRES(module:sympy)
>>> import kwimage
>>> self = kwimage.Matrix.random((3, 3))
>>> mat = self.rationalize()
>>> mat2 = kwimage.Matrix.random((3, 3))
>>> mat3 = mat @ mat2
>>> #assert 'sympy' in mat3.matrix.__class__.__module__
>>> mat3 = mat2 @ mat
>>> #assert 'sympy' in mat3.matrix.__class__.__module__
>>> assert not mat.isclose_identity()
>>> assert (mat @ mat.inv()).isclose_identity(rtol=0, atol=0)
astype(dtype)[source]

Convert the underlying matrix to a rational type to avoid floating point errors. This does decrease efficiency.

Parameters:

dtype (type)

isclose_identity(rtol=1e-05, atol=1e-08)[source]

Returns true if the matrix is nearly the identity.

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

Bases: Matrix

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

Bases: Projective

A thin wraper around a 3x3 matrix that represents an affine transform

Implements methods for:
  • creating random affine transforms

  • decomposing the matrix

  • finding a best-fit transform between corresponding points

  • TODO: - [ ] fully rational transform

Example

>>> import kwimage
>>> import math
>>> image = kwimage.grab_test_image()
>>> theta = 0.123 * math.tau
>>> components = {
>>>     'rotate': kwimage.Affine.affine(theta=theta),
>>>     'scale': kwimage.Affine.affine(scale=0.5),
>>>     'shear': kwimage.Affine.affine(shearx=0.2),
>>>     'translation': kwimage.Affine.affine(offset=(100, 200)),
>>>     'rotate+translate': kwimage.Affine.affine(theta=0.123 * math.tau, about=(256, 256)),
>>>     'random composed': kwimage.Affine.random(scale=(0.5, 1.5), translate=(-20, 20), theta=(-theta, theta), shearx=(0, .4), rng=900558176210808600),
>>> }
>>> warp_stack = []
>>> for key, aff in components.items():
...     warp = kwimage.warp_affine(image, aff)
...     warp = kwimage.draw_text_on_image(
...        warp,
...        ub.urepr(aff.matrix, nl=1, nobr=1, precision=2, si=1, sv=1, with_dtype=0),
...        org=(1, 1),
...        valign='top', halign='left',
...        fontScale=0.8, color='kw_blue',
...        border={'thickness': 3},
...        )
...     warp = kwimage.draw_header_text(warp, key, color='kw_green')
...     warp_stack.append(warp)
>>> warp_canvas = kwimage.stack_images_grid(warp_stack, chunksize=3, pad=10, bg_value='kitware_gray')
>>> # xdoctest: +REQUIRES(module:sympy)
>>> import sympy
>>> # Shows the symbolic construction of the code
>>> # https://groups.google.com/forum/#!topic/sympy/k1HnZK_bNNA
>>> from sympy.abc import theta
>>> params = x0, y0, sx, sy, theta, shearx, tx, ty = sympy.symbols(
>>>     'x0, y0, sx, sy, theta, shearx, tx, ty')
>>> theta = sympy.symbols('theta')
>>> # 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]])
>>> E = np.array([  # x-shear
>>>     [1,  shearx, 0],
>>>     [0,  1, 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 @ E @ 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.urepr(aff.tolist(), nl=1)))
>>> # This could be prettier
>>> texts = {
>>>     'Translation': sympy.pretty(R),
>>>     'Rotation': sympy.pretty(R),
>>>     'shEar-X': sympy.pretty(E),
>>>     'Scale': sympy.pretty(S),
>>> }
>>> print(ub.urepr(texts, nl=2, sv=1))
>>> equation_stack = []
>>> for text, m in texts.items():
>>>     render_canvas = kwimage.draw_text_on_image(None, m, color='kw_blue', fontScale=1.0)
>>>     render_canvas = kwimage.draw_header_text(render_canvas, text, color='kw_green')
>>>     render_canvas = kwimage.imresize(render_canvas, scale=1.3)
>>>     equation_stack.append(render_canvas)
>>> equation_canvas = kwimage.stack_images(equation_stack, pad=10, axis=1, bg_value='kitware_gray')
>>> render_canvas = kwimage.draw_text_on_image(None, sympy.pretty(aff), color='kw_blue', fontScale=1.0)
>>> render_canvas = kwimage.draw_header_text(render_canvas, 'Full Equation With Pre-Shift', color='kw_green')
>>> # xdoctest: -REQUIRES(module:sympy)
>>> # xdoctest: +REQUIRES(--show)
>>> import kwplot
>>> plt = kwplot.autoplt()
>>> canvas = kwimage.stack_images([warp_canvas, equation_canvas, render_canvas], pad=20, axis=0, bg_value='kitware_gray', resize='larger')
>>> canvas = kwimage.draw_header_text(canvas, 'Affine matrixes can represent', color='kw_green')
>>> kwplot.imshow(canvas)
>>> fig = plt.gcf()
>>> fig.set_size_inches(13, 13)
_images/fig_kwimage_transform_Affine_002.jpeg

Example

>>> import kwimage
>>> self = kwimage.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()
>>> 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.urepr(m)))
property shape
concise()[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

>>> import kwimage
>>> self = kwimage.Affine.random(rng=0, scale=1)
>>> params = self.concise()
>>> assert np.allclose(Affine.coerce(params).matrix, self.matrix)
>>> print('params = {}'.format(ub.urepr(params, nl=1, precision=2)))
params = {
    'offset': (0.08, 0.38),
    'theta': 0.08,
    'type': 'affine',
}

Example

>>> import kwimage
>>> self = kwimage.Affine.random(rng=0, scale=2, offset=0)
>>> params = self.concise()
>>> assert np.allclose(Affine.coerce(params).matrix, self.matrix)
>>> print('params = {}'.format(ub.urepr(params, nl=1, precision=2)))
params = {
    'scale': 2.00,
    'theta': 0.04,
    'type': 'affine',
}
classmethod from_shapely(sh_aff)[source]

Shapely affine tuples are in the format (a, b, d, e, x, y)

classmethod from_affine(aff)[source]
classmethod from_gdal(gdal_aff)[source]

gdal affine tuples are in the format (c, a, b, f, d, e)

classmethod from_skimage(sk_aff)[source]

gdal affine tuples are in the format (c, a, b, f, d, e)

classmethod coerce(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
>>> import skimage.transform
>>> 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({})
>>> kwimage.Affine.coerce(skimage.transform.AffineTransform(scale=30))
eccentricity()[source]

Eccentricity of the ellipse formed by this affine matrix

Returns:

large when there are big scale differences in principle

directions or skews.

Return type:

float

References

Example

>>> import kwimage
>>> kwimage.Affine.random(rng=432).eccentricity()
to_affine()[source]

Convert to an affine module

Returns:

affine.Affine

to_gdal()[source]

Convert to a gdal tuple (c, a, b, f, d, e)

Returns:

Tuple[float, float, float, float, float, float]

to_shapely()[source]

Returns a matrix suitable for shapely.affinity.affine_transform

Returns:

Tuple[float, float, float, float, float, float]

Example

>>> import kwimage
>>> self = kwimage.Affine.random()
>>> sh_transform = self.to_shapely()
>>> # Transform points with kwimage and shapley
>>> import shapely
>>> from shapely.affinity import affine_transform
>>> kw_poly = kwimage.Polygon.random()
>>> kw_warp_poly = kw_poly.warp(self)
>>> sh_poly = kw_poly.to_shapely()
>>> sh_warp_poly = affine_transform(sh_poly, sh_transform)
>>> kw_warp_poly_recon = kwimage.Polygon.from_shapely(sh_warp_poly)
>>> assert np.allclose(kw_warp_poly_recon.exterior.data, kw_warp_poly_recon.exterior.data)
to_skimage()[source]
Returns:

skimage.transform.AffineTransform

Example

>>> import kwimage
>>> self = kwimage.Affine.random()
>>> tf = self.to_skimage()
>>> # Transform points with kwimage and scikit-image
>>> kw_poly = kwimage.Polygon.random()
>>> kw_warp_xy = kw_poly.warp(self.matrix).exterior.data
>>> sk_warp_xy = tf(kw_poly.exterior.data)
>>> assert np.allclose(sk_warp_xy, sk_warp_xy)
classmethod scale(scale)[source]

Create a scale Affine object

Parameters:

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

Returns:

Affine

classmethod translate(offset)[source]

Create a translation Affine object

Parameters:

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

Returns:

Affine

Benchmark

>>> # xdoctest: +REQUIRES(--benchmark)
>>> # It is ~3x faster to use the more specific method
>>> import timerit
>>> import kwimage
>>> #
>>> offset = np.random.rand(2)
>>> ti = timerit.Timerit(100, bestof=10, verbose=2)
>>> for timer in ti.reset('time'):
>>>     with timer:
>>>         kwimage.Affine.translate(offset)
>>> #
>>> for timer in ti.reset('time'):
>>>     with timer:
>>>         kwimage.Affine.affine(offset=offset)
classmethod _scale_translate(scale, offset)[source]

helper method for speed

classmethod rotate(theta)[source]

Create a rotation Affine object

Parameters:

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

Returns:

Affine

classmethod random(shape=None, 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 shearx.

Returns:

Affine

classmethod random_params(rng=None, **kw)[source]
Parameters:
  • rng – random number generator

  • **kw – can contain coercable random distributions for scale, offset, about, theta, and shearx.

Returns:

affine parameters suitable to be passed to Affine.affine

Return type:

Dict

Todo

  • [ ] improve kwargs parameterization

decompose()[source]

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

Returns:

decomposed offset, scale, theta, and shearx params

Return type:

Dict

References

Example

>>> from kwimage.transform import *  # NOQA
>>> 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()

Example

>>> # xdoctest: +REQUIRES(module:sympy)
>>> # Test decompose with symbolic matrices
>>> from kwimage.transform import *  # NOQA
>>> self = Affine.random().rationalize()
>>> self.decompose()

Example

>>> # xdoctest: +REQUIRES(module:pandas)
>>> from kwimage.transform import *  # NOQA
>>> import kwimage
>>> import pandas as pd
>>> # Test consistency of decompose + reconstruct
>>> param_grid = list(ub.named_product({
>>>     'theta': np.linspace(-4 * np.pi, 4 * np.pi, 3),
>>>     'shearx': np.linspace(- 10 * np.pi, 10 * np.pi, 4),
>>> }))
>>> def normalize_angle(radian):
>>>     return np.arctan2(np.sin(radian), np.cos(radian))
>>> for pextra in param_grid:
>>>     params0 = dict(scale=(3.05, 3.07), offset=(10.5, 12.1), **pextra)
>>>     self = recon0 = kwimage.Affine.affine(**params0)
>>>     self.decompose()
>>>     # Test drift with multiple decompose / reconstructions
>>>     params_list = [params0]
>>>     recon_list = [recon0]
>>>     n = 4
>>>     for _ in range(n):
>>>         prev = recon_list[-1]
>>>         params = prev.decompose()
>>>         recon = kwimage.Affine.coerce(**params)
>>>         params_list.append(params)
>>>         recon_list.append(recon)
>>>     params_df = pd.DataFrame(params_list)
>>>     #print('params_list = {}'.format(ub.urepr(params_list, nl=1, precision=5)))
>>>     print(params_df)
>>>     assert ub.allsame(normalize_angle(params_df['theta']), eq=np.isclose)
>>>     assert ub.allsame(params_df['shearx'], eq=np.allclose)
>>>     assert ub.allsame(params_df['scale'], eq=np.allclose)
>>>     assert ub.allsame(params_df['offset'], eq=np.allclose)
classmethod affine(scale=None, offset=None, theta=None, shear=None, about=None, shearx=None, array_cls=None, math_mod=None, **kwargs)[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

  • shearx (float) – shear factor parallel to the x-axis.

  • about (float | Tuple[float, float]) – x, y location of the origin

  • shear (float) – BROKEN, dont use. counter-clockwise shear angle in radians

Todo

  • [ ] Add aliases? -

    origin : alias for about rotation : alias for theta translation : alias for offset

Returns:

the constructed Affine object

Return type:

Affine

Example

>>> from kwimage.transform import *  # NOQA
>>> rng = kwarray.ensure_rng(None)
>>> scale = rng.randn(2) * 10
>>> offset = rng.randn(2) * 10
>>> about = rng.randn(2) * 10
>>> theta = rng.randn() * 10
>>> shearx = rng.randn() * 10
>>> # Create combined matrix from all params
>>> F = Affine.affine(
>>>     scale=scale, offset=offset, theta=theta, shearx=shearx,
>>>     about=about)
>>> # Test that combining components matches
>>> S = Affine.affine(scale=scale)
>>> T = Affine.affine(offset=offset)
>>> R = Affine.affine(theta=theta)
>>> E = Affine.affine(shearx=shearx)
>>> O = Affine.affine(offset=about)
>>> # combine (note shear must be on the RHS of rotation)
>>> alt  = O @ T @ R @ E @ S @ O.inv()
>>> print('F    = {}'.format(ub.urepr(F.matrix.tolist(), nl=1)))
>>> print('alt  = {}'.format(ub.urepr(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
>>> params = x0, y0, sx, sy, theta, shearx, tx, ty = sympy.symbols(
>>>     'x0, y0, sx, sy, theta, shearx, 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]])
>>> E = np.array([  # x-shear
>>>     [1,  shearx, 0],
>>>     [0,  1, 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 @ E @ 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.urepr(aff.tolist(), nl=1)))
classmethod fit(pts1, pts2)[source]

Fit an affine transformation between a set of corresponding points

Parameters:
  • pts1 (ndarray) – An Nx2 array of points in “space 1”.

  • pts2 (ndarray) – A corresponding Nx2 array of points in “space 2”

Returns:

a transform that warps from “space1” to “space2”.

Return type:

Affine

Note

An affine matrix has 6 degrees of freedom, so at least 3 non-colinear xy-point pairs are needed.

References

..[Lowe04] https://www.cs.ubc.ca/~lowe/papers/ijcv04.pdf page 22

Example

>>> # Create a set of points, warp them, then recover the warp
>>> import kwimage
>>> points = kwimage.Points.random(6).scale(64)
>>> #A1 = kwimage.Affine.affine(scale=0.9, theta=-3.2, offset=(2, 3), about=(32, 32), skew=2.3)
>>> #A2 = kwimage.Affine.affine(scale=0.8, theta=0.8, offset=(2, 0), about=(32, 32))
>>> A1 = kwimage.Affine.random()
>>> A2 = kwimage.Affine.random()
>>> A12_real = A2 @ A1.inv()
>>> points1 = points.warp(A1)
>>> points2 = points.warp(A2)
>>> # Recover the warp
>>> pts1, pts2 = points1.xy, points2.xy
>>> A_recovered = kwimage.Affine.fit(pts1, pts2)
>>> assert np.all(np.isclose(A_recovered.matrix, A12_real.matrix))
>>> # xdoctest: +REQUIRES(--show)
>>> import kwplot
>>> kwplot.autompl()
>>> base1 = np.zeros((96, 96, 3))
>>> base1[32:-32, 5:-5] = 0.5
>>> base2 = np.zeros((96, 96, 3))
>>> img1 = points1.draw_on(base1, radius=3, color='blue')
>>> img2 = points2.draw_on(base2, radius=3, color='green')
>>> img1_warp = kwimage.warp_affine(img1, A_recovered)
>>> canvas = kwimage.stack_images([img1, img2, img1_warp], pad=10, axis=1, bg_value=(1., 1., 1.))
>>> kwplot.imshow(canvas)
_images/fig_kwimage_transform_Affine_fit_002.jpeg
classmethod fliprot(flip_axis=None, rot_k=0, axes=(0, 1), canvas_dsize=None)[source]

Creates a flip/rotation transform with respect to an image of a given size in the positive quadrent. (i.e. warped data within the specified canvas size will stay in the positive quadrant)

Parameters:
  • flip_axis (int) – the axis dimension to flip. I.e. 0 flips the y-axis and 1-flips the x-axis.

  • rot_k (int) – number of counterclockwise 90 degree rotations that occur after the flips.

  • axes (Tuple[int, int]) – The axis ordering. Unhandled in this version. Dont change this.

  • canvas_dsize (Tuple[int, int]) – The width / height of the canvas the fliprot is applied in.

Returns:

The affine matrix representing the canvas-aligned flip and rotation.

Return type:

Affine

Note

Requiring that the image size is known makes this a place that errors could occur depending on your interpretation of pixels as points or areas. There is probably a better way to describe the issue, but the second doctest shows the issue when trying to use warp-affine’s auto-dsize feature. See [MR81] for details.

References

CommandLine

xdoctest -m kwimage.transform Affine.fliprot:0 --show
xdoctest -m kwimage.transform Affine.fliprot:1 --show

Example

>>> import kwimage
>>> H, W = 64, 128
>>> canvas_dsize = (W, H)
>>> box1 = kwimage.Boxes.random(1).scale((W, H)).quantize()
>>> ltrb = box1.data
>>> rot_k = 4
>>> annot = box1
>>> annot = box1.to_polygons()[0]
>>> annot1 = annot.copy()
>>> # The first 8 are the cannonically unique group elements
>>> fliprot_params = [
>>>     {'rot_k': 0, 'flip_axis': None},
>>>     {'rot_k': 1, 'flip_axis': None},
>>>     {'rot_k': 2, 'flip_axis': None},
>>>     {'rot_k': 3, 'flip_axis': None},
>>>     {'rot_k': 0, 'flip_axis': (0,)},
>>>     {'rot_k': 1, 'flip_axis': (0,)},
>>>     {'rot_k': 2, 'flip_axis': (0,)},
>>>     {'rot_k': 3, 'flip_axis': (0,)},
>>>     # The rest of these dont result in any different data, but we need to test them
>>>     {'rot_k': 0, 'flip_axis': (1,)},
>>>     {'rot_k': 1, 'flip_axis': (1,)},
>>>     {'rot_k': 2, 'flip_axis': (1,)},
>>>     {'rot_k': 3, 'flip_axis': (1,)},
>>>     {'rot_k': 0, 'flip_axis': (0, 1)},
>>>     {'rot_k': 1, 'flip_axis': (0, 1)},
>>>     {'rot_k': 2, 'flip_axis': (0, 1)},
>>>     {'rot_k': 3, 'flip_axis': (0, 1)},
>>> ]
>>> results = []
>>> for params in fliprot_params:
>>>     tf = kwimage.Affine.fliprot(canvas_dsize=canvas_dsize, **params)
>>>     annot2 = annot.warp(tf)
>>>     annot3 = annot2.warp(tf.inv())
>>>     #annot3 = inv_fliprot_annot(annot2, canvas_dsize=canvas_dsize, **params)
>>>     results.append({
>>>         'annot2': annot2,
>>>         'annot3': annot3,
>>>         'params': params,
>>>         'tf': tf,
>>>         'canvas_dsize': canvas_dsize,
>>>     })
>>> box = kwimage.Box.coerce([0, 0, W, H], format='xywh')
>>> for result in results:
>>>     params = result['params']
>>>     warped = box.warp(result['tf'])
>>>     print('---')
>>>     print('params = {}'.format(ub.urepr(params, nl=1)))
>>>     print('box = {}'.format(ub.urepr(box, nl=1)))
>>>     print('warped = {}'.format(ub.urepr(warped, nl=1)))
>>>     print(ub.hzcat(['tf = ', ub.urepr(result['tf'], nl=1)]))
>>> # xdoctest: +REQUIRES(--show)
>>> import kwplot
>>> kwplot.autompl()
>>> S = max(W, H)
>>> image1 = kwimage.grab_test_image('astro', dsize=(S, S))[:H, :W]
>>> pnum_ = kwplot.PlotNums(nCols=4, nSubplots=len(results))
>>> for result in results:
>>>     #image2 = kwimage.warp_affine(image1.copy(), result['tf'], dsize=(S, S))  # fixme dsize=positive should work here
>>>     image2 = kwimage.warp_affine(image1.copy(), result['tf'], dsize='positive')  # fixme dsize=positive should work here
>>>     #image3 = kwimage.warp_affine(image2.copy(), result['tf'].inv(), dsize=(S, S))
>>>     image3 = kwimage.warp_affine(image2.copy(), result['tf'].inv(), dsize='positive')
>>>     annot2 = result['annot2']
>>>     annot3 = result['annot3']
>>>     canvas1 = annot1.draw_on(image1.copy(), edgecolor='kitware_blue', fill=False)
>>>     canvas2 = annot2.draw_on(image2.copy(), edgecolor='kitware_green', fill=False)
>>>     canvas3 = annot3.draw_on(image3.copy(), edgecolor='kitware_red', fill=False)
>>>     canvas = kwimage.stack_images([canvas1, canvas2, canvas3], axis=1, pad=10, bg_value='green')
>>>     kwplot.imshow(canvas, pnum=pnum_(), title=ub.urepr(result['params'], nl=0, compact=1, nobr=1))
>>> kwplot.show_if_requested()
_images/fig_kwimage_transform_Affine_fliprot_002.jpeg

Example

>>> # Second similar test with a very small image to catch small errors
>>> import kwimage
>>> H, W = 4, 8
>>> canvas_dsize = (W, H)
>>> box1 = kwimage.Boxes.random(1).scale((W, H)).quantize()
>>> ltrb = box1.data
>>> rot_k = 4
>>> annot = box1
>>> annot = box1.to_polygons()[0]
>>> annot1 = annot.copy()
>>> # The first 8 are the cannonically unique group elements
>>> fliprot_params = [
>>>     {'rot_k': 0, 'flip_axis': None},
>>>     {'rot_k': 1, 'flip_axis': None},
>>>     {'rot_k': 2, 'flip_axis': None},
>>>     {'rot_k': 3, 'flip_axis': None},
>>>     {'rot_k': 0, 'flip_axis': (0,)},
>>>     {'rot_k': 1, 'flip_axis': (0,)},
>>>     {'rot_k': 2, 'flip_axis': (0,)},
>>>     {'rot_k': 3, 'flip_axis': (0,)},
>>>     # The rest of these dont result in any different data, but we need to test them
>>>     {'rot_k': 0, 'flip_axis': (1,)},
>>>     {'rot_k': 1, 'flip_axis': (1,)},
>>>     {'rot_k': 2, 'flip_axis': (1,)},
>>>     {'rot_k': 3, 'flip_axis': (1,)},
>>>     {'rot_k': 0, 'flip_axis': (0, 1)},
>>>     {'rot_k': 1, 'flip_axis': (0, 1)},
>>>     {'rot_k': 2, 'flip_axis': (0, 1)},
>>>     {'rot_k': 3, 'flip_axis': (0, 1)},
>>> ]
>>> results = []
>>> for params in fliprot_params:
>>>     tf = kwimage.Affine.fliprot(canvas_dsize=canvas_dsize, **params)
>>>     annot2 = annot.warp(tf)
>>>     annot3 = annot2.warp(tf.inv())
>>>     #annot3 = inv_fliprot_annot(annot2, canvas_dsize=canvas_dsize, **params)
>>>     results.append({
>>>         'annot2': annot2,
>>>         'annot3': annot3,
>>>         'params': params,
>>>         'tf': tf,
>>>         'canvas_dsize': canvas_dsize,
>>>     })
>>> box = kwimage.Box.coerce([0, 0, W, H], format='xywh')
>>> print('box = {}'.format(ub.urepr(box, nl=1)))
>>> for result in results:
>>>     params = result['params']
>>>     warped = box.warp(result['tf'])
>>>     print('---')
>>>     print('params = {}'.format(ub.urepr(params, nl=1)))
>>>     print('warped = {}'.format(ub.urepr(warped, nl=1)))
>>>     print(ub.hzcat(['tf = ', ub.urepr(result['tf'], nl=1)]))
>>> # xdoctest: +REQUIRES(--show)
>>> import kwplot
>>> kwplot.autompl()
>>> S = max(W, H)
>>> image1 = np.linspace(.1, .9, W * H).reshape((H, W))
>>> image1 = kwimage.atleast_3channels(image1)
>>> image1[0, :, 0] = 1
>>> image1[:, 0, 2] = 1
>>> image1[1, :, 1] = 1
>>> image1[:, 1, 1] = 1
>>> image1[3, :, 0] = 0.5
>>> image1[:, 7, 1] = 0.5
>>> pnum_ = kwplot.PlotNums(nCols=4, nSubplots=len(results))
>>> # NOTE: setting new_dsize='positive' illustrates an issuew with
>>> # the pixel interpretation.
>>> new_dsize = (S, S)
>>> #new_dsize = 'positive'
>>> for result in results:
>>>     image2 = kwimage.warp_affine(image1.copy(), result['tf'], dsize=new_dsize)
>>>     image3 = kwimage.warp_affine(image2.copy(), result['tf'].inv(), dsize=new_dsize)
>>>     annot2 = result['annot2']
>>>     annot3 = result['annot3']
>>>     #canvas1 = annot1.draw_on(image1.copy(), edgecolor='kitware_blue', fill=False)
>>>     #canvas2 = annot2.draw_on(image2.copy(), edgecolor='kitware_green', fill=False)
>>>     #canvas3 = annot3.draw_on(image3.copy(), edgecolor='kitware_red', fill=False)
>>>     canvas = kwimage.stack_images([image1, image2, image3], axis=1, pad=1, bg_value='green')
>>>     kwplot.imshow(canvas, pnum=pnum_(), title=ub.urepr(result['params'], nl=0, compact=1, nobr=1))
>>> kwplot.show_if_requested()
_images/fig_kwimage_transform_Affine_fliprot_003.jpeg
class kwimage.transform.Projective(matrix)[source]

Bases: Linear

A thin wraper around a 3x3 matrix that represent a projective transform

Implements methods for:
  • creating random projective transforms

  • decomposing the matrix

  • finding a best-fit transform between corresponding points

  • TODO: - [ ] fully rational transform

Example

>>> import kwimage
>>> import math
>>> image = kwimage.grab_test_image()
>>> theta = 0.123 * math.tau
>>> components = {
>>>     'rotate': kwimage.Projective.projective(theta=theta),
>>>     'scale': kwimage.Projective.projective(scale=0.5),
>>>     'shear': kwimage.Projective.projective(shearx=0.2),
>>>     'translation': kwimage.Projective.projective(offset=(100, 200)),
>>>     'rotate+translate': kwimage.Projective.projective(theta=0.123 * math.tau, about=(256, 256)),
>>>     'perspective': kwimage.Projective.projective(uv=(0.0003, 0.0007)),
>>>     'random-composed': kwimage.Projective.random(scale=(0.5, 1.5), translate=(-20, 20), theta=(-theta, theta), shearx=(0, .4), rng=900558176210808600),
>>> }
>>> warp_stack = []
>>> for key, mat in components.items():
...     warp = kwimage.warp_projective(image, mat)
...     warp = kwimage.draw_text_on_image(
...        warp,
...        ub.urepr(mat.matrix, nl=1, nobr=1, precision=4, si=1, sv=1, with_dtype=0),
...        org=(1, 1),
...        valign='top', halign='left',
...        fontScale=0.8, color='kw_green',
...        border={'thickness': 3},
...        )
...     warp = kwimage.draw_header_text(warp, key, color='kw_blue')
...     warp_stack.append(warp)
>>> warp_canvas = kwimage.stack_images_grid(warp_stack, chunksize=4, pad=10, bg_value='kitware_gray')
>>> # xdoctest: +REQUIRES(module:sympy)
>>> import sympy
>>> # Shows the symbolic construction of the code
>>> # https://groups.google.com/forum/#!topic/sympy/k1HnZK_bNNA
>>> from sympy.abc import theta
>>> params = x0, y0, sx, sy, theta, shearx, tx, ty, u, v = sympy.symbols(
>>>     'x0, y0, sx, sy, theta, ex, tx, ty, u, v')
>>> # move the center to 0, 0
>>> tr1_ = sympy.Matrix([[1, 0,  -x0],
>>>                      [0, 1,  -y0],
>>>                      [0, 0,    1]])
>>> P = sympy.Matrix([  # projective part
>>>     [ 1,  0,  0],
>>>     [ 0,  1,  0],
>>>     [ u,  v,  1]])
>>> # Define core components of the affine transform
>>> S = sympy.Matrix([  # scale
>>>     [sx,  0, 0],
>>>     [ 0, sy, 0],
>>>     [ 0,  0, 1]])
>>> E = sympy.Matrix([  # x-shear
>>>     [1,  shearx, 0],
>>>     [0,  1, 0],
>>>     [0,  0, 1]])
>>> R = sympy.Matrix([  # rotation
>>>     [sympy.cos(theta), -sympy.sin(theta), 0],
>>>     [sympy.sin(theta),  sympy.cos(theta), 0],
>>>     [               0,                 0, 1]])
>>> T = sympy.Matrix([  # translation
>>>     [ 1,  0, tx],
>>>     [ 0,  1, ty],
>>>     [ 0,  0,  1]])
>>> # move 0, 0 back to the specified origin
>>> tr2_ = sympy.Matrix([[1, 0,  x0],
>>>                      [0, 1,  y0],
>>>                      [0, 0,   1]])
>>> # combine transformations
>>> homog_ = sympy.MatMul(tr2_, T, R, E, S, P, tr1_)
>>> #with sympy.evaluate(False):
>>> #    homog_ = sympy.MatMul(tr2_, T, R, E, S, P, tr1_)
>>> #    sympy.pprint(homog_)
>>> homog = homog_.doit()
>>> #sympy.pprint(homog)
>>> print('homog = {}'.format(ub.urepr(homog.tolist(), nl=1)))
>>> # This could be prettier
>>> texts = {
>>>     'Translation': sympy.pretty(R, use_unicode=0),
>>>     'Rotation': sympy.pretty(R, use_unicode=0),
>>>     'shEar-X': sympy.pretty(E, use_unicode=0),
>>>     'Scale': sympy.pretty(S, use_unicode=0),
>>>     'Perspective': sympy.pretty(P, use_unicode=0),
>>> }
>>> print(ub.urepr(texts, nl=2, sv=1))
>>> equation_stack = []
>>> for text, m in texts.items():
>>>     render_canvas = kwimage.draw_text_on_image(None, m, color='kw_green', fontScale=1.0)
>>>     render_canvas = kwimage.draw_header_text(render_canvas, text, color='kw_blue')
>>>     render_canvas = kwimage.imresize(render_canvas, scale=1.3)
>>>     equation_stack.append(render_canvas)
>>> equation_canvas = kwimage.stack_images(equation_stack, pad=10, axis=1, bg_value='kitware_gray')
>>> render_canvas = kwimage.draw_text_on_image(None, sympy.pretty(homog, use_unicode=0), color='kw_green', fontScale=1.0)
>>> render_canvas = kwimage.draw_header_text(render_canvas, 'Full Equation With Pre-Shift', color='kw_blue')
>>> # xdoctest: -REQUIRES(module:sympy)
>>> # xdoctest: +REQUIRES(--show)
>>> import kwplot
>>> plt = kwplot.autoplt()
>>> canvas = kwimage.stack_images([warp_canvas, equation_canvas, render_canvas], pad=20, axis=0, bg_value='kitware_gray', resize='larger')
>>> canvas = kwimage.draw_header_text(canvas, 'Projective matrixes can represent', color='kw_blue')
>>> kwplot.imshow(canvas)
>>> fig = plt.gcf()
>>> fig.set_size_inches(13, 13)
_images/fig_kwimage_transform_Projective_002.jpeg
classmethod fit(pts1, pts2)[source]

Fit an projective transformation between a set of corresponding points.

See [HomogEst] [SzeleskiBook] and [RansacDummies] for references on the subject.

Parameters:
  • pts1 (ndarray) – An Nx2 array of points in “space 1”.

  • pts2 (ndarray) – A corresponding Nx2 array of points in “space 2”

Returns:

a transform that warps from “space1” to “space2”.

Return type:

Projective

Note

A projective matrix has 8 degrees of freedome, so at least 8 point pairs are needed.

References

Example

>>> # Create a set of points, warp them, then recover the warp
>>> import kwimage
>>> points = kwimage.Points.random(9).scale(64)
>>> A1 = kwimage.Affine.affine(scale=0.9, theta=-3.2, offset=(2, 3), about=(32, 32), skew=2.3)
>>> A2 = kwimage.Affine.affine(scale=0.8, theta=0.8, offset=(2, 0), about=(32, 32))
>>> A12_real = A2 @ A1.inv()
>>> points1 = points.warp(A1)
>>> points2 = points.warp(A2)
>>> # Make the correspondence non-affine
>>> points2.data['xy'].data[0, 0] += 3.5
>>> points2.data['xy'].data[3, 1] += 8.5
>>> # Recover the warp
>>> pts1, pts2 = points1.xy, points2.xy
>>> A_recovered = kwimage.Projective.fit(pts1, pts2)
>>> #assert np.all(np.isclose(A_recovered.matrix, A12_real.matrix))
>>> # xdoctest: +REQUIRES(--show)
>>> import cv2
>>> import kwplot
>>> kwplot.autompl()
>>> base1 = np.zeros((96, 96, 3))
>>> base1[32:-32, 5:-5] = 0.5
>>> base2 = np.zeros((96, 96, 3))
>>> img1 = points1.draw_on(base1, radius=3, color='blue')
>>> img2 = points2.draw_on(base2, radius=3, color='green')
>>> img1_warp = kwimage.warp_projective(img1, A_recovered.matrix, dsize=img1.shape[0:2][::-1])
>>> canvas = kwimage.stack_images([img1, img2, img1_warp], pad=10, axis=1, bg_value=(1., 1., 1.))
>>> kwplot.imshow(canvas)
_images/fig_kwimage_transform_Projective_fit_002.jpeg
classmethod projective(scale=None, offset=None, shearx=None, theta=None, uv=None, about=None)[source]

Reconstruct from parameters

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
>>> params = x0, y0, sx, sy, theta, shearx, tx, ty, u, v = sympy.symbols(
>>>     'x0, y0, sx, sy, theta, ex, tx, ty, u, v')
>>> # move the center to 0, 0
>>> tr1_ = sympy.Matrix([[1, 0,  -x0],
>>>                      [0, 1,  -y0],
>>>                      [0, 0,    1]])
>>> P = sympy.Matrix([  # projective part
>>>     [ 1,  0,  0],
>>>     [ 0,  1,  0],
>>>     [ u,  v,  1]])
>>> # Define core components of the affine transform
>>> S = sympy.Matrix([  # scale
>>>     [sx,  0, 0],
>>>     [ 0, sy, 0],
>>>     [ 0,  0, 1]])
>>> E = sympy.Matrix([  # x-shear
>>>     [1,  shearx, 0],
>>>     [0,  1, 0],
>>>     [0,  0, 1]])
>>> R = sympy.Matrix([  # rotation
>>>     [sympy.cos(theta), -sympy.sin(theta), 0],
>>>     [sympy.sin(theta),  sympy.cos(theta), 0],
>>>     [               0,                 0, 1]])
>>> T = sympy.Matrix([  # translation
>>>     [ 1,  0, tx],
>>>     [ 0,  1, ty],
>>>     [ 0,  0,  1]])
>>> # move 0, 0 back to the specified origin
>>> tr2_ = sympy.Matrix([[1, 0,  x0],
>>>                      [0, 1,  y0],
>>>                      [0, 0,   1]])
>>> # combine transformations
>>> with sympy.evaluate(False):
>>>     homog_ = sympy.MatMul(tr2_, T, R, E, S, P, tr1_)
>>>     sympy.pprint(homog_)
>>> homog = homog_.doit()
>>> sympy.pprint(homog)
>>> print('homog = {}'.format(ub.urepr(homog.tolist(), nl=1)))
classmethod coerce(data=None, **kwargs)[source]

Attempt to coerce the data into an Projective object

Parameters:
  • data – some data we attempt to coerce to an Projective matrix

  • **kwargs – some data we attempt to coerce to an Projective matrix, mutually exclusive with data.

Returns:

Projective

Example

>>> import kwimage
>>> kwimage.Projective.coerce({'type': 'affine', 'matrix': [[1, 0, 0], [0, 1, 0]]})
>>> kwimage.Projective.coerce({'type': 'affine', 'scale': 2})
>>> kwimage.Projective.coerce({'type': 'projective', 'scale': 2})
>>> kwimage.Projective.coerce({'scale': 2})
>>> kwimage.Projective.coerce({'offset': 3})
>>> kwimage.Projective.coerce(np.eye(3))
>>> kwimage.Projective.coerce(None)
>>> import skimage
>>> kwimage.Projective.coerce(skimage.transform.AffineTransform(scale=30))
>>> kwimage.Projective.coerce(skimage.transform.ProjectiveTransform(matrix=None))
is_affine()[source]

If the bottom row is [[0, 0, 1]], then this can be safely turned into an affine matrix.

Returns:

bool

Example

>>> import kwimage
>>> kwimage.Projective.coerce(scale=2, uv=[1, 1]).is_affine()
False
>>> kwimage.Projective.coerce(scale=2, uv=[0, 0]).is_affine()
True
to_skimage()[source]
Returns:

skimage.transform.AffineTransform

Example

>>> import kwimage
>>> self = kwimage.Projective.random()
>>> tf = self.to_skimage()
>>> # Transform points with kwimage and scikit-image
>>> kw_poly = kwimage.Polygon.random()
>>> kw_warp_xy = kw_poly.warp(self.matrix).exterior.data
>>> sk_warp_xy = tf(kw_poly.exterior.data)
>>> assert np.allclose(sk_warp_xy, sk_warp_xy)
classmethod random(shape=None, rng=None, **kw)[source]
Example/
>>> import kwimage
>>> self = kwimage.Projective.random()
>>> print(f'self={self}')
>>> params = self.decompose()
>>> aff_part = kwimage.Affine.affine(**ub.dict_diff(params, ['uv']))
>>> proj_part = kwimage.Projective.coerce(uv=params['uv'])
>>> # xdoctest: +REQUIRES(module:kwplot)
>>> # xdoctest: +REQUIRES(--show)
>>> import cv2
>>> import kwplot
>>> dsize = (256, 256)
>>> kwplot.autompl()
>>> img1 = kwimage.grab_test_image(dsize=dsize)
>>> img1_affonly = kwimage.warp_projective(img1, aff_part.matrix, dsize=img1.shape[0:2][::-1])
>>> img1_projonly = kwimage.warp_projective(img1, proj_part.matrix, dsize=img1.shape[0:2][::-1])
>>> ###
>>> img2 = kwimage.ensure_uint255(kwimage.atleast_3channels(kwimage.checkerboard(dsize=dsize)))
>>> img1_fullwarp = kwimage.warp_projective(img1, self.matrix, dsize=img1.shape[0:2][::-1])
>>> img2_affonly = kwimage.warp_projective(img2, aff_part.matrix, dsize=img2.shape[0:2][::-1])
>>> img2_projonly = kwimage.warp_projective(img2, proj_part.matrix, dsize=img2.shape[0:2][::-1])
>>> img2_fullwarp = kwimage.warp_projective(img2, self.matrix, dsize=img2.shape[0:2][::-1])
>>> canvas1 = kwimage.stack_images([img1, img1_projonly, img1_affonly, img1_fullwarp], pad=10, axis=1, bg_value=(0.5, 0.9, 0.1))
>>> canvas2 = kwimage.stack_images([img2, img2_projonly, img2_affonly, img2_fullwarp], pad=10, axis=1, bg_value=(0.5, 0.9, 0.1))
>>> canvas = kwimage.stack_images([canvas1, canvas2], axis=0)
>>> kwplot.imshow(canvas)
_images/fig_kwimage_transform_Projective_random_002.jpeg
decompose()[source]

Based on the analysis done in [ME1319680].

Return type:

Dict

References

Example

>>> # Create a set of points, warp them, then recover the warp
>>> import kwimage
>>> points = kwimage.Points.random(9).scale(64)
>>> A1 = kwimage.Affine.affine(scale=0.9, theta=-3.2, offset=(2, 3), about=(32, 32), skew=2.3)
>>> A2 = kwimage.Affine.affine(scale=0.8, theta=0.8, offset=(2, 0), about=(32, 32))
>>> A12_real = A2 @ A1.inv()
>>> points1 = points.warp(A1)
>>> points2 = points.warp(A2)
>>> # Make the correspondence non-affine
>>> points2.data['xy'].data[0, 0] += 3.5
>>> points2.data['xy'].data[3, 1] += 8.5
>>> # Recover the warp
>>> pts1, pts2 = points1.xy, points2.xy
>>> self = kwimage.Projective.random()
>>> self.decompose()

Example

>>> # xdoctest: +REQUIRES(module:sympy)
>>> from kwimage.transform import *  # NOQA
>>> import kwimage
>>> from kwimage.transform import _RationalNDArray
>>> self = kwimage.Projective.random().rationalize()
>>> rat_decomp = self.decompose()
>>> print('rat_decomp = {}'.format(ub.urepr(rat_decomp, nl=1)))
>>> ####
>>> import sympy
>>> cells = sympy.symbols('h1, h2, h3, h4, h5, h6, h7, h8, h9')
>>> matrix = _RationalNDArray(cells).reshape(3, 3)
>>> # Symbolic decomposition. Neat.
>>> self = kwimage.Projective(matrix)
>>> self.decompose()