| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833 |
- """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)
- 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
- 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)
- 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
- 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)
- 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
- 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])
- 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()
- 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()
- 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)
- 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
- @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
- 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
- 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
- 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
- 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
- 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
- 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)
- 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)
- 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
- 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)
- 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])
- 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)
- 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)
- 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]
- 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)
- @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)
- 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)
- 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
- )
- 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)
- 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)
- 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
- 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
- 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
- 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)
- 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
- 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
- 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)
- 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)
- 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)
- 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
- 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_()
- 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
- 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)
- 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
- 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)
- 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])
- 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
- 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)
- 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
- 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)
- 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
- 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
- @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),))
|