kwimage.transform module¶
Objects for representing and manipulating image transforms.
XDEV_PROFILE=1 xdoctest ~/code/kwimage/kwimage/transform.py
- 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)
- 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
- 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)
- 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)
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:
- 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 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:
References
[WikiConic]Example
>>> import kwimage >>> kwimage.Affine.random(rng=432).eccentricity()
- 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 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
[SE3521141][SE70357473]https://stackoverflow.com/questions/70357473/how-to-decompose-a-2x2-affine-matrix-with-sympy
[WikiTranMat][WikiShear]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:
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:
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)
- 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:
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()
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()
- 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)
- 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
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.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)
- 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()