Source code for bbox_utils.bbox_2d

import numpy as np

from bbox_utils.utils import (
    order_points,
    point_within_dimensions,
    round_np,
    round_scalar,
)


[docs]class BoundingBox: def __init__(self, points, ordered=False): """Create a Bounding Box object Args: points (np.ndarray): an array of form [[x1, y1], [x2, y2], [x3, y3], [x4, y4]]. ordered (bool): whether the points are already sorted """ self.tl, self.tr, self.br, self.bl = ( order_points(points) if not ordered else points )
[docs] def validate_points(self, image_dimension): """Make sure all the bounding box points are within an image's dimensions Args: image_dimension (np.array): array with the image's dimensions in form (rows, cols, depth) Returns: bool: whether the points are valid """ # Convert rows, cols, depth to x, y, z xyz_dimension = np.copy(image_dimension) xyz_dimension[0] = image_dimension[1] xyz_dimension[1] = image_dimension[0] tl_valid = point_within_dimensions(self.tl, xyz_dimension) tr_valid = point_within_dimensions(self.tr, xyz_dimension) br_valid = point_within_dimensions(self.br, xyz_dimension) bl_valid = point_within_dimensions(self.bl, xyz_dimension) return tl_valid and tr_valid and br_valid and bl_valid
[docs] def to_xywh(self): """Returns the top-left point, width, and height Returns: tuple: tuple of form (np.array, float, float) """ return round_np(self.tl), round_scalar(self.width), round_scalar(self.height)
[docs] @staticmethod def from_xywh(top_left, width, height): """Create a bounding box object from the top-left point, width, and height. Args: top_left (np.array): array of form [x, y] width (float): width of the bounding box height (float): height of the bounding box Returns: BoundingBox: a new bounding box instance """ tl = top_left tr = tl + np.array([width, 0.0]) br = tl + np.array([width, height]) bl = tl + np.array([0.0, height]) points = np.array([tl, tr, br, bl]) box = BoundingBox(points, ordered=True) return box
[docs] def to_xyxy(self): """Returns the top-left and bottom-right coordinate Returns: tuple: tuple of form (np.array, np.array) """ return round_np(self.tl), round_np(self.br)
[docs] @staticmethod def from_xyxy(top_left, bottom_right): """Create a bounding box object from the top-left point and bottom-right point Args: top_left (np.array): array of form [x, y] bottom_right ((np.array): array of form [x, y] Returns: BoundingBox: a new bounding box instance """ # First make sure that the top_left and bottom_right are correctly ordered # We don't need to check top_left[1] < bottom_right[1] # because if the x-coordinate is bad, but the y-coordinate is good, # it will throw an assertion later if top_left[0] > bottom_right[0]: # top is below bottom, swap the points temp = bottom_right bottom_right = top_left top_left = temp # Make sure that the top_left is to left of the right point assert ( top_left[1] < bottom_right[1] and top_left[0] < bottom_right[0] ), "Invalid xyxy points: {}, {}".format(top_left, bottom_right) # Make sure all points are positive assert ( top_left[0] >= 0 and top_left[1] >= 0 ), "top_left point has negative value {}".format(top_left) assert ( bottom_right[0] >= 0 and bottom_right[1] >= 0 ), "bottom_right point has negative value {}".format(bottom_right) t_x, t_y = top_left b_x, b_y = bottom_right height = b_y - t_y width = b_x - t_x tr = top_left + np.array([width, 0.0]) bl = top_left + np.array([0.0, height]) points = np.array([top_left, tr, bottom_right, bl]) box = BoundingBox(points, ordered=True) return box
[docs] def to_yolo(self, image_dimension): """Generates a YOLO formatted np.array with center_x, center_y, width, height Args: image_dimension (np.array): array with image dimensions of form (rows, cols) or (rows, cols, depth) Returns: np.array: array with YOLO formatted bounding box """ # Make sure all points are within dimensions assert self.validate_points( image_dimension ), "Some points of the bounding box lie outside of image dimensions" cx, cy = self.center # Get normalized dimensions # (make sure to use rows for height and cols for width) img_h, img_w = image_dimension[:2] cx = float(cx) / img_w cy = float(cy) / img_h w = float(self.width) / img_w h = float(self.height) / img_h assert ( 0.0 <= cx <= 1.0 and 0.0 <= cy <= 1.0 and 0.0 <= w <= 1.0 and 0.0 <= h <= 1.0 ), "All YOLO values should be normalize between [0, 1]." return np.array([cx, cy, w, h])
[docs] @staticmethod def from_yolo(center, width, height, image_dimension): """Create a bounding box object from YOLO formatted data Args: center (np.array): the center coordinate of the box (x, y). Scaled [0, 1] width (float): the width of the bounding box [0, 1] height (float): the height of the bounding box [0, 1] image_dimension (np.array): the dimensions of the image (row, cols) Returns: BoundingBox: a new bounding box instance """ center_x, center_y = center img_height, img_width = image_dimension[:2] cx = center_x * img_width cy = center_y * img_height box_width = width * img_width box_height = height * img_height tl_x = cx - box_width / 2 tl_y = cy - box_height / 2 tl = tl_x, tl_y return BoundingBox.from_xywh(tl, box_width, box_height)
@property def center(self): """Returns the center point of the bounding box as (x, y) Returns: np.array: center of bounding box in (x, y) form. NOT (row, column) form. """ x = self.tl[0] + self.width // 2 y = self.tl[1] + self.height // 2 return round_np(np.array([x, y])) @property def width(self): return np.linalg.norm(self.tl - self.tr) @property def height(self): return np.linalg.norm(self.tl - self.bl) @property def points(self): return np.array([self.tl, self.tr, self.br, self.bl])