Source code for imgaug.augmentables.polys

"""Classes dealing with polygons."""
from __future__ import print_function, division, absolute_import

import traceback
import collections

import numpy as np
import scipy.spatial.distance
import six.moves as sm
import skimage.draw
import skimage.measure

from .. import imgaug as ia
from .. import random as iarandom
from .base import IAugmentable
from .utils import (normalize_shape,
                    interpolate_points,
                    _remove_out_of_image_fraction_,
                    project_coords_,
                    _normalize_shift_args)


[docs]def recover_psois_(psois, psois_orig, recoverer, random_state): """Apply a polygon recoverer to input polygons in-place. Parameters ---------- psois : list of imgaug.augmentables.polys.PolygonsOnImage or imgaug.augmentables.polys.PolygonsOnImage The possibly broken polygons, e.g. after augmentation. The `recoverer` is applied to them. psois_orig : list of imgaug.augmentables.polys.PolygonsOnImage or imgaug.augmentables.polys.PolygonsOnImage Original polygons that were later changed to `psois`. They are an extra input to `recoverer`. recoverer : imgaug.augmentables.polys._ConcavePolygonRecoverer The polygon recoverer used to repair broken input polygons. random_state : None or int or RNG or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.SeedSequence or numpy.random.RandomState An RNG to use during the polygon recovery. Returns ------- list of imgaug.augmentables.polys.PolygonsOnImage or imgaug.augmentables.polys.PolygonsOnImage List of repaired polygons. Note that this is `psois`, which was changed in-place. """ input_was_list = True if not isinstance(psois, list): input_was_list = False psois = [psois] psois_orig = [psois_orig] for i, psoi in enumerate(psois): for j, polygon in enumerate(psoi.polygons): poly_rec = recoverer.recover_from( polygon.exterior, psois_orig[i].polygons[j], random_state) # Don't write into `polygon.exterior[...] = ...` because the # shapes might have changed. We could also first check if the # shapes are identical and only then write in-place, but as the # array for `poly_rec.exterior` was already created, that would # not provide any benefits. polygon.exterior = poly_rec.exterior if not input_was_list: return psois[0] return psois
# TODO somehow merge with BoundingBox # TODO add functions: simplify() (eg via shapely.ops.simplify()), # extend(all_sides=0, top=0, right=0, bottom=0, left=0), # intersection(other, default=None), union(other), iou(other), to_heatmap, to_mask
[docs]class Polygon(object): """Class representing polygons. Each polygon is parameterized by its corner points, given as absolute x- and y-coordinates with sub-pixel accuracy. Parameters ---------- exterior : list of imgaug.augmentables.kps.Keypoint or list of tuple of float or (N,2) ndarray List of points defining the polygon. May be either a ``list`` of :class:`~imgaug.augmentables.kps.Keypoint` objects or a ``list`` of ``tuple`` s in xy-form or a numpy array of shape (N,2) for ``N`` points in xy-form. All coordinates are expected to be the absolute subpixel-coordinates on the image, given as ``float`` s, e.g. ``x=10.7`` and ``y=3.4`` for a point at coordinates ``(10.7, 3.4)``. Their order is expected to be clock-wise. They are expected to not be closed (i.e. first and last coordinate differ). label : None or str, optional Label of the polygon, e.g. a string representing the class. """ def __init__(self, exterior, label=None): """Create a new Polygon instance.""" # TODO get rid of this deferred import from imgaug.augmentables.kps import Keypoint if isinstance(exterior, list): if not exterior: # for empty lists, make sure that the shape is (0, 2) and # not (0,) as that is also expected when the input is a numpy # array self.exterior = np.zeros((0, 2), dtype=np.float32) elif isinstance(exterior[0], Keypoint): # list of Keypoint self.exterior = np.float32([[point.x, point.y] for point in exterior]) else: # list of tuples (x, y) # TODO just np.float32(exterior) here? self.exterior = np.float32([[point[0], point[1]] for point in exterior]) else: assert ia.is_np_array(exterior), ( "Expected exterior to be a list of tuples (x, y) or " "an (N, 2) array, got type %s" % (exterior,)) assert exterior.ndim == 2 and exterior.shape[1] == 2, ( "Expected exterior to be a list of tuples (x, y) or " "an (N, 2) array, got an array of shape %s" % ( exterior.shape,)) # TODO deal with int inputs here? self.exterior = np.float32(exterior) # Remove last point if it is essentially the same as the first # point (polygons are always assumed to be closed anyways). This also # prevents problems with shapely, which seems to add the last point # automatically. is_closed = ( len(self.exterior) >= 2 and np.allclose(self.exterior[0, :], self.exterior[-1, :])) if is_closed: self.exterior = self.exterior[:-1] self.label = label @property def coords(self): """Alias for attribute ``exterior``. Added in 0.4.0. Returns ------- ndarray An ``(N, 2)`` ``float32`` ndarray containing the coordinates of this polygon. This identical to the attribute ``exterior``. """ return self.exterior @property def xx(self): """Get the x-coordinates of all points on the exterior. Returns ------- (N,2) ndarray ``float32`` x-coordinates array of all points on the exterior. """ return self.exterior[:, 0] @property def yy(self): """Get the y-coordinates of all points on the exterior. Returns ------- (N,2) ndarray ``float32`` y-coordinates array of all points on the exterior. """ return self.exterior[:, 1] @property def xx_int(self): """Get the discretized x-coordinates of all points on the exterior. The conversion from ``float32`` coordinates to ``int32`` is done by first rounding the coordinates to the closest integer and then removing everything after the decimal point. Returns ------- (N,2) ndarray ``int32`` x-coordinates of all points on the exterior. """ return np.int32(np.round(self.xx)) @property def yy_int(self): """Get the discretized y-coordinates of all points on the exterior. The conversion from ``float32`` coordinates to ``int32`` is done by first rounding the coordinates to the closest integer and then removing everything after the decimal point. Returns ------- (N,2) ndarray ``int32`` y-coordinates of all points on the exterior. """ return np.int32(np.round(self.yy)) @property def is_valid(self): """Estimate whether the polygon has a valid geometry. To to be considered valid, the polygon must be made up of at least ``3`` points and have a concave shape, i.e. line segments may not intersect or overlap. Multiple consecutive points are allowed to have the same coordinates. Returns ------- bool ``True`` if polygon has at least ``3`` points and is concave, otherwise ``False``. """ if len(self.exterior) < 3: return False return self.to_shapely_polygon().is_valid @property def area(self): """Compute the area of the polygon. Returns ------- number Area of the polygon. """ if len(self.exterior) < 3: return 0.0 poly = self.to_shapely_polygon() return poly.area @property def height(self): """Compute the height of a bounding box encapsulating the polygon. The height is computed based on the two exterior coordinates with lowest and largest x-coordinates. Returns ------- number Height of the polygon. """ yy = self.yy return max(yy) - min(yy) @property def width(self): """Compute the width of a bounding box encapsulating the polygon. The width is computed based on the two exterior coordinates with lowest and largest x-coordinates. Returns ------- number Width of the polygon. """ xx = self.xx return max(xx) - min(xx)
[docs] def project_(self, from_shape, to_shape): """Project the polygon onto an image with different shape in-place. The relative coordinates of all points remain the same. E.g. a point at ``(x=20, y=20)`` on an image ``(width=100, height=200)`` will be projected on a new image ``(width=200, height=100)`` to ``(x=40, y=10)``. This is intended for cases where the original image is resized. It cannot be used for more complex changes (e.g. padding, cropping). Added in 0.4.0. Parameters ---------- from_shape : tuple of int Shape of the original image. (Before resize.) to_shape : tuple of int Shape of the new image. (After resize.) Returns ------- imgaug.augmentables.polys.Polygon Polygon object with new coordinates. The object may have been modified in-place. """ self.exterior = project_coords_(self.coords, from_shape, to_shape) return self
[docs] def project(self, from_shape, to_shape): """Project the polygon onto an image with different shape. The relative coordinates of all points remain the same. E.g. a point at ``(x=20, y=20)`` on an image ``(width=100, height=200)`` will be projected on a new image ``(width=200, height=100)`` to ``(x=40, y=10)``. This is intended for cases where the original image is resized. It cannot be used for more complex changes (e.g. padding, cropping). Parameters ---------- from_shape : tuple of int Shape of the original image. (Before resize.) to_shape : tuple of int Shape of the new image. (After resize.) Returns ------- imgaug.augmentables.polys.Polygon Polygon object with new coordinates. """ return self.deepcopy().project_(from_shape, to_shape)
[docs] def find_closest_point_index(self, x, y, return_distance=False): """Find the index of the exterior point closest to given coordinates. "Closeness" is here defined based on euclidean distance. This method will raise an ``AssertionError`` if the exterior contains no points. Parameters ---------- x : number X-coordinate around which to search for close points. y : number Y-coordinate around which to search for close points. return_distance : bool, optional Whether to also return the distance of the closest point. Returns ------- int Index of the closest point. number Euclidean distance to the closest point. This value is only returned if `return_distance` was set to ``True``. """ assert len(self.exterior) > 0, ( "Cannot find the closest point on a polygon which's exterior " "contains no points.") distances = [] for x2, y2 in self.exterior: dist = (x2 - x) ** 2 + (y2 - y) ** 2 distances.append(dist) distances = np.sqrt(distances) closest_idx = np.argmin(distances) if return_distance: return closest_idx, distances[closest_idx] return closest_idx
[docs] def compute_out_of_image_area(self, image): """Compute the area of the BB that is outside of the image plane. Added in 0.4.0. Parameters ---------- image : (H,W,...) ndarray or tuple of int Image dimensions to use. If an ``ndarray``, its shape will be used. If a ``tuple``, it is assumed to represent the image shape and must contain at least two integers. Returns ------- float Total area of the bounding box that is outside of the image plane. Can be ``0.0``. """ polys_clipped = self.clip_out_of_image(image) if len(polys_clipped) == 0: return self.area return self.area - sum([poly.area for poly in polys_clipped])
[docs] def compute_out_of_image_fraction(self, image): """Compute fraction of polygon area outside of the image plane. This estimates ``f = A_ooi / A``, where ``A_ooi`` is the area of the polygon that is outside of the image plane, while ``A`` is the total area of the bounding box. Added in 0.4.0. Parameters ---------- image : (H,W,...) ndarray or tuple of int Image dimensions to use. If an ``ndarray``, its shape will be used. If a ``tuple``, it is assumed to represent the image shape and must contain at least two integers. Returns ------- float Fraction of the polygon area that is outside of the image plane. Returns ``0.0`` if the polygon is fully inside of the image plane or has zero points. If the polygon has an area of zero, the polygon is treated similarly to a :class:`LineString`, i.e. the fraction of the line that is outside the image plane is returned. """ area = self.area if area == 0: return self.to_line_string().compute_out_of_image_fraction(image) return self.compute_out_of_image_area(image) / area
# TODO keep this method? it is almost an alias for is_out_of_image()
[docs] def is_fully_within_image(self, image): """Estimate whether the polygon is fully inside an image plane. Parameters ---------- image : (H,W,...) ndarray or tuple of int Image dimensions to use. If an ``ndarray``, its shape will be used. If a ``tuple``, it is assumed to represent the image shape and must contain at least two ``int`` s. Returns ------- bool ``True`` if the polygon is fully inside the image area. ``False`` otherwise. """ return not self.is_out_of_image(image, fully=True, partly=True)
# TODO keep this method? it is almost an alias for is_out_of_image()
[docs] def is_partly_within_image(self, image): """Estimate whether the polygon is at least partially inside an image. Parameters ---------- image : (H,W,...) ndarray or tuple of int Image dimensions to use. If an ``ndarray``, its shape will be used. If a ``tuple``, it is assumed to represent the image shape and must contain at least two ``int`` s. Returns ------- bool ``True`` if the polygon is at least partially inside the image area. ``False`` otherwise. """ return not self.is_out_of_image(image, fully=True, partly=False)
[docs] def is_out_of_image(self, image, fully=True, partly=False): """Estimate whether the polygon is partially/fully outside of an image. Parameters ---------- image : (H,W,...) ndarray or tuple of int Image dimensions to use. If an ``ndarray``, its shape will be used. If a ``tuple``, it is assumed to represent the image shape and must contain at least two ``int`` s. fully : bool, optional Whether to return ``True`` if the polygon is fully outside of the image area. partly : bool, optional Whether to return ``True`` if the polygon is at least partially outside fo the image area. Returns ------- bool ``True`` if the polygon is partially/fully outside of the image area, depending on defined parameters. ``False`` otherwise. """ # TODO this is inconsistent with line strings, which return a default # value in these cases if len(self.exterior) == 0: raise Exception("Cannot determine whether the polygon is inside " "the image, because it contains no points.") # The line string is identical to the edge of the polygon. # If the edge is fully inside the image, we know that the polygon must # be fully inside the image. # If the edge is partially outside of the image, we know that the # polygon is partially outside of the image. # Only if the edge is fully outside of the image we cannot be sure if # the polygon's inner area overlaps with the image (e.g. if the # polygon contains the whole image in it). ls = self.to_line_string() if ls.is_fully_within_image(image): return False if ls.is_out_of_image(image, fully=False, partly=True): return partly # LS is fully outside of the image. Estimate whether there is any # intersection with the image plane. If so, we know that there is # partial overlap (full overlap would mean that the LS was fully inside # the image). polys = self.clip_out_of_image(image) if len(polys) > 0: return partly return fully
[docs] @ia.deprecated(alt_func="Polygon.clip_out_of_image()", comment="clip_out_of_image() has the exactly same " "interface.") def cut_out_of_image(self, image): """Cut off all parts of the polygon that are outside of an image.""" return self.clip_out_of_image(image)
# TODO this currently can mess up the order of points - change somehow to # keep the order
[docs] def clip_out_of_image(self, image): """Cut off all parts of the polygon that are outside of an image. This operation may lead to new points being created. As a single polygon may be split into multiple new polygons, the result is always a list, which may contain more than one output polygon. This operation will return an empty list if the polygon is completely outside of the image plane. Parameters ---------- image : (H,W,...) ndarray or tuple of int Image dimensions to use for the clipping of the polygon. If an ``ndarray``, its shape will be used. If a ``tuple``, it is assumed to represent the image shape and must contain at least two ``int`` s. Returns ------- list of imgaug.augmentables.polys.Polygon Polygon, clipped to fall within the image dimensions. Returned as a ``list``, because the clipping can split the polygon into multiple parts. The list may also be empty, if the polygon was fully outside of the image plane. """ # load shapely lazily, which makes the dependency more optional import shapely.geometry # Shapely polygon conversion requires at least 3 coordinates if len(self.exterior) == 0: return [] if len(self.exterior) in [1, 2]: ls = self.to_line_string(closed=False) ls_clipped = ls.clip_out_of_image(image) assert len(ls_clipped) <= 1 if len(ls_clipped) == 0: return [] return [self.deepcopy(exterior=ls_clipped[0].coords)] h, w = image.shape[0:2] if ia.is_np_array(image) else image[0:2] poly_shapely = self.to_shapely_polygon() poly_image = shapely.geometry.Polygon([(0, 0), (w, 0), (w, h), (0, h)]) multipoly_inter_shapely = poly_shapely.intersection(poly_image) ignore_types = (shapely.geometry.LineString, shapely.geometry.MultiLineString, shapely.geometry.point.Point, shapely.geometry.MultiPoint) if isinstance(multipoly_inter_shapely, shapely.geometry.Polygon): multipoly_inter_shapely = shapely.geometry.MultiPolygon( [multipoly_inter_shapely]) elif isinstance(multipoly_inter_shapely, shapely.geometry.MultiPolygon): # we got a multipolygon from shapely, no need to change anything # anymore pass elif isinstance(multipoly_inter_shapely, ignore_types): # polygons that become (one or more) lines/points after clipping # are here ignored multipoly_inter_shapely = shapely.geometry.MultiPolygon([]) elif isinstance(multipoly_inter_shapely, shapely.geometry.GeometryCollection): # Shapely returns GEOMETRYCOLLECTION EMPTY if there is nothing # remaining after the clip. assert multipoly_inter_shapely.is_empty return [] else: print(multipoly_inter_shapely, image, self.exterior) raise Exception( "Got an unexpected result of type %s from Shapely for " "image (%d, %d) and polygon %s. This is an internal error. " "Please report." % ( type(multipoly_inter_shapely), h, w, self.exterior) ) polygons = [] for poly_inter_shapely in multipoly_inter_shapely.geoms: polygons.append(Polygon.from_shapely(poly_inter_shapely, label=self.label)) # Shapely changes the order of points, we try here to preserve it as # much as possible. # Note here, that all points of the new polygon might have high # distance to the points on the old polygon. This can happen if the # polygon overlaps with the image plane, but all of its points are # outside of the image plane. The new polygon will not be made up of # any of the old points. polygons_reordered = [] for polygon in polygons: best_idx = None best_dist = None for x, y in self.exterior: point_idx, dist = polygon.find_closest_point_index( x=x, y=y, return_distance=True) if best_idx is None or dist < best_dist: best_idx = point_idx best_dist = dist if best_idx is not None: polygon_reordered = \ polygon.change_first_point_by_index(best_idx) polygons_reordered.append(polygon_reordered) return polygons_reordered
[docs] def shift_(self, x=0, y=0): """Move this polygon along the x/y-axis in-place. The origin ``(0, 0)`` is at the top left of the image. Added in 0.4.0. Parameters ---------- x : number, optional Value to be added to all x-coordinates. Positive values shift towards the right images. y : number, optional Value to be added to all y-coordinates. Positive values shift towards the bottom images. Returns ------- imgaug.augmentables.polys.Polygon Shifted polygon. The object may have been modified in-place. """ self.exterior[:, 0] += x self.exterior[:, 1] += y return self
[docs] def shift(self, x=0, y=0, top=None, right=None, bottom=None, left=None): """Move this polygon along the x/y-axis. The origin ``(0, 0)`` is at the top left of the image. Parameters ---------- x : number, optional Value to be added to all x-coordinates. Positive values shift towards the right images. y : number, optional Value to be added to all y-coordinates. Positive values shift towards the bottom images. top : None or int, optional Deprecated since 0.4.0. Amount of pixels by which to shift this object *from* the top (towards the bottom). right : None or int, optional Deprecated since 0.4.0. Amount of pixels by which to shift this object *from* the right (towards the left). bottom : None or int, optional Deprecated since 0.4.0. Amount of pixels by which to shift this object *from* the bottom (towards the top). left : None or int, optional Deprecated since 0.4.0. Amount of pixels by which to shift this object *from* the left (towards the right). Returns ------- imgaug.augmentables.polys.Polygon Shifted polygon. """ x, y = _normalize_shift_args( x, y, top=top, right=right, bottom=bottom, left=left) return self.deepcopy().shift_(x=x, y=y)
# TODO separate this into draw_face_on_image() and draw_border_on_image() # TODO add tests for line thickness
[docs] def draw_on_image(self, image, color=(0, 255, 0), color_face=None, color_lines=None, color_points=None, alpha=1.0, alpha_face=None, alpha_lines=None, alpha_points=None, size=1, size_lines=None, size_points=None, raise_if_out_of_image=False): """Draw the polygon on an image. Parameters ---------- image : (H,W,C) ndarray The image onto which to draw the polygon. Usually expected to be of dtype ``uint8``, though other dtypes are also handled. color : iterable of int, optional The color to use for the whole polygon. Must correspond to the channel layout of the image. Usually RGB. The values for `color_face`, `color_lines` and `color_points` will be derived from this color if they are set to ``None``. This argument has no effect if `color_face`, `color_lines` and `color_points` are all set anything other than ``None``. color_face : None or iterable of int, optional The color to use for the inner polygon area (excluding perimeter). Must correspond to the channel layout of the image. Usually RGB. If this is ``None``, it will be derived from ``color * 1.0``. color_lines : None or iterable of int, optional The color to use for the line (aka perimeter/border) of the polygon. Must correspond to the channel layout of the image. Usually RGB. If this is ``None``, it will be derived from ``color * 0.5``. color_points : None or iterable of int, optional The color to use for the corner points of the polygon. Must correspond to the channel layout of the image. Usually RGB. If this is ``None``, it will be derived from ``color * 0.5``. alpha : float, optional The opacity of the whole polygon, where ``1.0`` denotes a completely visible polygon and ``0.0`` an invisible one. The values for `alpha_face`, `alpha_lines` and `alpha_points` will be derived from this alpha value if they are set to ``None``. This argument has no effect if `alpha_face`, `alpha_lines` and `alpha_points` are all set anything other than ``None``. alpha_face : None or number, optional The opacity of the polygon's inner area (excluding the perimeter), where ``1.0`` denotes a completely visible inner area and ``0.0`` an invisible one. If this is ``None``, it will be derived from ``alpha * 0.5``. alpha_lines : None or number, optional The opacity of the polygon's line (aka perimeter/border), where ``1.0`` denotes a completely visible line and ``0.0`` an invisible one. If this is ``None``, it will be derived from ``alpha * 1.0``. alpha_points : None or number, optional The opacity of the polygon's corner points, where ``1.0`` denotes completely visible corners and ``0.0`` invisible ones. If this is ``None``, it will be derived from ``alpha * 1.0``. size : int, optional Size of the polygon. The sizes of the line and points are derived from this value, unless they are set. size_lines : None or int, optional Thickness of the polygon's line (aka perimeter/border). If ``None``, this value is derived from `size`. size_points : int, optional Size of the points in pixels. If ``None``, this value is derived from ``3 * size``. raise_if_out_of_image : bool, optional Whether to raise an error if the polygon is fully outside of the image. If set to ``False``, no error will be raised and only the parts inside the image will be drawn. Returns ------- (H,W,C) ndarray Image with the polygon drawn on it. Result dtype is the same as the input dtype. """ # pylint: disable=invalid-name def _assert_not_none(arg_name, arg_value): assert arg_value is not None, ( "Expected '%s' to not be None, got type %s." % ( arg_name, type(arg_value),)) def _default_to(var, default): if var is None: return default return var _assert_not_none("color", color) _assert_not_none("alpha", alpha) _assert_not_none("size", size) # FIXME due to the np.array(.) and the assert at ndim==2 below, this # will always fail on 2D images? color_face = _default_to(color_face, np.array(color)) color_lines = _default_to(color_lines, np.array(color) * 0.5) color_points = _default_to(color_points, np.array(color) * 0.5) alpha_face = _default_to(alpha_face, alpha * 0.5) alpha_lines = _default_to(alpha_lines, alpha) alpha_points = _default_to(alpha_points, alpha) size_lines = _default_to(size_lines, size) size_points = _default_to(size_points, size * 3) if image.ndim == 2: assert ia.is_single_number(color_face), ( "Got a 2D image. Expected then 'color_face' to be a single " "number, but got %s." % (str(color_face),)) color_face = [color_face] elif image.ndim == 3 and ia.is_single_number(color_face): color_face = [color_face] * image.shape[-1] if alpha_face < 0.01: alpha_face = 0 elif alpha_face > 0.99: alpha_face = 1 if raise_if_out_of_image and self.is_out_of_image(image): raise Exception("Cannot draw polygon %s on image with " "shape %s." % (str(self), image.shape)) # TODO np.clip to image plane if is_fully_within_image(), similar to # how it is done for bounding boxes # TODO improve efficiency by only drawing in rectangle that covers # poly instead of drawing in the whole image # TODO for a rectangular polygon, the face coordinates include the # top/left boundary but not the right/bottom boundary. This may # be unintuitive when not drawing the boundary. Maybe somehow # remove the boundary coordinates from the face coordinates after # generating both? input_dtype = image.dtype result = image.astype(np.float32) rr, cc = skimage.draw.polygon( self.yy_int, self.xx_int, shape=image.shape) if len(rr) > 0: if alpha_face == 1: result[rr, cc] = np.float32(color_face) elif alpha_face == 0: pass else: result[rr, cc] = ( (1 - alpha_face) * result[rr, cc, :] + alpha_face * np.float32(color_face) ) ls_open = self.to_line_string(closed=False) ls_closed = self.to_line_string(closed=True) result = ls_closed.draw_lines_on_image( result, color=color_lines, alpha=alpha_lines, size=size_lines, raise_if_out_of_image=raise_if_out_of_image) result = ls_open.draw_points_on_image( result, color=color_points, alpha=alpha_points, size=size_points, raise_if_out_of_image=raise_if_out_of_image) if input_dtype.type == np.uint8: # TODO make clipping more flexible result = np.clip(np.round(result), 0, 255).astype(input_dtype) else: result = result.astype(input_dtype) return result
# TODO add pad, similar to LineStrings # TODO add pad_max, similar to LineStrings # TODO add prevent_zero_size, similar to LineStrings
[docs] def extract_from_image(self, image): """Extract all image pixels within the polygon area. This method returns a rectangular image array. All pixels within that rectangle that do not belong to the polygon area will be filled with zeros (i.e. they will be black). The method will also zero-pad the image if the polygon is partially/fully outside of the image. Parameters ---------- image : (H,W) ndarray or (H,W,C) ndarray The image from which to extract the pixels within the polygon. Returns ------- (H',W') ndarray or (H',W',C) ndarray Pixels within the polygon. Zero-padded if the polygon is partially/fully outside of the image. """ assert image.ndim in [2, 3], ( "Expected image of shape (H,W,[C]), got shape %s." % ( image.shape,)) if len(self.exterior) <= 2: raise Exception("Polygon must be made up of at least 3 points to " "extract its area from an image.") bb = self.to_bounding_box() bb_area = bb.extract_from_image(image) if self.is_out_of_image(image, fully=True, partly=False): return bb_area xx = self.xx_int yy = self.yy_int xx_mask = xx - np.min(xx) yy_mask = yy - np.min(yy) height_mask = np.max(yy_mask) width_mask = np.max(xx_mask) rr_face, cc_face = skimage.draw.polygon( yy_mask, xx_mask, shape=(height_mask, width_mask)) mask = np.zeros((height_mask, width_mask), dtype=np.bool) mask[rr_face, cc_face] = True if image.ndim == 3: mask = np.tile(mask[:, :, np.newaxis], (1, 1, image.shape[2])) return bb_area * mask
[docs] def change_first_point_by_coords(self, x, y, max_distance=1e-4, raise_if_too_far_away=True): """ Reorder exterior points so that the point closest to given x/y is first. This method takes a given ``(x,y)`` coordinate, finds the closest corner point on the exterior and reorders all exterior corner points so that the found point becomes the first one in the array. If no matching points are found, an exception is raised. Parameters ---------- x : number X-coordinate of the point. y : number Y-coordinate of the point. max_distance : None or number, optional Maximum distance past which possible matches are ignored. If ``None`` the distance limit is deactivated. raise_if_too_far_away : bool, optional Whether to raise an exception if the closest found point is too far away (``True``) or simply return an unchanged copy if this object (``False``). Returns ------- imgaug.augmentables.polys.Polygon Copy of this polygon with the new point order. """ if len(self.exterior) == 0: raise Exception("Cannot reorder polygon points, because it " "contains no points.") closest_idx, closest_dist = self.find_closest_point_index( x=x, y=y, return_distance=True) if max_distance is not None and closest_dist > max_distance: if not raise_if_too_far_away: return self.deepcopy() closest_point = self.exterior[closest_idx, :] raise Exception( "Closest found point (%.9f, %.9f) exceeds max_distance of " "%.9f exceeded" % ( closest_point[0], closest_point[1], closest_dist)) return self.change_first_point_by_index(closest_idx)
[docs] def change_first_point_by_index(self, point_idx): """ Reorder exterior points so that the point with given index is first. This method takes a given index and reorders all exterior corner points so that the point with that index becomes the first one in the array. An ``AssertionError`` will be raised if the index does not match any exterior point's index or the exterior does not contain any points. Parameters ---------- point_idx : int Index of the desired starting point. Returns ------- imgaug.augmentables.polys.Polygon Copy of this polygon with the new point order. """ assert 0 <= point_idx < len(self.exterior), ( "Expected index of new first point to be in the discrete interval " "[0..%d). Got index %d." % (len(self.exterior), point_idx)) if point_idx == 0: return self.deepcopy() exterior = np.concatenate( (self.exterior[point_idx:, :], self.exterior[:point_idx, :]), axis=0 ) return self.deepcopy(exterior=exterior)
[docs] def subdivide_(self, points_per_edge): """Derive a new poly with ``N`` interpolated points per edge in-place. See :func:`~imgaug.augmentables.lines.LineString.subdivide` for details. Added in 0.4.0. Parameters ---------- points_per_edge : int Number of points to interpolate on each edge. Returns ------- imgaug.augmentables.polys.Polygon Polygon with subdivided edges. The object may have been modified in-place. """ if len(self.exterior) == 1: return self ls = self.to_line_string(closed=True) ls_sub = ls.subdivide(points_per_edge) # [:-1] even works if the polygon contains zero points exterior_subdivided = ls_sub.coords[:-1] self.exterior = exterior_subdivided return self
[docs] def subdivide(self, points_per_edge): """Derive a new polygon with ``N`` interpolated points per edge. See :func:`~imgaug.augmentables.lines.LineString.subdivide` for details. Added in 0.4.0. Parameters ---------- points_per_edge : int Number of points to interpolate on each edge. Returns ------- imgaug.augmentables.polys.Polygon Polygon with subdivided edges. """ return self.deepcopy().subdivide_(points_per_edge)
[docs] def to_shapely_polygon(self): """Convert this polygon to a ``Shapely`` ``Polygon``. Returns ------- shapely.geometry.Polygon The ``Shapely`` ``Polygon`` matching this polygon's exterior. """ # load shapely lazily, which makes the dependency more optional import shapely.geometry return shapely.geometry.Polygon( [(point[0], point[1]) for point in self.exterior])
[docs] def to_shapely_line_string(self, closed=False, interpolate=0): """Convert this polygon to a ``Shapely`` ``LineString`` object. Parameters ---------- closed : bool, optional Whether to return the line string with the last point being identical to the first point. interpolate : int, optional Number of points to interpolate between any pair of two consecutive points. These points are added to the final line string. Returns ------- shapely.geometry.LineString The ``Shapely`` ``LineString`` matching the polygon's exterior. """ return _convert_points_to_shapely_line_string( self.exterior, closed=closed, interpolate=interpolate)
[docs] def to_bounding_box(self): """Convert this polygon to a bounding box containing the polygon. Returns ------- imgaug.augmentables.bbs.BoundingBox Bounding box that tightly encapsulates the polygon. """ # TODO get rid of this deferred import from imgaug.augmentables.bbs import BoundingBox xx = self.xx yy = self.yy return BoundingBox(x1=min(xx), x2=max(xx), y1=min(yy), y2=max(yy), label=self.label)
[docs] def to_keypoints(self): """Convert this polygon's exterior to ``Keypoint`` instances. Returns ------- list of imgaug.augmentables.kps.Keypoint Exterior vertices as :class:`~imgaug.augmentables.kps.Keypoint` instances. """ # TODO get rid of this deferred import from imgaug.augmentables.kps import Keypoint return [Keypoint(x=point[0], y=point[1]) for point in self.exterior]
[docs] def to_line_string(self, closed=True): """Convert this polygon's exterior to a ``LineString`` instance. Parameters ---------- closed : bool, optional Whether to close the line string, i.e. to add the first point of the `exterior` also as the last point at the end of the line string. This has no effect if the polygon has a single point or zero points. Returns ------- imgaug.augmentables.lines.LineString Exterior of the polygon as a line string. """ from imgaug.augmentables.lines import LineString if not closed or len(self.exterior) <= 1: return LineString(self.exterior, label=self.label) return LineString( np.concatenate([self.exterior, self.exterior[0:1, :]], axis=0), label=self.label)
[docs] @staticmethod def from_shapely(polygon_shapely, label=None): """Create a polygon from a ``Shapely`` ``Polygon``. .. note:: This will remove any holes in the shapely polygon. Parameters ---------- polygon_shapely : shapely.geometry.Polygon The shapely polygon. label : None or str, optional The label of the new polygon. Returns ------- imgaug.augmentables.polys.Polygon A polygon with the same exterior as the ``Shapely`` ``Polygon``. """ # load shapely lazily, which makes the dependency more optional import shapely.geometry assert isinstance(polygon_shapely, shapely.geometry.Polygon), ( "Expected the input to be a shapely.geometry.Polgon instance. " "Got %s." % (type(polygon_shapely),)) # polygon_shapely.exterior can be None if the polygon was # instantiated without points has_no_exterior = ( polygon_shapely.exterior is None or len(polygon_shapely.exterior.coords) == 0) if has_no_exterior: return Polygon([], label=label) exterior = np.float32([[x, y] for (x, y) in polygon_shapely.exterior.coords]) return Polygon(exterior, label=label)
[docs] def coords_almost_equals(self, other, max_distance=1e-4, points_per_edge=8): """Alias for :func:`Polygon.exterior_almost_equals`. Parameters ---------- other : imgaug.augmentables.polys.Polygon or (N,2) ndarray or list of tuple See :func:`~imgaug.augmentables.polys.Polygon.exterior_almost_equals`. max_distance : number, optional See :func:`~imgaug.augmentables.polys.Polygon.exterior_almost_equals`. points_per_edge : int, optional See :func:`~imgaug.augmentables.polys.Polygon.exterior_almost_equals`. Returns ------- bool Whether the two polygon's exteriors can be viewed as equal (approximate test). """ return self.exterior_almost_equals( other, max_distance=max_distance, points_per_edge=points_per_edge)
[docs] def exterior_almost_equals(self, other, max_distance=1e-4, points_per_edge=8): """Estimate if this and another polygon's exterior are almost identical. The two exteriors can have different numbers of points, but any point randomly sampled on the exterior of one polygon should be close to the closest point on the exterior of the other polygon. .. note:: This method works in an approximative way. One can come up with polygons with fairly different shapes that will still be estimated as equal by this method. In practice however this should be unlikely to be the case. The probability for something like that goes down as the interpolation parameter is increased. Parameters ---------- other : imgaug.augmentables.polys.Polygon or (N,2) ndarray or list of tuple The other polygon with which to compare the exterior. If this is an ``ndarray``, it is assumed to represent an exterior. It must then have dtype ``float32`` and shape ``(N,2)`` with the second dimension denoting xy-coordinates. If this is a ``list`` of ``tuple`` s, it is assumed to represent an exterior. Each tuple then must contain exactly two ``number`` s, denoting xy-coordinates. max_distance : number, optional The maximum euclidean distance between a point on one polygon and the closest point on the other polygon. If the distance is exceeded for any such pair, the two exteriors are not viewed as equal. The points are either the points contained in the polygon's exterior ndarray or interpolated points between these. points_per_edge : int, optional How many points to interpolate on each edge. Returns ------- bool Whether the two polygon's exteriors can be viewed as equal (approximate test). """ if isinstance(other, list): other = Polygon(np.float32(other)) elif ia.is_np_array(other): other = Polygon(other) else: assert isinstance(other, Polygon), ( "Expected 'other' to be a list of coordinates, a coordinate " "array or a single Polygon. Got type %s." % (type(other),)) return self.to_line_string(closed=True).coords_almost_equals( other.to_line_string(closed=True), max_distance=max_distance, points_per_edge=points_per_edge )
[docs] def almost_equals(self, other, max_distance=1e-4, points_per_edge=8): """ Estimate if this polygon's and another's geometry/labels are similar. This is the same as :func:`~imgaug.augmentables.polys.Polygon.exterior_almost_equals` but additionally compares the labels. Parameters ---------- other : imgaug.augmentables.polys.Polygon The other object to compare against. Expected to be a ``Polygon``. max_distance : float, optional See :func:`~imgaug.augmentables.polys.Polygon.exterior_almost_equals`. points_per_edge : int, optional See :func:`~imgaug.augmentables.polys.Polygon.exterior_almost_equals`. Returns ------- bool ``True`` if the coordinates are almost equal and additionally the labels are equal. Otherwise ``False``. """ if self.label != other.label: return False return self.exterior_almost_equals( other, max_distance=max_distance, points_per_edge=points_per_edge)
[docs] def copy(self, exterior=None, label=None): """Create a shallow copy of this object. Parameters ---------- exterior : list of imgaug.augmentables.kps.Keypoint or list of tuple or (N,2) ndarray, optional List of points defining the polygon. See :func:`~imgaug.augmentables.polys.Polygon.__init__` for details. label : None or str, optional If not ``None``, the ``label`` of the copied object will be set to this value. Returns ------- imgaug.augmentables.polys.Polygon Shallow copy. """ return self.deepcopy(exterior=exterior, label=label)
[docs] def deepcopy(self, exterior=None, label=None): """Create a deep copy of this object. Parameters ---------- exterior : list of Keypoint or list of tuple or (N,2) ndarray, optional List of points defining the polygon. See `imgaug.augmentables.polys.Polygon.__init__` for details. label : None or str If not ``None``, the ``label`` of the copied object will be set to this value. Returns ------- imgaug.augmentables.polys.Polygon Deep copy. """ return Polygon( exterior=np.copy(self.exterior) if exterior is None else exterior, label=self.label if label is None else label)
def __getitem__(self, indices): """Get the coordinate(s) with given indices. Added in 0.4.0. Returns ------- ndarray xy-coordinate(s) as ``ndarray``. """ return self.exterior[indices] def __iter__(self): """Iterate over the coordinates of this instance. Added in 0.4.0. Yields ------ ndarray An ``(2,)`` ``ndarray`` denoting an xy-coordinate pair. """ return iter(self.exterior) def __repr__(self): return self.__str__() def __str__(self): points_str = ", ".join([ "(x=%.3f, y=%.3f)" % (point[0], point[1]) for point in self.exterior]) return "Polygon([%s] (%d points), label=%s)" % ( points_str, len(self.exterior), self.label)
# TODO add tests for this
[docs]class PolygonsOnImage(IAugmentable): """Container for all polygons on a single image. Parameters ---------- polygons : list of imgaug.augmentables.polys.Polygon List of polygons on the image. shape : tuple of int or ndarray The shape of the image on which the objects are placed. Either an image with shape ``(H,W,[C])`` or a ``tuple`` denoting such an image shape. Examples -------- >>> import numpy as np >>> from imgaug.augmentables.polys import Polygon, PolygonsOnImage >>> image = np.zeros((100, 100)) >>> polys = [ >>> Polygon([(0.5, 0.5), (100.5, 0.5), (100.5, 100.5), (0.5, 100.5)]), >>> Polygon([(50.5, 0.5), (100.5, 50.5), (50.5, 100.5), (0.5, 50.5)]) >>> ] >>> polys_oi = PolygonsOnImage(polys, shape=image.shape) """ def __init__(self, polygons, shape): self.polygons = polygons self.shape = normalize_shape(shape) @property def items(self): """Get the polygons in this container. Added in 0.4.0. Returns ------- list of Polygon Polygons within this container. """ return self.polygons @items.setter def items(self, value): """Set the polygons in this container. Added in 0.4.0. Parameters ---------- value : list of Polygon Polygons within this container. """ self.polygons = value @property def empty(self): """Estimate whether this object contains zero polygons. Returns ------- bool ``True`` if this object contains zero polygons. """ return len(self.polygons) == 0
[docs] def on_(self, image): """Project all polygons from one image shape to a new one in-place. Added in 0.4.0. Parameters ---------- image : ndarray or tuple of int New image onto which the polygons are to be projected. May also simply be that new image's shape ``tuple``. Returns ------- imgaug.augmentables.polys.PolygonsOnImage Object containing all projected polygons. The object and its items may have been modified in-place. """ # pylint: disable=invalid-name on_shape = normalize_shape(image) if on_shape[0:2] == self.shape[0:2]: self.shape = on_shape # channels may differ return self for i, item in enumerate(self.items): self.polygons[i] = item.project_(self.shape, on_shape) self.shape = on_shape return self
[docs] def on(self, image): """Project all polygons from one image shape to a new one. Parameters ---------- image : ndarray or tuple of int New image onto which the polygons are to be projected. May also simply be that new image's shape ``tuple``. Returns ------- imgaug.augmentables.polys.PolygonsOnImage Object containing all projected polygons. """ # pylint: disable=invalid-name return self.deepcopy().on_(image)
[docs] def draw_on_image(self, image, color=(0, 255, 0), color_face=None, color_lines=None, color_points=None, alpha=1.0, alpha_face=None, alpha_lines=None, alpha_points=None, size=1, size_lines=None, size_points=None, raise_if_out_of_image=False): """Draw all polygons onto a given image. Parameters ---------- image : (H,W,C) ndarray The image onto which to draw the bounding boxes. This image should usually have the same shape as set in ``PolygonsOnImage.shape``. color : iterable of int, optional The color to use for the whole polygons. Must correspond to the channel layout of the image. Usually RGB. The values for `color_face`, `color_lines` and `color_points` will be derived from this color if they are set to ``None``. This argument has no effect if `color_face`, `color_lines` and `color_points` are all set anything other than ``None``. color_face : None or iterable of int, optional The color to use for the inner polygon areas (excluding perimeters). Must correspond to the channel layout of the image. Usually RGB. If this is ``None``, it will be derived from ``color * 1.0``. color_lines : None or iterable of int, optional The color to use for the lines (aka perimeters/borders) of the polygons. Must correspond to the channel layout of the image. Usually RGB. If this is ``None``, it will be derived from ``color * 0.5``. color_points : None or iterable of int, optional The color to use for the corner points of the polygons. Must correspond to the channel layout of the image. Usually RGB. If this is ``None``, it will be derived from ``color * 0.5``. alpha : float, optional The opacity of the whole polygons, where ``1.0`` denotes completely visible polygons and ``0.0`` invisible ones. The values for `alpha_face`, `alpha_lines` and `alpha_points` will be derived from this alpha value if they are set to ``None``. This argument has no effect if `alpha_face`, `alpha_lines` and `alpha_points` are all set anything other than ``None``. alpha_face : None or number, optional The opacity of the polygon's inner areas (excluding the perimeters), where ``1.0`` denotes completely visible inner areas and ``0.0`` invisible ones. If this is ``None``, it will be derived from ``alpha * 0.5``. alpha_lines : None or number, optional The opacity of the polygon's lines (aka perimeters/borders), where ``1.0`` denotes completely visible perimeters and ``0.0`` invisible ones. If this is ``None``, it will be derived from ``alpha * 1.0``. alpha_points : None or number, optional The opacity of the polygon's corner points, where ``1.0`` denotes completely visible corners and ``0.0`` invisible ones. Currently this is an on/off choice, i.e. only ``0.0`` or ``1.0`` are allowed. If this is ``None``, it will be derived from ``alpha * 1.0``. size : int, optional Size of the polygons. The sizes of the line and points are derived from this value, unless they are set. size_lines : None or int, optional Thickness of the polygon lines (aka perimeter/border). If ``None``, this value is derived from `size`. size_points : int, optional The size of all corner points. If set to ``C``, each corner point will be drawn as a square of size ``C x C``. raise_if_out_of_image : bool, optional Whether to raise an error if any polygon is fully outside of the image. If set to False, no error will be raised and only the parts inside the image will be drawn. Returns ------- (H,W,C) ndarray Image with drawn polygons. """ for poly in self.polygons: image = poly.draw_on_image( image, color=color, color_face=color_face, color_lines=color_lines, color_points=color_points, alpha=alpha, alpha_face=alpha_face, alpha_lines=alpha_lines, alpha_points=alpha_points, size=size, size_lines=size_lines, size_points=size_points, raise_if_out_of_image=raise_if_out_of_image ) return image
[docs] def remove_out_of_image_(self, fully=True, partly=False): """Remove all polygons that are fully/partially OOI in-place. 'OOI' is the abbreviation for 'out of image'. Added in 0.4.0. Parameters ---------- fully : bool, optional Whether to remove polygons that are fully outside of the image. partly : bool, optional Whether to remove polygons that are partially outside of the image. Returns ------- imgaug.augmentables.polys.PolygonsOnImage Reduced set of polygons. Those that are fully/partially outside of the given image plane are removed. The object and its items may have been modified in-place. """ self.polygons = [ poly for poly in self.polygons if not poly.is_out_of_image(self.shape, fully=fully, partly=partly) ] return self
[docs] def remove_out_of_image(self, fully=True, partly=False): """Remove all polygons that are fully/partially outside of an image. Parameters ---------- fully : bool, optional Whether to remove polygons that are fully outside of the image. partly : bool, optional Whether to remove polygons that are partially outside of the image. Returns ------- imgaug.augmentables.polys.PolygonsOnImage Reduced set of polygons. Those that are fully/partially outside of the given image plane are removed. """ return self.deepcopy().remove_out_of_image_(fully, partly)
[docs] def remove_out_of_image_fraction_(self, fraction): """Remove all Polys with an OOI fraction of ``>=fraction`` in-place. Added in 0.4.0. Parameters ---------- fraction : number Minimum out of image fraction that a polygon has to have in order to be removed. A fraction of ``1.0`` removes only polygons that are ``100%`` outside of the image. A fraction of ``0.0`` removes all polygons. Returns ------- imgaug.augmentables.polys.PolygonsOnImage Reduced set of polygons, with those that had an out of image fraction greater or equal the given one removed. The object and its items may have been modified in-place. """ return _remove_out_of_image_fraction_(self, fraction)
[docs] def remove_out_of_image_fraction(self, fraction): """Remove all Polys with an out of image fraction of ``>=fraction``. Added in 0.4.0. Parameters ---------- fraction : number Minimum out of image fraction that a polygon has to have in order to be removed. A fraction of ``1.0`` removes only polygons that are ``100%`` outside of the image. A fraction of ``0.0`` removes all polygons. Returns ------- imgaug.augmentables.polys.PolygonsOnImage Reduced set of polygons, with those that had an out of image fraction greater or equal the given one removed. """ return self.copy().remove_out_of_image_fraction_(fraction)
[docs] def clip_out_of_image_(self): """Clip off all parts from all polygons that are OOI in-place. 'OOI' is the abbreviation for 'out of image'. .. note:: The result can contain fewer polygons than the input did. That happens when a polygon is fully outside of the image plane. .. note:: The result can also contain *more* polygons than the input did. That happens when distinct parts of a polygon are only connected by areas that are outside of the image plane and hence will be clipped off, resulting in two or more unconnected polygon parts that are left in the image plane. Added in 0.4.0. Returns ------- imgaug.augmentables.polys.PolygonsOnImage Polygons, clipped to fall within the image dimensions. The count of output polygons may differ from the input count. The object and its items may have been modified in-place. """ self.polygons = [ poly_clipped for poly in self.polygons for poly_clipped in poly.clip_out_of_image(self.shape)] return self
[docs] def clip_out_of_image(self): """Clip off all parts from all polygons that are outside of an image. .. note:: The result can contain fewer polygons than the input did. That happens when a polygon is fully outside of the image plane. .. note:: The result can also contain *more* polygons than the input did. That happens when distinct parts of a polygon are only connected by areas that are outside of the image plane and hence will be clipped off, resulting in two or more unconnected polygon parts that are left in the image plane. Returns ------- imgaug.augmentables.polys.PolygonsOnImage Polygons, clipped to fall within the image dimensions. The count of output polygons may differ from the input count. """ return self.copy().clip_out_of_image_()
[docs] def shift_(self, x=0, y=0): """Move the polygons along the x/y-axis in-place. The origin ``(0, 0)`` is at the top left of the image. Added in 0.4.0. Parameters ---------- x : number, optional Value to be added to all x-coordinates. Positive values shift towards the right images. y : number, optional Value to be added to all y-coordinates. Positive values shift towards the bottom images. Returns ------- imgaug.augmentables.polys.PolygonsOnImage Shifted polygons. """ for i, poly in enumerate(self.polygons): self.polygons[i] = poly.shift_(x=x, y=y) return self
[docs] def shift(self, x=0, y=0, top=None, right=None, bottom=None, left=None): """Move the polygons along the x/y-axis. The origin ``(0, 0)`` is at the top left of the image. Parameters ---------- x : number, optional Value to be added to all x-coordinates. Positive values shift towards the right images. y : number, optional Value to be added to all y-coordinates. Positive values shift towards the bottom images. top : None or int, optional Deprecated since 0.4.0. Amount of pixels by which to shift all objects *from* the top (towards the bottom). right : None or int, optional Deprecated since 0.4.0. Amount of pixels by which to shift all objects *from* the right (towads the left). bottom : None or int, optional Deprecated since 0.4.0. Amount of pixels by which to shift all objects *from* the bottom (towards the top). left : None or int, optional Deprecated since 0.4.0. Amount of pixels by which to shift all objects *from* the left (towards the right). Returns ------- imgaug.augmentables.polys.PolygonsOnImage Shifted polygons. """ x, y = _normalize_shift_args( x, y, top=top, right=right, bottom=bottom, left=left) return self.deepcopy().shift_(x=x, y=y)
[docs] def subdivide_(self, points_per_edge): """Interpolate ``N`` points on each polygon. Added in 0.4.0. Parameters ---------- points_per_edge : int Number of points to interpolate on each edge. Returns ------- imgaug.augmentables.polys.PolygonsOnImage Subdivided polygons. """ for i, poly in enumerate(self.polygons): self.polygons[i] = poly.subdivide_(points_per_edge) return self
[docs] def subdivide(self, points_per_edge): """Interpolate ``N`` points on each polygon. Added in 0.4.0. Parameters ---------- points_per_edge : int Number of points to interpolate on each edge. Returns ------- imgaug.augmentables.polys.PolygonsOnImage Subdivided polygons. """ return self.deepcopy().subdivide_(points_per_edge)
[docs] def to_xy_array(self): """Convert all polygon coordinates to one array of shape ``(N,2)``. Added in 0.4.0. Returns ------- (N, 2) ndarray Array containing all xy-coordinates of all polygons within this instance. """ if self.empty: return np.zeros((0, 2), dtype=np.float32) return np.concatenate([poly.exterior for poly in self.polygons])
[docs] def fill_from_xy_array_(self, xy): """Modify the corner coordinates of all polygons in-place. .. note:: This currently expects that `xy` contains exactly as many coordinates as the polygons within this instance have corner points. Otherwise, an ``AssertionError`` will be raised. .. warning:: This does not validate the new coordinates or repair the resulting polygons. If bad coordinates are provided, the result will be invalid polygons (e.g. self-intersections). Added in 0.4.0. Parameters ---------- xy : (N, 2) ndarray or iterable of iterable of number XY-Coordinates of ``N`` corner points. ``N`` must match the number of corner points in all polygons within this instance. Returns ------- PolygonsOnImage This instance itself, with updated coordinates. Note that the instance was modified in-place. """ xy = np.array(xy, dtype=np.float32) # note that np.array([]) is (0,), not (0, 2) assert xy.shape[0] == 0 or (xy.ndim == 2 and xy.shape[-1] == 2), ( # pylint: disable=unsubscriptable-object "Expected input array to have shape (N,2), " "got shape %s." % (xy.shape,)) counter = 0 for poly in self.polygons: nb_points = len(poly.exterior) assert counter + nb_points <= len(xy), ( "Received fewer points than there are corner points in the " "exteriors of all polygons. Got %d points, expected %d." % ( len(xy), sum([len(p.exterior) for p in self.polygons]))) poly.exterior[:, ...] = xy[counter:counter+nb_points] counter += nb_points assert counter == len(xy), ( "Expected to get exactly as many xy-coordinates as there are " "points in the exteriors of all polygons within this instance. " "Got %d points, could only assign %d points." % ( len(xy), counter,)) return self
[docs] def to_keypoints_on_image(self): """Convert the polygons to one ``KeypointsOnImage`` instance. Added in 0.4.0. Returns ------- imgaug.augmentables.kps.KeypointsOnImage A keypoints instance containing ``N`` coordinates for a total of ``N`` points in all exteriors of the polygons within this container. Order matches the order in ``polygons``. """ from . import KeypointsOnImage if self.empty: return KeypointsOnImage([], shape=self.shape) exteriors = np.concatenate( [poly.exterior for poly in self.polygons], axis=0) return KeypointsOnImage.from_xy_array(exteriors, shape=self.shape)
[docs] def invert_to_keypoints_on_image_(self, kpsoi): """Invert the output of ``to_keypoints_on_image()`` in-place. This function writes in-place into this ``PolygonsOnImage`` instance. Added in 0.4.0. Parameters ---------- kpsoi : imgaug.augmentables.kps.KeypointsOnImages Keypoints to convert back to polygons, i.e. the outputs of ``to_keypoints_on_image()``. Returns ------- PolygonsOnImage Polygons container with updated coordinates. Note that the instance is also updated in-place. """ polys = self.polygons exteriors = [poly.exterior for poly in polys] nb_points_exp = sum([len(exterior) for exterior in exteriors]) assert len(kpsoi.keypoints) == nb_points_exp, ( "Expected %d coordinates, got %d." % ( nb_points_exp, len(kpsoi.keypoints))) xy_arr = kpsoi.to_xy_array() counter = 0 for poly in polys: exterior = poly.exterior exterior[:, :] = xy_arr[counter:counter+len(exterior), :] counter += len(exterior) self.shape = kpsoi.shape return self
[docs] def copy(self, polygons=None, shape=None): """Create a shallow copy of this object. Parameters ---------- polygons : None or list of imgaug.augmentables.polys.Polygons, optional List of polygons on the image. If not ``None``, then the ``polygons`` attribute of the copied object will be set to this value. shape : None or tuple of int or ndarray, optional The shape of the image on which the objects are placed. Either an image with shape ``(H,W,[C])`` or a tuple denoting such an image shape. If not ``None``, then the ``shape`` attribute of the copied object will be set to this value. Returns ------- imgaug.augmentables.polys.PolygonsOnImage Shallow copy. """ if polygons is None: polygons = self.polygons[:] if shape is None: # use tuple() here in case the shape was provided as a list shape = tuple(self.shape) return PolygonsOnImage(polygons, shape)
[docs] def deepcopy(self, polygons=None, shape=None): """Create a deep copy of this object. Parameters ---------- polygons : None or list of imgaug.augmentables.polys.Polygons, optional List of polygons on the image. If not ``None``, then the ``polygons`` attribute of the copied object will be set to this value. shape : None or tuple of int or ndarray, optional The shape of the image on which the objects are placed. Either an image with shape ``(H,W,[C])`` or a tuple denoting such an image shape. If not ``None``, then the ``shape`` attribute of the copied object will be set to this value. Returns ------- imgaug.augmentables.polys.PolygonsOnImage Deep copy. """ # Manual copy is far faster than deepcopy, so use manual copy here. if polygons is None: polygons = [poly.deepcopy() for poly in self.polygons] if shape is None: # use tuple() here in case the shape was provided as a list shape = tuple(self.shape) return PolygonsOnImage(polygons, shape)
def __getitem__(self, indices): """Get the polygon(s) with given indices. Added in 0.4.0. Returns ------- list of imgaug.augmentables.polys.Polygon Polygon(s) with given indices. """ return self.polygons[indices] def __iter__(self): """Iterate over the polygons in this container. Added in 0.4.0. Yields ------ Polygon A polygon in this container. The order is identical to the order in the polygon list provided upon class initialization. """ return iter(self.polygons) def __len__(self): """Get the number of items in this instance. Added in 0.4.0. Returns ------- int Number of items in this instance. """ return len(self.items) def __repr__(self): return self.__str__() def __str__(self): return "PolygonsOnImage(%s, shape=%s)" % ( str(self.polygons), self.shape)
def _convert_points_to_shapely_line_string(points, closed=False, interpolate=0): # load shapely lazily, which makes the dependency more optional import shapely.geometry if len(points) <= 1: raise Exception( "Conversion to shapely line string requires at least two points, " "but points input contains only %d points." % (len(points),)) points_tuples = [(point[0], point[1]) for point in points] # interpolate points between each consecutive pair of points if interpolate > 0: points_tuples = interpolate_points(points_tuples, interpolate) # close if requested and not yet closed # used here intentionally `points` instead of `points_tuples` if closed and len(points) > 1: points_tuples.append(points_tuples[0]) return shapely.geometry.LineString(points_tuples) class _ConcavePolygonRecoverer(object): def __init__(self, threshold_duplicate_points=1e-4, noise_strength=1e-4, oversampling=0.01, max_segment_difference=1e-4): self.threshold_duplicate_points = threshold_duplicate_points self.noise_strength = noise_strength self.oversampling = oversampling self.max_segment_difference = max_segment_difference # this limits the maximum amount of points after oversampling, i.e. # if N points are input into oversampling, then M oversampled points # are generated such that N+M <= this value self.oversample_up_to_n_points_max = 75 # ---- # parameters for _fit_best_valid_polygon() # ---- # how many changes may be done max to the initial (convex hull) polygon # before simply returning the result self.fit_n_changes_max = 100 # for how many iterations the optimization loop may run max # before simply returning the result self.fit_n_iters_max = 3 # how far (wrt. to their position in the input list) two points may be # apart max to consider adding an edge between them (in the first loop # iteration and the ones after that) self.fit_max_dist_first_iter = 1 self.fit_max_dist_other_iters = 2 # The fit loop first generates candidate edges and then modifies the # polygon based on these candidates. This limits the maximum amount # of considered candidates. If the number is less than the possible # number of candidates, they are randomly subsampled. Values beyond # 100 significantly increase runtime (for polygons that reach that # number). self.fit_n_candidates_before_sort_max = 100 # If abs(x) or abs(y) of any coordinate of a polygon is beyond this # value, no intersection points will be computed anymore. That is done, # because the underlying library to find these points uses float # values as keys and may therefore start to encounter inaccuracies # leading to exceptions within that library. self.limit_coords_values_for_inter_search = 50000 # Rounding of coordinates to use before feeding them into the # library to search for intersection points. Note that the library # was set to also use a corresponding eps of 1e-4. self.decimals = 4 def recover_from(self, new_exterior, old_polygon, random_state=0): assert isinstance(new_exterior, list) or ( ia.is_np_array(new_exterior) and new_exterior.ndim == 2 and new_exterior.shape[1] == 2), ( "Expected exterior as list or (N,2) ndarray, got type %s." % ( type(new_exterior),)) assert len(new_exterior) >= 3, \ "Cannot recover a concave polygon from less than three points." # create Polygon instance, if it is already valid then just return # immediately polygon = old_polygon.deepcopy(exterior=new_exterior) if polygon.is_valid: return polygon random_state = iarandom.RNG(random_state) rss = random_state.duplicate(3) # remove consecutive duplicate points new_exterior = self._remove_consecutive_duplicate_points(new_exterior) # check that points are not all identical or on a line new_exterior = self._fix_polygon_is_line(new_exterior, rss[0]) # jitter duplicate points new_exterior = self._jitter_duplicate_points(new_exterior, rss[1]) # generate intersection points segment_add_points = self._generate_intersection_points( new_exterior, decimals=self.decimals) # oversample points around intersections if self.oversampling is not None and self.oversampling > 0: segment_add_points = self._oversample_intersection_points( new_exterior, segment_add_points) # integrate new points into exterior new_exterior_inter = self._insert_intersection_points( new_exterior, segment_add_points) # find best fit polygon, starting from convext polygon new_exterior_concave_ids = self._fit_best_valid_polygon( new_exterior_inter, rss[2]) new_exterior_concave = [ new_exterior_inter[idx] for idx in new_exterior_concave_ids] # TODO return new_exterior_concave here instead of polygon, leave it to # caller to decide what to do with it return old_polygon.deepcopy(exterior=new_exterior_concave) def _remove_consecutive_duplicate_points(self, points): result = [] for point in points: if result: dist = np.linalg.norm( np.float32(point) - np.float32(result[-1])) is_same = (dist < self.threshold_duplicate_points) if not is_same: result.append(point) else: result.append(point) if len(result) >= 2: dist = np.linalg.norm( np.float32(result[0]) - np.float32(result[-1])) is_same = (dist < self.threshold_duplicate_points) result = result[0:-1] if is_same else result return result # fix polygons for which all points are on a line def _fix_polygon_is_line(self, exterior, random_state): assert len(exterior) >= 3, ( "Can only fix line-like polygons with an exterior containing at " "least 3 points. Got one with %d points." % (len(exterior),)) noise_strength = self.noise_strength while self._is_polygon_line(exterior): noise = random_state.uniform( -noise_strength, noise_strength, size=(len(exterior), 2) ).astype(np.float32) exterior = [(point[0] + noise_i[0], point[1] + noise_i[1]) for point, noise_i in zip(exterior, noise)] noise_strength = noise_strength * 10 assert noise_strength > 0, ( "Expected noise strength to be >0, got %.4f." % ( noise_strength,)) return exterior @classmethod def _is_polygon_line(cls, exterior): vec_down = np.float32([0, 1]) point1 = exterior[0] angles = set() for point2 in exterior[1:]: vec = np.float32(point2) - np.float32(point1) angle = ia.angle_between_vectors(vec_down, vec) angles.add(int(angle * 1000)) return len(angles) <= 1 def _jitter_duplicate_points(self, exterior, random_state): def _find_duplicates(exterior_with_duplicates): points_map = collections.defaultdict(list) for i, point in enumerate(exterior_with_duplicates): # we use 10/x here to be a bit more lenient, the precise # distance test is further below x = int(np.round(point[0] * ((1/10) / self.threshold_duplicate_points))) y = int(np.round(point[1] * ((1/10) / self.threshold_duplicate_points))) for direction0 in [-1, 0, 1]: for direction1 in [-1, 0, 1]: points_map[(x+direction0, y+direction1)].append(i) duplicates = [False] * len(exterior_with_duplicates) for key in points_map: candidates = points_map[key] for i, p0_idx in enumerate(candidates): p0_idx = candidates[i] point0 = exterior_with_duplicates[p0_idx] if duplicates[p0_idx]: continue for j in range(i+1, len(candidates)): p1_idx = candidates[j] point1 = exterior_with_duplicates[p1_idx] if duplicates[p1_idx]: continue dist = np.sqrt( (point0[0] - point1[0])**2 + (point0[1] - point1[1])**2) if dist < self.threshold_duplicate_points: duplicates[p1_idx] = True return duplicates noise_strength = self.noise_strength assert noise_strength > 0, ( "Expected noise strength to be >0, got %.4f." % (noise_strength,)) exterior = exterior[:] converged = False while not converged: duplicates = _find_duplicates(exterior) if any(duplicates): noise = random_state.uniform( -self.noise_strength, self.noise_strength, size=(len(exterior), 2) ).astype(np.float32) for i, is_duplicate in enumerate(duplicates): if is_duplicate: exterior[i] = ( exterior[i][0] + noise[i][0], exterior[i][1] + noise[i][1]) noise_strength *= 10 else: converged = True return exterior # TODO remove? @classmethod def _calculate_circumference(cls, points): assert len(points) >= 3, ( "Need at least 3 points on the exterior to compute the " "circumference. Got %d." % (len(points),)) points = np.array(points, dtype=np.float32) points_matrix = np.zeros((len(points), 4), dtype=np.float32) points_matrix[:, 0:2] = points points_matrix[0:-1, 2:4] = points_matrix[1:, 0:2] points_matrix[-1, 2:4] = points_matrix[0, 0:2] distances = np.linalg.norm( points_matrix[:, 0:2] - points_matrix[:, 2:4], axis=1) return np.sum(distances) def _generate_intersection_points(self, exterior, one_point_per_intersection=True, decimals=4): # pylint: disable=broad-except largest_value = np.max(np.abs(np.array(exterior, dtype=np.float32))) too_large_values = ( largest_value > self.limit_coords_values_for_inter_search) if too_large_values: ia.warn( "Encountered during polygon repair a polygon with extremely " "large coordinate values beyond %d. Will skip intersection " "point computation for that polygon. This avoids exceptions " "and is -- due to the extreme distortion -- likely pointless " "anyways (i.e. the polygon is already broken beyond repair). " "Try using weaker augmentation parameters to avoid such " "large coordinate values." % ( self.limit_coords_values_for_inter_search,) ) return [[] for _ in range(len(exterior))] if ia.is_np_array(exterior): exterior = list(exterior) assert isinstance(exterior, list), ( "Expected 'exterior' to be a list or a ndarray. " "Got type %s." % (type(exterior),)) assert all([len(point) == 2 for point in exterior]), ( "Expected 'exterior' to contain (x,y) coordinate pairs. " "Got lengths %s." % ( ", ".join([str(len(point)) for point in exterior]))) if len(exterior) <= 0: return [] # use (*[i][0], *[i][1]) formulation here instead of just *[i], # because this way we convert numpy arrays to tuples of floats, which # is required by isect_segments_include_segments segments = [ ( ( np.round(float(exterior[i][0]), decimals), np.round(float(exterior[i][1]), decimals) ), ( np.round(float(exterior[(i + 1) % len(exterior)][0]), decimals), np.round(float(exterior[(i + 1) % len(exterior)][1]), decimals) ) ) for i in range(len(exterior)) ] # returns [(point, [(segment_p0, segment_p1), ..]), ...] from imgaug.external.poly_point_isect_py2py3 import ( isect_segments_include_segments) try: intersections = isect_segments_include_segments(segments) except Exception as exc: # Exceptions in the segment intersection search can at least # happen due to large float coords (the library uses # floats as indices, which is bound to cause inaccuracies). # Usually such exceptions should not appear, as too large # coordinate values are already caught at the start of this # function. For the case that there are more errors, this block # will prevent a full crash. ia.warn( "Encountered exception %s during polygon repair in segment " "intersection computation. Will skip that step." % ( str(exc),)) traceback.print_exc() return [[] for _ in range(len(exterior))] # estimate to which segment the found intersection points belong segments_add_points = [[] for _ in range(len(segments))] for point, associated_segments in intersections: # the intersection point may be associated with multiple segments, # but we only want to add it once, so pick the first segment if one_point_per_intersection: associated_segments = [associated_segments[0]] for seg_inter_p0, seg_inter_p1 in associated_segments: diffs = [] dists = [] for seg_p0, seg_p1 in segments: dist_p0p0 = np.linalg.norm(seg_p0 - np.array(seg_inter_p0)) dist_p1p1 = np.linalg.norm(seg_p1 - np.array(seg_inter_p1)) dist_p0p1 = np.linalg.norm(seg_p0 - np.array(seg_inter_p1)) dist_p1p0 = np.linalg.norm(seg_p1 - np.array(seg_inter_p0)) diff = min(dist_p0p0 + dist_p1p1, dist_p0p1 + dist_p1p0) diffs.append(diff) dists.append(np.linalg.norm( (seg_p0[0] - point[0], seg_p0[1] - point[1]) )) min_diff = np.min(diffs) if min_diff < self.max_segment_difference: idx = int(np.argmin(diffs)) segments_add_points[idx].append((point, dists[idx])) else: ia.warn( "Couldn't find fitting segment in " "_generate_intersection_points(). Ignoring " "intersection point.") # sort intersection points by their distance to point 0 in each segment # (clockwise ordering, this does something only for segments with # >=2 intersection points) segment_add_points_sorted = [] for idx in range(len(segments_add_points)): points = [t[0] for t in segments_add_points[idx]] dists = [t[1] for t in segments_add_points[idx]] if len(points) < 2: segment_add_points_sorted.append(points) else: both = sorted(zip(points, dists), key=lambda t: t[1]) # keep points, drop distances segment_add_points_sorted.append([a for a, _b in both]) return segment_add_points_sorted def _oversample_intersection_points(self, exterior, segment_add_points): # segment_add_points must be sorted if self.oversampling is None or self.oversampling <= 0: return segment_add_points segment_add_points_sorted_overs = [ [] for _ in range(len(segment_add_points))] n_points = len(exterior) for i, last in enumerate(exterior): for j, p_inter in enumerate(segment_add_points[i]): direction = (p_inter[0] - last[0], p_inter[1] - last[1]) if j == 0: # previous point was non-intersection, place 1 new point oversample = [1.0 - self.oversampling] else: # previous point was intersection, place 2 new points oversample = [self.oversampling, 1.0 - self.oversampling] for dist in oversample: point_over = (last[0] + dist * direction[0], last[1] + dist * direction[1]) segment_add_points_sorted_overs[i].append(point_over) segment_add_points_sorted_overs[i].append(p_inter) last = p_inter is_last_in_group = (j == len(segment_add_points[i]) - 1) if is_last_in_group: # previous point was oversampled, next point is # non-intersection, place 1 new point between the two exterior_point = exterior[(i + 1) % len(exterior)] direction = (exterior_point[0] - last[0], exterior_point[1] - last[1]) segment_add_points_sorted_overs[i].append( (last[0] + self.oversampling * direction[0], last[1] + self.oversampling * direction[1]) ) last = segment_add_points_sorted_overs[i][-1] n_points += len(segment_add_points_sorted_overs[i]) if n_points > self.oversample_up_to_n_points_max: return segment_add_points_sorted_overs return segment_add_points_sorted_overs @classmethod def _insert_intersection_points(cls, exterior, segment_add_points): # segment_add_points must be sorted assert len(exterior) == len(segment_add_points), ( "Expected one entry in 'segment_add_points' for every point in " "the exterior. Got %d (segment_add_points) and %d (exterior) " "entries instead." % (len(segment_add_points), len(exterior))) exterior_interp = [] for i, point0 in enumerate(exterior): point0 = exterior[i] exterior_interp.append(point0) for p_inter in segment_add_points[i]: exterior_interp.append(p_inter) return exterior_interp def _fit_best_valid_polygon(self, points, random_state): if len(points) < 2: return None def _compute_distance_point_to_line(point, line_start, line_end): x_diff = line_end[0] - line_start[0] y_diff = line_end[1] - line_start[1] num = abs( y_diff*point[0] - x_diff*point[1] + line_end[0]*line_start[1] - line_end[1]*line_start[0] ) den = np.sqrt(y_diff**2 + x_diff**2) if den == 0: return np.sqrt( (point[0] - line_start[0])**2 + (point[1] - line_start[1])**2) return num / den poly = Polygon(points) if poly.is_valid: return sm.xrange(len(points)) hull = scipy.spatial.ConvexHull(points) points_kept = list(hull.vertices) points_left = [i for i in range(len(points)) if i not in points_kept] iteration = 0 n_changes = 0 converged = False while not converged: candidates = [] # estimate distance metrics for points-segment pairs: # (1) distance (in vertices) between point and segment-start-point # in original input point chain # (2) euclidean distance between point and segment/line # TODO this can be done more efficiently by caching the values and # only computing distances to segments that have changed in # the last iteration # TODO these distances are not really the best metrics here. # Something like IoU between new and old (invalid) polygon # would be better, but can probably only be computed for # pairs of valid polygons. Maybe something based on pointwise # distances, where the points are sampled on the edges (not # edge vertices themselves). Maybe something based on drawing # the perimeter on images or based on distance maps. point_kept_idx_to_pos = { point_idx: i for i, point_idx in enumerate(points_kept)} # generate all possible combinations from <points_kept> and # <points_left> combos = np.transpose([ np.tile( np.int32(points_left), len(np.int32(points_kept)) ), np.repeat( np.int32(points_kept), len(np.int32(points_left)) ) ]) combos = np.concatenate( (combos, np.zeros((combos.shape[0], 3), dtype=np.int32)), axis=1) # copy columns 0, 1 into 2, 3 so that 2 is always the lower value mask = combos[:, 0] < combos[:, 1] combos[:, 2:4] = combos[:, 0:2] combos[mask, 2] = combos[mask, 1] combos[mask, 3] = combos[mask, 0] # distance (in indices) between each pair of <point_kept> and # <point_left> combos[:, 4] = np.minimum( combos[:, 3] - combos[:, 2], len(points) - combos[:, 3] + combos[:, 2] ) # limit candidates max_dist = self.fit_max_dist_other_iters if iteration > 0: max_dist = self.fit_max_dist_first_iter candidate_rows = combos[combos[:, 4] <= max_dist] do_limit = ( self.fit_n_candidates_before_sort_max is not None and len(candidate_rows) > self.fit_n_candidates_before_sort_max) if do_limit: random_state.shuffle(candidate_rows) candidate_rows = candidate_rows[ 0:self.fit_n_candidates_before_sort_max] for row in candidate_rows: point_left_idx = row[0] point_kept_idx = row[1] in_points_kept_pos = point_kept_idx_to_pos[point_kept_idx] segment_start_idx = point_kept_idx segment_end_idx = points_kept[ (in_points_kept_pos+1) % len(points_kept)] segment_start = points[segment_start_idx] segment_end = points[segment_end_idx] if iteration == 0: dist_eucl = 0 else: dist_eucl = _compute_distance_point_to_line( points[point_left_idx], segment_start, segment_end) candidates.append( (point_left_idx, point_kept_idx, row[4], dist_eucl)) # Sort computed distances first by minimal vertex-distance (see # above, metric 1) (ASC), then by euclidean distance # (metric 2) (ASC). candidate_ids = np.arange(len(candidates)) candidate_ids = sorted( candidate_ids, key=lambda idx: (candidates[idx][2], candidates[idx][3])) if self.fit_n_changes_max is not None: candidate_ids = candidate_ids[:self.fit_n_changes_max] # Iterate over point-segment pairs in sorted order. For each such # candidate: Add the point to the already collected points, # create a polygon from that and check if the polygon is valid. # If it is, add the point to the output list and recalculate # distance metrics. If it isn't valid, proceed with the next # candidate until no more candidates are left. # # small change: this now no longer breaks upon the first found # point that leads to a valid polygon, but checks all candidates # instead is_valid = False done = set() for candidate_idx in candidate_ids: point_left_idx = candidates[candidate_idx][0] point_kept_idx = candidates[candidate_idx][1] if (point_left_idx, point_kept_idx) not in done: in_points_kept_idx = [ i for i, point_idx in enumerate(points_kept) if point_idx == point_kept_idx ][0] points_kept_hypothesis = points_kept[:] points_kept_hypothesis.insert( in_points_kept_idx+1, point_left_idx) poly_hypothesis = Polygon([ points[idx] for idx in points_kept_hypothesis]) if poly_hypothesis.is_valid: is_valid = True points_kept = points_kept_hypothesis points_left = [point_idx for point_idx in points_left if point_idx != point_left_idx] n_changes += 1 if n_changes >= self.fit_n_changes_max: return points_kept done.add((point_left_idx, point_kept_idx)) done.add((point_kept_idx, point_left_idx)) # none of the left points could be used to create a valid polygon? # (this automatically covers the case of no points being left) if not is_valid and iteration > 0: converged = True iteration += 1 has_reached_iters_max = ( self.fit_n_iters_max is not None and iteration > self.fit_n_iters_max) if has_reached_iters_max: break return points_kept # TODO remove this? was previously only used by Polygon.clip_*(), but that # doesn't use it anymore
[docs]class MultiPolygon(object): """ Class that represents several polygons. Parameters ---------- geoms : list of imgaug.augmentables.polys.Polygon List of the polygons. """ def __init__(self, geoms): """Create a new MultiPolygon instance.""" assert ( len(geoms) == 0 or all([isinstance(el, Polygon) for el in geoms])), ( "Expected 'geoms' to a list of Polygon instances. " "Got types %s." % (", ".join([str(el) for el in geoms]))) self.geoms = geoms
[docs] @staticmethod def from_shapely(geometry, label=None): """Create a MultiPolygon from a shapely object. This also creates all necessary ``Polygon`` s contained in this ``MultiPolygon``. Parameters ---------- geometry : shapely.geometry.MultiPolygon or shapely.geometry.Polygon or shapely.geometry.collection.GeometryCollection The object to convert to a MultiPolygon. label : None or str, optional A label assigned to all Polygons within the MultiPolygon. Returns ------- imgaug.augmentables.polys.MultiPolygon The derived MultiPolygon. """ # load shapely lazily, which makes the dependency more optional import shapely.geometry if isinstance(geometry, shapely.geometry.MultiPolygon): return MultiPolygon([ Polygon.from_shapely(poly, label=label) for poly in geometry.geoms]) if isinstance(geometry, shapely.geometry.Polygon): return MultiPolygon([Polygon.from_shapely(geometry, label=label)]) if isinstance(geometry, shapely.geometry.collection.GeometryCollection): assert all([ isinstance(poly, shapely.geometry.Polygon) for poly in geometry.geoms]), ( "Expected the geometry collection to only contain shapely " "polygons. Got types %s." % ( ", ".join([str(type(v)) for v in geometry.geoms]))) return MultiPolygon([ Polygon.from_shapely(poly, label=label) for poly in geometry.geoms]) raise Exception( "Unknown datatype '%s'. Expected shapely.geometry.Polygon or " "shapely.geometry.MultiPolygon or " "shapely.geometry.collections.GeometryCollection." % ( type(geometry),))