| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271 |
- """Classes representing bounding boxes."""
- from __future__ import print_function, division, absolute_import
- import copy
- import numpy as np
- import skimage.draw
- import skimage.measure
- from .. import imgaug as ia
- from .base import IAugmentable
- from .utils import (normalize_shape, project_coords,
- _remove_out_of_image_fraction_,
- _normalize_shift_args)
- # TODO functions: square(), to_aspect_ratio(), contains_point()
- class BoundingBox(object):
- """Class representing bounding boxes.
- Each bounding box is parameterized by its top left and bottom right
- corners. Both are given as x and y-coordinates. The corners are intended
- to lie inside the bounding box area. As a result, a bounding box that lies
- completely inside the image but has maximum extensions would have
- coordinates ``(0.0, 0.0)`` and ``(W - epsilon, H - epsilon)``. Note that
- coordinates are saved internally as floats.
- Parameters
- ----------
- x1 : number
- X-coordinate of the top left of the bounding box.
- y1 : number
- Y-coordinate of the top left of the bounding box.
- x2 : number
- X-coordinate of the bottom right of the bounding box.
- y2 : number
- Y-coordinate of the bottom right of the bounding box.
- label : None or str, optional
- Label of the bounding box, e.g. a string representing the class.
- """
- def __init__(self, x1, y1, x2, y2, label=None):
- """Create a new BoundingBox instance."""
- if x1 > x2:
- x2, x1 = x1, x2
- if y1 > y2:
- y2, y1 = y1, y2
- self.x1 = x1
- self.y1 = y1
- self.x2 = x2
- self.y2 = y2
- self.label = label
- @property
- def coords(self):
- """Get the top-left and bottom-right coordinates as one array.
- Added in 0.4.0.
- Returns
- -------
- ndarray
- A ``(N, 2)`` numpy array with ``N=2`` containing the top-left
- and bottom-right coordinates.
- """
- arr = np.empty((2, 2), dtype=np.float32)
- arr[0, :] = (self.x1, self.y1)
- arr[1, :] = (self.x2, self.y2)
- return arr
- @property
- def x1_int(self):
- """Get the x-coordinate of the top left corner as an integer.
- Returns
- -------
- int
- X-coordinate of the top left corner, rounded to the closest
- integer.
- """
- # use numpy's round to have consistent behaviour between python
- # versions
- return int(np.round(self.x1))
- @property
- def y1_int(self):
- """Get the y-coordinate of the top left corner as an integer.
- Returns
- -------
- int
- Y-coordinate of the top left corner, rounded to the closest
- integer.
- """
- # use numpy's round to have consistent behaviour between python
- # versions
- return int(np.round(self.y1))
- @property
- def x2_int(self):
- """Get the x-coordinate of the bottom left corner as an integer.
- Returns
- -------
- int
- X-coordinate of the bottom left corner, rounded to the closest
- integer.
- """
- # use numpy's round to have consistent behaviour between python
- # versions
- return int(np.round(self.x2))
- @property
- def y2_int(self):
- """Get the y-coordinate of the bottom left corner as an integer.
- Returns
- -------
- int
- Y-coordinate of the bottom left corner, rounded to the closest
- integer.
- """
- # use numpy's round to have consistent behaviour between python
- # versions
- return int(np.round(self.y2))
- @property
- def height(self):
- """Estimate the height of the bounding box.
- Returns
- -------
- number
- Height of the bounding box.
- """
- return self.y2 - self.y1
- @property
- def width(self):
- """Estimate the width of the bounding box.
- Returns
- -------
- number
- Width of the bounding box.
- """
- return self.x2 - self.x1
- @property
- def center_x(self):
- """Estimate the x-coordinate of the center point of the bounding box.
- Returns
- -------
- number
- X-coordinate of the center point of the bounding box.
- """
- return self.x1 + self.width/2
- @property
- def center_y(self):
- """Estimate the y-coordinate of the center point of the bounding box.
- Returns
- -------
- number
- Y-coordinate of the center point of the bounding box.
- """
- return self.y1 + self.height/2
- @property
- def area(self):
- """Estimate the area of the bounding box.
- Returns
- -------
- number
- Area of the bounding box, i.e. ``height * width``.
- """
- return self.height * self.width
- # TODO add test for tuple of number
- def contains(self, other):
- """Estimate whether the bounding box contains a given point.
- Parameters
- ----------
- other : tuple of number or imgaug.augmentables.kps.Keypoint
- Point to check for.
- Returns
- -------
- bool
- ``True`` if the point is contained in the bounding box,
- ``False`` otherwise.
- """
- if isinstance(other, tuple):
- x, y = other
- else:
- x, y = other.x, other.y
- return self.x1 <= x <= self.x2 and self.y1 <= y <= self.y2
- def project_(self, from_shape, to_shape):
- """Project the bounding box onto a differently shaped image in-place.
- E.g. if the bounding box is on its original image at
- ``x1=(10 of 100 pixels)`` and ``y1=(20 of 100 pixels)`` and is
- projected onto a new image with size ``(width=200, height=200)``,
- its new position will be ``(x1=20, y1=40)``.
- (Analogous for ``x2``/``y2``.)
- This is intended for cases where the original image is resized.
- It cannot be used for more complex changes (e.g. padding, cropping).
- Added in 0.4.0.
- Parameters
- ----------
- from_shape : tuple of int or ndarray
- Shape of the original image. (Before resize.)
- to_shape : tuple of int or ndarray
- Shape of the new image. (After resize.)
- Returns
- -------
- imgaug.augmentables.bbs.BoundingBox
- ``BoundingBox`` instance with new coordinates.
- The object may have been modified in-place.
- """
- (self.x1, self.y1), (self.x2, self.y2) = project_coords(
- [(self.x1, self.y1), (self.x2, self.y2)],
- from_shape,
- to_shape)
- return self
- # TODO add tests for ndarray inputs
- def project(self, from_shape, to_shape):
- """Project the bounding box onto a differently shaped image.
- E.g. if the bounding box is on its original image at
- ``x1=(10 of 100 pixels)`` and ``y1=(20 of 100 pixels)`` and is
- projected onto a new image with size ``(width=200, height=200)``,
- its new position will be ``(x1=20, y1=40)``.
- (Analogous for ``x2``/``y2``.)
- This is intended for cases where the original image is resized.
- It cannot be used for more complex changes (e.g. padding, cropping).
- Parameters
- ----------
- from_shape : tuple of int or ndarray
- Shape of the original image. (Before resize.)
- to_shape : tuple of int or ndarray
- Shape of the new image. (After resize.)
- Returns
- -------
- imgaug.augmentables.bbs.BoundingBox
- ``BoundingBox`` instance with new coordinates.
- """
- return self.deepcopy().project_(from_shape, to_shape)
- def extend_(self, all_sides=0, top=0, right=0, bottom=0, left=0):
- """Extend the size of the bounding box along its sides in-place.
- Added in 0.4.0.
- Parameters
- ----------
- all_sides : number, optional
- Value by which to extend the bounding box size along all
- sides.
- top : number, optional
- Value by which to extend the bounding box size along its top
- side.
- right : number, optional
- Value by which to extend the bounding box size along its right
- side.
- bottom : number, optional
- Value by which to extend the bounding box size along its bottom
- side.
- left : number, optional
- Value by which to extend the bounding box size along its left
- side.
- Returns
- -------
- imgaug.BoundingBox
- Extended bounding box.
- The object may have been modified in-place.
- """
- self.x1 = self.x1 - all_sides - left
- self.x2 = self.x2 + all_sides + right
- self.y1 = self.y1 - all_sides - top
- self.y2 = self.y2 + all_sides + bottom
- return self
- def extend(self, all_sides=0, top=0, right=0, bottom=0, left=0):
- """Extend the size of the bounding box along its sides.
- Parameters
- ----------
- all_sides : number, optional
- Value by which to extend the bounding box size along all
- sides.
- top : number, optional
- Value by which to extend the bounding box size along its top
- side.
- right : number, optional
- Value by which to extend the bounding box size along its right
- side.
- bottom : number, optional
- Value by which to extend the bounding box size along its bottom
- side.
- left : number, optional
- Value by which to extend the bounding box size along its left
- side.
- Returns
- -------
- imgaug.BoundingBox
- Extended bounding box.
- """
- return self.deepcopy().extend_(all_sides, top, right, bottom, left)
- def intersection(self, other, default=None):
- """Compute the intersection BB between this BB and another BB.
- Note that in extreme cases, the intersection can be a single point.
- In that case the intersection bounding box exists and it will be
- returned, but it will have a height and width of zero.
- Parameters
- ----------
- other : imgaug.augmentables.bbs.BoundingBox
- Other bounding box with which to generate the intersection.
- default : any, optional
- Default value to return if there is no intersection.
- Returns
- -------
- imgaug.augmentables.bbs.BoundingBox or any
- Intersection bounding box of the two bounding boxes if there is
- an intersection.
- If there is no intersection, the default value will be returned,
- which can by anything.
- """
- x1_i = max(self.x1, other.x1)
- y1_i = max(self.y1, other.y1)
- x2_i = min(self.x2, other.x2)
- y2_i = min(self.y2, other.y2)
- if x1_i > x2_i or y1_i > y2_i:
- return default
- return BoundingBox(x1=x1_i, y1=y1_i, x2=x2_i, y2=y2_i)
- def union(self, other):
- """Compute the union BB between this BB and another BB.
- This is equivalent to drawing a bounding box around all corner points
- of both bounding boxes.
- Parameters
- ----------
- other : imgaug.augmentables.bbs.BoundingBox
- Other bounding box with which to generate the union.
- Returns
- -------
- imgaug.augmentables.bbs.BoundingBox
- Union bounding box of the two bounding boxes.
- """
- return BoundingBox(
- x1=min(self.x1, other.x1),
- y1=min(self.y1, other.y1),
- x2=max(self.x2, other.x2),
- y2=max(self.y2, other.y2),
- )
- def iou(self, other):
- """Compute the IoU between this bounding box and another one.
- IoU is the intersection over union, defined as::
- ``area(intersection(A, B)) / area(union(A, B))``
- ``= area(intersection(A, B))
- / (area(A) + area(B) - area(intersection(A, B)))``
- Parameters
- ----------
- other : imgaug.augmentables.bbs.BoundingBox
- Other bounding box with which to compare.
- Returns
- -------
- float
- IoU between the two bounding boxes.
- """
- inters = self.intersection(other)
- if inters is None:
- return 0.0
- area_union = self.area + other.area - inters.area
- return inters.area / area_union if area_union > 0 else 0.0
- def compute_out_of_image_area(self, image):
- """Compute the area of the BB that is outside of the image plane.
- Added in 0.4.0.
- Parameters
- ----------
- image : (H,W,...) ndarray or tuple of int
- Image dimensions to use.
- If an ``ndarray``, its shape will be used.
- If a ``tuple``, it is assumed to represent the image shape
- and must contain at least two integers.
- Returns
- -------
- float
- Total area of the bounding box that is outside of the image plane.
- Can be ``0.0``.
- """
- shape = normalize_shape(image)
- height, width = shape[0:2]
- bb_image = BoundingBox(x1=0, y1=0, x2=width, y2=height)
- inter = self.intersection(bb_image, default=None)
- area = self.area
- return area if inter is None else area - inter.area
- def compute_out_of_image_fraction(self, image):
- """Compute fraction of BB area outside of the image plane.
- This estimates ``f = A_ooi / A``, where ``A_ooi`` is the area of the
- bounding box that is outside of the image plane, while ``A`` is the
- total area of the bounding box.
- Added in 0.4.0.
- Parameters
- ----------
- image : (H,W,...) ndarray or tuple of int
- Image dimensions to use.
- If an ``ndarray``, its shape will be used.
- If a ``tuple``, it is assumed to represent the image shape
- and must contain at least two integers.
- Returns
- -------
- float
- Fraction of the bounding box area that is outside of the image
- plane. Returns ``0.0`` if the bounding box is fully inside of
- the image plane. If the bounding box has an area of zero, the
- result is ``1.0`` if its coordinates are outside of the image
- plane, otherwise ``0.0``.
- """
- area = self.area
- if area == 0:
- shape = normalize_shape(image)
- height, width = shape[0:2]
- y1_outside = self.y1 < 0 or self.y1 >= height
- x1_outside = self.x1 < 0 or self.x1 >= width
- is_outside = (y1_outside or x1_outside)
- return 1.0 if is_outside else 0.0
- return self.compute_out_of_image_area(image) / area
- def is_fully_within_image(self, image):
- """Estimate whether the bounding box is fully inside the image area.
- Parameters
- ----------
- image : (H,W,...) ndarray or tuple of int
- Image dimensions to use.
- If an ``ndarray``, its shape will be used.
- If a ``tuple``, it is assumed to represent the image shape
- and must contain at least two integers.
- Returns
- -------
- bool
- ``True`` if the bounding box is fully inside the image area.
- ``False`` otherwise.
- """
- shape = normalize_shape(image)
- height, width = shape[0:2]
- return (
- self.x1 >= 0
- and self.x2 < width
- and self.y1 >= 0
- and self.y2 < height)
- def is_partly_within_image(self, image):
- """Estimate whether the BB is at least partially inside the image area.
- Parameters
- ----------
- image : (H,W,...) ndarray or tuple of int
- Image dimensions to use.
- If an ``ndarray``, its shape will be used.
- If a ``tuple``, it is assumed to represent the image shape
- and must contain at least two integers.
- Returns
- -------
- bool
- ``True`` if the bounding box is at least partially inside the
- image area.
- ``False`` otherwise.
- """
- shape = normalize_shape(image)
- height, width = shape[0:2]
- eps = np.finfo(np.float32).eps
- img_bb = BoundingBox(x1=0, x2=width-eps, y1=0, y2=height-eps)
- return self.intersection(img_bb) is not None
- def is_out_of_image(self, image, fully=True, partly=False):
- """Estimate whether the BB is partially/fully outside of the image area.
- Parameters
- ----------
- image : (H,W,...) ndarray or tuple of int
- Image dimensions to use.
- If an ``ndarray``, its shape will be used.
- If a ``tuple``, it is assumed to represent the image shape and
- must contain at least two integers.
- fully : bool, optional
- Whether to return ``True`` if the bounding box is fully outside
- of the image area.
- partly : bool, optional
- Whether to return ``True`` if the bounding box is at least
- partially outside fo the image area.
- Returns
- -------
- bool
- ``True`` if the bounding box is partially/fully outside of the
- image area, depending on defined parameters.
- ``False`` otherwise.
- """
- if self.is_fully_within_image(image):
- return False
- if self.is_partly_within_image(image):
- return partly
- return fully
- @ia.deprecated(alt_func="BoundingBox.clip_out_of_image()",
- comment="clip_out_of_image() has the exactly same "
- "interface.")
- def cut_out_of_image(self, *args, **kwargs):
- """Clip off all parts of the BB box that are outside of the image."""
- return self.clip_out_of_image(*args, **kwargs)
- def clip_out_of_image_(self, image):
- """Clip off parts of the BB box that are outside of the image in-place.
- Added in 0.4.0.
- Parameters
- ----------
- image : (H,W,...) ndarray or tuple of int
- Image dimensions to use for the clipping of the bounding box.
- If an ``ndarray``, its shape will be used.
- If a ``tuple``, it is assumed to represent the image shape and
- must contain at least two integers.
- Returns
- -------
- imgaug.augmentables.bbs.BoundingBox
- Bounding box, clipped to fall within the image dimensions.
- The object may have been modified in-place.
- """
- shape = normalize_shape(image)
- height, width = shape[0:2]
- assert height > 0, (
- "Expected image with height>0, got shape %s." % (image.shape,))
- assert width > 0, (
- "Expected image with width>0, got shape %s." % (image.shape,))
- eps = np.finfo(np.float32).eps
- self.x1 = np.clip(self.x1, 0, width - eps)
- self.x2 = np.clip(self.x2, 0, width - eps)
- self.y1 = np.clip(self.y1, 0, height - eps)
- self.y2 = np.clip(self.y2, 0, height - eps)
- return self
- def clip_out_of_image(self, image):
- """Clip off all parts of the BB box that are outside of the image.
- Parameters
- ----------
- image : (H,W,...) ndarray or tuple of int
- Image dimensions to use for the clipping of the bounding box.
- If an ``ndarray``, its shape will be used.
- If a ``tuple``, it is assumed to represent the image shape and
- must contain at least two integers.
- Returns
- -------
- imgaug.augmentables.bbs.BoundingBox
- Bounding box, clipped to fall within the image dimensions.
- """
- return self.deepcopy().clip_out_of_image_(image)
- def shift_(self, x=0, y=0):
- """Move this bounding box along the x/y-axis in-place.
- The origin ``(0, 0)`` is at the top left of the image.
- Added in 0.4.0.
- Parameters
- ----------
- x : number, optional
- Value to be added to all x-coordinates. Positive values shift
- towards the right images.
- y : number, optional
- Value to be added to all y-coordinates. Positive values shift
- towards the bottom images.
- Returns
- -------
- imgaug.augmentables.bbs.BoundingBox
- Shifted bounding box.
- The object may have been modified in-place.
- """
- self.x1 += x
- self.x2 += x
- self.y1 += y
- self.y2 += y
- return self
- def shift(self, x=0, y=0, top=None, right=None, bottom=None, left=None):
- """Move this bounding box along the x/y-axis.
- The origin ``(0, 0)`` is at the top left of the image.
- Parameters
- ----------
- x : number, optional
- Value to be added to all x-coordinates. Positive values shift
- towards the right images.
- y : number, optional
- Value to be added to all y-coordinates. Positive values shift
- towards the bottom images.
- top : None or int, optional
- Deprecated since 0.4.0.
- Amount of pixels by which to shift this object *from* the
- top (towards the bottom).
- right : None or int, optional
- Deprecated since 0.4.0.
- Amount of pixels by which to shift this object *from* the
- right (towards the left).
- bottom : None or int, optional
- Deprecated since 0.4.0.
- Amount of pixels by which to shift this object *from* the
- bottom (towards the top).
- left : None or int, optional
- Deprecated since 0.4.0.
- Amount of pixels by which to shift this object *from* the
- left (towards the right).
- Returns
- -------
- imgaug.augmentables.bbs.BoundingBox
- Shifted bounding box.
- """
- # pylint: disable=redefined-outer-name
- x, y = _normalize_shift_args(
- x, y, top=top, right=right, bottom=bottom, left=left)
- return self.deepcopy().shift_(x, y)
- def draw_label_on_image(self, image, color=(0, 255, 0),
- color_text=None, color_bg=None, alpha=1.0, size=1,
- size_text=20, height=30,
- copy=True, raise_if_out_of_image=False):
- """Draw a box showing the BB's label.
- The box is placed right above the BB's rectangle.
- Added in 0.4.0.
- Parameters
- ----------
- image : (H,W,C) ndarray
- The image onto which to draw the label.
- Currently expected to be ``uint8``.
- color : None or iterable of int, optional
- The color to use, corresponding to the channel layout of the
- image. Usually RGB. Text and background colors will be derived
- from this.
- color_text : None or iterable of int, optional
- The text color to use.
- If ``None``, derived from `color_bg`.
- color_bg : None or iterable of int, optional
- The background color of the label box.
- If ``None``, derived from `color`.
- alpha : float, optional
- The transparency of the drawn bounding box, where ``1.0`` denotes
- no transparency and ``0.0`` is invisible.
- size : int, optional
- The thickness of the bounding box in pixels. If the value is
- larger than ``1``, then additional pixels will be added around
- the bounding box (i.e. extension towards the outside).
- size_text : int, optional
- Font size to use.
- height : int, optional
- Height of the label box in pixels.
- copy : bool, optional
- Whether to copy the input image or change it in-place.
- raise_if_out_of_image : bool, optional
- Whether to raise an error if the bounding box is fully outside of
- the image. If set to ``False``, no error will be raised and only
- the parts inside the image will be drawn.
- Returns
- -------
- (H,W,C) ndarray(uint8)
- Image with bounding box drawn on it.
- """
- # pylint: disable=redefined-outer-name
- drawer = _LabelOnImageDrawer(
- color=color,
- color_text=color_text,
- color_bg=color_bg,
- size=size,
- alpha=alpha,
- raise_if_out_of_image=raise_if_out_of_image,
- height=height,
- size_text=size_text)
- if copy:
- return drawer.draw_on_image(image, self)
- return drawer.draw_on_image_(image, self)
- def draw_box_on_image(self, image, color=(0, 255, 0), alpha=1.0,
- size=1, copy=True, raise_if_out_of_image=False,
- thickness=None):
- """Draw the rectangle of the bounding box on an image.
- This method does not draw the label.
- Added in 0.4.0.
- Parameters
- ----------
- image : (H,W,C) ndarray
- The image onto which to draw the bounding box rectangle.
- Currently expected to be ``uint8``.
- color : iterable of int, optional
- The color to use, corresponding to the channel layout of the
- image. Usually RGB.
- alpha : float, optional
- The transparency of the drawn bounding box, where ``1.0`` denotes
- no transparency and ``0.0`` is invisible.
- size : int, optional
- The thickness of the bounding box in pixels. If the value is
- larger than ``1``, then additional pixels will be added around
- the bounding box (i.e. extension towards the outside).
- copy : bool, optional
- Whether to copy the input image or change it in-place.
- raise_if_out_of_image : bool, optional
- Whether to raise an error if the bounding box is fully outside of
- the image. If set to ``False``, no error will be raised and only
- the parts inside the image will be drawn.
- thickness : None or int, optional
- Deprecated.
- Returns
- -------
- (H,W,C) ndarray(uint8)
- Image with bounding box drawn on it.
- """
- # pylint: disable=invalid-name, redefined-outer-name
- if thickness is not None:
- ia.warn_deprecated(
- "Usage of argument 'thickness' in BoundingBox.draw_on_image() "
- "is deprecated. The argument was renamed to 'size'.")
- size = thickness
- if raise_if_out_of_image and self.is_out_of_image(image):
- raise Exception(
- "Cannot draw bounding box x1=%.8f, y1=%.8f, x2=%.8f, y2=%.8f "
- "on image with shape %s." % (
- self.x1, self.y1, self.x2, self.y2, image.shape))
- result = np.copy(image) if copy else image
- if isinstance(color, (tuple, list)):
- color = np.uint8(color)
- for i in range(size):
- y1, y2, x1, x2 = self.y1_int, self.y2_int, self.x1_int, self.x2_int
- # When y values get into the range (H-0.5, H), the *_int functions
- # round them to H. That is technically sensible, but in the case
- # of drawing means that the border lies just barely outside of
- # the image, making the border disappear, even though the BB is
- # fully inside the image. Here we correct for that because of
- # beauty reasons. Same is the case for x coordinates.
- if self.is_fully_within_image(image):
- y1 = np.clip(y1, 0, image.shape[0]-1)
- y2 = np.clip(y2, 0, image.shape[0]-1)
- x1 = np.clip(x1, 0, image.shape[1]-1)
- x2 = np.clip(x2, 0, image.shape[1]-1)
- y = [y1-i, y1-i, y2+i, y2+i]
- x = [x1-i, x2+i, x2+i, x1-i]
- rr, cc = skimage.draw.polygon_perimeter(y, x, shape=result.shape)
- if alpha >= 0.99:
- result[rr, cc, :] = color
- else:
- if ia.is_float_array(result):
- # TODO use blend_alpha here
- result[rr, cc, :] = (
- (1 - alpha) * result[rr, cc, :]
- + alpha * color)
- result = np.clip(result, 0, 255)
- else:
- input_dtype = result.dtype
- result = result.astype(np.float32)
- result[rr, cc, :] = (
- (1 - alpha) * result[rr, cc, :]
- + alpha * color)
- result = np.clip(result, 0, 255).astype(input_dtype)
- return result
- # TODO add explicit test for zero-sized BBs (worked when tested by hand)
- def draw_on_image(self, image, color=(0, 255, 0), alpha=1.0, size=1,
- copy=True, raise_if_out_of_image=False, thickness=None):
- """Draw the bounding box on an image.
- This will automatically also draw the label, unless it is ``None``.
- To only draw the box rectangle use
- :func:`~imgaug.augmentables.bbs.BoundingBox.draw_box_on_image`.
- To draw the label even if it is ``None`` or to configure e.g. its
- color, use
- :func:`~imgaug.augmentables.bbs.BoundingBox.draw_label_on_image`.
- Parameters
- ----------
- image : (H,W,C) ndarray
- The image onto which to draw the bounding box.
- Currently expected to be ``uint8``.
- color : iterable of int, optional
- The color to use, corresponding to the channel layout of the
- image. Usually RGB.
- alpha : float, optional
- The transparency of the drawn bounding box, where ``1.0`` denotes
- no transparency and ``0.0`` is invisible.
- size : int, optional
- The thickness of the bounding box in pixels. If the value is
- larger than ``1``, then additional pixels will be added around
- the bounding box (i.e. extension towards the outside).
- copy : bool, optional
- Whether to copy the input image or change it in-place.
- raise_if_out_of_image : bool, optional
- Whether to raise an error if the bounding box is fully outside of
- the image. If set to ``False``, no error will be raised and only
- the parts inside the image will be drawn.
- thickness : None or int, optional
- Deprecated.
- Returns
- -------
- (H,W,C) ndarray(uint8)
- Image with bounding box drawn on it.
- """
- # pylint: disable=redefined-outer-name
- image_drawn = self.draw_box_on_image(
- image, color=color, alpha=alpha, size=size,
- copy=copy, raise_if_out_of_image=raise_if_out_of_image,
- thickness=thickness
- )
- if self.label is not None:
- image_drawn = self.draw_label_on_image(
- image_drawn, color=color, alpha=alpha,
- size=size if thickness is None else thickness,
- copy=False, raise_if_out_of_image=raise_if_out_of_image
- )
- return image_drawn
- # TODO add tests for pad and pad_max
- def extract_from_image(self, image, pad=True, pad_max=None,
- prevent_zero_size=True):
- """Extract the image pixels within the bounding box.
- This function will zero-pad the image if the bounding box is
- partially/fully outside of the image.
- Parameters
- ----------
- image : (H,W) ndarray or (H,W,C) ndarray
- The image from which to extract the pixels within the bounding box.
- pad : bool, optional
- Whether to zero-pad the image if the object is partially/fully
- outside of it.
- pad_max : None or int, optional
- The maximum number of pixels that may be zero-paded on any side,
- i.e. if this has value ``N`` the total maximum of added pixels
- is ``4*N``.
- This option exists to prevent extremely large images as a result of
- single points being moved very far away during augmentation.
- prevent_zero_size : bool, optional
- Whether to prevent the height or width of the extracted image from
- becoming zero.
- If this is set to ``True`` and the height or width of the bounding
- box is below ``1``, the height/width will be increased to ``1``.
- This can be useful to prevent problems, e.g. with image saving or
- plotting.
- If it is set to ``False``, images will be returned as ``(H', W')``
- or ``(H', W', 3)`` with ``H`` or ``W`` potentially being 0.
- Returns
- -------
- (H',W') ndarray or (H',W',C) ndarray
- Pixels within the bounding box. Zero-padded if the bounding box
- is partially/fully outside of the image.
- If `prevent_zero_size` is activated, it is guarantueed that
- ``H'>0`` and ``W'>0``, otherwise only ``H'>=0`` and ``W'>=0``.
- """
- # pylint: disable=no-else-return, too-many-statements
- height, width = image.shape[0], image.shape[1]
- x1, x2, y1, y2 = self.x1_int, self.x2_int, self.y1_int, self.y2_int
- # When y values get into the range (H-0.5, H), the *_int functions
- # round them to H. That is technically sensible, but in the case of
- # extraction leads to a black border, which is both ugly and
- # unexpected after calling cut_out_of_image(). Here we correct for
- # that because of beauty reasons. Same is the case for x coordinates.
- fully_within = self.is_fully_within_image(image)
- if fully_within:
- y1, y2 = np.clip([y1, y2], 0, height-1)
- x1, x2 = np.clip([x1, x2], 0, width-1)
- # TODO add test
- if prevent_zero_size:
- if abs(x2 - x1) < 1:
- x2 = x1 + 1
- if abs(y2 - y1) < 1:
- y2 = y1 + 1
- if pad:
- # if the bb is outside of the image area, the following pads the
- # image first with black pixels until the bb is inside the image
- # and only then extracts the image area
- # TODO probably more efficient to initialize an array of zeros
- # and copy only the portions of the bb into that array that
- # are natively inside the image area
- from ..augmenters import size as iasize
- pad_top = 0
- pad_right = 0
- pad_bottom = 0
- pad_left = 0
- if x1 < 0:
- pad_left = abs(x1)
- x2 = x2 + pad_left
- width = width + pad_left
- x1 = 0
- if y1 < 0:
- pad_top = abs(y1)
- y2 = y2 + pad_top
- height = height + pad_top
- y1 = 0
- if x2 >= width:
- pad_right = x2 - width
- if y2 >= height:
- pad_bottom = y2 - height
- paddings = [pad_top, pad_right, pad_bottom, pad_left]
- any_padded = any([val > 0 for val in paddings])
- if any_padded:
- if pad_max is None:
- pad_max = max(paddings)
- image = iasize.pad(
- image,
- top=min(pad_top, pad_max),
- right=min(pad_right, pad_max),
- bottom=min(pad_bottom, pad_max),
- left=min(pad_left, pad_max)
- )
- return image[y1:y2, x1:x2]
- else:
- within_image = (
- (0, 0, 0, 0)
- <= (x1, y1, x2, y2)
- < (width, height, width, height)
- )
- out_height, out_width = (y2 - y1), (x2 - x1)
- nonzero_height = (out_height > 0)
- nonzero_width = (out_width > 0)
- if within_image and nonzero_height and nonzero_width:
- return image[y1:y2, x1:x2]
- if prevent_zero_size:
- out_height = 1
- out_width = 1
- else:
- out_height = 0
- out_width = 0
- if image.ndim == 2:
- return np.zeros((out_height, out_width), dtype=image.dtype)
- return np.zeros((out_height, out_width, image.shape[-1]),
- dtype=image.dtype)
- # TODO also add to_heatmap
- # TODO add this to BoundingBoxesOnImage
- # TODO add label to keypoints?
- def to_keypoints(self):
- """Convert the BB's corners to keypoints (clockwise, from top left).
- Returns
- -------
- list of imgaug.augmentables.kps.Keypoint
- Corners of the bounding box as keypoints.
- """
- # TODO get rid of this deferred import
- from imgaug.augmentables.kps import Keypoint
- return [
- Keypoint(x=self.x1, y=self.y1),
- Keypoint(x=self.x2, y=self.y1),
- Keypoint(x=self.x2, y=self.y2),
- Keypoint(x=self.x1, y=self.y2)
- ]
- def to_polygon(self):
- """Convert this bounding box to a polygon covering the same area.
- Added in 0.4.0.
- Returns
- -------
- imgaug.augmentables.polys.Polygon
- The bounding box converted to a polygon.
- """
- # TODO get rid of this deferred import
- from imgaug.augmentables.polys import Polygon
- return Polygon([
- (self.x1, self.y1),
- (self.x2, self.y1),
- (self.x2, self.y2),
- (self.x1, self.y2)
- ], label=self.label)
- # TODO also introduce similar area_almost_equals()
- def coords_almost_equals(self, other, max_distance=1e-4):
- """Estimate if this and another BB have almost identical coordinates.
- Added in 0.4.0.
- Parameters
- ----------
- other : imgaug.augmentables.bbs.BoundingBox or iterable
- The other bounding box with which to compare this one.
- If this is an ``iterable``, it is assumed to represent the top-left
- and bottom-right coordinates of that bounding box, given as e.g.
- an ``(2,2)`` ndarray or an ``(4,)`` ndarray or as a similar list.
- max_distance : number, optional
- The maximum euclidean distance between a corner on one bounding
- box and the closest corner on the other bounding box. If the
- distance is exceeded for any such pair, the two BBs are not
- viewed as equal.
- Returns
- -------
- bool
- Whether the two bounding boxes have almost identical corner
- coordinates.
- """
- if isinstance(other, BoundingBox):
- coords_b = other.coords.flat
- elif ia.is_np_array(other):
- # we use flat here in case other is (N,2) instead of (4,)
- coords_b = other.flat
- elif ia.is_iterable(other):
- coords_b = list(ia.flatten(other))
- else:
- raise ValueError(
- "Expected 'other' to be an iterable containing two "
- "(x,y)-coordinate pairs or a BoundingBox. "
- "Got type %s." % (type(other),))
- coords_a = self.coords
- return np.allclose(coords_a.flat, coords_b, atol=max_distance, rtol=0)
- def almost_equals(self, other, max_distance=1e-4):
- """Compare this and another BB's label and coordinates.
- This is the same as
- :func:`~imgaug.augmentables.bbs.BoundingBox.coords_almost_equals` but
- additionally compares the labels.
- Added in 0.4.0.
- Parameters
- ----------
- other : imgaug.augmentables.bbs.BoundingBox or iterable
- The other object to compare against. Expected to be a
- ``BoundingBox``.
- max_distance : number, optional
- See
- :func:`~imgaug.augmentables.bbs.BoundingBox.coords_almost_equals`.
- Returns
- -------
- bool
- ``True`` if the coordinates are almost equal and additionally
- the labels are equal. Otherwise ``False``.
- """
- if self.label != other.label:
- return False
- return self.coords_almost_equals(other, max_distance=max_distance)
- @classmethod
- def from_point_soup(cls, xy):
- """Convert a ``(2P,) or (P,2) ndarray`` to a BB instance.
- This is the inverse of
- :func:`~imgaug.BoundingBoxesOnImage.to_xyxy_array`.
- Added in 0.4.0.
- Parameters
- ----------
- xy : (2P,) ndarray or (P, 2) array or iterable of number or iterable of iterable of number
- Array containing ``P`` points in xy-form denoting a soup of
- points around which to place a bounding box.
- The array should usually be of dtype ``float32``.
- Returns
- -------
- imgaug.augmentables.bbs.BoundingBox
- Bounding box around the points.
- """
- # pylint: disable=unsubscriptable-object
- xy = np.array(xy, dtype=np.float32)
- assert len(xy) > 0, (
- "Expected to get at least one point to place a bounding box "
- "around, got shape %s." % (xy.shape,))
- assert xy.ndim == 1 or (xy.ndim == 2 and xy.shape[-1] == 2), (
- "Expected input array of shape (P,) or (P, 2), "
- "got shape %s." % (xy.shape,))
- if xy.ndim == 1:
- xy = xy.reshape((-1, 2))
- x1, y1 = np.min(xy, axis=0)
- x2, y2 = np.max(xy, axis=0)
- return cls(x1=x1, y1=y1, x2=x2, y2=y2)
- def copy(self, x1=None, y1=None, x2=None, y2=None, label=None):
- """Create a shallow copy of this BoundingBox instance.
- Parameters
- ----------
- x1 : None or number
- If not ``None``, then the ``x1`` coordinate of the copied object
- will be set to this value.
- y1 : None or number
- If not ``None``, then the ``y1`` coordinate of the copied object
- will be set to this value.
- x2 : None or number
- If not ``None``, then the ``x2`` coordinate of the copied object
- will be set to this value.
- y2 : None or number
- If not ``None``, then the ``y2`` coordinate of the copied object
- will be set to this value.
- label : None or string
- If not ``None``, then the ``label`` of the copied object
- will be set to this value.
- Returns
- -------
- imgaug.augmentables.bbs.BoundingBox
- Shallow copy.
- """
- return BoundingBox(
- x1=self.x1 if x1 is None else x1,
- x2=self.x2 if x2 is None else x2,
- y1=self.y1 if y1 is None else y1,
- y2=self.y2 if y2 is None else y2,
- label=copy.deepcopy(self.label) if label is None else label
- )
- def deepcopy(self, x1=None, y1=None, x2=None, y2=None, label=None):
- """
- Create a deep copy of the BoundingBox object.
- Parameters
- ----------
- x1 : None or number
- If not ``None``, then the ``x1`` coordinate of the copied object
- will be set to this value.
- y1 : None or number
- If not ``None``, then the ``y1`` coordinate of the copied object
- will be set to this value.
- x2 : None or number
- If not ``None``, then the ``x2`` coordinate of the copied object
- will be set to this value.
- y2 : None or number
- If not ``None``, then the ``y2`` coordinate of the copied object
- will be set to this value.
- label : None or string
- If not ``None``, then the ``label`` of the copied object
- will be set to this value.
- Returns
- -------
- imgaug.augmentables.bbs.BoundingBox
- Deep copy.
- """
- # TODO write specific copy routine with deepcopy for label and remove
- # the deepcopy from copy()
- return self.copy(x1=x1, y1=y1, x2=x2, y2=y2, label=label)
- def __getitem__(self, indices):
- """Get the coordinate(s) with given indices.
- Added in 0.4.0.
- Returns
- -------
- ndarray
- xy-coordinate(s) as ``ndarray``.
- """
- return self.coords[indices]
- def __iter__(self):
- """Iterate over the coordinates of this instance.
- Added in 0.4.0.
- Yields
- ------
- ndarray
- An ``(2,)`` ``ndarray`` denoting an xy-coordinate pair.
- """
- return iter(self.coords)
- def __repr__(self):
- return self.__str__()
- def __str__(self):
- return "BoundingBox(x1=%.4f, y1=%.4f, x2=%.4f, y2=%.4f, label=%s)" % (
- self.x1, self.y1, self.x2, self.y2, self.label)
- class BoundingBoxesOnImage(IAugmentable):
- """Container for the list of all bounding boxes on a single image.
- Parameters
- ----------
- bounding_boxes : list of imgaug.augmentables.bbs.BoundingBox
- List of bounding boxes on the image.
- shape : tuple of int or ndarray
- The shape of the image on which the objects are placed.
- Either an image with shape ``(H,W,[C])`` or a ``tuple`` denoting
- such an image shape.
- Examples
- --------
- >>> import numpy as np
- >>> from imgaug.augmentables.bbs import BoundingBox, BoundingBoxesOnImage
- >>>
- >>> image = np.zeros((100, 100))
- >>> bbs = [
- >>> BoundingBox(x1=10, y1=20, x2=20, y2=30),
- >>> BoundingBox(x1=25, y1=50, x2=30, y2=70)
- >>> ]
- >>> bbs_oi = BoundingBoxesOnImage(bbs, shape=image.shape)
- """
- def __init__(self, bounding_boxes, shape):
- self.bounding_boxes = bounding_boxes
- self.shape = normalize_shape(shape)
- @property
- def items(self):
- """Get the bounding boxes in this container.
- Added in 0.4.0.
- Returns
- -------
- list of BoundingBox
- Bounding boxes within this container.
- """
- return self.bounding_boxes
- @items.setter
- def items(self, value):
- """Set the bounding boxes in this container.
- Added in 0.4.0.
- Parameters
- ----------
- value : list of BoundingBox
- Bounding boxes within this container.
- """
- self.bounding_boxes = value
- # TODO remove this? here it is image height, but in BoundingBox it is
- # bounding box height
- @property
- def height(self):
- """Get the height of the image on which the bounding boxes fall.
- Returns
- -------
- int
- Image height.
- """
- return self.shape[0]
- # TODO remove this? here it is image width, but in BoundingBox it is
- # bounding box width
- @property
- def width(self):
- """Get the width of the image on which the bounding boxes fall.
- Returns
- -------
- int
- Image width.
- """
- return self.shape[1]
- @property
- def empty(self):
- """Determine whether this instance contains zero bounding boxes.
- Returns
- -------
- bool
- True if this object contains zero bounding boxes.
- """
- return len(self.bounding_boxes) == 0
- def on_(self, image):
- """Project BBs from one image (shape) to a another one in-place.
- Added in 0.4.0.
- Parameters
- ----------
- image : ndarray or tuple of int
- New image onto which the bounding boxes are to be projected.
- May also simply be that new image's shape tuple.
- Returns
- -------
- imgaug.augmentables.bbs.BoundingBoxesOnImage
- Object containing the same bounding boxes after projection to
- the new image shape.
- The object and its items may have been modified in-place.
- """
- # pylint: disable=invalid-name
- on_shape = normalize_shape(image)
- if on_shape[0:2] == self.shape[0:2]:
- self.shape = on_shape # channels may differ
- return self
- for i, item in enumerate(self.items):
- self.bounding_boxes[i] = item.project_(self.shape, on_shape)
- self.shape = on_shape
- return self
- def on(self, image):
- """Project bounding boxes from one image (shape) to a another one.
- Parameters
- ----------
- image : ndarray or tuple of int
- New image onto which the bounding boxes are to be projected.
- May also simply be that new image's shape tuple.
- Returns
- -------
- imgaug.augmentables.bbs.BoundingBoxesOnImage
- Object containing the same bounding boxes after projection to
- the new image shape.
- """
- # pylint: disable=invalid-name
- return self.deepcopy().on_(image)
- @classmethod
- def from_xyxy_array(cls, xyxy, shape):
- """Convert an ``(N, 4) or (N, 2, 2) ndarray`` to a BBsOI instance.
- This is the inverse of
- :func:`~imgaug.BoundingBoxesOnImage.to_xyxy_array`.
- Parameters
- ----------
- xyxy : (N, 4) ndarray or (N, 2, 2) array
- Array containing the corner coordinates of ``N`` bounding boxes.
- Each bounding box is represented by its top-left and bottom-right
- coordinates.
- The array should usually be of dtype ``float32``.
- shape : tuple of int
- Shape of the image on which the bounding boxes are placed.
- Should usually be ``(H, W, C)`` or ``(H, W)``.
- Returns
- -------
- imgaug.augmentables.bbs.BoundingBoxesOnImage
- Object containing a list of :class:`BoundingBox` instances
- derived from the provided corner coordinates.
- """
- # pylint: disable=unsubscriptable-object
- xyxy = np.array(xyxy, dtype=np.float32)
- # note that np.array([]) is (0,), not (0, 2)
- if xyxy.shape[0] == 0:
- return BoundingBoxesOnImage([], shape)
- assert (
- (xyxy.ndim == 2 and xyxy.shape[-1] == 4)
- or (xyxy.ndim == 3 and xyxy.shape[1:3] == (2, 2))), (
- "Expected input array of shape (N, 4) or (N, 2, 2), "
- "got shape %s." % (xyxy.shape,))
- xyxy = xyxy.reshape((-1, 2, 2))
- boxes = [BoundingBox.from_point_soup(row) for row in xyxy]
- return cls(boxes, shape)
- @classmethod
- def from_point_soups(cls, xy, shape):
- """Convert an ``(N, 2P) or (N, P, 2) ndarray`` to a BBsOI instance.
- Added in 0.4.0.
- Parameters
- ----------
- xy : (N, 2P) ndarray or (N, P, 2) array or iterable of iterable of number or iterable of iterable of iterable of number
- Array containing the corner coordinates of ``N`` bounding boxes.
- Each bounding box is represented by a soup of ``P`` points.
- If ``(N, P)`` then the second axis is expected to be in
- xy-form (e.g. ``x1``, ``y1``, ``x2``, ``y2``, ...).
- The final bounding box coordinates will be derived using ``min``
- and ``max`` operations on the xy-values.
- The array should usually be of dtype ``float32``.
- shape : tuple of int
- Shape of the image on which the bounding boxes are placed.
- Should usually be ``(H, W, C)`` or ``(H, W)``.
- Returns
- -------
- imgaug.augmentables.bbs.BoundingBoxesOnImage
- Object containing a list of :class:`BoundingBox` instances
- derived from the provided point soups.
- """
- xy = np.array(xy, dtype=np.float32)
- # from_xy_array() already checks the ndim/shape, so we don't have to
- # do it here
- boxes = [BoundingBox.from_point_soup(row) for row in xy]
- return cls(boxes, shape)
- def to_xyxy_array(self, dtype=np.float32):
- """Convert the ``BoundingBoxesOnImage`` object to an ``(N,4) ndarray``.
- This is the inverse of
- :func:`~imgaug.BoundingBoxesOnImage.from_xyxy_array`.
- Parameters
- ----------
- dtype : numpy.dtype, optional
- Desired output datatype of the ndarray.
- Returns
- -------
- ndarray
- ``(N,4) ndarray``, where ``N`` denotes the number of bounding
- boxes and ``4`` denotes the top-left and bottom-right bounding
- box corner coordinates in form ``(x1, y1, x2, y2)``.
- """
- xyxy_array = np.zeros((len(self.bounding_boxes), 4), dtype=np.float32)
- for i, box in enumerate(self.bounding_boxes):
- xyxy_array[i] = [box.x1, box.y1, box.x2, box.y2]
- return xyxy_array.astype(dtype)
- def to_xy_array(self):
- """Convert the ``BoundingBoxesOnImage`` object to an ``(N,2) ndarray``.
- Added in 0.4.0.
- Returns
- -------
- ndarray
- ``(2*B,2) ndarray`` of xy-coordinates, where ``B`` denotes the
- number of bounding boxes.
- """
- return self.to_xyxy_array().reshape((-1, 2))
- def fill_from_xyxy_array_(self, xyxy):
- """Modify the BB coordinates of this instance in-place.
- .. note::
- This currently expects exactly one entry in `xyxy` per bounding
- in this instance. (I.e. two corner coordinates per instance.)
- Otherwise, an ``AssertionError`` will be raised.
- .. note::
- This method will automatically flip x-coordinates if ``x1>x2``
- for a bounding box. (Analogous for y-coordinates.)
- Added in 0.4.0.
- Parameters
- ----------
- xyxy : (N, 4) ndarray or iterable of iterable of number
- Coordinates of ``N`` bounding boxes on an image, given as
- a ``(N,4)`` array of two corner xy-coordinates per bounding box.
- ``N`` must match the number of bounding boxes in this instance.
- Returns
- -------
- BoundingBoxesOnImage
- This instance itself, with updated bounding box coordinates.
- Note that the instance was modified in-place.
- """
- xyxy = np.array(xyxy, dtype=np.float32)
- # note that np.array([]) is (0,), not (0, 4)
- assert xyxy.shape[0] == 0 or (xyxy.ndim == 2 and xyxy.shape[-1] == 4), ( # pylint: disable=unsubscriptable-object
- "Expected input array to have shape (N,4), "
- "got shape %s." % (xyxy.shape,))
- assert len(xyxy) == len(self.bounding_boxes), (
- "Expected to receive an array with as many rows there are "
- "bounding boxes in this instance. Got %d rows, expected %d." % (
- len(xyxy), len(self.bounding_boxes)))
- for bb, (x1, y1, x2, y2) in zip(self.bounding_boxes, xyxy):
- bb.x1 = min([x1, x2])
- bb.y1 = min([y1, y2])
- bb.x2 = max([x1, x2])
- bb.y2 = max([y1, y2])
- return self
- def fill_from_xy_array_(self, xy):
- """Modify the BB coordinates of this instance in-place.
- See
- :func:`~imgaug.augmentables.bbs.BoundingBoxesOnImage.fill_from_xyxy_array_`.
- Added in 0.4.0.
- Parameters
- ----------
- xy : (2*B, 2) ndarray or iterable of iterable of number
- Coordinates of ``B`` bounding boxes on an image, given as
- a ``(2*B,2)`` array of two corner xy-coordinates per bounding box.
- ``B`` must match the number of bounding boxes in this instance.
- Returns
- -------
- BoundingBoxesOnImage
- This instance itself, with updated bounding box coordinates.
- Note that the instance was modified in-place.
- """
- xy = np.array(xy, dtype=np.float32)
- return self.fill_from_xyxy_array_(xy.reshape((-1, 4)))
- def draw_on_image(self, image, color=(0, 255, 0), alpha=1.0, size=1,
- copy=True, raise_if_out_of_image=False, thickness=None):
- """Draw all bounding boxes onto a given image.
- Parameters
- ----------
- image : (H,W,3) ndarray
- The image onto which to draw the bounding boxes.
- This image should usually have the same shape as set in
- ``BoundingBoxesOnImage.shape``.
- color : int or list of int or tuple of int or (3,) ndarray, optional
- The RGB color of all bounding boxes.
- If a single ``int`` ``C``, then that is equivalent to ``(C,C,C)``.
- alpha : float, optional
- Alpha/transparency of the bounding box.
- size : int, optional
- Thickness in pixels.
- copy : bool, optional
- Whether to copy the image before drawing the bounding boxes.
- raise_if_out_of_image : bool, optional
- Whether to raise an exception if any bounding box is outside of the
- image.
- thickness : None or int, optional
- Deprecated.
- Returns
- -------
- (H,W,3) ndarray
- Image with drawn bounding boxes.
- """
- # pylint: disable=redefined-outer-name
- image = np.copy(image) if copy else image
- for bb in self.bounding_boxes:
- image = bb.draw_on_image(
- image,
- color=color,
- alpha=alpha,
- size=size,
- copy=False,
- raise_if_out_of_image=raise_if_out_of_image,
- thickness=thickness
- )
- return image
- def remove_out_of_image_(self, fully=True, partly=False):
- """Remove in-place all BBs that are fully/partially outside of the image.
- Added in 0.4.0.
- Parameters
- ----------
- fully : bool, optional
- Whether to remove bounding boxes that are fully outside of the
- image.
- partly : bool, optional
- Whether to remove bounding boxes that are partially outside of
- the image.
- Returns
- -------
- imgaug.augmentables.bbs.BoundingBoxesOnImage
- Reduced set of bounding boxes, with those that were
- fully/partially outside of the image being removed.
- The object and its items may have been modified in-place.
- """
- self.bounding_boxes = [
- bb
- for bb
- in self.bounding_boxes
- if not bb.is_out_of_image(self.shape, fully=fully, partly=partly)]
- return self
- def remove_out_of_image(self, fully=True, partly=False):
- """Remove all BBs that are fully/partially outside of the image.
- Parameters
- ----------
- fully : bool, optional
- Whether to remove bounding boxes that are fully outside of the
- image.
- partly : bool, optional
- Whether to remove bounding boxes that are partially outside of
- the image.
- Returns
- -------
- imgaug.augmentables.bbs.BoundingBoxesOnImage
- Reduced set of bounding boxes, with those that were
- fully/partially outside of the image being removed.
- """
- return self.copy().remove_out_of_image_(fully=fully, partly=partly)
- def remove_out_of_image_fraction_(self, fraction):
- """Remove in-place all BBs with an OOI fraction of at least `fraction`.
- 'OOI' is the abbreviation for 'out of image'.
- Added in 0.4.0.
- Parameters
- ----------
- fraction : number
- Minimum out of image fraction that a bounding box has to have in
- order to be removed. A fraction of ``1.0`` removes only bounding
- boxes that are ``100%`` outside of the image. A fraction of ``0.0``
- removes all bounding boxes.
- Returns
- -------
- imgaug.augmentables.bbs.BoundingBoxesOnImage
- Reduced set of bounding boxes, with those that had an out of image
- fraction greater or equal the given one removed.
- The object and its items may have been modified in-place.
- """
- return _remove_out_of_image_fraction_(self, fraction)
- def remove_out_of_image_fraction(self, fraction):
- """Remove all BBs with an out of image fraction of at least `fraction`.
- Added in 0.4.0.
- Parameters
- ----------
- fraction : number
- Minimum out of image fraction that a bounding box has to have in
- order to be removed. A fraction of ``1.0`` removes only bounding
- boxes that are ``100%`` outside of the image. A fraction of ``0.0``
- removes all bounding boxes.
- Returns
- -------
- imgaug.augmentables.bbs.BoundingBoxesOnImage
- Reduced set of bounding boxes, with those that had an out of image
- fraction greater or equal the given one removed.
- """
- return self.copy().remove_out_of_image_fraction_(fraction)
- @ia.deprecated(alt_func="BoundingBoxesOnImage.clip_out_of_image()",
- comment="clip_out_of_image() has the exactly same "
- "interface.")
- def cut_out_of_image(self):
- """Clip off all parts from all BBs that are outside of the image."""
- return self.clip_out_of_image()
- def clip_out_of_image_(self):
- """
- Clip off in-place all parts from all BBs that are outside of the image.
- Added in 0.4.0.
- Returns
- -------
- imgaug.augmentables.bbs.BoundingBoxesOnImage
- Bounding boxes, clipped to fall within the image dimensions.
- The object and its items may have been modified in-place.
- """
- # remove bbs that are not at least partially inside the image plane
- self.bounding_boxes = [bb for bb in self.bounding_boxes
- if bb.is_partly_within_image(self.shape)]
- for i, bb in enumerate(self.bounding_boxes):
- self.bounding_boxes[i] = bb.clip_out_of_image(self.shape)
- return self
- def clip_out_of_image(self):
- """Clip off all parts from all BBs that are outside of the image.
- Returns
- -------
- imgaug.augmentables.bbs.BoundingBoxesOnImage
- Bounding boxes, clipped to fall within the image dimensions.
- """
- return self.deepcopy().clip_out_of_image_()
- def shift_(self, x=0, y=0):
- """Move all BBs along the x/y-axis in-place.
- The origin ``(0, 0)`` is at the top left of the image.
- Added in 0.4.0.
- Parameters
- ----------
- x : number, optional
- Value to be added to all x-coordinates. Positive values shift
- towards the right images.
- y : number, optional
- Value to be added to all y-coordinates. Positive values shift
- towards the bottom images.
- Returns
- -------
- imgaug.augmentables.bbs.BoundingBoxesOnImage
- Shifted bounding boxes.
- The object and its items may have been modified in-place.
- """
- for i, bb in enumerate(self.bounding_boxes):
- self.bounding_boxes[i] = bb.shift_(x=x, y=y)
- return self
- def shift(self, x=0, y=0, top=None, right=None, bottom=None, left=None):
- """Move all BBs along the x/y-axis.
- The origin ``(0, 0)`` is at the top left of the image.
- Parameters
- ----------
- x : number, optional
- Value to be added to all x-coordinates. Positive values shift
- towards the right images.
- y : number, optional
- Value to be added to all y-coordinates. Positive values shift
- towards the bottom images.
- top : None or int, optional
- Deprecated since 0.4.0.
- Amount of pixels by which to shift all objects *from* the
- top (towards the bottom).
- right : None or int, optional
- Deprecated since 0.4.0.
- Amount of pixels by which to shift all objects *from* the
- right (towads the left).
- bottom : None or int, optional
- Deprecated since 0.4.0.
- Amount of pixels by which to shift all objects *from* the
- bottom (towards the top).
- left : None or int, optional
- Deprecated since 0.4.0.
- Amount of pixels by which to shift all objects *from* the
- left (towards the right).
- Returns
- -------
- imgaug.augmentables.bbs.BoundingBoxesOnImage
- Shifted bounding boxes.
- """
- x, y = _normalize_shift_args(
- x, y, top=top, right=right, bottom=bottom, left=left)
- return self.deepcopy().shift_(x=x, y=y)
- def to_keypoints_on_image(self):
- """Convert the bounding boxes to one ``KeypointsOnImage`` instance.
- Added in 0.4.0.
- Returns
- -------
- imgaug.augmentables.kps.KeypointsOnImage
- A keypoints instance containing ``N*4`` coordinates for ``N``
- bounding boxes. Order matches the order in ``bounding_boxes``.
- """
- from .kps import KeypointsOnImage
- # This currently uses 4 points instead of 2 points as the method
- # is primarily used during augmentation and 4 points are overall
- # the better choice there.
- arr = np.zeros((len(self.bounding_boxes), 2*4), dtype=np.float32)
- for i, box in enumerate(self.bounding_boxes):
- arr[i] = [
- box.x1, box.y1,
- box.x2, box.y1,
- box.x2, box.y2,
- box.x1, box.y2
- ]
- return KeypointsOnImage.from_xy_array(
- arr.reshape((-1, 2)),
- shape=self.shape
- )
- def invert_to_keypoints_on_image_(self, kpsoi):
- """Invert the output of ``to_keypoints_on_image()`` in-place.
- This function writes in-place into this ``BoundingBoxesOnImage``
- instance.
- Added in 0.4.0.
- Parameters
- ----------
- kpsoi : imgaug.augmentables.kps.KeypointsOnImages
- Keypoints to convert back to bounding boxes, i.e. the outputs
- of ``to_keypoints_on_image()``.
- Returns
- -------
- BoundingBoxesOnImage
- Bounding boxes container with updated coordinates.
- Note that the instance is also updated in-place.
- """
- assert len(kpsoi.keypoints) == len(self.bounding_boxes) * 4, (
- "Expected %d coordinates, got %d." % (
- len(self.bounding_boxes) * 2, len(kpsoi.keypoints)))
- for i, bb in enumerate(self.bounding_boxes):
- xx = [kpsoi.keypoints[4*i+0].x, kpsoi.keypoints[4*i+1].x,
- kpsoi.keypoints[4*i+2].x, kpsoi.keypoints[4*i+3].x]
- yy = [kpsoi.keypoints[4*i+0].y, kpsoi.keypoints[4*i+1].y,
- kpsoi.keypoints[4*i+2].y, kpsoi.keypoints[4*i+3].y]
- bb.x1 = min(xx)
- bb.y1 = min(yy)
- bb.x2 = max(xx)
- bb.y2 = max(yy)
- self.shape = kpsoi.shape
- return self
- def to_polygons_on_image(self):
- """Convert the bounding boxes to one ``PolygonsOnImage`` instance.
- Added in 0.4.0.
- Returns
- -------
- imgaug.augmentables.polys.PolygonsOnImage
- A ``PolygonsOnImage`` containing polygons. Each polygon covers
- the same area as the corresponding bounding box.
- """
- from .polys import PolygonsOnImage
- polygons = [bb.to_polygon() for bb in self.bounding_boxes]
- return PolygonsOnImage(polygons, shape=self.shape)
- def copy(self, bounding_boxes=None, shape=None):
- """Create a shallow copy of the ``BoundingBoxesOnImage`` instance.
- Parameters
- ----------
- bounding_boxes : None or list of imgaug.augmntables.bbs.BoundingBox, optional
- List of bounding boxes on the image.
- If ``None``, the instance's bounding boxes will be copied.
- shape : tuple of int, optional
- The shape of the image on which the bounding boxes are placed.
- If ``None``, the instance's shape will be copied.
- Returns
- -------
- imgaug.augmentables.bbs.BoundingBoxesOnImage
- Shallow copy.
- """
- if bounding_boxes is None:
- bounding_boxes = self.bounding_boxes[:]
- if shape is None:
- # use tuple() here in case the shape was provided as a list
- shape = tuple(self.shape)
- return BoundingBoxesOnImage(bounding_boxes, shape)
- def deepcopy(self, bounding_boxes=None, shape=None):
- """Create a deep copy of the ``BoundingBoxesOnImage`` object.
- Parameters
- ----------
- bounding_boxes : None or list of imgaug.augmntables.bbs.BoundingBox, optional
- List of bounding boxes on the image.
- If ``None``, the instance's bounding boxes will be copied.
- shape : tuple of int, optional
- The shape of the image on which the bounding boxes are placed.
- If ``None``, the instance's shape will be copied.
- Returns
- -------
- imgaug.augmentables.bbs.BoundingBoxesOnImage
- Deep copy.
- """
- # Manual copy is far faster than deepcopy, so use manual copy here.
- if bounding_boxes is None:
- bounding_boxes = [bb.deepcopy() for bb in self.bounding_boxes]
- if shape is None:
- # use tuple() here in case the shape was provided as a list
- shape = tuple(self.shape)
- return BoundingBoxesOnImage(bounding_boxes, shape)
- def __getitem__(self, indices):
- """Get the bounding box(es) with given indices.
- Added in 0.4.0.
- Returns
- -------
- list of imgaug.augmentables.bbs.BoundingBoxes
- Bounding box(es) with given indices.
- """
- return self.bounding_boxes[indices]
- def __iter__(self):
- """Iterate over the bounding boxes in this container.
- Added in 0.4.0.
- Yields
- ------
- BoundingBox
- A bounding box in this container.
- The order is identical to the order in the bounding box list
- provided upon class initialization.
- """
- return iter(self.bounding_boxes)
- def __len__(self):
- """Get the number of items in this instance.
- Added in 0.4.0.
- Returns
- -------
- int
- Number of items in this instance.
- """
- return len(self.items)
- def __repr__(self):
- return self.__str__()
- def __str__(self):
- return (
- "BoundingBoxesOnImage(%s, shape=%s)"
- % (str(self.bounding_boxes), self.shape))
- class _LabelOnImageDrawer(object):
- # size refers to the thickness of the BB
- # height is the height of the label rectangle, not the whole BB
- def __init__(self, color=(0, 255, 0), color_text=None, color_bg=None,
- size=1, alpha=1.0, raise_if_out_of_image=False,
- height=30, size_text=20):
- self.color = color
- self.color_text = color_text
- self.color_bg = color_bg
- self.size = size
- self.alpha = alpha
- self.raise_if_out_of_image = raise_if_out_of_image
- self.height = height
- self.size_text = size_text
- def draw_on_image_(self, image, bounding_box):
- # pylint: disable=invalid-name, redefined-outer-name
- if self.raise_if_out_of_image:
- self._do_raise_if_out_of_image(image, bounding_box)
- color_text, color_bg = self._preprocess_colors()
- x1, y1, x2, y2 = self._compute_bg_corner_coords(image, bounding_box)
- # cant draw anything if OOI
- if x2 <= x1 or y2 <= y1:
- return image
- # can currently only draw on images with shape (H,W,C), not (H,W)
- label_arr = self._draw_label_arr(bounding_box.label,
- y2 - y1, x2 - x1, image.shape[-1],
- image.dtype,
- color_text, color_bg,
- self.size_text)
- image = self._blend_label_arr_with_image_(image, label_arr,
- x1, y1, x2, y2)
- return image
- def draw_on_image(self, image, bounding_box):
- return self.draw_on_image_(np.copy(image), bounding_box)
- @classmethod
- def _do_raise_if_out_of_image(cls, image, bounding_box):
- if bounding_box.is_out_of_image(image):
- raise Exception(
- "Cannot draw bounding box x1=%.8f, y1=%.8f, x2=%.8f, y2=%.8f "
- "on image with shape %s." % (
- bounding_box.x1, bounding_box.y1,
- bounding_box.x2, bounding_box.y2,
- image.shape))
- def _preprocess_colors(self):
- color = np.uint8(self.color) if self.color is not None else None
- color_bg = self.color_bg
- if self.color_bg is not None:
- color_bg = np.uint8(color_bg)
- else:
- assert color is not None, (
- "Expected `color` to be set when `color_bg` is not set, "
- "but it was None.")
- color_bg = color
- color_text = self.color_text
- if self.color_text is not None:
- color_text = np.uint8(color_text)
- else:
- # we follow the approach from https://stackoverflow.com/a/1855903
- # here
- gray = (0.299 * color_bg[0]
- + 0.587 * color_bg[1]
- + 0.114 * color_bg[2])
- color_text = np.full((3,),
- 0 if gray > 128 else 255,
- dtype=np.uint8)
- return color_text, color_bg
- def _compute_bg_corner_coords(self, image, bounding_box):
- bb = bounding_box
- offset = self.size
- height, width = image.shape[0:2]
- y1, x1, x2 = bb.y1_int, bb.x1_int, bb.x2_int
- # dont use bb.y2 here! we want the label to be above the BB
- y1 = y1 - 1 - self.height
- y2 = y1 + self.height
- x1 = x1 - offset + 1
- x2 = x2 + offset
- y1, y2 = np.clip([y1, y2], 0, height-1)
- x1, x2 = np.clip([x1, x2], 0, width-1)
- return x1, y1, x2, y2
- @classmethod
- def _draw_label_arr(cls, label, height, width, nb_channels, dtype,
- color_text, color_bg, size_text):
- label_arr = np.zeros((height, width, nb_channels), dtype=dtype)
- label_arr[...] = color_bg.reshape((1, 1, -1))
- label_arr = ia.draw_text(label_arr,
- x=2, y=2,
- text=str(label),
- color=color_text,
- size=size_text)
- return label_arr
- def _blend_label_arr_with_image_(self, image, label_arr, x1, y1, x2, y2):
- alpha = self.alpha
- if alpha >= 0.99:
- image[y1:y2, x1:x2, :] = label_arr
- else:
- input_dtype = image.dtype
- foreground = label_arr.astype(np.float64)
- background = image[y1:y2, x1:x2, :].astype(np.float64)
- blend = (1 - alpha) * background + alpha * foreground
- blend = np.clip(blend, 0, 255).astype(input_dtype)
- image[y1:y2, x1:x2, :] = blend
- return image
|