Basic objects

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

Anchor 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

Bx

 Bx (coords, label:list=None)

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.

Bx(annots[0])
Bx(coords=[[14, 51, 71, 92]], label=[])

So the correct way to do it would be to pass a list of list.

annots
[[14, 51, 71, 92]]
b = Bx(annots)
b
Bx(coords=[[14, 51, 71, 92]], label=[])
len(b)
1
b.cx
42.5
b.yolo()
(#1) [[42.5, 71.5, 57, 41]]

To get normalized coordinates wrt to the image dimensions.

b.yolo(224, 224, normalize=True)
(#1) [[0.18973214285714285, 0.31919642857142855, 0.2544642857142857, 0.18303571428571427]]
b.values
(#1) [[14, 51, 71, 92]]

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

 BaseBx (coords, label:list=None)

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:

BaseBx(annots)
BaseBx(coords=[[14, 51, 71, 92]], label=[])
b = BaseBx(annots, 'flower')
b
BaseBx(coords=[[14, 51, 71, 92]], label=['flower'])
b.coords
[[14, 51, 71, 92]]

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

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

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


source

BaseBx.iou

 BaseBx.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.

b
BaseBx(coords=[[14, 51, 71, 92]], label=['flower'])

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

b = BaseBx(annots, 'flower')
next(b)
BaseBx(coords=[[14, 51, 71, 92]], label=['flower'])
for b_ in b:
    print(b_)

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


source

MultiBx

 MultiBx (coords, label:list=None)

MultiBx represents a collection of bounding boxes as ndarrays. 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 records (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).

: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=[[14, 51, 71, 92], [20, 60, 82, 86], [74, 74, 87, 99]], label=['apple', 'coke', 'tree'])

Each index reveals the stored coordinate as a BaseBx

bxs[0]
BaseBx(coords=[[14, 51, 71, 92]], label=['apple'])

They can also be iterated:

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

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

[b.area for b in bxs]
[2337, 1612, 325]
bxs[0].valid
True
[True, True]
bxs[1].yolo()
(#1) [[51.0, 73.0, 62, 26]]
bxs[0].area
2337
[b_.area for b_ in bxs]
[1612, 325]

Extending BaseBx to also accept (json, dict) formatted coordinates and labels.


source

jbx

 jbx (coords=None, labels=None, keys=None)

Alias of the JsonBx class to process json records into MultiBx or BaseBx objects exposing many validation methods

Also accepts keys as a list, otherwise uses voc_keys.

annots = json.load(open('../data/annots.json'))
annots
[{'x_min': 150, 'y_min': 70, 'x_max': 270, 'y_max': 220, 'label': 'clock'},
 {'x_min': 10, 'y_min': 180, 'x_max': 115, 'y_max': 260, 'label': 'frame'}]
jbx(annots, keys=voc_keys)
__JsonBx(coords=[[150, 70, 270, 220], [10, 180, 115, 260]], label=['clock', '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']

Making MultiBx work with lists with more than 4 items. It is a common practice to have the class label along with the coordinates. This classmethod is useful in such situations

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

source

lbx

 lbx (coords=None, labels=None)

Alias of the __ListBx class to process list into MultiBx or BaseBx objects exposing many validation methods

annots = [[10, 20, 100, 200, 'apple'], [40, 50, 80, 90, 'coke'], ]
annots
[[10, 20, 100, 200, 'apple'], [40, 50, 80, 90, 'coke']]
lbx(annots)
__ListBx(coords=[[10, 20, 100, 200], [40, 50, 80, 90]], label=['apple', 'coke'])
lbx(annots)[0]
BaseBx(coords=[[10, 20, 100, 200]], label=['apple'])

Inserting classmethod to process lists and dicts in MultiBx.


source

mbx

 mbx (coords=None, label=None, keys=None)

Alias of the MultiBx class.


source

MultiBx.multibox

 MultiBx.multibox (coords, label:list=None, keys:list=None)

Classmethod for MultiBx. Same as mbx(coords, label). Calls classmethods of JsonBx and ListBx based on the type of coords passed.

annots_list = annots
annots_list
[[10, 20, 100, 200, 'apple'], [40, 50, 80, 90, 'coke']]
annots_json = json.load(open('../data/annots.json'))
annots_json
[{'x_min': 150, 'y_min': 70, 'x_max': 270, 'y_max': 220, 'label': 'clock'},
 {'x_min': 10, 'y_min': 180, 'x_max': 115, 'y_max': 260, 'label': 'frame'}]

How the class method works:

t = explode_types(annots_list)  # get all types
t
{list: [{list: [int, int, int, int, str]}, {list: [int, int, int, int, str]}]}
t[list][0]  # index into the nested list and call the right class
{list: [int, int, int, int, str]}
mbx(annots_json)
MultiBx(coords=[[150, 70, 270, 220], [10, 180, 115, 260]], label=['clock', 'frame'])
mbx(annots_list)
MultiBx(coords=[[10, 20, 100, 200], [40, 50, 80, 90]], label=['apple', 'coke'])
mbx(annots_json[0])
MultiBx(coords=[[150, 70, 270, 220]], label=['clock'])

Checking if it works with ndarrays

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

Allowing BaseBx to process a single dict and list directly.


source

bbx

 bbx (coords=None, labels=None, keys=['x_min', 'y_min', 'x_max', 'y_max',
      'label'])

Alias of the BaseBx class.


source

BaseBx.basebx

 BaseBx.basebx (coords, label:list=None, keys:list=['x_min', 'y_min',
                'x_max', 'y_max', 'label'])

Classmethod for BaseBx. Same as bbx(coords, label), made to work with other object types other than ndarray.

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']]

What does make_single_iterable do? It converts a single list or dict of coordinates into an iterable list that can be used by BaseBx.

annots_list[0]
[10, 20, 100, 200, 'apple']
make_single_iterable(annots_list[0], keys=voc_keys)
((#1) [[10, 20, 100, 200]], ['apple'])

The class method makes it easier to directly call BaseBx without making the coordinates a list of list.

bbx(annots_list[0])
BaseBx(coords=[[10, 20, 100, 200]], label=['apple'])
annots_list[0][:-1]
[10, 20, 100, 200]
bbx(annots_list[0][:-1])  # if label is not passed
BaseBx(coords=[[10, 20, 100, 200]], label=[])
bbx(annots_json[0])
BaseBx(coords=[[150, 70, 270, 220]], label=['clock'])

get_bx

When in doubt, use get_bx.

ITER_TYPES
(numpy.ndarray, list, fastcore.foundation.L)
/mnt/data/projects/pybx/.venv/lib/python3.7/site-packages/fastcore/docscrape.py:225: UserWarning: Unknown section Raises
  else: warn(msg)

source

get_bx

 get_bx (coords, label=None)

Helper function to check and call the correct type of Bx instance.

Checks for the type of data passed and calls the respective class to generate a Bx instance. Currently only supports ndarray, list, dict, tuple, nested list, nested tuple.

Type Default Details
coords ndarray, list, dict, tuple, nested list, nested tuple Coordinates of anchor boxes.
label NoneType None Labels for anchor boxes in order, by default None
Returns Bx An instance of MultiBx, ListBx, BaseBx or JsonBx

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

annots_json
[{'x_min': 150, 'y_min': 70, 'x_max': 270, 'y_max': 220, 'label': 'clock'},
 {'x_min': 10, 'y_min': 180, 'x_max': 115, 'y_max': 260, 'label': 'frame'}]
get_bx(annots_json)
MultiBx(coords=[[150, 70, 270, 220], [10, 180, 115, 260]], label=['clock', 'frame'])
len(annots_json[0])
5
get_bx([annots_json[0]])
MultiBx(coords=[[150, 70, 270, 220]], label=['clock'])
get_bx(annots_list)
MultiBx(coords=[[10, 20, 100, 200], [40, 50, 80, 90]], label=['apple', 'coke'])
get_bx([0, 1, 0, 1])
BaseBx(coords=[[0, 1, 0, 1]], label=[])

Enabling stacking of different boxes.


source

add_bxs

 add_bxs (b1, b2)

Alias of stack_bxs().


source

stack_bxs

 stack_bxs (b1, b2)

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

Type Details
b1 Bx, MultiBx Anchor box coordinates Bx
b2 Bx, MultiBx Anchor box coordinates Bx
Returns MultiBx Stacked anchor box coordinates of MultiBx type.
b
BaseBx(coords=[[14, 51, 71, 92]], label=['flower'])

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]],
 (#4) ['apple','coke','tree','flower'])
bxs + b
MultiBx(coords=[[14, 51, 71, 92], [20, 60, 82, 86], [74, 74, 87, 99], [14, 51, 71, 92]], label=['apple', 'coke', 'tree', 'flower'])

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
/home/gg/data/pybx/.venv/lib/python3.7/site-packages/ipykernel_launcher.py:19: BxViolation: Change of object type imminent if trying to add <class '__main__.BaseBx'>+<class '__main__.MultiBx'>. Use <class '__main__.MultiBx'>+<class '__main__.BaseBx'> instead or basics.stack_bxs().
MultiBx(coords=[[14, 51, 71, 92], [14, 51, 71, 92], [20, 60, 82, 86], [74, 74, 87, 99]], label=['flower', 'apple', 'coke', 'tree'])
stack_bxs(b, bxs)
MultiBx(coords=[[14, 51, 71, 92], [14, 51, 71, 92], [20, 60, 82, 86], [74, 74, 87, 99]], label=['flower', 'apple', 'coke', 'tree'])

To avoid the BxViolation, use the method stack_bxs.

stack_bxs(bxs, b)
MultiBx(coords=[[14, 51, 71, 92], [20, 60, 82, 86], [74, 74, 87, 99], [14, 51, 71, 92]], label=['apple', 'coke', 'tree', 'flower'])