polys.py 104 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833
  1. """Classes dealing with polygons."""
  2. from __future__ import print_function, division, absolute_import
  3. import traceback
  4. import collections
  5. import numpy as np
  6. import scipy.spatial.distance
  7. import six.moves as sm
  8. import skimage.draw
  9. import skimage.measure
  10. from .. import imgaug as ia
  11. from .. import random as iarandom
  12. from .base import IAugmentable
  13. from .utils import (normalize_shape,
  14. interpolate_points,
  15. _remove_out_of_image_fraction_,
  16. project_coords_,
  17. _normalize_shift_args)
  18. def recover_psois_(psois, psois_orig, recoverer, random_state):
  19. """Apply a polygon recoverer to input polygons in-place.
  20. Parameters
  21. ----------
  22. psois : list of imgaug.augmentables.polys.PolygonsOnImage or imgaug.augmentables.polys.PolygonsOnImage
  23. The possibly broken polygons, e.g. after augmentation.
  24. The `recoverer` is applied to them.
  25. psois_orig : list of imgaug.augmentables.polys.PolygonsOnImage or imgaug.augmentables.polys.PolygonsOnImage
  26. Original polygons that were later changed to `psois`.
  27. They are an extra input to `recoverer`.
  28. recoverer : imgaug.augmentables.polys._ConcavePolygonRecoverer
  29. The polygon recoverer used to repair broken input polygons.
  30. random_state : None or int or RNG or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.SeedSequence or numpy.random.RandomState
  31. An RNG to use during the polygon recovery.
  32. Returns
  33. -------
  34. list of imgaug.augmentables.polys.PolygonsOnImage or imgaug.augmentables.polys.PolygonsOnImage
  35. List of repaired polygons. Note that this is `psois`, which was
  36. changed in-place.
  37. """
  38. input_was_list = True
  39. if not isinstance(psois, list):
  40. input_was_list = False
  41. psois = [psois]
  42. psois_orig = [psois_orig]
  43. for i, psoi in enumerate(psois):
  44. for j, polygon in enumerate(psoi.polygons):
  45. poly_rec = recoverer.recover_from(
  46. polygon.exterior, psois_orig[i].polygons[j],
  47. random_state)
  48. # Don't write into `polygon.exterior[...] = ...` because the
  49. # shapes might have changed. We could also first check if the
  50. # shapes are identical and only then write in-place, but as the
  51. # array for `poly_rec.exterior` was already created, that would
  52. # not provide any benefits.
  53. polygon.exterior = poly_rec.exterior
  54. if not input_was_list:
  55. return psois[0]
  56. return psois
  57. # TODO somehow merge with BoundingBox
  58. # TODO add functions: simplify() (eg via shapely.ops.simplify()),
  59. # extend(all_sides=0, top=0, right=0, bottom=0, left=0),
  60. # intersection(other, default=None), union(other), iou(other), to_heatmap, to_mask
  61. class Polygon(object):
  62. """Class representing polygons.
  63. Each polygon is parameterized by its corner points, given as absolute
  64. x- and y-coordinates with sub-pixel accuracy.
  65. Parameters
  66. ----------
  67. exterior : list of imgaug.augmentables.kps.Keypoint or list of tuple of float or (N,2) ndarray
  68. List of points defining the polygon. May be either a ``list`` of
  69. :class:`~imgaug.augmentables.kps.Keypoint` objects or a ``list`` of
  70. ``tuple`` s in xy-form or a numpy array of shape (N,2) for ``N``
  71. points in xy-form.
  72. All coordinates are expected to be the absolute subpixel-coordinates
  73. on the image, given as ``float`` s, e.g. ``x=10.7`` and ``y=3.4`` for a
  74. point at coordinates ``(10.7, 3.4)``. Their order is expected to be
  75. clock-wise. They are expected to not be closed (i.e. first and last
  76. coordinate differ).
  77. label : None or str, optional
  78. Label of the polygon, e.g. a string representing the class.
  79. """
  80. def __init__(self, exterior, label=None):
  81. """Create a new Polygon instance."""
  82. # TODO get rid of this deferred import
  83. from imgaug.augmentables.kps import Keypoint
  84. if isinstance(exterior, list):
  85. if not exterior:
  86. # for empty lists, make sure that the shape is (0, 2) and
  87. # not (0,) as that is also expected when the input is a numpy
  88. # array
  89. self.exterior = np.zeros((0, 2), dtype=np.float32)
  90. elif isinstance(exterior[0], Keypoint):
  91. # list of Keypoint
  92. self.exterior = np.float32([[point.x, point.y]
  93. for point in exterior])
  94. else:
  95. # list of tuples (x, y)
  96. # TODO just np.float32(exterior) here?
  97. self.exterior = np.float32([[point[0], point[1]]
  98. for point in exterior])
  99. else:
  100. assert ia.is_np_array(exterior), (
  101. "Expected exterior to be a list of tuples (x, y) or "
  102. "an (N, 2) array, got type %s" % (exterior,))
  103. assert exterior.ndim == 2 and exterior.shape[1] == 2, (
  104. "Expected exterior to be a list of tuples (x, y) or "
  105. "an (N, 2) array, got an array of shape %s" % (
  106. exterior.shape,))
  107. # TODO deal with int inputs here?
  108. self.exterior = np.float32(exterior)
  109. # Remove last point if it is essentially the same as the first
  110. # point (polygons are always assumed to be closed anyways). This also
  111. # prevents problems with shapely, which seems to add the last point
  112. # automatically.
  113. is_closed = (
  114. len(self.exterior) >= 2
  115. and np.allclose(self.exterior[0, :], self.exterior[-1, :]))
  116. if is_closed:
  117. self.exterior = self.exterior[:-1]
  118. self.label = label
  119. @property
  120. def coords(self):
  121. """Alias for attribute ``exterior``.
  122. Added in 0.4.0.
  123. Returns
  124. -------
  125. ndarray
  126. An ``(N, 2)`` ``float32`` ndarray containing the coordinates of
  127. this polygon. This identical to the attribute ``exterior``.
  128. """
  129. return self.exterior
  130. @property
  131. def xx(self):
  132. """Get the x-coordinates of all points on the exterior.
  133. Returns
  134. -------
  135. (N,2) ndarray
  136. ``float32`` x-coordinates array of all points on the exterior.
  137. """
  138. return self.exterior[:, 0]
  139. @property
  140. def yy(self):
  141. """Get the y-coordinates of all points on the exterior.
  142. Returns
  143. -------
  144. (N,2) ndarray
  145. ``float32`` y-coordinates array of all points on the exterior.
  146. """
  147. return self.exterior[:, 1]
  148. @property
  149. def xx_int(self):
  150. """Get the discretized x-coordinates of all points on the exterior.
  151. The conversion from ``float32`` coordinates to ``int32`` is done
  152. by first rounding the coordinates to the closest integer and then
  153. removing everything after the decimal point.
  154. Returns
  155. -------
  156. (N,2) ndarray
  157. ``int32`` x-coordinates of all points on the exterior.
  158. """
  159. return np.int32(np.round(self.xx))
  160. @property
  161. def yy_int(self):
  162. """Get the discretized y-coordinates of all points on the exterior.
  163. The conversion from ``float32`` coordinates to ``int32`` is done
  164. by first rounding the coordinates to the closest integer and then
  165. removing everything after the decimal point.
  166. Returns
  167. -------
  168. (N,2) ndarray
  169. ``int32`` y-coordinates of all points on the exterior.
  170. """
  171. return np.int32(np.round(self.yy))
  172. @property
  173. def is_valid(self):
  174. """Estimate whether the polygon has a valid geometry.
  175. To to be considered valid, the polygon must be made up of at
  176. least ``3`` points and have a concave shape, i.e. line segments may
  177. not intersect or overlap. Multiple consecutive points are allowed to
  178. have the same coordinates.
  179. Returns
  180. -------
  181. bool
  182. ``True`` if polygon has at least ``3`` points and is concave,
  183. otherwise ``False``.
  184. """
  185. if len(self.exterior) < 3:
  186. return False
  187. return self.to_shapely_polygon().is_valid
  188. @property
  189. def area(self):
  190. """Compute the area of the polygon.
  191. Returns
  192. -------
  193. number
  194. Area of the polygon.
  195. """
  196. if len(self.exterior) < 3:
  197. return 0.0
  198. poly = self.to_shapely_polygon()
  199. return poly.area
  200. @property
  201. def height(self):
  202. """Compute the height of a bounding box encapsulating the polygon.
  203. The height is computed based on the two exterior coordinates with
  204. lowest and largest x-coordinates.
  205. Returns
  206. -------
  207. number
  208. Height of the polygon.
  209. """
  210. yy = self.yy
  211. return max(yy) - min(yy)
  212. @property
  213. def width(self):
  214. """Compute the width of a bounding box encapsulating the polygon.
  215. The width is computed based on the two exterior coordinates with
  216. lowest and largest x-coordinates.
  217. Returns
  218. -------
  219. number
  220. Width of the polygon.
  221. """
  222. xx = self.xx
  223. return max(xx) - min(xx)
  224. def project_(self, from_shape, to_shape):
  225. """Project the polygon onto an image with different shape in-place.
  226. The relative coordinates of all points remain the same.
  227. E.g. a point at ``(x=20, y=20)`` on an image
  228. ``(width=100, height=200)`` will be projected on a new
  229. image ``(width=200, height=100)`` to ``(x=40, y=10)``.
  230. This is intended for cases where the original image is resized.
  231. It cannot be used for more complex changes (e.g. padding, cropping).
  232. Added in 0.4.0.
  233. Parameters
  234. ----------
  235. from_shape : tuple of int
  236. Shape of the original image. (Before resize.)
  237. to_shape : tuple of int
  238. Shape of the new image. (After resize.)
  239. Returns
  240. -------
  241. imgaug.augmentables.polys.Polygon
  242. Polygon object with new coordinates.
  243. The object may have been modified in-place.
  244. """
  245. self.exterior = project_coords_(self.coords, from_shape, to_shape)
  246. return self
  247. def project(self, from_shape, to_shape):
  248. """Project the polygon onto an image with different shape.
  249. The relative coordinates of all points remain the same.
  250. E.g. a point at ``(x=20, y=20)`` on an image
  251. ``(width=100, height=200)`` will be projected on a new
  252. image ``(width=200, height=100)`` to ``(x=40, y=10)``.
  253. This is intended for cases where the original image is resized.
  254. It cannot be used for more complex changes (e.g. padding, cropping).
  255. Parameters
  256. ----------
  257. from_shape : tuple of int
  258. Shape of the original image. (Before resize.)
  259. to_shape : tuple of int
  260. Shape of the new image. (After resize.)
  261. Returns
  262. -------
  263. imgaug.augmentables.polys.Polygon
  264. Polygon object with new coordinates.
  265. """
  266. return self.deepcopy().project_(from_shape, to_shape)
  267. def find_closest_point_index(self, x, y, return_distance=False):
  268. """Find the index of the exterior point closest to given coordinates.
  269. "Closeness" is here defined based on euclidean distance.
  270. This method will raise an ``AssertionError`` if the exterior contains
  271. no points.
  272. Parameters
  273. ----------
  274. x : number
  275. X-coordinate around which to search for close points.
  276. y : number
  277. Y-coordinate around which to search for close points.
  278. return_distance : bool, optional
  279. Whether to also return the distance of the closest point.
  280. Returns
  281. -------
  282. int
  283. Index of the closest point.
  284. number
  285. Euclidean distance to the closest point.
  286. This value is only returned if `return_distance` was set
  287. to ``True``.
  288. """
  289. assert len(self.exterior) > 0, (
  290. "Cannot find the closest point on a polygon which's exterior "
  291. "contains no points.")
  292. distances = []
  293. for x2, y2 in self.exterior:
  294. dist = (x2 - x) ** 2 + (y2 - y) ** 2
  295. distances.append(dist)
  296. distances = np.sqrt(distances)
  297. closest_idx = np.argmin(distances)
  298. if return_distance:
  299. return closest_idx, distances[closest_idx]
  300. return closest_idx
  301. def compute_out_of_image_area(self, image):
  302. """Compute the area of the BB that is outside of the image plane.
  303. Added in 0.4.0.
  304. Parameters
  305. ----------
  306. image : (H,W,...) ndarray or tuple of int
  307. Image dimensions to use.
  308. If an ``ndarray``, its shape will be used.
  309. If a ``tuple``, it is assumed to represent the image shape
  310. and must contain at least two integers.
  311. Returns
  312. -------
  313. float
  314. Total area of the bounding box that is outside of the image plane.
  315. Can be ``0.0``.
  316. """
  317. polys_clipped = self.clip_out_of_image(image)
  318. if len(polys_clipped) == 0:
  319. return self.area
  320. return self.area - sum([poly.area for poly in polys_clipped])
  321. def compute_out_of_image_fraction(self, image):
  322. """Compute fraction of polygon area outside of the image plane.
  323. This estimates ``f = A_ooi / A``, where ``A_ooi`` is the area of the
  324. polygon that is outside of the image plane, while ``A`` is the
  325. total area of the bounding box.
  326. Added in 0.4.0.
  327. Parameters
  328. ----------
  329. image : (H,W,...) ndarray or tuple of int
  330. Image dimensions to use.
  331. If an ``ndarray``, its shape will be used.
  332. If a ``tuple``, it is assumed to represent the image shape
  333. and must contain at least two integers.
  334. Returns
  335. -------
  336. float
  337. Fraction of the polygon area that is outside of the image
  338. plane. Returns ``0.0`` if the polygon is fully inside of
  339. the image plane or has zero points. If the polygon has an area
  340. of zero, the polygon is treated similarly to a :class:`LineString`,
  341. i.e. the fraction of the line that is outside the image plane is
  342. returned.
  343. """
  344. area = self.area
  345. if area == 0:
  346. return self.to_line_string().compute_out_of_image_fraction(image)
  347. return self.compute_out_of_image_area(image) / area
  348. # TODO keep this method? it is almost an alias for is_out_of_image()
  349. def is_fully_within_image(self, image):
  350. """Estimate whether the polygon is fully inside an image plane.
  351. Parameters
  352. ----------
  353. image : (H,W,...) ndarray or tuple of int
  354. Image dimensions to use.
  355. If an ``ndarray``, its shape will be used.
  356. If a ``tuple``, it is assumed to represent the image shape and
  357. must contain at least two ``int`` s.
  358. Returns
  359. -------
  360. bool
  361. ``True`` if the polygon is fully inside the image area.
  362. ``False`` otherwise.
  363. """
  364. return not self.is_out_of_image(image, fully=True, partly=True)
  365. # TODO keep this method? it is almost an alias for is_out_of_image()
  366. def is_partly_within_image(self, image):
  367. """Estimate whether the polygon is at least partially inside an image.
  368. Parameters
  369. ----------
  370. image : (H,W,...) ndarray or tuple of int
  371. Image dimensions to use.
  372. If an ``ndarray``, its shape will be used.
  373. If a ``tuple``, it is assumed to represent the image shape and
  374. must contain at least two ``int`` s.
  375. Returns
  376. -------
  377. bool
  378. ``True`` if the polygon is at least partially inside the image area.
  379. ``False`` otherwise.
  380. """
  381. return not self.is_out_of_image(image, fully=True, partly=False)
  382. def is_out_of_image(self, image, fully=True, partly=False):
  383. """Estimate whether the polygon is partially/fully outside of an image.
  384. Parameters
  385. ----------
  386. image : (H,W,...) ndarray or tuple of int
  387. Image dimensions to use.
  388. If an ``ndarray``, its shape will be used.
  389. If a ``tuple``, it is assumed to represent the image shape and
  390. must contain at least two ``int`` s.
  391. fully : bool, optional
  392. Whether to return ``True`` if the polygon is fully outside of the
  393. image area.
  394. partly : bool, optional
  395. Whether to return ``True`` if the polygon is at least partially
  396. outside fo the image area.
  397. Returns
  398. -------
  399. bool
  400. ``True`` if the polygon is partially/fully outside of the image
  401. area, depending on defined parameters.
  402. ``False`` otherwise.
  403. """
  404. # TODO this is inconsistent with line strings, which return a default
  405. # value in these cases
  406. if len(self.exterior) == 0:
  407. raise Exception("Cannot determine whether the polygon is inside "
  408. "the image, because it contains no points.")
  409. # The line string is identical to the edge of the polygon.
  410. # If the edge is fully inside the image, we know that the polygon must
  411. # be fully inside the image.
  412. # If the edge is partially outside of the image, we know that the
  413. # polygon is partially outside of the image.
  414. # Only if the edge is fully outside of the image we cannot be sure if
  415. # the polygon's inner area overlaps with the image (e.g. if the
  416. # polygon contains the whole image in it).
  417. ls = self.to_line_string()
  418. if ls.is_fully_within_image(image):
  419. return False
  420. if ls.is_out_of_image(image, fully=False, partly=True):
  421. return partly
  422. # LS is fully outside of the image. Estimate whether there is any
  423. # intersection with the image plane. If so, we know that there is
  424. # partial overlap (full overlap would mean that the LS was fully inside
  425. # the image).
  426. polys = self.clip_out_of_image(image)
  427. if len(polys) > 0:
  428. return partly
  429. return fully
  430. @ia.deprecated(alt_func="Polygon.clip_out_of_image()",
  431. comment="clip_out_of_image() has the exactly same "
  432. "interface.")
  433. def cut_out_of_image(self, image):
  434. """Cut off all parts of the polygon that are outside of an image."""
  435. return self.clip_out_of_image(image)
  436. # TODO this currently can mess up the order of points - change somehow to
  437. # keep the order
  438. def clip_out_of_image(self, image):
  439. """Cut off all parts of the polygon that are outside of an image.
  440. This operation may lead to new points being created.
  441. As a single polygon may be split into multiple new polygons, the result
  442. is always a list, which may contain more than one output polygon.
  443. This operation will return an empty list if the polygon is completely
  444. outside of the image plane.
  445. Parameters
  446. ----------
  447. image : (H,W,...) ndarray or tuple of int
  448. Image dimensions to use for the clipping of the polygon.
  449. If an ``ndarray``, its shape will be used.
  450. If a ``tuple``, it is assumed to represent the image shape and must
  451. contain at least two ``int`` s.
  452. Returns
  453. -------
  454. list of imgaug.augmentables.polys.Polygon
  455. Polygon, clipped to fall within the image dimensions.
  456. Returned as a ``list``, because the clipping can split the polygon
  457. into multiple parts. The list may also be empty, if the polygon was
  458. fully outside of the image plane.
  459. """
  460. # load shapely lazily, which makes the dependency more optional
  461. import shapely.geometry
  462. # Shapely polygon conversion requires at least 3 coordinates
  463. if len(self.exterior) == 0:
  464. return []
  465. if len(self.exterior) in [1, 2]:
  466. ls = self.to_line_string(closed=False)
  467. ls_clipped = ls.clip_out_of_image(image)
  468. assert len(ls_clipped) <= 1
  469. if len(ls_clipped) == 0:
  470. return []
  471. return [self.deepcopy(exterior=ls_clipped[0].coords)]
  472. h, w = image.shape[0:2] if ia.is_np_array(image) else image[0:2]
  473. poly_shapely = self.to_shapely_polygon()
  474. poly_image = shapely.geometry.Polygon([(0, 0), (w, 0), (w, h), (0, h)])
  475. multipoly_inter_shapely = poly_shapely.intersection(poly_image)
  476. ignore_types = (shapely.geometry.LineString,
  477. shapely.geometry.MultiLineString,
  478. shapely.geometry.point.Point,
  479. shapely.geometry.MultiPoint)
  480. if isinstance(multipoly_inter_shapely, shapely.geometry.Polygon):
  481. multipoly_inter_shapely = shapely.geometry.MultiPolygon(
  482. [multipoly_inter_shapely])
  483. elif isinstance(multipoly_inter_shapely,
  484. shapely.geometry.MultiPolygon):
  485. # we got a multipolygon from shapely, no need to change anything
  486. # anymore
  487. pass
  488. elif isinstance(multipoly_inter_shapely, ignore_types):
  489. # polygons that become (one or more) lines/points after clipping
  490. # are here ignored
  491. multipoly_inter_shapely = shapely.geometry.MultiPolygon([])
  492. elif isinstance(multipoly_inter_shapely,
  493. shapely.geometry.GeometryCollection):
  494. # Shapely returns GEOMETRYCOLLECTION EMPTY if there is nothing
  495. # remaining after the clip.
  496. assert multipoly_inter_shapely.is_empty
  497. return []
  498. else:
  499. print(multipoly_inter_shapely, image, self.exterior)
  500. raise Exception(
  501. "Got an unexpected result of type %s from Shapely for "
  502. "image (%d, %d) and polygon %s. This is an internal error. "
  503. "Please report." % (
  504. type(multipoly_inter_shapely), h, w, self.exterior)
  505. )
  506. polygons = []
  507. for poly_inter_shapely in multipoly_inter_shapely.geoms:
  508. polygons.append(Polygon.from_shapely(poly_inter_shapely,
  509. label=self.label))
  510. # Shapely changes the order of points, we try here to preserve it as
  511. # much as possible.
  512. # Note here, that all points of the new polygon might have high
  513. # distance to the points on the old polygon. This can happen if the
  514. # polygon overlaps with the image plane, but all of its points are
  515. # outside of the image plane. The new polygon will not be made up of
  516. # any of the old points.
  517. polygons_reordered = []
  518. for polygon in polygons:
  519. best_idx = None
  520. best_dist = None
  521. for x, y in self.exterior:
  522. point_idx, dist = polygon.find_closest_point_index(
  523. x=x, y=y, return_distance=True)
  524. if best_idx is None or dist < best_dist:
  525. best_idx = point_idx
  526. best_dist = dist
  527. if best_idx is not None:
  528. polygon_reordered = \
  529. polygon.change_first_point_by_index(best_idx)
  530. polygons_reordered.append(polygon_reordered)
  531. return polygons_reordered
  532. def shift_(self, x=0, y=0):
  533. """Move this polygon along the x/y-axis in-place.
  534. The origin ``(0, 0)`` is at the top left of the image.
  535. Added in 0.4.0.
  536. Parameters
  537. ----------
  538. x : number, optional
  539. Value to be added to all x-coordinates. Positive values shift
  540. towards the right images.
  541. y : number, optional
  542. Value to be added to all y-coordinates. Positive values shift
  543. towards the bottom images.
  544. Returns
  545. -------
  546. imgaug.augmentables.polys.Polygon
  547. Shifted polygon.
  548. The object may have been modified in-place.
  549. """
  550. self.exterior[:, 0] += x
  551. self.exterior[:, 1] += y
  552. return self
  553. def shift(self, x=0, y=0, top=None, right=None, bottom=None, left=None):
  554. """Move this polygon along the x/y-axis.
  555. The origin ``(0, 0)`` is at the top left of the image.
  556. Parameters
  557. ----------
  558. x : number, optional
  559. Value to be added to all x-coordinates. Positive values shift
  560. towards the right images.
  561. y : number, optional
  562. Value to be added to all y-coordinates. Positive values shift
  563. towards the bottom images.
  564. top : None or int, optional
  565. Deprecated since 0.4.0.
  566. Amount of pixels by which to shift this object *from* the
  567. top (towards the bottom).
  568. right : None or int, optional
  569. Deprecated since 0.4.0.
  570. Amount of pixels by which to shift this object *from* the
  571. right (towards the left).
  572. bottom : None or int, optional
  573. Deprecated since 0.4.0.
  574. Amount of pixels by which to shift this object *from* the
  575. bottom (towards the top).
  576. left : None or int, optional
  577. Deprecated since 0.4.0.
  578. Amount of pixels by which to shift this object *from* the
  579. left (towards the right).
  580. Returns
  581. -------
  582. imgaug.augmentables.polys.Polygon
  583. Shifted polygon.
  584. """
  585. x, y = _normalize_shift_args(
  586. x, y, top=top, right=right, bottom=bottom, left=left)
  587. return self.deepcopy().shift_(x=x, y=y)
  588. # TODO separate this into draw_face_on_image() and draw_border_on_image()
  589. # TODO add tests for line thickness
  590. def draw_on_image(self,
  591. image,
  592. color=(0, 255, 0), color_face=None,
  593. color_lines=None, color_points=None,
  594. alpha=1.0, alpha_face=None,
  595. alpha_lines=None, alpha_points=None,
  596. size=1, size_lines=None, size_points=None,
  597. raise_if_out_of_image=False):
  598. """Draw the polygon on an image.
  599. Parameters
  600. ----------
  601. image : (H,W,C) ndarray
  602. The image onto which to draw the polygon. Usually expected to be
  603. of dtype ``uint8``, though other dtypes are also handled.
  604. color : iterable of int, optional
  605. The color to use for the whole polygon.
  606. Must correspond to the channel layout of the image. Usually RGB.
  607. The values for `color_face`, `color_lines` and `color_points`
  608. will be derived from this color if they are set to ``None``.
  609. This argument has no effect if `color_face`, `color_lines`
  610. and `color_points` are all set anything other than ``None``.
  611. color_face : None or iterable of int, optional
  612. The color to use for the inner polygon area (excluding perimeter).
  613. Must correspond to the channel layout of the image. Usually RGB.
  614. If this is ``None``, it will be derived from ``color * 1.0``.
  615. color_lines : None or iterable of int, optional
  616. The color to use for the line (aka perimeter/border) of the
  617. polygon.
  618. Must correspond to the channel layout of the image. Usually RGB.
  619. If this is ``None``, it will be derived from ``color * 0.5``.
  620. color_points : None or iterable of int, optional
  621. The color to use for the corner points of the polygon.
  622. Must correspond to the channel layout of the image. Usually RGB.
  623. If this is ``None``, it will be derived from ``color * 0.5``.
  624. alpha : float, optional
  625. The opacity of the whole polygon, where ``1.0`` denotes a
  626. completely visible polygon and ``0.0`` an invisible one.
  627. The values for `alpha_face`, `alpha_lines` and `alpha_points`
  628. will be derived from this alpha value if they are set to ``None``.
  629. This argument has no effect if `alpha_face`, `alpha_lines`
  630. and `alpha_points` are all set anything other than ``None``.
  631. alpha_face : None or number, optional
  632. The opacity of the polygon's inner area (excluding the perimeter),
  633. where ``1.0`` denotes a completely visible inner area and ``0.0``
  634. an invisible one.
  635. If this is ``None``, it will be derived from ``alpha * 0.5``.
  636. alpha_lines : None or number, optional
  637. The opacity of the polygon's line (aka perimeter/border),
  638. where ``1.0`` denotes a completely visible line and ``0.0`` an
  639. invisible one.
  640. If this is ``None``, it will be derived from ``alpha * 1.0``.
  641. alpha_points : None or number, optional
  642. The opacity of the polygon's corner points, where ``1.0`` denotes
  643. completely visible corners and ``0.0`` invisible ones.
  644. If this is ``None``, it will be derived from ``alpha * 1.0``.
  645. size : int, optional
  646. Size of the polygon.
  647. The sizes of the line and points are derived from this value,
  648. unless they are set.
  649. size_lines : None or int, optional
  650. Thickness of the polygon's line (aka perimeter/border).
  651. If ``None``, this value is derived from `size`.
  652. size_points : int, optional
  653. Size of the points in pixels.
  654. If ``None``, this value is derived from ``3 * size``.
  655. raise_if_out_of_image : bool, optional
  656. Whether to raise an error if the polygon is fully
  657. outside of the image. If set to ``False``, no error will be
  658. raised and only the parts inside the image will be drawn.
  659. Returns
  660. -------
  661. (H,W,C) ndarray
  662. Image with the polygon drawn on it. Result dtype is the same as the
  663. input dtype.
  664. """
  665. # pylint: disable=invalid-name
  666. def _assert_not_none(arg_name, arg_value):
  667. assert arg_value is not None, (
  668. "Expected '%s' to not be None, got type %s." % (
  669. arg_name, type(arg_value),))
  670. def _default_to(var, default):
  671. if var is None:
  672. return default
  673. return var
  674. _assert_not_none("color", color)
  675. _assert_not_none("alpha", alpha)
  676. _assert_not_none("size", size)
  677. # FIXME due to the np.array(.) and the assert at ndim==2 below, this
  678. # will always fail on 2D images?
  679. color_face = _default_to(color_face, np.array(color))
  680. color_lines = _default_to(color_lines, np.array(color) * 0.5)
  681. color_points = _default_to(color_points, np.array(color) * 0.5)
  682. alpha_face = _default_to(alpha_face, alpha * 0.5)
  683. alpha_lines = _default_to(alpha_lines, alpha)
  684. alpha_points = _default_to(alpha_points, alpha)
  685. size_lines = _default_to(size_lines, size)
  686. size_points = _default_to(size_points, size * 3)
  687. if image.ndim == 2:
  688. assert ia.is_single_number(color_face), (
  689. "Got a 2D image. Expected then 'color_face' to be a single "
  690. "number, but got %s." % (str(color_face),))
  691. color_face = [color_face]
  692. elif image.ndim == 3 and ia.is_single_number(color_face):
  693. color_face = [color_face] * image.shape[-1]
  694. if alpha_face < 0.01:
  695. alpha_face = 0
  696. elif alpha_face > 0.99:
  697. alpha_face = 1
  698. if raise_if_out_of_image and self.is_out_of_image(image):
  699. raise Exception("Cannot draw polygon %s on image with "
  700. "shape %s." % (str(self), image.shape))
  701. # TODO np.clip to image plane if is_fully_within_image(), similar to
  702. # how it is done for bounding boxes
  703. # TODO improve efficiency by only drawing in rectangle that covers
  704. # poly instead of drawing in the whole image
  705. # TODO for a rectangular polygon, the face coordinates include the
  706. # top/left boundary but not the right/bottom boundary. This may
  707. # be unintuitive when not drawing the boundary. Maybe somehow
  708. # remove the boundary coordinates from the face coordinates after
  709. # generating both?
  710. input_dtype = image.dtype
  711. result = image.astype(np.float32)
  712. rr, cc = skimage.draw.polygon(
  713. self.yy_int, self.xx_int, shape=image.shape)
  714. if len(rr) > 0:
  715. if alpha_face == 1:
  716. result[rr, cc] = np.float32(color_face)
  717. elif alpha_face == 0:
  718. pass
  719. else:
  720. result[rr, cc] = (
  721. (1 - alpha_face) * result[rr, cc, :]
  722. + alpha_face * np.float32(color_face)
  723. )
  724. ls_open = self.to_line_string(closed=False)
  725. ls_closed = self.to_line_string(closed=True)
  726. result = ls_closed.draw_lines_on_image(
  727. result, color=color_lines, alpha=alpha_lines,
  728. size=size_lines, raise_if_out_of_image=raise_if_out_of_image)
  729. result = ls_open.draw_points_on_image(
  730. result, color=color_points, alpha=alpha_points,
  731. size=size_points, raise_if_out_of_image=raise_if_out_of_image)
  732. if input_dtype.type == np.uint8:
  733. # TODO make clipping more flexible
  734. result = np.clip(np.round(result), 0, 255).astype(input_dtype)
  735. else:
  736. result = result.astype(input_dtype)
  737. return result
  738. # TODO add pad, similar to LineStrings
  739. # TODO add pad_max, similar to LineStrings
  740. # TODO add prevent_zero_size, similar to LineStrings
  741. def extract_from_image(self, image):
  742. """Extract all image pixels within the polygon area.
  743. This method returns a rectangular image array. All pixels within
  744. that rectangle that do not belong to the polygon area will be filled
  745. with zeros (i.e. they will be black).
  746. The method will also zero-pad the image if the polygon is
  747. partially/fully outside of the image.
  748. Parameters
  749. ----------
  750. image : (H,W) ndarray or (H,W,C) ndarray
  751. The image from which to extract the pixels within the polygon.
  752. Returns
  753. -------
  754. (H',W') ndarray or (H',W',C) ndarray
  755. Pixels within the polygon. Zero-padded if the polygon is
  756. partially/fully outside of the image.
  757. """
  758. assert image.ndim in [2, 3], (
  759. "Expected image of shape (H,W,[C]), got shape %s." % (
  760. image.shape,))
  761. if len(self.exterior) <= 2:
  762. raise Exception("Polygon must be made up of at least 3 points to "
  763. "extract its area from an image.")
  764. bb = self.to_bounding_box()
  765. bb_area = bb.extract_from_image(image)
  766. if self.is_out_of_image(image, fully=True, partly=False):
  767. return bb_area
  768. xx = self.xx_int
  769. yy = self.yy_int
  770. xx_mask = xx - np.min(xx)
  771. yy_mask = yy - np.min(yy)
  772. height_mask = np.max(yy_mask)
  773. width_mask = np.max(xx_mask)
  774. rr_face, cc_face = skimage.draw.polygon(
  775. yy_mask, xx_mask, shape=(height_mask, width_mask))
  776. mask = np.zeros((height_mask, width_mask), dtype=np.bool)
  777. mask[rr_face, cc_face] = True
  778. if image.ndim == 3:
  779. mask = np.tile(mask[:, :, np.newaxis], (1, 1, image.shape[2]))
  780. return bb_area * mask
  781. def change_first_point_by_coords(self, x, y, max_distance=1e-4,
  782. raise_if_too_far_away=True):
  783. """
  784. Reorder exterior points so that the point closest to given x/y is first.
  785. This method takes a given ``(x,y)`` coordinate, finds the closest
  786. corner point on the exterior and reorders all exterior corner points
  787. so that the found point becomes the first one in the array.
  788. If no matching points are found, an exception is raised.
  789. Parameters
  790. ----------
  791. x : number
  792. X-coordinate of the point.
  793. y : number
  794. Y-coordinate of the point.
  795. max_distance : None or number, optional
  796. Maximum distance past which possible matches are ignored.
  797. If ``None`` the distance limit is deactivated.
  798. raise_if_too_far_away : bool, optional
  799. Whether to raise an exception if the closest found point is too
  800. far away (``True``) or simply return an unchanged copy if this
  801. object (``False``).
  802. Returns
  803. -------
  804. imgaug.augmentables.polys.Polygon
  805. Copy of this polygon with the new point order.
  806. """
  807. if len(self.exterior) == 0:
  808. raise Exception("Cannot reorder polygon points, because it "
  809. "contains no points.")
  810. closest_idx, closest_dist = self.find_closest_point_index(
  811. x=x, y=y, return_distance=True)
  812. if max_distance is not None and closest_dist > max_distance:
  813. if not raise_if_too_far_away:
  814. return self.deepcopy()
  815. closest_point = self.exterior[closest_idx, :]
  816. raise Exception(
  817. "Closest found point (%.9f, %.9f) exceeds max_distance of "
  818. "%.9f exceeded" % (
  819. closest_point[0], closest_point[1], closest_dist))
  820. return self.change_first_point_by_index(closest_idx)
  821. def change_first_point_by_index(self, point_idx):
  822. """
  823. Reorder exterior points so that the point with given index is first.
  824. This method takes a given index and reorders all exterior corner points
  825. so that the point with that index becomes the first one in the array.
  826. An ``AssertionError`` will be raised if the index does not match
  827. any exterior point's index or the exterior does not contain any points.
  828. Parameters
  829. ----------
  830. point_idx : int
  831. Index of the desired starting point.
  832. Returns
  833. -------
  834. imgaug.augmentables.polys.Polygon
  835. Copy of this polygon with the new point order.
  836. """
  837. assert 0 <= point_idx < len(self.exterior), (
  838. "Expected index of new first point to be in the discrete interval "
  839. "[0..%d). Got index %d." % (len(self.exterior), point_idx))
  840. if point_idx == 0:
  841. return self.deepcopy()
  842. exterior = np.concatenate(
  843. (self.exterior[point_idx:, :], self.exterior[:point_idx, :]),
  844. axis=0
  845. )
  846. return self.deepcopy(exterior=exterior)
  847. def subdivide_(self, points_per_edge):
  848. """Derive a new poly with ``N`` interpolated points per edge in-place.
  849. See :func:`~imgaug.augmentables.lines.LineString.subdivide` for details.
  850. Added in 0.4.0.
  851. Parameters
  852. ----------
  853. points_per_edge : int
  854. Number of points to interpolate on each edge.
  855. Returns
  856. -------
  857. imgaug.augmentables.polys.Polygon
  858. Polygon with subdivided edges.
  859. The object may have been modified in-place.
  860. """
  861. if len(self.exterior) == 1:
  862. return self
  863. ls = self.to_line_string(closed=True)
  864. ls_sub = ls.subdivide(points_per_edge)
  865. # [:-1] even works if the polygon contains zero points
  866. exterior_subdivided = ls_sub.coords[:-1]
  867. self.exterior = exterior_subdivided
  868. return self
  869. def subdivide(self, points_per_edge):
  870. """Derive a new polygon with ``N`` interpolated points per edge.
  871. See :func:`~imgaug.augmentables.lines.LineString.subdivide` for details.
  872. Added in 0.4.0.
  873. Parameters
  874. ----------
  875. points_per_edge : int
  876. Number of points to interpolate on each edge.
  877. Returns
  878. -------
  879. imgaug.augmentables.polys.Polygon
  880. Polygon with subdivided edges.
  881. """
  882. return self.deepcopy().subdivide_(points_per_edge)
  883. def to_shapely_polygon(self):
  884. """Convert this polygon to a ``Shapely`` ``Polygon``.
  885. Returns
  886. -------
  887. shapely.geometry.Polygon
  888. The ``Shapely`` ``Polygon`` matching this polygon's exterior.
  889. """
  890. # load shapely lazily, which makes the dependency more optional
  891. import shapely.geometry
  892. return shapely.geometry.Polygon(
  893. [(point[0], point[1]) for point in self.exterior])
  894. def to_shapely_line_string(self, closed=False, interpolate=0):
  895. """Convert this polygon to a ``Shapely`` ``LineString`` object.
  896. Parameters
  897. ----------
  898. closed : bool, optional
  899. Whether to return the line string with the last point being
  900. identical to the first point.
  901. interpolate : int, optional
  902. Number of points to interpolate between any pair of two
  903. consecutive points. These points are added to the final line string.
  904. Returns
  905. -------
  906. shapely.geometry.LineString
  907. The ``Shapely`` ``LineString`` matching the polygon's exterior.
  908. """
  909. return _convert_points_to_shapely_line_string(
  910. self.exterior, closed=closed, interpolate=interpolate)
  911. def to_bounding_box(self):
  912. """Convert this polygon to a bounding box containing the polygon.
  913. Returns
  914. -------
  915. imgaug.augmentables.bbs.BoundingBox
  916. Bounding box that tightly encapsulates the polygon.
  917. """
  918. # TODO get rid of this deferred import
  919. from imgaug.augmentables.bbs import BoundingBox
  920. xx = self.xx
  921. yy = self.yy
  922. return BoundingBox(x1=min(xx), x2=max(xx),
  923. y1=min(yy), y2=max(yy),
  924. label=self.label)
  925. def to_keypoints(self):
  926. """Convert this polygon's exterior to ``Keypoint`` instances.
  927. Returns
  928. -------
  929. list of imgaug.augmentables.kps.Keypoint
  930. Exterior vertices as :class:`~imgaug.augmentables.kps.Keypoint`
  931. instances.
  932. """
  933. # TODO get rid of this deferred import
  934. from imgaug.augmentables.kps import Keypoint
  935. return [Keypoint(x=point[0], y=point[1]) for point in self.exterior]
  936. def to_line_string(self, closed=True):
  937. """Convert this polygon's exterior to a ``LineString`` instance.
  938. Parameters
  939. ----------
  940. closed : bool, optional
  941. Whether to close the line string, i.e. to add the first point of
  942. the `exterior` also as the last point at the end of the line string.
  943. This has no effect if the polygon has a single point or zero
  944. points.
  945. Returns
  946. -------
  947. imgaug.augmentables.lines.LineString
  948. Exterior of the polygon as a line string.
  949. """
  950. from imgaug.augmentables.lines import LineString
  951. if not closed or len(self.exterior) <= 1:
  952. return LineString(self.exterior, label=self.label)
  953. return LineString(
  954. np.concatenate([self.exterior, self.exterior[0:1, :]], axis=0),
  955. label=self.label)
  956. @staticmethod
  957. def from_shapely(polygon_shapely, label=None):
  958. """Create a polygon from a ``Shapely`` ``Polygon``.
  959. .. note::
  960. This will remove any holes in the shapely polygon.
  961. Parameters
  962. ----------
  963. polygon_shapely : shapely.geometry.Polygon
  964. The shapely polygon.
  965. label : None or str, optional
  966. The label of the new polygon.
  967. Returns
  968. -------
  969. imgaug.augmentables.polys.Polygon
  970. A polygon with the same exterior as the ``Shapely`` ``Polygon``.
  971. """
  972. # load shapely lazily, which makes the dependency more optional
  973. import shapely.geometry
  974. assert isinstance(polygon_shapely, shapely.geometry.Polygon), (
  975. "Expected the input to be a shapely.geometry.Polgon instance. "
  976. "Got %s." % (type(polygon_shapely),))
  977. # polygon_shapely.exterior can be None if the polygon was
  978. # instantiated without points
  979. has_no_exterior = (
  980. polygon_shapely.exterior is None
  981. or len(polygon_shapely.exterior.coords) == 0)
  982. if has_no_exterior:
  983. return Polygon([], label=label)
  984. exterior = np.float32([[x, y]
  985. for (x, y)
  986. in polygon_shapely.exterior.coords])
  987. return Polygon(exterior, label=label)
  988. def coords_almost_equals(self, other, max_distance=1e-4,
  989. points_per_edge=8):
  990. """Alias for :func:`Polygon.exterior_almost_equals`.
  991. Parameters
  992. ----------
  993. other : imgaug.augmentables.polys.Polygon or (N,2) ndarray or list of tuple
  994. See
  995. :func:`~imgaug.augmentables.polys.Polygon.exterior_almost_equals`.
  996. max_distance : number, optional
  997. See
  998. :func:`~imgaug.augmentables.polys.Polygon.exterior_almost_equals`.
  999. points_per_edge : int, optional
  1000. See
  1001. :func:`~imgaug.augmentables.polys.Polygon.exterior_almost_equals`.
  1002. Returns
  1003. -------
  1004. bool
  1005. Whether the two polygon's exteriors can be viewed as equal
  1006. (approximate test).
  1007. """
  1008. return self.exterior_almost_equals(
  1009. other, max_distance=max_distance, points_per_edge=points_per_edge)
  1010. def exterior_almost_equals(self, other, max_distance=1e-4,
  1011. points_per_edge=8):
  1012. """Estimate if this and another polygon's exterior are almost identical.
  1013. The two exteriors can have different numbers of points, but any point
  1014. randomly sampled on the exterior of one polygon should be close to the
  1015. closest point on the exterior of the other polygon.
  1016. .. note::
  1017. This method works in an approximative way. One can come up with
  1018. polygons with fairly different shapes that will still be estimated
  1019. as equal by this method. In practice however this should be
  1020. unlikely to be the case. The probability for something like that
  1021. goes down as the interpolation parameter is increased.
  1022. Parameters
  1023. ----------
  1024. other : imgaug.augmentables.polys.Polygon or (N,2) ndarray or list of tuple
  1025. The other polygon with which to compare the exterior.
  1026. If this is an ``ndarray``, it is assumed to represent an exterior.
  1027. It must then have dtype ``float32`` and shape ``(N,2)`` with the
  1028. second dimension denoting xy-coordinates.
  1029. If this is a ``list`` of ``tuple`` s, it is assumed to represent
  1030. an exterior. Each tuple then must contain exactly two ``number`` s,
  1031. denoting xy-coordinates.
  1032. max_distance : number, optional
  1033. The maximum euclidean distance between a point on one polygon and
  1034. the closest point on the other polygon. If the distance is exceeded
  1035. for any such pair, the two exteriors are not viewed as equal. The
  1036. points are either the points contained in the polygon's exterior
  1037. ndarray or interpolated points between these.
  1038. points_per_edge : int, optional
  1039. How many points to interpolate on each edge.
  1040. Returns
  1041. -------
  1042. bool
  1043. Whether the two polygon's exteriors can be viewed as equal
  1044. (approximate test).
  1045. """
  1046. if isinstance(other, list):
  1047. other = Polygon(np.float32(other))
  1048. elif ia.is_np_array(other):
  1049. other = Polygon(other)
  1050. else:
  1051. assert isinstance(other, Polygon), (
  1052. "Expected 'other' to be a list of coordinates, a coordinate "
  1053. "array or a single Polygon. Got type %s." % (type(other),))
  1054. return self.to_line_string(closed=True).coords_almost_equals(
  1055. other.to_line_string(closed=True),
  1056. max_distance=max_distance,
  1057. points_per_edge=points_per_edge
  1058. )
  1059. def almost_equals(self, other, max_distance=1e-4, points_per_edge=8):
  1060. """
  1061. Estimate if this polygon's and another's geometry/labels are similar.
  1062. This is the same as
  1063. :func:`~imgaug.augmentables.polys.Polygon.exterior_almost_equals` but
  1064. additionally compares the labels.
  1065. Parameters
  1066. ----------
  1067. other : imgaug.augmentables.polys.Polygon
  1068. The other object to compare against. Expected to be a ``Polygon``.
  1069. max_distance : float, optional
  1070. See
  1071. :func:`~imgaug.augmentables.polys.Polygon.exterior_almost_equals`.
  1072. points_per_edge : int, optional
  1073. See
  1074. :func:`~imgaug.augmentables.polys.Polygon.exterior_almost_equals`.
  1075. Returns
  1076. -------
  1077. bool
  1078. ``True`` if the coordinates are almost equal and additionally
  1079. the labels are equal. Otherwise ``False``.
  1080. """
  1081. if self.label != other.label:
  1082. return False
  1083. return self.exterior_almost_equals(
  1084. other, max_distance=max_distance, points_per_edge=points_per_edge)
  1085. def copy(self, exterior=None, label=None):
  1086. """Create a shallow copy of this object.
  1087. Parameters
  1088. ----------
  1089. exterior : list of imgaug.augmentables.kps.Keypoint or list of tuple or (N,2) ndarray, optional
  1090. List of points defining the polygon. See
  1091. :func:`~imgaug.augmentables.polys.Polygon.__init__` for details.
  1092. label : None or str, optional
  1093. If not ``None``, the ``label`` of the copied object will be set
  1094. to this value.
  1095. Returns
  1096. -------
  1097. imgaug.augmentables.polys.Polygon
  1098. Shallow copy.
  1099. """
  1100. return self.deepcopy(exterior=exterior, label=label)
  1101. def deepcopy(self, exterior=None, label=None):
  1102. """Create a deep copy of this object.
  1103. Parameters
  1104. ----------
  1105. exterior : list of Keypoint or list of tuple or (N,2) ndarray, optional
  1106. List of points defining the polygon. See
  1107. `imgaug.augmentables.polys.Polygon.__init__` for details.
  1108. label : None or str
  1109. If not ``None``, the ``label`` of the copied object will be set
  1110. to this value.
  1111. Returns
  1112. -------
  1113. imgaug.augmentables.polys.Polygon
  1114. Deep copy.
  1115. """
  1116. return Polygon(
  1117. exterior=np.copy(self.exterior) if exterior is None else exterior,
  1118. label=self.label if label is None else label)
  1119. def __getitem__(self, indices):
  1120. """Get the coordinate(s) with given indices.
  1121. Added in 0.4.0.
  1122. Returns
  1123. -------
  1124. ndarray
  1125. xy-coordinate(s) as ``ndarray``.
  1126. """
  1127. return self.exterior[indices]
  1128. def __iter__(self):
  1129. """Iterate over the coordinates of this instance.
  1130. Added in 0.4.0.
  1131. Yields
  1132. ------
  1133. ndarray
  1134. An ``(2,)`` ``ndarray`` denoting an xy-coordinate pair.
  1135. """
  1136. return iter(self.exterior)
  1137. def __repr__(self):
  1138. return self.__str__()
  1139. def __str__(self):
  1140. points_str = ", ".join([
  1141. "(x=%.3f, y=%.3f)" % (point[0], point[1])
  1142. for point
  1143. in self.exterior])
  1144. return "Polygon([%s] (%d points), label=%s)" % (
  1145. points_str, len(self.exterior), self.label)
  1146. # TODO add tests for this
  1147. class PolygonsOnImage(IAugmentable):
  1148. """Container for all polygons on a single image.
  1149. Parameters
  1150. ----------
  1151. polygons : list of imgaug.augmentables.polys.Polygon
  1152. List of polygons on the image.
  1153. shape : tuple of int or ndarray
  1154. The shape of the image on which the objects are placed.
  1155. Either an image with shape ``(H,W,[C])`` or a ``tuple`` denoting
  1156. such an image shape.
  1157. Examples
  1158. --------
  1159. >>> import numpy as np
  1160. >>> from imgaug.augmentables.polys import Polygon, PolygonsOnImage
  1161. >>> image = np.zeros((100, 100))
  1162. >>> polys = [
  1163. >>> Polygon([(0.5, 0.5), (100.5, 0.5), (100.5, 100.5), (0.5, 100.5)]),
  1164. >>> Polygon([(50.5, 0.5), (100.5, 50.5), (50.5, 100.5), (0.5, 50.5)])
  1165. >>> ]
  1166. >>> polys_oi = PolygonsOnImage(polys, shape=image.shape)
  1167. """
  1168. def __init__(self, polygons, shape):
  1169. self.polygons = polygons
  1170. self.shape = normalize_shape(shape)
  1171. @property
  1172. def items(self):
  1173. """Get the polygons in this container.
  1174. Added in 0.4.0.
  1175. Returns
  1176. -------
  1177. list of Polygon
  1178. Polygons within this container.
  1179. """
  1180. return self.polygons
  1181. @items.setter
  1182. def items(self, value):
  1183. """Set the polygons in this container.
  1184. Added in 0.4.0.
  1185. Parameters
  1186. ----------
  1187. value : list of Polygon
  1188. Polygons within this container.
  1189. """
  1190. self.polygons = value
  1191. @property
  1192. def empty(self):
  1193. """Estimate whether this object contains zero polygons.
  1194. Returns
  1195. -------
  1196. bool
  1197. ``True`` if this object contains zero polygons.
  1198. """
  1199. return len(self.polygons) == 0
  1200. def on_(self, image):
  1201. """Project all polygons from one image shape to a new one in-place.
  1202. Added in 0.4.0.
  1203. Parameters
  1204. ----------
  1205. image : ndarray or tuple of int
  1206. New image onto which the polygons are to be projected.
  1207. May also simply be that new image's shape ``tuple``.
  1208. Returns
  1209. -------
  1210. imgaug.augmentables.polys.PolygonsOnImage
  1211. Object containing all projected polygons.
  1212. The object and its items may have been modified in-place.
  1213. """
  1214. # pylint: disable=invalid-name
  1215. on_shape = normalize_shape(image)
  1216. if on_shape[0:2] == self.shape[0:2]:
  1217. self.shape = on_shape # channels may differ
  1218. return self
  1219. for i, item in enumerate(self.items):
  1220. self.polygons[i] = item.project_(self.shape, on_shape)
  1221. self.shape = on_shape
  1222. return self
  1223. def on(self, image):
  1224. """Project all polygons from one image shape to a new one.
  1225. Parameters
  1226. ----------
  1227. image : ndarray or tuple of int
  1228. New image onto which the polygons are to be projected.
  1229. May also simply be that new image's shape ``tuple``.
  1230. Returns
  1231. -------
  1232. imgaug.augmentables.polys.PolygonsOnImage
  1233. Object containing all projected polygons.
  1234. """
  1235. # pylint: disable=invalid-name
  1236. return self.deepcopy().on_(image)
  1237. def draw_on_image(self,
  1238. image,
  1239. color=(0, 255, 0), color_face=None,
  1240. color_lines=None, color_points=None,
  1241. alpha=1.0, alpha_face=None,
  1242. alpha_lines=None, alpha_points=None,
  1243. size=1, size_lines=None, size_points=None,
  1244. raise_if_out_of_image=False):
  1245. """Draw all polygons onto a given image.
  1246. Parameters
  1247. ----------
  1248. image : (H,W,C) ndarray
  1249. The image onto which to draw the bounding boxes.
  1250. This image should usually have the same shape as set in
  1251. ``PolygonsOnImage.shape``.
  1252. color : iterable of int, optional
  1253. The color to use for the whole polygons.
  1254. Must correspond to the channel layout of the image. Usually RGB.
  1255. The values for `color_face`, `color_lines` and `color_points`
  1256. will be derived from this color if they are set to ``None``.
  1257. This argument has no effect if `color_face`, `color_lines`
  1258. and `color_points` are all set anything other than ``None``.
  1259. color_face : None or iterable of int, optional
  1260. The color to use for the inner polygon areas (excluding perimeters).
  1261. Must correspond to the channel layout of the image. Usually RGB.
  1262. If this is ``None``, it will be derived from ``color * 1.0``.
  1263. color_lines : None or iterable of int, optional
  1264. The color to use for the lines (aka perimeters/borders) of the
  1265. polygons. Must correspond to the channel layout of the image.
  1266. Usually RGB. If this is ``None``, it will be derived
  1267. from ``color * 0.5``.
  1268. color_points : None or iterable of int, optional
  1269. The color to use for the corner points of the polygons.
  1270. Must correspond to the channel layout of the image. Usually RGB.
  1271. If this is ``None``, it will be derived from ``color * 0.5``.
  1272. alpha : float, optional
  1273. The opacity of the whole polygons, where ``1.0`` denotes
  1274. completely visible polygons and ``0.0`` invisible ones.
  1275. The values for `alpha_face`, `alpha_lines` and `alpha_points`
  1276. will be derived from this alpha value if they are set to ``None``.
  1277. This argument has no effect if `alpha_face`, `alpha_lines`
  1278. and `alpha_points` are all set anything other than ``None``.
  1279. alpha_face : None or number, optional
  1280. The opacity of the polygon's inner areas (excluding the perimeters),
  1281. where ``1.0`` denotes completely visible inner areas and ``0.0``
  1282. invisible ones.
  1283. If this is ``None``, it will be derived from ``alpha * 0.5``.
  1284. alpha_lines : None or number, optional
  1285. The opacity of the polygon's lines (aka perimeters/borders),
  1286. where ``1.0`` denotes completely visible perimeters and ``0.0``
  1287. invisible ones.
  1288. If this is ``None``, it will be derived from ``alpha * 1.0``.
  1289. alpha_points : None or number, optional
  1290. The opacity of the polygon's corner points, where ``1.0`` denotes
  1291. completely visible corners and ``0.0`` invisible ones.
  1292. Currently this is an on/off choice, i.e. only ``0.0`` or ``1.0``
  1293. are allowed.
  1294. If this is ``None``, it will be derived from ``alpha * 1.0``.
  1295. size : int, optional
  1296. Size of the polygons.
  1297. The sizes of the line and points are derived from this value,
  1298. unless they are set.
  1299. size_lines : None or int, optional
  1300. Thickness of the polygon lines (aka perimeter/border).
  1301. If ``None``, this value is derived from `size`.
  1302. size_points : int, optional
  1303. The size of all corner points. If set to ``C``, each corner point
  1304. will be drawn as a square of size ``C x C``.
  1305. raise_if_out_of_image : bool, optional
  1306. Whether to raise an error if any polygon is fully
  1307. outside of the image. If set to False, no error will be raised and
  1308. only the parts inside the image will be drawn.
  1309. Returns
  1310. -------
  1311. (H,W,C) ndarray
  1312. Image with drawn polygons.
  1313. """
  1314. for poly in self.polygons:
  1315. image = poly.draw_on_image(
  1316. image,
  1317. color=color,
  1318. color_face=color_face,
  1319. color_lines=color_lines,
  1320. color_points=color_points,
  1321. alpha=alpha,
  1322. alpha_face=alpha_face,
  1323. alpha_lines=alpha_lines,
  1324. alpha_points=alpha_points,
  1325. size=size,
  1326. size_lines=size_lines,
  1327. size_points=size_points,
  1328. raise_if_out_of_image=raise_if_out_of_image
  1329. )
  1330. return image
  1331. def remove_out_of_image_(self, fully=True, partly=False):
  1332. """Remove all polygons that are fully/partially OOI in-place.
  1333. 'OOI' is the abbreviation for 'out of image'.
  1334. Added in 0.4.0.
  1335. Parameters
  1336. ----------
  1337. fully : bool, optional
  1338. Whether to remove polygons that are fully outside of the image.
  1339. partly : bool, optional
  1340. Whether to remove polygons that are partially outside of the image.
  1341. Returns
  1342. -------
  1343. imgaug.augmentables.polys.PolygonsOnImage
  1344. Reduced set of polygons. Those that are fully/partially
  1345. outside of the given image plane are removed.
  1346. The object and its items may have been modified in-place.
  1347. """
  1348. self.polygons = [
  1349. poly for poly in self.polygons
  1350. if not poly.is_out_of_image(self.shape, fully=fully, partly=partly)
  1351. ]
  1352. return self
  1353. def remove_out_of_image(self, fully=True, partly=False):
  1354. """Remove all polygons that are fully/partially outside of an image.
  1355. Parameters
  1356. ----------
  1357. fully : bool, optional
  1358. Whether to remove polygons that are fully outside of the image.
  1359. partly : bool, optional
  1360. Whether to remove polygons that are partially outside of the image.
  1361. Returns
  1362. -------
  1363. imgaug.augmentables.polys.PolygonsOnImage
  1364. Reduced set of polygons. Those that are fully/partially
  1365. outside of the given image plane are removed.
  1366. """
  1367. return self.deepcopy().remove_out_of_image_(fully, partly)
  1368. def remove_out_of_image_fraction_(self, fraction):
  1369. """Remove all Polys with an OOI fraction of ``>=fraction`` in-place.
  1370. Added in 0.4.0.
  1371. Parameters
  1372. ----------
  1373. fraction : number
  1374. Minimum out of image fraction that a polygon has to have in
  1375. order to be removed. A fraction of ``1.0`` removes only polygons
  1376. that are ``100%`` outside of the image. A fraction of ``0.0``
  1377. removes all polygons.
  1378. Returns
  1379. -------
  1380. imgaug.augmentables.polys.PolygonsOnImage
  1381. Reduced set of polygons, with those that had an out of image
  1382. fraction greater or equal the given one removed.
  1383. The object and its items may have been modified in-place.
  1384. """
  1385. return _remove_out_of_image_fraction_(self, fraction)
  1386. def remove_out_of_image_fraction(self, fraction):
  1387. """Remove all Polys with an out of image fraction of ``>=fraction``.
  1388. Added in 0.4.0.
  1389. Parameters
  1390. ----------
  1391. fraction : number
  1392. Minimum out of image fraction that a polygon has to have in
  1393. order to be removed. A fraction of ``1.0`` removes only polygons
  1394. that are ``100%`` outside of the image. A fraction of ``0.0``
  1395. removes all polygons.
  1396. Returns
  1397. -------
  1398. imgaug.augmentables.polys.PolygonsOnImage
  1399. Reduced set of polygons, with those that had an out of image
  1400. fraction greater or equal the given one removed.
  1401. """
  1402. return self.copy().remove_out_of_image_fraction_(fraction)
  1403. def clip_out_of_image_(self):
  1404. """Clip off all parts from all polygons that are OOI in-place.
  1405. 'OOI' is the abbreviation for 'out of image'.
  1406. .. note::
  1407. The result can contain fewer polygons than the input did. That
  1408. happens when a polygon is fully outside of the image plane.
  1409. .. note::
  1410. The result can also contain *more* polygons than the input
  1411. did. That happens when distinct parts of a polygon are only
  1412. connected by areas that are outside of the image plane and hence
  1413. will be clipped off, resulting in two or more unconnected polygon
  1414. parts that are left in the image plane.
  1415. Added in 0.4.0.
  1416. Returns
  1417. -------
  1418. imgaug.augmentables.polys.PolygonsOnImage
  1419. Polygons, clipped to fall within the image dimensions.
  1420. The count of output polygons may differ from the input count.
  1421. The object and its items may have been modified in-place.
  1422. """
  1423. self.polygons = [
  1424. poly_clipped
  1425. for poly in self.polygons
  1426. for poly_clipped in poly.clip_out_of_image(self.shape)]
  1427. return self
  1428. def clip_out_of_image(self):
  1429. """Clip off all parts from all polygons that are outside of an image.
  1430. .. note::
  1431. The result can contain fewer polygons than the input did. That
  1432. happens when a polygon is fully outside of the image plane.
  1433. .. note::
  1434. The result can also contain *more* polygons than the input
  1435. did. That happens when distinct parts of a polygon are only
  1436. connected by areas that are outside of the image plane and hence
  1437. will be clipped off, resulting in two or more unconnected polygon
  1438. parts that are left in the image plane.
  1439. Returns
  1440. -------
  1441. imgaug.augmentables.polys.PolygonsOnImage
  1442. Polygons, clipped to fall within the image dimensions.
  1443. The count of output polygons may differ from the input count.
  1444. """
  1445. return self.copy().clip_out_of_image_()
  1446. def shift_(self, x=0, y=0):
  1447. """Move the polygons along the x/y-axis in-place.
  1448. The origin ``(0, 0)`` is at the top left of the image.
  1449. Added in 0.4.0.
  1450. Parameters
  1451. ----------
  1452. x : number, optional
  1453. Value to be added to all x-coordinates. Positive values shift
  1454. towards the right images.
  1455. y : number, optional
  1456. Value to be added to all y-coordinates. Positive values shift
  1457. towards the bottom images.
  1458. Returns
  1459. -------
  1460. imgaug.augmentables.polys.PolygonsOnImage
  1461. Shifted polygons.
  1462. """
  1463. for i, poly in enumerate(self.polygons):
  1464. self.polygons[i] = poly.shift_(x=x, y=y)
  1465. return self
  1466. def shift(self, x=0, y=0, top=None, right=None, bottom=None, left=None):
  1467. """Move the polygons along the x/y-axis.
  1468. The origin ``(0, 0)`` is at the top left of the image.
  1469. Parameters
  1470. ----------
  1471. x : number, optional
  1472. Value to be added to all x-coordinates. Positive values shift
  1473. towards the right images.
  1474. y : number, optional
  1475. Value to be added to all y-coordinates. Positive values shift
  1476. towards the bottom images.
  1477. top : None or int, optional
  1478. Deprecated since 0.4.0.
  1479. Amount of pixels by which to shift all objects *from* the
  1480. top (towards the bottom).
  1481. right : None or int, optional
  1482. Deprecated since 0.4.0.
  1483. Amount of pixels by which to shift all objects *from* the
  1484. right (towads the left).
  1485. bottom : None or int, optional
  1486. Deprecated since 0.4.0.
  1487. Amount of pixels by which to shift all objects *from* the
  1488. bottom (towards the top).
  1489. left : None or int, optional
  1490. Deprecated since 0.4.0.
  1491. Amount of pixels by which to shift all objects *from* the
  1492. left (towards the right).
  1493. Returns
  1494. -------
  1495. imgaug.augmentables.polys.PolygonsOnImage
  1496. Shifted polygons.
  1497. """
  1498. x, y = _normalize_shift_args(
  1499. x, y, top=top, right=right, bottom=bottom, left=left)
  1500. return self.deepcopy().shift_(x=x, y=y)
  1501. def subdivide_(self, points_per_edge):
  1502. """Interpolate ``N`` points on each polygon.
  1503. Added in 0.4.0.
  1504. Parameters
  1505. ----------
  1506. points_per_edge : int
  1507. Number of points to interpolate on each edge.
  1508. Returns
  1509. -------
  1510. imgaug.augmentables.polys.PolygonsOnImage
  1511. Subdivided polygons.
  1512. """
  1513. for i, poly in enumerate(self.polygons):
  1514. self.polygons[i] = poly.subdivide_(points_per_edge)
  1515. return self
  1516. def subdivide(self, points_per_edge):
  1517. """Interpolate ``N`` points on each polygon.
  1518. Added in 0.4.0.
  1519. Parameters
  1520. ----------
  1521. points_per_edge : int
  1522. Number of points to interpolate on each edge.
  1523. Returns
  1524. -------
  1525. imgaug.augmentables.polys.PolygonsOnImage
  1526. Subdivided polygons.
  1527. """
  1528. return self.deepcopy().subdivide_(points_per_edge)
  1529. def to_xy_array(self):
  1530. """Convert all polygon coordinates to one array of shape ``(N,2)``.
  1531. Added in 0.4.0.
  1532. Returns
  1533. -------
  1534. (N, 2) ndarray
  1535. Array containing all xy-coordinates of all polygons within this
  1536. instance.
  1537. """
  1538. if self.empty:
  1539. return np.zeros((0, 2), dtype=np.float32)
  1540. return np.concatenate([poly.exterior for poly in self.polygons])
  1541. def fill_from_xy_array_(self, xy):
  1542. """Modify the corner coordinates of all polygons in-place.
  1543. .. note::
  1544. This currently expects that `xy` contains exactly as many
  1545. coordinates as the polygons within this instance have corner
  1546. points. Otherwise, an ``AssertionError`` will be raised.
  1547. .. warning::
  1548. This does not validate the new coordinates or repair the resulting
  1549. polygons. If bad coordinates are provided, the result will be
  1550. invalid polygons (e.g. self-intersections).
  1551. Added in 0.4.0.
  1552. Parameters
  1553. ----------
  1554. xy : (N, 2) ndarray or iterable of iterable of number
  1555. XY-Coordinates of ``N`` corner points. ``N`` must match the
  1556. number of corner points in all polygons within this instance.
  1557. Returns
  1558. -------
  1559. PolygonsOnImage
  1560. This instance itself, with updated coordinates.
  1561. Note that the instance was modified in-place.
  1562. """
  1563. xy = np.array(xy, dtype=np.float32)
  1564. # note that np.array([]) is (0,), not (0, 2)
  1565. assert xy.shape[0] == 0 or (xy.ndim == 2 and xy.shape[-1] == 2), ( # pylint: disable=unsubscriptable-object
  1566. "Expected input array to have shape (N,2), "
  1567. "got shape %s." % (xy.shape,))
  1568. counter = 0
  1569. for poly in self.polygons:
  1570. nb_points = len(poly.exterior)
  1571. assert counter + nb_points <= len(xy), (
  1572. "Received fewer points than there are corner points in the "
  1573. "exteriors of all polygons. Got %d points, expected %d." % (
  1574. len(xy), sum([len(p.exterior) for p in self.polygons])))
  1575. poly.exterior[:, ...] = xy[counter:counter+nb_points]
  1576. counter += nb_points
  1577. assert counter == len(xy), (
  1578. "Expected to get exactly as many xy-coordinates as there are "
  1579. "points in the exteriors of all polygons within this instance. "
  1580. "Got %d points, could only assign %d points." % (
  1581. len(xy), counter,))
  1582. return self
  1583. def to_keypoints_on_image(self):
  1584. """Convert the polygons to one ``KeypointsOnImage`` instance.
  1585. Added in 0.4.0.
  1586. Returns
  1587. -------
  1588. imgaug.augmentables.kps.KeypointsOnImage
  1589. A keypoints instance containing ``N`` coordinates for a total
  1590. of ``N`` points in all exteriors of the polygons within this
  1591. container. Order matches the order in ``polygons``.
  1592. """
  1593. from . import KeypointsOnImage
  1594. if self.empty:
  1595. return KeypointsOnImage([], shape=self.shape)
  1596. exteriors = np.concatenate(
  1597. [poly.exterior for poly in self.polygons],
  1598. axis=0)
  1599. return KeypointsOnImage.from_xy_array(exteriors, shape=self.shape)
  1600. def invert_to_keypoints_on_image_(self, kpsoi):
  1601. """Invert the output of ``to_keypoints_on_image()`` in-place.
  1602. This function writes in-place into this ``PolygonsOnImage``
  1603. instance.
  1604. Added in 0.4.0.
  1605. Parameters
  1606. ----------
  1607. kpsoi : imgaug.augmentables.kps.KeypointsOnImages
  1608. Keypoints to convert back to polygons, i.e. the outputs
  1609. of ``to_keypoints_on_image()``.
  1610. Returns
  1611. -------
  1612. PolygonsOnImage
  1613. Polygons container with updated coordinates.
  1614. Note that the instance is also updated in-place.
  1615. """
  1616. polys = self.polygons
  1617. exteriors = [poly.exterior for poly in polys]
  1618. nb_points_exp = sum([len(exterior) for exterior in exteriors])
  1619. assert len(kpsoi.keypoints) == nb_points_exp, (
  1620. "Expected %d coordinates, got %d." % (
  1621. nb_points_exp, len(kpsoi.keypoints)))
  1622. xy_arr = kpsoi.to_xy_array()
  1623. counter = 0
  1624. for poly in polys:
  1625. exterior = poly.exterior
  1626. exterior[:, :] = xy_arr[counter:counter+len(exterior), :]
  1627. counter += len(exterior)
  1628. self.shape = kpsoi.shape
  1629. return self
  1630. def copy(self, polygons=None, shape=None):
  1631. """Create a shallow copy of this object.
  1632. Parameters
  1633. ----------
  1634. polygons : None or list of imgaug.augmentables.polys.Polygons, optional
  1635. List of polygons on the image.
  1636. If not ``None``, then the ``polygons`` attribute of the copied
  1637. object will be set to this value.
  1638. shape : None or tuple of int or ndarray, optional
  1639. The shape of the image on which the objects are placed.
  1640. Either an image with shape ``(H,W,[C])`` or a tuple denoting
  1641. such an image shape.
  1642. If not ``None``, then the ``shape`` attribute of the copied object
  1643. will be set to this value.
  1644. Returns
  1645. -------
  1646. imgaug.augmentables.polys.PolygonsOnImage
  1647. Shallow copy.
  1648. """
  1649. if polygons is None:
  1650. polygons = self.polygons[:]
  1651. if shape is None:
  1652. # use tuple() here in case the shape was provided as a list
  1653. shape = tuple(self.shape)
  1654. return PolygonsOnImage(polygons, shape)
  1655. def deepcopy(self, polygons=None, shape=None):
  1656. """Create a deep copy of this object.
  1657. Parameters
  1658. ----------
  1659. polygons : None or list of imgaug.augmentables.polys.Polygons, optional
  1660. List of polygons on the image.
  1661. If not ``None``, then the ``polygons`` attribute of the copied
  1662. object will be set to this value.
  1663. shape : None or tuple of int or ndarray, optional
  1664. The shape of the image on which the objects are placed.
  1665. Either an image with shape ``(H,W,[C])`` or a tuple denoting
  1666. such an image shape.
  1667. If not ``None``, then the ``shape`` attribute of the copied object
  1668. will be set to this value.
  1669. Returns
  1670. -------
  1671. imgaug.augmentables.polys.PolygonsOnImage
  1672. Deep copy.
  1673. """
  1674. # Manual copy is far faster than deepcopy, so use manual copy here.
  1675. if polygons is None:
  1676. polygons = [poly.deepcopy() for poly in self.polygons]
  1677. if shape is None:
  1678. # use tuple() here in case the shape was provided as a list
  1679. shape = tuple(self.shape)
  1680. return PolygonsOnImage(polygons, shape)
  1681. def __getitem__(self, indices):
  1682. """Get the polygon(s) with given indices.
  1683. Added in 0.4.0.
  1684. Returns
  1685. -------
  1686. list of imgaug.augmentables.polys.Polygon
  1687. Polygon(s) with given indices.
  1688. """
  1689. return self.polygons[indices]
  1690. def __iter__(self):
  1691. """Iterate over the polygons in this container.
  1692. Added in 0.4.0.
  1693. Yields
  1694. ------
  1695. Polygon
  1696. A polygon in this container.
  1697. The order is identical to the order in the polygon list
  1698. provided upon class initialization.
  1699. """
  1700. return iter(self.polygons)
  1701. def __len__(self):
  1702. """Get the number of items in this instance.
  1703. Added in 0.4.0.
  1704. Returns
  1705. -------
  1706. int
  1707. Number of items in this instance.
  1708. """
  1709. return len(self.items)
  1710. def __repr__(self):
  1711. return self.__str__()
  1712. def __str__(self):
  1713. return "PolygonsOnImage(%s, shape=%s)" % (
  1714. str(self.polygons), self.shape)
  1715. def _convert_points_to_shapely_line_string(points, closed=False,
  1716. interpolate=0):
  1717. # load shapely lazily, which makes the dependency more optional
  1718. import shapely.geometry
  1719. if len(points) <= 1:
  1720. raise Exception(
  1721. "Conversion to shapely line string requires at least two points, "
  1722. "but points input contains only %d points." % (len(points),))
  1723. points_tuples = [(point[0], point[1]) for point in points]
  1724. # interpolate points between each consecutive pair of points
  1725. if interpolate > 0:
  1726. points_tuples = interpolate_points(points_tuples, interpolate)
  1727. # close if requested and not yet closed
  1728. # used here intentionally `points` instead of `points_tuples`
  1729. if closed and len(points) > 1:
  1730. points_tuples.append(points_tuples[0])
  1731. return shapely.geometry.LineString(points_tuples)
  1732. class _ConcavePolygonRecoverer(object):
  1733. def __init__(self, threshold_duplicate_points=1e-4, noise_strength=1e-4,
  1734. oversampling=0.01, max_segment_difference=1e-4):
  1735. self.threshold_duplicate_points = threshold_duplicate_points
  1736. self.noise_strength = noise_strength
  1737. self.oversampling = oversampling
  1738. self.max_segment_difference = max_segment_difference
  1739. # this limits the maximum amount of points after oversampling, i.e.
  1740. # if N points are input into oversampling, then M oversampled points
  1741. # are generated such that N+M <= this value
  1742. self.oversample_up_to_n_points_max = 75
  1743. # ----
  1744. # parameters for _fit_best_valid_polygon()
  1745. # ----
  1746. # how many changes may be done max to the initial (convex hull) polygon
  1747. # before simply returning the result
  1748. self.fit_n_changes_max = 100
  1749. # for how many iterations the optimization loop may run max
  1750. # before simply returning the result
  1751. self.fit_n_iters_max = 3
  1752. # how far (wrt. to their position in the input list) two points may be
  1753. # apart max to consider adding an edge between them (in the first loop
  1754. # iteration and the ones after that)
  1755. self.fit_max_dist_first_iter = 1
  1756. self.fit_max_dist_other_iters = 2
  1757. # The fit loop first generates candidate edges and then modifies the
  1758. # polygon based on these candidates. This limits the maximum amount
  1759. # of considered candidates. If the number is less than the possible
  1760. # number of candidates, they are randomly subsampled. Values beyond
  1761. # 100 significantly increase runtime (for polygons that reach that
  1762. # number).
  1763. self.fit_n_candidates_before_sort_max = 100
  1764. # If abs(x) or abs(y) of any coordinate of a polygon is beyond this
  1765. # value, no intersection points will be computed anymore. That is done,
  1766. # because the underlying library to find these points uses float
  1767. # values as keys and may therefore start to encounter inaccuracies
  1768. # leading to exceptions within that library.
  1769. self.limit_coords_values_for_inter_search = 50000
  1770. # Rounding of coordinates to use before feeding them into the
  1771. # library to search for intersection points. Note that the library
  1772. # was set to also use a corresponding eps of 1e-4.
  1773. self.decimals = 4
  1774. def recover_from(self, new_exterior, old_polygon, random_state=0):
  1775. assert isinstance(new_exterior, list) or (
  1776. ia.is_np_array(new_exterior)
  1777. and new_exterior.ndim == 2
  1778. and new_exterior.shape[1] == 2), (
  1779. "Expected exterior as list or (N,2) ndarray, got type %s." % (
  1780. type(new_exterior),))
  1781. assert len(new_exterior) >= 3, \
  1782. "Cannot recover a concave polygon from less than three points."
  1783. # create Polygon instance, if it is already valid then just return
  1784. # immediately
  1785. polygon = old_polygon.deepcopy(exterior=new_exterior)
  1786. if polygon.is_valid:
  1787. return polygon
  1788. random_state = iarandom.RNG(random_state)
  1789. rss = random_state.duplicate(3)
  1790. # remove consecutive duplicate points
  1791. new_exterior = self._remove_consecutive_duplicate_points(new_exterior)
  1792. # check that points are not all identical or on a line
  1793. new_exterior = self._fix_polygon_is_line(new_exterior, rss[0])
  1794. # jitter duplicate points
  1795. new_exterior = self._jitter_duplicate_points(new_exterior, rss[1])
  1796. # generate intersection points
  1797. segment_add_points = self._generate_intersection_points(
  1798. new_exterior, decimals=self.decimals)
  1799. # oversample points around intersections
  1800. if self.oversampling is not None and self.oversampling > 0:
  1801. segment_add_points = self._oversample_intersection_points(
  1802. new_exterior, segment_add_points)
  1803. # integrate new points into exterior
  1804. new_exterior_inter = self._insert_intersection_points(
  1805. new_exterior, segment_add_points)
  1806. # find best fit polygon, starting from convext polygon
  1807. new_exterior_concave_ids = self._fit_best_valid_polygon(
  1808. new_exterior_inter, rss[2])
  1809. new_exterior_concave = [
  1810. new_exterior_inter[idx] for idx in new_exterior_concave_ids]
  1811. # TODO return new_exterior_concave here instead of polygon, leave it to
  1812. # caller to decide what to do with it
  1813. return old_polygon.deepcopy(exterior=new_exterior_concave)
  1814. def _remove_consecutive_duplicate_points(self, points):
  1815. result = []
  1816. for point in points:
  1817. if result:
  1818. dist = np.linalg.norm(
  1819. np.float32(point) - np.float32(result[-1]))
  1820. is_same = (dist < self.threshold_duplicate_points)
  1821. if not is_same:
  1822. result.append(point)
  1823. else:
  1824. result.append(point)
  1825. if len(result) >= 2:
  1826. dist = np.linalg.norm(
  1827. np.float32(result[0]) - np.float32(result[-1]))
  1828. is_same = (dist < self.threshold_duplicate_points)
  1829. result = result[0:-1] if is_same else result
  1830. return result
  1831. # fix polygons for which all points are on a line
  1832. def _fix_polygon_is_line(self, exterior, random_state):
  1833. assert len(exterior) >= 3, (
  1834. "Can only fix line-like polygons with an exterior containing at "
  1835. "least 3 points. Got one with %d points." % (len(exterior),))
  1836. noise_strength = self.noise_strength
  1837. while self._is_polygon_line(exterior):
  1838. noise = random_state.uniform(
  1839. -noise_strength, noise_strength, size=(len(exterior), 2)
  1840. ).astype(np.float32)
  1841. exterior = [(point[0] + noise_i[0], point[1] + noise_i[1])
  1842. for point, noise_i in zip(exterior, noise)]
  1843. noise_strength = noise_strength * 10
  1844. assert noise_strength > 0, (
  1845. "Expected noise strength to be >0, got %.4f." % (
  1846. noise_strength,))
  1847. return exterior
  1848. @classmethod
  1849. def _is_polygon_line(cls, exterior):
  1850. vec_down = np.float32([0, 1])
  1851. point1 = exterior[0]
  1852. angles = set()
  1853. for point2 in exterior[1:]:
  1854. vec = np.float32(point2) - np.float32(point1)
  1855. angle = ia.angle_between_vectors(vec_down, vec)
  1856. angles.add(int(angle * 1000))
  1857. return len(angles) <= 1
  1858. def _jitter_duplicate_points(self, exterior, random_state):
  1859. def _find_duplicates(exterior_with_duplicates):
  1860. points_map = collections.defaultdict(list)
  1861. for i, point in enumerate(exterior_with_duplicates):
  1862. # we use 10/x here to be a bit more lenient, the precise
  1863. # distance test is further below
  1864. x = int(np.round(point[0]
  1865. * ((1/10) / self.threshold_duplicate_points)))
  1866. y = int(np.round(point[1]
  1867. * ((1/10) / self.threshold_duplicate_points)))
  1868. for direction0 in [-1, 0, 1]:
  1869. for direction1 in [-1, 0, 1]:
  1870. points_map[(x+direction0, y+direction1)].append(i)
  1871. duplicates = [False] * len(exterior_with_duplicates)
  1872. for key in points_map:
  1873. candidates = points_map[key]
  1874. for i, p0_idx in enumerate(candidates):
  1875. p0_idx = candidates[i]
  1876. point0 = exterior_with_duplicates[p0_idx]
  1877. if duplicates[p0_idx]:
  1878. continue
  1879. for j in range(i+1, len(candidates)):
  1880. p1_idx = candidates[j]
  1881. point1 = exterior_with_duplicates[p1_idx]
  1882. if duplicates[p1_idx]:
  1883. continue
  1884. dist = np.sqrt(
  1885. (point0[0] - point1[0])**2
  1886. + (point0[1] - point1[1])**2)
  1887. if dist < self.threshold_duplicate_points:
  1888. duplicates[p1_idx] = True
  1889. return duplicates
  1890. noise_strength = self.noise_strength
  1891. assert noise_strength > 0, (
  1892. "Expected noise strength to be >0, got %.4f." % (noise_strength,))
  1893. exterior = exterior[:]
  1894. converged = False
  1895. while not converged:
  1896. duplicates = _find_duplicates(exterior)
  1897. if any(duplicates):
  1898. noise = random_state.uniform(
  1899. -self.noise_strength,
  1900. self.noise_strength,
  1901. size=(len(exterior), 2)
  1902. ).astype(np.float32)
  1903. for i, is_duplicate in enumerate(duplicates):
  1904. if is_duplicate:
  1905. exterior[i] = (
  1906. exterior[i][0] + noise[i][0],
  1907. exterior[i][1] + noise[i][1])
  1908. noise_strength *= 10
  1909. else:
  1910. converged = True
  1911. return exterior
  1912. # TODO remove?
  1913. @classmethod
  1914. def _calculate_circumference(cls, points):
  1915. assert len(points) >= 3, (
  1916. "Need at least 3 points on the exterior to compute the "
  1917. "circumference. Got %d." % (len(points),))
  1918. points = np.array(points, dtype=np.float32)
  1919. points_matrix = np.zeros((len(points), 4), dtype=np.float32)
  1920. points_matrix[:, 0:2] = points
  1921. points_matrix[0:-1, 2:4] = points_matrix[1:, 0:2]
  1922. points_matrix[-1, 2:4] = points_matrix[0, 0:2]
  1923. distances = np.linalg.norm(
  1924. points_matrix[:, 0:2] - points_matrix[:, 2:4], axis=1)
  1925. return np.sum(distances)
  1926. def _generate_intersection_points(self, exterior,
  1927. one_point_per_intersection=True,
  1928. decimals=4):
  1929. # pylint: disable=broad-except
  1930. largest_value = np.max(np.abs(np.array(exterior, dtype=np.float32)))
  1931. too_large_values = (
  1932. largest_value > self.limit_coords_values_for_inter_search)
  1933. if too_large_values:
  1934. ia.warn(
  1935. "Encountered during polygon repair a polygon with extremely "
  1936. "large coordinate values beyond %d. Will skip intersection "
  1937. "point computation for that polygon. This avoids exceptions "
  1938. "and is -- due to the extreme distortion -- likely pointless "
  1939. "anyways (i.e. the polygon is already broken beyond repair). "
  1940. "Try using weaker augmentation parameters to avoid such "
  1941. "large coordinate values." % (
  1942. self.limit_coords_values_for_inter_search,)
  1943. )
  1944. return [[] for _ in range(len(exterior))]
  1945. if ia.is_np_array(exterior):
  1946. exterior = list(exterior)
  1947. assert isinstance(exterior, list), (
  1948. "Expected 'exterior' to be a list or a ndarray. "
  1949. "Got type %s." % (type(exterior),))
  1950. assert all([len(point) == 2 for point in exterior]), (
  1951. "Expected 'exterior' to contain (x,y) coordinate pairs. "
  1952. "Got lengths %s." % (
  1953. ", ".join([str(len(point)) for point in exterior])))
  1954. if len(exterior) <= 0:
  1955. return []
  1956. # use (*[i][0], *[i][1]) formulation here instead of just *[i],
  1957. # because this way we convert numpy arrays to tuples of floats, which
  1958. # is required by isect_segments_include_segments
  1959. segments = [
  1960. (
  1961. (
  1962. np.round(float(exterior[i][0]), decimals),
  1963. np.round(float(exterior[i][1]), decimals)
  1964. ),
  1965. (
  1966. np.round(float(exterior[(i + 1) % len(exterior)][0]),
  1967. decimals),
  1968. np.round(float(exterior[(i + 1) % len(exterior)][1]),
  1969. decimals)
  1970. )
  1971. )
  1972. for i in range(len(exterior))
  1973. ]
  1974. # returns [(point, [(segment_p0, segment_p1), ..]), ...]
  1975. from imgaug.external.poly_point_isect_py2py3 import (
  1976. isect_segments_include_segments)
  1977. try:
  1978. intersections = isect_segments_include_segments(segments)
  1979. except Exception as exc:
  1980. # Exceptions in the segment intersection search can at least
  1981. # happen due to large float coords (the library uses
  1982. # floats as indices, which is bound to cause inaccuracies).
  1983. # Usually such exceptions should not appear, as too large
  1984. # coordinate values are already caught at the start of this
  1985. # function. For the case that there are more errors, this block
  1986. # will prevent a full crash.
  1987. ia.warn(
  1988. "Encountered exception %s during polygon repair in segment "
  1989. "intersection computation. Will skip that step." % (
  1990. str(exc),))
  1991. traceback.print_exc()
  1992. return [[] for _ in range(len(exterior))]
  1993. # estimate to which segment the found intersection points belong
  1994. segments_add_points = [[] for _ in range(len(segments))]
  1995. for point, associated_segments in intersections:
  1996. # the intersection point may be associated with multiple segments,
  1997. # but we only want to add it once, so pick the first segment
  1998. if one_point_per_intersection:
  1999. associated_segments = [associated_segments[0]]
  2000. for seg_inter_p0, seg_inter_p1 in associated_segments:
  2001. diffs = []
  2002. dists = []
  2003. for seg_p0, seg_p1 in segments:
  2004. dist_p0p0 = np.linalg.norm(seg_p0 - np.array(seg_inter_p0))
  2005. dist_p1p1 = np.linalg.norm(seg_p1 - np.array(seg_inter_p1))
  2006. dist_p0p1 = np.linalg.norm(seg_p0 - np.array(seg_inter_p1))
  2007. dist_p1p0 = np.linalg.norm(seg_p1 - np.array(seg_inter_p0))
  2008. diff = min(dist_p0p0 + dist_p1p1, dist_p0p1 + dist_p1p0)
  2009. diffs.append(diff)
  2010. dists.append(np.linalg.norm(
  2011. (seg_p0[0] - point[0], seg_p0[1] - point[1])
  2012. ))
  2013. min_diff = np.min(diffs)
  2014. if min_diff < self.max_segment_difference:
  2015. idx = int(np.argmin(diffs))
  2016. segments_add_points[idx].append((point, dists[idx]))
  2017. else:
  2018. ia.warn(
  2019. "Couldn't find fitting segment in "
  2020. "_generate_intersection_points(). Ignoring "
  2021. "intersection point.")
  2022. # sort intersection points by their distance to point 0 in each segment
  2023. # (clockwise ordering, this does something only for segments with
  2024. # >=2 intersection points)
  2025. segment_add_points_sorted = []
  2026. for idx in range(len(segments_add_points)):
  2027. points = [t[0] for t in segments_add_points[idx]]
  2028. dists = [t[1] for t in segments_add_points[idx]]
  2029. if len(points) < 2:
  2030. segment_add_points_sorted.append(points)
  2031. else:
  2032. both = sorted(zip(points, dists), key=lambda t: t[1])
  2033. # keep points, drop distances
  2034. segment_add_points_sorted.append([a for a, _b in both])
  2035. return segment_add_points_sorted
  2036. def _oversample_intersection_points(self, exterior, segment_add_points):
  2037. # segment_add_points must be sorted
  2038. if self.oversampling is None or self.oversampling <= 0:
  2039. return segment_add_points
  2040. segment_add_points_sorted_overs = [
  2041. [] for _ in range(len(segment_add_points))]
  2042. n_points = len(exterior)
  2043. for i, last in enumerate(exterior):
  2044. for j, p_inter in enumerate(segment_add_points[i]):
  2045. direction = (p_inter[0] - last[0], p_inter[1] - last[1])
  2046. if j == 0:
  2047. # previous point was non-intersection, place 1 new point
  2048. oversample = [1.0 - self.oversampling]
  2049. else:
  2050. # previous point was intersection, place 2 new points
  2051. oversample = [self.oversampling, 1.0 - self.oversampling]
  2052. for dist in oversample:
  2053. point_over = (last[0] + dist * direction[0],
  2054. last[1] + dist * direction[1])
  2055. segment_add_points_sorted_overs[i].append(point_over)
  2056. segment_add_points_sorted_overs[i].append(p_inter)
  2057. last = p_inter
  2058. is_last_in_group = (j == len(segment_add_points[i]) - 1)
  2059. if is_last_in_group:
  2060. # previous point was oversampled, next point is
  2061. # non-intersection, place 1 new point between the two
  2062. exterior_point = exterior[(i + 1) % len(exterior)]
  2063. direction = (exterior_point[0] - last[0],
  2064. exterior_point[1] - last[1])
  2065. segment_add_points_sorted_overs[i].append(
  2066. (last[0] + self.oversampling * direction[0],
  2067. last[1] + self.oversampling * direction[1])
  2068. )
  2069. last = segment_add_points_sorted_overs[i][-1]
  2070. n_points += len(segment_add_points_sorted_overs[i])
  2071. if n_points > self.oversample_up_to_n_points_max:
  2072. return segment_add_points_sorted_overs
  2073. return segment_add_points_sorted_overs
  2074. @classmethod
  2075. def _insert_intersection_points(cls, exterior, segment_add_points):
  2076. # segment_add_points must be sorted
  2077. assert len(exterior) == len(segment_add_points), (
  2078. "Expected one entry in 'segment_add_points' for every point in "
  2079. "the exterior. Got %d (segment_add_points) and %d (exterior) "
  2080. "entries instead." % (len(segment_add_points), len(exterior)))
  2081. exterior_interp = []
  2082. for i, point0 in enumerate(exterior):
  2083. point0 = exterior[i]
  2084. exterior_interp.append(point0)
  2085. for p_inter in segment_add_points[i]:
  2086. exterior_interp.append(p_inter)
  2087. return exterior_interp
  2088. def _fit_best_valid_polygon(self, points, random_state):
  2089. if len(points) < 2:
  2090. return None
  2091. def _compute_distance_point_to_line(point, line_start, line_end):
  2092. x_diff = line_end[0] - line_start[0]
  2093. y_diff = line_end[1] - line_start[1]
  2094. num = abs(
  2095. y_diff*point[0] - x_diff*point[1]
  2096. + line_end[0]*line_start[1] - line_end[1]*line_start[0]
  2097. )
  2098. den = np.sqrt(y_diff**2 + x_diff**2)
  2099. if den == 0:
  2100. return np.sqrt(
  2101. (point[0] - line_start[0])**2
  2102. + (point[1] - line_start[1])**2)
  2103. return num / den
  2104. poly = Polygon(points)
  2105. if poly.is_valid:
  2106. return sm.xrange(len(points))
  2107. hull = scipy.spatial.ConvexHull(points)
  2108. points_kept = list(hull.vertices)
  2109. points_left = [i for i in range(len(points)) if i not in points_kept]
  2110. iteration = 0
  2111. n_changes = 0
  2112. converged = False
  2113. while not converged:
  2114. candidates = []
  2115. # estimate distance metrics for points-segment pairs:
  2116. # (1) distance (in vertices) between point and segment-start-point
  2117. # in original input point chain
  2118. # (2) euclidean distance between point and segment/line
  2119. # TODO this can be done more efficiently by caching the values and
  2120. # only computing distances to segments that have changed in
  2121. # the last iteration
  2122. # TODO these distances are not really the best metrics here.
  2123. # Something like IoU between new and old (invalid) polygon
  2124. # would be better, but can probably only be computed for
  2125. # pairs of valid polygons. Maybe something based on pointwise
  2126. # distances, where the points are sampled on the edges (not
  2127. # edge vertices themselves). Maybe something based on drawing
  2128. # the perimeter on images or based on distance maps.
  2129. point_kept_idx_to_pos = {
  2130. point_idx: i for i, point_idx in enumerate(points_kept)}
  2131. # generate all possible combinations from <points_kept> and
  2132. # <points_left>
  2133. combos = np.transpose([
  2134. np.tile(
  2135. np.int32(points_left), len(np.int32(points_kept))
  2136. ),
  2137. np.repeat(
  2138. np.int32(points_kept), len(np.int32(points_left))
  2139. )
  2140. ])
  2141. combos = np.concatenate(
  2142. (combos, np.zeros((combos.shape[0], 3), dtype=np.int32)),
  2143. axis=1)
  2144. # copy columns 0, 1 into 2, 3 so that 2 is always the lower value
  2145. mask = combos[:, 0] < combos[:, 1]
  2146. combos[:, 2:4] = combos[:, 0:2]
  2147. combos[mask, 2] = combos[mask, 1]
  2148. combos[mask, 3] = combos[mask, 0]
  2149. # distance (in indices) between each pair of <point_kept> and
  2150. # <point_left>
  2151. combos[:, 4] = np.minimum(
  2152. combos[:, 3] - combos[:, 2],
  2153. len(points) - combos[:, 3] + combos[:, 2]
  2154. )
  2155. # limit candidates
  2156. max_dist = self.fit_max_dist_other_iters
  2157. if iteration > 0:
  2158. max_dist = self.fit_max_dist_first_iter
  2159. candidate_rows = combos[combos[:, 4] <= max_dist]
  2160. do_limit = (
  2161. self.fit_n_candidates_before_sort_max is not None
  2162. and len(candidate_rows) > self.fit_n_candidates_before_sort_max)
  2163. if do_limit:
  2164. random_state.shuffle(candidate_rows)
  2165. candidate_rows = candidate_rows[
  2166. 0:self.fit_n_candidates_before_sort_max]
  2167. for row in candidate_rows:
  2168. point_left_idx = row[0]
  2169. point_kept_idx = row[1]
  2170. in_points_kept_pos = point_kept_idx_to_pos[point_kept_idx]
  2171. segment_start_idx = point_kept_idx
  2172. segment_end_idx = points_kept[
  2173. (in_points_kept_pos+1) % len(points_kept)]
  2174. segment_start = points[segment_start_idx]
  2175. segment_end = points[segment_end_idx]
  2176. if iteration == 0:
  2177. dist_eucl = 0
  2178. else:
  2179. dist_eucl = _compute_distance_point_to_line(
  2180. points[point_left_idx], segment_start, segment_end)
  2181. candidates.append(
  2182. (point_left_idx, point_kept_idx, row[4], dist_eucl))
  2183. # Sort computed distances first by minimal vertex-distance (see
  2184. # above, metric 1) (ASC), then by euclidean distance
  2185. # (metric 2) (ASC).
  2186. candidate_ids = np.arange(len(candidates))
  2187. candidate_ids = sorted(
  2188. candidate_ids,
  2189. key=lambda idx: (candidates[idx][2], candidates[idx][3]))
  2190. if self.fit_n_changes_max is not None:
  2191. candidate_ids = candidate_ids[:self.fit_n_changes_max]
  2192. # Iterate over point-segment pairs in sorted order. For each such
  2193. # candidate: Add the point to the already collected points,
  2194. # create a polygon from that and check if the polygon is valid.
  2195. # If it is, add the point to the output list and recalculate
  2196. # distance metrics. If it isn't valid, proceed with the next
  2197. # candidate until no more candidates are left.
  2198. #
  2199. # small change: this now no longer breaks upon the first found
  2200. # point that leads to a valid polygon, but checks all candidates
  2201. # instead
  2202. is_valid = False
  2203. done = set()
  2204. for candidate_idx in candidate_ids:
  2205. point_left_idx = candidates[candidate_idx][0]
  2206. point_kept_idx = candidates[candidate_idx][1]
  2207. if (point_left_idx, point_kept_idx) not in done:
  2208. in_points_kept_idx = [
  2209. i
  2210. for i, point_idx
  2211. in enumerate(points_kept)
  2212. if point_idx == point_kept_idx
  2213. ][0]
  2214. points_kept_hypothesis = points_kept[:]
  2215. points_kept_hypothesis.insert(
  2216. in_points_kept_idx+1,
  2217. point_left_idx)
  2218. poly_hypothesis = Polygon([
  2219. points[idx] for idx in points_kept_hypothesis])
  2220. if poly_hypothesis.is_valid:
  2221. is_valid = True
  2222. points_kept = points_kept_hypothesis
  2223. points_left = [point_idx
  2224. for point_idx
  2225. in points_left
  2226. if point_idx != point_left_idx]
  2227. n_changes += 1
  2228. if n_changes >= self.fit_n_changes_max:
  2229. return points_kept
  2230. done.add((point_left_idx, point_kept_idx))
  2231. done.add((point_kept_idx, point_left_idx))
  2232. # none of the left points could be used to create a valid polygon?
  2233. # (this automatically covers the case of no points being left)
  2234. if not is_valid and iteration > 0:
  2235. converged = True
  2236. iteration += 1
  2237. has_reached_iters_max = (
  2238. self.fit_n_iters_max is not None
  2239. and iteration > self.fit_n_iters_max)
  2240. if has_reached_iters_max:
  2241. break
  2242. return points_kept
  2243. # TODO remove this? was previously only used by Polygon.clip_*(), but that
  2244. # doesn't use it anymore
  2245. class MultiPolygon(object):
  2246. """
  2247. Class that represents several polygons.
  2248. Parameters
  2249. ----------
  2250. geoms : list of imgaug.augmentables.polys.Polygon
  2251. List of the polygons.
  2252. """
  2253. def __init__(self, geoms):
  2254. """Create a new MultiPolygon instance."""
  2255. assert (
  2256. len(geoms) == 0
  2257. or all([isinstance(el, Polygon) for el in geoms])), (
  2258. "Expected 'geoms' to a list of Polygon instances. "
  2259. "Got types %s." % (", ".join([str(el) for el in geoms])))
  2260. self.geoms = geoms
  2261. @staticmethod
  2262. def from_shapely(geometry, label=None):
  2263. """Create a MultiPolygon from a shapely object.
  2264. This also creates all necessary ``Polygon`` s contained in this
  2265. ``MultiPolygon``.
  2266. Parameters
  2267. ----------
  2268. geometry : shapely.geometry.MultiPolygon or shapely.geometry.Polygon or shapely.geometry.collection.GeometryCollection
  2269. The object to convert to a MultiPolygon.
  2270. label : None or str, optional
  2271. A label assigned to all Polygons within the MultiPolygon.
  2272. Returns
  2273. -------
  2274. imgaug.augmentables.polys.MultiPolygon
  2275. The derived MultiPolygon.
  2276. """
  2277. # load shapely lazily, which makes the dependency more optional
  2278. import shapely.geometry
  2279. if isinstance(geometry, shapely.geometry.MultiPolygon):
  2280. return MultiPolygon([
  2281. Polygon.from_shapely(poly, label=label)
  2282. for poly
  2283. in geometry.geoms])
  2284. if isinstance(geometry, shapely.geometry.Polygon):
  2285. return MultiPolygon([Polygon.from_shapely(geometry, label=label)])
  2286. if isinstance(geometry,
  2287. shapely.geometry.collection.GeometryCollection):
  2288. assert all([
  2289. isinstance(poly, shapely.geometry.Polygon)
  2290. for poly
  2291. in geometry.geoms]), (
  2292. "Expected the geometry collection to only contain shapely "
  2293. "polygons. Got types %s." % (
  2294. ", ".join([str(type(v)) for v in geometry.geoms])))
  2295. return MultiPolygon([
  2296. Polygon.from_shapely(poly, label=label)
  2297. for poly
  2298. in geometry.geoms])
  2299. raise Exception(
  2300. "Unknown datatype '%s'. Expected shapely.geometry.Polygon or "
  2301. "shapely.geometry.MultiPolygon or "
  2302. "shapely.geometry.collections.GeometryCollection." % (
  2303. type(geometry),))