bbs.py 74 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271
  1. """Classes representing bounding boxes."""
  2. from __future__ import print_function, division, absolute_import
  3. import copy
  4. import numpy as np
  5. import skimage.draw
  6. import skimage.measure
  7. from .. import imgaug as ia
  8. from .base import IAugmentable
  9. from .utils import (normalize_shape, project_coords,
  10. _remove_out_of_image_fraction_,
  11. _normalize_shift_args)
  12. # TODO functions: square(), to_aspect_ratio(), contains_point()
  13. class BoundingBox(object):
  14. """Class representing bounding boxes.
  15. Each bounding box is parameterized by its top left and bottom right
  16. corners. Both are given as x and y-coordinates. The corners are intended
  17. to lie inside the bounding box area. As a result, a bounding box that lies
  18. completely inside the image but has maximum extensions would have
  19. coordinates ``(0.0, 0.0)`` and ``(W - epsilon, H - epsilon)``. Note that
  20. coordinates are saved internally as floats.
  21. Parameters
  22. ----------
  23. x1 : number
  24. X-coordinate of the top left of the bounding box.
  25. y1 : number
  26. Y-coordinate of the top left of the bounding box.
  27. x2 : number
  28. X-coordinate of the bottom right of the bounding box.
  29. y2 : number
  30. Y-coordinate of the bottom right of the bounding box.
  31. label : None or str, optional
  32. Label of the bounding box, e.g. a string representing the class.
  33. """
  34. def __init__(self, x1, y1, x2, y2, label=None):
  35. """Create a new BoundingBox instance."""
  36. if x1 > x2:
  37. x2, x1 = x1, x2
  38. if y1 > y2:
  39. y2, y1 = y1, y2
  40. self.x1 = x1
  41. self.y1 = y1
  42. self.x2 = x2
  43. self.y2 = y2
  44. self.label = label
  45. @property
  46. def coords(self):
  47. """Get the top-left and bottom-right coordinates as one array.
  48. Added in 0.4.0.
  49. Returns
  50. -------
  51. ndarray
  52. A ``(N, 2)`` numpy array with ``N=2`` containing the top-left
  53. and bottom-right coordinates.
  54. """
  55. arr = np.empty((2, 2), dtype=np.float32)
  56. arr[0, :] = (self.x1, self.y1)
  57. arr[1, :] = (self.x2, self.y2)
  58. return arr
  59. @property
  60. def x1_int(self):
  61. """Get the x-coordinate of the top left corner as an integer.
  62. Returns
  63. -------
  64. int
  65. X-coordinate of the top left corner, rounded to the closest
  66. integer.
  67. """
  68. # use numpy's round to have consistent behaviour between python
  69. # versions
  70. return int(np.round(self.x1))
  71. @property
  72. def y1_int(self):
  73. """Get the y-coordinate of the top left corner as an integer.
  74. Returns
  75. -------
  76. int
  77. Y-coordinate of the top left corner, rounded to the closest
  78. integer.
  79. """
  80. # use numpy's round to have consistent behaviour between python
  81. # versions
  82. return int(np.round(self.y1))
  83. @property
  84. def x2_int(self):
  85. """Get the x-coordinate of the bottom left corner as an integer.
  86. Returns
  87. -------
  88. int
  89. X-coordinate of the bottom left corner, rounded to the closest
  90. integer.
  91. """
  92. # use numpy's round to have consistent behaviour between python
  93. # versions
  94. return int(np.round(self.x2))
  95. @property
  96. def y2_int(self):
  97. """Get the y-coordinate of the bottom left corner as an integer.
  98. Returns
  99. -------
  100. int
  101. Y-coordinate of the bottom left corner, rounded to the closest
  102. integer.
  103. """
  104. # use numpy's round to have consistent behaviour between python
  105. # versions
  106. return int(np.round(self.y2))
  107. @property
  108. def height(self):
  109. """Estimate the height of the bounding box.
  110. Returns
  111. -------
  112. number
  113. Height of the bounding box.
  114. """
  115. return self.y2 - self.y1
  116. @property
  117. def width(self):
  118. """Estimate the width of the bounding box.
  119. Returns
  120. -------
  121. number
  122. Width of the bounding box.
  123. """
  124. return self.x2 - self.x1
  125. @property
  126. def center_x(self):
  127. """Estimate the x-coordinate of the center point of the bounding box.
  128. Returns
  129. -------
  130. number
  131. X-coordinate of the center point of the bounding box.
  132. """
  133. return self.x1 + self.width/2
  134. @property
  135. def center_y(self):
  136. """Estimate the y-coordinate of the center point of the bounding box.
  137. Returns
  138. -------
  139. number
  140. Y-coordinate of the center point of the bounding box.
  141. """
  142. return self.y1 + self.height/2
  143. @property
  144. def area(self):
  145. """Estimate the area of the bounding box.
  146. Returns
  147. -------
  148. number
  149. Area of the bounding box, i.e. ``height * width``.
  150. """
  151. return self.height * self.width
  152. # TODO add test for tuple of number
  153. def contains(self, other):
  154. """Estimate whether the bounding box contains a given point.
  155. Parameters
  156. ----------
  157. other : tuple of number or imgaug.augmentables.kps.Keypoint
  158. Point to check for.
  159. Returns
  160. -------
  161. bool
  162. ``True`` if the point is contained in the bounding box,
  163. ``False`` otherwise.
  164. """
  165. if isinstance(other, tuple):
  166. x, y = other
  167. else:
  168. x, y = other.x, other.y
  169. return self.x1 <= x <= self.x2 and self.y1 <= y <= self.y2
  170. def project_(self, from_shape, to_shape):
  171. """Project the bounding box onto a differently shaped image in-place.
  172. E.g. if the bounding box is on its original image at
  173. ``x1=(10 of 100 pixels)`` and ``y1=(20 of 100 pixels)`` and is
  174. projected onto a new image with size ``(width=200, height=200)``,
  175. its new position will be ``(x1=20, y1=40)``.
  176. (Analogous for ``x2``/``y2``.)
  177. This is intended for cases where the original image is resized.
  178. It cannot be used for more complex changes (e.g. padding, cropping).
  179. Added in 0.4.0.
  180. Parameters
  181. ----------
  182. from_shape : tuple of int or ndarray
  183. Shape of the original image. (Before resize.)
  184. to_shape : tuple of int or ndarray
  185. Shape of the new image. (After resize.)
  186. Returns
  187. -------
  188. imgaug.augmentables.bbs.BoundingBox
  189. ``BoundingBox`` instance with new coordinates.
  190. The object may have been modified in-place.
  191. """
  192. (self.x1, self.y1), (self.x2, self.y2) = project_coords(
  193. [(self.x1, self.y1), (self.x2, self.y2)],
  194. from_shape,
  195. to_shape)
  196. return self
  197. # TODO add tests for ndarray inputs
  198. def project(self, from_shape, to_shape):
  199. """Project the bounding box onto a differently shaped image.
  200. E.g. if the bounding box is on its original image at
  201. ``x1=(10 of 100 pixels)`` and ``y1=(20 of 100 pixels)`` and is
  202. projected onto a new image with size ``(width=200, height=200)``,
  203. its new position will be ``(x1=20, y1=40)``.
  204. (Analogous for ``x2``/``y2``.)
  205. This is intended for cases where the original image is resized.
  206. It cannot be used for more complex changes (e.g. padding, cropping).
  207. Parameters
  208. ----------
  209. from_shape : tuple of int or ndarray
  210. Shape of the original image. (Before resize.)
  211. to_shape : tuple of int or ndarray
  212. Shape of the new image. (After resize.)
  213. Returns
  214. -------
  215. imgaug.augmentables.bbs.BoundingBox
  216. ``BoundingBox`` instance with new coordinates.
  217. """
  218. return self.deepcopy().project_(from_shape, to_shape)
  219. def extend_(self, all_sides=0, top=0, right=0, bottom=0, left=0):
  220. """Extend the size of the bounding box along its sides in-place.
  221. Added in 0.4.0.
  222. Parameters
  223. ----------
  224. all_sides : number, optional
  225. Value by which to extend the bounding box size along all
  226. sides.
  227. top : number, optional
  228. Value by which to extend the bounding box size along its top
  229. side.
  230. right : number, optional
  231. Value by which to extend the bounding box size along its right
  232. side.
  233. bottom : number, optional
  234. Value by which to extend the bounding box size along its bottom
  235. side.
  236. left : number, optional
  237. Value by which to extend the bounding box size along its left
  238. side.
  239. Returns
  240. -------
  241. imgaug.BoundingBox
  242. Extended bounding box.
  243. The object may have been modified in-place.
  244. """
  245. self.x1 = self.x1 - all_sides - left
  246. self.x2 = self.x2 + all_sides + right
  247. self.y1 = self.y1 - all_sides - top
  248. self.y2 = self.y2 + all_sides + bottom
  249. return self
  250. def extend(self, all_sides=0, top=0, right=0, bottom=0, left=0):
  251. """Extend the size of the bounding box along its sides.
  252. Parameters
  253. ----------
  254. all_sides : number, optional
  255. Value by which to extend the bounding box size along all
  256. sides.
  257. top : number, optional
  258. Value by which to extend the bounding box size along its top
  259. side.
  260. right : number, optional
  261. Value by which to extend the bounding box size along its right
  262. side.
  263. bottom : number, optional
  264. Value by which to extend the bounding box size along its bottom
  265. side.
  266. left : number, optional
  267. Value by which to extend the bounding box size along its left
  268. side.
  269. Returns
  270. -------
  271. imgaug.BoundingBox
  272. Extended bounding box.
  273. """
  274. return self.deepcopy().extend_(all_sides, top, right, bottom, left)
  275. def intersection(self, other, default=None):
  276. """Compute the intersection BB between this BB and another BB.
  277. Note that in extreme cases, the intersection can be a single point.
  278. In that case the intersection bounding box exists and it will be
  279. returned, but it will have a height and width of zero.
  280. Parameters
  281. ----------
  282. other : imgaug.augmentables.bbs.BoundingBox
  283. Other bounding box with which to generate the intersection.
  284. default : any, optional
  285. Default value to return if there is no intersection.
  286. Returns
  287. -------
  288. imgaug.augmentables.bbs.BoundingBox or any
  289. Intersection bounding box of the two bounding boxes if there is
  290. an intersection.
  291. If there is no intersection, the default value will be returned,
  292. which can by anything.
  293. """
  294. x1_i = max(self.x1, other.x1)
  295. y1_i = max(self.y1, other.y1)
  296. x2_i = min(self.x2, other.x2)
  297. y2_i = min(self.y2, other.y2)
  298. if x1_i > x2_i or y1_i > y2_i:
  299. return default
  300. return BoundingBox(x1=x1_i, y1=y1_i, x2=x2_i, y2=y2_i)
  301. def union(self, other):
  302. """Compute the union BB between this BB and another BB.
  303. This is equivalent to drawing a bounding box around all corner points
  304. of both bounding boxes.
  305. Parameters
  306. ----------
  307. other : imgaug.augmentables.bbs.BoundingBox
  308. Other bounding box with which to generate the union.
  309. Returns
  310. -------
  311. imgaug.augmentables.bbs.BoundingBox
  312. Union bounding box of the two bounding boxes.
  313. """
  314. return BoundingBox(
  315. x1=min(self.x1, other.x1),
  316. y1=min(self.y1, other.y1),
  317. x2=max(self.x2, other.x2),
  318. y2=max(self.y2, other.y2),
  319. )
  320. def iou(self, other):
  321. """Compute the IoU between this bounding box and another one.
  322. IoU is the intersection over union, defined as::
  323. ``area(intersection(A, B)) / area(union(A, B))``
  324. ``= area(intersection(A, B))
  325. / (area(A) + area(B) - area(intersection(A, B)))``
  326. Parameters
  327. ----------
  328. other : imgaug.augmentables.bbs.BoundingBox
  329. Other bounding box with which to compare.
  330. Returns
  331. -------
  332. float
  333. IoU between the two bounding boxes.
  334. """
  335. inters = self.intersection(other)
  336. if inters is None:
  337. return 0.0
  338. area_union = self.area + other.area - inters.area
  339. return inters.area / area_union if area_union > 0 else 0.0
  340. def compute_out_of_image_area(self, image):
  341. """Compute the area of the BB that is outside of the image plane.
  342. Added in 0.4.0.
  343. Parameters
  344. ----------
  345. image : (H,W,...) ndarray or tuple of int
  346. Image dimensions to use.
  347. If an ``ndarray``, its shape will be used.
  348. If a ``tuple``, it is assumed to represent the image shape
  349. and must contain at least two integers.
  350. Returns
  351. -------
  352. float
  353. Total area of the bounding box that is outside of the image plane.
  354. Can be ``0.0``.
  355. """
  356. shape = normalize_shape(image)
  357. height, width = shape[0:2]
  358. bb_image = BoundingBox(x1=0, y1=0, x2=width, y2=height)
  359. inter = self.intersection(bb_image, default=None)
  360. area = self.area
  361. return area if inter is None else area - inter.area
  362. def compute_out_of_image_fraction(self, image):
  363. """Compute fraction of BB area outside of the image plane.
  364. This estimates ``f = A_ooi / A``, where ``A_ooi`` is the area of the
  365. bounding box that is outside of the image plane, while ``A`` is the
  366. total area of the bounding box.
  367. Added in 0.4.0.
  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
  374. and must contain at least two integers.
  375. Returns
  376. -------
  377. float
  378. Fraction of the bounding box area that is outside of the image
  379. plane. Returns ``0.0`` if the bounding box is fully inside of
  380. the image plane. If the bounding box has an area of zero, the
  381. result is ``1.0`` if its coordinates are outside of the image
  382. plane, otherwise ``0.0``.
  383. """
  384. area = self.area
  385. if area == 0:
  386. shape = normalize_shape(image)
  387. height, width = shape[0:2]
  388. y1_outside = self.y1 < 0 or self.y1 >= height
  389. x1_outside = self.x1 < 0 or self.x1 >= width
  390. is_outside = (y1_outside or x1_outside)
  391. return 1.0 if is_outside else 0.0
  392. return self.compute_out_of_image_area(image) / area
  393. def is_fully_within_image(self, image):
  394. """Estimate whether the bounding box is fully inside the image area.
  395. Parameters
  396. ----------
  397. image : (H,W,...) ndarray or tuple of int
  398. Image dimensions to use.
  399. If an ``ndarray``, its shape will be used.
  400. If a ``tuple``, it is assumed to represent the image shape
  401. and must contain at least two integers.
  402. Returns
  403. -------
  404. bool
  405. ``True`` if the bounding box is fully inside the image area.
  406. ``False`` otherwise.
  407. """
  408. shape = normalize_shape(image)
  409. height, width = shape[0:2]
  410. return (
  411. self.x1 >= 0
  412. and self.x2 < width
  413. and self.y1 >= 0
  414. and self.y2 < height)
  415. def is_partly_within_image(self, image):
  416. """Estimate whether the BB is at least partially inside the image area.
  417. Parameters
  418. ----------
  419. image : (H,W,...) ndarray or tuple of int
  420. Image dimensions to use.
  421. If an ``ndarray``, its shape will be used.
  422. If a ``tuple``, it is assumed to represent the image shape
  423. and must contain at least two integers.
  424. Returns
  425. -------
  426. bool
  427. ``True`` if the bounding box is at least partially inside the
  428. image area.
  429. ``False`` otherwise.
  430. """
  431. shape = normalize_shape(image)
  432. height, width = shape[0:2]
  433. eps = np.finfo(np.float32).eps
  434. img_bb = BoundingBox(x1=0, x2=width-eps, y1=0, y2=height-eps)
  435. return self.intersection(img_bb) is not None
  436. def is_out_of_image(self, image, fully=True, partly=False):
  437. """Estimate whether the BB is partially/fully outside of the image area.
  438. Parameters
  439. ----------
  440. image : (H,W,...) ndarray or tuple of int
  441. Image dimensions to use.
  442. If an ``ndarray``, its shape will be used.
  443. If a ``tuple``, it is assumed to represent the image shape and
  444. must contain at least two integers.
  445. fully : bool, optional
  446. Whether to return ``True`` if the bounding box is fully outside
  447. of the image area.
  448. partly : bool, optional
  449. Whether to return ``True`` if the bounding box is at least
  450. partially outside fo the image area.
  451. Returns
  452. -------
  453. bool
  454. ``True`` if the bounding box is partially/fully outside of the
  455. image area, depending on defined parameters.
  456. ``False`` otherwise.
  457. """
  458. if self.is_fully_within_image(image):
  459. return False
  460. if self.is_partly_within_image(image):
  461. return partly
  462. return fully
  463. @ia.deprecated(alt_func="BoundingBox.clip_out_of_image()",
  464. comment="clip_out_of_image() has the exactly same "
  465. "interface.")
  466. def cut_out_of_image(self, *args, **kwargs):
  467. """Clip off all parts of the BB box that are outside of the image."""
  468. return self.clip_out_of_image(*args, **kwargs)
  469. def clip_out_of_image_(self, image):
  470. """Clip off parts of the BB box that are outside of the image in-place.
  471. Added in 0.4.0.
  472. Parameters
  473. ----------
  474. image : (H,W,...) ndarray or tuple of int
  475. Image dimensions to use for the clipping of the bounding box.
  476. If an ``ndarray``, its shape will be used.
  477. If a ``tuple``, it is assumed to represent the image shape and
  478. must contain at least two integers.
  479. Returns
  480. -------
  481. imgaug.augmentables.bbs.BoundingBox
  482. Bounding box, clipped to fall within the image dimensions.
  483. The object may have been modified in-place.
  484. """
  485. shape = normalize_shape(image)
  486. height, width = shape[0:2]
  487. assert height > 0, (
  488. "Expected image with height>0, got shape %s." % (image.shape,))
  489. assert width > 0, (
  490. "Expected image with width>0, got shape %s." % (image.shape,))
  491. eps = np.finfo(np.float32).eps
  492. self.x1 = np.clip(self.x1, 0, width - eps)
  493. self.x2 = np.clip(self.x2, 0, width - eps)
  494. self.y1 = np.clip(self.y1, 0, height - eps)
  495. self.y2 = np.clip(self.y2, 0, height - eps)
  496. return self
  497. def clip_out_of_image(self, image):
  498. """Clip off all parts of the BB box that are outside of the image.
  499. Parameters
  500. ----------
  501. image : (H,W,...) ndarray or tuple of int
  502. Image dimensions to use for the clipping of the bounding box.
  503. If an ``ndarray``, its shape will be used.
  504. If a ``tuple``, it is assumed to represent the image shape and
  505. must contain at least two integers.
  506. Returns
  507. -------
  508. imgaug.augmentables.bbs.BoundingBox
  509. Bounding box, clipped to fall within the image dimensions.
  510. """
  511. return self.deepcopy().clip_out_of_image_(image)
  512. def shift_(self, x=0, y=0):
  513. """Move this bounding box along the x/y-axis in-place.
  514. The origin ``(0, 0)`` is at the top left of the image.
  515. Added in 0.4.0.
  516. Parameters
  517. ----------
  518. x : number, optional
  519. Value to be added to all x-coordinates. Positive values shift
  520. towards the right images.
  521. y : number, optional
  522. Value to be added to all y-coordinates. Positive values shift
  523. towards the bottom images.
  524. Returns
  525. -------
  526. imgaug.augmentables.bbs.BoundingBox
  527. Shifted bounding box.
  528. The object may have been modified in-place.
  529. """
  530. self.x1 += x
  531. self.x2 += x
  532. self.y1 += y
  533. self.y2 += y
  534. return self
  535. def shift(self, x=0, y=0, top=None, right=None, bottom=None, left=None):
  536. """Move this bounding box along the x/y-axis.
  537. The origin ``(0, 0)`` is at the top left of the image.
  538. Parameters
  539. ----------
  540. x : number, optional
  541. Value to be added to all x-coordinates. Positive values shift
  542. towards the right images.
  543. y : number, optional
  544. Value to be added to all y-coordinates. Positive values shift
  545. towards the bottom images.
  546. top : None or int, optional
  547. Deprecated since 0.4.0.
  548. Amount of pixels by which to shift this object *from* the
  549. top (towards the bottom).
  550. right : None or int, optional
  551. Deprecated since 0.4.0.
  552. Amount of pixels by which to shift this object *from* the
  553. right (towards the left).
  554. bottom : None or int, optional
  555. Deprecated since 0.4.0.
  556. Amount of pixels by which to shift this object *from* the
  557. bottom (towards the top).
  558. left : None or int, optional
  559. Deprecated since 0.4.0.
  560. Amount of pixels by which to shift this object *from* the
  561. left (towards the right).
  562. Returns
  563. -------
  564. imgaug.augmentables.bbs.BoundingBox
  565. Shifted bounding box.
  566. """
  567. # pylint: disable=redefined-outer-name
  568. x, y = _normalize_shift_args(
  569. x, y, top=top, right=right, bottom=bottom, left=left)
  570. return self.deepcopy().shift_(x, y)
  571. def draw_label_on_image(self, image, color=(0, 255, 0),
  572. color_text=None, color_bg=None, alpha=1.0, size=1,
  573. size_text=20, height=30,
  574. copy=True, raise_if_out_of_image=False):
  575. """Draw a box showing the BB's label.
  576. The box is placed right above the BB's rectangle.
  577. Added in 0.4.0.
  578. Parameters
  579. ----------
  580. image : (H,W,C) ndarray
  581. The image onto which to draw the label.
  582. Currently expected to be ``uint8``.
  583. color : None or iterable of int, optional
  584. The color to use, corresponding to the channel layout of the
  585. image. Usually RGB. Text and background colors will be derived
  586. from this.
  587. color_text : None or iterable of int, optional
  588. The text color to use.
  589. If ``None``, derived from `color_bg`.
  590. color_bg : None or iterable of int, optional
  591. The background color of the label box.
  592. If ``None``, derived from `color`.
  593. alpha : float, optional
  594. The transparency of the drawn bounding box, where ``1.0`` denotes
  595. no transparency and ``0.0`` is invisible.
  596. size : int, optional
  597. The thickness of the bounding box in pixels. If the value is
  598. larger than ``1``, then additional pixels will be added around
  599. the bounding box (i.e. extension towards the outside).
  600. size_text : int, optional
  601. Font size to use.
  602. height : int, optional
  603. Height of the label box in pixels.
  604. copy : bool, optional
  605. Whether to copy the input image or change it in-place.
  606. raise_if_out_of_image : bool, optional
  607. Whether to raise an error if the bounding box is fully outside of
  608. the image. If set to ``False``, no error will be raised and only
  609. the parts inside the image will be drawn.
  610. Returns
  611. -------
  612. (H,W,C) ndarray(uint8)
  613. Image with bounding box drawn on it.
  614. """
  615. # pylint: disable=redefined-outer-name
  616. drawer = _LabelOnImageDrawer(
  617. color=color,
  618. color_text=color_text,
  619. color_bg=color_bg,
  620. size=size,
  621. alpha=alpha,
  622. raise_if_out_of_image=raise_if_out_of_image,
  623. height=height,
  624. size_text=size_text)
  625. if copy:
  626. return drawer.draw_on_image(image, self)
  627. return drawer.draw_on_image_(image, self)
  628. def draw_box_on_image(self, image, color=(0, 255, 0), alpha=1.0,
  629. size=1, copy=True, raise_if_out_of_image=False,
  630. thickness=None):
  631. """Draw the rectangle of the bounding box on an image.
  632. This method does not draw the label.
  633. Added in 0.4.0.
  634. Parameters
  635. ----------
  636. image : (H,W,C) ndarray
  637. The image onto which to draw the bounding box rectangle.
  638. Currently expected to be ``uint8``.
  639. color : iterable of int, optional
  640. The color to use, corresponding to the channel layout of the
  641. image. Usually RGB.
  642. alpha : float, optional
  643. The transparency of the drawn bounding box, where ``1.0`` denotes
  644. no transparency and ``0.0`` is invisible.
  645. size : int, optional
  646. The thickness of the bounding box in pixels. If the value is
  647. larger than ``1``, then additional pixels will be added around
  648. the bounding box (i.e. extension towards the outside).
  649. copy : bool, optional
  650. Whether to copy the input image or change it in-place.
  651. raise_if_out_of_image : bool, optional
  652. Whether to raise an error if the bounding box is fully outside of
  653. the image. If set to ``False``, no error will be raised and only
  654. the parts inside the image will be drawn.
  655. thickness : None or int, optional
  656. Deprecated.
  657. Returns
  658. -------
  659. (H,W,C) ndarray(uint8)
  660. Image with bounding box drawn on it.
  661. """
  662. # pylint: disable=invalid-name, redefined-outer-name
  663. if thickness is not None:
  664. ia.warn_deprecated(
  665. "Usage of argument 'thickness' in BoundingBox.draw_on_image() "
  666. "is deprecated. The argument was renamed to 'size'.")
  667. size = thickness
  668. if raise_if_out_of_image and self.is_out_of_image(image):
  669. raise Exception(
  670. "Cannot draw bounding box x1=%.8f, y1=%.8f, x2=%.8f, y2=%.8f "
  671. "on image with shape %s." % (
  672. self.x1, self.y1, self.x2, self.y2, image.shape))
  673. result = np.copy(image) if copy else image
  674. if isinstance(color, (tuple, list)):
  675. color = np.uint8(color)
  676. for i in range(size):
  677. y1, y2, x1, x2 = self.y1_int, self.y2_int, self.x1_int, self.x2_int
  678. # When y values get into the range (H-0.5, H), the *_int functions
  679. # round them to H. That is technically sensible, but in the case
  680. # of drawing means that the border lies just barely outside of
  681. # the image, making the border disappear, even though the BB is
  682. # fully inside the image. Here we correct for that because of
  683. # beauty reasons. Same is the case for x coordinates.
  684. if self.is_fully_within_image(image):
  685. y1 = np.clip(y1, 0, image.shape[0]-1)
  686. y2 = np.clip(y2, 0, image.shape[0]-1)
  687. x1 = np.clip(x1, 0, image.shape[1]-1)
  688. x2 = np.clip(x2, 0, image.shape[1]-1)
  689. y = [y1-i, y1-i, y2+i, y2+i]
  690. x = [x1-i, x2+i, x2+i, x1-i]
  691. rr, cc = skimage.draw.polygon_perimeter(y, x, shape=result.shape)
  692. if alpha >= 0.99:
  693. result[rr, cc, :] = color
  694. else:
  695. if ia.is_float_array(result):
  696. # TODO use blend_alpha here
  697. result[rr, cc, :] = (
  698. (1 - alpha) * result[rr, cc, :]
  699. + alpha * color)
  700. result = np.clip(result, 0, 255)
  701. else:
  702. input_dtype = result.dtype
  703. result = result.astype(np.float32)
  704. result[rr, cc, :] = (
  705. (1 - alpha) * result[rr, cc, :]
  706. + alpha * color)
  707. result = np.clip(result, 0, 255).astype(input_dtype)
  708. return result
  709. # TODO add explicit test for zero-sized BBs (worked when tested by hand)
  710. def draw_on_image(self, image, color=(0, 255, 0), alpha=1.0, size=1,
  711. copy=True, raise_if_out_of_image=False, thickness=None):
  712. """Draw the bounding box on an image.
  713. This will automatically also draw the label, unless it is ``None``.
  714. To only draw the box rectangle use
  715. :func:`~imgaug.augmentables.bbs.BoundingBox.draw_box_on_image`.
  716. To draw the label even if it is ``None`` or to configure e.g. its
  717. color, use
  718. :func:`~imgaug.augmentables.bbs.BoundingBox.draw_label_on_image`.
  719. Parameters
  720. ----------
  721. image : (H,W,C) ndarray
  722. The image onto which to draw the bounding box.
  723. Currently expected to be ``uint8``.
  724. color : iterable of int, optional
  725. The color to use, corresponding to the channel layout of the
  726. image. Usually RGB.
  727. alpha : float, optional
  728. The transparency of the drawn bounding box, where ``1.0`` denotes
  729. no transparency and ``0.0`` is invisible.
  730. size : int, optional
  731. The thickness of the bounding box in pixels. If the value is
  732. larger than ``1``, then additional pixels will be added around
  733. the bounding box (i.e. extension towards the outside).
  734. copy : bool, optional
  735. Whether to copy the input image or change it in-place.
  736. raise_if_out_of_image : bool, optional
  737. Whether to raise an error if the bounding box is fully outside of
  738. the image. If set to ``False``, no error will be raised and only
  739. the parts inside the image will be drawn.
  740. thickness : None or int, optional
  741. Deprecated.
  742. Returns
  743. -------
  744. (H,W,C) ndarray(uint8)
  745. Image with bounding box drawn on it.
  746. """
  747. # pylint: disable=redefined-outer-name
  748. image_drawn = self.draw_box_on_image(
  749. image, color=color, alpha=alpha, size=size,
  750. copy=copy, raise_if_out_of_image=raise_if_out_of_image,
  751. thickness=thickness
  752. )
  753. if self.label is not None:
  754. image_drawn = self.draw_label_on_image(
  755. image_drawn, color=color, alpha=alpha,
  756. size=size if thickness is None else thickness,
  757. copy=False, raise_if_out_of_image=raise_if_out_of_image
  758. )
  759. return image_drawn
  760. # TODO add tests for pad and pad_max
  761. def extract_from_image(self, image, pad=True, pad_max=None,
  762. prevent_zero_size=True):
  763. """Extract the image pixels within the bounding box.
  764. This function will zero-pad the image if the bounding box is
  765. partially/fully outside of the image.
  766. Parameters
  767. ----------
  768. image : (H,W) ndarray or (H,W,C) ndarray
  769. The image from which to extract the pixels within the bounding box.
  770. pad : bool, optional
  771. Whether to zero-pad the image if the object is partially/fully
  772. outside of it.
  773. pad_max : None or int, optional
  774. The maximum number of pixels that may be zero-paded on any side,
  775. i.e. if this has value ``N`` the total maximum of added pixels
  776. is ``4*N``.
  777. This option exists to prevent extremely large images as a result of
  778. single points being moved very far away during augmentation.
  779. prevent_zero_size : bool, optional
  780. Whether to prevent the height or width of the extracted image from
  781. becoming zero.
  782. If this is set to ``True`` and the height or width of the bounding
  783. box is below ``1``, the height/width will be increased to ``1``.
  784. This can be useful to prevent problems, e.g. with image saving or
  785. plotting.
  786. If it is set to ``False``, images will be returned as ``(H', W')``
  787. or ``(H', W', 3)`` with ``H`` or ``W`` potentially being 0.
  788. Returns
  789. -------
  790. (H',W') ndarray or (H',W',C) ndarray
  791. Pixels within the bounding box. Zero-padded if the bounding box
  792. is partially/fully outside of the image.
  793. If `prevent_zero_size` is activated, it is guarantueed that
  794. ``H'>0`` and ``W'>0``, otherwise only ``H'>=0`` and ``W'>=0``.
  795. """
  796. # pylint: disable=no-else-return, too-many-statements
  797. height, width = image.shape[0], image.shape[1]
  798. x1, x2, y1, y2 = self.x1_int, self.x2_int, self.y1_int, self.y2_int
  799. # When y values get into the range (H-0.5, H), the *_int functions
  800. # round them to H. That is technically sensible, but in the case of
  801. # extraction leads to a black border, which is both ugly and
  802. # unexpected after calling cut_out_of_image(). Here we correct for
  803. # that because of beauty reasons. Same is the case for x coordinates.
  804. fully_within = self.is_fully_within_image(image)
  805. if fully_within:
  806. y1, y2 = np.clip([y1, y2], 0, height-1)
  807. x1, x2 = np.clip([x1, x2], 0, width-1)
  808. # TODO add test
  809. if prevent_zero_size:
  810. if abs(x2 - x1) < 1:
  811. x2 = x1 + 1
  812. if abs(y2 - y1) < 1:
  813. y2 = y1 + 1
  814. if pad:
  815. # if the bb is outside of the image area, the following pads the
  816. # image first with black pixels until the bb is inside the image
  817. # and only then extracts the image area
  818. # TODO probably more efficient to initialize an array of zeros
  819. # and copy only the portions of the bb into that array that
  820. # are natively inside the image area
  821. from ..augmenters import size as iasize
  822. pad_top = 0
  823. pad_right = 0
  824. pad_bottom = 0
  825. pad_left = 0
  826. if x1 < 0:
  827. pad_left = abs(x1)
  828. x2 = x2 + pad_left
  829. width = width + pad_left
  830. x1 = 0
  831. if y1 < 0:
  832. pad_top = abs(y1)
  833. y2 = y2 + pad_top
  834. height = height + pad_top
  835. y1 = 0
  836. if x2 >= width:
  837. pad_right = x2 - width
  838. if y2 >= height:
  839. pad_bottom = y2 - height
  840. paddings = [pad_top, pad_right, pad_bottom, pad_left]
  841. any_padded = any([val > 0 for val in paddings])
  842. if any_padded:
  843. if pad_max is None:
  844. pad_max = max(paddings)
  845. image = iasize.pad(
  846. image,
  847. top=min(pad_top, pad_max),
  848. right=min(pad_right, pad_max),
  849. bottom=min(pad_bottom, pad_max),
  850. left=min(pad_left, pad_max)
  851. )
  852. return image[y1:y2, x1:x2]
  853. else:
  854. within_image = (
  855. (0, 0, 0, 0)
  856. <= (x1, y1, x2, y2)
  857. < (width, height, width, height)
  858. )
  859. out_height, out_width = (y2 - y1), (x2 - x1)
  860. nonzero_height = (out_height > 0)
  861. nonzero_width = (out_width > 0)
  862. if within_image and nonzero_height and nonzero_width:
  863. return image[y1:y2, x1:x2]
  864. if prevent_zero_size:
  865. out_height = 1
  866. out_width = 1
  867. else:
  868. out_height = 0
  869. out_width = 0
  870. if image.ndim == 2:
  871. return np.zeros((out_height, out_width), dtype=image.dtype)
  872. return np.zeros((out_height, out_width, image.shape[-1]),
  873. dtype=image.dtype)
  874. # TODO also add to_heatmap
  875. # TODO add this to BoundingBoxesOnImage
  876. # TODO add label to keypoints?
  877. def to_keypoints(self):
  878. """Convert the BB's corners to keypoints (clockwise, from top left).
  879. Returns
  880. -------
  881. list of imgaug.augmentables.kps.Keypoint
  882. Corners of the bounding box as keypoints.
  883. """
  884. # TODO get rid of this deferred import
  885. from imgaug.augmentables.kps import Keypoint
  886. return [
  887. Keypoint(x=self.x1, y=self.y1),
  888. Keypoint(x=self.x2, y=self.y1),
  889. Keypoint(x=self.x2, y=self.y2),
  890. Keypoint(x=self.x1, y=self.y2)
  891. ]
  892. def to_polygon(self):
  893. """Convert this bounding box to a polygon covering the same area.
  894. Added in 0.4.0.
  895. Returns
  896. -------
  897. imgaug.augmentables.polys.Polygon
  898. The bounding box converted to a polygon.
  899. """
  900. # TODO get rid of this deferred import
  901. from imgaug.augmentables.polys import Polygon
  902. return Polygon([
  903. (self.x1, self.y1),
  904. (self.x2, self.y1),
  905. (self.x2, self.y2),
  906. (self.x1, self.y2)
  907. ], label=self.label)
  908. # TODO also introduce similar area_almost_equals()
  909. def coords_almost_equals(self, other, max_distance=1e-4):
  910. """Estimate if this and another BB have almost identical coordinates.
  911. Added in 0.4.0.
  912. Parameters
  913. ----------
  914. other : imgaug.augmentables.bbs.BoundingBox or iterable
  915. The other bounding box with which to compare this one.
  916. If this is an ``iterable``, it is assumed to represent the top-left
  917. and bottom-right coordinates of that bounding box, given as e.g.
  918. an ``(2,2)`` ndarray or an ``(4,)`` ndarray or as a similar list.
  919. max_distance : number, optional
  920. The maximum euclidean distance between a corner on one bounding
  921. box and the closest corner on the other bounding box. If the
  922. distance is exceeded for any such pair, the two BBs are not
  923. viewed as equal.
  924. Returns
  925. -------
  926. bool
  927. Whether the two bounding boxes have almost identical corner
  928. coordinates.
  929. """
  930. if isinstance(other, BoundingBox):
  931. coords_b = other.coords.flat
  932. elif ia.is_np_array(other):
  933. # we use flat here in case other is (N,2) instead of (4,)
  934. coords_b = other.flat
  935. elif ia.is_iterable(other):
  936. coords_b = list(ia.flatten(other))
  937. else:
  938. raise ValueError(
  939. "Expected 'other' to be an iterable containing two "
  940. "(x,y)-coordinate pairs or a BoundingBox. "
  941. "Got type %s." % (type(other),))
  942. coords_a = self.coords
  943. return np.allclose(coords_a.flat, coords_b, atol=max_distance, rtol=0)
  944. def almost_equals(self, other, max_distance=1e-4):
  945. """Compare this and another BB's label and coordinates.
  946. This is the same as
  947. :func:`~imgaug.augmentables.bbs.BoundingBox.coords_almost_equals` but
  948. additionally compares the labels.
  949. Added in 0.4.0.
  950. Parameters
  951. ----------
  952. other : imgaug.augmentables.bbs.BoundingBox or iterable
  953. The other object to compare against. Expected to be a
  954. ``BoundingBox``.
  955. max_distance : number, optional
  956. See
  957. :func:`~imgaug.augmentables.bbs.BoundingBox.coords_almost_equals`.
  958. Returns
  959. -------
  960. bool
  961. ``True`` if the coordinates are almost equal and additionally
  962. the labels are equal. Otherwise ``False``.
  963. """
  964. if self.label != other.label:
  965. return False
  966. return self.coords_almost_equals(other, max_distance=max_distance)
  967. @classmethod
  968. def from_point_soup(cls, xy):
  969. """Convert a ``(2P,) or (P,2) ndarray`` to a BB instance.
  970. This is the inverse of
  971. :func:`~imgaug.BoundingBoxesOnImage.to_xyxy_array`.
  972. Added in 0.4.0.
  973. Parameters
  974. ----------
  975. xy : (2P,) ndarray or (P, 2) array or iterable of number or iterable of iterable of number
  976. Array containing ``P`` points in xy-form denoting a soup of
  977. points around which to place a bounding box.
  978. The array should usually be of dtype ``float32``.
  979. Returns
  980. -------
  981. imgaug.augmentables.bbs.BoundingBox
  982. Bounding box around the points.
  983. """
  984. # pylint: disable=unsubscriptable-object
  985. xy = np.array(xy, dtype=np.float32)
  986. assert len(xy) > 0, (
  987. "Expected to get at least one point to place a bounding box "
  988. "around, got shape %s." % (xy.shape,))
  989. assert xy.ndim == 1 or (xy.ndim == 2 and xy.shape[-1] == 2), (
  990. "Expected input array of shape (P,) or (P, 2), "
  991. "got shape %s." % (xy.shape,))
  992. if xy.ndim == 1:
  993. xy = xy.reshape((-1, 2))
  994. x1, y1 = np.min(xy, axis=0)
  995. x2, y2 = np.max(xy, axis=0)
  996. return cls(x1=x1, y1=y1, x2=x2, y2=y2)
  997. def copy(self, x1=None, y1=None, x2=None, y2=None, label=None):
  998. """Create a shallow copy of this BoundingBox instance.
  999. Parameters
  1000. ----------
  1001. x1 : None or number
  1002. If not ``None``, then the ``x1`` coordinate of the copied object
  1003. will be set to this value.
  1004. y1 : None or number
  1005. If not ``None``, then the ``y1`` coordinate of the copied object
  1006. will be set to this value.
  1007. x2 : None or number
  1008. If not ``None``, then the ``x2`` coordinate of the copied object
  1009. will be set to this value.
  1010. y2 : None or number
  1011. If not ``None``, then the ``y2`` coordinate of the copied object
  1012. will be set to this value.
  1013. label : None or string
  1014. If not ``None``, then the ``label`` of the copied object
  1015. will be set to this value.
  1016. Returns
  1017. -------
  1018. imgaug.augmentables.bbs.BoundingBox
  1019. Shallow copy.
  1020. """
  1021. return BoundingBox(
  1022. x1=self.x1 if x1 is None else x1,
  1023. x2=self.x2 if x2 is None else x2,
  1024. y1=self.y1 if y1 is None else y1,
  1025. y2=self.y2 if y2 is None else y2,
  1026. label=copy.deepcopy(self.label) if label is None else label
  1027. )
  1028. def deepcopy(self, x1=None, y1=None, x2=None, y2=None, label=None):
  1029. """
  1030. Create a deep copy of the BoundingBox object.
  1031. Parameters
  1032. ----------
  1033. x1 : None or number
  1034. If not ``None``, then the ``x1`` coordinate of the copied object
  1035. will be set to this value.
  1036. y1 : None or number
  1037. If not ``None``, then the ``y1`` coordinate of the copied object
  1038. will be set to this value.
  1039. x2 : None or number
  1040. If not ``None``, then the ``x2`` coordinate of the copied object
  1041. will be set to this value.
  1042. y2 : None or number
  1043. If not ``None``, then the ``y2`` coordinate of the copied object
  1044. will be set to this value.
  1045. label : None or string
  1046. If not ``None``, then the ``label`` of the copied object
  1047. will be set to this value.
  1048. Returns
  1049. -------
  1050. imgaug.augmentables.bbs.BoundingBox
  1051. Deep copy.
  1052. """
  1053. # TODO write specific copy routine with deepcopy for label and remove
  1054. # the deepcopy from copy()
  1055. return self.copy(x1=x1, y1=y1, x2=x2, y2=y2, label=label)
  1056. def __getitem__(self, indices):
  1057. """Get the coordinate(s) with given indices.
  1058. Added in 0.4.0.
  1059. Returns
  1060. -------
  1061. ndarray
  1062. xy-coordinate(s) as ``ndarray``.
  1063. """
  1064. return self.coords[indices]
  1065. def __iter__(self):
  1066. """Iterate over the coordinates of this instance.
  1067. Added in 0.4.0.
  1068. Yields
  1069. ------
  1070. ndarray
  1071. An ``(2,)`` ``ndarray`` denoting an xy-coordinate pair.
  1072. """
  1073. return iter(self.coords)
  1074. def __repr__(self):
  1075. return self.__str__()
  1076. def __str__(self):
  1077. return "BoundingBox(x1=%.4f, y1=%.4f, x2=%.4f, y2=%.4f, label=%s)" % (
  1078. self.x1, self.y1, self.x2, self.y2, self.label)
  1079. class BoundingBoxesOnImage(IAugmentable):
  1080. """Container for the list of all bounding boxes on a single image.
  1081. Parameters
  1082. ----------
  1083. bounding_boxes : list of imgaug.augmentables.bbs.BoundingBox
  1084. List of bounding boxes on the image.
  1085. shape : tuple of int or ndarray
  1086. The shape of the image on which the objects are placed.
  1087. Either an image with shape ``(H,W,[C])`` or a ``tuple`` denoting
  1088. such an image shape.
  1089. Examples
  1090. --------
  1091. >>> import numpy as np
  1092. >>> from imgaug.augmentables.bbs import BoundingBox, BoundingBoxesOnImage
  1093. >>>
  1094. >>> image = np.zeros((100, 100))
  1095. >>> bbs = [
  1096. >>> BoundingBox(x1=10, y1=20, x2=20, y2=30),
  1097. >>> BoundingBox(x1=25, y1=50, x2=30, y2=70)
  1098. >>> ]
  1099. >>> bbs_oi = BoundingBoxesOnImage(bbs, shape=image.shape)
  1100. """
  1101. def __init__(self, bounding_boxes, shape):
  1102. self.bounding_boxes = bounding_boxes
  1103. self.shape = normalize_shape(shape)
  1104. @property
  1105. def items(self):
  1106. """Get the bounding boxes in this container.
  1107. Added in 0.4.0.
  1108. Returns
  1109. -------
  1110. list of BoundingBox
  1111. Bounding boxes within this container.
  1112. """
  1113. return self.bounding_boxes
  1114. @items.setter
  1115. def items(self, value):
  1116. """Set the bounding boxes in this container.
  1117. Added in 0.4.0.
  1118. Parameters
  1119. ----------
  1120. value : list of BoundingBox
  1121. Bounding boxes within this container.
  1122. """
  1123. self.bounding_boxes = value
  1124. # TODO remove this? here it is image height, but in BoundingBox it is
  1125. # bounding box height
  1126. @property
  1127. def height(self):
  1128. """Get the height of the image on which the bounding boxes fall.
  1129. Returns
  1130. -------
  1131. int
  1132. Image height.
  1133. """
  1134. return self.shape[0]
  1135. # TODO remove this? here it is image width, but in BoundingBox it is
  1136. # bounding box width
  1137. @property
  1138. def width(self):
  1139. """Get the width of the image on which the bounding boxes fall.
  1140. Returns
  1141. -------
  1142. int
  1143. Image width.
  1144. """
  1145. return self.shape[1]
  1146. @property
  1147. def empty(self):
  1148. """Determine whether this instance contains zero bounding boxes.
  1149. Returns
  1150. -------
  1151. bool
  1152. True if this object contains zero bounding boxes.
  1153. """
  1154. return len(self.bounding_boxes) == 0
  1155. def on_(self, image):
  1156. """Project BBs from one image (shape) to a another one in-place.
  1157. Added in 0.4.0.
  1158. Parameters
  1159. ----------
  1160. image : ndarray or tuple of int
  1161. New image onto which the bounding boxes are to be projected.
  1162. May also simply be that new image's shape tuple.
  1163. Returns
  1164. -------
  1165. imgaug.augmentables.bbs.BoundingBoxesOnImage
  1166. Object containing the same bounding boxes after projection to
  1167. the new image shape.
  1168. The object and its items may have been modified in-place.
  1169. """
  1170. # pylint: disable=invalid-name
  1171. on_shape = normalize_shape(image)
  1172. if on_shape[0:2] == self.shape[0:2]:
  1173. self.shape = on_shape # channels may differ
  1174. return self
  1175. for i, item in enumerate(self.items):
  1176. self.bounding_boxes[i] = item.project_(self.shape, on_shape)
  1177. self.shape = on_shape
  1178. return self
  1179. def on(self, image):
  1180. """Project bounding boxes from one image (shape) to a another one.
  1181. Parameters
  1182. ----------
  1183. image : ndarray or tuple of int
  1184. New image onto which the bounding boxes are to be projected.
  1185. May also simply be that new image's shape tuple.
  1186. Returns
  1187. -------
  1188. imgaug.augmentables.bbs.BoundingBoxesOnImage
  1189. Object containing the same bounding boxes after projection to
  1190. the new image shape.
  1191. """
  1192. # pylint: disable=invalid-name
  1193. return self.deepcopy().on_(image)
  1194. @classmethod
  1195. def from_xyxy_array(cls, xyxy, shape):
  1196. """Convert an ``(N, 4) or (N, 2, 2) ndarray`` to a BBsOI instance.
  1197. This is the inverse of
  1198. :func:`~imgaug.BoundingBoxesOnImage.to_xyxy_array`.
  1199. Parameters
  1200. ----------
  1201. xyxy : (N, 4) ndarray or (N, 2, 2) array
  1202. Array containing the corner coordinates of ``N`` bounding boxes.
  1203. Each bounding box is represented by its top-left and bottom-right
  1204. coordinates.
  1205. The array should usually be of dtype ``float32``.
  1206. shape : tuple of int
  1207. Shape of the image on which the bounding boxes are placed.
  1208. Should usually be ``(H, W, C)`` or ``(H, W)``.
  1209. Returns
  1210. -------
  1211. imgaug.augmentables.bbs.BoundingBoxesOnImage
  1212. Object containing a list of :class:`BoundingBox` instances
  1213. derived from the provided corner coordinates.
  1214. """
  1215. # pylint: disable=unsubscriptable-object
  1216. xyxy = np.array(xyxy, dtype=np.float32)
  1217. # note that np.array([]) is (0,), not (0, 2)
  1218. if xyxy.shape[0] == 0:
  1219. return BoundingBoxesOnImage([], shape)
  1220. assert (
  1221. (xyxy.ndim == 2 and xyxy.shape[-1] == 4)
  1222. or (xyxy.ndim == 3 and xyxy.shape[1:3] == (2, 2))), (
  1223. "Expected input array of shape (N, 4) or (N, 2, 2), "
  1224. "got shape %s." % (xyxy.shape,))
  1225. xyxy = xyxy.reshape((-1, 2, 2))
  1226. boxes = [BoundingBox.from_point_soup(row) for row in xyxy]
  1227. return cls(boxes, shape)
  1228. @classmethod
  1229. def from_point_soups(cls, xy, shape):
  1230. """Convert an ``(N, 2P) or (N, P, 2) ndarray`` to a BBsOI instance.
  1231. Added in 0.4.0.
  1232. Parameters
  1233. ----------
  1234. xy : (N, 2P) ndarray or (N, P, 2) array or iterable of iterable of number or iterable of iterable of iterable of number
  1235. Array containing the corner coordinates of ``N`` bounding boxes.
  1236. Each bounding box is represented by a soup of ``P`` points.
  1237. If ``(N, P)`` then the second axis is expected to be in
  1238. xy-form (e.g. ``x1``, ``y1``, ``x2``, ``y2``, ...).
  1239. The final bounding box coordinates will be derived using ``min``
  1240. and ``max`` operations on the xy-values.
  1241. The array should usually be of dtype ``float32``.
  1242. shape : tuple of int
  1243. Shape of the image on which the bounding boxes are placed.
  1244. Should usually be ``(H, W, C)`` or ``(H, W)``.
  1245. Returns
  1246. -------
  1247. imgaug.augmentables.bbs.BoundingBoxesOnImage
  1248. Object containing a list of :class:`BoundingBox` instances
  1249. derived from the provided point soups.
  1250. """
  1251. xy = np.array(xy, dtype=np.float32)
  1252. # from_xy_array() already checks the ndim/shape, so we don't have to
  1253. # do it here
  1254. boxes = [BoundingBox.from_point_soup(row) for row in xy]
  1255. return cls(boxes, shape)
  1256. def to_xyxy_array(self, dtype=np.float32):
  1257. """Convert the ``BoundingBoxesOnImage`` object to an ``(N,4) ndarray``.
  1258. This is the inverse of
  1259. :func:`~imgaug.BoundingBoxesOnImage.from_xyxy_array`.
  1260. Parameters
  1261. ----------
  1262. dtype : numpy.dtype, optional
  1263. Desired output datatype of the ndarray.
  1264. Returns
  1265. -------
  1266. ndarray
  1267. ``(N,4) ndarray``, where ``N`` denotes the number of bounding
  1268. boxes and ``4`` denotes the top-left and bottom-right bounding
  1269. box corner coordinates in form ``(x1, y1, x2, y2)``.
  1270. """
  1271. xyxy_array = np.zeros((len(self.bounding_boxes), 4), dtype=np.float32)
  1272. for i, box in enumerate(self.bounding_boxes):
  1273. xyxy_array[i] = [box.x1, box.y1, box.x2, box.y2]
  1274. return xyxy_array.astype(dtype)
  1275. def to_xy_array(self):
  1276. """Convert the ``BoundingBoxesOnImage`` object to an ``(N,2) ndarray``.
  1277. Added in 0.4.0.
  1278. Returns
  1279. -------
  1280. ndarray
  1281. ``(2*B,2) ndarray`` of xy-coordinates, where ``B`` denotes the
  1282. number of bounding boxes.
  1283. """
  1284. return self.to_xyxy_array().reshape((-1, 2))
  1285. def fill_from_xyxy_array_(self, xyxy):
  1286. """Modify the BB coordinates of this instance in-place.
  1287. .. note::
  1288. This currently expects exactly one entry in `xyxy` per bounding
  1289. in this instance. (I.e. two corner coordinates per instance.)
  1290. Otherwise, an ``AssertionError`` will be raised.
  1291. .. note::
  1292. This method will automatically flip x-coordinates if ``x1>x2``
  1293. for a bounding box. (Analogous for y-coordinates.)
  1294. Added in 0.4.0.
  1295. Parameters
  1296. ----------
  1297. xyxy : (N, 4) ndarray or iterable of iterable of number
  1298. Coordinates of ``N`` bounding boxes on an image, given as
  1299. a ``(N,4)`` array of two corner xy-coordinates per bounding box.
  1300. ``N`` must match the number of bounding boxes in this instance.
  1301. Returns
  1302. -------
  1303. BoundingBoxesOnImage
  1304. This instance itself, with updated bounding box coordinates.
  1305. Note that the instance was modified in-place.
  1306. """
  1307. xyxy = np.array(xyxy, dtype=np.float32)
  1308. # note that np.array([]) is (0,), not (0, 4)
  1309. assert xyxy.shape[0] == 0 or (xyxy.ndim == 2 and xyxy.shape[-1] == 4), ( # pylint: disable=unsubscriptable-object
  1310. "Expected input array to have shape (N,4), "
  1311. "got shape %s." % (xyxy.shape,))
  1312. assert len(xyxy) == len(self.bounding_boxes), (
  1313. "Expected to receive an array with as many rows there are "
  1314. "bounding boxes in this instance. Got %d rows, expected %d." % (
  1315. len(xyxy), len(self.bounding_boxes)))
  1316. for bb, (x1, y1, x2, y2) in zip(self.bounding_boxes, xyxy):
  1317. bb.x1 = min([x1, x2])
  1318. bb.y1 = min([y1, y2])
  1319. bb.x2 = max([x1, x2])
  1320. bb.y2 = max([y1, y2])
  1321. return self
  1322. def fill_from_xy_array_(self, xy):
  1323. """Modify the BB coordinates of this instance in-place.
  1324. See
  1325. :func:`~imgaug.augmentables.bbs.BoundingBoxesOnImage.fill_from_xyxy_array_`.
  1326. Added in 0.4.0.
  1327. Parameters
  1328. ----------
  1329. xy : (2*B, 2) ndarray or iterable of iterable of number
  1330. Coordinates of ``B`` bounding boxes on an image, given as
  1331. a ``(2*B,2)`` array of two corner xy-coordinates per bounding box.
  1332. ``B`` must match the number of bounding boxes in this instance.
  1333. Returns
  1334. -------
  1335. BoundingBoxesOnImage
  1336. This instance itself, with updated bounding box coordinates.
  1337. Note that the instance was modified in-place.
  1338. """
  1339. xy = np.array(xy, dtype=np.float32)
  1340. return self.fill_from_xyxy_array_(xy.reshape((-1, 4)))
  1341. def draw_on_image(self, image, color=(0, 255, 0), alpha=1.0, size=1,
  1342. copy=True, raise_if_out_of_image=False, thickness=None):
  1343. """Draw all bounding boxes onto a given image.
  1344. Parameters
  1345. ----------
  1346. image : (H,W,3) ndarray
  1347. The image onto which to draw the bounding boxes.
  1348. This image should usually have the same shape as set in
  1349. ``BoundingBoxesOnImage.shape``.
  1350. color : int or list of int or tuple of int or (3,) ndarray, optional
  1351. The RGB color of all bounding boxes.
  1352. If a single ``int`` ``C``, then that is equivalent to ``(C,C,C)``.
  1353. alpha : float, optional
  1354. Alpha/transparency of the bounding box.
  1355. size : int, optional
  1356. Thickness in pixels.
  1357. copy : bool, optional
  1358. Whether to copy the image before drawing the bounding boxes.
  1359. raise_if_out_of_image : bool, optional
  1360. Whether to raise an exception if any bounding box is outside of the
  1361. image.
  1362. thickness : None or int, optional
  1363. Deprecated.
  1364. Returns
  1365. -------
  1366. (H,W,3) ndarray
  1367. Image with drawn bounding boxes.
  1368. """
  1369. # pylint: disable=redefined-outer-name
  1370. image = np.copy(image) if copy else image
  1371. for bb in self.bounding_boxes:
  1372. image = bb.draw_on_image(
  1373. image,
  1374. color=color,
  1375. alpha=alpha,
  1376. size=size,
  1377. copy=False,
  1378. raise_if_out_of_image=raise_if_out_of_image,
  1379. thickness=thickness
  1380. )
  1381. return image
  1382. def remove_out_of_image_(self, fully=True, partly=False):
  1383. """Remove in-place all BBs that are fully/partially outside of the image.
  1384. Added in 0.4.0.
  1385. Parameters
  1386. ----------
  1387. fully : bool, optional
  1388. Whether to remove bounding boxes that are fully outside of the
  1389. image.
  1390. partly : bool, optional
  1391. Whether to remove bounding boxes that are partially outside of
  1392. the image.
  1393. Returns
  1394. -------
  1395. imgaug.augmentables.bbs.BoundingBoxesOnImage
  1396. Reduced set of bounding boxes, with those that were
  1397. fully/partially outside of the image being removed.
  1398. The object and its items may have been modified in-place.
  1399. """
  1400. self.bounding_boxes = [
  1401. bb
  1402. for bb
  1403. in self.bounding_boxes
  1404. if not bb.is_out_of_image(self.shape, fully=fully, partly=partly)]
  1405. return self
  1406. def remove_out_of_image(self, fully=True, partly=False):
  1407. """Remove all BBs that are fully/partially outside of the image.
  1408. Parameters
  1409. ----------
  1410. fully : bool, optional
  1411. Whether to remove bounding boxes that are fully outside of the
  1412. image.
  1413. partly : bool, optional
  1414. Whether to remove bounding boxes that are partially outside of
  1415. the image.
  1416. Returns
  1417. -------
  1418. imgaug.augmentables.bbs.BoundingBoxesOnImage
  1419. Reduced set of bounding boxes, with those that were
  1420. fully/partially outside of the image being removed.
  1421. """
  1422. return self.copy().remove_out_of_image_(fully=fully, partly=partly)
  1423. def remove_out_of_image_fraction_(self, fraction):
  1424. """Remove in-place all BBs with an OOI fraction of at least `fraction`.
  1425. 'OOI' is the abbreviation for 'out of image'.
  1426. Added in 0.4.0.
  1427. Parameters
  1428. ----------
  1429. fraction : number
  1430. Minimum out of image fraction that a bounding box has to have in
  1431. order to be removed. A fraction of ``1.0`` removes only bounding
  1432. boxes that are ``100%`` outside of the image. A fraction of ``0.0``
  1433. removes all bounding boxes.
  1434. Returns
  1435. -------
  1436. imgaug.augmentables.bbs.BoundingBoxesOnImage
  1437. Reduced set of bounding boxes, with those that had an out of image
  1438. fraction greater or equal the given one removed.
  1439. The object and its items may have been modified in-place.
  1440. """
  1441. return _remove_out_of_image_fraction_(self, fraction)
  1442. def remove_out_of_image_fraction(self, fraction):
  1443. """Remove all BBs with an out of image fraction of at least `fraction`.
  1444. Added in 0.4.0.
  1445. Parameters
  1446. ----------
  1447. fraction : number
  1448. Minimum out of image fraction that a bounding box has to have in
  1449. order to be removed. A fraction of ``1.0`` removes only bounding
  1450. boxes that are ``100%`` outside of the image. A fraction of ``0.0``
  1451. removes all bounding boxes.
  1452. Returns
  1453. -------
  1454. imgaug.augmentables.bbs.BoundingBoxesOnImage
  1455. Reduced set of bounding boxes, with those that had an out of image
  1456. fraction greater or equal the given one removed.
  1457. """
  1458. return self.copy().remove_out_of_image_fraction_(fraction)
  1459. @ia.deprecated(alt_func="BoundingBoxesOnImage.clip_out_of_image()",
  1460. comment="clip_out_of_image() has the exactly same "
  1461. "interface.")
  1462. def cut_out_of_image(self):
  1463. """Clip off all parts from all BBs that are outside of the image."""
  1464. return self.clip_out_of_image()
  1465. def clip_out_of_image_(self):
  1466. """
  1467. Clip off in-place all parts from all BBs that are outside of the image.
  1468. Added in 0.4.0.
  1469. Returns
  1470. -------
  1471. imgaug.augmentables.bbs.BoundingBoxesOnImage
  1472. Bounding boxes, clipped to fall within the image dimensions.
  1473. The object and its items may have been modified in-place.
  1474. """
  1475. # remove bbs that are not at least partially inside the image plane
  1476. self.bounding_boxes = [bb for bb in self.bounding_boxes
  1477. if bb.is_partly_within_image(self.shape)]
  1478. for i, bb in enumerate(self.bounding_boxes):
  1479. self.bounding_boxes[i] = bb.clip_out_of_image(self.shape)
  1480. return self
  1481. def clip_out_of_image(self):
  1482. """Clip off all parts from all BBs that are outside of the image.
  1483. Returns
  1484. -------
  1485. imgaug.augmentables.bbs.BoundingBoxesOnImage
  1486. Bounding boxes, clipped to fall within the image dimensions.
  1487. """
  1488. return self.deepcopy().clip_out_of_image_()
  1489. def shift_(self, x=0, y=0):
  1490. """Move all BBs along the x/y-axis in-place.
  1491. The origin ``(0, 0)`` is at the top left of the image.
  1492. Added in 0.4.0.
  1493. Parameters
  1494. ----------
  1495. x : number, optional
  1496. Value to be added to all x-coordinates. Positive values shift
  1497. towards the right images.
  1498. y : number, optional
  1499. Value to be added to all y-coordinates. Positive values shift
  1500. towards the bottom images.
  1501. Returns
  1502. -------
  1503. imgaug.augmentables.bbs.BoundingBoxesOnImage
  1504. Shifted bounding boxes.
  1505. The object and its items may have been modified in-place.
  1506. """
  1507. for i, bb in enumerate(self.bounding_boxes):
  1508. self.bounding_boxes[i] = bb.shift_(x=x, y=y)
  1509. return self
  1510. def shift(self, x=0, y=0, top=None, right=None, bottom=None, left=None):
  1511. """Move all BBs along the x/y-axis.
  1512. The origin ``(0, 0)`` is at the top left of the image.
  1513. Parameters
  1514. ----------
  1515. x : number, optional
  1516. Value to be added to all x-coordinates. Positive values shift
  1517. towards the right images.
  1518. y : number, optional
  1519. Value to be added to all y-coordinates. Positive values shift
  1520. towards the bottom images.
  1521. top : None or int, optional
  1522. Deprecated since 0.4.0.
  1523. Amount of pixels by which to shift all objects *from* the
  1524. top (towards the bottom).
  1525. right : None or int, optional
  1526. Deprecated since 0.4.0.
  1527. Amount of pixels by which to shift all objects *from* the
  1528. right (towads the left).
  1529. bottom : None or int, optional
  1530. Deprecated since 0.4.0.
  1531. Amount of pixels by which to shift all objects *from* the
  1532. bottom (towards the top).
  1533. left : None or int, optional
  1534. Deprecated since 0.4.0.
  1535. Amount of pixels by which to shift all objects *from* the
  1536. left (towards the right).
  1537. Returns
  1538. -------
  1539. imgaug.augmentables.bbs.BoundingBoxesOnImage
  1540. Shifted bounding boxes.
  1541. """
  1542. x, y = _normalize_shift_args(
  1543. x, y, top=top, right=right, bottom=bottom, left=left)
  1544. return self.deepcopy().shift_(x=x, y=y)
  1545. def to_keypoints_on_image(self):
  1546. """Convert the bounding boxes to one ``KeypointsOnImage`` instance.
  1547. Added in 0.4.0.
  1548. Returns
  1549. -------
  1550. imgaug.augmentables.kps.KeypointsOnImage
  1551. A keypoints instance containing ``N*4`` coordinates for ``N``
  1552. bounding boxes. Order matches the order in ``bounding_boxes``.
  1553. """
  1554. from .kps import KeypointsOnImage
  1555. # This currently uses 4 points instead of 2 points as the method
  1556. # is primarily used during augmentation and 4 points are overall
  1557. # the better choice there.
  1558. arr = np.zeros((len(self.bounding_boxes), 2*4), dtype=np.float32)
  1559. for i, box in enumerate(self.bounding_boxes):
  1560. arr[i] = [
  1561. box.x1, box.y1,
  1562. box.x2, box.y1,
  1563. box.x2, box.y2,
  1564. box.x1, box.y2
  1565. ]
  1566. return KeypointsOnImage.from_xy_array(
  1567. arr.reshape((-1, 2)),
  1568. shape=self.shape
  1569. )
  1570. def invert_to_keypoints_on_image_(self, kpsoi):
  1571. """Invert the output of ``to_keypoints_on_image()`` in-place.
  1572. This function writes in-place into this ``BoundingBoxesOnImage``
  1573. instance.
  1574. Added in 0.4.0.
  1575. Parameters
  1576. ----------
  1577. kpsoi : imgaug.augmentables.kps.KeypointsOnImages
  1578. Keypoints to convert back to bounding boxes, i.e. the outputs
  1579. of ``to_keypoints_on_image()``.
  1580. Returns
  1581. -------
  1582. BoundingBoxesOnImage
  1583. Bounding boxes container with updated coordinates.
  1584. Note that the instance is also updated in-place.
  1585. """
  1586. assert len(kpsoi.keypoints) == len(self.bounding_boxes) * 4, (
  1587. "Expected %d coordinates, got %d." % (
  1588. len(self.bounding_boxes) * 2, len(kpsoi.keypoints)))
  1589. for i, bb in enumerate(self.bounding_boxes):
  1590. xx = [kpsoi.keypoints[4*i+0].x, kpsoi.keypoints[4*i+1].x,
  1591. kpsoi.keypoints[4*i+2].x, kpsoi.keypoints[4*i+3].x]
  1592. yy = [kpsoi.keypoints[4*i+0].y, kpsoi.keypoints[4*i+1].y,
  1593. kpsoi.keypoints[4*i+2].y, kpsoi.keypoints[4*i+3].y]
  1594. bb.x1 = min(xx)
  1595. bb.y1 = min(yy)
  1596. bb.x2 = max(xx)
  1597. bb.y2 = max(yy)
  1598. self.shape = kpsoi.shape
  1599. return self
  1600. def to_polygons_on_image(self):
  1601. """Convert the bounding boxes to one ``PolygonsOnImage`` instance.
  1602. Added in 0.4.0.
  1603. Returns
  1604. -------
  1605. imgaug.augmentables.polys.PolygonsOnImage
  1606. A ``PolygonsOnImage`` containing polygons. Each polygon covers
  1607. the same area as the corresponding bounding box.
  1608. """
  1609. from .polys import PolygonsOnImage
  1610. polygons = [bb.to_polygon() for bb in self.bounding_boxes]
  1611. return PolygonsOnImage(polygons, shape=self.shape)
  1612. def copy(self, bounding_boxes=None, shape=None):
  1613. """Create a shallow copy of the ``BoundingBoxesOnImage`` instance.
  1614. Parameters
  1615. ----------
  1616. bounding_boxes : None or list of imgaug.augmntables.bbs.BoundingBox, optional
  1617. List of bounding boxes on the image.
  1618. If ``None``, the instance's bounding boxes will be copied.
  1619. shape : tuple of int, optional
  1620. The shape of the image on which the bounding boxes are placed.
  1621. If ``None``, the instance's shape will be copied.
  1622. Returns
  1623. -------
  1624. imgaug.augmentables.bbs.BoundingBoxesOnImage
  1625. Shallow copy.
  1626. """
  1627. if bounding_boxes is None:
  1628. bounding_boxes = self.bounding_boxes[:]
  1629. if shape is None:
  1630. # use tuple() here in case the shape was provided as a list
  1631. shape = tuple(self.shape)
  1632. return BoundingBoxesOnImage(bounding_boxes, shape)
  1633. def deepcopy(self, bounding_boxes=None, shape=None):
  1634. """Create a deep copy of the ``BoundingBoxesOnImage`` object.
  1635. Parameters
  1636. ----------
  1637. bounding_boxes : None or list of imgaug.augmntables.bbs.BoundingBox, optional
  1638. List of bounding boxes on the image.
  1639. If ``None``, the instance's bounding boxes will be copied.
  1640. shape : tuple of int, optional
  1641. The shape of the image on which the bounding boxes are placed.
  1642. If ``None``, the instance's shape will be copied.
  1643. Returns
  1644. -------
  1645. imgaug.augmentables.bbs.BoundingBoxesOnImage
  1646. Deep copy.
  1647. """
  1648. # Manual copy is far faster than deepcopy, so use manual copy here.
  1649. if bounding_boxes is None:
  1650. bounding_boxes = [bb.deepcopy() for bb in self.bounding_boxes]
  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 BoundingBoxesOnImage(bounding_boxes, shape)
  1655. def __getitem__(self, indices):
  1656. """Get the bounding box(es) with given indices.
  1657. Added in 0.4.0.
  1658. Returns
  1659. -------
  1660. list of imgaug.augmentables.bbs.BoundingBoxes
  1661. Bounding box(es) with given indices.
  1662. """
  1663. return self.bounding_boxes[indices]
  1664. def __iter__(self):
  1665. """Iterate over the bounding boxes in this container.
  1666. Added in 0.4.0.
  1667. Yields
  1668. ------
  1669. BoundingBox
  1670. A bounding box in this container.
  1671. The order is identical to the order in the bounding box list
  1672. provided upon class initialization.
  1673. """
  1674. return iter(self.bounding_boxes)
  1675. def __len__(self):
  1676. """Get the number of items in this instance.
  1677. Added in 0.4.0.
  1678. Returns
  1679. -------
  1680. int
  1681. Number of items in this instance.
  1682. """
  1683. return len(self.items)
  1684. def __repr__(self):
  1685. return self.__str__()
  1686. def __str__(self):
  1687. return (
  1688. "BoundingBoxesOnImage(%s, shape=%s)"
  1689. % (str(self.bounding_boxes), self.shape))
  1690. class _LabelOnImageDrawer(object):
  1691. # size refers to the thickness of the BB
  1692. # height is the height of the label rectangle, not the whole BB
  1693. def __init__(self, color=(0, 255, 0), color_text=None, color_bg=None,
  1694. size=1, alpha=1.0, raise_if_out_of_image=False,
  1695. height=30, size_text=20):
  1696. self.color = color
  1697. self.color_text = color_text
  1698. self.color_bg = color_bg
  1699. self.size = size
  1700. self.alpha = alpha
  1701. self.raise_if_out_of_image = raise_if_out_of_image
  1702. self.height = height
  1703. self.size_text = size_text
  1704. def draw_on_image_(self, image, bounding_box):
  1705. # pylint: disable=invalid-name, redefined-outer-name
  1706. if self.raise_if_out_of_image:
  1707. self._do_raise_if_out_of_image(image, bounding_box)
  1708. color_text, color_bg = self._preprocess_colors()
  1709. x1, y1, x2, y2 = self._compute_bg_corner_coords(image, bounding_box)
  1710. # cant draw anything if OOI
  1711. if x2 <= x1 or y2 <= y1:
  1712. return image
  1713. # can currently only draw on images with shape (H,W,C), not (H,W)
  1714. label_arr = self._draw_label_arr(bounding_box.label,
  1715. y2 - y1, x2 - x1, image.shape[-1],
  1716. image.dtype,
  1717. color_text, color_bg,
  1718. self.size_text)
  1719. image = self._blend_label_arr_with_image_(image, label_arr,
  1720. x1, y1, x2, y2)
  1721. return image
  1722. def draw_on_image(self, image, bounding_box):
  1723. return self.draw_on_image_(np.copy(image), bounding_box)
  1724. @classmethod
  1725. def _do_raise_if_out_of_image(cls, image, bounding_box):
  1726. if bounding_box.is_out_of_image(image):
  1727. raise Exception(
  1728. "Cannot draw bounding box x1=%.8f, y1=%.8f, x2=%.8f, y2=%.8f "
  1729. "on image with shape %s." % (
  1730. bounding_box.x1, bounding_box.y1,
  1731. bounding_box.x2, bounding_box.y2,
  1732. image.shape))
  1733. def _preprocess_colors(self):
  1734. color = np.uint8(self.color) if self.color is not None else None
  1735. color_bg = self.color_bg
  1736. if self.color_bg is not None:
  1737. color_bg = np.uint8(color_bg)
  1738. else:
  1739. assert color is not None, (
  1740. "Expected `color` to be set when `color_bg` is not set, "
  1741. "but it was None.")
  1742. color_bg = color
  1743. color_text = self.color_text
  1744. if self.color_text is not None:
  1745. color_text = np.uint8(color_text)
  1746. else:
  1747. # we follow the approach from https://stackoverflow.com/a/1855903
  1748. # here
  1749. gray = (0.299 * color_bg[0]
  1750. + 0.587 * color_bg[1]
  1751. + 0.114 * color_bg[2])
  1752. color_text = np.full((3,),
  1753. 0 if gray > 128 else 255,
  1754. dtype=np.uint8)
  1755. return color_text, color_bg
  1756. def _compute_bg_corner_coords(self, image, bounding_box):
  1757. bb = bounding_box
  1758. offset = self.size
  1759. height, width = image.shape[0:2]
  1760. y1, x1, x2 = bb.y1_int, bb.x1_int, bb.x2_int
  1761. # dont use bb.y2 here! we want the label to be above the BB
  1762. y1 = y1 - 1 - self.height
  1763. y2 = y1 + self.height
  1764. x1 = x1 - offset + 1
  1765. x2 = x2 + offset
  1766. y1, y2 = np.clip([y1, y2], 0, height-1)
  1767. x1, x2 = np.clip([x1, x2], 0, width-1)
  1768. return x1, y1, x2, y2
  1769. @classmethod
  1770. def _draw_label_arr(cls, label, height, width, nb_channels, dtype,
  1771. color_text, color_bg, size_text):
  1772. label_arr = np.zeros((height, width, nb_channels), dtype=dtype)
  1773. label_arr[...] = color_bg.reshape((1, 1, -1))
  1774. label_arr = ia.draw_text(label_arr,
  1775. x=2, y=2,
  1776. text=str(label),
  1777. color=color_text,
  1778. size=size_text)
  1779. return label_arr
  1780. def _blend_label_arr_with_image_(self, image, label_arr, x1, y1, x2, y2):
  1781. alpha = self.alpha
  1782. if alpha >= 0.99:
  1783. image[y1:y2, x1:x2, :] = label_arr
  1784. else:
  1785. input_dtype = image.dtype
  1786. foreground = label_arr.astype(np.float64)
  1787. background = image[y1:y2, x1:x2, :].astype(np.float64)
  1788. blend = (1 - alpha) * background + alpha * foreground
  1789. blend = np.clip(blend, 0, 255).astype(input_dtype)
  1790. image[y1:y2, x1:x2, :] = blend
  1791. return image