Spaces:
Sleeping
Sleeping
from abc import abstractmethod | |
from typing import List,Iterator, Optional, Sequence, Tuple, Union | |
import numpy as np | |
import torch | |
try: | |
from pytorch3d.ops import box3d_overlap | |
from pytorch3d.transforms import (euler_angles_to_matrix, | |
matrix_to_euler_angles) | |
except ImportError: | |
box3d_overlap = None | |
euler_angles_to_matrix = None | |
matrix_to_euler_angles = None | |
from torch import Tensor | |
class BaseInstance3DBoxes: | |
"""Base class for 3D Boxes. | |
Note: | |
The box is bottom centered, i.e. the relative position of origin in the | |
box is (0.5, 0.5, 0). | |
Args: | |
tensor (Tensor or np.ndarray or Sequence[Sequence[float]]): The boxes | |
data with shape (N, box_dim). | |
box_dim (int): Number of the dimension of a box. Each row is | |
(x, y, z, x_size, y_size, z_size, yaw). Defaults to 7. | |
with_yaw (bool): Whether the box is with yaw rotation. If False, the | |
value of yaw will be set to 0 as minmax boxes. Defaults to True. | |
origin (Tuple[float]): Relative position of the box origin. | |
Defaults to (0.5, 0.5, 0). This will guide the box be converted to | |
(0.5, 0.5, 0) mode. | |
Attributes: | |
tensor (Tensor): Float matrix with shape (N, box_dim). | |
box_dim (int): Integer indicating the dimension of a box. Each row is | |
(x, y, z, x_size, y_size, z_size, yaw, ...). | |
with_yaw (bool): If True, the value of yaw will be set to 0 as minmax | |
boxes. | |
""" | |
YAW_AXIS: int = 0 | |
def __init__( | |
self, | |
tensor: Union[Tensor, np.ndarray, Sequence[Sequence[float]]], | |
box_dim: int = 7, | |
with_yaw: bool = True, | |
origin: Tuple[float, float, float] = (0.5, 0.5, 0) | |
) -> None: | |
if isinstance(tensor, Tensor): | |
device = tensor.device | |
else: | |
device = torch.device('cpu') | |
tensor = torch.as_tensor(tensor, dtype=torch.float32, device=device) | |
if tensor.numel() == 0: | |
# Use reshape, so we don't end up creating a new tensor that does | |
# not depend on the inputs (and consequently confuses jit) | |
tensor = tensor.reshape((-1, box_dim)) | |
assert tensor.dim() == 2 and tensor.size(-1) == box_dim, \ | |
('The box dimension must be 2 and the length of the last ' | |
f'dimension must be {box_dim}, but got boxes with shape ' | |
f'{tensor.shape}.') | |
if tensor.shape[-1] == 6: | |
# If the dimension of boxes is 6, we expand box_dim by padding 0 as | |
# a fake yaw and set with_yaw to False | |
assert box_dim == 6 | |
fake_rot = tensor.new_zeros(tensor.shape[0], 1) | |
tensor = torch.cat((tensor, fake_rot), dim=-1) | |
self.box_dim = box_dim + 1 | |
self.with_yaw = False | |
else: | |
self.box_dim = box_dim | |
self.with_yaw = with_yaw | |
self.tensor = tensor.clone() | |
if origin != (0.5, 0.5, 0): | |
dst = self.tensor.new_tensor((0.5, 0.5, 0)) | |
src = self.tensor.new_tensor(origin) | |
self.tensor[:, :3] += self.tensor[:, 3:6] * (dst - src) | |
def shape(self) -> torch.Size: | |
"""torch.Size: Shape of boxes.""" | |
return self.tensor.shape | |
def volume(self) -> Tensor: | |
"""Tensor: A vector with volume of each box in shape (N, ).""" | |
return self.tensor[:, 3] * self.tensor[:, 4] * self.tensor[:, 5] | |
def dims(self) -> Tensor: | |
"""Tensor: Size dimensions of each box in shape (N, 3).""" | |
return self.tensor[:, 3:6] | |
def yaw(self) -> Tensor: | |
"""Tensor: A vector with yaw of each box in shape (N, ).""" | |
return self.tensor[:, 6] | |
def height(self) -> Tensor: | |
"""Tensor: A vector with height of each box in shape (N, ).""" | |
return self.tensor[:, 5] | |
def top_height(self) -> Tensor: | |
"""Tensor: A vector with top height of each box in shape (N, ).""" | |
return self.bottom_height + self.height | |
def bottom_height(self) -> Tensor: | |
"""Tensor: A vector with bottom height of each box in shape (N, ).""" | |
return self.tensor[:, 2] | |
def center(self) -> Tensor: | |
"""Calculate the center of all the boxes. | |
Note: | |
In MMDetection3D's convention, the bottom center is usually taken | |
as the default center. | |
The relative position of the centers in different kinds of boxes | |
are different, e.g., the relative center of a boxes is | |
(0.5, 1.0, 0.5) in camera and (0.5, 0.5, 0) in lidar. It is | |
recommended to use ``bottom_center`` or ``gravity_center`` for | |
clearer usage. | |
Returns: | |
Tensor: A tensor with center of each box in shape (N, 3). | |
""" | |
return self.bottom_center | |
def bottom_center(self) -> Tensor: | |
"""Tensor: A tensor with center of each box in shape (N, 3).""" | |
return self.tensor[:, :3] | |
def gravity_center(self) -> Tensor: | |
"""Tensor: A tensor with center of each box in shape (N, 3).""" | |
bottom_center = self.bottom_center | |
gravity_center = torch.zeros_like(bottom_center) | |
gravity_center[:, :2] = bottom_center[:, :2] | |
gravity_center[:, 2] = bottom_center[:, 2] + self.tensor[:, 5] * 0.5 | |
return gravity_center | |
def corners(self) -> Tensor: | |
"""Tensor: A tensor with 8 corners of each box in shape (N, 8, 3).""" | |
pass | |
def bev(self) -> Tensor: | |
"""Tensor: 2D BEV box of each box with rotation in XYWHR format, in | |
shape (N, 5).""" | |
return self.tensor[:, [0, 1, 3, 4, 6]] | |
def in_range_bev( | |
self, box_range: Union[Tensor, np.ndarray, | |
Sequence[float]]) -> Tensor: | |
"""Check whether the boxes are in the given range. | |
Args: | |
box_range (Tensor or np.ndarray or Sequence[float]): The range of | |
box in order of (x_min, y_min, x_max, y_max). | |
Note: | |
The original implementation of SECOND checks whether boxes in a | |
range by checking whether the points are in a convex polygon, we | |
reduce the burden for simpler cases. | |
Returns: | |
Tensor: A binary vector indicating whether each box is inside the | |
reference range. | |
""" | |
in_range_flags = ((self.bev[:, 0] > box_range[0]) | |
& (self.bev[:, 1] > box_range[1]) | |
& (self.bev[:, 0] < box_range[2]) | |
& (self.bev[:, 1] < box_range[3])) | |
return in_range_flags | |
def rotate( | |
self, | |
angle: Union[Tensor, np.ndarray, float], | |
points: Optional[Union[Tensor, np.ndarray]] = None | |
) -> Union[Tuple[Tensor, Tensor], Tuple[np.ndarray, np.ndarray], | |
Tuple[Tensor], None]: | |
"""Rotate boxes with points (optional) with the given angle or rotation | |
matrix. | |
Args: | |
angle (Tensor or np.ndarray or float): Rotation angle or rotation | |
matrix. | |
points (Tensor or np.ndarray or :obj:``, optional): | |
Points to rotate. Defaults to None. | |
Returns: | |
tuple or None: When ``points`` is None, the function returns None, | |
otherwise it returns the rotated points and the rotation matrix | |
``rot_mat_T``. | |
""" | |
pass | |
def flip( | |
self, | |
bev_direction: str = 'horizontal', | |
points: Optional[Union[Tensor, np.ndarray, ]] = None | |
) -> Union[Tensor, np.ndarray, None]: | |
"""Flip the boxes in BEV along given BEV direction. | |
Args: | |
bev_direction (str): Direction by which to flip. Can be chosen from | |
'horizontal' and 'vertical'. Defaults to 'horizontal'. | |
points (Tensor or np.ndarray or :obj:``, optional): | |
Points to flip. Defaults to None. | |
Returns: | |
Tensor or np.ndarray or :obj:`` or None: When ``points`` | |
is None, the function returns None, otherwise it returns the | |
flipped points. | |
""" | |
pass | |
def translate(self, trans_vector: Union[Tensor, np.ndarray]) -> None: | |
"""Translate boxes with the given translation vector. | |
Args: | |
trans_vector (Tensor or np.ndarray): Translation vector of size | |
1x3. | |
""" | |
if not isinstance(trans_vector, Tensor): | |
trans_vector = self.tensor.new_tensor(trans_vector) | |
self.tensor[:, :3] += trans_vector | |
def in_range_3d( | |
self, box_range: Union[Tensor, np.ndarray, | |
Sequence[float]]) -> Tensor: | |
"""Check whether the boxes are in the given range. | |
Args: | |
box_range (Tensor or np.ndarray or Sequence[float]): The range of | |
box (x_min, y_min, z_min, x_max, y_max, z_max). | |
Note: | |
In the original implementation of SECOND, checking whether a box in | |
the range checks whether the points are in a convex polygon, we try | |
to reduce the burden for simpler cases. | |
Returns: | |
Tensor: A binary vector indicating whether each point is inside the | |
reference range. | |
""" | |
in_range_flags = ((self.tensor[:, 0] > box_range[0]) | |
& (self.tensor[:, 1] > box_range[1]) | |
& (self.tensor[:, 2] > box_range[2]) | |
& (self.tensor[:, 0] < box_range[3]) | |
& (self.tensor[:, 1] < box_range[4]) | |
& (self.tensor[:, 2] < box_range[5])) | |
return in_range_flags | |
def convert_to(self, | |
dst: int, | |
rt_mat: Optional[Union[Tensor, np.ndarray]] = None, | |
correct_yaw: bool = False) -> 'BaseInstance3DBoxes': | |
"""Convert self to ``dst`` mode. | |
Args: | |
dst (int): The target Box mode. | |
rt_mat (Tensor or np.ndarray, optional): The rotation and | |
translation matrix between different coordinates. | |
Defaults to None. The conversion from ``src`` coordinates to | |
``dst`` coordinates usually comes along the change of sensors, | |
e.g., from camera to LiDAR. This requires a transformation | |
matrix. | |
correct_yaw (bool): Whether to convert the yaw angle to the target | |
coordinate. Defaults to False. | |
Returns: | |
:obj:`BaseInstance3DBoxes`: The converted box of the same type in | |
the ``dst`` mode. | |
""" | |
pass | |
def scale(self, scale_factor: float) -> None: | |
"""Scale the box with horizontal and vertical scaling factors. | |
Args: | |
scale_factors (float): Scale factors to scale the boxes. | |
""" | |
self.tensor[:, :6] *= scale_factor | |
self.tensor[:, 7:] *= scale_factor # velocity | |
def nonempty(self, threshold: float = 0.0) -> Tensor: | |
"""Find boxes that are non-empty. | |
A box is considered empty if either of its side is no larger than | |
threshold. | |
Args: | |
threshold (float): The threshold of minimal sizes. Defaults to 0.0. | |
Returns: | |
Tensor: A binary vector which represents whether each box is empty | |
(False) or non-empty (True). | |
""" | |
box = self.tensor | |
size_x = box[..., 3] | |
size_y = box[..., 4] | |
size_z = box[..., 5] | |
keep = ((size_x > threshold) | |
& (size_y > threshold) & (size_z > threshold)) | |
return keep | |
def __getitem__( | |
self, item: Union[int, slice, np.ndarray, | |
Tensor]) -> 'BaseInstance3DBoxes': | |
""" | |
Args: | |
item (int or slice or np.ndarray or Tensor): Index of boxes. | |
Note: | |
The following usage are allowed: | |
1. `new_boxes = boxes[3]`: Return a `Boxes` that contains only one | |
box. | |
2. `new_boxes = boxes[2:10]`: Return a slice of boxes. | |
3. `new_boxes = boxes[vector]`: Where vector is a | |
torch.BoolTensor with `length = len(boxes)`. Nonzero elements in | |
the vector will be selected. | |
Note that the returned Boxes might share storage with this Boxes, | |
subject to PyTorch's indexing semantics. | |
Returns: | |
:obj:`BaseInstance3DBoxes`: A new object of | |
:class:`BaseInstance3DBoxes` after indexing. | |
""" | |
original_type = type(self) | |
if isinstance(item, int): | |
return original_type(self.tensor[item].view(1, -1), | |
box_dim=self.box_dim, | |
with_yaw=self.with_yaw) | |
b = self.tensor[item] | |
assert b.dim() == 2, \ | |
f'Indexing on Boxes with {item} failed to return a matrix!' | |
return original_type(b, box_dim=self.box_dim, with_yaw=self.with_yaw) | |
def __len__(self) -> int: | |
"""int: Number of boxes in the current object.""" | |
return self.tensor.shape[0] | |
def __repr__(self) -> str: | |
"""str: Return a string that describes the object.""" | |
return self.__class__.__name__ + '(\n ' + str(self.tensor) + ')' | |
def cat(cls, boxes_list: Sequence['BaseInstance3DBoxes'] | |
) -> 'BaseInstance3DBoxes': | |
"""Concatenate a list of Boxes into a single Boxes. | |
Args: | |
boxes_list (Sequence[:obj:`BaseInstance3DBoxes`]): List of boxes. | |
Returns: | |
:obj:`BaseInstance3DBoxes`: The concatenated boxes. | |
""" | |
assert isinstance(boxes_list, (list, tuple)) | |
if len(boxes_list) == 0: | |
return cls(torch.empty(0)) | |
assert all(isinstance(box, cls) for box in boxes_list) | |
# use torch.cat (v.s. layers.cat) | |
# so the returned boxes never share storage with input | |
cat_boxes = cls(torch.cat([b.tensor for b in boxes_list], dim=0), | |
box_dim=boxes_list[0].box_dim, | |
with_yaw=boxes_list[0].with_yaw) | |
return cat_boxes | |
def numpy(self) -> np.ndarray: | |
"""Reload ``numpy`` from self.tensor.""" | |
return self.tensor.numpy() | |
def to(self, device: Union[str, torch.device], *args, | |
**kwargs) -> 'BaseInstance3DBoxes': | |
"""Convert current boxes to a specific device. | |
Args: | |
device (str or :obj:`torch.device`): The name of the device. | |
Returns: | |
:obj:`BaseInstance3DBoxes`: A new boxes object on the specific | |
device. | |
""" | |
original_type = type(self) | |
return original_type(self.tensor.to(device, *args, **kwargs), | |
box_dim=self.box_dim, | |
with_yaw=self.with_yaw) | |
def cpu(self) -> 'BaseInstance3DBoxes': | |
"""Convert current boxes to cpu device. | |
Returns: | |
:obj:`BaseInstance3DBoxes`: A new boxes object on the cpu device. | |
""" | |
original_type = type(self) | |
return original_type(self.tensor.cpu(), | |
box_dim=self.box_dim, | |
with_yaw=self.with_yaw) | |
def cuda(self, *args, **kwargs) -> 'BaseInstance3DBoxes': | |
"""Convert current boxes to cuda device. | |
Returns: | |
:obj:`BaseInstance3DBoxes`: A new boxes object on the cuda device. | |
""" | |
original_type = type(self) | |
return original_type(self.tensor.cuda(*args, **kwargs), | |
box_dim=self.box_dim, | |
with_yaw=self.with_yaw) | |
def clone(self) -> 'BaseInstance3DBoxes': | |
"""Clone the boxes. | |
Returns: | |
:obj:`BaseInstance3DBoxes`: Box object with the same properties as | |
self. | |
""" | |
original_type = type(self) | |
return original_type(self.tensor.clone(), | |
box_dim=self.box_dim, | |
with_yaw=self.with_yaw) | |
def detach(self) -> 'BaseInstance3DBoxes': | |
"""Detach the boxes. | |
Returns: | |
:obj:`BaseInstance3DBoxes`: Box object with the same properties as | |
self. | |
""" | |
original_type = type(self) | |
return original_type(self.tensor.detach(), | |
box_dim=self.box_dim, | |
with_yaw=self.with_yaw) | |
def device(self) -> torch.device: | |
"""torch.device: The device of the boxes are on.""" | |
return self.tensor.device | |
def __iter__(self) -> Iterator[Tensor]: | |
"""Yield a box as a Tensor at a time. | |
Returns: | |
Iterator[Tensor]: A box of shape (box_dim, ). | |
""" | |
yield from self.tensor | |
def height_overlaps(cls, boxes1: 'BaseInstance3DBoxes', | |
boxes2: 'BaseInstance3DBoxes') -> Tensor: | |
"""Calculate height overlaps of two boxes. | |
Note: | |
This function calculates the height overlaps between ``boxes1`` and | |
``boxes2``, ``boxes1`` and ``boxes2`` should be in the same type. | |
Args: | |
boxes1 (:obj:`BaseInstance3DBoxes`): Boxes 1 contain N boxes. | |
boxes2 (:obj:`BaseInstance3DBoxes`): Boxes 2 contain M boxes. | |
Returns: | |
Tensor: Calculated height overlap of the boxes. | |
""" | |
assert isinstance(boxes1, BaseInstance3DBoxes) | |
assert isinstance(boxes2, BaseInstance3DBoxes) | |
assert type(boxes1) == type(boxes2), \ | |
'"boxes1" and "boxes2" should be in the same type, ' \ | |
f'but got {type(boxes1)} and {type(boxes2)}.' | |
boxes1_top_height = boxes1.top_height.view(-1, 1) | |
boxes1_bottom_height = boxes1.bottom_height.view(-1, 1) | |
boxes2_top_height = boxes2.top_height.view(1, -1) | |
boxes2_bottom_height = boxes2.bottom_height.view(1, -1) | |
heighest_of_bottom = torch.max(boxes1_bottom_height, | |
boxes2_bottom_height) | |
lowest_of_top = torch.min(boxes1_top_height, boxes2_top_height) | |
overlaps_h = torch.clamp(lowest_of_top - heighest_of_bottom, min=0) | |
return overlaps_h | |
def new_box( | |
self, data: Union[Tensor, np.ndarray, Sequence[Sequence[float]]] | |
) -> 'BaseInstance3DBoxes': | |
"""Create a new box object with data. | |
The new box and its tensor has the similar properties as self and | |
self.tensor, respectively. | |
Args: | |
data (Tensor or np.ndarray or Sequence[Sequence[float]]): Data to | |
be copied. | |
Returns: | |
:obj:`BaseInstance3DBoxes`: A new bbox object with ``data``, the | |
object's other properties are similar to ``self``. | |
""" | |
new_tensor = self.tensor.new_tensor(data) \ | |
if not isinstance(data, Tensor) else data.to(self.device) | |
original_type = type(self) | |
return original_type(new_tensor, | |
box_dim=self.box_dim, | |
with_yaw=self.with_yaw) | |
class EulerInstance3DBoxes(BaseInstance3DBoxes): | |
"""3D boxes with 1-D orientation represented by three Euler angles. | |
See https://en.wikipedia.org/wiki/Euler_angles for | |
regarding the definition of Euler angles. | |
Attributes: | |
tensor (torch.Tensor): Float matrix of N x box_dim. | |
box_dim (int): Integer indicates the dimension of a box | |
Each row is (x, y, z, x_size, y_size, z_size, alpha, beta, gamma). | |
""" | |
def __init__(self, tensor, box_dim=9, origin=(0.5, 0.5, 0.5)): | |
if isinstance(tensor, torch.Tensor): | |
device = tensor.device | |
else: | |
device = torch.device('cpu') | |
tensor = torch.as_tensor(tensor, dtype=torch.float32, device=device) | |
if tensor.numel() == 0: | |
# Use reshape, so we don't end up creating a new tensor that | |
# does not depend on the inputs (and consequently confuses jit) | |
tensor = tensor.reshape((0, box_dim)).to(dtype=torch.float32, | |
device=device) | |
assert tensor.dim() == 2 and tensor.size(-1) == box_dim, tensor.size() | |
if tensor.shape[-1] == 6: | |
# If the dimension of boxes is 6, we expand box_dim by padding | |
# (0, 0, 0) as a fake euler angle. | |
assert box_dim == 6 | |
fake_rot = tensor.new_zeros(tensor.shape[0], 3) | |
tensor = torch.cat((tensor, fake_rot), dim=-1) | |
self.box_dim = box_dim + 3 | |
elif tensor.shape[-1] == 7: | |
assert box_dim == 7 | |
fake_euler = tensor.new_zeros(tensor.shape[0], 2) | |
tensor = torch.cat((tensor, fake_euler), dim=-1) | |
self.box_dim = box_dim + 2 | |
else: | |
assert tensor.shape[-1] == 9 | |
self.box_dim = box_dim | |
self.tensor = tensor.clone() | |
self.origin = origin | |
if origin != (0.5, 0.5, 0.5): | |
dst = self.tensor.new_tensor((0.5, 0.5, 0.5)) | |
src = self.tensor.new_tensor(origin) | |
self.tensor[:, :3] += self.tensor[:, 3:6] * (dst - src) | |
def get_corners(self, tensor1): | |
"""torch.Tensor: Coordinates of corners of all the boxes | |
in shape (N, 8, 3). | |
Convert the boxes to corners in clockwise order, in form of | |
``(x0y0z0, x0y0z1, x0y1z1, x0y1z0, x1y0z0, x1y0z1, x1y1z1, x1y1z0)`` | |
.. code-block:: none | |
up z | |
front y ^ | |
/ | | |
/ | | |
(x0, y1, z1) + ----------- + (x1, y1, z1) | |
/| / | | |
/ | / | | |
(x0, y0, z1) + ----------- + + (x1, y1, z0) | |
| / . | / | |
| / origin | / | |
(x0, y0, z0) + ----------- + --------> right x | |
(x1, y0, z0) | |
""" | |
if tensor1.numel() == 0: | |
return torch.empty([0, 8, 3], device=tensor1.device) | |
dims = tensor1[:, 3:6] | |
corners_norm = torch.from_numpy( | |
np.stack(np.unravel_index(np.arange(8), [2] * 3), | |
axis=1)).to(device=dims.device, dtype=dims.dtype) | |
corners_norm = corners_norm[[0, 1, 3, 2, 4, 5, 7, 6]] | |
# use relative origin | |
assert self.origin == (0.5, 0.5, 0.5), \ | |
'self.origin != (0.5, 0.5, 0.5) needs to be checked!' | |
corners_norm = corners_norm - dims.new_tensor(self.origin) | |
corners = dims.view([-1, 1, 3]) * corners_norm.reshape([1, 8, 3]) | |
# rotate | |
corners = rotation_3d_in_euler(corners, tensor1[:, 6:]) | |
corners += tensor1[:, :3].view(-1, 1, 3) | |
return corners | |
def overlaps(cls, boxes1, boxes2, mode='iou', eps=1e-4): | |
"""Calculate 3D overlaps of two boxes. | |
Note: | |
This function calculates the overlaps between ``boxes1`` and | |
``boxes2``, ``boxes1`` and ``boxes2`` should be in the same type. | |
Args: | |
boxes1 (:obj:`EulerInstance3DBoxes`): Boxes 1 contain N boxes. | |
boxes2 (:obj:`EulerInstance3DBoxes`): Boxes 2 contain M boxes. | |
mode (str): Mode of iou calculation. Defaults to 'iou'. | |
eps (bool): Epsilon. Defaults to 1e-4. | |
Returns: | |
torch.Tensor: Calculated 3D overlaps of the boxes. | |
""" | |
assert isinstance(boxes1, EulerInstance3DBoxes) | |
assert isinstance(boxes2, EulerInstance3DBoxes) | |
assert type(boxes1) == type(boxes2), '"boxes1" and "boxes2" should' \ | |
f'be in the same type, got {type(boxes1)} and {type(boxes2)}.' | |
assert mode in ['iou'] | |
rows = len(boxes1) | |
cols = len(boxes2) | |
if rows * cols == 0: | |
return boxes1.tensor.new(rows, cols) | |
corners1 = boxes1.corners | |
corners2 = boxes2.corners | |
_, iou3d = box3d_overlap(corners1, corners2, eps=eps) | |
return iou3d | |
def gravity_center(self): | |
"""torch.Tensor: A tensor with center of each box in shape (N, 3).""" | |
return self.tensor[:, :3] | |
def corners(self): | |
"""torch.Tensor: Coordinates of corners of all the boxes | |
in shape (N, 8, 3). | |
Convert the boxes to corners in clockwise order, in form of | |
``(x0y0z0, x0y0z1, x0y1z1, x0y1z0, x1y0z0, x1y0z1, x1y1z1, x1y1z0)`` | |
.. code-block:: none | |
up z | |
front y ^ | |
/ | | |
/ | | |
(x0, y1, z1) + ----------- + (x1, y1, z1) | |
/| / | | |
/ | / | | |
(x0, y0, z1) + ----------- + + (x1, y1, z0) | |
| / . | / | |
| / origin | / | |
(x0, y0, z0) + ----------- + --------> right x | |
(x1, y0, z0) | |
""" | |
if self.tensor.numel() == 0: | |
return torch.empty([0, 8, 3], device=self.tensor.device) | |
dims = self.dims | |
corners_norm = torch.from_numpy( | |
np.stack(np.unravel_index(np.arange(8), [2] * 3), | |
axis=1)).to(device=dims.device, dtype=dims.dtype) | |
corners_norm = corners_norm[[0, 1, 3, 2, 4, 5, 7, 6]] | |
# use relative origin | |
assert self.origin == (0.5, 0.5, 0.5), \ | |
'self.origin != (0.5, 0.5, 0.5) needs to be checked!' | |
corners_norm = corners_norm - dims.new_tensor(self.origin) | |
corners = dims.view([-1, 1, 3]) * corners_norm.reshape([1, 8, 3]) | |
# rotate | |
corners = rotation_3d_in_euler(corners, self.tensor[:, 6:]) | |
corners += self.tensor[:, :3].view(-1, 1, 3) | |
return corners | |
def transform(self, matrix): | |
if self.tensor.shape[0] == 0: | |
return | |
if not isinstance(matrix, torch.Tensor): | |
matrix = self.tensor.new_tensor(matrix) | |
points = self.tensor[:, :3] | |
constant = points.new_ones(points.shape[0], 1) | |
points_extend = torch.concat([points, constant], dim=-1) | |
points_trans = torch.matmul(points_extend, matrix.transpose(-2, | |
-1))[:, :3] | |
size = self.tensor[:, 3:6] | |
# angle_delta = matrix_to_euler_angles(matrix[:3,:3], 'ZXY') | |
# angle = self.tensor[:,6:] + angle_delta | |
ori_matrix = euler_angles_to_matrix(self.tensor[:, 6:], 'ZXY') | |
rot_matrix = matrix[:3, :3].expand_as(ori_matrix) | |
final = torch.bmm(rot_matrix, ori_matrix) | |
angle = matrix_to_euler_angles(final, 'ZXY') | |
self.tensor = torch.cat([points_trans, size, angle], dim=-1) | |
def scale(self, scale_factor: float) -> None: | |
"""Scale the box with horizontal and vertical scaling factors. | |
Args: | |
scale_factors (float): Scale factors to scale the boxes. | |
""" | |
self.tensor[:, :6] *= scale_factor | |
def rotate(self, angle, points=None): | |
"""Rotate boxes with points (optional) with the given angle or rotation | |
matrix. | |
Args: | |
angle (float | torch.Tensor | np.ndarray): | |
Rotation angle or rotation matrix. | |
points (torch.Tensor | np.ndarray | :obj:``, optional): | |
Points to rotate. Defaults to None. | |
Returns: | |
tuple or None: When ``points`` is None, the function returns | |
None, otherwise it returns the rotated points and the | |
rotation matrix ``rot_mat_T``. | |
""" | |
if not isinstance(angle, torch.Tensor): | |
angle = self.tensor.new_tensor(angle) | |
if angle.numel() == 1: # only given yaw angle for rotation | |
angle = self.tensor.new_tensor([angle, 0., 0.]) | |
rot_matrix = euler_angles_to_matrix(angle, 'ZXY') | |
elif angle.numel() == 3: | |
rot_matrix = euler_angles_to_matrix(angle, 'ZXY') | |
elif angle.shape == torch.Size([3, 3]): | |
rot_matrix = angle | |
else: | |
raise NotImplementedError | |
rot_mat_T = rot_matrix.T | |
transform_matrix = torch.eye(4) | |
transform_matrix[:3, :3] = rot_matrix | |
self.transform(transform_matrix) | |
if points is not None: | |
if isinstance(points, torch.Tensor): | |
points[:, :3] = points[:, :3] @ rot_mat_T | |
elif isinstance(points, np.ndarray): | |
rot_mat_T = rot_mat_T.cpu().numpy() | |
points[:, :3] = np.dot(points[:, :3], rot_mat_T) | |
elif isinstance(points, ): | |
points.rotate(rot_mat_T) | |
else: | |
raise ValueError | |
return points, rot_mat_T | |
else: | |
return rot_mat_T | |
def flip(self, direction='X'): | |
"""Flip the boxes along the corresponding axis. | |
Args: | |
direction (str, optional): Flip axis. Defaults to 'X'. | |
""" | |
assert direction in ['X', 'Y', 'Z'] | |
if direction == 'X': | |
self.tensor[:, 0] = -self.tensor[:, 0] | |
self.tensor[:, 6] = -self.tensor[:, 6] + np.pi | |
self.tensor[:, 8] = -self.tensor[:, 8] | |
elif direction == 'Y': | |
self.tensor[:, 1] = -self.tensor[:, 1] | |
self.tensor[:, 6] = -self.tensor[:, 6] | |
self.tensor[:, 7] = -self.tensor[:, 7] + np.pi | |
elif direction == 'Z': | |
self.tensor[:, 2] = -self.tensor[:, 2] | |
self.tensor[:, 7] = -self.tensor[:, 7] | |
self.tensor[:, 8] = -self.tensor[:, 8] + np.pi | |
def rotation_3d_in_euler(points, angles, return_mat=False, clockwise=False): | |
"""Rotate points by angles according to axis. | |
Args: | |
points (np.ndarray | torch.Tensor | list | tuple ): | |
Points of shape (N, M, 3). | |
angles (np.ndarray | torch.Tensor | list | tuple): | |
Vector of angles in shape (N, 3) | |
return_mat: Whether or not return the rotation matrix (transposed). | |
Defaults to False. | |
clockwise: Whether the rotation is clockwise. Defaults to False. | |
Raises: | |
ValueError: when the axis is not in range [0, 1, 2], it will | |
raise value error. | |
Returns: | |
(torch.Tensor | np.ndarray): Rotated points in shape (N, M, 3). | |
""" | |
batch_free = len(points.shape) == 2 | |
if batch_free: | |
points = points[None] | |
if len(angles.shape) == 1: | |
angles = angles.expand(points.shape[:1] + (3, )) | |
# angles = torch.full(points.shape[:1], angles) | |
assert len(points.shape) == 3 and len(angles.shape) == 2 \ | |
and points.shape[0] == angles.shape[0], f'Incorrect shape of points ' \ | |
f'angles: {points.shape}, {angles.shape}' | |
assert points.shape[-1] in [2, 3], \ | |
f'Points size should be 2 or 3 instead of {points.shape[-1]}' | |
rot_mat_T = euler_angles_to_matrix(angles, 'ZXY') # N, 3,3 | |
rot_mat_T = rot_mat_T.transpose(-2, -1) | |
if clockwise: | |
raise NotImplementedError('clockwise') | |
if points.shape[0] == 0: | |
points_new = points | |
else: | |
points_new = torch.bmm(points, rot_mat_T) | |
if batch_free: | |
points_new = points_new.squeeze(0) | |
if return_mat: | |
if batch_free: | |
rot_mat_T = rot_mat_T.squeeze(0) | |
return points_new, rot_mat_T | |
else: | |
return points_new | |
def _axis_angle_rotation(axis: str, angle: np.ndarray) -> np.ndarray: | |
"""Return the rotation matrices for one of the rotations about an axis of | |
which Euler angles describe, for each value of the angle given. | |
Args: | |
axis: Axis label "X" or "Y or "Z". | |
angle: any shape tensor of Euler angles in radians | |
Returns: | |
Rotation matrices as tensor of shape (..., 3, 3). | |
""" | |
cos = np.cos(angle) | |
sin = np.sin(angle) | |
one = np.ones_like(angle) | |
zero = np.zeros_like(angle) | |
if axis == 'X': | |
R_flat = (one, zero, zero, zero, cos, -sin, zero, sin, cos) | |
elif axis == 'Y': | |
R_flat = (cos, zero, sin, zero, one, zero, -sin, zero, cos) | |
elif axis == 'Z': | |
R_flat = (cos, -sin, zero, sin, cos, zero, zero, zero, one) | |
else: | |
raise ValueError('letter must be either X, Y or Z.') | |
return np.stack(R_flat, -1).reshape(angle.shape + (3, 3)) | |
def is_inside_box(points, center, size, rotation_mat): | |
"""Check if points are inside a 3D bounding box. | |
Args: | |
points: 3D points, numpy array of shape (n, 3). | |
center: center of the box, numpy array of shape (3, ). | |
size: size of the box, numpy array of shape (3, ). | |
rotation_mat: rotation matrix of the box, | |
numpy array of shape (3, 3). | |
Returns: | |
Boolean array of shape (n, ) | |
indicating if each point is inside the box. | |
""" | |
assert points.shape[1] == 3, 'points should be of shape (n, 3)' | |
points = np.array(points) # n,3 | |
center = np.array(center) # n, 3 | |
size = np.array(size) # n, 3 | |
rotation_mat = np.array(rotation_mat) | |
assert rotation_mat.shape == ( | |
3, | |
3, | |
), f'R should be shape (3,3), but got {rotation_mat.shape}' | |
pcd_local = (points - center) @ rotation_mat # n, 3 | |
pcd_local = pcd_local / size * 2.0 # scale to [-1, 1] # n, 3 | |
pcd_local = abs(pcd_local) | |
return ((pcd_local[:, 0] <= 1) | |
& (pcd_local[:, 1] <= 1) | |
& (pcd_local[:, 2] <= 1)) | |
def normalize_box(scene_pcd, embodied_scan_bbox): | |
"""Find the smallest 6 DoF box that covers these points which 9 DoF box | |
covers. | |
Args: | |
scene_pcd (Tensor / ndarray): | |
(..., 3) | |
embodied_scan_bbox (Tensor / ndarray): | |
(9,) 9 DoF box | |
Returns: | |
Tensor: Transformed 3D box of shape (N, 8, 3). | |
""" | |
bbox = np.array(embodied_scan_bbox) | |
orientation = euler_to_matrix_np(bbox[np.newaxis, 6:])[0] | |
position = np.array(bbox[:3]) | |
size = np.array(bbox[3:6]) | |
obj_mask = np.array( | |
is_inside_box(scene_pcd[:, :3], position, size, orientation), | |
dtype=bool, | |
) | |
obj_pc = scene_pcd[obj_mask] | |
# resume the same if there's None | |
if obj_pc.shape[0] < 1: | |
return embodied_scan_bbox[:6] | |
xmin = np.min(obj_pc[:, 0]) | |
ymin = np.min(obj_pc[:, 1]) | |
zmin = np.min(obj_pc[:, 2]) | |
xmax = np.max(obj_pc[:, 0]) | |
ymax = np.max(obj_pc[:, 1]) | |
zmax = np.max(obj_pc[:, 2]) | |
bbox = np.array([ | |
(xmin + xmax) / 2, | |
(ymin + ymax) / 2, | |
(zmin + zmax) / 2, | |
xmax - xmin, | |
ymax - ymin, | |
zmax - zmin, | |
]) | |
return bbox | |
def from_9dof_to_6dof(pcd_data, bbox_): | |
# that's a kind of loss of information, so we don't recommend | |
return normalize_box(pcd_data, bbox_) | |
def bbox_to_corners(centers, sizes, rot_mat: torch.Tensor) -> torch.Tensor: | |
"""Transform bbox parameters to the 8 corners. | |
Args: | |
bbox (Tensor): 3D box of shape (N, 6) or (N, 7) or (N, 9). | |
Returns: | |
Tensor: Transformed 3D box of shape (N, 8, 3). | |
""" | |
device = centers.device | |
use_batch = False | |
if len(centers.shape) == 3: | |
use_batch = True | |
batch_size, n_proposals = centers.shape[0], centers.shape[1] | |
centers = centers.reshape(-1, 3) | |
sizes = sizes.reshape(-1, 3) | |
rot_mat = rot_mat.reshape(-1, 3, 3) | |
n_box = centers.shape[0] | |
if use_batch: | |
assert n_box == batch_size * n_proposals | |
centers = centers.unsqueeze(1).repeat(1, 8, 1) # shape (N, 8, 3) | |
half_sizes = sizes.unsqueeze(1).repeat(1, 8, 1) / 2 # shape (N, 8, 3) | |
eight_corners_x = (torch.tensor([1, 1, 1, 1, -1, -1, -1, -1], | |
device=device).unsqueeze(0).repeat( | |
n_box, 1)) # shape (N, 8) | |
eight_corners_y = (torch.tensor([1, 1, -1, -1, 1, 1, -1, -1], | |
device=device).unsqueeze(0).repeat( | |
n_box, 1)) # shape (N, 8) | |
eight_corners_z = (torch.tensor([1, -1, -1, 1, 1, -1, -1, 1], | |
device=device).unsqueeze(0).repeat( | |
n_box, 1)) # shape (N, 8) | |
eight_corners = torch.stack( | |
(eight_corners_x, eight_corners_y, eight_corners_z), | |
dim=-1) # shape (N, 8, 3) | |
eight_corners = eight_corners * half_sizes # shape (N, 8, 3) | |
# rot_mat: (N, 3, 3), eight_corners: (N, 8, 3) | |
rotated_corners = torch.matmul(eight_corners, | |
rot_mat.transpose(1, 2)) # shape (N, 8, 3) | |
res = centers + rotated_corners | |
if use_batch: | |
res = res.reshape(batch_size, n_proposals, 8, 3) | |
return res | |
def euler_iou3d_corners(boxes1, boxes2): | |
rows = boxes1.shape[0] | |
cols = boxes2.shape[0] | |
if rows * cols == 0: | |
return boxes1.new(rows, cols) | |
_, iou3d = box3d_overlap(boxes1, boxes2) | |
return iou3d | |
def euler_iou3d_bbox(center1, size1, rot1, center2, size2, rot2): | |
"""Calculate the 3D IoU between two grounps of 9DOF bounding boxes. | |
Args: | |
center1 (Tensor): (n, cx, cy, cz) of grounp1. | |
size1 (Tensor): (n, l, w, h) of grounp1. | |
rot1 (Tensor): rot matrix of grounp1. | |
center1 (Tensor): (m, cx, cy, cz) of grounp2. | |
size1 (Tensor): (m, l, w, h) of grounp2. | |
rot1 (Tensor): rot matrix of grounp2. | |
Returns: | |
numpy.ndarray: (n, m) the 3D IoU. | |
""" | |
if torch.cuda.is_available(): | |
center1 = center1.cuda() | |
size1 = size1.cuda() | |
rot1 = rot1.cuda() | |
center2 = center2.cuda() | |
size2 = size2.cuda() | |
rot2 = rot2.cuda() | |
corners1 = bbox_to_corners(center1, size1, rot1) | |
corners2 = bbox_to_corners(center2, size2, rot2) | |
result = euler_iou3d_corners(corners1, corners2) | |
if torch.cuda.is_available(): | |
result = result.detach().cpu() | |
return result.numpy() | |
def index_box(boxes: List[torch.tensor], | |
indices: Union[List[torch.tensor], torch.tensor])\ | |
-> Union[List[torch.tensor], torch.tensor]: | |
"""Convert a grounp of bounding boxes represented in [center, size, rot] | |
format to 9 DoF format. | |
Args: | |
box (list/tuple, tensor): boxes in a grounp. | |
Returns: | |
Tensor : 9 DoF format. (num,9) | |
""" | |
if isinstance(boxes, (list, tuple)): | |
return [index_box(box, indices) for box in boxes] | |
else: | |
return boxes[indices] | |
def to_9dof_box(box: List[torch.tensor]): | |
"""Convert a grounp of bounding boxes represented in [center, size, rot] | |
format to 9 DoF format. | |
Args: | |
box (list/tuple, tensor): boxes in a grounp. | |
Returns: | |
Tensor : 9 DoF format. (num,9) | |
""" | |
return EulerInstance3DBoxes(box, origin=(0.5, 0.5, 0.5)) | |