kwimage.structs.boxes module¶
Vectorized Bounding Boxes
kwimage.Boxes
is a tool for efficiently transporting a set of bounding
boxes within python as well as methods for operating on bounding boxes. It is a
VERY thin wrapper around a pure numpy/torch array/tensor representation, and
thus it is very fast.
Raw bounding boxes come in lots of different formats. There are lots of ways to parameterize two points! Because of this THE USER MUST ALWAYS BE EXPLICIT ABOUT THE BOX FORMAT.
There are 3 main bounding box formats:
xywh: top left xy-coordinates and width height offsets
cxywh: center xy-coordinates and width height offsets
ltrb: top left and bottom right xy coordinates
Here is some example usage
Example
>>> import kwimage
>>> data = np.array([[ 0, 0, 10, 10],
>>> [ 5, 5, 50, 50],
>>> [10, 0, 20, 10],
>>> [20, 0, 30, 10]])
>>> # Note that the format of raw data is ambiguous, so you must specify
>>> boxes = kwimage.Boxes(data, 'ltrb')
>>> print('boxes = {!r}'.format(boxes))
boxes = <Boxes(ltrb,
array([[ 0, 0, 10, 10],
[ 5, 5, 50, 50],
[10, 0, 20, 10],
[20, 0, 30, 10]]))>
>>> # Now you can operate on those boxes easily
>>> print(boxes.translate((10, 10)))
<Boxes(ltrb,
array([[10., 10., 20., 20.],
[15., 15., 60., 60.],
[20., 10., 30., 20.],
[30., 10., 40., 20.]]))>
>>> print(boxes.to_cxywh())
<Boxes(cxywh,
array([[ 5. , 5. , 10. , 10. ],
[27.5, 27.5, 45. , 45. ],
[15. , 5. , 10. , 10. ],
[25. , 5. , 10. , 10. ]]))>
>>> print(ub.urepr(boxes.ious(boxes), precision=2, with_dtype=False))
np.array([[1. , 0.01, 0. , 0. ],
[0.01, 1. , 0.02, 0.02],
[0. , 0.02, 1. , 0. ],
[0. , 0.02, 0. , 1. ]])
>>> # OpenCV and Matplotlib have first class visualization support
>>> # xdoctest: +REQUIRES(--show)
>>> # xdoctest: +REQUIRES(module:kwplot)
>>> import kwplot
>>> plt = kwplot.autoplt()
>>> # opencv "draw_on" method
>>> background = kwimage.checkerboard(dsize=(64, 64), dtype=np.uint8, on_value='kw_green', off_value='kw_blue')
>>> canvas = background.copy()
>>> boxes.draw_on(canvas, color='kw_red')
>>> kwplot.imshow(canvas, fnum=1, pnum=(1, 2, 1), doclf=1, title='[cv2] kwimage.Boxes.draw_on')
>>> # matplotlib "draw_on" method
>>> kwplot.imshow(background, fnum=1, pnum=(1, 2, 2), title='[mpl] kwimage.Boxes.draw')
>>> boxes.draw(color='kw_red')
>>> plt.gcf().suptitle('Matplotlib and OpenCV have first class visualization support')
>>> kwplot.show_if_requested()
- SeeAlso:
- class kwimage.structs.boxes.Boxes(data, format=None, check=True)[source]¶
Bases:
_BoxConversionMixins
,_BoxPropertyMixins
,_BoxTransformMixins
,_BoxDrawMixins
,NiceRepr
Converts boxes between different formats as long as the last dimension contains 4 coordinates and the format is specified.
This is a convinience class, and should not not store the data for very long. The general idiom should be create class, convert data, and then get the raw data and let the class be garbage collected. This will help ensure that your code is portable and understandable if this class is not available.
This class is meant to efficiently store and manipulate multiple boxes. In the case of a single box the
kwimage.structs.single_box.Box
class can be used instead.Example
>>> # xdoctest: +IGNORE_WHITESPACE >>> import kwimage >>> import numpy as np >>> # Given an array / tensor that represents one or more boxes >>> data = np.array([[ 0, 0, 10, 10], >>> [ 5, 5, 50, 50], >>> [20, 0, 30, 10]]) >>> # The kwimage.Boxes data structure is a thin fast wrapper >>> # that provides methods for operating on the boxes. >>> # It requires that the user explicitly provide a code that denotes >>> # the format of the boxes (i.e. what each column represents) >>> boxes = kwimage.Boxes(data, 'ltrb') >>> # This means that there is no ambiguity about box format >>> # The representation string of the Boxes object demonstrates this >>> print('boxes = {!r}'.format(boxes)) boxes = <Boxes(ltrb, array([[ 0, 0, 10, 10], [ 5, 5, 50, 50], [20, 0, 30, 10]]))> >>> # if you pass this data around. You can convert to other formats >>> # For docs on available format codes see :class:`BoxFormat`. >>> # In this example we will convert (left, top, right, bottom) >>> # to (left-x, top-y, width, height). >>> boxes.toformat('xywh') <Boxes(xywh, array([[ 0, 0, 10, 10], [ 5, 5, 45, 45], [20, 0, 10, 10]]))> >>> # In addition to format conversion there are other operations >>> # We can quickly (using a C-backend) find IoUs >>> ious = boxes.ious(boxes) >>> print('{}'.format(ub.urepr(ious, nl=1, precision=2, with_dtype=False))) np.array([[1. , 0.01, 0. ], [0.01, 1. , 0.02], [0. , 0.02, 1. ]]) >>> # We can ask for the area of each box >>> print('boxes.area = {}'.format(ub.urepr(boxes.area, nl=0, with_dtype=False))) boxes.area = np.array([[ 100],[2025],[ 100]]) >>> # We can ask for the center of each box >>> print('boxes.center = {}'.format(ub.urepr(boxes.center, nl=1, with_dtype=False))) boxes.center = ( np.array([[ 5. ],[27.5],[25. ]]), np.array([[ 5. ],[27.5],[ 5. ]]), ) >>> # We can translate / scale the boxes >>> boxes.translate((10, 10)).scale(100) <Boxes(ltrb, array([[1000., 1000., 2000., 2000.], [1500., 1500., 6000., 6000.], [3000., 1000., 4000., 2000.]]))> >>> # We can clip the bounding boxes >>> boxes.translate((10, 10)).scale(100).clip(1200, 1200, 1700, 1800) <Boxes(ltrb, array([[1200., 1200., 1700., 1800.], [1500., 1500., 1700., 1800.], [1700., 1200., 1700., 1800.]]))> >>> # We can perform arbitrary warping of the boxes >>> # (note that if the transform is not axis aligned, the axis aligned >>> # bounding box of the transform result will be returned) >>> transform = np.array([[-0.83907153, 0.54402111, 0. ], >>> [-0.54402111, -0.83907153, 0. ], >>> [ 0. , 0. , 1. ]]) >>> boxes.warp(transform) <Boxes(ltrb, array([[ -8.3907153 , -13.8309264 , 5.4402111 , 0. ], [-39.23347095, -69.154632 , 23.00569785, -6.9154632 ], [-25.1721459 , -24.7113486 , -11.3412195 , -10.8804222 ]]))> >>> # Note, that we can transform the box to a Polygon for more >>> # accurate warping. >>> transform = np.array([[-0.83907153, 0.54402111, 0. ], >>> [-0.54402111, -0.83907153, 0. ], >>> [ 0. , 0. , 1. ]]) >>> warped_polys = boxes.to_polygons().warp(transform) >>> print(ub.urepr(warped_polys.data, sv=1)) [ <Polygon({ 'exterior': <Coords(data= array([[ 0. , 0. ], [ 5.4402111, -8.3907153], [ -2.9505042, -13.8309264], [ -8.3907153, -5.4402111], [ 0. , 0. ]]))>, 'interiors': [], })>, <Polygon({ 'exterior': <Coords(data= array([[ -1.4752521 , -6.9154632 ], [ 23.00569785, -44.67368205], [-14.752521 , -69.154632 ], [-39.23347095, -31.39641315], [ -1.4752521 , -6.9154632 ]]))>, 'interiors': [], })>, <Polygon({ 'exterior': <Coords(data= array([[-16.7814306, -10.8804222], [-11.3412195, -19.2711375], [-19.7319348, -24.7113486], [-25.1721459, -16.3206333], [-16.7814306, -10.8804222]]))>, 'interiors': [], })>, ] >>> # The kwimage.Boxes data structure is also convertable to >>> # several alternative data structures, like shapely, coco, and imgaug. >>> print(ub.urepr(boxes.to_shapely(), sv=1)) [ POLYGON ((0 0, 0 10, 10 10, 10 0, 0 0)), POLYGON ((5 5, 5 50, 50 50, 50 5, 5 5)), POLYGON ((20 0, 20 10, 30 10, 30 0, 20 0)), ] >>> # xdoctest: +REQUIRES(module:imgaug) >>> print(ub.urepr(boxes[0:1].to_imgaug(shape=(100, 100)), sv=1)) BoundingBoxesOnImage([BoundingBox(x1=0.0000, y1=0.0000, x2=10.0000, y2=10.0000, label=None)], shape=(100, 100)) >>> # xdoctest: -REQUIRES(module:imgaug) >>> print(ub.urepr(list(boxes.to_coco()), sv=1)) [ [0, 0, 10, 10], [5, 5, 45, 45], [20, 0, 10, 10], ] >>> # Finally, when you are done with your boxes object, you can >>> # unwrap the raw data by using the ``.data`` attribute >>> # all operations are done on this data, which gives the >>> # kwiamge.Boxes data structure almost no overhead when >>> # inserted into existing code. >>> print('boxes.data =\n{}'.format(ub.urepr(boxes.data, nl=1))) boxes.data = np.array([[ 0, 0, 10, 10], [ 5, 5, 50, 50], [20, 0, 30, 10]], dtype=...) >>> # xdoctest: +REQUIRES(module:torch) >>> # This data structure was designed for use with both torch >>> # and numpy, the underlying data can be either an array or tensor. >>> boxes.tensor() <Boxes(ltrb, tensor([[ 0, 0, 10, 10], [ 5, 5, 50, 50], [20, 0, 30, 10]]...))> >>> boxes.numpy() <Boxes(ltrb, array([[ 0, 0, 10, 10], [ 5, 5, 50, 50], [20, 0, 30, 10]]))>
Example
>>> # xdoctest: +IGNORE_WHITESPACE >>> from kwimage.structs.boxes import * # NOQA >>> # Demo of conversion methods >>> import kwimage >>> kwimage.Boxes([[25, 30, 15, 10]], 'xywh') <Boxes(xywh, array([[25, 30, 15, 10]]))> >>> kwimage.Boxes([[25, 30, 15, 10]], 'xywh').to_xywh() <Boxes(xywh, array([[25, 30, 15, 10]]))> >>> kwimage.Boxes([[25, 30, 15, 10]], 'xywh').to_cxywh() <Boxes(cxywh, array([[32.5, 35. , 15. , 10. ]]))> >>> kwimage.Boxes([[25, 30, 15, 10]], 'xywh').to_ltrb() <Boxes(ltrb, array([[25, 30, 40, 40]]))> >>> kwimage.Boxes([[25, 30, 15, 10]], 'xywh').scale(2).to_ltrb() <Boxes(ltrb, array([[50., 60., 80., 80.]]))> >>> # xdoctest: +REQUIRES(module:torch) >>> import torch >>> kwimage.Boxes(torch.FloatTensor([[25, 30, 15, 20]]), 'xywh').scale(.1).to_ltrb() <Boxes(ltrb, tensor([[ 2.5000, 3.0000, 4.0000, 5.0000]]))>
Note
In the following examples we show cases where
Boxes
can hold a single 1-dimensional box array. This is a holdover from an older codebase, and some functions may assume that the input is at least 2-D. Thus when representing a single bounding box it is best practice to view it as a list of 1 box. While many function will work in the 1-D case, not all functions have been tested and thus we cannot gaurentee correctness.Example
>>> # xdoctest: +IGNORE_WHITESPACE >>> Boxes([25, 30, 15, 10], 'xywh') <Boxes(xywh, array([25, 30, 15, 10]))> >>> Boxes([25, 30, 15, 10], 'xywh').to_xywh() <Boxes(xywh, array([25, 30, 15, 10]))> >>> Boxes([25, 30, 15, 10], 'xywh').to_cxywh() <Boxes(cxywh, array([32.5, 35. , 15. , 10. ]))> >>> Boxes([25, 30, 15, 10], 'xywh').to_ltrb() <Boxes(ltrb, array([25, 30, 40, 40]))> >>> Boxes([25, 30, 15, 10], 'xywh').scale(2).to_ltrb() <Boxes(ltrb, array([50., 60., 80., 80.]))> >>> # xdoctest: +REQUIRES(module:torch) >>> import torch >>> Boxes(torch.FloatTensor([[25, 30, 15, 20]]), 'xywh').scale(.1).to_ltrb() <Boxes(ltrb, tensor([[ 2.5000, 3.0000, 4.0000, 5.0000]]))>
Example
>>> datas = [ >>> [1, 2, 3, 4], >>> [[1, 2, 3, 4], [4, 5, 6, 7]], >>> [[[1, 2, 3, 4], [4, 5, 6, 7]]], >>> ] >>> formats = BoxFormat.cannonical >>> for format1 in formats: >>> for data in datas: >>> self = box1 = Boxes(data, format1) >>> for format2 in formats: >>> box2 = box1.toformat(format2) >>> back = box2.toformat(format1) >>> assert box1 == back
- Parameters:
data (ndarray | Tensor | Boxes) – Either an ndarray or Tensor with trailing shape of 4, or an existing Boxes object.
format (str) – format code indicating which coordinates are represented by data. If data is a Boxes object then this is not necessary.
check (bool) – if True runs input checks on raw data.
- Raises:
ValueError – if data is specified without a format
- classmethod random(num=1, scale=1.0, format='xywh', anchors=None, anchor_std=0.16666666666666666, tensor=False, rng=None)[source]¶
Makes random boxes; typically for testing purposes
- Parameters:
num (int) – number of boxes to generate
scale (float | Tuple[float, float]) – size of imgdims
format (str) – format of boxes to be created (e.g. ltrb, xywh)
anchors (ndarray) – normalized width / heights of anchor boxes to perterb and randomly place. (must be in range 0-1)
anchor_std (float) – magnitude of noise applied to anchor shapes
tensor (bool) – if True, returns boxes in tensor format
rng (None | int | RandomState) – initial random seed
- Returns:
random boxes
- Return type:
Example
>>> # xdoctest: +IGNORE_WHITESPACE >>> Boxes.random(3, rng=0, scale=100) <Boxes(xywh, array([[54, 54, 6, 17], [42, 64, 1, 25], [79, 38, 17, 14]]...))> >>> # xdoctest: +REQUIRES(module:torch) >>> Boxes.random(3, rng=0, scale=100).tensor() <Boxes(xywh, tensor([[ 54, 54, 6, 17], [ 42, 64, 1, 25], [ 79, 38, 17, 14]]...))> >>> anchors = np.array([[.5, .5], [.3, .3]]) >>> Boxes.random(3, rng=0, scale=100, anchors=anchors) <Boxes(xywh, array([[ 2, 13, 51, 51], [32, 51, 32, 36], [36, 28, 23, 26]]...))>
Example
>>> # Boxes position/shape within 0-1 space should be uniform. >>> # xdoctest: +REQUIRES(--show) >>> import kwplot >>> kwplot.autompl() >>> fig = kwplot.figure(fnum=1, doclf=True) >>> fig.gca().set_xlim(0, 128) >>> fig.gca().set_ylim(0, 128) >>> import kwimage >>> kwimage.Boxes.random(num=10).scale(128).draw()
- classmethod concatenate(boxes, axis=0)[source]¶
Concatenates multiple boxes together
- Parameters:
boxes (Sequence[Boxes]) – list of boxes to concatenate
axis (int) – axis to stack on. Defaults to 0.
- Returns:
stacked boxes
- Return type:
Example
>>> boxes = [Boxes.random(3) for _ in range(3)] >>> new = Boxes.concatenate(boxes) >>> assert len(new) == 9 >>> assert np.all(new.data[3:6] == boxes[1].data)
Example
>>> boxes = [Boxes.random(3) for _ in range(3)] >>> boxes[0].data = boxes[0].data[0] >>> boxes[1].data = boxes[0].data[0:0] >>> new = Boxes.concatenate(boxes) >>> assert len(new) == 4 >>> # xdoctest: +REQUIRES(module:torch) >>> new = Boxes.concatenate([b.tensor() for b in boxes]) >>> assert len(new) == 4
- compress(flags, axis=0, inplace=False)[source]¶
Filters boxes based on a boolean criterion
- Parameters:
flags (ArrayLike) – true for items to be kept. Extended type: ArrayLike[bool]
axis (int) – you usually want this to be 0
inplace (bool) – if True, modifies this object
- Returns:
the boxes corresponding to where flags were true
- Return type:
Example
>>> self = Boxes([[25, 30, 15, 10]], 'ltrb') >>> self.compress([True]) <Boxes(ltrb, array([[25, 30, 15, 10]]))> >>> self.compress([False]) <Boxes(ltrb, array([], shape=(0, 4), dtype=...))>
- take(idxs, axis=0, inplace=False)[source]¶
Takes a subset of items at specific indices
- Parameters:
indices (ArrayLike) – Indexes of items to take. Extended type ArrayLike[int].
axis (int) – you usually want this to be 0
inplace (bool) – if True, modifies this object
- Returns:
the boxes corresponding to the specified indices
- Return type:
Example
>>> self = Boxes([[25, 30, 15, 10]], 'ltrb') >>> self.take([0]) <Boxes(ltrb, array([[25, 30, 15, 10]]))> >>> self.take([]) <Boxes(ltrb, array([], shape=(0, 4), dtype=...))>
- is_tensor()[source]¶
is the backend fueled by torch?
- Returns:
True if the Boxes are torch tensors
- Return type:
- is_numpy()[source]¶
is the backend fueled by numpy?
- Returns:
True if the Boxes are numpy arrays
- Return type:
- property _impl¶
returns the kwarray.ArrayAPI implementation for the data
- Returns:
the array API for the box backend
- Return type:
Example
>>> assert Boxes.random().numpy()._impl.is_numpy >>> # xdoctest: +REQUIRES(module:torch) >>> assert Boxes.random().tensor()._impl.is_tensor
- property device¶
If the backend is torch returns the data device, otherwise None
- astype(dtype)[source]¶
Changes the type of the internal array used to represent the boxes
Note
this operation is not inplace
- Returns:
the boxes with the chosen type
- Return type:
Example
>>> # xdoctest: +IGNORE_WHITESPACE >>> # xdoctest: +REQUIRES(module:torch) >>> Boxes.random(3, 100, rng=0).tensor().astype('int16') <Boxes(xywh, tensor([[54, 54, 6, 17], [42, 64, 1, 25], [79, 38, 17, 14]], dtype=torch.int16))> >>> Boxes.random(3, 100, rng=0).numpy().astype('int16') <Boxes(xywh, array([[54, 54, 6, 17], [42, 64, 1, 25], [79, 38, 17, 14]], dtype=int16))> >>> Boxes.random(3, 100, rng=0).tensor().astype('float32') >>> Boxes.random(3, 100, rng=0).numpy().astype('float32')
- round(inplace=False)[source]¶
Rounds data coordinates to the nearest integer.
This operation is applied directly to the box coordinates, so its output will depend on the format the boxes are stored in.
- Parameters:
inplace (bool) – if True, modifies this object. Defaults to False.
- Returns:
the boxes with rounded coordinates
- Return type:
- SeeAlso:
Example
>>> import kwimage >>> self = kwimage.Boxes.random(3, rng=0).scale(10) >>> new = self.round() >>> print('self = {!r}'.format(self)) >>> print('new = {!r}'.format(new)) self = <Boxes(xywh, array([[5.48813522, 5.44883192, 0.53949833, 1.70306146], [4.23654795, 6.4589411 , 0.13932407, 2.45878875], [7.91725039, 3.83441508, 1.71937704, 1.45453393]]))> new = <Boxes(xywh, array([[5., 5., 1., 2.], [4., 6., 0., 2.], [8., 4., 2., 1.]]))>
- quantize(inplace=False, dtype=<class 'numpy.int32'>)[source]¶
Converts the box to integer coordinates.
This operation takes the floor of the left side and the ceil of the right side. Thus the area of the box will never decreases. But this will often increase the width / height of the box by a pixel.
- Parameters:
inplace (bool) – if True, modifies this object
dtype (type) – type to cast as
- Returns:
the boxes with quantized coordinates
- Return type:
- SeeAlso:
Boxes.round()
Boxes.resize()
if you need to ensure the size does not change
Example
>>> import kwimage >>> self = kwimage.Boxes.random(3, rng=0).scale(10) >>> new = self.quantize() >>> print('self = {!r}'.format(self)) >>> print('new = {!r}'.format(new)) self = <Boxes(xywh, array([[5.48813522, 5.44883192, 0.53949833, 1.70306146], [4.23654795, 6.4589411 , 0.13932407, 2.45878875], [7.91725039, 3.83441508, 1.71937704, 1.45453393]]))> new = <Boxes(xywh, array([[5, 5, 2, 3], [4, 6, 1, 3], [7, 3, 3, 3]]...))>
Example
>>> import kwimage >>> # Be careful if it is important to preserve the width/height >>> self = kwimage.Boxes([[0, 0, 10, 10]], 'xywh') >>> aff = kwimage.Affine.coerce(offset=(0.5, 0.0)) >>> warped = self.warp(aff) >>> new = warped.quantize(dtype=int) >>> print('self = {!r}'.format(self)) >>> print('warped = {!r}'.format(warped)) >>> print('new = {!r}'.format(new)) self = <Boxes(xywh, array([[ 0, 0, 10, 10]]))> warped = <Boxes(xywh, array([[ 0.5, 0. , 10. , 10. ]]))> new = <Boxes(xywh, array([[ 0, 0, 11, 10]]))>
Example
>>> import kwimage >>> self = kwimage.Boxes.random(3, rng=0) >>> orig = self.copy() >>> self.quantize(inplace=True) >>> assert np.any(self.data != orig.data)
- numpy()[source]¶
Converts tensors to numpy. Does not change memory if possible.
- Returns:
the boxes with a numpy backend
- Return type:
Example
>>> # xdoctest: +REQUIRES(module:torch) >>> self = Boxes.random(3).tensor() >>> newself = self.numpy() >>> self.data[0, 0] = 0 >>> assert newself.data[0, 0] == 0 >>> self.data[0, 0] = 1 >>> assert self.data[0, 0] == 1
- tensor(device=NoParam)[source]¶
Converts numpy to tensors. Does not change memory if possible.
- Parameters:
device (int | None | torch.device) – The torch device to put the backend tensors on
- Returns:
the boxes with a torch backend
- Return type:
Example
>>> # xdoctest: +REQUIRES(module:torch) >>> self = Boxes.random(3) >>> # xdoctest: +REQUIRES(module:torch) >>> newself = self.tensor() >>> self.data[0, 0] = 0 >>> assert newself.data[0, 0] == 0 >>> self.data[0, 0] = 1 >>> assert self.data[0, 0] == 1
- ious(other, bias=0, impl='auto', mode=None)[source]¶
Intersection over union.
Compute IOUs (intersection area over union area) between these boxes and another set of boxes. This is a symmetric measure of similarity between boxes.
Todo
- [ ] Add pairwise flag to toggle between one-vs-one and all-vs-all
computation. I.E. Add option for componentwise calculation.
- Parameters:
other (Boxes) – boxes to compare IoUs against
bias (int) – either 0 or 1, does TL=BR have area of 0 or 1? Defaults to 0.
impl (str) – code to specify implementation used to ious. Can be either torch, py, c, or auto. Efficiency and the exact result will vary by implementation, but they will always be close. Some implementations only accept certain data types (e.g. impl=’c’, only accepts float32 numpy arrays). See ~/code/kwimage/dev/bench_bbox.py for benchmark details. On my system the torch impl was fastest (when the data was on the GPU). Defaults to ‘auto’
mode (str) – depricated, use impl
- Returns:
the ious
- Return type:
ndarray
- SeeAlso:
iooas - for a measure of coverage between boxes
Examples
>>> import kwimage >>> self = kwimage.Boxes(np.array([[ 0, 0, 10, 10], >>> [10, 0, 20, 10], >>> [20, 0, 30, 10]]), 'ltrb') >>> other = kwimage.Boxes(np.array([6, 2, 20, 10]), 'ltrb') >>> overlaps = self.ious(other, bias=1).round(2) >>> assert np.all(np.isclose(overlaps, [0.21, 0.63, 0.04])), repr(overlaps)
Examples
>>> import kwimage >>> boxes1 = kwimage.Boxes(np.array([[ 0, 0, 10, 10], >>> [10, 0, 20, 10], >>> [20, 0, 30, 10]]), 'ltrb') >>> other = kwimage.Boxes(np.array([[6, 2, 20, 10], >>> [100, 200, 300, 300]]), 'ltrb') >>> overlaps = boxes1.ious(other) >>> print('{}'.format(ub.urepr(overlaps, precision=2, nl=1))) np.array([[0.18, 0. ], [0.61, 0. ], [0. , 0. ]]...)
Examples
>>> # xdoctest: +IGNORE_WHITESPACE >>> Boxes(np.empty(0), 'xywh').ious(Boxes(np.empty(4), 'xywh')).shape (0,) >>> #Boxes(np.empty(4), 'xywh').ious(Boxes(np.empty(0), 'xywh')).shape >>> Boxes(np.empty((0, 4)), 'xywh').ious(Boxes(np.empty((0, 4)), 'xywh')).shape (0, 0) >>> Boxes(np.empty((1, 4)), 'xywh').ious(Boxes(np.empty((0, 4)), 'xywh')).shape (1, 0) >>> Boxes(np.empty((0, 4)), 'xywh').ious(Boxes(np.empty((1, 4)), 'xywh')).shape (0, 1)
Examples
>>> # xdoctest: +REQUIRES(module:torch) >>> import torch >>> formats = BoxFormat.cannonical >>> istensors = [False, True] >>> results = {} >>> for format in formats: >>> for tensor in istensors: >>> boxes1 = Boxes.random(5, scale=10.0, rng=0, format=format, tensor=tensor) >>> boxes2 = Boxes.random(7, scale=10.0, rng=1, format=format, tensor=tensor) >>> ious = boxes1.ious(boxes2) >>> results[(format, tensor)] = ious >>> results = {k: v.numpy() if torch.is_tensor(v) else v for k, v in results.items() } >>> results = {k: v.tolist() for k, v in results.items()} >>> print(ub.urepr(results, sk=True, precision=3, nl=2)) >>> from functools import partial >>> assert ub.allsame(results.values(), partial(np.allclose, atol=1e-07))
- iooas(other, bias=0)[source]¶
Intersection over other area.
This is an asymetric measure of coverage. How much of the “other” boxes are covered by these boxes. It is the area of intersection between each pair of boxes and the area of the “other” boxes.
- SeeAlso:
ious - for a measure of similarity between boxes
- Parameters:
other (Boxes) – boxes to compare IoOA against
bias (int) – either 0 or 1, does TL=BR have area of 0 or 1? Defaults to 0.
- Returns:
the iooas
- Return type:
ndarray
Examples
>>> self = Boxes(np.array([[ 0, 0, 10, 10], >>> [10, 0, 20, 10], >>> [20, 0, 30, 10]]), 'ltrb') >>> other = Boxes(np.array([[6, 2, 20, 10], [0, 0, 0, 3]]), 'xywh') >>> coverage = self.iooas(other, bias=0).round(2) >>> print('coverage = {!r}'.format(coverage))
- isect_area(other, bias=0)[source]¶
Intersection part of intersection over union computation
- Parameters:
other (Boxes) – boxes to compare IoOA against
bias (int) – either 0 or 1, does TL=BR have area of 0 or 1? Defaults to 0.
- Returns:
the iooas
- Return type:
ndarray
Examples
>>> # xdoctest: +IGNORE_WHITESPACE >>> self = Boxes.random(5, scale=10.0, rng=0, format='ltrb') >>> other = Boxes.random(3, scale=10.0, rng=1, format='ltrb') >>> isect = self.isect_area(other, bias=0) >>> ious_v1 = isect / ((self.area + other.area.T) - isect) >>> ious_v2 = self.ious(other, bias=0) >>> assert np.allclose(ious_v1, ious_v2)
- intersection(other)[source]¶
Componentwise intersection between two sets of Boxes
intersections of boxes are always boxes, so this works
- Parameters:
other (Boxes) – boxes to intersect with this object. (must be of same length)
- Returns:
the component-wise intersection geometry
- Return type:
Examples
>>> # xdoctest: +IGNORE_WHITESPACE >>> from kwimage.structs.boxes import * # NOQA >>> self = Boxes.random(5, rng=0).scale(10.) >>> other = self.translate(1) >>> new = self.intersection(other) >>> new_area = np.nan_to_num(new.area).ravel() >>> alt_area = np.diag(self.isect_area(other)) >>> close = np.isclose(new_area, alt_area) >>> assert np.all(close)
- union_hull(other)[source]¶
Componentwise bounds of the union between two sets of Boxes
NOTE: convert to polygon to do a real union.
Note
This is not a real hull. A better name for this might be union_bounds because we are returning the axis-aligned bounds of the convex hull of the union.
- Parameters:
other (Boxes) – boxes to union with this object. (must be of same length)
- Returns:
bounding box of the unioned boxes
- Return type:
Examples
>>> # xdoctest: +IGNORE_WHITESPACE >>> from kwimage.structs.boxes import * # NOQA >>> self = Boxes.random(5, rng=0).scale(10.) >>> other = self.translate(1) >>> new = self.union_hull(other) >>> new_area = np.nan_to_num(new.area).ravel()
- bounding_box()[source]¶
Returns the box that bounds all of the contained boxes
- Returns:
a single box
- Return type:
Examples
>>> # xdoctest: +IGNORE_WHITESPACE >>> from kwimage.structs.boxes import * # NOQA >>> self = Boxes.random(5, rng=0).scale(10.) >>> other = self.translate(1) >>> new = self.union_hull(other) >>> new_area = np.nan_to_num(new.area).ravel()
- contains(other)[source]¶
Determine of points are completely contained by these boxes
- Parameters:
other (kwimage.Points) – points to test for containment. TODO: support generic data types
- Returns:
- flags - N x M boolean matrix indicating which box
contains which points, where N is the number of boxes and M is the number of points.
- Return type:
ArrayLike
Examples
>>> import kwimage >>> self = kwimage.Boxes.random(10).scale(10).round() >>> other = kwimage.Points.random(10).scale(10).round() >>> flags = self.contains(other) >>> flags = self.contains(self.xy_center) >>> assert np.all(np.diag(flags))
- view(*shape)[source]¶
Passthrough method to view or reshape
- Parameters:
*shape (Tuple[int, …]) – new shape
- Returns:
data with a different view
- Return type:
Example
>>> # xdoctest: +REQUIRES(module:torch) >>> self = Boxes.random(6, scale=10.0, rng=0, format='xywh').tensor() >>> assert list(self.view(3, 2, 4).data.shape) == [3, 2, 4] >>> self = Boxes.random(6, scale=10.0, rng=0, format='ltrb').tensor() >>> assert list(self.view(3, 2, 4).data.shape) == [3, 2, 4]
- _ensure_nonnegative_extent(inplace=False)[source]¶
Experimental. If the box has a negative width / height make them positive and adjust the tlxy point.
Need a better name for this function.
- FIXME:
- [ ] Slice semantics (i.e. start/stop) of boxes break under
rotations and reflections. This function needs to be thought out a bit more before becoming non-experimental.
- Returns:
Boxes
Example
>>> import kwimage >>> self = kwimage.Boxes(np.array([ >>> [20, 30, -10, -20], >>> [0, 0, 10, 20], >>> [0, 0, -10, 20], >>> [0, 0, 10, -20], >>> ]), 'xywh') >>> new = self._ensure_nonnegative_extent(inplace=0) >>> assert np.any(self.width < 0) >>> assert not np.any(new.width < 0) >>> assert np.any(self.height < 0) >>> assert not np.any(new.height < 0)
>>> import kwimage >>> self = kwimage.Boxes(np.array([ >>> [0, 3, 8, -4], >>> ]), 'xywh') >>> new = self._ensure_nonnegative_extent(inplace=0) >>> print('self = {}'.format(ub.urepr(self, nl=1))) >>> print('new = {}'.format(ub.urepr(new, nl=1))) >>> assert not np.any(self.width < 0) >>> assert not np.any(new.width < 0) >>> assert np.any(self.height < 0) >>> assert not np.any(new.height < 0)