lines.py 85 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442
  1. """Classes representing lines."""
  2. from __future__ import print_function, division, absolute_import
  3. import copy as copylib
  4. import numpy as np
  5. import skimage.draw
  6. import skimage.measure
  7. import cv2
  8. from .. import imgaug as ia
  9. from .base import IAugmentable
  10. from .utils import (normalize_shape,
  11. project_coords_,
  12. interpolate_points,
  13. _remove_out_of_image_fraction_,
  14. _normalize_shift_args)
  15. # TODO Add Line class and make LineString a list of Line elements
  16. # TODO add to_distance_maps(), compute_hausdorff_distance(), intersects(),
  17. # find_self_intersections(), is_self_intersecting(),
  18. # remove_self_intersections()
  19. class LineString(object):
  20. """Class representing line strings.
  21. A line string is a collection of connected line segments, each
  22. having a start and end point. Each point is given as its ``(x, y)``
  23. absolute (sub-)pixel coordinates. The end point of each segment is
  24. also the start point of the next segment.
  25. The line string is not closed, i.e. start and end point are expected to
  26. differ and will not be connected in drawings.
  27. Parameters
  28. ----------
  29. coords : iterable of tuple of number or ndarray
  30. The points of the line string.
  31. label : None or str, optional
  32. The label of the line string.
  33. """
  34. def __init__(self, coords, label=None):
  35. """Create a new LineString instance."""
  36. # use the conditions here to avoid unnecessary copies of ndarray inputs
  37. if ia.is_np_array(coords):
  38. if coords.dtype.name != "float32":
  39. coords = coords.astype(np.float32)
  40. elif len(coords) == 0:
  41. coords = np.zeros((0, 2), dtype=np.float32)
  42. else:
  43. assert ia.is_iterable(coords), (
  44. "Expected 'coords' to be an iterable, "
  45. "got type %s." % (type(coords),))
  46. assert all([len(coords_i) == 2 for coords_i in coords]), (
  47. "Expected 'coords' to contain (x,y) tuples, "
  48. "got %s." % (str(coords),))
  49. coords = np.float32(coords)
  50. assert coords.ndim == 2 and coords.shape[-1] == 2, (
  51. "Expected 'coords' to have shape (N, 2), got shape %s." % (
  52. coords.shape,))
  53. self.coords = coords
  54. self.label = label
  55. @property
  56. def length(self):
  57. """Compute the total euclidean length of the line string.
  58. Returns
  59. -------
  60. float
  61. The length based on euclidean distance, i.e. the sum of the
  62. lengths of each line segment.
  63. """
  64. if len(self.coords) == 0:
  65. return 0
  66. return np.sum(self.compute_neighbour_distances())
  67. @property
  68. def xx(self):
  69. """Get an array of x-coordinates of all points of the line string.
  70. Returns
  71. -------
  72. ndarray
  73. ``float32`` x-coordinates of the line string points.
  74. """
  75. return self.coords[:, 0]
  76. @property
  77. def yy(self):
  78. """Get an array of y-coordinates of all points of the line string.
  79. Returns
  80. -------
  81. ndarray
  82. ``float32`` y-coordinates of the line string points.
  83. """
  84. return self.coords[:, 1]
  85. @property
  86. def xx_int(self):
  87. """Get an array of discrete x-coordinates of all points.
  88. The conversion from ``float32`` coordinates to ``int32`` is done
  89. by first rounding the coordinates to the closest integer and then
  90. removing everything after the decimal point.
  91. Returns
  92. -------
  93. ndarray
  94. ``int32`` x-coordinates of the line string points.
  95. """
  96. return np.round(self.xx).astype(np.int32)
  97. @property
  98. def yy_int(self):
  99. """Get an array of discrete y-coordinates of all points.
  100. The conversion from ``float32`` coordinates to ``int32`` is done
  101. by first rounding the coordinates to the closest integer and then
  102. removing everything after the decimal point.
  103. Returns
  104. -------
  105. ndarray
  106. ``int32`` y-coordinates of the line string points.
  107. """
  108. return np.round(self.yy).astype(np.int32)
  109. @property
  110. def height(self):
  111. """Compute the height of a bounding box encapsulating the line.
  112. The height is computed based on the two points with lowest and
  113. largest y-coordinates.
  114. Returns
  115. -------
  116. float
  117. The height of the line string.
  118. """
  119. if len(self.coords) <= 1:
  120. return 0
  121. return np.max(self.yy) - np.min(self.yy)
  122. @property
  123. def width(self):
  124. """Compute the width of a bounding box encapsulating the line.
  125. The width is computed based on the two points with lowest and
  126. largest x-coordinates.
  127. Returns
  128. -------
  129. float
  130. The width of the line string.
  131. """
  132. if len(self.coords) <= 1:
  133. return 0
  134. return np.max(self.xx) - np.min(self.xx)
  135. def get_pointwise_inside_image_mask(self, image):
  136. """Determine per point whether it is inside of a given image plane.
  137. Parameters
  138. ----------
  139. image : ndarray or tuple of int
  140. Either an image with shape ``(H,W,[C])`` or a ``tuple`` denoting
  141. such an image shape.
  142. Returns
  143. -------
  144. ndarray
  145. ``(N,) ``bool`` array with one value for each of the ``N`` points
  146. indicating whether it is inside of the provided image
  147. plane (``True``) or not (``False``).
  148. """
  149. # pylint: disable=misplaced-comparison-constant
  150. if len(self.coords) == 0:
  151. return np.zeros((0,), dtype=bool)
  152. shape = normalize_shape(image)
  153. height, width = shape[0:2]
  154. x_within = np.logical_and(0 <= self.xx, self.xx < width)
  155. y_within = np.logical_and(0 <= self.yy, self.yy < height)
  156. return np.logical_and(x_within, y_within)
  157. # TODO add closed=False/True?
  158. def compute_neighbour_distances(self):
  159. """Compute the euclidean distance between each two consecutive points.
  160. Returns
  161. -------
  162. ndarray
  163. ``(N-1,)`` ``float32`` array of euclidean distances between point
  164. pairs. Same order as in `coords`.
  165. """
  166. if len(self.coords) <= 1:
  167. return np.zeros((0,), dtype=np.float32)
  168. return np.sqrt(
  169. np.sum(
  170. (self.coords[:-1, :] - self.coords[1:, :]) ** 2,
  171. axis=1
  172. )
  173. )
  174. # TODO change output to array
  175. def compute_pointwise_distances(self, other, default=None):
  176. """Compute min distances between points of this and another line string.
  177. Parameters
  178. ----------
  179. other : tuple of number or imgaug.augmentables.kps.Keypoint or imgaug.augmentables.LineString
  180. Other object to which to compute the distances.
  181. default : any
  182. Value to return if `other` contains no points.
  183. Returns
  184. -------
  185. list of float or any
  186. For each coordinate of this line string, the distance to any
  187. closest location on `other`.
  188. `default` if no distance could be computed.
  189. """
  190. import shapely.geometry
  191. from .kps import Keypoint
  192. if isinstance(other, Keypoint):
  193. other = shapely.geometry.Point((other.x, other.y))
  194. elif isinstance(other, LineString):
  195. if len(other.coords) == 0:
  196. return default
  197. if len(other.coords) == 1:
  198. other = shapely.geometry.Point(other.coords[0, :])
  199. else:
  200. other = shapely.geometry.LineString(other.coords)
  201. elif isinstance(other, tuple):
  202. assert len(other) == 2, (
  203. "Expected tuple 'other' to contain exactly two entries, "
  204. "got %d." % (len(other),))
  205. other = shapely.geometry.Point(other)
  206. else:
  207. raise ValueError(
  208. "Expected Keypoint or LineString or tuple (x,y), "
  209. "got type %s." % (type(other),))
  210. return [shapely.geometry.Point(point).distance(other)
  211. for point in self.coords]
  212. def compute_distance(self, other, default=None):
  213. """Compute the minimal distance between the line string and `other`.
  214. Parameters
  215. ----------
  216. other : tuple of number or imgaug.augmentables.kps.Keypoint or imgaug.augmentables.LineString
  217. Other object to which to compute the distance.
  218. default : any
  219. Value to return if this line string or `other` contain no points.
  220. Returns
  221. -------
  222. float or any
  223. Minimal distance to `other` or `default` if no distance could be
  224. computed.
  225. """
  226. # FIXME this computes distance pointwise, does not have to be identical
  227. # with the actual min distance (e.g. edge center to other's point)
  228. distances = self.compute_pointwise_distances(other, default=[])
  229. if len(distances) == 0:
  230. return default
  231. return min(distances)
  232. # TODO update BB's contains(), which can only accept Keypoint currently
  233. def contains(self, other, max_distance=1e-4):
  234. """Estimate whether a point is on this line string.
  235. This method uses a maximum distance to estimate whether a point is
  236. on a line string.
  237. Parameters
  238. ----------
  239. other : tuple of number or imgaug.augmentables.kps.Keypoint
  240. Point to check for.
  241. max_distance : float
  242. Maximum allowed euclidean distance between the point and the
  243. closest point on the line. If the threshold is exceeded, the point
  244. is not considered to fall on the line.
  245. Returns
  246. -------
  247. bool
  248. ``True`` if the point is on the line string, ``False`` otherwise.
  249. """
  250. return self.compute_distance(other, default=np.inf) < max_distance
  251. def project_(self, from_shape, to_shape):
  252. """Project the line string onto a differently shaped image in-place.
  253. E.g. if a point of the line string is on its original image at
  254. ``x=(10 of 100 pixels)`` and ``y=(20 of 100 pixels)`` and is projected
  255. onto a new image with size ``(width=200, height=200)``, its new
  256. position will be ``(x=20, y=40)``.
  257. This is intended for cases where the original image is resized.
  258. It cannot be used for more complex changes (e.g. padding, cropping).
  259. Added in 0.4.0.
  260. Parameters
  261. ----------
  262. from_shape : tuple of int or ndarray
  263. Shape of the original image. (Before resize.)
  264. to_shape : tuple of int or ndarray
  265. Shape of the new image. (After resize.)
  266. Returns
  267. -------
  268. imgaug.augmentables.lines.LineString
  269. Line string with new coordinates.
  270. The object may have been modified in-place.
  271. """
  272. self.coords = project_coords_(self.coords, from_shape, to_shape)
  273. return self
  274. def project(self, from_shape, to_shape):
  275. """Project the line string onto a differently shaped image.
  276. E.g. if a point of the line string is on its original image at
  277. ``x=(10 of 100 pixels)`` and ``y=(20 of 100 pixels)`` and is projected
  278. onto a new image with size ``(width=200, height=200)``, its new
  279. position will be ``(x=20, y=40)``.
  280. This is intended for cases where the original image is resized.
  281. It cannot be used for more complex changes (e.g. padding, cropping).
  282. Parameters
  283. ----------
  284. from_shape : tuple of int or ndarray
  285. Shape of the original image. (Before resize.)
  286. to_shape : tuple of int or ndarray
  287. Shape of the new image. (After resize.)
  288. Returns
  289. -------
  290. imgaug.augmentables.lines.LineString
  291. Line string with new coordinates.
  292. """
  293. return self.deepcopy().project_(from_shape, to_shape)
  294. def compute_out_of_image_fraction(self, image):
  295. """Compute fraction of polygon area outside of the image plane.
  296. This estimates ``f = A_ooi / A``, where ``A_ooi`` is the area of the
  297. polygon that is outside of the image plane, while ``A`` is the
  298. total area of the bounding box.
  299. Added in 0.4.0.
  300. Parameters
  301. ----------
  302. image : (H,W,...) ndarray or tuple of int
  303. Image dimensions to use.
  304. If an ``ndarray``, its shape will be used.
  305. If a ``tuple``, it is assumed to represent the image shape
  306. and must contain at least two integers.
  307. Returns
  308. -------
  309. float
  310. Fraction of the polygon area that is outside of the image
  311. plane. Returns ``0.0`` if the polygon is fully inside of
  312. the image plane. If the polygon has an area of zero, the polygon
  313. is treated similarly to a :class:`LineString`, i.e. the fraction
  314. of the line that is inside the image plane is returned.
  315. """
  316. length = self.length
  317. if length == 0:
  318. if len(self.coords) == 0:
  319. return 0.0
  320. points_ooi = ~self.get_pointwise_inside_image_mask(image)
  321. return 1.0 if np.all(points_ooi) else 0.0
  322. lss_clipped = self.clip_out_of_image(image)
  323. length_after_clip = sum([ls.length for ls in lss_clipped])
  324. inside_image_factor = length_after_clip / length
  325. return 1.0 - inside_image_factor
  326. def is_fully_within_image(self, image, default=False):
  327. """Estimate whether the line string is fully inside an image plane.
  328. Parameters
  329. ----------
  330. image : ndarray or tuple of int
  331. Either an image with shape ``(H,W,[C])`` or a ``tuple`` denoting
  332. such an image shape.
  333. default : any
  334. Default value to return if the line string contains no points.
  335. Returns
  336. -------
  337. bool or any
  338. ``True`` if the line string is fully inside the image area.
  339. ``False`` otherwise.
  340. Will return `default` if this line string contains no points.
  341. """
  342. if len(self.coords) == 0:
  343. return default
  344. return np.all(self.get_pointwise_inside_image_mask(image))
  345. def is_partly_within_image(self, image, default=False):
  346. """
  347. Estimate whether the line string is at least partially inside the image.
  348. Parameters
  349. ----------
  350. image : ndarray or tuple of int
  351. Either an image with shape ``(H,W,[C])`` or a ``tuple`` denoting
  352. such an image shape.
  353. default : any
  354. Default value to return if the line string contains no points.
  355. Returns
  356. -------
  357. bool or any
  358. ``True`` if the line string is at least partially inside the image
  359. area. ``False`` otherwise.
  360. Will return `default` if this line string contains no points.
  361. """
  362. if len(self.coords) == 0:
  363. return default
  364. # check mask first to avoid costly computation of intersection points
  365. # whenever possible
  366. mask = self.get_pointwise_inside_image_mask(image)
  367. if np.any(mask):
  368. return True
  369. return len(self.clip_out_of_image(image)) > 0
  370. def is_out_of_image(self, image, fully=True, partly=False, default=True):
  371. """
  372. Estimate whether the line is partially/fully outside of the image area.
  373. Parameters
  374. ----------
  375. image : ndarray or tuple of int
  376. Either an image with shape ``(H,W,[C])`` or a tuple denoting
  377. such an image shape.
  378. fully : bool, optional
  379. Whether to return ``True`` if the line string is fully outside
  380. of the image area.
  381. partly : bool, optional
  382. Whether to return ``True`` if the line string is at least partially
  383. outside fo the image area.
  384. default : any
  385. Default value to return if the line string contains no points.
  386. Returns
  387. -------
  388. bool or any
  389. ``True`` if the line string is partially/fully outside of the image
  390. area, depending on defined parameters.
  391. ``False`` otherwise.
  392. Will return `default` if this line string contains no points.
  393. """
  394. if len(self.coords) == 0:
  395. return default
  396. if self.is_fully_within_image(image):
  397. return False
  398. if self.is_partly_within_image(image):
  399. return partly
  400. return fully
  401. def clip_out_of_image(self, image):
  402. """Clip off all parts of the line string that are outside of the image.
  403. Parameters
  404. ----------
  405. image : ndarray or tuple of int
  406. Either an image with shape ``(H,W,[C])`` or a ``tuple`` denoting
  407. such an image shape.
  408. Returns
  409. -------
  410. list of imgaug.augmentables.lines.LineString
  411. Line strings, clipped to the image shape.
  412. The result may contain any number of line strins, including zero.
  413. """
  414. if len(self.coords) == 0:
  415. return []
  416. inside_image_mask = self.get_pointwise_inside_image_mask(image)
  417. ooi_mask = ~inside_image_mask
  418. if len(self.coords) == 1:
  419. if not np.any(inside_image_mask):
  420. return []
  421. return [self.copy()]
  422. if np.all(inside_image_mask):
  423. return [self.copy()]
  424. # top, right, bottom, left image edges
  425. # we subtract eps here, because intersection() works inclusively,
  426. # i.e. not subtracting eps would be equivalent to 0<=x<=C for C being
  427. # height or width
  428. # don't set the eps too low, otherwise points at height/width seem
  429. # to get rounded to height/width by shapely, which can cause problems
  430. # when first clipping and then calling is_fully_within_image()
  431. # returning false
  432. height, width = normalize_shape(image)[0:2]
  433. eps = 1e-3
  434. edges = [
  435. LineString([(0.0, 0.0), (width - eps, 0.0)]),
  436. LineString([(width - eps, 0.0), (width - eps, height - eps)]),
  437. LineString([(width - eps, height - eps), (0.0, height - eps)]),
  438. LineString([(0.0, height - eps), (0.0, 0.0)])
  439. ]
  440. intersections = self.find_intersections_with(edges)
  441. points = []
  442. gen = enumerate(zip(self.coords[:-1], self.coords[1:],
  443. ooi_mask[:-1], ooi_mask[1:],
  444. intersections))
  445. for i, (line_start, line_end, ooi_start, ooi_end, inter_line) in gen:
  446. points.append((line_start, False, ooi_start))
  447. for p_inter in inter_line:
  448. points.append((p_inter, True, False))
  449. is_last = (i == len(self.coords) - 2)
  450. if is_last and not ooi_end:
  451. points.append((line_end, False, ooi_end))
  452. lines = []
  453. line = []
  454. for i, (coord, was_added, ooi) in enumerate(points):
  455. # remove any point that is outside of the image,
  456. # also start a new line once such a point is detected
  457. if ooi:
  458. if len(line) > 0:
  459. lines.append(line)
  460. line = []
  461. continue
  462. if not was_added:
  463. # add all points that were part of the original line string
  464. # AND that are inside the image plane
  465. line.append(coord)
  466. else:
  467. is_last_point = (i == len(points)-1)
  468. # ooi is a numpy.bool_, hence the bool(.)
  469. is_next_ooi = (not is_last_point
  470. and bool(points[i+1][2]) is True)
  471. # Add all points that were new (i.e. intersections), so
  472. # long that they aren't essentially identical to other point.
  473. # This prevents adding overlapping intersections multiple times.
  474. # (E.g. when a line intersects with a corner of the image plane
  475. # and also with one of its edges.)
  476. p_prev = line[-1] if len(line) > 0 else None
  477. # ignore next point if end reached or next point is out of image
  478. p_next = None
  479. if not is_last_point and not is_next_ooi:
  480. p_next = points[i+1][0]
  481. dist_prev = None
  482. dist_next = None
  483. if p_prev is not None:
  484. dist_prev = np.linalg.norm(
  485. np.float32(coord) - np.float32(p_prev))
  486. if p_next is not None:
  487. dist_next = np.linalg.norm(
  488. np.float32(coord) - np.float32(p_next))
  489. dist_prev_ok = (dist_prev is None or dist_prev > 1e-2)
  490. dist_next_ok = (dist_next is None or dist_next > 1e-2)
  491. if dist_prev_ok and dist_next_ok:
  492. line.append(coord)
  493. if len(line) > 0:
  494. lines.append(line)
  495. lines = [line for line in lines if len(line) > 0]
  496. return [self.deepcopy(coords=line) for line in lines]
  497. # TODO add tests for this
  498. # TODO extend this to non line string geometries
  499. def find_intersections_with(self, other):
  500. """Find all intersection points between this line string and `other`.
  501. Parameters
  502. ----------
  503. other : tuple of number or list of tuple of number or list of LineString or LineString
  504. The other geometry to use during intersection tests.
  505. Returns
  506. -------
  507. list of list of tuple of number
  508. All intersection points. One list per pair of consecutive start
  509. and end point, i.e. `N-1` lists of `N` points. Each list may
  510. be empty or may contain multiple points.
  511. """
  512. import shapely.geometry
  513. geom = _convert_var_to_shapely_geometry(other)
  514. result = []
  515. for p_start, p_end in zip(self.coords[:-1], self.coords[1:]):
  516. ls = shapely.geometry.LineString([p_start, p_end])
  517. intersections = ls.intersection(geom)
  518. intersections = list(_flatten_shapely_collection(intersections))
  519. intersections_points = []
  520. for inter in intersections:
  521. if isinstance(inter, shapely.geometry.linestring.LineString):
  522. # Since shapely 1.7a2 (tested in python 3.8),
  523. # .intersection() apprently can return LINE STRING EMPTY
  524. # (i.e. .coords is an empty list). Before that, the result
  525. # of .intersection() was just []. Hence, we first check
  526. # the length here.
  527. if len(inter.coords) > 0:
  528. inter_start = (inter.coords[0][0], inter.coords[0][1])
  529. inter_end = (inter.coords[-1][0], inter.coords[-1][1])
  530. intersections_points.extend([inter_start, inter_end])
  531. else:
  532. assert isinstance(inter, shapely.geometry.point.Point), (
  533. "Expected to find shapely.geometry.point.Point or "
  534. "shapely.geometry.linestring.LineString intersection, "
  535. "actually found %s." % (type(inter),))
  536. intersections_points.append((inter.x, inter.y))
  537. # sort by distance to start point, this makes it later on easier
  538. # to remove duplicate points
  539. inter_sorted = sorted(
  540. intersections_points,
  541. key=lambda p, ps=p_start: np.linalg.norm(np.float32(p) - ps)
  542. )
  543. result.append(inter_sorted)
  544. return result
  545. def shift_(self, x=0, y=0):
  546. """Move this line string along the x/y-axis in-place.
  547. The origin ``(0, 0)`` is at the top left of the image.
  548. Added in 0.4.0.
  549. Parameters
  550. ----------
  551. x : number, optional
  552. Value to be added to all x-coordinates. Positive values shift
  553. towards the right images.
  554. y : number, optional
  555. Value to be added to all y-coordinates. Positive values shift
  556. towards the bottom images.
  557. Returns
  558. -------
  559. result : imgaug.augmentables.lines.LineString
  560. Shifted line string.
  561. The object may have been modified in-place.
  562. """
  563. self.coords[:, 0] += x
  564. self.coords[:, 1] += y
  565. return self
  566. def shift(self, x=0, y=0, top=None, right=None, bottom=None, left=None):
  567. """Move this line string along the x/y-axis.
  568. The origin ``(0, 0)`` is at the top left of the image.
  569. Parameters
  570. ----------
  571. x : number, optional
  572. Value to be added to all x-coordinates. Positive values shift
  573. towards the right images.
  574. y : number, optional
  575. Value to be added to all y-coordinates. Positive values shift
  576. towards the bottom images.
  577. top : None or int, optional
  578. Deprecated since 0.4.0.
  579. Amount of pixels by which to shift this object *from* the
  580. top (towards the bottom).
  581. right : None or int, optional
  582. Deprecated since 0.4.0.
  583. Amount of pixels by which to shift this object *from* the
  584. right (towards the left).
  585. bottom : None or int, optional
  586. Deprecated since 0.4.0.
  587. Amount of pixels by which to shift this object *from* the
  588. bottom (towards the top).
  589. left : None or int, optional
  590. Deprecated since 0.4.0.
  591. Amount of pixels by which to shift this object *from* the
  592. left (towards the right).
  593. Returns
  594. -------
  595. result : imgaug.augmentables.lines.LineString
  596. Shifted line string.
  597. """
  598. x, y = _normalize_shift_args(
  599. x, y, top=top, right=right, bottom=bottom, left=left)
  600. return self.deepcopy().shift_(x=x, y=y)
  601. def draw_mask(self, image_shape, size_lines=1, size_points=0,
  602. raise_if_out_of_image=False):
  603. """Draw this line segment as a binary image mask.
  604. Parameters
  605. ----------
  606. image_shape : tuple of int
  607. The shape of the image onto which to draw the line mask.
  608. size_lines : int, optional
  609. Thickness of the line segments.
  610. size_points : int, optional
  611. Size of the points in pixels.
  612. raise_if_out_of_image : bool, optional
  613. Whether to raise an error if the line string is fully
  614. outside of the image. If set to ``False``, no error will be
  615. raised and only the parts inside the image will be drawn.
  616. Returns
  617. -------
  618. ndarray
  619. Boolean line mask of shape `image_shape` (no channel axis).
  620. """
  621. heatmap = self.draw_heatmap_array(
  622. image_shape,
  623. alpha_lines=1.0, alpha_points=1.0,
  624. size_lines=size_lines, size_points=size_points,
  625. antialiased=False,
  626. raise_if_out_of_image=raise_if_out_of_image)
  627. return heatmap > 0.5
  628. def draw_lines_heatmap_array(self, image_shape, alpha=1.0,
  629. size=1, antialiased=True,
  630. raise_if_out_of_image=False):
  631. """Draw the line segments of this line string as a heatmap array.
  632. Parameters
  633. ----------
  634. image_shape : tuple of int
  635. The shape of the image onto which to draw the line mask.
  636. alpha : float, optional
  637. Opacity of the line string. Higher values denote a more visible
  638. line string.
  639. size : int, optional
  640. Thickness of the line segments.
  641. antialiased : bool, optional
  642. Whether to draw the line with anti-aliasing activated.
  643. raise_if_out_of_image : bool, optional
  644. Whether to raise an error if the line string is fully
  645. outside of the image. If set to ``False``, no error will be
  646. raised and only the parts inside the image will be drawn.
  647. Returns
  648. -------
  649. ndarray
  650. ``float32`` array of shape `image_shape` (no channel axis) with
  651. drawn line string. All values are in the interval ``[0.0, 1.0]``.
  652. """
  653. assert len(image_shape) == 2 or (
  654. len(image_shape) == 3 and image_shape[-1] == 1), (
  655. "Expected (H,W) or (H,W,1) as image_shape, got %s." % (
  656. image_shape,))
  657. arr = self.draw_lines_on_image(
  658. np.zeros(image_shape, dtype=np.uint8),
  659. color=255, alpha=alpha, size=size,
  660. antialiased=antialiased,
  661. raise_if_out_of_image=raise_if_out_of_image
  662. )
  663. return arr.astype(np.float32) / 255.0
  664. def draw_points_heatmap_array(self, image_shape, alpha=1.0,
  665. size=1, raise_if_out_of_image=False):
  666. """Draw the points of this line string as a heatmap array.
  667. Parameters
  668. ----------
  669. image_shape : tuple of int
  670. The shape of the image onto which to draw the point mask.
  671. alpha : float, optional
  672. Opacity of the line string points. Higher values denote a more
  673. visible points.
  674. size : int, optional
  675. Size of the points in pixels.
  676. raise_if_out_of_image : bool, optional
  677. Whether to raise an error if the line string is fully
  678. outside of the image. If set to ``False``, no error will be
  679. raised and only the parts inside the image will be drawn.
  680. Returns
  681. -------
  682. ndarray
  683. ``float32`` array of shape `image_shape` (no channel axis) with
  684. drawn line string points. All values are in the
  685. interval ``[0.0, 1.0]``.
  686. """
  687. assert len(image_shape) == 2 or (
  688. len(image_shape) == 3 and image_shape[-1] == 1), (
  689. "Expected (H,W) or (H,W,1) as image_shape, got %s." % (
  690. image_shape,))
  691. arr = self.draw_points_on_image(
  692. np.zeros(image_shape, dtype=np.uint8),
  693. color=255, alpha=alpha, size=size,
  694. raise_if_out_of_image=raise_if_out_of_image
  695. )
  696. return arr.astype(np.float32) / 255.0
  697. def draw_heatmap_array(self, image_shape, alpha_lines=1.0, alpha_points=1.0,
  698. size_lines=1, size_points=0, antialiased=True,
  699. raise_if_out_of_image=False):
  700. """
  701. Draw the line segments and points of the line string as a heatmap array.
  702. Parameters
  703. ----------
  704. image_shape : tuple of int
  705. The shape of the image onto which to draw the line mask.
  706. alpha_lines : float, optional
  707. Opacity of the line string. Higher values denote a more visible
  708. line string.
  709. alpha_points : float, optional
  710. Opacity of the line string points. Higher values denote a more
  711. visible points.
  712. size_lines : int, optional
  713. Thickness of the line segments.
  714. size_points : int, optional
  715. Size of the points in pixels.
  716. antialiased : bool, optional
  717. Whether to draw the line with anti-aliasing activated.
  718. raise_if_out_of_image : bool, optional
  719. Whether to raise an error if the line string is fully
  720. outside of the image. If set to ``False``, no error will be
  721. raised and only the parts inside the image will be drawn.
  722. Returns
  723. -------
  724. ndarray
  725. ``float32`` array of shape `image_shape` (no channel axis) with
  726. drawn line segments and points. All values are in the
  727. interval ``[0.0, 1.0]``.
  728. """
  729. heatmap_lines = self.draw_lines_heatmap_array(
  730. image_shape,
  731. alpha=alpha_lines,
  732. size=size_lines,
  733. antialiased=antialiased,
  734. raise_if_out_of_image=raise_if_out_of_image)
  735. if size_points <= 0:
  736. return heatmap_lines
  737. heatmap_points = self.draw_points_heatmap_array(
  738. image_shape,
  739. alpha=alpha_points,
  740. size=size_points,
  741. raise_if_out_of_image=raise_if_out_of_image)
  742. heatmap = np.dstack([heatmap_lines, heatmap_points])
  743. return np.max(heatmap, axis=2)
  744. # TODO only draw line on image of size BB around line, then paste into full
  745. # sized image
  746. def draw_lines_on_image(self, image, color=(0, 255, 0),
  747. alpha=1.0, size=3,
  748. antialiased=True,
  749. raise_if_out_of_image=False):
  750. """Draw the line segments of this line string on a given image.
  751. Parameters
  752. ----------
  753. image : ndarray or tuple of int
  754. The image onto which to draw.
  755. Expected to be ``uint8`` and of shape ``(H, W, C)`` with ``C``
  756. usually being ``3`` (other values are not tested).
  757. If a tuple, expected to be ``(H, W, C)`` and will lead to a new
  758. ``uint8`` array of zeros being created.
  759. color : int or iterable of int
  760. Color to use as RGB, i.e. three values.
  761. alpha : float, optional
  762. Opacity of the line string. Higher values denote a more visible
  763. line string.
  764. size : int, optional
  765. Thickness of the line segments.
  766. antialiased : bool, optional
  767. Whether to draw the line with anti-aliasing activated.
  768. raise_if_out_of_image : bool, optional
  769. Whether to raise an error if the line string is fully
  770. outside of the image. If set to ``False``, no error will be
  771. raised and only the parts inside the image will be drawn.
  772. Returns
  773. -------
  774. ndarray
  775. `image` with line drawn on it.
  776. """
  777. # pylint: disable=invalid-name, misplaced-comparison-constant
  778. from .. import dtypes as iadt
  779. from ..augmenters import blend as blendlib
  780. image_was_empty = False
  781. if isinstance(image, tuple):
  782. image_was_empty = True
  783. image = np.zeros(image, dtype=np.uint8)
  784. assert image.ndim in [2, 3], (
  785. "Expected image or shape of form (H,W) or (H,W,C), "
  786. "got shape %s." % (image.shape,))
  787. if len(self.coords) <= 1 or alpha < 0 + 1e-4 or size < 1:
  788. return np.copy(image)
  789. if raise_if_out_of_image \
  790. and self.is_out_of_image(image, partly=False, fully=True):
  791. raise Exception(
  792. "Cannot draw line string '%s' on image with shape %s, because "
  793. "it would be out of bounds." % (
  794. self.__str__(), image.shape))
  795. if image.ndim == 2:
  796. assert ia.is_single_number(color), (
  797. "Got a 2D image. Expected then 'color' to be a single number, "
  798. "but got %s." % (str(color),))
  799. color = [color]
  800. elif image.ndim == 3 and ia.is_single_number(color):
  801. color = [color] * image.shape[-1]
  802. image = image.astype(np.float32)
  803. height, width = image.shape[0:2]
  804. # We can't trivially exclude lines outside of the image here, because
  805. # even if start and end point are outside, there can still be parts of
  806. # the line inside the image.
  807. # TODO Do this with edge-wise intersection tests
  808. lines = []
  809. for line_start, line_end in zip(self.coords[:-1], self.coords[1:]):
  810. # note that line() expects order (y1, x1, y2, x2), hence ([1], [0])
  811. lines.append((line_start[1], line_start[0],
  812. line_end[1], line_end[0]))
  813. # skimage.draw.line can only handle integers
  814. lines = np.round(np.float32(lines)).astype(np.int32)
  815. # size == 0 is already covered above
  816. # Note here that we have to be careful not to draw lines two times
  817. # at their intersection points, e.g. for (p0, p1), (p1, 2) we could
  818. # end up drawing at p1 twice, leading to higher values if alpha is
  819. # used.
  820. color = np.float32(color)
  821. heatmap = np.zeros(image.shape[0:2], dtype=np.float32)
  822. for line in lines:
  823. if antialiased:
  824. rr, cc, val = skimage.draw.line_aa(*line)
  825. else:
  826. rr, cc = skimage.draw.line(*line)
  827. val = 1.0
  828. # mask check here, because line() can generate coordinates
  829. # outside of the image plane
  830. rr_mask = np.logical_and(0 <= rr, rr < height)
  831. cc_mask = np.logical_and(0 <= cc, cc < width)
  832. mask = np.logical_and(rr_mask, cc_mask)
  833. if np.any(mask):
  834. rr = rr[mask]
  835. cc = cc[mask]
  836. val = val[mask] if not ia.is_single_number(val) else val
  837. heatmap[rr, cc] = val * alpha
  838. if size > 1:
  839. kernel = np.ones((size, size), dtype=np.uint8)
  840. heatmap = cv2.dilate(heatmap, kernel)
  841. if image_was_empty:
  842. image_blend = image + heatmap * color
  843. else:
  844. image_color_shape = image.shape[0:2]
  845. if image.ndim == 3:
  846. image_color_shape = image_color_shape + (1,)
  847. image_color = np.tile(color, image_color_shape)
  848. image_blend = blendlib.blend_alpha(image_color, image, heatmap)
  849. image_blend = iadt.restore_dtypes_(image_blend, np.uint8)
  850. return image_blend
  851. def draw_points_on_image(self, image, color=(0, 128, 0),
  852. alpha=1.0, size=3,
  853. copy=True, raise_if_out_of_image=False):
  854. """Draw the points of this line string onto a given image.
  855. Parameters
  856. ----------
  857. image : ndarray or tuple of int
  858. The image onto which to draw.
  859. Expected to be ``uint8`` and of shape ``(H, W, C)`` with ``C``
  860. usually being ``3`` (other values are not tested).
  861. If a tuple, expected to be ``(H, W, C)`` and will lead to a new
  862. ``uint8`` array of zeros being created.
  863. color : iterable of int
  864. Color to use as RGB, i.e. three values.
  865. alpha : float, optional
  866. Opacity of the line string points. Higher values denote a more
  867. visible points.
  868. size : int, optional
  869. Size of the points in pixels.
  870. copy : bool, optional
  871. Whether it is allowed to draw directly in the input
  872. array (``False``) or it has to be copied (``True``).
  873. The routine may still have to copy, even if ``copy=False`` was
  874. used. Always use the return value.
  875. raise_if_out_of_image : bool, optional
  876. Whether to raise an error if the line string is fully
  877. outside of the image. If set to ``False``, no error will be
  878. raised and only the parts inside the image will be drawn.
  879. Returns
  880. -------
  881. ndarray
  882. ``float32`` array of shape `image_shape` (no channel axis) with
  883. drawn line string points. All values are in the
  884. interval ``[0.0, 1.0]``.
  885. """
  886. from .kps import KeypointsOnImage
  887. kpsoi = KeypointsOnImage.from_xy_array(self.coords, shape=image.shape)
  888. image = kpsoi.draw_on_image(
  889. image, color=color, alpha=alpha,
  890. size=size, copy=copy,
  891. raise_if_out_of_image=raise_if_out_of_image)
  892. return image
  893. def draw_on_image(self, image,
  894. color=(0, 255, 0), color_lines=None, color_points=None,
  895. alpha=1.0, alpha_lines=None, alpha_points=None,
  896. size=1, size_lines=None, size_points=None,
  897. antialiased=True,
  898. raise_if_out_of_image=False):
  899. """Draw this line string onto an image.
  900. Parameters
  901. ----------
  902. image : ndarray
  903. The `(H,W,C)` `uint8` image onto which to draw the line string.
  904. color : iterable of int, optional
  905. Color to use as RGB, i.e. three values.
  906. The color of the line and points are derived from this value,
  907. unless they are set.
  908. color_lines : None or iterable of int
  909. Color to use for the line segments as RGB, i.e. three values.
  910. If ``None``, this value is derived from `color`.
  911. color_points : None or iterable of int
  912. Color to use for the points as RGB, i.e. three values.
  913. If ``None``, this value is derived from ``0.5 * color``.
  914. alpha : float, optional
  915. Opacity of the line string. Higher values denote more visible
  916. points.
  917. The alphas of the line and points are derived from this value,
  918. unless they are set.
  919. alpha_lines : None or float, optional
  920. Opacity of the line string. Higher values denote more visible
  921. line string.
  922. If ``None``, this value is derived from `alpha`.
  923. alpha_points : None or float, optional
  924. Opacity of the line string points. Higher values denote more
  925. visible points.
  926. If ``None``, this value is derived from `alpha`.
  927. size : int, optional
  928. Size of the line string.
  929. The sizes of the line and points are derived from this value,
  930. unless they are set.
  931. size_lines : None or int, optional
  932. Thickness of the line segments.
  933. If ``None``, this value is derived from `size`.
  934. size_points : None or int, optional
  935. Size of the points in pixels.
  936. If ``None``, this value is derived from ``3 * size``.
  937. antialiased : bool, optional
  938. Whether to draw the line with anti-aliasing activated.
  939. This does currently not affect the point drawing.
  940. raise_if_out_of_image : bool, optional
  941. Whether to raise an error if the line string is fully
  942. outside of the image. If set to ``False``, no error will be
  943. raised and only the parts inside the image will be drawn.
  944. Returns
  945. -------
  946. ndarray
  947. Image with line string drawn on it.
  948. """
  949. def _assert_not_none(arg_name, arg_value):
  950. assert arg_value is not None, (
  951. "Expected '%s' to not be None, got type %s." % (
  952. arg_name, type(arg_value),))
  953. _assert_not_none("color", color)
  954. _assert_not_none("alpha", alpha)
  955. _assert_not_none("size", size)
  956. color_lines = color_lines if color_lines is not None \
  957. else np.float32(color)
  958. color_points = color_points if color_points is not None \
  959. else np.float32(color) * 0.5
  960. alpha_lines = alpha_lines if alpha_lines is not None \
  961. else np.float32(alpha)
  962. alpha_points = alpha_points if alpha_points is not None \
  963. else np.float32(alpha)
  964. size_lines = size_lines if size_lines is not None else size
  965. size_points = size_points if size_points is not None else size * 3
  966. image = self.draw_lines_on_image(
  967. image, color=np.array(color_lines).astype(np.uint8),
  968. alpha=alpha_lines, size=size_lines,
  969. antialiased=antialiased,
  970. raise_if_out_of_image=raise_if_out_of_image)
  971. image = self.draw_points_on_image(
  972. image, color=np.array(color_points).astype(np.uint8),
  973. alpha=alpha_points, size=size_points,
  974. copy=False,
  975. raise_if_out_of_image=raise_if_out_of_image)
  976. return image
  977. def extract_from_image(self, image, size=1, pad=True, pad_max=None,
  978. antialiased=True, prevent_zero_size=True):
  979. """Extract all image pixels covered by the line string.
  980. This will only extract pixels overlapping with the line string.
  981. As a rectangular image array has to be returned, non-overlapping
  982. pixels will be set to zero.
  983. This function will by default zero-pad the image if the line string is
  984. partially/fully outside of the image. This is for consistency with
  985. the same methods for bounding boxes and polygons.
  986. Parameters
  987. ----------
  988. image : ndarray
  989. The image of shape `(H,W,[C])` from which to extract the pixels
  990. within the line string.
  991. size : int, optional
  992. Thickness of the line.
  993. pad : bool, optional
  994. Whether to zero-pad the image if the object is partially/fully
  995. outside of it.
  996. pad_max : None or int, optional
  997. The maximum number of pixels that may be zero-paded on any side,
  998. i.e. if this has value ``N`` the total maximum of added pixels
  999. is ``4*N``.
  1000. This option exists to prevent extremely large images as a result of
  1001. single points being moved very far away during augmentation.
  1002. antialiased : bool, optional
  1003. Whether to apply anti-aliasing to the line string.
  1004. prevent_zero_size : bool, optional
  1005. Whether to prevent height or width of the extracted image from
  1006. becoming zero. If this is set to ``True`` and height or width of
  1007. the line string is below ``1``, the height/width will be increased
  1008. to ``1``. This can be useful to prevent problems, e.g. with image
  1009. saving or plotting. If it is set to ``False``, images will be
  1010. returned as ``(H', W')`` or ``(H', W', 3)`` with ``H`` or ``W``
  1011. potentially being ``0``.
  1012. Returns
  1013. -------
  1014. (H',W') ndarray or (H',W',C) ndarray
  1015. Pixels overlapping with the line string. Zero-padded if the
  1016. line string is partially/fully outside of the image and
  1017. ``pad=True``. If `prevent_zero_size` is activated, it is
  1018. guarantueed that ``H'>0`` and ``W'>0``, otherwise only
  1019. ``H'>=0`` and ``W'>=0``.
  1020. """
  1021. from .bbs import BoundingBox
  1022. assert image.ndim in [2, 3], (
  1023. "Expected image of shape (H,W,[C]), got shape %s." % (
  1024. image.shape,))
  1025. if len(self.coords) == 0 or size <= 0:
  1026. if prevent_zero_size:
  1027. return np.zeros((1, 1) + image.shape[2:], dtype=image.dtype)
  1028. return np.zeros((0, 0) + image.shape[2:], dtype=image.dtype)
  1029. xx = self.xx_int
  1030. yy = self.yy_int
  1031. # this would probably work if drawing was subpixel-accurate
  1032. # x1 = np.min(self.coords[:, 0]) - (size / 2)
  1033. # y1 = np.min(self.coords[:, 1]) - (size / 2)
  1034. # x2 = np.max(self.coords[:, 0]) + (size / 2)
  1035. # y2 = np.max(self.coords[:, 1]) + (size / 2)
  1036. # this works currently with non-subpixel-accurate drawing
  1037. sizeh = (size - 1) / 2
  1038. x1 = np.min(xx) - sizeh
  1039. y1 = np.min(yy) - sizeh
  1040. x2 = np.max(xx) + 1 + sizeh
  1041. y2 = np.max(yy) + 1 + sizeh
  1042. bb = BoundingBox(x1=x1, y1=y1, x2=x2, y2=y2)
  1043. if len(self.coords) == 1:
  1044. return bb.extract_from_image(image, pad=pad, pad_max=pad_max,
  1045. prevent_zero_size=prevent_zero_size)
  1046. heatmap = self.draw_lines_heatmap_array(
  1047. image.shape[0:2], alpha=1.0, size=size, antialiased=antialiased)
  1048. if image.ndim == 3:
  1049. heatmap = np.atleast_3d(heatmap)
  1050. image_masked = image.astype(np.float32) * heatmap
  1051. extract = bb.extract_from_image(image_masked, pad=pad, pad_max=pad_max,
  1052. prevent_zero_size=prevent_zero_size)
  1053. return np.clip(np.round(extract), 0, 255).astype(np.uint8)
  1054. def concatenate(self, other):
  1055. """Concatenate this line string with another one.
  1056. This will add a line segment between the end point of this line string
  1057. and the start point of `other`.
  1058. Parameters
  1059. ----------
  1060. other : imgaug.augmentables.lines.LineString or ndarray or iterable of tuple of number
  1061. The points to add to this line string.
  1062. Returns
  1063. -------
  1064. imgaug.augmentables.lines.LineString
  1065. New line string with concatenated points.
  1066. The `label` of this line string will be kept.
  1067. """
  1068. if not isinstance(other, LineString):
  1069. other = LineString(other)
  1070. return self.deepcopy(
  1071. coords=np.concatenate([self.coords, other.coords], axis=0))
  1072. # TODO add tests
  1073. def subdivide(self, points_per_edge):
  1074. """Derive a new line string with ``N`` interpolated points per edge.
  1075. The interpolated points have (per edge) regular distances to each
  1076. other.
  1077. For each edge between points ``A`` and ``B`` this adds points
  1078. at ``A + (i/(1+N)) * (B - A)``, where ``i`` is the index of the added
  1079. point and ``N`` is the number of points to add per edge.
  1080. Calling this method two times will split each edge at its center
  1081. and then again split each newly created edge at their center.
  1082. It is equivalent to calling `subdivide(3)`.
  1083. Parameters
  1084. ----------
  1085. points_per_edge : int
  1086. Number of points to interpolate on each edge.
  1087. Returns
  1088. -------
  1089. imgaug.augmentables.lines.LineString
  1090. Line string with subdivided edges.
  1091. """
  1092. if len(self.coords) <= 1 or points_per_edge < 1:
  1093. return self.deepcopy()
  1094. coords = interpolate_points(self.coords, nb_steps=points_per_edge,
  1095. closed=False)
  1096. return self.deepcopy(coords=coords)
  1097. def to_keypoints(self):
  1098. """Convert the line string points to keypoints.
  1099. Returns
  1100. -------
  1101. list of imgaug.augmentables.kps.Keypoint
  1102. Points of the line string as keypoints.
  1103. """
  1104. # TODO get rid of this deferred import
  1105. from imgaug.augmentables.kps import Keypoint
  1106. return [Keypoint(x=x, y=y) for (x, y) in self.coords]
  1107. def to_bounding_box(self):
  1108. """Generate a bounding box encapsulating the line string.
  1109. Returns
  1110. -------
  1111. None or imgaug.augmentables.bbs.BoundingBox
  1112. Bounding box encapsulating the line string.
  1113. ``None`` if the line string contained no points.
  1114. """
  1115. from .bbs import BoundingBox
  1116. # we don't have to mind the case of len(.) == 1 here, because
  1117. # zero-sized BBs are considered valid
  1118. if len(self.coords) == 0:
  1119. return None
  1120. return BoundingBox(x1=np.min(self.xx), y1=np.min(self.yy),
  1121. x2=np.max(self.xx), y2=np.max(self.yy),
  1122. label=self.label)
  1123. def to_polygon(self):
  1124. """Generate a polygon from the line string points.
  1125. Returns
  1126. -------
  1127. imgaug.augmentables.polys.Polygon
  1128. Polygon with the same corner points as the line string.
  1129. Note that the polygon might be invalid, e.g. contain less
  1130. than ``3`` points or have self-intersections.
  1131. """
  1132. from .polys import Polygon
  1133. return Polygon(self.coords, label=self.label)
  1134. def to_heatmap(self, image_shape, size_lines=1, size_points=0,
  1135. antialiased=True, raise_if_out_of_image=False):
  1136. """Generate a heatmap object from the line string.
  1137. This is similar to
  1138. :func:`~imgaug.augmentables.lines.LineString.draw_lines_heatmap_array`,
  1139. executed with ``alpha=1.0``. The result is wrapped in a
  1140. :class:`~imgaug.augmentables.heatmaps.HeatmapsOnImage` object instead
  1141. of just an array. No points are drawn.
  1142. Parameters
  1143. ----------
  1144. image_shape : tuple of int
  1145. The shape of the image onto which to draw the line mask.
  1146. size_lines : int, optional
  1147. Thickness of the line.
  1148. size_points : int, optional
  1149. Size of the points in pixels.
  1150. antialiased : bool, optional
  1151. Whether to draw the line with anti-aliasing activated.
  1152. raise_if_out_of_image : bool, optional
  1153. Whether to raise an error if the line string is fully
  1154. outside of the image. If set to ``False``, no error will be
  1155. raised and only the parts inside the image will be drawn.
  1156. Returns
  1157. -------
  1158. imgaug.augmentables.heatmaps.HeatmapsOnImage
  1159. Heatmap object containing drawn line string.
  1160. """
  1161. from .heatmaps import HeatmapsOnImage
  1162. return HeatmapsOnImage(
  1163. self.draw_heatmap_array(
  1164. image_shape, size_lines=size_lines, size_points=size_points,
  1165. antialiased=antialiased,
  1166. raise_if_out_of_image=raise_if_out_of_image),
  1167. shape=image_shape
  1168. )
  1169. def to_segmentation_map(self, image_shape, size_lines=1, size_points=0,
  1170. raise_if_out_of_image=False):
  1171. """Generate a segmentation map object from the line string.
  1172. This is similar to
  1173. :func:`~imgaug.augmentables.lines.LineString.draw_mask`.
  1174. The result is wrapped in a ``SegmentationMapsOnImage`` object
  1175. instead of just an array.
  1176. Parameters
  1177. ----------
  1178. image_shape : tuple of int
  1179. The shape of the image onto which to draw the line mask.
  1180. size_lines : int, optional
  1181. Thickness of the line.
  1182. size_points : int, optional
  1183. Size of the points in pixels.
  1184. raise_if_out_of_image : bool, optional
  1185. Whether to raise an error if the line string is fully
  1186. outside of the image. If set to ``False``, no error will be
  1187. raised and only the parts inside the image will be drawn.
  1188. Returns
  1189. -------
  1190. imgaug.augmentables.segmaps.SegmentationMapsOnImage
  1191. Segmentation map object containing drawn line string.
  1192. """
  1193. from .segmaps import SegmentationMapsOnImage
  1194. return SegmentationMapsOnImage(
  1195. self.draw_mask(
  1196. image_shape, size_lines=size_lines, size_points=size_points,
  1197. raise_if_out_of_image=raise_if_out_of_image),
  1198. shape=image_shape
  1199. )
  1200. # TODO make this non-approximate
  1201. def coords_almost_equals(self, other, max_distance=1e-4, points_per_edge=8):
  1202. """Compare this and another LineString's coordinates.
  1203. This is an approximate method based on pointwise distances and can
  1204. in rare corner cases produce wrong outputs.
  1205. Parameters
  1206. ----------
  1207. other : imgaug.augmentables.lines.LineString or tuple of number or ndarray or list of ndarray or list of tuple of number
  1208. The other line string or its coordinates.
  1209. max_distance : float, optional
  1210. Max distance of any point from the other line string before
  1211. the two line strings are evaluated to be unequal.
  1212. points_per_edge : int, optional
  1213. How many points to interpolate on each edge.
  1214. Returns
  1215. -------
  1216. bool
  1217. Whether the two LineString's coordinates are almost identical,
  1218. i.e. the max distance is below the threshold.
  1219. If both have no coordinates, ``True`` is returned.
  1220. If only one has no coordinates, ``False`` is returned.
  1221. Beyond that, the number of points is not evaluated.
  1222. """
  1223. if isinstance(other, LineString):
  1224. pass
  1225. elif isinstance(other, tuple):
  1226. other = LineString([other])
  1227. else:
  1228. other = LineString(other)
  1229. if len(self.coords) == 0 and len(other.coords) == 0:
  1230. return True
  1231. if 0 in [len(self.coords), len(other.coords)]:
  1232. # only one of the two line strings has no coords
  1233. return False
  1234. self_subd = self.subdivide(points_per_edge)
  1235. other_subd = other.subdivide(points_per_edge)
  1236. dist_self2other = self_subd.compute_pointwise_distances(other_subd)
  1237. dist_other2self = other_subd.compute_pointwise_distances(self_subd)
  1238. dist = max(np.max(dist_self2other), np.max(dist_other2self))
  1239. return dist < max_distance
  1240. def almost_equals(self, other, max_distance=1e-4, points_per_edge=8):
  1241. """Compare this and another line string.
  1242. Parameters
  1243. ----------
  1244. other: imgaug.augmentables.lines.LineString
  1245. The other object to compare against. Expected to be a
  1246. ``LineString``.
  1247. max_distance : float, optional
  1248. See :func:`~imgaug.augmentables.lines.LineString.coords_almost_equals`.
  1249. points_per_edge : int, optional
  1250. See :func:`~imgaug.augmentables.lines.LineString.coords_almost_equals`.
  1251. Returns
  1252. -------
  1253. bool
  1254. ``True`` if the coordinates are almost equal and additionally
  1255. the labels are equal. Otherwise ``False``.
  1256. """
  1257. if self.label != other.label:
  1258. return False
  1259. return self.coords_almost_equals(
  1260. other, max_distance=max_distance, points_per_edge=points_per_edge)
  1261. def copy(self, coords=None, label=None):
  1262. """Create a shallow copy of this line string.
  1263. Parameters
  1264. ----------
  1265. coords : None or iterable of tuple of number or ndarray
  1266. If not ``None``, then the coords of the copied object will be set
  1267. to this value.
  1268. label : None or str
  1269. If not ``None``, then the label of the copied object will be set to
  1270. this value.
  1271. Returns
  1272. -------
  1273. imgaug.augmentables.lines.LineString
  1274. Shallow copy.
  1275. """
  1276. return LineString(coords=self.coords if coords is None else coords,
  1277. label=self.label if label is None else label)
  1278. def deepcopy(self, coords=None, label=None):
  1279. """Create a deep copy of this line string.
  1280. Parameters
  1281. ----------
  1282. coords : None or iterable of tuple of number or ndarray
  1283. If not ``None``, then the coords of the copied object will be set
  1284. to this value.
  1285. label : None or str
  1286. If not ``None``, then the label of the copied object will be set to
  1287. this value.
  1288. Returns
  1289. -------
  1290. imgaug.augmentables.lines.LineString
  1291. Deep copy.
  1292. """
  1293. return LineString(
  1294. coords=np.copy(self.coords) if coords is None else coords,
  1295. label=copylib.deepcopy(self.label) if label is None else label)
  1296. def __getitem__(self, indices):
  1297. """Get the coordinate(s) with given indices.
  1298. Added in 0.4.0.
  1299. Returns
  1300. -------
  1301. ndarray
  1302. xy-coordinate(s) as ``ndarray``.
  1303. """
  1304. return self.coords[indices]
  1305. def __iter__(self):
  1306. """Iterate over the coordinates of this instance.
  1307. Added in 0.4.0.
  1308. Yields
  1309. ------
  1310. ndarray
  1311. An ``(2,)`` ``ndarray`` denoting an xy-coordinate pair.
  1312. """
  1313. return iter(self.coords)
  1314. def __repr__(self):
  1315. return self.__str__()
  1316. def __str__(self):
  1317. points_str = ", ".join(
  1318. ["(%.2f, %.2f)" % (x, y) for x, y in self.coords])
  1319. return "LineString([%s], label=%s)" % (points_str, self.label)
  1320. # TODO
  1321. # distance
  1322. # hausdorff_distance
  1323. # is_fully_within_image()
  1324. # is_partly_within_image()
  1325. # is_out_of_image()
  1326. # draw()
  1327. # draw_mask()
  1328. # extract_from_image()
  1329. # to_keypoints()
  1330. # intersects(other)
  1331. # concat(other)
  1332. # is_self_intersecting()
  1333. # remove_self_intersections()
  1334. class LineStringsOnImage(IAugmentable):
  1335. """Object that represents all line strings on a single image.
  1336. Parameters
  1337. ----------
  1338. line_strings : list of imgaug.augmentables.lines.LineString
  1339. List of line strings on the image.
  1340. shape : tuple of int or ndarray
  1341. The shape of the image on which the objects are placed.
  1342. Either an image with shape ``(H,W,[C])`` or a ``tuple`` denoting
  1343. such an image shape.
  1344. Examples
  1345. --------
  1346. >>> import numpy as np
  1347. >>> from imgaug.augmentables.lines import LineString, LineStringsOnImage
  1348. >>>
  1349. >>> image = np.zeros((100, 100))
  1350. >>> lss = [
  1351. >>> LineString([(0, 0), (10, 0)]),
  1352. >>> LineString([(10, 20), (30, 30), (50, 70)])
  1353. >>> ]
  1354. >>> lsoi = LineStringsOnImage(lss, shape=image.shape)
  1355. """
  1356. def __init__(self, line_strings, shape):
  1357. assert ia.is_iterable(line_strings), (
  1358. "Expected 'line_strings' to be an iterable, got type '%s'." % (
  1359. type(line_strings),))
  1360. assert all([isinstance(v, LineString) for v in line_strings]), (
  1361. "Expected iterable of LineString, got types: %s." % (
  1362. ", ".join([str(type(v)) for v in line_strings])
  1363. ))
  1364. self.line_strings = line_strings
  1365. self.shape = normalize_shape(shape)
  1366. @property
  1367. def items(self):
  1368. """Get the line strings in this container.
  1369. Added in 0.4.0.
  1370. Returns
  1371. -------
  1372. list of LineString
  1373. Line strings within this container.
  1374. """
  1375. return self.line_strings
  1376. @items.setter
  1377. def items(self, value):
  1378. """Set the line strings in this container.
  1379. Added in 0.4.0.
  1380. Parameters
  1381. ----------
  1382. value : list of LineString
  1383. Line strings within this container.
  1384. """
  1385. self.line_strings = value
  1386. @property
  1387. def empty(self):
  1388. """Estimate whether this object contains zero line strings.
  1389. Returns
  1390. -------
  1391. bool
  1392. ``True`` if this object contains zero line strings.
  1393. """
  1394. return len(self.line_strings) == 0
  1395. def on_(self, image):
  1396. """Project the line strings from one image shape to a new one in-place.
  1397. Added in 0.4.0.
  1398. Parameters
  1399. ----------
  1400. image : ndarray or tuple of int
  1401. The new image onto which to project.
  1402. Either an image with shape ``(H,W,[C])`` or a tuple denoting
  1403. such an image shape.
  1404. Returns
  1405. -------
  1406. imgaug.augmentables.lines.LineStrings
  1407. Object containing all projected line strings.
  1408. The object and its items may have been modified in-place.
  1409. """
  1410. # pylint: disable=invalid-name
  1411. on_shape = normalize_shape(image)
  1412. if on_shape[0:2] == self.shape[0:2]:
  1413. self.shape = on_shape # channels may differ
  1414. return self
  1415. for i, item in enumerate(self.items):
  1416. self.line_strings[i] = item.project_(self.shape, on_shape)
  1417. self.shape = on_shape
  1418. return self
  1419. def on(self, image):
  1420. """Project the line strings from one image shape to a new one.
  1421. Parameters
  1422. ----------
  1423. image : ndarray or tuple of int
  1424. The new image onto which to project.
  1425. Either an image with shape ``(H,W,[C])`` or a tuple denoting
  1426. such an image shape.
  1427. Returns
  1428. -------
  1429. imgaug.augmentables.lines.LineStrings
  1430. Object containing all projected line strings.
  1431. """
  1432. # pylint: disable=invalid-name
  1433. return self.deepcopy().on_(image)
  1434. @classmethod
  1435. def from_xy_arrays(cls, xy, shape):
  1436. """Convert an ``(N,M,2)`` ndarray to a ``LineStringsOnImage`` object.
  1437. This is the inverse of
  1438. :func:`~imgaug.augmentables.lines.LineStringsOnImage.to_xy_array`.
  1439. Parameters
  1440. ----------
  1441. xy : (N,M,2) ndarray or iterable of (M,2) ndarray
  1442. Array containing the point coordinates ``N`` line strings
  1443. with each ``M`` points given as ``(x,y)`` coordinates.
  1444. ``M`` may differ if an iterable of arrays is used.
  1445. Each array should usually be of dtype ``float32``.
  1446. shape : tuple of int
  1447. ``(H,W,[C])`` shape of the image on which the line strings are
  1448. placed.
  1449. Returns
  1450. -------
  1451. imgaug.augmentables.lines.LineStringsOnImage
  1452. Object containing a list of ``LineString`` objects following the
  1453. provided point coordinates.
  1454. """
  1455. lss = []
  1456. for xy_ls in xy:
  1457. lss.append(LineString(xy_ls))
  1458. return cls(lss, shape)
  1459. def to_xy_arrays(self, dtype=np.float32):
  1460. """Convert this object to an iterable of ``(M,2)`` arrays of points.
  1461. This is the inverse of
  1462. :func:`~imgaug.augmentables.lines.LineStringsOnImage.from_xy_array`.
  1463. Parameters
  1464. ----------
  1465. dtype : numpy.dtype, optional
  1466. Desired output datatype of the ndarray.
  1467. Returns
  1468. -------
  1469. list of ndarray
  1470. The arrays of point coordinates, each given as ``(M,2)``.
  1471. """
  1472. from .. import dtypes as iadt
  1473. return [iadt.restore_dtypes_(np.copy(ls.coords), dtype)
  1474. for ls in self.line_strings]
  1475. def draw_on_image(self, image,
  1476. color=(0, 255, 0), color_lines=None, color_points=None,
  1477. alpha=1.0, alpha_lines=None, alpha_points=None,
  1478. size=1, size_lines=None, size_points=None,
  1479. antialiased=True,
  1480. raise_if_out_of_image=False):
  1481. """Draw all line strings onto a given image.
  1482. Parameters
  1483. ----------
  1484. image : ndarray
  1485. The ``(H,W,C)`` ``uint8`` image onto which to draw the line
  1486. strings.
  1487. color : iterable of int, optional
  1488. Color to use as RGB, i.e. three values.
  1489. The color of the lines and points are derived from this value,
  1490. unless they are set.
  1491. color_lines : None or iterable of int
  1492. Color to use for the line segments as RGB, i.e. three values.
  1493. If ``None``, this value is derived from `color`.
  1494. color_points : None or iterable of int
  1495. Color to use for the points as RGB, i.e. three values.
  1496. If ``None``, this value is derived from ``0.5 * color``.
  1497. alpha : float, optional
  1498. Opacity of the line strings. Higher values denote more visible
  1499. points.
  1500. The alphas of the line and points are derived from this value,
  1501. unless they are set.
  1502. alpha_lines : None or float, optional
  1503. Opacity of the line strings. Higher values denote more visible
  1504. line string.
  1505. If ``None``, this value is derived from `alpha`.
  1506. alpha_points : None or float, optional
  1507. Opacity of the line string points. Higher values denote more
  1508. visible points.
  1509. If ``None``, this value is derived from `alpha`.
  1510. size : int, optional
  1511. Size of the line strings.
  1512. The sizes of the line and points are derived from this value,
  1513. unless they are set.
  1514. size_lines : None or int, optional
  1515. Thickness of the line segments.
  1516. If ``None``, this value is derived from `size`.
  1517. size_points : None or int, optional
  1518. Size of the points in pixels.
  1519. If ``None``, this value is derived from ``3 * size``.
  1520. antialiased : bool, optional
  1521. Whether to draw the lines with anti-aliasing activated.
  1522. This does currently not affect the point drawing.
  1523. raise_if_out_of_image : bool, optional
  1524. Whether to raise an error if a line string is fully
  1525. outside of the image. If set to ``False``, no error will be
  1526. raised and only the parts inside the image will be drawn.
  1527. Returns
  1528. -------
  1529. ndarray
  1530. Image with line strings drawn on it.
  1531. """
  1532. # TODO improve efficiency here by copying only once
  1533. for ls in self.line_strings:
  1534. image = ls.draw_on_image(
  1535. image,
  1536. color=color, color_lines=color_lines, color_points=color_points,
  1537. alpha=alpha, alpha_lines=alpha_lines, alpha_points=alpha_points,
  1538. size=size, size_lines=size_lines, size_points=size_points,
  1539. antialiased=antialiased,
  1540. raise_if_out_of_image=raise_if_out_of_image
  1541. )
  1542. return image
  1543. def remove_out_of_image_(self, fully=True, partly=False):
  1544. """
  1545. Remove all LS that are fully/partially outside of an image in-place.
  1546. Added in 0.4.0.
  1547. Parameters
  1548. ----------
  1549. fully : bool, optional
  1550. Whether to remove line strings that are fully outside of the image.
  1551. partly : bool, optional
  1552. Whether to remove line strings that are partially outside of the
  1553. image.
  1554. Returns
  1555. -------
  1556. imgaug.augmentables.lines.LineStringsOnImage
  1557. Reduced set of line strings. Those that are fully/partially
  1558. outside of the given image plane are removed.
  1559. The object and its items may have been modified in-place.
  1560. """
  1561. self.line_strings = [
  1562. ls for ls in self.line_strings
  1563. if not ls.is_out_of_image(self.shape, fully=fully, partly=partly)]
  1564. return self
  1565. def remove_out_of_image(self, fully=True, partly=False):
  1566. """
  1567. Remove all line strings that are fully/partially outside of an image.
  1568. Parameters
  1569. ----------
  1570. fully : bool, optional
  1571. Whether to remove line strings that are fully outside of the image.
  1572. partly : bool, optional
  1573. Whether to remove line strings that are partially outside of the
  1574. image.
  1575. Returns
  1576. -------
  1577. imgaug.augmentables.lines.LineStringsOnImage
  1578. Reduced set of line strings. Those that are fully/partially
  1579. outside of the given image plane are removed.
  1580. """
  1581. return self.copy().remove_out_of_image_(fully=fully, partly=partly)
  1582. def remove_out_of_image_fraction_(self, fraction):
  1583. """Remove all LS with an OOI fraction of at least `fraction` in-place.
  1584. 'OOI' is the abbreviation for 'out of image'.
  1585. Added in 0.4.0.
  1586. Parameters
  1587. ----------
  1588. fraction : number
  1589. Minimum out of image fraction that a line string has to have in
  1590. order to be removed. A fraction of ``1.0`` removes only line
  1591. strings that are ``100%`` outside of the image. A fraction of
  1592. ``0.0`` removes all line strings.
  1593. Returns
  1594. -------
  1595. imgaug.augmentables.lines.LineStringsOnImage
  1596. Reduced set of line strings, with those that had an out of image
  1597. fraction greater or equal the given one removed.
  1598. The object and its items may have been modified in-place.
  1599. """
  1600. return _remove_out_of_image_fraction_(self, fraction)
  1601. def remove_out_of_image_fraction(self, fraction):
  1602. """Remove all LS with an out of image fraction of at least `fraction`.
  1603. Parameters
  1604. ----------
  1605. fraction : number
  1606. Minimum out of image fraction that a line string has to have in
  1607. order to be removed. A fraction of ``1.0`` removes only line
  1608. strings that are ``100%`` outside of the image. A fraction of
  1609. ``0.0`` removes all line strings.
  1610. Returns
  1611. -------
  1612. imgaug.augmentables.lines.LineStringsOnImage
  1613. Reduced set of line strings, with those that had an out of image
  1614. fraction greater or equal the given one removed.
  1615. """
  1616. return self.copy().remove_out_of_image_fraction_(fraction)
  1617. def clip_out_of_image_(self):
  1618. """
  1619. Clip off all parts of the LSs that are outside of an image in-place.
  1620. .. note::
  1621. The result can contain fewer line strings than the input did. That
  1622. happens when a polygon is fully outside of the image plane.
  1623. .. note::
  1624. The result can also contain *more* line strings than the input
  1625. did. That happens when distinct parts of a line string are only
  1626. connected by line segments that are outside of the image plane and
  1627. hence will be clipped off, resulting in two or more unconnected
  1628. line string parts that are left in the image plane.
  1629. Added in 0.4.0.
  1630. Returns
  1631. -------
  1632. imgaug.augmentables.lines.LineStringsOnImage
  1633. Line strings, clipped to fall within the image dimensions.
  1634. The count of output line strings may differ from the input count.
  1635. """
  1636. self.line_strings = [
  1637. ls_clipped
  1638. for ls in self.line_strings
  1639. for ls_clipped in ls.clip_out_of_image(self.shape)]
  1640. return self
  1641. def clip_out_of_image(self):
  1642. """
  1643. Clip off all parts of the line strings that are outside of an image.
  1644. .. note::
  1645. The result can contain fewer line strings than the input did. That
  1646. happens when a polygon is fully outside of the image plane.
  1647. .. note::
  1648. The result can also contain *more* line strings than the input
  1649. did. That happens when distinct parts of a line string are only
  1650. connected by line segments that are outside of the image plane and
  1651. hence will be clipped off, resulting in two or more unconnected
  1652. line string parts that are left in the image plane.
  1653. Returns
  1654. -------
  1655. imgaug.augmentables.lines.LineStringsOnImage
  1656. Line strings, clipped to fall within the image dimensions.
  1657. The count of output line strings may differ from the input count.
  1658. """
  1659. return self.copy().clip_out_of_image_()
  1660. def shift_(self, x=0, y=0):
  1661. """Move the line strings along the x/y-axis in-place.
  1662. The origin ``(0, 0)`` is at the top left of the image.
  1663. Added in 0.4.0.
  1664. Parameters
  1665. ----------
  1666. x : number, optional
  1667. Value to be added to all x-coordinates. Positive values shift
  1668. towards the right images.
  1669. y : number, optional
  1670. Value to be added to all y-coordinates. Positive values shift
  1671. towards the bottom images.
  1672. Returns
  1673. -------
  1674. imgaug.augmentables.lines.LineStringsOnImage
  1675. Shifted line strings.
  1676. The object and its items may have been modified in-place.
  1677. """
  1678. for i, ls in enumerate(self.line_strings):
  1679. self.line_strings[i] = ls.shift_(x=x, y=y)
  1680. return self
  1681. def shift(self, x=0, y=0, top=None, right=None, bottom=None, left=None):
  1682. """Move the line strings along the x/y-axis.
  1683. The origin ``(0, 0)`` is at the top left of the image.
  1684. Parameters
  1685. ----------
  1686. x : number, optional
  1687. Value to be added to all x-coordinates. Positive values shift
  1688. towards the right images.
  1689. y : number, optional
  1690. Value to be added to all y-coordinates. Positive values shift
  1691. towards the bottom images.
  1692. top : None or int, optional
  1693. Deprecated since 0.4.0.
  1694. Amount of pixels by which to shift all objects *from* the
  1695. top (towards the bottom).
  1696. right : None or int, optional
  1697. Deprecated since 0.4.0.
  1698. Amount of pixels by which to shift all objects *from* the
  1699. right (towads the left).
  1700. bottom : None or int, optional
  1701. Deprecated since 0.4.0.
  1702. Amount of pixels by which to shift all objects *from* the
  1703. bottom (towards the top).
  1704. left : None or int, optional
  1705. Deprecated since 0.4.0.
  1706. Amount of pixels by which to shift all objects *from* the
  1707. left (towards the right).
  1708. Returns
  1709. -------
  1710. imgaug.augmentables.lines.LineStringsOnImage
  1711. Shifted line strings.
  1712. """
  1713. x, y = _normalize_shift_args(
  1714. x, y, top=top, right=right, bottom=bottom, left=left)
  1715. return self.deepcopy().shift_(x=x, y=y)
  1716. def to_xy_array(self):
  1717. """Convert all line string coordinates to one array of shape ``(N,2)``.
  1718. Added in 0.4.0.
  1719. Returns
  1720. -------
  1721. (N, 2) ndarray
  1722. Array containing all xy-coordinates of all line strings within this
  1723. instance.
  1724. """
  1725. if self.empty:
  1726. return np.zeros((0, 2), dtype=np.float32)
  1727. return np.concatenate([ls.coords for ls in self.line_strings])
  1728. def fill_from_xy_array_(self, xy):
  1729. """Modify the corner coordinates of all line strings in-place.
  1730. .. note::
  1731. This currently expects that `xy` contains exactly as many
  1732. coordinates as the line strings within this instance have corner
  1733. points. Otherwise, an ``AssertionError`` will be raised.
  1734. Added in 0.4.0.
  1735. Parameters
  1736. ----------
  1737. xy : (N, 2) ndarray or iterable of iterable of number
  1738. XY-Coordinates of ``N`` corner points. ``N`` must match the
  1739. number of corner points in all line strings within this instance.
  1740. Returns
  1741. -------
  1742. LineStringsOnImage
  1743. This instance itself, with updated coordinates.
  1744. Note that the instance was modified in-place.
  1745. """
  1746. xy = np.array(xy, dtype=np.float32)
  1747. # note that np.array([]) is (0,), not (0, 2)
  1748. assert xy.shape[0] == 0 or (xy.ndim == 2 and xy.shape[-1] == 2), ( # pylint: disable=unsubscriptable-object
  1749. "Expected input array to have shape (N,2), "
  1750. "got shape %s." % (xy.shape,))
  1751. counter = 0
  1752. for ls in self.line_strings:
  1753. nb_points = len(ls.coords)
  1754. assert counter + nb_points <= len(xy), (
  1755. "Received fewer points than there are corner points in "
  1756. "all line strings. Got %d points, expected %d." % (
  1757. len(xy),
  1758. sum([len(ls_.coords) for ls_ in self.line_strings])))
  1759. ls.coords[:, ...] = xy[counter:counter+nb_points]
  1760. counter += nb_points
  1761. assert counter == len(xy), (
  1762. "Expected to get exactly as many xy-coordinates as there are "
  1763. "points in all line strings polygons within this instance. "
  1764. "Got %d points, could only assign %d points." % (
  1765. len(xy), counter,))
  1766. return self
  1767. def to_keypoints_on_image(self):
  1768. """Convert the line strings to one ``KeypointsOnImage`` instance.
  1769. Added in 0.4.0.
  1770. Returns
  1771. -------
  1772. imgaug.augmentables.kps.KeypointsOnImage
  1773. A keypoints instance containing ``N`` coordinates for a total
  1774. of ``N`` points in the ``coords`` attributes of all line strings.
  1775. Order matches the order in ``line_strings`` and ``coords``
  1776. attributes.
  1777. """
  1778. from . import KeypointsOnImage
  1779. if self.empty:
  1780. return KeypointsOnImage([], shape=self.shape)
  1781. coords = np.concatenate(
  1782. [ls.coords for ls in self.line_strings],
  1783. axis=0)
  1784. return KeypointsOnImage.from_xy_array(coords,
  1785. shape=self.shape)
  1786. def invert_to_keypoints_on_image_(self, kpsoi):
  1787. """Invert the output of ``to_keypoints_on_image()`` in-place.
  1788. This function writes in-place into this ``LineStringsOnImage``
  1789. instance.
  1790. Added in 0.4.0.
  1791. Parameters
  1792. ----------
  1793. kpsoi : imgaug.augmentables.kps.KeypointsOnImages
  1794. Keypoints to convert back to line strings, i.e. the outputs
  1795. of ``to_keypoints_on_image()``.
  1796. Returns
  1797. -------
  1798. LineStringsOnImage
  1799. Line strings container with updated coordinates.
  1800. Note that the instance is also updated in-place.
  1801. """
  1802. lss = self.line_strings
  1803. coordss = [ls.coords for ls in lss]
  1804. nb_points_exp = sum([len(coords) for coords in coordss])
  1805. assert len(kpsoi.keypoints) == nb_points_exp, (
  1806. "Expected %d coordinates, got %d." % (
  1807. nb_points_exp, len(kpsoi.keypoints)))
  1808. xy_arr = kpsoi.to_xy_array()
  1809. counter = 0
  1810. for ls in lss:
  1811. coords = ls.coords
  1812. coords[:, :] = xy_arr[counter:counter+len(coords), :]
  1813. counter += len(coords)
  1814. self.shape = kpsoi.shape
  1815. return self
  1816. def copy(self, line_strings=None, shape=None):
  1817. """Create a shallow copy of this object.
  1818. Parameters
  1819. ----------
  1820. line_strings : None or list of imgaug.augmentables.lines.LineString, optional
  1821. List of line strings on the image.
  1822. If not ``None``, then the ``line_strings`` attribute of the copied
  1823. object will be set to this value.
  1824. shape : None or tuple of int or ndarray, optional
  1825. The shape of the image on which the objects are placed.
  1826. Either an image with shape ``(H,W,[C])`` or a tuple denoting
  1827. such an image shape.
  1828. If not ``None``, then the ``shape`` attribute of the copied object
  1829. will be set to this value.
  1830. Returns
  1831. -------
  1832. imgaug.augmentables.lines.LineStringsOnImage
  1833. Shallow copy.
  1834. """
  1835. if line_strings is None:
  1836. line_strings = self.line_strings[:]
  1837. if shape is None:
  1838. # use tuple() here in case the shape was provided as a list
  1839. shape = tuple(self.shape)
  1840. return LineStringsOnImage(line_strings, shape)
  1841. def deepcopy(self, line_strings=None, shape=None):
  1842. """Create a deep copy of the object.
  1843. Parameters
  1844. ----------
  1845. line_strings : None or list of imgaug.augmentables.lines.LineString, optional
  1846. List of line strings on the image.
  1847. If not ``None``, then the ``line_strings`` attribute of the copied
  1848. object will be set to this value.
  1849. shape : None or tuple of int or ndarray, optional
  1850. The shape of the image on which the objects are placed.
  1851. Either an image with shape ``(H,W,[C])`` or a tuple denoting
  1852. such an image shape.
  1853. If not ``None``, then the ``shape`` attribute of the copied object
  1854. will be set to this value.
  1855. Returns
  1856. -------
  1857. imgaug.augmentables.lines.LineStringsOnImage
  1858. Deep copy.
  1859. """
  1860. # Manual copy is far faster than deepcopy, so use manual copy here.
  1861. if line_strings is None:
  1862. line_strings = [ls.deepcopy() for ls in self.line_strings]
  1863. if shape is None:
  1864. # use tuple() here in case the shape was provided as a list
  1865. shape = tuple(self.shape)
  1866. return LineStringsOnImage(line_strings, shape)
  1867. def __getitem__(self, indices):
  1868. """Get the line string(s) with given indices.
  1869. Added in 0.4.0.
  1870. Returns
  1871. -------
  1872. list of imgaug.augmentables.lines.LineString
  1873. Line string(s) with given indices.
  1874. """
  1875. return self.line_strings[indices]
  1876. def __iter__(self):
  1877. """Iterate over the line strings in this container.
  1878. Added in 0.4.0.
  1879. Yields
  1880. ------
  1881. LineString
  1882. A line string in this container.
  1883. The order is identical to the order in the line string list
  1884. provided upon class initialization.
  1885. """
  1886. return iter(self.line_strings)
  1887. def __len__(self):
  1888. """Get the number of items in this instance.
  1889. Added in 0.4.0.
  1890. Returns
  1891. -------
  1892. int
  1893. Number of items in this instance.
  1894. """
  1895. return len(self.items)
  1896. def __repr__(self):
  1897. return self.__str__()
  1898. def __str__(self):
  1899. return "LineStringsOnImage(%s, shape=%s)" % (
  1900. str(self.line_strings), self.shape)
  1901. def _is_point_on_line(line_start, line_end, point, eps=1e-4):
  1902. dist_s2e = np.linalg.norm(np.float32(line_start) - np.float32(line_end))
  1903. dist_s2p2e = (
  1904. np.linalg.norm(np.float32(line_start) - np.float32(point))
  1905. + np.linalg.norm(np.float32(point) - np.float32(line_end))
  1906. )
  1907. return -eps < (dist_s2p2e - dist_s2e) < eps
  1908. def _flatten_shapely_collection(collection):
  1909. import shapely.geometry
  1910. if not isinstance(collection, list):
  1911. collection = [collection]
  1912. for item in collection:
  1913. if hasattr(item, "geoms"):
  1914. for subitem in _flatten_shapely_collection(item.geoms):
  1915. # MultiPoint.geoms actually returns a GeometrySequence
  1916. if isinstance(subitem, shapely.geometry.base.GeometrySequence):
  1917. for subsubel in subitem:
  1918. yield subsubel
  1919. else:
  1920. yield _flatten_shapely_collection(subitem)
  1921. else:
  1922. yield item
  1923. def _convert_var_to_shapely_geometry(var):
  1924. import shapely.geometry
  1925. if isinstance(var, tuple):
  1926. geom = shapely.geometry.Point(var[0], var[1])
  1927. elif isinstance(var, list):
  1928. assert len(var) > 0, (
  1929. "Expected list to contain at least one coordinate, "
  1930. "got %d coordinates." % (len(var),))
  1931. if isinstance(var[0], tuple):
  1932. geom = shapely.geometry.LineString(var)
  1933. elif all([isinstance(v, LineString) for v in var]):
  1934. geom = shapely.geometry.MultiLineString([
  1935. shapely.geometry.LineString(ls.coords) for ls in var
  1936. ])
  1937. else:
  1938. raise ValueError(
  1939. "Could not convert list-input to shapely geometry. Invalid "
  1940. "datatype. List elements had datatypes: %s." % (
  1941. ", ".join([str(type(v)) for v in var]),))
  1942. elif isinstance(var, LineString):
  1943. geom = shapely.geometry.LineString(var.coords)
  1944. else:
  1945. raise ValueError(
  1946. "Could not convert input to shapely geometry. Invalid datatype. "
  1947. "Got: %s" % (type(var),))
  1948. return geom