pilbox / boxer.py
John Ho
Add Annotate Video tab: per-frame box + mask annotation over video
c4e47f4
Raw
History Blame Contribute Delete
7.45 kB
###############################################################################
# Home of all Boudning Box Utility Functions
###############################################################################
import numpy as np
# Supported bounding-box conventions, all converted to pascal_voc {x0,y0,x1,y1}.
BBOX_FORMATS = ("pascal_voc", "albumentations", "coco", "coco_normalized")
def to_pascal_voc(coords, fmt: str, im_w: int, im_h: int) -> dict:
"""Convert a bounding box in any supported format to pascal_voc.
All formats collapse to the pascal_voc convention — a dict of absolute
top-left/bottom-right pixels ``{x0, y0, x1, y1}`` (matching :func:`get_bbox_dict`
and ``pilbox``). Given four ordered values ``(c0, c1, c2, c3)``:
- ``pascal_voc``: ``(x0, y0, x1, y1)`` absolute pixels.
- ``albumentations``: ``(x0, y0, x1, y1)`` normalized to ``[0, 1]``.
- ``coco``: ``(x0, y0, w, h)`` absolute pixels (top-left + size).
- ``coco_normalized``: ``(x0, y0, w, h)`` normalized to ``[0, 1]``.
Args:
coords: The four box values ``(c0, c1, c2, c3)`` in the given ``fmt``.
fmt: One of :data:`BBOX_FORMATS`.
im_w: Reference image width in pixels (for the normalized formats).
im_h: Reference image height in pixels (for the normalized formats).
Returns:
``{"x0", "y0", "x1", "y1"}`` of absolute integer pixels.
Raises:
ValueError: If ``fmt`` is not one of :data:`BBOX_FORMATS`.
>>> to_pascal_voc((10, 20, 40, 60), "pascal_voc", 100, 100)
{'x0': 10, 'y0': 20, 'x1': 40, 'y1': 60}
>>> to_pascal_voc((0.1, 0.2, 0.4, 0.6), "albumentations", 100, 200)
{'x0': 10, 'y0': 40, 'x1': 40, 'y1': 120}
>>> to_pascal_voc((10, 20, 30, 40), "coco", 100, 100)
{'x0': 10, 'y0': 20, 'x1': 40, 'y1': 60}
>>> to_pascal_voc((0.1, 0.2, 0.3, 0.4), "coco_normalized", 100, 200)
{'x0': 10, 'y0': 40, 'x1': 40, 'y1': 120}
"""
c0, c1, c2, c3 = coords
if fmt == "pascal_voc":
return {"x0": int(c0), "y0": int(c1), "x1": int(c2), "y1": int(c3)}
if fmt == "albumentations":
return {
"x0": int(c0 * im_w),
"y0": int(c1 * im_h),
"x1": int(c2 * im_w),
"y1": int(c3 * im_h),
}
if fmt == "coco":
return get_bbox_dict(c0, c1, c2, c3)
if fmt == "coco_normalized":
return get_bbox_dict(c0, c1, c2, c3, im_wh=(im_w, im_h))
raise ValueError(f"unknown bbox format {fmt!r}; expected one of {list(BBOX_FORMATS)}")
def get_bbox_dict(x, y, width, height, im_wh = None)-> dict:
'''given top-left (x,y), return a bbox dict with absolute coordinates x0, y0, x1, y1
Args:
im_width: if provided, treats x,y,w,h as relative coordinates
im_height: if provided, treats x,y,w,h as relative coordinates
'''
is_relative = type(im_wh) != type(None)
if is_relative:
w,h = im_wh
return {'x0': int(x * w) if is_relative else int(x),
'y0': int(y * h) if is_relative else int(y),
'x1': int((x+width) * w) if is_relative else int(x + width),
'y1': int((y+height) * h) if is_relative else int(y + height)}
def bbox_rebase_xy(x,y,w,h, to_yolo_format = False):
'''convert bbox format frorm yolo (centroid) to standard (top-left) or vice versa
'''
x = x+w/2 if to_yolo_format else x - w/2
y = y+h/2 if to_yolo_format else y - h/2
return {'x': x, 'y': y, 'w': w, 'h': h}
def bbox_get_xywh(x0,y0,x1,y1):
'''returns dict of x,y,w,h
'''
return {'x': x0, 'y': y0, 'w': x1-x0 ,'h': y1-y0}
def bbox_convert(x0, y0, x1, y1, width, height):
'''convert bounding box from relative to absolute and vice versa
Args:
width: reference image's width
height: reference image's height
'''
if all([i <= 1 for i in [x0,y0,x1,y1]]):
# relative to absolute
return {
'x0': int(x0 * width), 'x1': int(x1 * width),
'y0': int(y0 * height), 'y1': int(y1 * height)
}
else:
# absolute to relative
if x0 > width or x1> width:
raise ValueError(f'{x0} or {x1} is greater than width: {width}')
if y0 > height or y1> height:
raise ValueError(f'{y0} or {y1} is greater than height: {height}')
return {
'x0': x0 / width, 'x1': x1 / width,
'y0': y0 / height, 'y1': y1 / height
}
def bboxes_to_im_mask(l_bboxes, im_wh):
''' return a binary mask given a list of bounding boxes
'''
mask = np.zeros(shape = (im_wh[::-1]), dtype = np.uint8)
for bbox in l_bboxes:
mask[bbox['y0']: bbox['y1'], bbox['x0']: bbox['x1']] = 1
return mask
def bbox_intersects(bbox_a, bbox_b):
'''return True if two pascal_voc boxes overlap.
Uses the standard axis-aligned overlap test rather than corner-in-rect
checks: the latter miss "cross" overlaps where the boxes intersect but no
corner of either lies inside the other (e.g. a tall thin box crossing a
short wide one).
>>> a = {'x0': 0, 'y0': 0, 'x1': 10, 'y1': 10}
>>> bbox_intersects(a, {'x0': 5, 'y0': 5, 'x1': 15, 'y1': 15})
True
>>> bbox_intersects(a, {'x0': 20, 'y0': 20, 'x1': 30, 'y1': 30})
False
>>> # cross overlap: no corner of either box is inside the other
>>> bbox_intersects({'x0': 4, 'y0': 0, 'x1': 6, 'y1': 10},
... {'x0': 0, 'y0': 4, 'x1': 10, 'y1': 6})
True
'''
return (
bbox_a['x0'] <= bbox_b['x1'] and bbox_a['x1'] >= bbox_b['x0']
and bbox_a['y0'] <= bbox_b['y1'] and bbox_a['y1'] >= bbox_b['y0']
)
def bbox_area(x0, y0, x1, y1):
return (x1-x0+1) * (y1-y0+1)
def get_bbox_iou(bbox_a, bbox_b):
if bbox_intersects(bbox_a, bbox_b):
x_left = max(bbox_a['x0'], bbox_b['x0'])
x_right = min(bbox_a['x1'], bbox_b['x1'])
y_top = max(bbox_a['y0'], bbox_b['y0'])
y_bottom = min(bbox_a['y1'], bbox_b['y1'])
inter_area = bbox_area(x0 = x_left, x1 = x_right, y0 = y_top , y1 = y_bottom)
bbox_a_area = bbox_area(**bbox_a)
bbox_b_area = bbox_area(**bbox_b)
return inter_area / float(bbox_a_area + bbox_b_area - inter_area)
else:
return 0
def boxer(lsXY, pctBuffer = 0.3, lXBounds = None, lYBounds = None):
'''
Create a minimum Bounding Box given a list of x,y coordinates
and a buffer given in percentages of the output image size
if pctBuffer is given as a tuple:
pctBuffer[0] will be the x buffer
pctBuffer[1] will be the y buffer
Optional: Provide XBounds and YBounds to ensure the returned values
fits within range.
'''
minX = minY = float("inf")
maxX = maxY = float("-inf")
for x, y in lsXY:
# set min coords
if x < minX:
minX = x
if y < minY:
minY = y
# set max coords
if x > maxX:
maxX = x
if y > maxY:
maxY = y
width = maxX - minX
height = maxY - minY
if type(pctBuffer) == tuple:
xBuffer = pctBuffer[0]
yBuffer = pctBuffer[1]
else:
xBuffer = yBuffer = pctBuffer
coordsDict ={
'x1': int(minX - xBuffer * width),
'x2': int(maxX + xBuffer * width),
'y1': int(minY - yBuffer * height),
'y2': int(maxY + yBuffer * height)
}
if lXBounds:
coordsDict['x1'] = max( lXBounds[0], coordsDict['x1'])
coordsDict['x2'] = min( lXBounds[1], coordsDict['x2'])
if lYBounds:
coordsDict['y1'] = max( lYBounds[0], coordsDict['y1'])
coordsDict['y2'] = min( lYBounds[1], coordsDict['y2'])
return coordsDict