"git@developer.sourcefind.cn:OpenDAS/ollama.git" did not exist on "38f0c54c6474c32edac717349f033abc79b9774d"
Commit 51e12dea authored by zhangwenwei's avatar zhangwenwei
Browse files

Support box3d structure with unittests

parent 99397168
...@@ -7,6 +7,7 @@ from .iou_calculators import (BboxOverlaps3D, BboxOverlapsNearest3D, ...@@ -7,6 +7,7 @@ from .iou_calculators import (BboxOverlaps3D, BboxOverlapsNearest3D,
from .samplers import (BaseSampler, CombinedSampler, from .samplers import (BaseSampler, CombinedSampler,
InstanceBalancedPosSampler, IoUBalancedNegSampler, InstanceBalancedPosSampler, IoUBalancedNegSampler,
PseudoSampler, RandomSampler, SamplingResult) PseudoSampler, RandomSampler, SamplingResult)
from .structures import Box3DMode, LiDARInstance3DBoxes
from .transforms import boxes3d_to_bev_torch_lidar from .transforms import boxes3d_to_bev_torch_lidar
from .assign_sampling import ( # isort:skip, avoid recursive imports from .assign_sampling import ( # isort:skip, avoid recursive imports
...@@ -20,5 +21,5 @@ __all__ = [ ...@@ -20,5 +21,5 @@ __all__ = [
'build_assigner', 'build_sampler', 'assign_and_sample', 'box_torch_ops', 'build_assigner', 'build_sampler', 'assign_and_sample', 'box_torch_ops',
'build_bbox_coder', 'DeltaXYZWLHRBBoxCoder', 'boxes3d_to_bev_torch_lidar', 'build_bbox_coder', 'DeltaXYZWLHRBBoxCoder', 'boxes3d_to_bev_torch_lidar',
'BboxOverlapsNearest3D', 'BboxOverlaps3D', 'bbox_overlaps_nearest_3d', 'BboxOverlapsNearest3D', 'BboxOverlaps3D', 'bbox_overlaps_nearest_3d',
'bbox_overlaps_3d' 'bbox_overlaps_3d', 'Box3DMode', 'LiDARInstance3DBoxes'
] ]
from .box_3d_mode import Box3DMode
from .lidar_box3d import LiDARInstance3DBoxes
__all__ = ['Box3DMode', 'LiDARInstance3DBoxes']
from abc import abstractmethod
import torch
class BaseInstance3DBoxes(object):
"""Base class for 3D Boxes
"""
def __init__(self, tensor, box_dim=7):
"""
Args:
tensor (torch.Tensor | np.ndarray): a Nxbox_dim matrix.
box_dim (int): number of the dimension of a box
Each row is (x, y, z, x_size, y_size, z_size, yaw).
"""
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()
self.box_dim = box_dim
self.tensor = tensor
@abstractmethod
def volume(self):
"""Computes the volume of all the boxes.
Returns:
torch.Tensor: a vector with volume of each box.
"""
return self.tensor[:, 3] * self.tensor[:, 4] * self.tensor[:, 5]
@abstractmethod
def bottom_center(self):
"""Calculate the bottom center of all the boxes.
Returns:
torch.Tensor: a tensor with center of each box.
"""
return self.tensor[..., :3]
@abstractmethod
def gravity_center(self):
"""Calculate the gravity center of all the boxes.
Returns:
torch.Tensor: a tensor with center of each box.
"""
pass
@abstractmethod
def corners(self):
"""Calculate the coordinates of corners of all the boxes.
Returns:
torch.Tensor: a tensor with 8 corners of each box.
"""
pass
@abstractmethod
def rotate(self, angles, axis=0):
"""Calculate whether the points is in any of the boxes
Args:
angles (float): rotation angles
axis (int): the axis to rotate the boxes
"""
pass
@abstractmethod
def flip(self):
"""Flip the boxes in horizontal direction
"""
pass
@abstractmethod
def translate(self, trans_vector):
"""Calculate whether the points is in any of the boxes
Args:
trans_vector (torch.Tensor): translation vector of size 1x3
"""
pass
@abstractmethod
def in_range(self, box_range):
"""Check whether the boxes are in the given range
Args:
box_range (list | torch.Tensor): the range of box
(x_min, y_min, z_min, x_max, y_max, z_max)
Returns:
a binary vector, indicating whether each box is inside
the reference range.
"""
pass
def nonempty(self, threshold: float = 0.0):
"""Find boxes that are non-empty.
A box is considered empty,
if either of its side is no larger than threshold.
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 scale(self, scale_factors):
"""Scale the box with horizontal and vertical scaling factors
Args:
scale_factors (float | torch.Tensor | list[float]):
scale factors to scale the boxes.
"""
pass
def __getitem__(self, item):
"""
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:
Boxes: Create a new :class:`Boxes` by indexing.
"""
original_type = type(self)
if isinstance(item, int):
return original_type(self.tensor[item].view(1, -1))
b = self.tensor[item]
assert b.dim() == 2, \
f'Indexing on Boxes with {item} failed to return a matrix!'
return original_type(b)
def __len__(self):
return self.tensor.shape[0]
def __repr__(self):
return self.__class__.__name__ + '(\n ' + str(self.tensor) + ')'
@classmethod
def cat(cls, boxes_list):
"""Concatenates a list of Boxes into a single Boxes
Arguments:
boxes_list (list[Boxes])
Returns:
Boxes: 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))
return cat_boxes
def to(self, device):
original_type = type(self)
return original_type(self.tensor.to(device))
def clone(self):
"""Clone the Boxes.
Returns:
Boxes
"""
original_type = type(self)
return original_type(self.tensor.clone())
@property
def device(self):
return self.tensor.device
def __iter__(self):
"""
Yield a box as a Tensor of shape (4,) at a time.
"""
yield from self.tensor
from enum import IntEnum, unique
import numpy as np
import torch
@unique
class Box3DMode(IntEnum):
"""
Enum of different ways to represent a box.
"""
LIDAR = 0
"""
Coordinates in velodyne/LiDAR sensors.
up z x front
^ ^
| /
| /
left y <------ 0
"""
CAM = 1
"""
Coordinates in camera.
x right
/
/
front z <------ 0
|
|
v
down y
"""
DEPTH = 2
"""
Coordinates in Depth mode.
up z x right
^ ^
| /
| /
front y <------ 0
"""
@staticmethod
def convert(box, from_mode, to_mode):
"""
Args:
box: can be a k-tuple, k-list or an Nxk array/tensor, where k = 7
from_mode, to_mode (BoxMode)
Returns:
The converted box of the same type.
"""
if from_mode == to_mode:
return box
original_type = type(box)
is_numpy = isinstance(box, np.ndarray)
single_box = isinstance(box, (list, tuple))
if single_box:
assert len(box) >= 7, (
'BoxMode.convert takes either a k-tuple/list or '
'an Nxk array/tensor, where k >= 7')
arr = torch.tensor(box)[None, :]
else:
# avoid modifying the input box
if is_numpy:
arr = torch.from_numpy(np.asarray(box)).clone()
else:
arr = box.clone()
# converting logic
if single_box:
return original_type(arr.flatten().tolist())
if is_numpy:
return arr.numpy()
else:
return arr
import numpy as np
import torch
from .base_box3d import BaseInstance3DBoxes
from .utils import limit_period, rotation_3d_in_axis
class LiDARInstance3DBoxes(BaseInstance3DBoxes):
"""
This structure stores a list of boxes as a Nx7 torch.Tensor.
It supports some common methods about boxes
(`area`, `clip`, `nonempty`, etc),
and also behaves like a Tensor
(support indexing, `to(device)`, `.device`, and iteration over all boxes)
By default the (x, y, z) is the bottom center of a box
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, yaw, ...).
"""
def gravity_center(self):
"""Calculate the gravity center of all the boxes.
Returns:
torch.Tensor: a tensor with center of each box.
"""
bottom_center = self.bottom_center()
gravity_center = torch.zeros_like(bottom_center)
gravity_center[:, :2] = bottom_center[:, :2]
gravity_center[:, 2] = bottom_center[:, 2] + bottom_center[:, 5] * 0.5
return gravity_center
def corners(self, origin=[0.5, 1.0, 0.5], axis=1):
"""Calculate the coordinates of corners of all the boxes.
Convert the boxes to the form of
(x0y0z0, x0y0z1, x0y1z0, x0y1z1, x1y0z0, x1y0z1, x1y1z0, x1y1z1)
Args:
origin (list[float]): origin point relate to smallest point.
use [0.5, 1.0, 0.5] in camera and [0.5, 0.5, 0] in lidar.
axis (int): rotation axis. 1 for camera and 2 for lidar.
Returns:
torch.Tensor: corners of each box with size (N, 8, 3)
"""
dims = self.tensor[:, 3:6]
corners_norm = torch.from_numpy(
np.stack(np.unravel_index(np.arange(2**3), [2] * 3), axis=1)).to(
device=dims.device, dtype=dims.dtype)
corners_norm = corners_norm[[0, 1, 3, 2, 4, 5, 7, 6]]
corners_norm = corners_norm - dims.new_tensor(origin)
corners = dims.view([-1, 1, 3]) * corners_norm.reshape([1, 2**3, 3])
corners = rotation_3d_in_axis(corners, self.tensor[:, 6], axis=axis)
corners += self.tensor[:, :3].view(-1, 1, 3)
return corners
def nearset_bev(self):
"""Calculate the 2D bounding boxes in BEV without rotation
Returns:
torch.Tensor: a tensor of 2D BEV box of each box.
"""
# Obtain BEV boxes with rotation in XYWHR format
bev_rotated_boxes = self.tensor[:, [0, 1, 3, 4, 6]]
# convert the rotation to a valid range
rotations = bev_rotated_boxes[:, -1]
normed_rotations = torch.abs(limit_period(rotations, 0.5, np.pi))
# find the center of boxes
conditions = (normed_rotations > np.pi / 4)[..., None]
bboxes_xywh = torch.where(conditions, bev_rotated_boxes[:,
[0, 1, 3, 2]],
bev_rotated_boxes[:, :4])
centers = bboxes_xywh[:, :2]
dims = bboxes_xywh[:, 2:]
bev_boxes = torch.cat([centers - dims / 2, centers + dims / 2], dim=-1)
return bev_boxes
def rotate(self, angle):
"""Calculate whether the points is in any of the boxes
Args:
angles (float | torch.Tensor): rotation angle
Returns:
None if `return_rot_mat=False`,
torch.Tensor if `return_rot_mat=True`
"""
if not isinstance(angle, torch.Tensor):
angle = self.tensor.new_tensor(angle)
rot_sin = torch.sin(angle)
rot_cos = torch.cos(angle)
rot_mat_T = self.tensor.new_tensor([[rot_cos, -rot_sin, 0],
[rot_sin, rot_cos, 0], [0, 0, 1]])
self.tensor[:, :3] = self.tensor[:, :3] @ rot_mat_T
self.tensor[:, 6] += angle
def flip(self):
self.tensor[:, 1::7] = -self.tensor[:, 1::7]
self.tensor[:, 6] = -self.tensor[:, 6] + np.pi
def translate(self, trans_vector):
"""Calculate whether the points is in any of the boxes
Args:
trans_vector (torch.Tensor): translation vector of size 1x3
"""
if not isinstance(trans_vector, torch.Tensor):
trans_vector = self.tensor.new_tensor(trans_vector)
self.tensor[:, :3] += trans_vector
def in_range_3d(self, box_range):
"""Check whether the boxes are in the given range
Args:
box_range (list | torch.Tensor): 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 burdun for simpler cases.
TODO: check whether this will effect the performance
Returns:
a binary vector, indicating whether each box 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 in_range_bev(self, box_range):
"""Check whether the boxes are in the given range
Args:
box_range (list | torch.Tensor): the range of box
(x_min, y_min, x_max, y_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 burdun for simpler cases.
TODO: check whether this will effect the performance
Returns:
a binary vector, indicating whether each box is inside
the reference range.
"""
in_range_flags = ((self.tensor[:, 0] > box_range[0])
& (self.tensor[:, 1] > box_range[1])
& (self.tensor[:, 0] < box_range[2])
& (self.tensor[:, 1] < box_range[3]))
return in_range_flags
def scale(self, scale_factor):
"""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
def limit_yaw(self, offset=0.5, period=np.pi):
"""Limit the yaw to a given period and offset
Args:
offset (float): the offset of the yaw
period (float): the expected period
"""
self.tensor[:, 6] = limit_period(self.tensor[:, 6], offset, period)
import numpy as np
import torch
def limit_period(val, offset=0.5, period=np.pi):
return val - torch.floor(val / period + offset) * period
def rotation_3d_in_axis(points, angles, axis=0):
# points: [N, point_size, 3]
# angles: [N]
rot_sin = torch.sin(angles)
rot_cos = torch.cos(angles)
ones = torch.ones_like(rot_cos)
zeros = torch.zeros_like(rot_cos)
if axis == 1:
rot_mat_T = torch.stack([
torch.stack([rot_cos, zeros, -rot_sin]),
torch.stack([zeros, ones, zeros]),
torch.stack([rot_sin, zeros, rot_cos])
])
elif axis == 2 or axis == -1:
rot_mat_T = torch.stack([
torch.stack([rot_cos, -rot_sin, zeros]),
torch.stack([rot_sin, rot_cos, zeros]),
torch.stack([zeros, zeros, ones])
])
elif axis == 0:
rot_mat_T = torch.stack([
torch.stack([zeros, rot_cos, -rot_sin]),
torch.stack([zeros, rot_sin, rot_cos]),
torch.stack([ones, zeros, zeros])
])
else:
raise ValueError('axis should in range')
return torch.einsum('aij,jka->aik', (points, rot_mat_T))
import numpy as np
import torch
from mmdet3d.core.bbox import LiDARInstance3DBoxes
def test_lidar_boxes3d():
# Test init with numpy array
np_boxes = np.array(
[[1.7802081, 2.516249, -1.7501148, 1.75, 3.39, 1.65, 1.48],
[8.959413, 2.4567227, -1.6357126, 1.54, 4.01, 1.57, 1.62]],
dtype=np.float32)
boxes_1 = LiDARInstance3DBoxes(np_boxes)
assert torch.allclose(boxes_1.tensor, torch.from_numpy(np_boxes))
# test init with torch.Tensor
th_boxes = torch.tensor(
[[
28.29669987, -0.5557558, -1.30332506, 1.47000003, 2.23000002,
1.48000002, -1.57000005
],
[
26.66901946, 21.82302134, -1.73605708, 1.55999994, 3.48000002,
1.39999998, -1.69000006
],
[
31.31977974, 8.16214412, -1.62177875, 1.74000001, 3.76999998,
1.48000002, 2.78999996
]],
dtype=torch.float32)
boxes_2 = LiDARInstance3DBoxes(th_boxes)
assert torch.allclose(boxes_2.tensor, th_boxes)
# test clone/to/device
boxes_2 = boxes_2.clone()
boxes_1 = boxes_1.to(boxes_2.device)
# test box concatenation
expected_tensor = torch.tensor(
[[1.7802081, 2.516249, -1.7501148, 1.75, 3.39, 1.65, 1.48],
[8.959413, 2.4567227, -1.6357126, 1.54, 4.01, 1.57, 1.62],
[28.2967, -0.5557558, -1.303325, 1.47, 2.23, 1.48, -1.57],
[26.66902, 21.82302, -1.736057, 1.56, 3.48, 1.4, -1.69],
[31.31978, 8.162144, -1.6217787, 1.74, 3.77, 1.48, 2.79]])
boxes = LiDARInstance3DBoxes.cat([boxes_1, boxes_2])
assert torch.allclose(boxes.tensor, expected_tensor)
# test box flip
expected_tensor = torch.tensor(
[[1.7802081, -2.516249, -1.7501148, 1.75, 3.39, 1.65, 1.6615927],
[8.959413, -2.4567227, -1.6357126, 1.54, 4.01, 1.57, 1.5215927],
[28.2967, 0.5557558, -1.303325, 1.47, 2.23, 1.48, 4.7115927],
[26.66902, -21.82302, -1.736057, 1.56, 3.48, 1.4, 4.8315926],
[31.31978, -8.162144, -1.6217787, 1.74, 3.77, 1.48, 0.35159278]])
boxes.flip()
assert torch.allclose(boxes.tensor, expected_tensor)
# test box rotation
expected_tensor = torch.tensor(
[[1.0385344, -2.9020846, -1.7501148, 1.75, 3.39, 1.65, 1.9336663],
[7.969653, -4.774011, -1.6357126, 1.54, 4.01, 1.57, 1.7936664],
[27.405172, -7.0688415, -1.303325, 1.47, 2.23, 1.48, 4.9836664],
[19.823532, -28.187025, -1.736057, 1.56, 3.48, 1.4, 5.1036663],
[27.974297, -16.27845, -1.6217787, 1.74, 3.77, 1.48, 0.6236664]])
boxes.rotate(0.27207362796436096)
assert torch.allclose(boxes.tensor, expected_tensor)
# test box scaling
expected_tensor = torch.tensor([[
1.0443488, -2.9183323, -1.7599131, 1.7597977, 3.4089797, 1.6592377,
1.9336663
],
[
8.014273, -4.8007393, -1.6448704,
1.5486219, 4.0324507, 1.57879,
1.7936664
],
[
27.558605, -7.1084175, -1.310622,
1.4782301, 2.242485, 1.488286,
4.9836664
],
[
19.934517, -28.344835, -1.7457767,
1.5687338, 3.4994833, 1.4078381,
5.1036663
],
[
28.130915, -16.369587, -1.6308585,
1.7497417, 3.791107, 1.488286,
0.6236664
]])
boxes.scale(1.00559866335275)
assert torch.allclose(boxes.tensor, expected_tensor)
# test box translation
expected_tensor = torch.tensor([[
1.1281544, -3.0507944, -1.9169292, 1.7597977, 3.4089797, 1.6592377,
1.9336663
],
[
8.098079, -4.9332013, -1.8018866,
1.5486219, 4.0324507, 1.57879,
1.7936664
],
[
27.64241, -7.2408795, -1.4676381,
1.4782301, 2.242485, 1.488286,
4.9836664
],
[
20.018322, -28.477297, -1.9027928,
1.5687338, 3.4994833, 1.4078381,
5.1036663
],
[
28.21472, -16.502048, -1.7878747,
1.7497417, 3.791107, 1.488286,
0.6236664
]])
boxes.translate([0.0838056, -0.13246193, -0.15701613])
assert torch.allclose(boxes.tensor, expected_tensor)
# test bbox in_range_bev
expected_tensor = torch.tensor([1, 1, 1, 1, 1], dtype=torch.bool)
mask = boxes.in_range_bev([0., -40., 70.4, 40.])
assert (mask == expected_tensor).all()
mask = boxes.nonempty()
assert (mask == expected_tensor).all()
# test bbox indexing
index_boxes = boxes[2:5]
expected_tensor = torch.tensor([[
27.64241, -7.2408795, -1.4676381, 1.4782301, 2.242485, 1.488286,
4.9836664
],
[
20.018322, -28.477297, -1.9027928,
1.5687338, 3.4994833, 1.4078381,
5.1036663
],
[
28.21472, -16.502048, -1.7878747,
1.7497417, 3.791107, 1.488286,
0.6236664
]])
assert len(index_boxes) == 3
assert torch.allclose(index_boxes.tensor, expected_tensor)
index_boxes = boxes[2]
expected_tensor = torch.tensor([[
27.64241, -7.2408795, -1.4676381, 1.4782301, 2.242485, 1.488286,
4.9836664
]])
assert len(index_boxes) == 1
assert torch.allclose(index_boxes.tensor, expected_tensor)
index_boxes = boxes[[2, 4]]
expected_tensor = torch.tensor([[
27.64241, -7.2408795, -1.4676381, 1.4782301, 2.242485, 1.488286,
4.9836664
],
[
28.21472, -16.502048, -1.7878747,
1.7497417, 3.791107, 1.488286,
0.6236664
]])
assert len(index_boxes) == 2
assert torch.allclose(index_boxes.tensor, expected_tensor)
# test iteration
for i, box in enumerate(index_boxes):
torch.allclose(box, expected_tensor[i])
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment