:py:mod:`kwimage.structs.coords` ================================ .. py:module:: kwimage.structs.coords .. autoapi-nested-parse:: Coordinates the fundamental "point" datatype. They do not contain metadata, only geometry. See the `Points` data type for a structure that maintains metadata on top of coordinate data. Module Contents --------------- Classes ~~~~~~~ .. autoapisummary:: kwimage.structs.coords.Coords Attributes ~~~~~~~~~~ .. autoapisummary:: kwimage.structs.coords._HAS_IMGAUG_FLIP_BUG .. py:data:: _HAS_IMGAUG_FLIP_BUG .. py:class:: Coords(data=None, meta=None) Bases: :py:obj:`kwimage.structs._generic.Spatial`, :py:obj:`ubelt.NiceRepr` A data structure to store n-dimensional coordinate geometry. Currently it is up to the user to maintain what coordinate system this geometry belongs to. .. note:: This class was designed to hold coordinates in r/c format, but in general this class is anostic to dimension ordering as long as you are consistent. However, there are two places where this matters: (1) drawing and (2) gdal/imgaug-warping. In these places we will assume x/y for legacy reasons. This may change in the future. The term axes with resepct to ``Coords`` always refers to the final numpy axis. In other words the final numpy-axis represents ALL of the coordinate-axes. CommandLine: xdoctest -m kwimage.structs.coords Coords .. rubric:: Example >>> from kwimage.structs.coords import * # NOQA >>> import kwarray >>> rng = kwarray.ensure_rng(0) >>> self = Coords.random(num=4, dim=3, rng=rng) >>> print('self = {}'.format(self)) self = >>> matrix = rng.rand(4, 4) >>> self.warp(matrix) >>> self.translate(3, inplace=True) >>> self.translate(3, inplace=True) >>> self.scale(2) >>> # xdoctest: +REQUIRES(module:torch) >>> self.tensor() >>> self.tensor().tensor().numpy().numpy() >>> self.numpy() >>> #self.draw_on() .. py:attribute:: __repr__ .. py:method:: __nice__(self) .. py:method:: __len__(self) .. py:method:: dtype(self) :property: .. py:method:: dim(self) :property: .. py:method:: shape(self) :property: .. py:method:: copy(self) .. py:method:: random(Coords, num=1, dim=2, rng=None, meta=None) :classmethod: Makes random coordinates; typically for testing purposes .. py:method:: is_numpy(self) .. py:method:: is_tensor(self) .. py:method:: compress(self, flags, axis=0, inplace=False) Filters items based on a boolean criterion :Parameters: * **flags** (*ArrayLike[bool]*) -- true for items to be kept * **axis** (*int*) -- you usually want this to be 0 * **inplace** (*bool, default=False*) -- if True, modifies this object :returns: filtered coords :rtype: Coords .. rubric:: Example >>> from kwimage.structs.coords import * # NOQA >>> self = Coords.random(10, rng=0) >>> self.compress([True] * len(self)) >>> self.compress([False] * len(self)) >>> # xdoctest: +REQUIRES(module:torch) >>> self = self.tensor() >>> self.compress([True] * len(self)) >>> self.compress([False] * len(self)) .. py:method:: take(self, indices, axis=0, inplace=False) Takes a subset of items at specific indices :Parameters: * **indices** (*ArrayLike[int]*) -- indexes of items to take * **axis** (*int*) -- you usually want this to be 0 * **inplace** (*bool, default=False*) -- if True, modifies this object :returns: filtered coords :rtype: Coords .. rubric:: Example >>> self = Coords(np.array([[25, 30, 15, 10]])) >>> self.take([0]) >>> self.take([]) .. py:method:: astype(self, dtype, inplace=False) Changes the data type :Parameters: * **dtype** -- new type * **inplace** (*bool, default=False*) -- if True, modifies this object :returns: modified coordinates :rtype: Coords .. py:method:: round(self, inplace=False) Rounds data to the nearest integer :Parameters: **inplace** (*bool, default=False*) -- if True, modifies this object .. rubric:: Example >>> import kwimage >>> self = kwimage.Coords.random(3).scale(10) >>> self.round() .. py:method:: view(self, *shape) Passthrough method to view or reshape :Parameters: **\*shape** -- new shape of the data :returns: modified coordinates :rtype: Coords .. rubric:: Example >>> self = Coords.random(6, dim=4).numpy() >>> assert list(self.view(3, 2, 4).data.shape) == [3, 2, 4] >>> # xdoctest: +REQUIRES(module:torch) >>> self = Coords.random(6, dim=4).tensor() >>> assert list(self.view(3, 2, 4).data.shape) == [3, 2, 4] .. py:method:: concatenate(cls, coords, axis=0) :classmethod: Concatenates lists of coordinates together :Parameters: * **coords** (*Sequence[Coords]*) -- list of coords to concatenate * **axis** (*int, default=0*) -- axis to stack on :returns: stacked coords :rtype: Coords CommandLine: xdoctest -m kwimage.structs.coords Coords.concatenate .. rubric:: Example >>> coords = [Coords.random(3) for _ in range(3)] >>> new = Coords.concatenate(coords) >>> assert len(new) == 9 >>> assert np.all(new.data[3:6] == coords[1].data) .. py:method:: device(self) :property: If the backend is torch returns the data device, otherwise None .. py:method:: _impl(self) :property: Returns the internal tensor/numpy ArrayAPI implementation .. py:method:: tensor(self, device=ub.NoParam) Converts numpy to tensors. Does not change memory if possible. :returns: modified coordinates :rtype: Coords .. rubric:: Example >>> # xdoctest: +REQUIRES(module:torch) >>> self = Coords.random(3).numpy() >>> newself = self.tensor() >>> self.data[0, 0] = 0 >>> assert newself.data[0, 0] == 0 >>> self.data[0, 0] = 1 >>> assert self.data[0, 0] == 1 .. py:method:: numpy(self) Converts tensors to numpy. Does not change memory if possible. :returns: modified coordinates :rtype: Coords .. rubric:: Example >>> # xdoctest: +REQUIRES(module:torch) >>> self = Coords.random(3).tensor() >>> newself = self.numpy() >>> self.data[0, 0] = 0 >>> assert newself.data[0, 0] == 0 >>> self.data[0, 0] = 1 >>> assert self.data[0, 0] == 1 .. py:method:: reorder_axes(self, new_order, inplace=False) Change the ordering of the coordinate axes. :Parameters: * **new_order** (*Tuple[int]*) -- ``new_order[i]`` should specify which axes in the original coordinates should be mapped to the ``i-th`` position in the returned axes. * **inplace** (*bool, default=False*) -- if True, modifies data inplace :returns: modified coordinates :rtype: Coords .. note:: This is the ordering of the "columns" in final numpy axis, not the numpy axes themselves. .. rubric:: Example >>> from kwimage.structs.coords import * # NOQA >>> self = Coords(data=np.array([ >>> [7, 11], >>> [13, 17], >>> [21, 23], >>> ])) >>> new = self.reorder_axes((1, 0)) >>> print('new = {!r}'.format(new)) new = .. rubric:: Example >>> from kwimage.structs.coords import * # NOQA >>> self = Coords.random(10, rng=0) >>> new = self.reorder_axes((1, 0)) >>> # Remapping using 1, 0 reverses the axes >>> assert np.all(new.data[:, 0] == self.data[:, 1]) >>> assert np.all(new.data[:, 1] == self.data[:, 0]) >>> # Remapping using 0, 1 does nothing >>> eye = self.reorder_axes((0, 1)) >>> assert np.all(eye.data == self.data) >>> # Remapping using 0, 0, destroys the 1-th column >>> bad = self.reorder_axes((0, 0)) >>> assert np.all(bad.data[:, 0] == self.data[:, 0]) >>> assert np.all(bad.data[:, 1] == self.data[:, 0]) .. py:method:: warp(self, transform, input_dims=None, output_dims=None, inplace=False) Generalized coordinate transform. :Parameters: * **transform** (*GeometricTransform | ArrayLike | Augmenter | callable*) -- scikit-image tranform, a 3x3 transformation matrix, an imgaug Augmenter, or generic callable which transforms an NxD ndarray. * **input_dims** (*Tuple*) -- shape of the image these objects correspond to (only needed / used when transform is an imgaug augmenter) * **output_dims** (*Tuple*) -- unused in non-raster structures, only exists for compatibility. * **inplace** (*bool, default=False*) -- if True, modifies data inplace :returns: modified coordinates :rtype: Coords .. rubric:: Notes Let D = self.dims transformation matrices can be either: * (D + 1) x (D + 1) # for homog * D x D # for scale / rotate * D x (D + 1) # for affine .. rubric:: Example >>> from kwimage.structs.coords import * # NOQA >>> self = Coords.random(10, rng=0) >>> transform = skimage.transform.AffineTransform(scale=(2, 2)) >>> new = self.warp(transform) >>> assert np.all(new.data == self.scale(2).data) Doctest: >>> self = Coords.random(10, rng=0) >>> assert np.all(self.warp(np.eye(3)).data == self.data) >>> assert np.all(self.warp(np.eye(2)).data == self.data) Doctest: >>> # xdoctest: +REQUIRES(module:osgeo) >>> from osgeo import osr >>> wgs84_crs = osr.SpatialReference() >>> wgs84_crs.ImportFromEPSG(4326) >>> dst_crs = osr.SpatialReference() >>> dst_crs.ImportFromEPSG(2927) >>> transform = osr.CoordinateTransformation(wgs84_crs, dst_crs) >>> self = Coords.random(10, rng=0) >>> new = self.warp(transform) >>> assert np.all(new.data != self.data) >>> # Alternative using generic func >>> def _gdal_coord_tranform(pts): ... return np.array([transform.TransformPoint(x, y, 0)[0:2] ... for x, y in pts]) >>> alt = self.warp(_gdal_coord_tranform) >>> assert np.all(alt.data != self.data) >>> assert np.all(alt.data == new.data) Doctest: >>> # can use a generic function >>> def func(xy): ... return np.zeros_like(xy) >>> self = Coords.random(10, rng=0) >>> assert np.all(self.warp(func).data == 0) .. py:method:: _warp_imgaug(self, augmenter, input_dims, inplace=False) Warps by applying an augmenter from the imgaug library .. note:: We are assuming you are using X/Y coordinates here. :Parameters: * **augmenter** (*imgaug.augmenters.Augmenter*) * **input_dims** (*Tuple*) -- h/w of the input image * **inplace** (*bool, default=False*) -- if True, modifies data inplace CommandLine: xdoctest -m ~/code/kwimage/kwimage/structs/coords.py Coords._warp_imgaug .. rubric:: Example >>> # xdoctest: +REQUIRES(module:imgaug) >>> from kwimage.structs.coords import * # NOQA >>> import imgaug >>> input_dims = (10, 10) >>> self = Coords.random(10).scale(input_dims) >>> augmenter = imgaug.augmenters.Fliplr(p=1) >>> new = self._warp_imgaug(augmenter, input_dims) >>> # y coordinate should not change >>> assert np.allclose(self.data[:, 1], new.data[:, 1]) >>> assert np.allclose(input_dims[0] - self.data[:, 0], new.data[:, 0]) >>> # xdoc: +REQUIRES(--show) >>> import kwplot >>> kwplot.autompl() >>> kwplot.figure(fnum=1, doclf=True) >>> from matplotlib import pyplot as pl >>> ax = plt.gca() >>> ax.set_xlim(0, input_dims[0]) >>> ax.set_ylim(0, input_dims[1]) >>> self.draw(color='red', alpha=.4, radius=0.1) >>> new.draw(color='blue', alpha=.4, radius=0.1) .. rubric:: Example >>> # xdoctest: +REQUIRES(module:imgaug) >>> from kwimage.structs.coords import * # NOQA >>> import imgaug >>> input_dims = (32, 32) >>> inplace = 0 >>> self = Coords.random(1000, rng=142).scale(input_dims).scale(.8) >>> self.data = self.data.astype(np.int32).astype(np.float32) >>> augmenter = imgaug.augmenters.CropAndPad(px=(-4, 4), keep_size=1).to_deterministic() >>> new = self._warp_imgaug(augmenter, input_dims) >>> # Change should be linear >>> norm1 = (self.data - self.data.min(axis=0)) / (self.data.max(axis=0) - self.data.min(axis=0)) >>> norm2 = (new.data - new.data.min(axis=0)) / (new.data.max(axis=0) - new.data.min(axis=0)) >>> diff = norm1 - norm2 >>> assert np.allclose(diff, 0, atol=1e-6, rtol=1e-4) >>> #assert np.allclose(self.data[:, 1], new.data[:, 1]) >>> #assert np.allclose(input_dims[0] - self.data[:, 0], new.data[:, 0]) >>> # xdoc: +REQUIRES(--show) >>> import kwimage >>> im = kwimage.imresize(kwimage.grab_test_image(), dsize=input_dims[::-1]) >>> new_im = augmenter.augment_image(im) >>> import kwplot >>> plt = kwplot.autoplt() >>> kwplot.figure(fnum=1, doclf=True) >>> kwplot.imshow(im, pnum=(1, 2, 1), fnum=1) >>> self.draw(color='red', alpha=.8, radius=0.5) >>> kwplot.imshow(new_im, pnum=(1, 2, 2), fnum=1) >>> new.draw(color='blue', alpha=.8, radius=0.5, coord_axes=[1, 0]) .. py:method:: to_imgaug(self, input_dims) Translate to an imgaug object :returns: imgaug data structure :rtype: imgaug.KeypointsOnImage .. rubric:: Example >>> # xdoctest: +REQUIRES(module:imgaug) >>> from kwimage.structs.coords import * # NOQA >>> self = Coords.random(10) >>> input_dims = (10, 10) >>> kpoi = self.to_imgaug(input_dims) >>> new = Coords.from_imgaug(kpoi) >>> assert np.allclose(new.data, self.data) .. py:method:: from_imgaug(cls, kpoi) :classmethod: .. py:method:: scale(self, factor, about=None, output_dims=None, inplace=False) Scale coordinates by a factor :Parameters: * **factor** (*float or Tuple[float, float]*) -- scale factor as either a scalar or per-dimension tuple. * **about** (*Tuple | None*) -- if unspecified scales about the origin (0, 0), otherwise the rotation is about this point. * **output_dims** (*Tuple*) -- unused in non-raster spatial structures * **inplace** (*bool, default=False*) -- if True, modifies data inplace :returns: modified coordinates :rtype: Coords .. rubric:: Example >>> from kwimage.structs.coords import * # NOQA >>> self = Coords.random(10, rng=0) >>> new = self.scale(10) >>> assert new.data.max() <= 10 >>> self = Coords.random(10, rng=0) >>> self.data = (self.data * 10).astype(int) >>> new = self.scale(10) >>> assert new.data.dtype.kind == 'i' >>> new = self.scale(10.0) >>> assert new.data.dtype.kind == 'f' .. py:method:: translate(self, offset, output_dims=None, inplace=False) Shift the coordinates :Parameters: * **offset** (*float or Tuple[float]*) -- transation offset as either a scalar or a per-dimension tuple. * **output_dims** (*Tuple*) -- unused in non-raster spatial structures * **inplace** (*bool, default=False*) -- if True, modifies data inplace :returns: modified coordinates :rtype: Coords .. rubric:: Example >>> from kwimage.structs.coords import * # NOQA >>> self = Coords.random(10, dim=3, rng=0) >>> new = self.translate(10) >>> assert new.data.min() >= 10 >>> assert new.data.max() <= 11 >>> Coords.random(3, dim=3, rng=0) >>> Coords.random(3, dim=3, rng=0).translate((1, 2, 3)) .. py:method:: rotate(self, theta, about=None, output_dims=None, inplace=False) Rotate the coordinates about a point. :Parameters: * **theta** (*float*) -- rotation angle in radians * **about** (*Tuple | None*) -- if unspecified rotates about the origin (0, 0), otherwise the rotation is about this point. * **output_dims** (*Tuple*) -- unused in non-raster spatial structures * **inplace** (*bool, default=False*) -- if True, modifies data inplace :returns: modified coordinates :rtype: Coords .. todo:: - [ ] Generalized ND Rotations? .. rubric:: References https://math.stackexchange.com/questions/197772/gen-rot-matrix .. rubric:: Example >>> from kwimage.structs.coords import * # NOQA >>> self = Coords.random(10, dim=2, rng=0) >>> theta = np.pi / 2 >>> new = self.rotate(theta) >>> # Test rotate agrees with warp >>> sin_ = np.sin(theta) >>> cos_ = np.cos(theta) >>> rot_ = np.array([[cos_, -sin_], [sin_, cos_]]) >>> new2 = self.warp(rot_) >>> assert np.allclose(new.data, new2.data) >>> # >>> # Rotate about a custom point >>> theta = np.pi / 2 >>> new3 = self.rotate(theta, about=(0.5, 0.5)) >>> # >>> # Rotate about the center of mass >>> about = self.data.mean(axis=0) >>> new4 = self.rotate(theta, about=about) >>> # xdoc: +REQUIRES(--show) >>> # xdoc: +REQUIRES(module:kwplot) >>> import kwplot >>> kwplot.figure(fnum=1, doclf=True) >>> plt = kwplot.autoplt() >>> self.draw(radius=0.01, color='blue', alpha=.5, coord_axes=[1, 0], setlim='grow') >>> plt.gca().set_aspect('equal') >>> new3.draw(radius=0.01, color='red', alpha=.5, coord_axes=[1, 0], setlim='grow') .. py:method:: _rectify_about(self, about) Ensures that about returns a specified point. Allows for special keys like center to be used. .. rubric:: Example >>> from kwimage.structs.coords import * # NOQA >>> self = Coords.random(10, dim=2, rng=0) .. py:method:: fill(self, image, value, coord_axes=None, interp='bilinear') Sets sub-coordinate locations in a grid to a particular value :Parameters: **coord_axes** (*Tuple*) -- specify which image axes each coordinate dim corresponds to. For 2D images, if you are storing r/c data, set to [0,1], if you are storing x/y data, set to [1,0]. :returns: image with coordinates rasterized on it :rtype: ndarray .. py:method:: soft_fill(self, image, coord_axes=None, radius=5) Used for drawing keypoint truth in heatmaps :Parameters: **coord_axes** (*Tuple*) -- specify which image axes each coordinate dim corresponds to. For 2D images, if you are storing r/c data, set to [0,1], if you are storing x/y data, set to [1,0]. In other words the i-th entry in coord_axes specifies which row-major spatial dimension the i-th column of a coordinate corresponds to. The index is the coordinate dimension and the value is the axes dimension. :returns: image with coordinates rasterized on it :rtype: ndarray .. rubric:: References https://stackoverflow.com/questions/54726703/generating-keypoint-heatmaps-in-tensorflow .. rubric:: Example >>> from kwimage.structs.coords import * # NOQA >>> s = 64 >>> self = Coords.random(10, meta={'shape': (s, s)}).scale(s) >>> # Put points on edges to to verify "edge cases" >>> self.data[1] = [0, 0] # top left >>> self.data[2] = [s, s] # bottom right >>> self.data[3] = [0, s + 10] # bottom left >>> self.data[4] = [-3, s // 2] # middle left >>> self.data[5] = [s + 1, -1] # top right >>> # Put points in the middle to verify overlap blending >>> self.data[6] = [32.5, 32.5] # middle >>> self.data[7] = [34.5, 34.5] # middle >>> fill_value = 1 >>> coord_axes = [1, 0] >>> radius = 10 >>> image1 = np.zeros((s, s)) >>> self.soft_fill(image1, coord_axes=coord_axes, radius=radius) >>> radius = 3.0 >>> image2 = np.zeros((s, s)) >>> self.soft_fill(image2, coord_axes=coord_axes, radius=radius) >>> # xdoc: +REQUIRES(--show) >>> # xdoc: +REQUIRES(module:kwplot) >>> import kwplot >>> kwplot.autompl() >>> kwplot.imshow(image1, pnum=(1, 2, 1)) >>> kwplot.imshow(image2, pnum=(1, 2, 2)) .. py:method:: draw_on(self, image=None, fill_value=1, coord_axes=[1, 0], interp='bilinear') .. note:: unlike other methods, the defaults assume x/y internal data :Parameters: **coord_axes** (*Tuple*) -- specify which image axes each coordinate dim corresponds to. For 2D images, if you are storing r/c data, set to [0,1], if you are storing x/y data, set to [1,0]. In other words the i-th entry in coord_axes specifies which row-major spatial dimension the i-th column of a coordinate corresponds to. The index is the coordinate dimension and the value is the axes dimension. :returns: image with coordinates drawn on it :rtype: ndarray .. rubric:: Example >>> # xdoc: +REQUIRES(module:kwplot) >>> from kwimage.structs.coords import * # NOQA >>> s = 256 >>> self = Coords.random(10, meta={'shape': (s, s)}).scale(s) >>> self.data[0] = [10, 10] >>> self.data[1] = [20, 40] >>> image = np.zeros((s, s)) >>> fill_value = 1 >>> image = self.draw_on(image, fill_value, coord_axes=[1, 0], interp='bilinear') >>> # image = self.draw_on(image, fill_value, coord_axes=[0, 1], interp='nearest') >>> # image = self.draw_on(image, fill_value, coord_axes=[1, 0], interp='bilinear') >>> # image = self.draw_on(image, fill_value, coord_axes=[1, 0], interp='nearest') >>> # xdoc: +REQUIRES(--show) >>> # xdoc: +REQUIRES(module:kwplot) >>> import kwplot >>> kwplot.autompl() >>> kwplot.figure(fnum=1, doclf=True) >>> kwplot.imshow(image) >>> self.draw(radius=3, alpha=.5, coord_axes=[1, 0]) .. py:method:: draw(self, color='blue', ax=None, alpha=None, coord_axes=[1, 0], radius=1, setlim=False) .. note:: unlike other methods, the defaults assume x/y internal data :Parameters: * **setlim** (*bool*) -- if True ensures the limits of the axes contains the polygon * **coord_axes** (*Tuple*) -- specify which image axes each coordinate dim corresponds to. For 2D images, if you are storing r/c data, set to [0,1], if you are storing x/y data, set to [1,0]. :returns: drawn matplotlib objects :rtype: List[mpl.collections.PatchCollection] .. rubric:: Example >>> # xdoc: +REQUIRES(module:kwplot) >>> from kwimage.structs.coords import * # NOQA >>> self = Coords.random(10) >>> # xdoc: +REQUIRES(--show) >>> self.draw(radius=3.0, setlim=True) >>> import kwplot >>> kwplot.autompl() >>> self.draw(radius=3.0)