|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import math |
|
|
from typing import Tuple |
|
|
|
|
|
import cv2 |
|
|
import numpy as np |
|
|
|
|
|
|
|
|
def bbox_xyxy2xywh(bbox_xyxy: np.ndarray) -> np.ndarray: |
|
|
"""Transform the bbox format from x1y1x2y2 to xywh. |
|
|
|
|
|
Args: |
|
|
bbox_xyxy (np.ndarray): Bounding boxes (with scores), shaped (n, 4) or |
|
|
(n, 5). (left, top, right, bottom, [score]) |
|
|
|
|
|
Returns: |
|
|
np.ndarray: Bounding boxes (with scores), |
|
|
shaped (n, 4) or (n, 5). (left, top, width, height, [score]) |
|
|
""" |
|
|
bbox_xywh = bbox_xyxy.copy() |
|
|
bbox_xywh[:, 2] = bbox_xywh[:, 2] - bbox_xywh[:, 0] |
|
|
bbox_xywh[:, 3] = bbox_xywh[:, 3] - bbox_xywh[:, 1] |
|
|
|
|
|
return bbox_xywh |
|
|
|
|
|
|
|
|
def bbox_xywh2xyxy(bbox_xywh: np.ndarray) -> np.ndarray: |
|
|
"""Transform the bbox format from xywh to x1y1x2y2. |
|
|
|
|
|
Args: |
|
|
bbox_xywh (ndarray): Bounding boxes (with scores), |
|
|
shaped (n, 4) or (n, 5). (left, top, width, height, [score]) |
|
|
Returns: |
|
|
np.ndarray: Bounding boxes (with scores), shaped (n, 4) or |
|
|
(n, 5). (left, top, right, bottom, [score]) |
|
|
""" |
|
|
bbox_xyxy = bbox_xywh.copy() |
|
|
bbox_xyxy[:, 2] = bbox_xyxy[:, 2] + bbox_xyxy[:, 0] |
|
|
bbox_xyxy[:, 3] = bbox_xyxy[:, 3] + bbox_xyxy[:, 1] |
|
|
|
|
|
return bbox_xyxy |
|
|
|
|
|
|
|
|
def bbox_xyxy2cs(bbox: np.ndarray, |
|
|
padding: float = 1.) -> Tuple[np.ndarray, np.ndarray]: |
|
|
"""Transform the bbox format from (x,y,w,h) into (center, scale) |
|
|
|
|
|
Args: |
|
|
bbox (ndarray): Bounding box(es) in shape (4,) or (n, 4), formatted |
|
|
as (left, top, right, bottom) |
|
|
padding (float): BBox padding factor that will be multilied to scale. |
|
|
Default: 1.0 |
|
|
|
|
|
Returns: |
|
|
tuple: A tuple containing center and scale. |
|
|
- np.ndarray[float32]: Center (x, y) of the bbox in shape (2,) or |
|
|
(n, 2) |
|
|
- np.ndarray[float32]: Scale (w, h) of the bbox in shape (2,) or |
|
|
(n, 2) |
|
|
""" |
|
|
|
|
|
dim = bbox.ndim |
|
|
if dim == 1: |
|
|
bbox = bbox[None, :] |
|
|
|
|
|
x1, y1, x2, y2 = np.hsplit(bbox, [1, 2, 3]) |
|
|
center = np.hstack([x1 + x2, y1 + y2]) * 0.5 |
|
|
scale = np.hstack([x2 - x1, y2 - y1]) * padding |
|
|
|
|
|
if dim == 1: |
|
|
center = center[0] |
|
|
scale = scale[0] |
|
|
|
|
|
return center, scale |
|
|
|
|
|
|
|
|
def bbox_xywh2cs(bbox: np.ndarray, |
|
|
padding: float = 1.) -> Tuple[np.ndarray, np.ndarray]: |
|
|
"""Transform the bbox format from (x,y,w,h) into (center, scale) |
|
|
|
|
|
Args: |
|
|
bbox (ndarray): Bounding box(es) in shape (4,) or (n, 4), formatted |
|
|
as (x, y, h, w) |
|
|
padding (float): BBox padding factor that will be multilied to scale. |
|
|
Default: 1.0 |
|
|
|
|
|
Returns: |
|
|
tuple: A tuple containing center and scale. |
|
|
- np.ndarray[float32]: Center (x, y) of the bbox in shape (2,) or |
|
|
(n, 2) |
|
|
- np.ndarray[float32]: Scale (w, h) of the bbox in shape (2,) or |
|
|
(n, 2) |
|
|
""" |
|
|
|
|
|
|
|
|
dim = bbox.ndim |
|
|
if dim == 1: |
|
|
bbox = bbox[None, :] |
|
|
|
|
|
x, y, w, h = np.hsplit(bbox, [1, 2, 3]) |
|
|
center = np.hstack([x + w * 0.5, y + h * 0.5]) |
|
|
scale = np.hstack([w, h]) * padding |
|
|
|
|
|
if dim == 1: |
|
|
center = center[0] |
|
|
scale = scale[0] |
|
|
|
|
|
return center, scale |
|
|
|
|
|
|
|
|
def bbox_cs2xyxy(center: np.ndarray, |
|
|
scale: np.ndarray, |
|
|
padding: float = 1.) -> np.ndarray: |
|
|
"""Transform the bbox format from (center, scale) to (x1,y1,x2,y2). |
|
|
|
|
|
Args: |
|
|
center (ndarray): BBox center (x, y) in shape (2,) or (n, 2) |
|
|
scale (ndarray): BBox scale (w, h) in shape (2,) or (n, 2) |
|
|
padding (float): BBox padding factor that will be multilied to scale. |
|
|
Default: 1.0 |
|
|
|
|
|
Returns: |
|
|
ndarray[float32]: BBox (x1, y1, x2, y2) in shape (4, ) or (n, 4) |
|
|
""" |
|
|
|
|
|
dim = center.ndim |
|
|
assert scale.ndim == dim |
|
|
|
|
|
if dim == 1: |
|
|
center = center[None, :] |
|
|
scale = scale[None, :] |
|
|
|
|
|
wh = scale / padding |
|
|
xy = center - 0.5 * wh |
|
|
bbox = np.hstack((xy, xy + wh)) |
|
|
|
|
|
if dim == 1: |
|
|
bbox = bbox[0] |
|
|
|
|
|
return bbox |
|
|
|
|
|
|
|
|
def bbox_cs2xywh(center: np.ndarray, |
|
|
scale: np.ndarray, |
|
|
padding: float = 1.) -> np.ndarray: |
|
|
"""Transform the bbox format from (center, scale) to (x,y,w,h). |
|
|
|
|
|
Args: |
|
|
center (ndarray): BBox center (x, y) in shape (2,) or (n, 2) |
|
|
scale (ndarray): BBox scale (w, h) in shape (2,) or (n, 2) |
|
|
padding (float): BBox padding factor that will be multilied to scale. |
|
|
Default: 1.0 |
|
|
|
|
|
Returns: |
|
|
ndarray[float32]: BBox (x, y, w, h) in shape (4, ) or (n, 4) |
|
|
""" |
|
|
|
|
|
dim = center.ndim |
|
|
assert scale.ndim == dim |
|
|
|
|
|
if dim == 1: |
|
|
center = center[None, :] |
|
|
scale = scale[None, :] |
|
|
|
|
|
wh = scale / padding |
|
|
xy = center - 0.5 * wh |
|
|
bbox = np.hstack((xy, wh)) |
|
|
|
|
|
if dim == 1: |
|
|
bbox = bbox[0] |
|
|
|
|
|
return bbox |
|
|
|
|
|
|
|
|
def flip_bbox(bbox: np.ndarray, |
|
|
image_size: Tuple[int, int], |
|
|
bbox_format: str = 'xywh', |
|
|
direction: str = 'horizontal') -> np.ndarray: |
|
|
"""Flip the bbox in the given direction. |
|
|
|
|
|
Args: |
|
|
bbox (np.ndarray): The bounding boxes. The shape should be (..., 4) |
|
|
if ``bbox_format`` is ``'xyxy'`` or ``'xywh'``, and (..., 2) if |
|
|
``bbox_format`` is ``'center'`` |
|
|
image_size (tuple): The image shape in [w, h] |
|
|
bbox_format (str): The bbox format. Options are ``'xywh'``, ``'xyxy'`` |
|
|
and ``'center'``. |
|
|
direction (str): The flip direction. Options are ``'horizontal'``, |
|
|
``'vertical'`` and ``'diagonal'``. Defaults to ``'horizontal'`` |
|
|
|
|
|
Returns: |
|
|
np.ndarray: The flipped bounding boxes. |
|
|
""" |
|
|
direction_options = {'horizontal', 'vertical', 'diagonal'} |
|
|
assert direction in direction_options, ( |
|
|
f'Invalid flipping direction "{direction}". ' |
|
|
f'Options are {direction_options}') |
|
|
|
|
|
format_options = {'xywh', 'xyxy', 'center'} |
|
|
assert bbox_format in format_options, ( |
|
|
f'Invalid bbox format "{bbox_format}". ' |
|
|
f'Options are {format_options}') |
|
|
|
|
|
bbox_flipped = bbox.copy() |
|
|
w, h = image_size |
|
|
|
|
|
|
|
|
if direction == 'horizontal': |
|
|
if bbox_format == 'xywh' or bbox_format == 'center': |
|
|
bbox_flipped[..., 0] = w - bbox[..., 0] - 1 |
|
|
elif bbox_format == 'xyxy': |
|
|
bbox_flipped[..., ::2] = w - bbox[..., ::2] - 1 |
|
|
elif direction == 'vertical': |
|
|
if bbox_format == 'xywh' or bbox_format == 'center': |
|
|
bbox_flipped[..., 1] = h - bbox[..., 1] - 1 |
|
|
elif bbox_format == 'xyxy': |
|
|
bbox_flipped[..., 1::2] = h - bbox[..., 1::2] - 1 |
|
|
elif direction == 'diagonal': |
|
|
if bbox_format == 'xywh' or bbox_format == 'center': |
|
|
bbox_flipped[..., :2] = [w, h] - bbox[..., :2] - 1 |
|
|
elif bbox_format == 'xyxy': |
|
|
bbox_flipped[...] = [w, h, w, h] - bbox - 1 |
|
|
|
|
|
return bbox_flipped |
|
|
|
|
|
|
|
|
def get_udp_warp_matrix( |
|
|
center: np.ndarray, |
|
|
scale: np.ndarray, |
|
|
rot: float, |
|
|
output_size: Tuple[int, int], |
|
|
) -> np.ndarray: |
|
|
"""Calculate the affine transformation matrix under the unbiased |
|
|
constraint. See `UDP (CVPR 2020)`_ for details. |
|
|
|
|
|
Note: |
|
|
|
|
|
- The bbox number: N |
|
|
|
|
|
Args: |
|
|
center (np.ndarray[2, ]): Center of the bounding box (x, y). |
|
|
scale (np.ndarray[2, ]): Scale of the bounding box |
|
|
wrt [width, height]. |
|
|
rot (float): Rotation angle (degree). |
|
|
output_size (tuple): Size ([w, h]) of the output image |
|
|
|
|
|
Returns: |
|
|
np.ndarray: A 2x3 transformation matrix |
|
|
|
|
|
.. _`UDP (CVPR 2020)`: https://arxiv.org/abs/1911.07524 |
|
|
""" |
|
|
assert len(center) == 2 |
|
|
assert len(scale) == 2 |
|
|
assert len(output_size) == 2 |
|
|
|
|
|
input_size = center * 2 |
|
|
rot_rad = np.deg2rad(rot) |
|
|
warp_mat = np.zeros((2, 3), dtype=np.float32) |
|
|
scale_x = (output_size[0] - 1) / scale[0] |
|
|
scale_y = (output_size[1] - 1) / scale[1] |
|
|
warp_mat[0, 0] = math.cos(rot_rad) * scale_x |
|
|
warp_mat[0, 1] = -math.sin(rot_rad) * scale_x |
|
|
warp_mat[0, 2] = scale_x * (-0.5 * input_size[0] * math.cos(rot_rad) + |
|
|
0.5 * input_size[1] * math.sin(rot_rad) + |
|
|
0.5 * scale[0]) |
|
|
warp_mat[1, 0] = math.sin(rot_rad) * scale_y |
|
|
warp_mat[1, 1] = math.cos(rot_rad) * scale_y |
|
|
warp_mat[1, 2] = scale_y * (-0.5 * input_size[0] * math.sin(rot_rad) - |
|
|
0.5 * input_size[1] * math.cos(rot_rad) + |
|
|
0.5 * scale[1]) |
|
|
return warp_mat |
|
|
|
|
|
|
|
|
def get_warp_matrix(center: np.ndarray, |
|
|
scale: np.ndarray, |
|
|
rot: float, |
|
|
output_size: Tuple[int, int], |
|
|
shift: Tuple[float, float] = (0., 0.), |
|
|
inv: bool = False) -> np.ndarray: |
|
|
"""Calculate the affine transformation matrix that can warp the bbox area |
|
|
in the input image to the output size. |
|
|
|
|
|
Args: |
|
|
center (np.ndarray[2, ]): Center of the bounding box (x, y). |
|
|
scale (np.ndarray[2, ]): Scale of the bounding box |
|
|
wrt [width, height]. |
|
|
rot (float): Rotation angle (degree). |
|
|
output_size (np.ndarray[2, ] | list(2,)): Size of the |
|
|
destination heatmaps. |
|
|
shift (0-100%): Shift translation ratio wrt the width/height. |
|
|
Default (0., 0.). |
|
|
inv (bool): Option to inverse the affine transform direction. |
|
|
(inv=False: src->dst or inv=True: dst->src) |
|
|
|
|
|
Returns: |
|
|
np.ndarray: A 2x3 transformation matrix |
|
|
""" |
|
|
assert len(center) == 2 |
|
|
assert len(scale) == 2 |
|
|
assert len(output_size) == 2 |
|
|
assert len(shift) == 2 |
|
|
|
|
|
shift = np.array(shift) |
|
|
src_w = scale[0] |
|
|
dst_w = output_size[0] |
|
|
dst_h = output_size[1] |
|
|
|
|
|
rot_rad = np.deg2rad(rot) |
|
|
src_dir = _rotate_point(np.array([0., src_w * -0.5]), rot_rad) |
|
|
dst_dir = np.array([0., dst_w * -0.5]) |
|
|
|
|
|
src = np.zeros((3, 2), dtype=np.float32) |
|
|
src[0, :] = center + scale * shift |
|
|
src[1, :] = center + src_dir + scale * shift |
|
|
src[2, :] = _get_3rd_point(src[0, :], src[1, :]) |
|
|
|
|
|
dst = np.zeros((3, 2), dtype=np.float32) |
|
|
dst[0, :] = [dst_w * 0.5, dst_h * 0.5] |
|
|
dst[1, :] = np.array([dst_w * 0.5, dst_h * 0.5]) + dst_dir |
|
|
dst[2, :] = _get_3rd_point(dst[0, :], dst[1, :]) |
|
|
|
|
|
if inv: |
|
|
warp_mat = cv2.getAffineTransform(np.float32(dst), np.float32(src)) |
|
|
else: |
|
|
warp_mat = cv2.getAffineTransform(np.float32(src), np.float32(dst)) |
|
|
return warp_mat |
|
|
|
|
|
|
|
|
def _rotate_point(pt: np.ndarray, angle_rad: float) -> np.ndarray: |
|
|
"""Rotate a point by an angle. |
|
|
|
|
|
Args: |
|
|
pt (np.ndarray): 2D point coordinates (x, y) in shape (2, ) |
|
|
angle_rad (float): rotation angle in radian |
|
|
|
|
|
Returns: |
|
|
np.ndarray: Rotated point in shape (2, ) |
|
|
""" |
|
|
|
|
|
sn, cs = np.sin(angle_rad), np.cos(angle_rad) |
|
|
rot_mat = np.array([[cs, -sn], [sn, cs]]) |
|
|
return rot_mat @ pt |
|
|
|
|
|
|
|
|
def _get_3rd_point(a: np.ndarray, b: np.ndarray): |
|
|
"""To calculate the affine matrix, three pairs of points are required. This |
|
|
function is used to get the 3rd point, given 2D points a & b. |
|
|
|
|
|
The 3rd point is defined by rotating vector `a - b` by 90 degrees |
|
|
anticlockwise, using b as the rotation center. |
|
|
|
|
|
Args: |
|
|
a (np.ndarray): The 1st point (x,y) in shape (2, ) |
|
|
b (np.ndarray): The 2nd point (x,y) in shape (2, ) |
|
|
|
|
|
Returns: |
|
|
np.ndarray: The 3rd point. |
|
|
""" |
|
|
direction = a - b |
|
|
c = b + np.r_[-direction[1], direction[0]] |
|
|
return c |
|
|
|