kwimage.transform module¶
Objects for representing and manipulating image transforms.
- 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)
- property T¶
Transpose the underlying matrix
- rationalize()[source]¶
Convert the underlying matrix to a rational type to avoid floating point errors. This does decrease efficiency.
Example
>>> # xdoctest: +REQUIRES(module:sympy) >>> import kwimage >>> self = mat = kwimage.Matrix.random((3, 3)).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)
- 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.repr2(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.repr2(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.repr2(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)
- 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
Note
A projective matrix has 8 degrees of freedome, so at least 8 point pairs are needed.
References
- HomogEst
http://dip.sun.ac.za/~stefan/TW793/attach/notes/homography_estimation.pdf
- SzeleskiBook
http://szeliski.org/Book/drafts/SzeliskiBook_20100903_draft.pdf Page 317
- RansacDummies
http://vision.ece.ucsb.edu/~zuliani/Research/RANSAC/docs/RANSAC4Dummies.pdf page 53
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)
- 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.repr2(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)
- 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()
- 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.repr2(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.repr2(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.repr2(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)
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.repr2(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
- 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.repr2(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.repr2(params, nl=1, precision=2))) params = { 'scale': 2.00, 'theta': 0.04, 'type': 'affine', }
- 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 >>> 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))
- 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
References
https://en.wikipedia.org/wiki/Conic_section https://github.com/rasterio/affine/blob/78c20a0cfbb5ec/affine/__init__.py#L368
Example
>>> import kwimage >>> kwimage.Affine.random(rng=432).eccentricity()
- 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 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
https://math.stackexchange.com/questions/612006/decompose-affine https://math.stackexchange.com/a/3521141/353527 https://stackoverflow.com/questions/70357473/how-to-decompose-a-2x2-affine-matrix-with-sympy https://en.wikipedia.org/wiki/Transformation_matrix https://en.wikipedia.org/wiki/Shear_mapping
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: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.repr2(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
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.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 >>> 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.repr2(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
Note
An affine matrix has 6 degrees of freedome, so at least 6 point pairs are needed.
References
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)