import json
from fastcore.test import test_eq, test_fail, test_warns, ExceptionExpectedBasic objects
Check validity of boxes
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).dtypedtype('int64')
check_format_types(np.array(list_bbox_nolabel), verbose=True)Passed `check_format_types`.
True
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
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.
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.
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']
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}'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'])
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])
bBx(coords=[[14, 51, 71, 92]], label=['unknown'])
b.n_points4
len(b)1
b.cx42.5
If label is passed along with coords, this is used.
b = Bx(annots[0], label="item")
bBx(coords=[[14, 51, 71, 92]], label=['item'])
Can be a list of single item name also.
b = Bx(annots[0], label=["item"])
bBx(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_numpyarray([[14, 51, 71, 92]])
b.xywh()[[14, 51, 57, 41, 'item']]
Performs all checks for the validity of the box.
b.validTrue
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.
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")
bBaseBx(coords=[[14, 51, 71, 92]], label=['flower'])
b.coords[[14, 51, 71, 92]]
b.coords_as_numpyarray([[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.
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 passedBaseBx(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])
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.
bBaseBx(coords=[[14, 51, 71, 92]], label=['flower'])
b2 = bbx(annots_list[1])
b2BaseBx(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).
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"}'
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 stringBaseBx(coords=[[14, 51, 71, 92]], label=['item'])
jbx(json_bbox_file) # read from file as python objectBaseBx(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'}
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_JSONtyping.Dict[str, str]
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'])
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
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)
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"])
bxsMultiBx(coords: 3, labels: 3)
bxs.coords_as_numpyarray([[14, 51, 71, 92],
[20, 60, 82, 86],
[74, 74, 87, 99]])
If no labels are passed, unknown is assigned.
bxs = MultiBx(annots)
bxsMultiBx(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].validTrue
bxs[1].yolo()[[51.0, 73.0, 62.0, 26.0, 'unknown']]
bxs[0].area2337
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_listMultiBx(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_dictMultiBx(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_jsonMultiBx(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_fileMultiBx(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.
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 multibxBaseBx(coords=[[20, 30, 50, 90]], label=['apple'])
get_bx
When in doubt, use get_bx.
ITER_TYPES(numpy.ndarray, list, fastcore.foundation.L)
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 lengthBaseBx(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 nowMultiBx(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 MultiBxMultiBx(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.
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().
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().
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
bMultiBx(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 + bMultiBx(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 + bxsMultiBx(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)
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_bxsMultiBx(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.]])
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]