Basic objects

Basic object types and methods used in PyBx
import json
from fastcore.test import test_eq, test_fail, test_warns, ExceptionExpected

Check validity of boxes


source

check_format_types

def check_format_types(
    b:list, verbose:bool=False
):

Checks if the provided bounding box is in the correct format, typically of [int, int, int, int, str].

Args: b (list): bounding box coordinates with or without label

Returns: bool

list_bbox = [14, 51, 71, 92, "item"]
list_bbox_nolabel = [14, 51, 71, 92]
check_format_types(list_bbox), check_format_types(list_bbox_nolabel)
(True, True)
np.array(list_bbox_nolabel).dtype
dtype('int64')
check_format_types(np.array(list_bbox_nolabel), verbose=True)
Passed `check_format_types`.
True

source

check_length_types

def check_length_types(
    b:list, verbose:bool=False
):

Checks if the provided bounding box has an acceptable length and type, typically of [int, int, int, int, str] or [int, int, int, int]

Also checks that all values are positive.

Args: b (list): bounding box coordinates with or without label

Returns: bool

check_length_types(list_bbox), check_length_types(list_bbox_nolabel)
(True, True)
(
    check_length_types([14, 51, 92, "item1"]),
    check_length_types([14, 51, 92]),
    check_length_types([14, 51, -1, 92]),
)
(False, False, False)
check_length_types([14, 51, 92, "item1"], verbose=True)
Failed `check_length_types`. Results were label:True, len:True, coords:False
False

source

check_max_voc

def check_max_voc(
    b:list, verbose:bool=False
):

Checks if the provided bounding box bottom right corner (x_max, y_max) is greater than the top left corner (x_min, y_min), which is true for voc format.

Args: b (list): bounding box coordinates with or without label

Returns: bool

list_bbox = [14, 51, 71, 92, "item"]
list_bbox_nolabel = [14, 51, 71, 92]
check_max_voc(list_bbox), check_max_voc(list_bbox_nolabel)
(True, True)
check_max_voc([14, 51, 10, 92]), check_max_voc([0, -1, 10, 92])
(False, True)
check_max_voc([14, 51, 10, 92], verbose=True)
Failed `check_max_voc`. Results were check_xs:False, check_ys:True
False

Method that does all of the above checks.


source

perform_box_checks

def perform_box_checks(
    b:list, verbose:bool=False
):

Calls all checks for bounding boxes: check_max_voc, check_length_types, check_format_types.

perform_box_checks([14, 51, 10, 92], verbose=True)
Failed `check_max_voc`. Results were check_xs:False, check_ys:True
Passed `check_length_types`. Results were label:True, len:True, coords:True
Passed `check_format_types`.
False

Parse bounding boxes

Bounding box coordinates of type list/dict/json/array can be converted to a Bx instance. Once wrapped as a Bx instance, some interesting properties can be calculated from the coordinates.


source

parse_list

def parse_list(
    b:Union, verbose:bool=False, no_check:bool=False
):

Takes a list and splits into bounding box coordinates and label

Args: b (list): Bounding box coordinates with or without label

Returns: list: Bounding box coordinates str: Bounding box label

parse_list([10, 0, 11, 6])
([[10, 0, 11, 6]], ['unknown'])
list_bbox = [14, 51, 71, 92, "item"]
list_bbox_nolabel = [14, 51, 71, 92]
parse_list(list_bbox), parse_list(list_bbox_nolabel)
(([[14, 51, 71, 92]], ['item']), ([[14, 51, 71, 92]], ['unknown']))

Works the same way with arrays.

perform_box_checks(np.array(list_bbox_nolabel))
True
parse_list(np.array(list_bbox_nolabel))
([[14, 51, 71, 92]], ['unknown'])

If wrong or bad bounding boxes are passed, it will throw an Error.

try:
    parse_list([14, 51, 71, -1])
except AssertionError as A:
    print(A)
Failed `perform_box_checks` for bounding box coordinates [14, 51, 71, -1]
try:
    parse_list([14, 51, 71])
except AssertionError as A:
    print(A)
Not enough items in passed bounding box [14, 51, 71].
try:
    parse_list([14, 51, 71, "item"])
except AssertionError as A:
    print(A)
Got `y_min`, `y_max` of wrong type [51, 'item']

Same operations with dict and json strings.

dict_bbox = {"x_min": 14, "y_min": 51, "x_max": 71, "y_max": 92, "label": "item"}
dict_bbox_labelkey = {
    "x_min": 14,
    "y_min": 51,
    "x_max": 71,
    "y_max": 92,
    "object": "item",
}
dict_bbox_nolabel = {"x_min": 14, "y_min": 51, "x_max": 71, "y_max": 92}
voc_keys
['x_min', 'y_min', 'x_max', 'y_max', 'label']

source

parse_dict

def parse_dict(
    b:dict, **kwargs
):

Takes a dict and splits into bounding box coordinates and label

Args: b (dict): Bounding box coordinates with or without label

Returns: list: Bounding box coordinates str: Bounding box label

update_keys(dict_bbox_nolabel)
['x_min', 'y_min', 'x_max', 'y_max', 'label']
parse_dict(dict_bbox), parse_dict(dict_bbox_labelkey), parse_dict(dict_bbox_nolabel)
/var/folders/sl/tgj9ztc51hz_d2yt8kg97f840000gn/T/ipykernel_15739/1918640422.py:21: UserWarning: No label key in {'x_min': 14, 'y_min': 51, 'x_max': 71, 'y_max': 92}
  warnings.warn(f"No {k} key in {b}")
(([[14, 51, 71, 92]], ['item']),
 ([[14, 51, 71, 92]], ['item']),
 ([[14, 51, 71, 92]], ['unknown']))

Same operations with Json strings.

json_bbox = '{"x_min": 14, "y_min": 51, "x_max": 71, "y_max": 92, "label": "item"}'
json_bbox_labelkey = (
    '{"x_min": 14, "y_min": 51, "x_max": 71, "y_max": 92, "object": "item"}'
)
json_bbox_nolabel = '{"x_min": 14, "y_min": 51, "x_max": 71, "y_max": 92}'

source

parse_json

def parse_json(
    b:Union, **kwargs
):

Takes a json string or dict and splits into bounding box coordinates and label

Args: b (json str): Bounding box coordinates with or without label

Returns: list: Bounding box coordinates str: Bounding box label

json_bbox
'{"x_min": 14, "y_min": 51, "x_max": 71, "y_max": 92, "label": "item"}'
parse_json(json_bbox), parse_json(json_bbox_labelkey), parse_json(json_bbox_nolabel)
/var/folders/sl/tgj9ztc51hz_d2yt8kg97f840000gn/T/ipykernel_15739/1918640422.py:21: UserWarning: No label key in {'x_min': 14, 'y_min': 51, 'x_max': 71, 'y_max': 92}
  warnings.warn(f"No {k} key in {b}")
(([[14, 51, 71, 92]], ['item']),
 ([[14, 51, 71, 92]], ['item']),
 ([[14, 51, 71, 92]], ['unknown']))

It could also be a json object read from file.

json_bbox_file = json.load(open("../data/annots.json"))[0]
json_bbox_file
{'x_min': 130, 'y_min': 63, 'x_max': 225, 'y_max': 180, 'label': 'clock'}
parse_json(json_bbox_file)
([[130, 63, 225, 180]], ['clock'])

source

Bx

def Bx(
    coords, label:list=None, verbose:bool=False, no_check:bool=False
):

Interface for all future Bx’s

Initializing an empty Bx class. It does a whole lot of things!

Generate random coordinates for one anchor boxes.

np.random.seed(42)
annots = [sorted([np.random.randint(100) for i in range(4)])]
annots
[[14, 51, 71, 92]]

If a single list is passed, Bx will make it a list of list.

annots[0]
[14, 51, 71, 92]
b = Bx(annots[0])
b
Bx(coords=[[14, 51, 71, 92]], label=['unknown'])
b.n_points
4
len(b)
1
b.cx
42.5

If label is passed along with coords, this is used.

b = Bx(annots[0], label="item")
b
Bx(coords=[[14, 51, 71, 92]], label=['item'])

Can be a list of single item name also.

b = Bx(annots[0], label=["item"])
b
Bx(coords=[[14, 51, 71, 92]], label=['item'])
b.yolo
<bound method Bx.yolo of Bx(coords=[[14, 51, 71, 92]], label=['item'])>

To get normalized coordinates wrt to the image dimensions.

b.yolo(224, 224, normalize=True)
[[0.18973214285714285,
  0.31919642857142855,
  0.2544642857142857,
  0.18303571428571427,
  'item']]
b.values
(#1) [[14, 51, 71, 92, 'item']]
b.coords_as_numpy
array([[14, 51, 71, 92]])
b.xywh()
[[14, 51, 57, 41, 'item']]

Performs all checks for the validity of the box.

b.valid
True

Bx is inherited by all other types in pybx: BaseBx, MultiBx, ListBx, JsonBx, exposing the same properties.

BaseBx works with other types of coordinates too. It accepts the coordinates and label for one anchor box in a list or ndarray format.


source

BaseBx

def BaseBx(
    coords, label:list=None, no_check:bool=False
):

BaseBx is the most primitive form of representing a bounding box. Coordinates and label of a bounding box can be wrapped as a BaseBx using: bbx(coords, label).

:param coords: can be of type list or array representing a single box. - list can be formatted with label: [x_min, y_min, x_max, y_max, label] or without label: [x_min, y_min, x_max, y_max] - array should be a 1-dimensional array of shape (4,)

:param label: a list or str that has the class name or label for the object in the corresponding box.

Works with arrays and lists:

annots[0]
[14, 51, 71, 92]
BaseBx(annots[0])
BaseBx(coords=[[14, 51, 71, 92]], label=['unknown'])
b = BaseBx(annots[0], "flower")
b
BaseBx(coords=[[14, 51, 71, 92]], label=['flower'])
b.coords
[[14, 51, 71, 92]]
b.coords_as_numpy
array([[14, 51, 71, 92]])

Calling the values attribute returns the labels along with the coordinates.

b.values
(#1) [[14, 51, 71, 92, 'flower']]

A short cut function that calles BaseBx with a list of coordinates.


source

bbx

def bbx(
    coords:NoneType=None, label:NoneType=None, no_check:bool=False
):

Alias of the BaseBx class using lists.

Remember that BaseBx can only have one box coordinate and label at a time.

annots_list = [
    [10, 20, 100, 200, "apple"],
    [40, 50, 80, 90, "coke"],
]
annots_list[0]
[10, 20, 100, 200, 'apple']
bbx(annots_list[0])
BaseBx(coords=[[10, 20, 100, 200]], label=['apple'])

Boxes with errors will not be initialized.

try:
    bbx([10, 20, 100, -1, "apple"])
except AssertionError as A:
    print(A)
Failed `perform_box_checks` for bounding box coordinates [10, 20, 100, -1, 'apple']
bbx(annots_list[0][:4])  # if label is not passed
BaseBx(coords=[[10, 20, 100, 200]], label=['unknown'])

BaseBx also exposes a method to calculate the Intersection Over Union (IOU) using the intersection_box method

intersection_box([10, 10, 100, 100], [10, 10, 150, 150])
array([ 10,  10, 100, 100])

source

BaseBx.iou

def iou(
    other
):

Caclulates the Intersection Over Union (IOU) of the box w.r.t. another BaseBx. Returns the IOU only if the box is considered valid, ie non-zero area.

b
BaseBx(coords=[[14, 51, 71, 92]], label=['flower'])
b2 = bbx(annots_list[1])
b2
BaseBx(coords=[[40, 50, 80, 90]], label=['coke'])
b.iou(b2)
0.4432
b.iou(b)
1.0

BaseBx is also pseudo-iterable (calling an iterator returns self itself and not the coordinates or labels).


source

BaseBx.__next__

def __next__():

Iteration is allowed only for valid boxes


source

BaseBx.__getitem__

def __getitem__(
    idx
):

Gets the item at index idx as a BaseBx.


source

BaseBx.__iter__

def __iter__():

Iterates through the boxes in BaseBx where self.valid is True.

b = BaseBx(annots[0], "flower")
next(b)
BaseBx(coords=[[14, 51, 71, 92]], label=['flower'])
b = BaseBx(annots_list[0])
for b_ in b:
    print(b_)
BaseBx(coords=[[10, 20, 100, 200]], label=['apple'])

Working with json strings.

json_bbox
'{"x_min": 14, "y_min": 51, "x_max": 71, "y_max": 92, "label": "item"}'

source

jbx

def jbx(
    coords:NoneType=None, label:NoneType=None, no_check:bool=False
):

Alias of the BaseBx class using json strings.

jbx(json_bbox)  # json string
BaseBx(coords=[[14, 51, 71, 92]], label=['item'])
jbx(json_bbox_file)  # read from file as python object
BaseBx(coords=[[130, 63, 225, 180]], label=['clock'])

Working with dicts.

dict_bbox
{'x_min': 14, 'y_min': 51, 'x_max': 71, 'y_max': 92, 'label': 'item'}

source

dbx

def dbx(
    coords:NoneType=None, label:NoneType=None, no_check:bool=False
):

Alias of the BaseBx class using dict.

dbx(dict_bbox)
BaseBx(coords=[[14, 51, 71, 92]], label=['item'])

To make it work with other types, need to infer the datatype. Basically need to call the parse_X function based on the datatype, where X=datatype.

ALL_ITER_TYPES
(numpy.ndarray,
 list,
 fastcore.foundation.L,
 tuple,
 dict,
 typing.Dict[str, str])
ITER_TYPE_JSON
typing.Dict[str, str]

source

parse_basebx

def parse_basebx(
    b:BaseBx, no_check:NoneType=None
):

Reads the attribute of a BaseBx

parse_basebx(b)
([[10, 20, 100, 200]], ['apple'])
parse_basebx(b, no_check=False)
/var/folders/sl/tgj9ztc51hz_d2yt8kg97f840000gn/T/ipykernel_15739/3032591698.py:8: UserWarning: no_check=False passed to parse_basebx: are you sure you want to do this?
  warnings.warn(
([[10, 20, 100, 200]], ['apple'])

source

infer_box_dtype

def infer_box_dtype(
    b:(<class 'numpy.ndarray'>, <class 'list'>, <class 'fastcore.foundation.L'>, <class 'tuple'>, <class 'dict'>),
    **kwargs
):

Call self as a function.

(
    infer_box_dtype(json_bbox),
    infer_box_dtype(list_bbox),
    infer_box_dtype(json_bbox_file),
    infer_box_dtype(b),
)
('json', 'list', 'dict', 'basebx')

Parser method to call the correct method based on the type


source

parse_to_bbx

def parse_to_bbx(
    b, label:str='unknown', no_check:bool=False
):

Takes the box and converts it to a BaseBx after inferring the type.

(
    parse_to_bbx(json_bbox),
    parse_to_bbx(list_bbox),
    parse_to_bbx(json_bbox_file),
)
(BaseBx(coords=[[14, 51, 71, 92]], label=['item']),
 BaseBx(coords=[[14, 51, 71, 92]], label=['item']),
 BaseBx(coords=[[130, 63, 225, 180]], label=['clock']))

With no_check, the validity of the boxes are not checked.

parse_to_bbx([0, 0, 0, 0], no_check=True)
BaseBx(coords=[[0, 0, 0, 0]], label=['unknown'])
(
    parse_to_bbx(list_bbox_nolabel),
    parse_to_bbx(list_bbox_nolabel, ["item"]),
    parse_to_bbx(list_bbox_nolabel, "item"),
)
(BaseBx(coords=[[14, 51, 71, 92]], label=['unknown']),
 BaseBx(coords=[[14, 51, 71, 92]], label=['item']),
 BaseBx(coords=[[14, 51, 71, 92]], label=['item']))

Multiple bounding boxes

Working with multiple bounding boxes and annotaions is usually done with the help of MultiBx. MultiBx allows for iteration.

annots_list  # a good candidate for Multibox
[[10, 20, 100, 200, 'apple'], [40, 50, 80, 90, 'coke']]

It could be imagined as such a thing, maybe thats all we need. But maybe it can be made prettier.

primitive_mbx = [bbx(a) for a in annots_list]
primitive_mbx
[BaseBx(coords=[[10, 20, 100, 200]], label=['apple']),
 BaseBx(coords=[[40, 50, 80, 90]], label=['coke'])]
primitive_mbx[0].iou(primitive_mbx[1])
0.0988
is_listy(annots), is_listy(annots[0])
(True, True)

source

MultiBx

def MultiBx(
    coords, label:list=None, no_check:bool=False
):

MultiBx represents a collection of bounding boxes as lists. Objects of type MultiBx can be indexed into, which returns a BaseBx exposing a suite of box-bound operations. Multiple coordinates and labels of bounding boxes can be wrapped as a MultiBx using: mbx(coords, label). :param coords: can be nested coordinates of type list of lists/json strings (lists of dicts)/ndarrays representing multiple boxes. If passing a list/json each index of the object should be of the following formats: - list can be formatted with label: [x_min, y_min, x_max, y_max, label] or without label: [x_min, y_min, x_max, y_max] - dict should be in pascal_voc format using the keys {“x_min”: 0, “y_min”: 0, “x_max”: 1, “y_max”: 1, “label”: ‘none’} If passing an ndarray, it should be of shape (N,4). Coordinates are stored uniformly as numeric (N, 4) rows; embedded labels are separated.

:param label: a list of strs that has the class name or label for the object in the corresponding box.

Generate random coordinates:

np.random.seed(42)
annots = [sorted([np.random.randint(100) for i in range(4)]) for j in range(3)]
annots
[[14, 51, 71, 92], [20, 60, 82, 86], [74, 74, 87, 99]]

All annotations are stored as a BaseBx in a container called MultiBx

bxs = MultiBx(annots, ["apple", "coke", "tree"])
bxs
MultiBx(coords: 3, labels: 3)
bxs.coords_as_numpy
array([[14, 51, 71, 92],
       [20, 60, 82, 86],
       [74, 74, 87, 99]])

If no labels are passed, unknown is assigned.

bxs = MultiBx(annots)
bxs
MultiBx(coords: 3, labels: 3)

If a single bounding box is passed, AssertionError is NOT rasied anymore. The previous philosophy was that a single annotaiton BaseBx cannot be a MultiBx. It is upto the user to provide a list of list and not a list. There are better checks with get_bx.

try:
    b = MultiBx([annots[0]])
    print(f"box {annots[0]} is {b}")
except AssertionError as A:
    print(A)
box [14, 51, 71, 92] is MultiBx(coords: 1, labels: 1)

Each index reveals the stored coordinate as a BaseBx

bxs[0], bxs.shape
(BaseBx(coords=[[14, 51, 71, 92]], label=['unknown']), (3, 4))
bxs.label
['unknown', 'unknown', 'unknown']

They can also be iterated:

next(bxs)
BaseBx(coords=[[14, 51, 71, 92]], label=['unknown'])

Or using list comprehension, properties of individual boxes can be extracted

[b.area for b in bxs]
[1612, 325]
bxs[0].valid
True
bxs[1].yolo()
[[51.0, 73.0, 62.0, 26.0, 'unknown']]
bxs[0].area
2337
annots_json = json.load(open("../data/annots.json"))
annots_json
[{'x_min': 130, 'y_min': 63, 'x_max': 225, 'y_max': 180, 'label': 'clock'},
 {'x_min': 13, 'y_min': 158, 'x_max': 90, 'y_max': 213, 'label': 'frame'}]

If lists of lists are passed.

list_bboxes = annots
list_bboxes
[[14, 51, 71, 92], [20, 60, 82, 86], [74, 74, 87, 99]]
mbx_list = MultiBx(list_bboxes)
mbx_list
MultiBx(coords: 3, labels: 3)
mbx_list[0]
BaseBx(coords=[[14, 51, 71, 92]], label=['unknown'])

If dicts are passed.

dict_bboxes = (
    {"x_min": 14, "y_min": 51, "x_max": 71, "y_max": 92, "label": "item"},
    {
        "x_min": 20,
        "y_min": 30,
        "x_max": 50,
        "y_max": 90,
        "label": "apple",
    },
)
mbx_dict = MultiBx(dict_bboxes)
mbx_dict
MultiBx(coords: 2, labels: 2)
mbx_dict[0]
BaseBx(coords=[[14, 51, 71, 92]], label=['item'])

If json strings are passed.

json_bboxes = (
    '{"x_min": 14, "y_min": 51, "x_max": 71, "y_max": 92, "label": "item"}',
    '{"x_min": 20, "y_min": 30, "x_max": 50, "y_max": 90, "label": "apple"}',
)
mbx_json = MultiBx(json_bboxes)
mbx_json
MultiBx(coords: 2, labels: 2)
mbx_json[0]
BaseBx(coords=[[14, 51, 71, 92]], label=['item'])
json_bboxes_file = json.load(open("../data/annots.json"))
json_bboxes_file
[{'x_min': 130, 'y_min': 63, 'x_max': 225, 'y_max': 180, 'label': 'clock'},
 {'x_min': 13, 'y_min': 158, 'x_max': 90, 'y_max': 213, 'label': 'frame'}]
mbx_json_file = MultiBx(json_bboxes_file)
mbx_json_file
MultiBx(coords: 2, labels: 2)
mbx_json_file[0], mbx_json_file[1]
(BaseBx(coords=[[130, 63, 225, 180]], label=['clock']),
 BaseBx(coords=[[13, 158, 90, 213]], label=['frame']))

Also accepts keys (for the dict) as a list, otherwise uses voc_keys.

voc_keys
['x_min', 'y_min', 'x_max', 'y_max', 'label']

Defining shortcut function to process lists and dicts in MultiBx.


source

mbx

def mbx(
    coords:NoneType=None, label:NoneType=None, no_check:bool=False
):

Alias of the MultiBx class.

annots_list = annots
annots_list
[[14, 51, 71, 92], [20, 60, 82, 86], [74, 74, 87, 99]]
mbx_list = mbx(annots_list)
mbx_list.coords
[[14, 51, 71, 92], [20, 60, 82, 86], [74, 74, 87, 99]]

When the annotation file contains the key label

annots_json = json.load(open("../data/annots.json"))
annots_json
[{'x_min': 130, 'y_min': 63, 'x_max': 225, 'y_max': 180, 'label': 'clock'},
 {'x_min': 13, 'y_min': 158, 'x_max': 90, 'y_max': 213, 'label': 'frame'}]

Still preserves the original format

mbx_json_ = mbx(annots_json)
mbx_json.coords
('{"x_min": 14, "y_min": 51, "x_max": 71, "y_max": 92, "label": "item"}',
 '{"x_min": 20, "y_min": 30, "x_max": 50, "y_max": 90, "label": "apple"}')
# known issue
mbx_json.label
['unknown', 'unknown']
mbx_json[1]  # after indexing the 1st index, the label gets assigned to multibx
BaseBx(coords=[[20, 30, 50, 90]], label=['apple'])

get_bx

When in doubt, use get_bx.

ITER_TYPES
(numpy.ndarray, list, fastcore.foundation.L)

source

get_bx

def get_bx(
    coords, label:NoneType=None, no_check:bool=False
):

Create a BaseBx or MultiBx from supported coordinates.

Args: coords: A box or collection of boxes represented by lists, tuples, dictionaries, JSON strings, NumPy arrays, or existing Bx objects. label: An optional label or list of labels corresponding to the boxes. no_check: Skip coordinate validation for trusted internal data.

Returns: A BaseBx for one box or MultiBx for a collection of boxes.

Raises: ValueError: If coordinates are empty or malformed. NotImplementedError: If the coordinate representation is unsupported.

get_bx runs a bunch of if-else statements to call the right module when in doubt.

annots_json
[{'x_min': 130, 'y_min': 63, 'x_max': 225, 'y_max': 180, 'label': 'clock'},
 {'x_min': 13, 'y_min': 158, 'x_max': 90, 'y_max': 213, 'label': 'frame'}]
get_bx(annots_json)
MultiBx(coords: 2, labels: 2)
len(annots_json[0])
5
get_bx([annots_json[0]])
MultiBx(coords: 1, labels: 1)
get_bx(annots_list)
MultiBx(coords: 3, labels: 3)
get_bx([0, 1, 1, 2, "juice"])  # boxes should be atleast of 1px length
BaseBx(coords=[[0, 1, 1, 2]], label=['juice'])

The below doesnt fail immediately but fails upon indexing into the box

get_bx([[0, 1, 1, 1]])  # two brackets mean we have a multibx now
MultiBx(coords: 1, labels: 1)

Coming back to the original problem of the users burden in deciding which function to call:

try:
    b = MultiBx([annots[0]])
    print(f"box {annots[0]} is {b}")
except AssertionError as A:
    print(A)

get_bx addresses this the following way:

mbx(annots[0])  # a single coordinate becomes a one-item MultiBx
MultiBx(coords: 4, labels: 4)
get_bx(annots[0])
BaseBx(coords=[[14, 51, 71, 92]], label=['unknown'])
get_bx([annots[0]])
MultiBx(coords: 1, labels: 1)

The addition operation stacks the bounding boxes.


source

MultiBx.__add__

def __add__(
    other
):

Pseudo-add method that stacks the provided boxes and labels. Stacking two boxes imply that the resulting box is a MultiBx: MultiBx + MultiBx = MultiBx. Same as basics.stack_bxs().


source

BaseBx.__add__

def __add__(
    other
):

Pseudo-add method that stacks the provided boxes and labels. Stacking two boxes imply that the resulting box is a MultiBx: BaseBx + BaseBx = MultiBx. This violates the idea of BaseBx since the result holds more than 1 coordinate/label for the box. From v.2.0, a UserWarning is issued if called. Recommended use is either: BaseBx + BaseBx = MultiBx or basics.stack_bxs().


source

add_bxs

def add_bxs(
    b1, b2
):

Alias of stack_bxs().


source

stack_bxs

def stack_bxs(
    b1, # Anchor box coordinates Bx
    b2, # Anchor box coordinates Bx
): # Stacked anchor box coordinates of MultiBx type.

Method to stack two Bx-types together. Similar to __add__ of BxTypes but avoids UserWarning. :param b1: :param b2: :return: summary

b
MultiBx(coords: 1, labels: 1)

Internally this is what is done to stack them:

bxs.coords + b.coords, bxs.label + b.label
([[14, 51, 71, 92], [20, 60, 82, 86], [74, 74, 87, 99], [14, 51, 71, 92]],
 ['unknown', 'unknown', 'unknown', 'unknown'])
bxs + b
MultiBx(coords: 4, labels: 4)

Adding a MultiBx to a BaseBx makes the new set of coordinates a MultiBx, so a BxViolation warning is issued if this was not intended.

b + bxs
MultiBx(coords: 4, labels: 4)
stack_bxs(b, bxs)
MultiBx(coords: 4, labels: 4)

To avoid the BxViolation, use the method stack_bxs.

stack_bxs(bxs, b)
MultiBx(coords: 4, labels: 4)

source

stack_bxs_inplace

def stack_bxs_inplace(
    b, *args
):

Stack the passed boxes on top of the first item.

boxes = [
    BaseBx(coords=[200, 100, 300, 200], label=["a_3x3_1.0_5"]),
    BaseBx(coords=[214, 79, 285, 220], label=["a_3x3_0.5_5"]),
    BaseBx(coords=[222, 58, 277, 241], label=["a_3x3_0.3_5"]),
]
stacked_bxs = stack_bxs_inplace(*boxes)
stacked_bxs
MultiBx(coords: 3, labels: 3)
stacked_bxs.coords
[[200, 100, 300, 200], [214, 79, 285, 220], [222, 58, 277, 241]]
len(stacked_bxs)
3

BaseBx also supports calculation of bounding box offset by calling the get_offset() method.

b1 = b
b2 = BaseBx(
    [18, 44, 71, 90], "probably_flower"
)  # simulating the case where we might have a bbox prediction
b1, b2
(MultiBx(coords: 1, labels: 1),
 BaseBx(coords=[[18, 44, 71, 90]], label=['probably_flower']))
(b.coords_as_numpy - b2.coords_as_numpy)
array([[-4,  7,  0,  2]])
(b.coords_as_numpy - b2.coords_as_numpy) / np.tile([2, 1], 2)
array([[-2.,  7.,  0.,  2.]])

source

BaseBx.get_offset

def get_offset(
    other:BaseBx, normalize:bool=True, log_func:ufunc=log, sigma:tuple=(0.1, 0.2), self_is_anchor:bool=False
):

Caclulates the offset of the box I with another box O. The most basic calculation of offset involves a) taking the distance between the centers: I_cx - O_cx, I_cy - O_cy. b) taking the ratio of the two boxes: I_w/Ow, I_h/O_h.

If normalize=True, the center distances and ratios are normalized as per https://arxiv.org/pdf/1512.02325.pdf (I_cx - O_cx)/O_w, (I_cy - O_cy)/O_h, log(I_w/Ow), log(I_h/O_h) These are further scaled with an appoximation of standard deviation for the distances and ratios ((I_cx - O_cx)/O_w)/sigma_c, ((I_cy - O_cy)/O_h)/sigma_c, log(I_w/Ow)/sigma_r, log(I_h/O_h)/sigma_r

Args: other (BaseBx): Any supported type of bounding box format, even takes a list of coordinates. Typically the anchor box. normalize (bool, optional): Whether to normalize the offsets. Defaults to True. log_func (func, optional): Function for normalizing the ratio of widths and heights. Defaults to np.log. sigma (tuple, optional): Estimated of standard deviation for the distances and ratios. Defaults to (0.1, 0.2). self_is_anchor (bool, optional): Typically other is assumed to be the anchor box, this flag tells that this assumption is False. Defaults to False.

Returns: list: Offsets of the two bounding boxes

np.repeat([1, 2], 2)
array([1, 1, 2, 2])
isinstance(b, BaseBx)
False
b, b2
(MultiBx(coords: 1, labels: 1),
 BaseBx(coords=[[18, 44, 71, 90]], label=['probably_flower']))

Index into the multibx to make the offset calculation correct.

b[0].get_offset(b2, normalize=False)
(#4) [-2.0,4.5,57.0,41.0]
b[0].get_offset(b2, normalize=True)
(#4) [-0.3774,0.9783,0.3638,-0.5753]

To make sure consistency with the reverse case where self is the ground truth anchor box and ground truth bounding box is the other box, use the self_is_anchor flag.

b2.get_offset(b[0], normalize=True, self_is_anchor=True)  # correct
(#4) [-0.3774,0.9783,0.3638,-0.5753]

If self_is_anchor=False flag is not turned on, when ground truth bounding box is the other box, the latter can lead to a systematic bug.

b2.get_offset(b[0], normalize=True)  # not correct
(#4) [0.3509,-1.0976,-0.3638,0.5753]