Unverified Commit 32a4328b authored by Wenwei Zhang's avatar Wenwei Zhang Committed by GitHub
Browse files

Bump version to V1.0.0rc0

Bump version to V1.0.0rc0
parents 86cc487c a8817998
...@@ -29,7 +29,7 @@ class PartialBinBasedBBoxCoder(BaseBBoxCoder): ...@@ -29,7 +29,7 @@ class PartialBinBasedBBoxCoder(BaseBBoxCoder):
"""Encode ground truth to prediction targets. """Encode ground truth to prediction targets.
Args: Args:
gt_bboxes_3d (BaseInstance3DBoxes): Ground truth bboxes \ gt_bboxes_3d (BaseInstance3DBoxes): Ground truth bboxes
with shape (n, 7). with shape (n, 7).
gt_labels_3d (torch.Tensor): Ground truth classes. gt_labels_3d (torch.Tensor): Ground truth classes.
......
# Copyright (c) OpenMMLab. All rights reserved.
import numpy as np
import torch
from torch.nn import functional as F
from mmdet.core.bbox.builder import BBOX_CODERS
from .fcos3d_bbox_coder import FCOS3DBBoxCoder
@BBOX_CODERS.register_module()
class PGDBBoxCoder(FCOS3DBBoxCoder):
"""Bounding box coder for PGD."""
def encode(self, gt_bboxes_3d, gt_labels_3d, gt_bboxes, gt_labels):
# TODO: refactor the encoder codes in the FCOS3D and PGD head
pass
def decode_2d(self,
bbox,
scale,
stride,
max_regress_range,
training,
pred_keypoints=False,
pred_bbox2d=True):
"""Decode regressed 2D attributes.
Args:
bbox (torch.Tensor): Raw bounding box predictions in shape
[N, C, H, W].
scale (tuple[`Scale`]): Learnable scale parameters.
stride (int): Stride for a specific feature level.
max_regress_range (int): Maximum regression range for a specific
feature level.
training (bool): Whether the decoding is in the training
procedure.
pred_keypoints (bool, optional): Whether to predict keypoints.
Defaults to False.
pred_bbox2d (bool, optional): Whether to predict 2D bounding
boxes. Defaults to False.
Returns:
torch.Tensor: Decoded boxes.
"""
clone_bbox = bbox.clone()
if pred_keypoints:
scale_kpts = scale[3]
# 2 dimension of offsets x 8 corners of a 3D bbox
bbox[:, self.bbox_code_size:self.bbox_code_size + 16] = \
torch.tanh(scale_kpts(clone_bbox[
:, self.bbox_code_size:self.bbox_code_size + 16]).float())
if pred_bbox2d:
scale_bbox2d = scale[-1]
# The last four dimensions are offsets to four sides of a 2D bbox
bbox[:, -4:] = scale_bbox2d(clone_bbox[:, -4:]).float()
if self.norm_on_bbox:
if pred_bbox2d:
bbox[:, -4:] = F.relu(bbox.clone()[:, -4:])
if not training:
if pred_keypoints:
bbox[
:, self.bbox_code_size:self.bbox_code_size + 16] *= \
max_regress_range
if pred_bbox2d:
bbox[:, -4:] *= stride
else:
if pred_bbox2d:
bbox[:, -4:] = bbox.clone()[:, -4:].exp()
return bbox
def decode_prob_depth(self, depth_cls_preds, depth_range, depth_unit,
division, num_depth_cls):
"""Decode probabilistic depth map.
Args:
depth_cls_preds (torch.Tensor): Depth probabilistic map in shape
[..., self.num_depth_cls] (raw output before softmax).
depth_range (tuple[float]): Range of depth estimation.
depth_unit (int): Unit of depth range division.
division (str): Depth division method. Options include 'uniform',
'linear', 'log', 'loguniform'.
num_depth_cls (int): Number of depth classes.
Returns:
torch.Tensor: Decoded probabilistic depth estimation.
"""
if division == 'uniform':
depth_multiplier = depth_unit * \
depth_cls_preds.new_tensor(
list(range(num_depth_cls))).reshape([1, -1])
prob_depth_preds = (F.softmax(depth_cls_preds.clone(), dim=-1) *
depth_multiplier).sum(dim=-1)
return prob_depth_preds
elif division == 'linear':
split_pts = depth_cls_preds.new_tensor(list(
range(num_depth_cls))).reshape([1, -1])
depth_multiplier = depth_range[0] + (
depth_range[1] - depth_range[0]) / \
(num_depth_cls * (num_depth_cls - 1)) * \
(split_pts * (split_pts+1))
prob_depth_preds = (F.softmax(depth_cls_preds.clone(), dim=-1) *
depth_multiplier).sum(dim=-1)
return prob_depth_preds
elif division == 'log':
split_pts = depth_cls_preds.new_tensor(list(
range(num_depth_cls))).reshape([1, -1])
start = max(depth_range[0], 1)
end = depth_range[1]
depth_multiplier = (np.log(start) +
split_pts * np.log(end / start) /
(num_depth_cls - 1)).exp()
prob_depth_preds = (F.softmax(depth_cls_preds.clone(), dim=-1) *
depth_multiplier).sum(dim=-1)
return prob_depth_preds
elif division == 'loguniform':
split_pts = depth_cls_preds.new_tensor(list(
range(num_depth_cls))).reshape([1, -1])
start = max(depth_range[0], 1)
end = depth_range[1]
log_multiplier = np.log(start) + \
split_pts * np.log(end / start) / (num_depth_cls - 1)
prob_depth_preds = (F.softmax(depth_cls_preds.clone(), dim=-1) *
log_multiplier).sum(dim=-1).exp()
return prob_depth_preds
else:
raise NotImplementedError
# Copyright (c) OpenMMLab. All rights reserved.
import numpy as np
import torch
from mmdet.core.bbox import BaseBBoxCoder
from mmdet.core.bbox.builder import BBOX_CODERS
@BBOX_CODERS.register_module()
class PointXYZWHLRBBoxCoder(BaseBBoxCoder):
"""Point based bbox coder for 3D boxes.
Args:
code_size (int): The dimension of boxes to be encoded.
use_mean_size (bool, optional): Whether using anchors based on class.
Defaults to True.
mean_size (list[list[float]], optional): Mean size of bboxes in
each class. Defaults to None.
"""
def __init__(self, code_size=7, use_mean_size=True, mean_size=None):
super(PointXYZWHLRBBoxCoder, self).__init__()
self.code_size = code_size
self.use_mean_size = use_mean_size
if self.use_mean_size:
self.mean_size = torch.from_numpy(np.array(mean_size)).float()
assert self.mean_size.min() > 0, \
f'The min of mean_size should > 0, however currently it is '\
f'{self.mean_size.min()}, please check it in your config.'
def encode(self, gt_bboxes_3d, points, gt_labels_3d=None):
"""Encode ground truth to prediction targets.
Args:
gt_bboxes_3d (:obj:`BaseInstance3DBoxes`): Ground truth bboxes
with shape (N, 7 + C).
points (torch.Tensor): Point cloud with shape (N, 3).
gt_labels_3d (torch.Tensor, optional): Ground truth classes.
Defaults to None.
Returns:
torch.Tensor: Encoded boxes with shape (N, 8 + C).
"""
gt_bboxes_3d[:, 3:6] = torch.clamp_min(gt_bboxes_3d[:, 3:6], min=1e-5)
xg, yg, zg, dxg, dyg, dzg, rg, *cgs = torch.split(
gt_bboxes_3d, 1, dim=-1)
xa, ya, za = torch.split(points, 1, dim=-1)
if self.use_mean_size:
assert gt_labels_3d.max() <= self.mean_size.shape[0] - 1, \
f'the max gt label {gt_labels_3d.max()} is bigger than' \
f'anchor types {self.mean_size.shape[0] - 1}.'
self.mean_size = self.mean_size.to(gt_labels_3d.device)
point_anchor_size = self.mean_size[gt_labels_3d]
dxa, dya, dza = torch.split(point_anchor_size, 1, dim=-1)
diagonal = torch.sqrt(dxa**2 + dya**2)
xt = (xg - xa) / diagonal
yt = (yg - ya) / diagonal
zt = (zg - za) / dza
dxt = torch.log(dxg / dxa)
dyt = torch.log(dyg / dya)
dzt = torch.log(dzg / dza)
else:
xt = (xg - xa)
yt = (yg - ya)
zt = (zg - za)
dxt = torch.log(dxg)
dyt = torch.log(dyg)
dzt = torch.log(dzg)
return torch.cat(
[xt, yt, zt, dxt, dyt, dzt,
torch.cos(rg),
torch.sin(rg), *cgs],
dim=-1)
def decode(self, box_encodings, points, pred_labels_3d=None):
"""Decode predicted parts and points to bbox3d.
Args:
box_encodings (torch.Tensor): Encoded boxes with shape (N, 8 + C).
points (torch.Tensor): Point cloud with shape (N, 3).
pred_labels_3d (torch.Tensor): Bbox predicted labels (N, M).
Returns:
torch.Tensor: Decoded boxes with shape (N, 7 + C)
"""
xt, yt, zt, dxt, dyt, dzt, cost, sint, *cts = torch.split(
box_encodings, 1, dim=-1)
xa, ya, za = torch.split(points, 1, dim=-1)
if self.use_mean_size:
assert pred_labels_3d.max() <= self.mean_size.shape[0] - 1, \
f'The max pred label {pred_labels_3d.max()} is bigger than' \
f'anchor types {self.mean_size.shape[0] - 1}.'
self.mean_size = self.mean_size.to(pred_labels_3d.device)
point_anchor_size = self.mean_size[pred_labels_3d]
dxa, dya, dza = torch.split(point_anchor_size, 1, dim=-1)
diagonal = torch.sqrt(dxa**2 + dya**2)
xg = xt * diagonal + xa
yg = yt * diagonal + ya
zg = zt * dza + za
dxg = torch.exp(dxt) * dxa
dyg = torch.exp(dyt) * dya
dzg = torch.exp(dzt) * dza
else:
xg = xt + xa
yg = yt + ya
zg = zt + za
dxg, dyg, dzg = torch.split(
torch.exp(box_encodings[..., 3:6]), 1, dim=-1)
rg = torch.atan2(sint, cost)
return torch.cat([xg, yg, zg, dxg, dyg, dzg, rg, *cts], dim=-1)
# Copyright (c) OpenMMLab. All rights reserved.
import numpy as np
import torch
from mmdet.core.bbox import BaseBBoxCoder
from mmdet.core.bbox.builder import BBOX_CODERS
@BBOX_CODERS.register_module()
class SMOKECoder(BaseBBoxCoder):
"""Bbox Coder for SMOKE.
Args:
base_depth (tuple[float]): Depth references for decode box depth.
base_dims (tuple[tuple[float]]): Dimension references [l, h, w]
for decode box dimension for each category.
code_size (int): The dimension of boxes to be encoded.
"""
def __init__(self, base_depth, base_dims, code_size):
super(SMOKECoder, self).__init__()
self.base_depth = base_depth
self.base_dims = base_dims
self.bbox_code_size = code_size
def encode(self, locations, dimensions, orientations, input_metas):
"""Encode CameraInstance3DBoxes by locations, dimensions, orientations.
Args:
locations (Tensor): Center location for 3D boxes.
(N, 3)
dimensions (Tensor): Dimensions for 3D boxes.
shape (N, 3)
orientations (Tensor): Orientations for 3D boxes.
shape (N, 1)
input_metas (list[dict]): Meta information of each image, e.g.,
image size, scaling factor, etc.
Return:
:obj:`CameraInstance3DBoxes`: 3D bboxes of batch images,
shape (N, bbox_code_size).
"""
bboxes = torch.cat((locations, dimensions, orientations), dim=1)
assert bboxes.shape[1] == self.bbox_code_size, 'bboxes shape dose not'\
'match the bbox_code_size.'
batch_bboxes = input_metas[0]['box_type_3d'](
bboxes, box_dim=self.bbox_code_size)
return batch_bboxes
def decode(self,
reg,
points,
labels,
cam2imgs,
trans_mats,
locations=None):
"""Decode regression into locations, dimensions, orientations.
Args:
reg (Tensor): Batch regression for each predict center2d point.
shape: (batch * K (max_objs), C)
points(Tensor): Batch projected bbox centers on image plane.
shape: (batch * K (max_objs) , 2)
labels (Tensor): Batch predict class label for each predict
center2d point.
shape: (batch, K (max_objs))
cam2imgs (Tensor): Batch images' camera intrinsic matrix.
shape: kitti (batch, 4, 4) nuscenes (batch, 3, 3)
trans_mats (Tensor): transformation matrix from original image
to feature map.
shape: (batch, 3, 3)
locations (None | Tensor): if locations is None, this function
is used to decode while inference, otherwise, it's used while
training using the ground truth 3d bbox locations.
shape: (batch * K (max_objs), 3)
Return:
tuple(Tensor): The tuple has components below:
- locations (Tensor): Centers of 3D boxes.
shape: (batch * K (max_objs), 3)
- dimensions (Tensor): Dimensions of 3D boxes.
shape: (batch * K (max_objs), 3)
- orientations (Tensor): Orientations of 3D
boxes.
shape: (batch * K (max_objs), 1)
"""
depth_offsets = reg[:, 0]
centers2d_offsets = reg[:, 1:3]
dimensions_offsets = reg[:, 3:6]
orientations = reg[:, 6:8]
depths = self._decode_depth(depth_offsets)
# get the 3D Bounding box's center location.
pred_locations = self._decode_location(points, centers2d_offsets,
depths, cam2imgs, trans_mats)
pred_dimensions = self._decode_dimension(labels, dimensions_offsets)
if locations is None:
pred_orientations = self._decode_orientation(
orientations, pred_locations)
else:
pred_orientations = self._decode_orientation(
orientations, locations)
return pred_locations, pred_dimensions, pred_orientations
def _decode_depth(self, depth_offsets):
"""Transform depth offset to depth."""
base_depth = depth_offsets.new_tensor(self.base_depth)
depths = depth_offsets * base_depth[1] + base_depth[0]
return depths
def _decode_location(self, points, centers2d_offsets, depths, cam2imgs,
trans_mats):
"""Retrieve objects location in camera coordinate based on projected
points.
Args:
points (Tensor): Projected points on feature map in (x, y)
shape: (batch * K, 2)
centers2d_offset (Tensor): Project points offset in
(delta_x, delta_y). shape: (batch * K, 2)
depths (Tensor): Object depth z.
shape: (batch * K)
cam2imgs (Tensor): Batch camera intrinsics matrix.
shape: kitti (batch, 4, 4) nuscenes (batch, 3, 3)
trans_mats (Tensor): transformation matrix from original image
to feature map.
shape: (batch, 3, 3)
"""
# number of points
N = centers2d_offsets.shape[0]
# batch_size
N_batch = cam2imgs.shape[0]
batch_id = torch.arange(N_batch).unsqueeze(1)
obj_id = batch_id.repeat(1, N // N_batch).flatten()
trans_mats_inv = trans_mats.inverse()[obj_id]
cam2imgs_inv = cam2imgs.inverse()[obj_id]
centers2d = points + centers2d_offsets
centers2d_extend = torch.cat((centers2d, centers2d.new_ones(N, 1)),
dim=1)
# expand project points as [N, 3, 1]
centers2d_extend = centers2d_extend.unsqueeze(-1)
# transform project points back on original image
centers2d_img = torch.matmul(trans_mats_inv, centers2d_extend)
centers2d_img = centers2d_img * depths.view(N, -1, 1)
if cam2imgs.shape[1] == 4:
centers2d_img = torch.cat(
(centers2d_img, centers2d.new_ones(N, 1, 1)), dim=1)
locations = torch.matmul(cam2imgs_inv, centers2d_img).squeeze(2)
return locations[:, :3]
def _decode_dimension(self, labels, dims_offset):
"""Transform dimension offsets to dimension according to its category.
Args:
labels (Tensor): Each points' category id.
shape: (N, K)
dims_offset (Tensor): Dimension offsets.
shape: (N, 3)
"""
labels = labels.flatten().long()
base_dims = dims_offset.new_tensor(self.base_dims)
dims_select = base_dims[labels, :]
dimensions = dims_offset.exp() * dims_select
return dimensions
def _decode_orientation(self, ori_vector, locations):
"""Retrieve object orientation.
Args:
ori_vector (Tensor): Local orientation in [sin, cos] format.
shape: (N, 2)
locations (Tensor): Object location.
shape: (N, 3)
Return:
Tensor: yaw(Orientation). Notice that the yaw's
range is [-np.pi, np.pi].
shape:(N, 1)
"""
assert len(ori_vector) == len(locations)
locations = locations.view(-1, 3)
rays = torch.atan(locations[:, 0] / (locations[:, 2] + 1e-7))
alphas = torch.atan(ori_vector[:, 0] / (ori_vector[:, 1] + 1e-7))
# get cosine value positive and negative index.
cos_pos_inds = (ori_vector[:, 1] >= 0).nonzero(as_tuple=False)
cos_neg_inds = (ori_vector[:, 1] < 0).nonzero(as_tuple=False)
alphas[cos_pos_inds] -= np.pi / 2
alphas[cos_neg_inds] += np.pi / 2
# retrieve object rotation y angle.
yaws = alphas + rays
larger_inds = (yaws > np.pi).nonzero(as_tuple=False)
small_inds = (yaws < -np.pi).nonzero(as_tuple=False)
if len(larger_inds) != 0:
yaws[larger_inds] -= 2 * np.pi
if len(small_inds) != 0:
yaws[small_inds] += 2 * np.pi
yaws = yaws.unsqueeze(-1)
return yaws
...@@ -31,15 +31,17 @@ class BboxOverlapsNearest3D(object): ...@@ -31,15 +31,17 @@ class BboxOverlapsNearest3D(object):
between each aligned pair of bboxes1 and bboxes2. between each aligned pair of bboxes1 and bboxes2.
Args: Args:
bboxes1 (torch.Tensor): shape (N, 7+N) [x, y, z, h, w, l, ry, v]. bboxes1 (torch.Tensor): shape (N, 7+N)
bboxes2 (torch.Tensor): shape (M, 7+N) [x, y, z, h, w, l, ry, v]. [x, y, z, x_size, y_size, z_size, ry, v].
bboxes2 (torch.Tensor): shape (M, 7+N)
[x, y, z, x_size, y_size, z_size, ry, v].
mode (str): "iou" (intersection over union) or iof mode (str): "iou" (intersection over union) or iof
(intersection over foreground). (intersection over foreground).
is_aligned (bool): Whether the calculation is aligned. is_aligned (bool): Whether the calculation is aligned.
Return: Return:
torch.Tensor: If ``is_aligned`` is ``True``, return ious between \ torch.Tensor: If ``is_aligned`` is ``True``, return ious between
bboxes1 and bboxes2 with shape (M, N). If ``is_aligned`` is \ bboxes1 and bboxes2 with shape (M, N). If ``is_aligned`` is
``False``, return shape is M. ``False``, return shape is M.
""" """
return bbox_overlaps_nearest_3d(bboxes1, bboxes2, mode, is_aligned, return bbox_overlaps_nearest_3d(bboxes1, bboxes2, mode, is_aligned,
...@@ -74,13 +76,15 @@ class BboxOverlaps3D(object): ...@@ -74,13 +76,15 @@ class BboxOverlaps3D(object):
calculate the actual 3D IoUs of boxes. calculate the actual 3D IoUs of boxes.
Args: Args:
bboxes1 (torch.Tensor): shape (N, 7+C) [x, y, z, h, w, l, ry]. bboxes1 (torch.Tensor): with shape (N, 7+C),
bboxes2 (torch.Tensor): shape (M, 7+C) [x, y, z, h, w, l, ry]. (x, y, z, x_size, y_size, z_size, ry, v*).
bboxes2 (torch.Tensor): with shape (M, 7+C),
(x, y, z, x_size, y_size, z_size, ry, v*).
mode (str): "iou" (intersection over union) or mode (str): "iou" (intersection over union) or
iof (intersection over foreground). iof (intersection over foreground).
Return: Return:
torch.Tensor: Bbox overlaps results of bboxes1 and bboxes2 \ torch.Tensor: Bbox overlaps results of bboxes1 and bboxes2
with shape (M, N) (aligned mode is not supported currently). with shape (M, N) (aligned mode is not supported currently).
""" """
return bbox_overlaps_3d(bboxes1, bboxes2, mode, self.coordinate) return bbox_overlaps_3d(bboxes1, bboxes2, mode, self.coordinate)
...@@ -102,7 +106,7 @@ def bbox_overlaps_nearest_3d(bboxes1, ...@@ -102,7 +106,7 @@ def bbox_overlaps_nearest_3d(bboxes1,
Note: Note:
This function first finds the nearest 2D boxes in bird eye view This function first finds the nearest 2D boxes in bird eye view
(BEV), and then calculates the 2D IoU using :meth:`bbox_overlaps`. (BEV), and then calculates the 2D IoU using :meth:`bbox_overlaps`.
Ths IoU calculator :class:`BboxOverlapsNearest3D` uses this This IoU calculator :class:`BboxOverlapsNearest3D` uses this
function to calculate IoUs of boxes. function to calculate IoUs of boxes.
If ``is_aligned`` is ``False``, then it calculates the ious between If ``is_aligned`` is ``False``, then it calculates the ious between
...@@ -110,15 +114,17 @@ def bbox_overlaps_nearest_3d(bboxes1, ...@@ -110,15 +114,17 @@ def bbox_overlaps_nearest_3d(bboxes1,
aligned pair of bboxes1 and bboxes2. aligned pair of bboxes1 and bboxes2.
Args: Args:
bboxes1 (torch.Tensor): shape (N, 7+C) [x, y, z, h, w, l, ry, v]. bboxes1 (torch.Tensor): with shape (N, 7+C),
bboxes2 (torch.Tensor): shape (M, 7+C) [x, y, z, h, w, l, ry, v]. (x, y, z, x_size, y_size, z_size, ry, v*).
bboxes2 (torch.Tensor): with shape (M, 7+C),
(x, y, z, x_size, y_size, z_size, ry, v*).
mode (str): "iou" (intersection over union) or iof mode (str): "iou" (intersection over union) or iof
(intersection over foreground). (intersection over foreground).
is_aligned (bool): Whether the calculation is aligned is_aligned (bool): Whether the calculation is aligned
Return: Return:
torch.Tensor: If ``is_aligned`` is ``True``, return ious between \ torch.Tensor: If ``is_aligned`` is ``True``, return ious between
bboxes1 and bboxes2 with shape (M, N). If ``is_aligned`` is \ bboxes1 and bboxes2 with shape (M, N). If ``is_aligned`` is
``False``, return shape is M. ``False``, return shape is M.
""" """
assert bboxes1.size(-1) == bboxes2.size(-1) >= 7 assert bboxes1.size(-1) == bboxes2.size(-1) >= 7
...@@ -148,14 +154,16 @@ def bbox_overlaps_3d(bboxes1, bboxes2, mode='iou', coordinate='camera'): ...@@ -148,14 +154,16 @@ def bbox_overlaps_3d(bboxes1, bboxes2, mode='iou', coordinate='camera'):
calculate the actual IoUs of boxes. calculate the actual IoUs of boxes.
Args: Args:
bboxes1 (torch.Tensor): shape (N, 7+C) [x, y, z, h, w, l, ry]. bboxes1 (torch.Tensor): with shape (N, 7+C),
bboxes2 (torch.Tensor): shape (M, 7+C) [x, y, z, h, w, l, ry]. (x, y, z, x_size, y_size, z_size, ry, v*).
bboxes2 (torch.Tensor): with shape (M, 7+C),
(x, y, z, x_size, y_size, z_size, ry, v*).
mode (str): "iou" (intersection over union) or mode (str): "iou" (intersection over union) or
iof (intersection over foreground). iof (intersection over foreground).
coordinate (str): 'camera' or 'lidar' coordinate system. coordinate (str): 'camera' or 'lidar' coordinate system.
Return: Return:
torch.Tensor: Bbox overlaps results of bboxes1 and bboxes2 \ torch.Tensor: Bbox overlaps results of bboxes1 and bboxes2
with shape (M, N) (aligned mode is not supported currently). with shape (M, N) (aligned mode is not supported currently).
""" """
assert bboxes1.size(-1) == bboxes2.size(-1) >= 7 assert bboxes1.size(-1) == bboxes2.size(-1) >= 7
...@@ -185,7 +193,7 @@ class AxisAlignedBboxOverlaps3D(object): ...@@ -185,7 +193,7 @@ class AxisAlignedBboxOverlaps3D(object):
mode (str): "iou" (intersection over union) or "giou" (generalized mode (str): "iou" (intersection over union) or "giou" (generalized
intersection over union). intersection over union).
is_aligned (bool, optional): If True, then m and n must be equal. is_aligned (bool, optional): If True, then m and n must be equal.
Default False. Defaults to False.
Returns: Returns:
Tensor: shape (m, n) if ``is_aligned`` is False else shape (m,) Tensor: shape (m, n) if ``is_aligned`` is False else shape (m,)
""" """
...@@ -219,9 +227,9 @@ def axis_aligned_bbox_overlaps_3d(bboxes1, ...@@ -219,9 +227,9 @@ def axis_aligned_bbox_overlaps_3d(bboxes1,
mode (str): "iou" (intersection over union) or "giou" (generalized mode (str): "iou" (intersection over union) or "giou" (generalized
intersection over union). intersection over union).
is_aligned (bool, optional): If True, then m and n must be equal. is_aligned (bool, optional): If True, then m and n must be equal.
Default False. Defaults to False.
eps (float, optional): A value added to the denominator for numerical eps (float, optional): A value added to the denominator for numerical
stability. Default 1e-6. stability. Defaults to 1e-6.
Returns: Returns:
Tensor: shape (m, n) if ``is_aligned`` is False else shape (m,) Tensor: shape (m, n) if ``is_aligned`` is False else shape (m,)
...@@ -250,7 +258,7 @@ def axis_aligned_bbox_overlaps_3d(bboxes1, ...@@ -250,7 +258,7 @@ def axis_aligned_bbox_overlaps_3d(bboxes1,
""" """
assert mode in ['iou', 'giou'], f'Unsupported mode {mode}' assert mode in ['iou', 'giou'], f'Unsupported mode {mode}'
# Either the boxes are empty or the length of boxes's last dimenstion is 6 # Either the boxes are empty or the length of boxes's last dimension is 6
assert (bboxes1.size(-1) == 6 or bboxes1.size(0) == 0) assert (bboxes1.size(-1) == 6 or bboxes1.size(0) == 0)
assert (bboxes2.size(-1) == 6 or bboxes2.size(0) == 0) assert (bboxes2.size(-1) == 6 or bboxes2.size(0) == 0)
......
...@@ -9,8 +9,8 @@ from . import RandomSampler, SamplingResult ...@@ -9,8 +9,8 @@ from . import RandomSampler, SamplingResult
class IoUNegPiecewiseSampler(RandomSampler): class IoUNegPiecewiseSampler(RandomSampler):
"""IoU Piece-wise Sampling. """IoU Piece-wise Sampling.
Sampling negtive proposals according to a list of IoU thresholds. Sampling negative proposals according to a list of IoU thresholds.
The negtive proposals are divided into several pieces according The negative proposals are divided into several pieces according
to `neg_iou_piece_thrs`. And the ratio of each piece is indicated to `neg_iou_piece_thrs`. And the ratio of each piece is indicated
by `neg_piece_fractions`. by `neg_piece_fractions`.
...@@ -18,11 +18,11 @@ class IoUNegPiecewiseSampler(RandomSampler): ...@@ -18,11 +18,11 @@ class IoUNegPiecewiseSampler(RandomSampler):
num (int): Number of proposals. num (int): Number of proposals.
pos_fraction (float): The fraction of positive proposals. pos_fraction (float): The fraction of positive proposals.
neg_piece_fractions (list): A list contains fractions that indicates neg_piece_fractions (list): A list contains fractions that indicates
the ratio of each piece of total negtive samplers. the ratio of each piece of total negative samplers.
neg_iou_piece_thrs (list): A list contains IoU thresholds that neg_iou_piece_thrs (list): A list contains IoU thresholds that
indicate the upper bound of this piece. indicate the upper bound of this piece.
neg_pos_ub (float): The total ratio to limit the upper bound neg_pos_ub (float): The total ratio to limit the upper bound
number of negtive samples. number of negative samples.
add_gt_as_proposals (bool): Whether to add gt as proposals. add_gt_as_proposals (bool): Whether to add gt as proposals.
""" """
...@@ -59,8 +59,8 @@ class IoUNegPiecewiseSampler(RandomSampler): ...@@ -59,8 +59,8 @@ class IoUNegPiecewiseSampler(RandomSampler):
neg_inds = torch.nonzero(assign_result.gt_inds == 0, as_tuple=False) neg_inds = torch.nonzero(assign_result.gt_inds == 0, as_tuple=False)
if neg_inds.numel() != 0: if neg_inds.numel() != 0:
neg_inds = neg_inds.squeeze(1) neg_inds = neg_inds.squeeze(1)
if len(neg_inds) <= num_expected: if len(neg_inds) <= 0:
return neg_inds return neg_inds.squeeze(1)
else: else:
neg_inds_choice = neg_inds.new_zeros([0]) neg_inds_choice = neg_inds.new_zeros([0])
extend_num = 0 extend_num = 0
...@@ -88,12 +88,38 @@ class IoUNegPiecewiseSampler(RandomSampler): ...@@ -88,12 +88,38 @@ class IoUNegPiecewiseSampler(RandomSampler):
neg_inds_choice = torch.cat( neg_inds_choice = torch.cat(
[neg_inds_choice, neg_inds[piece_neg_inds]], dim=0) [neg_inds_choice, neg_inds[piece_neg_inds]], dim=0)
extend_num += piece_expected_num - len(piece_neg_inds) extend_num += piece_expected_num - len(piece_neg_inds)
# for the last piece
if piece_inds == self.neg_piece_num - 1:
extend_neg_num = num_expected - len(neg_inds_choice)
# if the numbers of nagetive samples > 0, we will
# randomly select num_expected samples in last piece
if piece_neg_inds.numel() > 0:
rand_idx = torch.randint(
low=0,
high=piece_neg_inds.numel(),
size=(extend_neg_num, )).long()
neg_inds_choice = torch.cat(
[neg_inds_choice, piece_neg_inds[rand_idx]],
dim=0)
# if the numbers of nagetive samples == 0, we will
# randomly select num_expected samples in all
# previous pieces
else:
rand_idx = torch.randint(
low=0,
high=neg_inds_choice.numel(),
size=(extend_neg_num, )).long()
neg_inds_choice = torch.cat(
[neg_inds_choice, neg_inds_choice[rand_idx]],
dim=0)
else: else:
piece_choice = self.random_choice(piece_neg_inds, piece_choice = self.random_choice(piece_neg_inds,
piece_expected_num) piece_expected_num)
neg_inds_choice = torch.cat( neg_inds_choice = torch.cat(
[neg_inds_choice, neg_inds[piece_choice]], dim=0) [neg_inds_choice, neg_inds[piece_choice]], dim=0)
extend_num = 0 extend_num = 0
assert len(neg_inds_choice) == num_expected
return neg_inds_choice return neg_inds_choice
def sample(self, def sample(self,
...@@ -111,7 +137,7 @@ class IoUNegPiecewiseSampler(RandomSampler): ...@@ -111,7 +137,7 @@ class IoUNegPiecewiseSampler(RandomSampler):
assign_result (:obj:`AssignResult`): Bbox assigning results. assign_result (:obj:`AssignResult`): Bbox assigning results.
bboxes (torch.Tensor): Boxes to be sampled from. bboxes (torch.Tensor): Boxes to be sampled from.
gt_bboxes (torch.Tensor): Ground truth bboxes. gt_bboxes (torch.Tensor): Ground truth bboxes.
gt_labels (torch.Tensor, optional): Class labels of ground truth \ gt_labels (torch.Tensor, optional): Class labels of ground truth
bboxes. bboxes.
Returns: Returns:
...@@ -145,7 +171,6 @@ class IoUNegPiecewiseSampler(RandomSampler): ...@@ -145,7 +171,6 @@ class IoUNegPiecewiseSampler(RandomSampler):
num_expected_neg = neg_upper_bound num_expected_neg = neg_upper_bound
neg_inds = self.neg_sampler._sample_neg( neg_inds = self.neg_sampler._sample_neg(
assign_result, num_expected_neg, bboxes=bboxes, **kwargs) assign_result, num_expected_neg, bboxes=bboxes, **kwargs)
neg_inds = neg_inds.unique()
sampling_result = SamplingResult(pos_inds, neg_inds, bboxes, gt_bboxes, sampling_result = SamplingResult(pos_inds, neg_inds, bboxes, gt_bboxes,
assign_result, gt_flags) assign_result, gt_flags)
......
...@@ -6,12 +6,13 @@ from .coord_3d_mode import Coord3DMode ...@@ -6,12 +6,13 @@ from .coord_3d_mode import Coord3DMode
from .depth_box3d import DepthInstance3DBoxes from .depth_box3d import DepthInstance3DBoxes
from .lidar_box3d import LiDARInstance3DBoxes from .lidar_box3d import LiDARInstance3DBoxes
from .utils import (get_box_type, get_proj_mat_by_coord_type, limit_period, from .utils import (get_box_type, get_proj_mat_by_coord_type, limit_period,
mono_cam_box2vis, points_cam2img, rotation_3d_in_axis, mono_cam_box2vis, points_cam2img, points_img2cam,
xywhr2xyxyr) rotation_3d_in_axis, xywhr2xyxyr)
__all__ = [ __all__ = [
'Box3DMode', 'BaseInstance3DBoxes', 'LiDARInstance3DBoxes', 'Box3DMode', 'BaseInstance3DBoxes', 'LiDARInstance3DBoxes',
'CameraInstance3DBoxes', 'DepthInstance3DBoxes', 'xywhr2xyxyr', 'CameraInstance3DBoxes', 'DepthInstance3DBoxes', 'xywhr2xyxyr',
'get_box_type', 'rotation_3d_in_axis', 'limit_period', 'points_cam2img', 'get_box_type', 'rotation_3d_in_axis', 'limit_period', 'points_cam2img',
'Coord3DMode', 'mono_cam_box2vis', 'get_proj_mat_by_coord_type' 'points_img2cam', 'Coord3DMode', 'mono_cam_box2vis',
'get_proj_mat_by_coord_type'
] ]
# Copyright (c) OpenMMLab. All rights reserved. # Copyright (c) OpenMMLab. All rights reserved.
import warnings
from abc import abstractmethod
import numpy as np import numpy as np
import torch import torch
from abc import abstractmethod
from mmdet3d.ops import points_in_boxes_all, points_in_boxes_part
from mmdet3d.ops.iou3d import iou3d_cuda from mmdet3d.ops.iou3d import iou3d_cuda
from .utils import limit_period, xywhr2xyxyr from .utils import limit_period, xywhr2xyxyr
...@@ -18,12 +21,12 @@ class BaseInstance3DBoxes(object): ...@@ -18,12 +21,12 @@ class BaseInstance3DBoxes(object):
tensor (torch.Tensor | np.ndarray | list): a N x box_dim matrix. tensor (torch.Tensor | np.ndarray | list): a N x box_dim matrix.
box_dim (int): Number of the dimension of a box. box_dim (int): Number of the dimension of a box.
Each row is (x, y, z, x_size, y_size, z_size, yaw). Each row is (x, y, z, x_size, y_size, z_size, yaw).
Default to 7. Defaults to 7.
with_yaw (bool): Whether the box is with yaw rotation. with_yaw (bool): Whether the box is with yaw rotation.
If False, the value of yaw will be set to 0 as minmax boxes. If False, the value of yaw will be set to 0 as minmax boxes.
Default to True. Defaults to True.
origin (tuple[float]): The relative position of origin in the box. origin (tuple[float], optional): Relative position of the box origin.
Default to (0.5, 0.5, 0). This will guide the box be converted to Defaults to (0.5, 0.5, 0). This will guide the box be converted to
(0.5, 0.5, 0) mode. (0.5, 0.5, 0) mode.
Attributes: Attributes:
...@@ -72,27 +75,29 @@ class BaseInstance3DBoxes(object): ...@@ -72,27 +75,29 @@ class BaseInstance3DBoxes(object):
@property @property
def dims(self): def dims(self):
"""torch.Tensor: Corners of each box with size (N, 8, 3).""" """torch.Tensor: Size dimensions of each box in shape (N, 3)."""
return self.tensor[:, 3:6] return self.tensor[:, 3:6]
@property @property
def yaw(self): def yaw(self):
"""torch.Tensor: A vector with yaw of each box.""" """torch.Tensor: A vector with yaw of each box in shape (N, )."""
return self.tensor[:, 6] return self.tensor[:, 6]
@property @property
def height(self): def height(self):
"""torch.Tensor: A vector with height of each box.""" """torch.Tensor: A vector with height of each box in shape (N, )."""
return self.tensor[:, 5] return self.tensor[:, 5]
@property @property
def top_height(self): def top_height(self):
"""torch.Tensor: A vector with the top height of each box.""" """torch.Tensor:
A vector with the top height of each box in shape (N, )."""
return self.bottom_height + self.height return self.bottom_height + self.height
@property @property
def bottom_height(self): def bottom_height(self):
"""torch.Tensor: A vector with bottom's height of each box.""" """torch.Tensor:
A vector with bottom's height of each box in shape (N, )."""
return self.tensor[:, 2] return self.tensor[:, 2]
@property @property
...@@ -100,58 +105,114 @@ class BaseInstance3DBoxes(object): ...@@ -100,58 +105,114 @@ class BaseInstance3DBoxes(object):
"""Calculate the center of all the boxes. """Calculate the center of all the boxes.
Note: Note:
In the MMDetection3D's convention, the bottom center is In MMDetection3D's convention, the bottom center is
usually taken as the default center. usually taken as the default center.
The relative position of the centers in different kinds of The relative position of the centers in different kinds of
boxes are different, e.g., the relative center of a boxes is 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. (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`` It is recommended to use ``bottom_center`` or ``gravity_center``
for more clear usage. for clearer usage.
Returns: Returns:
torch.Tensor: A tensor with center of each box. torch.Tensor: A tensor with center of each box in shape (N, 3).
""" """
return self.bottom_center return self.bottom_center
@property @property
def bottom_center(self): def bottom_center(self):
"""torch.Tensor: A tensor with center of each box.""" """torch.Tensor: A tensor with center of each box in shape (N, 3)."""
return self.tensor[:, :3] return self.tensor[:, :3]
@property @property
def gravity_center(self): def gravity_center(self):
"""torch.Tensor: A tensor with center of each box.""" """torch.Tensor: A tensor with center of each box in shape (N, 3)."""
pass pass
@property @property
def corners(self): def corners(self):
"""torch.Tensor: a tensor with 8 corners of each box.""" """torch.Tensor:
a tensor with 8 corners of each box in shape (N, 8, 3)."""
pass pass
@property
def bev(self):
"""torch.Tensor: 2D BEV box of each box with rotation
in XYWHR format, in shape (N, 5)."""
return self.tensor[:, [0, 1, 3, 4, 6]]
@property
def nearest_bev(self):
"""torch.Tensor: A tensor of 2D BEV box of each box
without rotation."""
# Obtain BEV boxes with rotation in XYWHR format
bev_rotated_boxes = self.bev
# 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 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:
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:
torch.Tensor: 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
@abstractmethod @abstractmethod
def rotate(self, angle, points=None): def rotate(self, angle, points=None):
"""Rotate boxes with points (optional) with the given angle or \ """Rotate boxes with points (optional) with the given angle or rotation
rotation matrix. matrix.
Args: Args:
angle (float | torch.Tensor | np.ndarray): angle (float | torch.Tensor | np.ndarray):
Rotation angle or rotation matrix. Rotation angle or rotation matrix.
points (torch.Tensor, numpy.ndarray, :obj:`BasePoints`, optional): points (torch.Tensor | numpy.ndarray |
:obj:`BasePoints`, optional):
Points to rotate. Defaults to None. Points to rotate. Defaults to None.
""" """
pass pass
@abstractmethod @abstractmethod
def flip(self, bev_direction='horizontal'): def flip(self, bev_direction='horizontal'):
"""Flip the boxes in BEV along given BEV direction.""" """Flip the boxes in BEV along given BEV direction.
Args:
bev_direction (str, optional): Direction by which to flip.
Can be chosen from 'horizontal' and 'vertical'.
Defaults to 'horizontal'.
"""
pass pass
def translate(self, trans_vector): def translate(self, trans_vector):
"""Translate boxes with the given translation vector. """Translate boxes with the given translation vector.
Args: Args:
trans_vector (torch.Tensor): Translation vector of size 1x3. trans_vector (torch.Tensor): Translation vector of size (1, 3).
""" """
if not isinstance(trans_vector, torch.Tensor): if not isinstance(trans_vector, torch.Tensor):
trans_vector = self.tensor.new_tensor(trans_vector) trans_vector = self.tensor.new_tensor(trans_vector)
...@@ -170,7 +231,7 @@ class BaseInstance3DBoxes(object): ...@@ -170,7 +231,7 @@ class BaseInstance3DBoxes(object):
polygon, we try to reduce the burden for simpler cases. polygon, we try to reduce the burden for simpler cases.
Returns: Returns:
torch.Tensor: A binary vector indicating whether each box is \ torch.Tensor: A binary vector indicating whether each box is
inside the reference range. inside the reference range.
""" """
in_range_flags = ((self.tensor[:, 0] > box_range[0]) in_range_flags = ((self.tensor[:, 0] > box_range[0])
...@@ -181,34 +242,21 @@ class BaseInstance3DBoxes(object): ...@@ -181,34 +242,21 @@ class BaseInstance3DBoxes(object):
& (self.tensor[:, 2] < box_range[5])) & (self.tensor[:, 2] < box_range[5]))
return in_range_flags return in_range_flags
@abstractmethod
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
in order of (x_min, y_min, x_max, y_max).
Returns:
torch.Tensor: Indicating whether each box is inside \
the reference range.
"""
pass
@abstractmethod @abstractmethod
def convert_to(self, dst, rt_mat=None): def convert_to(self, dst, rt_mat=None):
"""Convert self to ``dst`` mode. """Convert self to ``dst`` mode.
Args: Args:
dst (:obj:`Box3DMode`): The target Box mode. dst (:obj:`Box3DMode`): The target Box mode.
rt_mat (np.ndarray | torch.Tensor): The rotation and translation rt_mat (np.ndarray | torch.Tensor, optional): The rotation and
matrix between different coordinates. Defaults to None. translation matrix between different coordinates.
Defaults to None.
The conversion from `src` coordinates to `dst` coordinates The conversion from `src` coordinates to `dst` coordinates
usually comes along the change of sensors, e.g., from camera usually comes along the change of sensors, e.g., from camera
to LiDAR. This requires a transformation matrix. to LiDAR. This requires a transformation matrix.
Returns: Returns:
:obj:`BaseInstance3DBoxes`: The converted box of the same type \ :obj:`BaseInstance3DBoxes`: The converted box of the same type
in the `dst` mode. in the `dst` mode.
""" """
pass pass
...@@ -220,28 +268,29 @@ class BaseInstance3DBoxes(object): ...@@ -220,28 +268,29 @@ class BaseInstance3DBoxes(object):
scale_factors (float): Scale factors to scale the boxes. scale_factors (float): Scale factors to scale the boxes.
""" """
self.tensor[:, :6] *= scale_factor self.tensor[:, :6] *= scale_factor
self.tensor[:, 7:] *= scale_factor self.tensor[:, 7:] *= scale_factor # velocity
def limit_yaw(self, offset=0.5, period=np.pi): def limit_yaw(self, offset=0.5, period=np.pi):
"""Limit the yaw to a given period and offset. """Limit the yaw to a given period and offset.
Args: Args:
offset (float): The offset of the yaw. offset (float, optional): The offset of the yaw. Defaults to 0.5.
period (float): The expected period. period (float, optional): The expected period. Defaults to np.pi.
""" """
self.tensor[:, 6] = limit_period(self.tensor[:, 6], offset, period) self.tensor[:, 6] = limit_period(self.tensor[:, 6], offset, period)
def nonempty(self, threshold: float = 0.0): def nonempty(self, threshold=0.0):
"""Find boxes that are non-empty. """Find boxes that are non-empty.
A box is considered empty, A box is considered empty,
if either of its side is no larger than threshold. if either of its side is no larger than threshold.
Args: Args:
threshold (float): The threshold of minimal sizes. threshold (float, optional): The threshold of minimal sizes.
Defaults to 0.0.
Returns: Returns:
torch.Tensor: A binary vector which represents whether each \ torch.Tensor: A binary vector which represents whether each
box is empty (False) or non-empty (True). box is empty (False) or non-empty (True).
""" """
box = self.tensor box = self.tensor
...@@ -267,8 +316,8 @@ class BaseInstance3DBoxes(object): ...@@ -267,8 +316,8 @@ class BaseInstance3DBoxes(object):
subject to Pytorch's indexing semantics. subject to Pytorch's indexing semantics.
Returns: Returns:
:obj:`BaseInstance3DBoxes`: A new object of \ :obj:`BaseInstance3DBoxes`: A new object of
:class:`BaseInstances3DBoxes` after indexing. :class:`BaseInstance3DBoxes` after indexing.
""" """
original_type = type(self) original_type = type(self)
if isinstance(item, int): if isinstance(item, int):
...@@ -319,7 +368,7 @@ class BaseInstance3DBoxes(object): ...@@ -319,7 +368,7 @@ class BaseInstance3DBoxes(object):
device (str | :obj:`torch.device`): The name of the device. device (str | :obj:`torch.device`): The name of the device.
Returns: Returns:
:obj:`BaseInstance3DBoxes`: A new boxes object on the \ :obj:`BaseInstance3DBoxes`: A new boxes object on the
specific device. specific device.
""" """
original_type = type(self) original_type = type(self)
...@@ -332,7 +381,7 @@ class BaseInstance3DBoxes(object): ...@@ -332,7 +381,7 @@ class BaseInstance3DBoxes(object):
"""Clone the Boxes. """Clone the Boxes.
Returns: Returns:
:obj:`BaseInstance3DBoxes`: Box object with the same properties \ :obj:`BaseInstance3DBoxes`: Box object with the same properties
as self. as self.
""" """
original_type = type(self) original_type = type(self)
...@@ -363,7 +412,7 @@ class BaseInstance3DBoxes(object): ...@@ -363,7 +412,7 @@ class BaseInstance3DBoxes(object):
Args: Args:
boxes1 (:obj:`BaseInstance3DBoxes`): Boxes 1 contain N boxes. boxes1 (:obj:`BaseInstance3DBoxes`): Boxes 1 contain N boxes.
boxes2 (:obj:`BaseInstance3DBoxes`): Boxes 2 contain M boxes. boxes2 (:obj:`BaseInstance3DBoxes`): Boxes 2 contain M boxes.
mode (str, optional): Mode of iou calculation. Defaults to 'iou'. mode (str, optional): Mode of IoU calculation. Defaults to 'iou'.
Returns: Returns:
torch.Tensor: Calculated iou of boxes. torch.Tensor: Calculated iou of boxes.
...@@ -444,14 +493,14 @@ class BaseInstance3DBoxes(object): ...@@ -444,14 +493,14 @@ class BaseInstance3DBoxes(object):
def new_box(self, data): def new_box(self, data):
"""Create a new box object with data. """Create a new box object with data.
The new box and its tensor has the similar properties \ The new box and its tensor has the similar properties
as self and self.tensor, respectively. as self and self.tensor, respectively.
Args: Args:
data (torch.Tensor | numpy.array | list): Data to be copied. data (torch.Tensor | numpy.array | list): Data to be copied.
Returns: Returns:
:obj:`BaseInstance3DBoxes`: A new bbox object with ``data``, \ :obj:`BaseInstance3DBoxes`: A new bbox object with ``data``,
the object's other properties are similar to ``self``. the object's other properties are similar to ``self``.
""" """
new_tensor = self.tensor.new_tensor(data) \ new_tensor = self.tensor.new_tensor(data) \
...@@ -459,3 +508,75 @@ class BaseInstance3DBoxes(object): ...@@ -459,3 +508,75 @@ class BaseInstance3DBoxes(object):
original_type = type(self) original_type = type(self)
return original_type( return original_type(
new_tensor, box_dim=self.box_dim, with_yaw=self.with_yaw) new_tensor, box_dim=self.box_dim, with_yaw=self.with_yaw)
def points_in_boxes_part(self, points, boxes_override=None):
"""Find the box in which each point is.
Args:
points (torch.Tensor): Points in shape (1, M, 3) or (M, 3),
3 dimensions are (x, y, z) in LiDAR or depth coordinate.
boxes_override (torch.Tensor, optional): Boxes to override
`self.tensor`. Defaults to None.
Returns:
torch.Tensor: The index of the first box that each point
is in, in shape (M, ). Default value is -1
(if the point is not enclosed by any box).
Note:
If a point is enclosed by multiple boxes, the index of the
first box will be returned.
"""
if boxes_override is not None:
boxes = boxes_override
else:
boxes = self.tensor
if points.dim() == 2:
points = points.unsqueeze(0)
box_idx = points_in_boxes_part(points,
boxes.unsqueeze(0).to(
points.device)).squeeze(0)
return box_idx
def points_in_boxes_all(self, points, boxes_override=None):
"""Find all boxes in which each point is.
Args:
points (torch.Tensor): Points in shape (1, M, 3) or (M, 3),
3 dimensions are (x, y, z) in LiDAR or depth coordinate.
boxes_override (torch.Tensor, optional): Boxes to override
`self.tensor`. Defaults to None.
Returns:
torch.Tensor: A tensor indicating whether a point is in a box,
in shape (M, T). T is the number of boxes. Denote this
tensor as A, if the m^th point is in the t^th box, then
`A[m, t] == 1`, elsewise `A[m, t] == 0`.
"""
if boxes_override is not None:
boxes = boxes_override
else:
boxes = self.tensor
points_clone = points.clone()[..., :3]
if points_clone.dim() == 2:
points_clone = points_clone.unsqueeze(0)
else:
assert points_clone.dim() == 3 and points_clone.shape[0] == 1
boxes = boxes.to(points_clone.device).unsqueeze(0)
box_idxs_of_pts = points_in_boxes_all(points_clone, boxes)
return box_idxs_of_pts.squeeze(0)
def points_in_boxes(self, points, boxes_override=None):
warnings.warn('DeprecationWarning: points_in_boxes is a '
'deprecated method, please consider using '
'points_in_boxes_part.')
return self.points_in_boxes_part(points, boxes_override)
def points_in_boxes_batch(self, points, boxes_override=None):
warnings.warn('DeprecationWarning: points_in_boxes_batch is a '
'deprecated method, please consider using '
'points_in_boxes_all.')
return self.points_in_boxes_all(points, boxes_override)
# Copyright (c) OpenMMLab. All rights reserved. # Copyright (c) OpenMMLab. All rights reserved.
from enum import IntEnum, unique
import numpy as np import numpy as np
import torch import torch
from enum import IntEnum, unique
from .base_box3d import BaseInstance3DBoxes from .base_box3d import BaseInstance3DBoxes
from .cam_box3d import CameraInstance3DBoxes from .cam_box3d import CameraInstance3DBoxes
from .depth_box3d import DepthInstance3DBoxes from .depth_box3d import DepthInstance3DBoxes
from .lidar_box3d import LiDARInstance3DBoxes from .lidar_box3d import LiDARInstance3DBoxes
from .utils import limit_period
@unique @unique
...@@ -61,23 +63,28 @@ class Box3DMode(IntEnum): ...@@ -61,23 +63,28 @@ class Box3DMode(IntEnum):
DEPTH = 2 DEPTH = 2
@staticmethod @staticmethod
def convert(box, src, dst, rt_mat=None): def convert(box, src, dst, rt_mat=None, with_yaw=True):
"""Convert boxes from `src` mode to `dst` mode. """Convert boxes from `src` mode to `dst` mode.
Args: Args:
box (tuple | list | np.ndarray | box (tuple | list | np.ndarray |
torch.Tensor | BaseInstance3DBoxes): torch.Tensor | :obj:`BaseInstance3DBoxes`):
Can be a k-tuple, k-list or an Nxk array/tensor, where k = 7. Can be a k-tuple, k-list or an Nxk array/tensor, where k = 7.
src (:obj:`Box3DMode`): The src Box mode. src (:obj:`Box3DMode`): The src Box mode.
dst (:obj:`Box3DMode`): The target Box mode. dst (:obj:`Box3DMode`): The target Box mode.
rt_mat (np.ndarray | torch.Tensor): The rotation and translation rt_mat (np.ndarray | torch.Tensor, optional): The rotation and
matrix between different coordinates. Defaults to None. translation matrix between different coordinates.
Defaults to None.
The conversion from `src` coordinates to `dst` coordinates The conversion from `src` coordinates to `dst` coordinates
usually comes along the change of sensors, e.g., from camera usually comes along the change of sensors, e.g., from camera
to LiDAR. This requires a transformation matrix. to LiDAR. This requires a transformation matrix.
with_yaw (bool, optional): If `box` is an instance of
:obj:`BaseInstance3DBoxes`, whether or not it has a yaw angle.
Defaults to True.
Returns: Returns:
(tuple | list | np.ndarray | torch.Tensor | BaseInstance3DBoxes): \ (tuple | list | np.ndarray | torch.Tensor |
:obj:`BaseInstance3DBoxes`):
The converted box of the same type. The converted box of the same type.
""" """
if src == dst: if src == dst:
...@@ -100,32 +107,53 @@ class Box3DMode(IntEnum): ...@@ -100,32 +107,53 @@ class Box3DMode(IntEnum):
else: else:
arr = box.clone() arr = box.clone()
if is_Instance3DBoxes:
with_yaw = box.with_yaw
# convert box from `src` mode to `dst` mode. # convert box from `src` mode to `dst` mode.
x_size, y_size, z_size = arr[..., 3:4], arr[..., 4:5], arr[..., 5:6] x_size, y_size, z_size = arr[..., 3:4], arr[..., 4:5], arr[..., 5:6]
if with_yaw:
yaw = arr[..., 6:7]
if src == Box3DMode.LIDAR and dst == Box3DMode.CAM: if src == Box3DMode.LIDAR and dst == Box3DMode.CAM:
if rt_mat is None: if rt_mat is None:
rt_mat = arr.new_tensor([[0, -1, 0], [0, 0, -1], [1, 0, 0]]) rt_mat = arr.new_tensor([[0, -1, 0], [0, 0, -1], [1, 0, 0]])
xyz_size = torch.cat([y_size, z_size, x_size], dim=-1) xyz_size = torch.cat([x_size, z_size, y_size], dim=-1)
if with_yaw:
yaw = -yaw - np.pi / 2
yaw = limit_period(yaw, period=np.pi * 2)
elif src == Box3DMode.CAM and dst == Box3DMode.LIDAR: elif src == Box3DMode.CAM and dst == Box3DMode.LIDAR:
if rt_mat is None: if rt_mat is None:
rt_mat = arr.new_tensor([[0, 0, 1], [-1, 0, 0], [0, -1, 0]]) rt_mat = arr.new_tensor([[0, 0, 1], [-1, 0, 0], [0, -1, 0]])
xyz_size = torch.cat([z_size, x_size, y_size], dim=-1) xyz_size = torch.cat([x_size, z_size, y_size], dim=-1)
if with_yaw:
yaw = -yaw - np.pi / 2
yaw = limit_period(yaw, period=np.pi * 2)
elif src == Box3DMode.DEPTH and dst == Box3DMode.CAM: elif src == Box3DMode.DEPTH and dst == Box3DMode.CAM:
if rt_mat is None: if rt_mat is None:
rt_mat = arr.new_tensor([[1, 0, 0], [0, 0, 1], [0, -1, 0]]) rt_mat = arr.new_tensor([[1, 0, 0], [0, 0, -1], [0, 1, 0]])
xyz_size = torch.cat([x_size, z_size, y_size], dim=-1) xyz_size = torch.cat([x_size, z_size, y_size], dim=-1)
if with_yaw:
yaw = -yaw
elif src == Box3DMode.CAM and dst == Box3DMode.DEPTH: elif src == Box3DMode.CAM and dst == Box3DMode.DEPTH:
if rt_mat is None: if rt_mat is None:
rt_mat = arr.new_tensor([[1, 0, 0], [0, 0, -1], [0, 1, 0]]) rt_mat = arr.new_tensor([[1, 0, 0], [0, 0, 1], [0, -1, 0]])
xyz_size = torch.cat([x_size, z_size, y_size], dim=-1) xyz_size = torch.cat([x_size, z_size, y_size], dim=-1)
if with_yaw:
yaw = -yaw
elif src == Box3DMode.LIDAR and dst == Box3DMode.DEPTH: elif src == Box3DMode.LIDAR and dst == Box3DMode.DEPTH:
if rt_mat is None: if rt_mat is None:
rt_mat = arr.new_tensor([[0, -1, 0], [1, 0, 0], [0, 0, 1]]) rt_mat = arr.new_tensor([[0, -1, 0], [1, 0, 0], [0, 0, 1]])
xyz_size = torch.cat([y_size, x_size, z_size], dim=-1) xyz_size = torch.cat([x_size, y_size, z_size], dim=-1)
if with_yaw:
yaw = yaw + np.pi / 2
yaw = limit_period(yaw, period=np.pi * 2)
elif src == Box3DMode.DEPTH and dst == Box3DMode.LIDAR: elif src == Box3DMode.DEPTH and dst == Box3DMode.LIDAR:
if rt_mat is None: if rt_mat is None:
rt_mat = arr.new_tensor([[0, 1, 0], [-1, 0, 0], [0, 0, 1]]) rt_mat = arr.new_tensor([[0, 1, 0], [-1, 0, 0], [0, 0, 1]])
xyz_size = torch.cat([y_size, x_size, z_size], dim=-1) xyz_size = torch.cat([x_size, y_size, z_size], dim=-1)
if with_yaw:
yaw = yaw - np.pi / 2
yaw = limit_period(yaw, period=np.pi * 2)
else: else:
raise NotImplementedError( raise NotImplementedError(
f'Conversion from Box3DMode {src} to {dst} ' f'Conversion from Box3DMode {src} to {dst} '
...@@ -135,13 +163,17 @@ class Box3DMode(IntEnum): ...@@ -135,13 +163,17 @@ class Box3DMode(IntEnum):
rt_mat = arr.new_tensor(rt_mat) rt_mat = arr.new_tensor(rt_mat)
if rt_mat.size(1) == 4: if rt_mat.size(1) == 4:
extended_xyz = torch.cat( extended_xyz = torch.cat(
[arr[:, :3], arr.new_ones(arr.size(0), 1)], dim=-1) [arr[..., :3], arr.new_ones(arr.size(0), 1)], dim=-1)
xyz = extended_xyz @ rt_mat.t() xyz = extended_xyz @ rt_mat.t()
else: else:
xyz = arr[:, :3] @ rt_mat.t() xyz = arr[..., :3] @ rt_mat.t()
remains = arr[..., 6:] if with_yaw:
arr = torch.cat([xyz[:, :3], xyz_size, remains], dim=-1) remains = arr[..., 7:]
arr = torch.cat([xyz[..., :3], xyz_size, yaw, remains], dim=-1)
else:
remains = arr[..., 6:]
arr = torch.cat([xyz[..., :3], xyz_size, remains], dim=-1)
# convert arr to the original type # convert arr to the original type
original_type = type(box) original_type = type(box)
...@@ -160,7 +192,6 @@ class Box3DMode(IntEnum): ...@@ -160,7 +192,6 @@ class Box3DMode(IntEnum):
raise NotImplementedError( raise NotImplementedError(
f'Conversion to {dst} through {original_type}' f'Conversion to {dst} through {original_type}'
' is not supported yet') ' is not supported yet')
return target_type( return target_type(arr, box_dim=arr.size(-1), with_yaw=with_yaw)
arr, box_dim=arr.size(-1), with_yaw=box.with_yaw)
else: else:
return arr return arr
...@@ -2,9 +2,9 @@ ...@@ -2,9 +2,9 @@
import numpy as np import numpy as np
import torch import torch
from mmdet3d.core.points import BasePoints from ...points import BasePoints
from .base_box3d import BaseInstance3DBoxes from .base_box3d import BaseInstance3DBoxes
from .utils import limit_period, rotation_3d_in_axis from .utils import rotation_3d_in_axis, yaw2local
class CameraInstance3DBoxes(BaseInstance3DBoxes): class CameraInstance3DBoxes(BaseInstance3DBoxes):
...@@ -28,16 +28,14 @@ class CameraInstance3DBoxes(BaseInstance3DBoxes): ...@@ -28,16 +28,14 @@ class CameraInstance3DBoxes(BaseInstance3DBoxes):
The yaw is 0 at the positive direction of x axis, and decreases from The yaw is 0 at the positive direction of x axis, and decreases from
the positive direction of x to the positive direction of z. the positive direction of x to the positive direction of z.
A refactor is ongoing to make the three coordinate systems
easier to understand and convert between each other.
Attributes: Attributes:
tensor (torch.Tensor): Float matrix of N x box_dim. tensor (torch.Tensor): Float matrix in shape (N, box_dim).
box_dim (int): Integer indicates the dimension of a box box_dim (int): Integer indicating the dimension of a box
Each row is (x, y, z, x_size, y_size, z_size, yaw, ...). 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 with_yaw (bool): If True, the value of yaw will be set to 0 as
boxes. axis-aligned boxes tightly enclosing the original boxes.
""" """
YAW_AXIS = 1
def __init__(self, def __init__(self,
tensor, tensor,
...@@ -76,23 +74,39 @@ class CameraInstance3DBoxes(BaseInstance3DBoxes): ...@@ -76,23 +74,39 @@ class CameraInstance3DBoxes(BaseInstance3DBoxes):
@property @property
def height(self): def height(self):
"""torch.Tensor: A vector with height of each box.""" """torch.Tensor: A vector with height of each box in shape (N, )."""
return self.tensor[:, 4] return self.tensor[:, 4]
@property @property
def top_height(self): def top_height(self):
"""torch.Tensor: A vector with the top height of each box.""" """torch.Tensor:
A vector with the top height of each box in shape (N, )."""
# the positive direction is down rather than up # the positive direction is down rather than up
return self.bottom_height - self.height return self.bottom_height - self.height
@property @property
def bottom_height(self): def bottom_height(self):
"""torch.Tensor: A vector with bottom's height of each box.""" """torch.Tensor:
A vector with bottom's height of each box in shape (N, )."""
return self.tensor[:, 1] return self.tensor[:, 1]
@property
def local_yaw(self):
"""torch.Tensor:
A vector with local yaw of each box in shape (N, ).
local_yaw equals to alpha in kitti, which is commonly
used in monocular 3D object detection task, so only
:obj:`CameraInstance3DBoxes` has the property.
"""
yaw = self.yaw
loc = self.gravity_center
local_yaw = yaw2local(yaw, loc)
return local_yaw
@property @property
def gravity_center(self): def gravity_center(self):
"""torch.Tensor: A tensor with center of each box.""" """torch.Tensor: A tensor with center of each box in shape (N, 3)."""
bottom_center = self.bottom_center bottom_center = self.bottom_center
gravity_center = torch.zeros_like(bottom_center) gravity_center = torch.zeros_like(bottom_center)
gravity_center[:, [0, 2]] = bottom_center[:, [0, 2]] gravity_center[:, [0, 2]] = bottom_center[:, [0, 2]]
...@@ -137,82 +151,66 @@ class CameraInstance3DBoxes(BaseInstance3DBoxes): ...@@ -137,82 +151,66 @@ class CameraInstance3DBoxes(BaseInstance3DBoxes):
corners_norm = corners_norm - dims.new_tensor([0.5, 1, 0.5]) corners_norm = corners_norm - dims.new_tensor([0.5, 1, 0.5])
corners = dims.view([-1, 1, 3]) * corners_norm.reshape([1, 8, 3]) corners = dims.view([-1, 1, 3]) * corners_norm.reshape([1, 8, 3])
# rotate around y axis corners = rotation_3d_in_axis(
corners = rotation_3d_in_axis(corners, self.tensor[:, 6], axis=1) corners, self.tensor[:, 6], axis=self.YAW_AXIS)
corners += self.tensor[:, :3].view(-1, 1, 3) corners += self.tensor[:, :3].view(-1, 1, 3)
return corners return corners
@property @property
def bev(self): def bev(self):
"""torch.Tensor: A n x 5 tensor of 2D BEV box of each box """torch.Tensor: 2D BEV box of each box with rotation
with rotation in XYWHR format.""" in XYWHR format, in shape (N, 5)."""
return self.tensor[:, [0, 2, 3, 5, 6]] bev = self.tensor[:, [0, 2, 3, 5, 6]].clone()
# positive direction of the gravity axis
@property # in cam coord system points to the earth
def nearest_bev(self): # so the bev yaw angle needs to be reversed
"""torch.Tensor: A tensor of 2D BEV box of each box bev[:, -1] = -bev[:, -1]
without rotation.""" return bev
# Obtain BEV boxes with rotation in XZWHR format
bev_rotated_boxes = self.bev
# 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, points=None): def rotate(self, angle, points=None):
"""Rotate boxes with points (optional) with the given angle or \ """Rotate boxes with points (optional) with the given angle or rotation
rotation matrix. matrix.
Args: Args:
angle (float | torch.Tensor | np.ndarray): angle (float | torch.Tensor | np.ndarray):
Rotation angle or rotation matrix. Rotation angle or rotation matrix.
points (torch.Tensor, numpy.ndarray, :obj:`BasePoints`, optional): points (torch.Tensor | np.ndarray | :obj:`BasePoints`, optional):
Points to rotate. Defaults to None. Points to rotate. Defaults to None.
Returns: Returns:
tuple or None: When ``points`` is None, the function returns \ tuple or None: When ``points`` is None, the function returns
None, otherwise it returns the rotated points and the \ None, otherwise it returns the rotated points and the
rotation matrix ``rot_mat_T``. rotation matrix ``rot_mat_T``.
""" """
if not isinstance(angle, torch.Tensor): if not isinstance(angle, torch.Tensor):
angle = self.tensor.new_tensor(angle) angle = self.tensor.new_tensor(angle)
assert angle.shape == torch.Size([3, 3]) or angle.numel() == 1, \ assert angle.shape == torch.Size([3, 3]) or angle.numel() == 1, \
f'invalid rotation angle shape {angle.shape}' f'invalid rotation angle shape {angle.shape}'
if angle.numel() == 1: if angle.numel() == 1:
rot_sin = torch.sin(angle) self.tensor[:, 0:3], rot_mat_T = rotation_3d_in_axis(
rot_cos = torch.cos(angle) self.tensor[:, 0:3],
rot_mat_T = self.tensor.new_tensor([[rot_cos, 0, -rot_sin], angle,
[0, 1, 0], axis=self.YAW_AXIS,
[rot_sin, 0, rot_cos]]) return_mat=True)
else: else:
rot_mat_T = angle rot_mat_T = angle
rot_sin = rot_mat_T[2, 0] rot_sin = rot_mat_T[2, 0]
rot_cos = rot_mat_T[0, 0] rot_cos = rot_mat_T[0, 0]
angle = np.arctan2(rot_sin, rot_cos) angle = np.arctan2(rot_sin, rot_cos)
self.tensor[:, 0:3] = self.tensor[:, 0:3] @ rot_mat_T
self.tensor[:, :3] = self.tensor[:, :3] @ rot_mat_T
self.tensor[:, 6] += angle self.tensor[:, 6] += angle
if points is not None: if points is not None:
if isinstance(points, torch.Tensor): if isinstance(points, torch.Tensor):
points[:, :3] = points[:, :3] @ rot_mat_T points[:, :3] = points[:, :3] @ rot_mat_T
elif isinstance(points, np.ndarray): elif isinstance(points, np.ndarray):
rot_mat_T = rot_mat_T.numpy() rot_mat_T = rot_mat_T.cpu().numpy()
points[:, :3] = np.dot(points[:, :3], rot_mat_T) points[:, :3] = np.dot(points[:, :3], rot_mat_T)
elif isinstance(points, BasePoints): elif isinstance(points, BasePoints):
# clockwise points.rotate(rot_mat_T)
points.rotate(-angle)
else: else:
raise ValueError raise ValueError
return points, rot_mat_T return points, rot_mat_T
...@@ -224,7 +222,7 @@ class CameraInstance3DBoxes(BaseInstance3DBoxes): ...@@ -224,7 +222,7 @@ class CameraInstance3DBoxes(BaseInstance3DBoxes):
Args: Args:
bev_direction (str): Flip direction (horizontal or vertical). bev_direction (str): Flip direction (horizontal or vertical).
points (torch.Tensor, numpy.ndarray, :obj:`BasePoints`, None): points (torch.Tensor | np.ndarray | :obj:`BasePoints`, optional):
Points to flip. Defaults to None. Points to flip. Defaults to None.
Returns: Returns:
...@@ -251,28 +249,6 @@ class CameraInstance3DBoxes(BaseInstance3DBoxes): ...@@ -251,28 +249,6 @@ class CameraInstance3DBoxes(BaseInstance3DBoxes):
points.flip(bev_direction) points.flip(bev_direction)
return points return points
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, z_min, x_max, z_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:
torch.Tensor: Indicating whether each box is inside \
the reference range.
"""
in_range_flags = ((self.tensor[:, 0] > box_range[0])
& (self.tensor[:, 2] > box_range[1])
& (self.tensor[:, 0] < box_range[2])
& (self.tensor[:, 2] < box_range[3]))
return in_range_flags
@classmethod @classmethod
def height_overlaps(cls, boxes1, boxes2, mode='iou'): def height_overlaps(cls, boxes1, boxes2, mode='iou'):
"""Calculate height overlaps of two boxes. """Calculate height overlaps of two boxes.
...@@ -296,8 +272,8 @@ class CameraInstance3DBoxes(BaseInstance3DBoxes): ...@@ -296,8 +272,8 @@ class CameraInstance3DBoxes(BaseInstance3DBoxes):
boxes2_top_height = boxes2.top_height.view(1, -1) boxes2_top_height = boxes2.top_height.view(1, -1)
boxes2_bottom_height = boxes2.bottom_height.view(1, -1) boxes2_bottom_height = boxes2.bottom_height.view(1, -1)
# In camera coordinate system # positive direction of the gravity axis
# from up to down is the positive direction # in cam coord system points to the earth
heighest_of_bottom = torch.min(boxes1_bottom_height, heighest_of_bottom = torch.min(boxes1_bottom_height,
boxes2_bottom_height) boxes2_bottom_height)
lowest_of_top = torch.max(boxes1_top_height, boxes2_top_height) lowest_of_top = torch.max(boxes1_top_height, boxes2_top_height)
...@@ -309,16 +285,70 @@ class CameraInstance3DBoxes(BaseInstance3DBoxes): ...@@ -309,16 +285,70 @@ class CameraInstance3DBoxes(BaseInstance3DBoxes):
Args: Args:
dst (:obj:`Box3DMode`): The target Box mode. dst (:obj:`Box3DMode`): The target Box mode.
rt_mat (np.ndarray | torch.Tensor): The rotation and translation rt_mat (np.ndarray | torch.Tensor, optional): The rotation and
matrix between different coordinates. Defaults to None. translation matrix between different coordinates.
Defaults to None.
The conversion from ``src`` coordinates to ``dst`` coordinates The conversion from ``src`` coordinates to ``dst`` coordinates
usually comes along the change of sensors, e.g., from camera usually comes along the change of sensors, e.g., from camera
to LiDAR. This requires a transformation matrix. to LiDAR. This requires a transformation matrix.
Returns: Returns:
:obj:`BaseInstance3DBoxes`: \ :obj:`BaseInstance3DBoxes`:
The converted box of the same type in the ``dst`` mode. The converted box of the same type in the ``dst`` mode.
""" """
from .box_3d_mode import Box3DMode from .box_3d_mode import Box3DMode
return Box3DMode.convert( return Box3DMode.convert(
box=self, src=Box3DMode.CAM, dst=dst, rt_mat=rt_mat) box=self, src=Box3DMode.CAM, dst=dst, rt_mat=rt_mat)
def points_in_boxes_part(self, points, boxes_override=None):
"""Find the box in which each point is.
Args:
points (torch.Tensor): Points in shape (1, M, 3) or (M, 3),
3 dimensions are (x, y, z) in LiDAR or depth coordinate.
boxes_override (torch.Tensor, optional): Boxes to override
`self.tensor `. Defaults to None.
Returns:
torch.Tensor: The index of the box in which
each point is, in shape (M, ). Default value is -1
(if the point is not enclosed by any box).
"""
from .coord_3d_mode import Coord3DMode
points_lidar = Coord3DMode.convert(points, Coord3DMode.CAM,
Coord3DMode.LIDAR)
if boxes_override is not None:
boxes_lidar = boxes_override
else:
boxes_lidar = Coord3DMode.convert(self.tensor, Coord3DMode.CAM,
Coord3DMode.LIDAR)
box_idx = super().points_in_boxes_part(points_lidar, boxes_lidar)
return box_idx
def points_in_boxes_all(self, points, boxes_override=None):
"""Find all boxes in which each point is.
Args:
points (torch.Tensor): Points in shape (1, M, 3) or (M, 3),
3 dimensions are (x, y, z) in LiDAR or depth coordinate.
boxes_override (torch.Tensor, optional): Boxes to override
`self.tensor `. Defaults to None.
Returns:
torch.Tensor: The index of all boxes in which each point is,
in shape (B, M, T).
"""
from .coord_3d_mode import Coord3DMode
points_lidar = Coord3DMode.convert(points, Coord3DMode.CAM,
Coord3DMode.LIDAR)
if boxes_override is not None:
boxes_lidar = boxes_override
else:
boxes_lidar = Coord3DMode.convert(self.tensor, Coord3DMode.CAM,
Coord3DMode.LIDAR)
box_idx = super().points_in_boxes_all(points_lidar, boxes_lidar)
return box_idx
# Copyright (c) OpenMMLab. All rights reserved. # Copyright (c) OpenMMLab. All rights reserved.
from enum import IntEnum, unique
import numpy as np import numpy as np
import torch import torch
from enum import IntEnum, unique
from mmdet3d.core.points import (BasePoints, CameraPoints, DepthPoints, from ...points import BasePoints, CameraPoints, DepthPoints, LiDARPoints
LiDARPoints)
from .base_box3d import BaseInstance3DBoxes from .base_box3d import BaseInstance3DBoxes
from .cam_box3d import CameraInstance3DBoxes from .box_3d_mode import Box3DMode
from .depth_box3d import DepthInstance3DBoxes
from .lidar_box3d import LiDARInstance3DBoxes
@unique @unique
...@@ -64,119 +62,75 @@ class Coord3DMode(IntEnum): ...@@ -64,119 +62,75 @@ class Coord3DMode(IntEnum):
DEPTH = 2 DEPTH = 2
@staticmethod @staticmethod
def convert(input, src, dst, rt_mat=None): def convert(input, src, dst, rt_mat=None, with_yaw=True, is_point=True):
"""Convert boxes or points from `src` mode to `dst` mode.""" """Convert boxes or points from `src` mode to `dst` mode.
Args:
input (tuple | list | np.ndarray | torch.Tensor |
:obj:`BaseInstance3DBoxes` | :obj:`BasePoints`):
Can be a k-tuple, k-list or an Nxk array/tensor, where k = 7.
src (:obj:`Box3DMode` | :obj:`Coord3DMode`): The source mode.
dst (:obj:`Box3DMode` | :obj:`Coord3DMode`): The target mode.
rt_mat (np.ndarray | torch.Tensor, 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.
with_yaw (bool): If `box` is an instance of
:obj:`BaseInstance3DBoxes`, whether or not it has a yaw angle.
Defaults to True.
is_point (bool): If `input` is neither an instance of
:obj:`BaseInstance3DBoxes` nor an instance of
:obj:`BasePoints`, whether or not it is point data.
Defaults to True.
Returns:
(tuple | list | np.ndarray | torch.Tensor |
:obj:`BaseInstance3DBoxes` | :obj:`BasePoints`):
The converted box of the same type.
"""
if isinstance(input, BaseInstance3DBoxes): if isinstance(input, BaseInstance3DBoxes):
return Coord3DMode.convert_box(input, src, dst, rt_mat=rt_mat) return Coord3DMode.convert_box(
input, src, dst, rt_mat=rt_mat, with_yaw=with_yaw)
elif isinstance(input, BasePoints): elif isinstance(input, BasePoints):
return Coord3DMode.convert_point(input, src, dst, rt_mat=rt_mat) return Coord3DMode.convert_point(input, src, dst, rt_mat=rt_mat)
elif isinstance(input, (tuple, list, np.ndarray, torch.Tensor)):
if is_point:
return Coord3DMode.convert_point(
input, src, dst, rt_mat=rt_mat)
else:
return Coord3DMode.convert_box(
input, src, dst, rt_mat=rt_mat, with_yaw=with_yaw)
else: else:
raise NotImplementedError raise NotImplementedError
@staticmethod @staticmethod
def convert_box(box, src, dst, rt_mat=None): def convert_box(box, src, dst, rt_mat=None, with_yaw=True):
"""Convert boxes from `src` mode to `dst` mode. """Convert boxes from `src` mode to `dst` mode.
Args: Args:
box (tuple | list | np.ndarray | box (tuple | list | np.ndarray |
torch.Tensor | BaseInstance3DBoxes): torch.Tensor | :obj:`BaseInstance3DBoxes`):
Can be a k-tuple, k-list or an Nxk array/tensor, where k = 7. Can be a k-tuple, k-list or an Nxk array/tensor, where k = 7.
src (:obj:`CoordMode`): The src Box mode. src (:obj:`Box3DMode`): The src Box mode.
dst (:obj:`CoordMode`): The target Box mode. dst (:obj:`Box3DMode`): The target Box mode.
rt_mat (np.ndarray | torch.Tensor): The rotation and translation rt_mat (np.ndarray | torch.Tensor, optional): The rotation and
matrix between different coordinates. Defaults to None. translation matrix between different coordinates.
Defaults to None.
The conversion from `src` coordinates to `dst` coordinates The conversion from `src` coordinates to `dst` coordinates
usually comes along the change of sensors, e.g., from camera usually comes along the change of sensors, e.g., from camera
to LiDAR. This requires a transformation matrix. to LiDAR. This requires a transformation matrix.
with_yaw (bool): If `box` is an instance of
:obj:`BaseInstance3DBoxes`, whether or not it has a yaw angle.
Defaults to True.
Returns: Returns:
(tuple | list | np.ndarray | torch.Tensor | BaseInstance3DBoxes): \ (tuple | list | np.ndarray | torch.Tensor |
:obj:`BaseInstance3DBoxes`):
The converted box of the same type. The converted box of the same type.
""" """
if src == dst: return Box3DMode.convert(box, src, dst, rt_mat=rt_mat)
return box
is_numpy = isinstance(box, np.ndarray)
is_Instance3DBoxes = isinstance(box, BaseInstance3DBoxes)
single_box = isinstance(box, (list, tuple))
if single_box:
assert len(box) >= 7, (
'CoordMode.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()
elif is_Instance3DBoxes:
arr = box.tensor.clone()
else:
arr = box.clone()
# convert box from `src` mode to `dst` mode.
x_size, y_size, z_size = arr[..., 3:4], arr[..., 4:5], arr[..., 5:6]
if src == Coord3DMode.LIDAR and dst == Coord3DMode.CAM:
if rt_mat is None:
rt_mat = arr.new_tensor([[0, -1, 0], [0, 0, -1], [1, 0, 0]])
xyz_size = torch.cat([y_size, z_size, x_size], dim=-1)
elif src == Coord3DMode.CAM and dst == Coord3DMode.LIDAR:
if rt_mat is None:
rt_mat = arr.new_tensor([[0, 0, 1], [-1, 0, 0], [0, -1, 0]])
xyz_size = torch.cat([z_size, x_size, y_size], dim=-1)
elif src == Coord3DMode.DEPTH and dst == Coord3DMode.CAM:
if rt_mat is None:
rt_mat = arr.new_tensor([[1, 0, 0], [0, 0, 1], [0, -1, 0]])
xyz_size = torch.cat([x_size, z_size, y_size], dim=-1)
elif src == Coord3DMode.CAM and dst == Coord3DMode.DEPTH:
if rt_mat is None:
rt_mat = arr.new_tensor([[1, 0, 0], [0, 0, -1], [0, 1, 0]])
xyz_size = torch.cat([x_size, z_size, y_size], dim=-1)
elif src == Coord3DMode.LIDAR and dst == Coord3DMode.DEPTH:
if rt_mat is None:
rt_mat = arr.new_tensor([[0, -1, 0], [1, 0, 0], [0, 0, 1]])
xyz_size = torch.cat([y_size, x_size, z_size], dim=-1)
elif src == Coord3DMode.DEPTH and dst == Coord3DMode.LIDAR:
if rt_mat is None:
rt_mat = arr.new_tensor([[0, 1, 0], [-1, 0, 0], [0, 0, 1]])
xyz_size = torch.cat([y_size, x_size, z_size], dim=-1)
else:
raise NotImplementedError(
f'Conversion from Coord3DMode {src} to {dst} '
'is not supported yet')
if not isinstance(rt_mat, torch.Tensor):
rt_mat = arr.new_tensor(rt_mat)
if rt_mat.size(1) == 4:
extended_xyz = torch.cat(
[arr[:, :3], arr.new_ones(arr.size(0), 1)], dim=-1)
xyz = extended_xyz @ rt_mat.t()
else:
xyz = arr[:, :3] @ rt_mat.t()
remains = arr[..., 6:]
arr = torch.cat([xyz[:, :3], xyz_size, remains], dim=-1)
# convert arr to the original type
original_type = type(box)
if single_box:
return original_type(arr.flatten().tolist())
if is_numpy:
return arr.numpy()
elif is_Instance3DBoxes:
if dst == Coord3DMode.CAM:
target_type = CameraInstance3DBoxes
elif dst == Coord3DMode.LIDAR:
target_type = LiDARInstance3DBoxes
elif dst == Coord3DMode.DEPTH:
target_type = DepthInstance3DBoxes
else:
raise NotImplementedError(
f'Conversion to {dst} through {original_type}'
' is not supported yet')
return target_type(
arr, box_dim=arr.size(-1), with_yaw=box.with_yaw)
else:
return arr
@staticmethod @staticmethod
def convert_point(point, src, dst, rt_mat=None): def convert_point(point, src, dst, rt_mat=None):
...@@ -184,18 +138,19 @@ class Coord3DMode(IntEnum): ...@@ -184,18 +138,19 @@ class Coord3DMode(IntEnum):
Args: Args:
point (tuple | list | np.ndarray | point (tuple | list | np.ndarray |
torch.Tensor | BasePoints): torch.Tensor | :obj:`BasePoints`):
Can be a k-tuple, k-list or an Nxk array/tensor. Can be a k-tuple, k-list or an Nxk array/tensor.
src (:obj:`CoordMode`): The src Point mode. src (:obj:`CoordMode`): The src Point mode.
dst (:obj:`CoordMode`): The target Point mode. dst (:obj:`CoordMode`): The target Point mode.
rt_mat (np.ndarray | torch.Tensor): The rotation and translation rt_mat (np.ndarray | torch.Tensor, optional): The rotation and
matrix between different coordinates. Defaults to None. translation matrix between different coordinates.
Defaults to None.
The conversion from `src` coordinates to `dst` coordinates The conversion from `src` coordinates to `dst` coordinates
usually comes along the change of sensors, e.g., from camera usually comes along the change of sensors, e.g., from camera
to LiDAR. This requires a transformation matrix. to LiDAR. This requires a transformation matrix.
Returns: Returns:
(tuple | list | np.ndarray | torch.Tensor | BasePoints): \ (tuple | list | np.ndarray | torch.Tensor | :obj:`BasePoints`):
The converted point of the same type. The converted point of the same type.
""" """
if src == dst: if src == dst:
...@@ -219,8 +174,6 @@ class Coord3DMode(IntEnum): ...@@ -219,8 +174,6 @@ class Coord3DMode(IntEnum):
arr = point.clone() arr = point.clone()
# convert point from `src` mode to `dst` mode. # convert point from `src` mode to `dst` mode.
# TODO: LIDAR
# only implemented provided Rt matrix in cam-depth conversion
if src == Coord3DMode.LIDAR and dst == Coord3DMode.CAM: if src == Coord3DMode.LIDAR and dst == Coord3DMode.CAM:
if rt_mat is None: if rt_mat is None:
rt_mat = arr.new_tensor([[0, -1, 0], [0, 0, -1], [1, 0, 0]]) rt_mat = arr.new_tensor([[0, -1, 0], [0, 0, -1], [1, 0, 0]])
...@@ -248,13 +201,13 @@ class Coord3DMode(IntEnum): ...@@ -248,13 +201,13 @@ class Coord3DMode(IntEnum):
rt_mat = arr.new_tensor(rt_mat) rt_mat = arr.new_tensor(rt_mat)
if rt_mat.size(1) == 4: if rt_mat.size(1) == 4:
extended_xyz = torch.cat( extended_xyz = torch.cat(
[arr[:, :3], arr.new_ones(arr.size(0), 1)], dim=-1) [arr[..., :3], arr.new_ones(arr.size(0), 1)], dim=-1)
xyz = extended_xyz @ rt_mat.t() xyz = extended_xyz @ rt_mat.t()
else: else:
xyz = arr[:, :3] @ rt_mat.t() xyz = arr[..., :3] @ rt_mat.t()
remains = arr[:, 3:] remains = arr[..., 3:]
arr = torch.cat([xyz[:, :3], remains], dim=-1) arr = torch.cat([xyz[..., :3], remains], dim=-1)
# convert arr to the original type # convert arr to the original type
original_type = type(point) original_type = type(point)
......
...@@ -3,9 +3,8 @@ import numpy as np ...@@ -3,9 +3,8 @@ import numpy as np
import torch import torch
from mmdet3d.core.points import BasePoints from mmdet3d.core.points import BasePoints
from mmdet3d.ops import points_in_boxes_batch
from .base_box3d import BaseInstance3DBoxes from .base_box3d import BaseInstance3DBoxes
from .utils import limit_period, rotation_3d_in_axis from .utils import rotation_3d_in_axis
class DepthInstance3DBoxes(BaseInstance3DBoxes): class DepthInstance3DBoxes(BaseInstance3DBoxes):
...@@ -38,10 +37,11 @@ class DepthInstance3DBoxes(BaseInstance3DBoxes): ...@@ -38,10 +37,11 @@ class DepthInstance3DBoxes(BaseInstance3DBoxes):
with_yaw (bool): If True, the value of yaw will be set to 0 as minmax with_yaw (bool): If True, the value of yaw will be set to 0 as minmax
boxes. boxes.
""" """
YAW_AXIS = 2
@property @property
def gravity_center(self): def gravity_center(self):
"""torch.Tensor: A tensor with center of each box.""" """torch.Tensor: A tensor with center of each box in shape (N, 3)."""
bottom_center = self.bottom_center bottom_center = self.bottom_center
gravity_center = torch.zeros_like(bottom_center) gravity_center = torch.zeros_like(bottom_center)
gravity_center[:, :2] = bottom_center[:, :2] gravity_center[:, :2] = bottom_center[:, :2]
...@@ -85,73 +85,50 @@ class DepthInstance3DBoxes(BaseInstance3DBoxes): ...@@ -85,73 +85,50 @@ class DepthInstance3DBoxes(BaseInstance3DBoxes):
corners = dims.view([-1, 1, 3]) * corners_norm.reshape([1, 8, 3]) corners = dims.view([-1, 1, 3]) * corners_norm.reshape([1, 8, 3])
# rotate around z axis # rotate around z axis
corners = rotation_3d_in_axis(corners, self.tensor[:, 6], axis=2) corners = rotation_3d_in_axis(
corners, self.tensor[:, 6], axis=self.YAW_AXIS)
corners += self.tensor[:, :3].view(-1, 1, 3) corners += self.tensor[:, :3].view(-1, 1, 3)
return corners return corners
@property
def bev(self):
"""torch.Tensor: A n x 5 tensor of 2D BEV box of each box
in XYWHR format."""
return self.tensor[:, [0, 1, 3, 4, 6]]
@property
def nearest_bev(self):
"""torch.Tensor: A tensor of 2D BEV box of each box
without rotation."""
# Obtain BEV boxes with rotation in XYWHR format
bev_rotated_boxes = self.bev
# 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, points=None): def rotate(self, angle, points=None):
"""Rotate boxes with points (optional) with the given angle or \ """Rotate boxes with points (optional) with the given angle or rotation
rotation matrix. matrix.
Args: Args:
angle (float | torch.Tensor | np.ndarray): angle (float | torch.Tensor | np.ndarray):
Rotation angle or rotation matrix. Rotation angle or rotation matrix.
points (torch.Tensor, numpy.ndarray, :obj:`BasePoints`, optional): points (torch.Tensor | np.ndarray | :obj:`BasePoints`, optional):
Points to rotate. Defaults to None. Points to rotate. Defaults to None.
Returns: Returns:
tuple or None: When ``points`` is None, the function returns \ tuple or None: When ``points`` is None, the function returns
None, otherwise it returns the rotated points and the \ None, otherwise it returns the rotated points and the
rotation matrix ``rot_mat_T``. rotation matrix ``rot_mat_T``.
""" """
if not isinstance(angle, torch.Tensor): if not isinstance(angle, torch.Tensor):
angle = self.tensor.new_tensor(angle) angle = self.tensor.new_tensor(angle)
assert angle.shape == torch.Size([3, 3]) or angle.numel() == 1, \ assert angle.shape == torch.Size([3, 3]) or angle.numel() == 1, \
f'invalid rotation angle shape {angle.shape}' f'invalid rotation angle shape {angle.shape}'
if angle.numel() == 1: if angle.numel() == 1:
rot_sin = torch.sin(angle) self.tensor[:, 0:3], rot_mat_T = rotation_3d_in_axis(
rot_cos = torch.cos(angle) self.tensor[:, 0:3],
rot_mat_T = self.tensor.new_tensor([[rot_cos, -rot_sin, 0], angle,
[rot_sin, rot_cos, 0], axis=self.YAW_AXIS,
[0, 0, 1]]).T return_mat=True)
else: else:
rot_mat_T = angle.T rot_mat_T = angle
rot_sin = rot_mat_T[0, 1] rot_sin = rot_mat_T[0, 1]
rot_cos = rot_mat_T[0, 0] rot_cos = rot_mat_T[0, 0]
angle = np.arctan2(rot_sin, rot_cos) angle = np.arctan2(rot_sin, rot_cos)
self.tensor[:, 0:3] = self.tensor[:, 0:3] @ rot_mat_T
self.tensor[:, 0:3] = self.tensor[:, 0:3] @ rot_mat_T
if self.with_yaw: if self.with_yaw:
self.tensor[:, 6] -= angle self.tensor[:, 6] += angle
else: else:
# for axis-aligned boxes, we take the new
# enclosing axis-aligned boxes after rotation
corners_rot = self.corners @ rot_mat_T corners_rot = self.corners @ rot_mat_T
new_x_size = corners_rot[..., 0].max( new_x_size = corners_rot[..., 0].max(
dim=1, keepdim=True)[0] - corners_rot[..., 0].min( dim=1, keepdim=True)[0] - corners_rot[..., 0].min(
...@@ -165,11 +142,10 @@ class DepthInstance3DBoxes(BaseInstance3DBoxes): ...@@ -165,11 +142,10 @@ class DepthInstance3DBoxes(BaseInstance3DBoxes):
if isinstance(points, torch.Tensor): if isinstance(points, torch.Tensor):
points[:, :3] = points[:, :3] @ rot_mat_T points[:, :3] = points[:, :3] @ rot_mat_T
elif isinstance(points, np.ndarray): elif isinstance(points, np.ndarray):
rot_mat_T = rot_mat_T.numpy() rot_mat_T = rot_mat_T.cpu().numpy()
points[:, :3] = np.dot(points[:, :3], rot_mat_T) points[:, :3] = np.dot(points[:, :3], rot_mat_T)
elif isinstance(points, BasePoints): elif isinstance(points, BasePoints):
# anti-clockwise points.rotate(rot_mat_T)
points.rotate(angle)
else: else:
raise ValueError raise ValueError
return points, rot_mat_T return points, rot_mat_T
...@@ -180,8 +156,9 @@ class DepthInstance3DBoxes(BaseInstance3DBoxes): ...@@ -180,8 +156,9 @@ class DepthInstance3DBoxes(BaseInstance3DBoxes):
In Depth coordinates, it flips x (horizontal) or y (vertical) axis. In Depth coordinates, it flips x (horizontal) or y (vertical) axis.
Args: Args:
bev_direction (str): Flip direction (horizontal or vertical). bev_direction (str, optional): Flip direction
points (torch.Tensor, numpy.ndarray, :obj:`BasePoints`, None): (horizontal or vertical). Defaults to 'horizontal'.
points (torch.Tensor | np.ndarray | :obj:`BasePoints`, optional):
Points to flip. Defaults to None. Points to flip. Defaults to None.
Returns: Returns:
...@@ -208,75 +185,26 @@ class DepthInstance3DBoxes(BaseInstance3DBoxes): ...@@ -208,75 +185,26 @@ class DepthInstance3DBoxes(BaseInstance3DBoxes):
points.flip(bev_direction) points.flip(bev_direction)
return points return points
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.
Returns:
torch.Tensor: 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 convert_to(self, dst, rt_mat=None): def convert_to(self, dst, rt_mat=None):
"""Convert self to ``dst`` mode. """Convert self to ``dst`` mode.
Args: Args:
dst (:obj:`Box3DMode`): The target Box mode. dst (:obj:`Box3DMode`): The target Box mode.
rt_mat (np.ndarray | torch.Tensor): The rotation and translation rt_mat (np.ndarray | torch.Tensor, optional): The rotation and
matrix between different coordinates. Defaults to None. translation matrix between different coordinates.
Defaults to None.
The conversion from ``src`` coordinates to ``dst`` coordinates The conversion from ``src`` coordinates to ``dst`` coordinates
usually comes along the change of sensors, e.g., from camera usually comes along the change of sensors, e.g., from camera
to LiDAR. This requires a transformation matrix. to LiDAR. This requires a transformation matrix.
Returns: Returns:
:obj:`DepthInstance3DBoxes`: \ :obj:`DepthInstance3DBoxes`:
The converted box of the same type in the ``dst`` mode. The converted box of the same type in the ``dst`` mode.
""" """
from .box_3d_mode import Box3DMode from .box_3d_mode import Box3DMode
return Box3DMode.convert( return Box3DMode.convert(
box=self, src=Box3DMode.DEPTH, dst=dst, rt_mat=rt_mat) box=self, src=Box3DMode.DEPTH, dst=dst, rt_mat=rt_mat)
def points_in_boxes(self, points):
"""Find points that are in boxes (CUDA).
Args:
points (torch.Tensor): Points in shape [1, M, 3] or [M, 3], \
3 dimensions are [x, y, z] in LiDAR coordinate.
Returns:
torch.Tensor: The index of boxes each point lies in with shape \
of (B, M, T).
"""
from .box_3d_mode import Box3DMode
# to lidar
points_lidar = points.clone()
points_lidar = points_lidar[..., [1, 0, 2]]
points_lidar[..., 1] *= -1
if points.dim() == 2:
points_lidar = points_lidar.unsqueeze(0)
else:
assert points.dim() == 3 and points_lidar.shape[0] == 1
boxes_lidar = self.convert_to(Box3DMode.LIDAR).tensor
boxes_lidar = boxes_lidar.to(points.device).unsqueeze(0)
box_idxs_of_pts = points_in_boxes_batch(points_lidar, boxes_lidar)
return box_idxs_of_pts.squeeze(0)
def enlarged_box(self, extra_width): def enlarged_box(self, extra_width):
"""Enlarge the length, width and height boxes. """Enlarge the length, width and height boxes.
...@@ -284,7 +212,7 @@ class DepthInstance3DBoxes(BaseInstance3DBoxes): ...@@ -284,7 +212,7 @@ class DepthInstance3DBoxes(BaseInstance3DBoxes):
extra_width (float | torch.Tensor): Extra width to enlarge the box. extra_width (float | torch.Tensor): Extra width to enlarge the box.
Returns: Returns:
:obj:`LiDARInstance3DBoxes`: Enlarged boxes. :obj:`DepthInstance3DBoxes`: Enlarged boxes.
""" """
enlarged_boxes = self.tensor.clone() enlarged_boxes = self.tensor.clone()
enlarged_boxes[:, 3:6] += extra_width * 2 enlarged_boxes[:, 3:6] += extra_width * 2
...@@ -331,13 +259,12 @@ class DepthInstance3DBoxes(BaseInstance3DBoxes): ...@@ -331,13 +259,12 @@ class DepthInstance3DBoxes(BaseInstance3DBoxes):
-1, 3) -1, 3)
surface_rot = rot_mat_T.repeat(6, 1, 1) surface_rot = rot_mat_T.repeat(6, 1, 1)
surface_3d = torch.matmul( surface_3d = torch.matmul(surface_3d.unsqueeze(-2),
surface_3d.unsqueeze(-2), surface_rot.transpose(2, 1)).squeeze(-2) surface_rot).squeeze(-2)
surface_center = center.repeat(1, 6, 1).reshape(-1, 3) + surface_3d surface_center = center.repeat(1, 6, 1).reshape(-1, 3) + surface_3d
line_rot = rot_mat_T.repeat(12, 1, 1) line_rot = rot_mat_T.repeat(12, 1, 1)
line_3d = torch.matmul( line_3d = torch.matmul(line_3d.unsqueeze(-2), line_rot).squeeze(-2)
line_3d.unsqueeze(-2), line_rot.transpose(2, 1)).squeeze(-2)
line_center = center.repeat(1, 12, 1).reshape(-1, 3) + line_3d line_center = center.repeat(1, 12, 1).reshape(-1, 3) + line_3d
return surface_center, line_center return surface_center, line_center
...@@ -3,9 +3,8 @@ import numpy as np ...@@ -3,9 +3,8 @@ import numpy as np
import torch import torch
from mmdet3d.core.points import BasePoints from mmdet3d.core.points import BasePoints
from mmdet3d.ops.roiaware_pool3d import points_in_boxes_gpu
from .base_box3d import BaseInstance3DBoxes from .base_box3d import BaseInstance3DBoxes
from .utils import limit_period, rotation_3d_in_axis from .utils import rotation_3d_in_axis
class LiDARInstance3DBoxes(BaseInstance3DBoxes): class LiDARInstance3DBoxes(BaseInstance3DBoxes):
...@@ -15,16 +14,16 @@ class LiDARInstance3DBoxes(BaseInstance3DBoxes): ...@@ -15,16 +14,16 @@ class LiDARInstance3DBoxes(BaseInstance3DBoxes):
.. code-block:: none .. code-block:: none
up z x front (yaw=-0.5*pi) up z x front (yaw=0)
^ ^ ^ ^
| / | /
| / | /
(yaw=-pi) left y <------ 0 -------- (yaw=0) (yaw=0.5*pi) left y <------ 0
The relative coordinate of bottom center in a LiDAR box is (0.5, 0.5, 0), The relative coordinate of bottom center in a LiDAR box is (0.5, 0.5, 0),
and the yaw is around the z axis, thus the rotation axis=2. and the yaw is around the z axis, thus the rotation axis=2.
The yaw is 0 at the negative direction of y axis, and decreases from The yaw is 0 at the positive direction of x axis, and increases from
the negative direction of y to the positive direction of x. the positive direction of x to the positive direction of y.
A refactor is ongoing to make the three coordinate systems A refactor is ongoing to make the three coordinate systems
easier to understand and convert between each other. easier to understand and convert between each other.
...@@ -36,10 +35,11 @@ class LiDARInstance3DBoxes(BaseInstance3DBoxes): ...@@ -36,10 +35,11 @@ class LiDARInstance3DBoxes(BaseInstance3DBoxes):
with_yaw (bool): If True, the value of yaw will be set to 0 as minmax with_yaw (bool): If True, the value of yaw will be set to 0 as minmax
boxes. boxes.
""" """
YAW_AXIS = 2
@property @property
def gravity_center(self): def gravity_center(self):
"""torch.Tensor: A tensor with center of each box.""" """torch.Tensor: A tensor with center of each box in shape (N, 3)."""
bottom_center = self.bottom_center bottom_center = self.bottom_center
gravity_center = torch.zeros_like(bottom_center) gravity_center = torch.zeros_like(bottom_center)
gravity_center[:, :2] = bottom_center[:, :2] gravity_center[:, :2] = bottom_center[:, :2]
...@@ -83,70 +83,45 @@ class LiDARInstance3DBoxes(BaseInstance3DBoxes): ...@@ -83,70 +83,45 @@ class LiDARInstance3DBoxes(BaseInstance3DBoxes):
corners = dims.view([-1, 1, 3]) * corners_norm.reshape([1, 8, 3]) corners = dims.view([-1, 1, 3]) * corners_norm.reshape([1, 8, 3])
# rotate around z axis # rotate around z axis
corners = rotation_3d_in_axis(corners, self.tensor[:, 6], axis=2) corners = rotation_3d_in_axis(
corners, self.tensor[:, 6], axis=self.YAW_AXIS)
corners += self.tensor[:, :3].view(-1, 1, 3) corners += self.tensor[:, :3].view(-1, 1, 3)
return corners return corners
@property
def bev(self):
"""torch.Tensor: 2D BEV box of each box with rotation
in XYWHR format."""
return self.tensor[:, [0, 1, 3, 4, 6]]
@property
def nearest_bev(self):
"""torch.Tensor: A tensor of 2D BEV box of each box
without rotation."""
# Obtain BEV boxes with rotation in XYWHR format
bev_rotated_boxes = self.bev
# 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, points=None): def rotate(self, angle, points=None):
"""Rotate boxes with points (optional) with the given angle or \ """Rotate boxes with points (optional) with the given angle or rotation
rotation matrix. matrix.
Args: Args:
angles (float | torch.Tensor | np.ndarray): angles (float | torch.Tensor | np.ndarray):
Rotation angle or rotation matrix. Rotation angle or rotation matrix.
points (torch.Tensor, numpy.ndarray, :obj:`BasePoints`, optional): points (torch.Tensor | np.ndarray | :obj:`BasePoints`, optional):
Points to rotate. Defaults to None. Points to rotate. Defaults to None.
Returns: Returns:
tuple or None: When ``points`` is None, the function returns \ tuple or None: When ``points`` is None, the function returns
None, otherwise it returns the rotated points and the \ None, otherwise it returns the rotated points and the
rotation matrix ``rot_mat_T``. rotation matrix ``rot_mat_T``.
""" """
if not isinstance(angle, torch.Tensor): if not isinstance(angle, torch.Tensor):
angle = self.tensor.new_tensor(angle) angle = self.tensor.new_tensor(angle)
assert angle.shape == torch.Size([3, 3]) or angle.numel() == 1, \ assert angle.shape == torch.Size([3, 3]) or angle.numel() == 1, \
f'invalid rotation angle shape {angle.shape}' f'invalid rotation angle shape {angle.shape}'
if angle.numel() == 1: if angle.numel() == 1:
rot_sin = torch.sin(angle) self.tensor[:, 0:3], rot_mat_T = rotation_3d_in_axis(
rot_cos = torch.cos(angle) self.tensor[:, 0:3],
rot_mat_T = self.tensor.new_tensor([[rot_cos, -rot_sin, 0], angle,
[rot_sin, rot_cos, 0], axis=self.YAW_AXIS,
[0, 0, 1]]) return_mat=True)
else: else:
rot_mat_T = angle rot_mat_T = angle
rot_sin = rot_mat_T[1, 0] rot_sin = rot_mat_T[0, 1]
rot_cos = rot_mat_T[0, 0] rot_cos = rot_mat_T[0, 0]
angle = np.arctan2(rot_sin, rot_cos) angle = np.arctan2(rot_sin, rot_cos)
self.tensor[:, 0:3] = self.tensor[:, 0:3] @ rot_mat_T
self.tensor[:, :3] = self.tensor[:, :3] @ rot_mat_T
self.tensor[:, 6] += angle self.tensor[:, 6] += angle
if self.tensor.shape[1] == 9: if self.tensor.shape[1] == 9:
...@@ -157,11 +132,10 @@ class LiDARInstance3DBoxes(BaseInstance3DBoxes): ...@@ -157,11 +132,10 @@ class LiDARInstance3DBoxes(BaseInstance3DBoxes):
if isinstance(points, torch.Tensor): if isinstance(points, torch.Tensor):
points[:, :3] = points[:, :3] @ rot_mat_T points[:, :3] = points[:, :3] @ rot_mat_T
elif isinstance(points, np.ndarray): elif isinstance(points, np.ndarray):
rot_mat_T = rot_mat_T.numpy() rot_mat_T = rot_mat_T.cpu().numpy()
points[:, :3] = np.dot(points[:, :3], rot_mat_T) points[:, :3] = np.dot(points[:, :3], rot_mat_T)
elif isinstance(points, BasePoints): elif isinstance(points, BasePoints):
# clockwise points.rotate(rot_mat_T)
points.rotate(-angle)
else: else:
raise ValueError raise ValueError
return points, rot_mat_T return points, rot_mat_T
...@@ -173,7 +147,7 @@ class LiDARInstance3DBoxes(BaseInstance3DBoxes): ...@@ -173,7 +147,7 @@ class LiDARInstance3DBoxes(BaseInstance3DBoxes):
Args: Args:
bev_direction (str): Flip direction (horizontal or vertical). bev_direction (str): Flip direction (horizontal or vertical).
points (torch.Tensor, numpy.ndarray, :obj:`BasePoints`, None): points (torch.Tensor | np.ndarray | :obj:`BasePoints`, optional):
Points to flip. Defaults to None. Points to flip. Defaults to None.
Returns: Returns:
...@@ -183,11 +157,11 @@ class LiDARInstance3DBoxes(BaseInstance3DBoxes): ...@@ -183,11 +157,11 @@ class LiDARInstance3DBoxes(BaseInstance3DBoxes):
if bev_direction == 'horizontal': if bev_direction == 'horizontal':
self.tensor[:, 1::7] = -self.tensor[:, 1::7] self.tensor[:, 1::7] = -self.tensor[:, 1::7]
if self.with_yaw: if self.with_yaw:
self.tensor[:, 6] = -self.tensor[:, 6] + np.pi self.tensor[:, 6] = -self.tensor[:, 6]
elif bev_direction == 'vertical': elif bev_direction == 'vertical':
self.tensor[:, 0::7] = -self.tensor[:, 0::7] self.tensor[:, 0::7] = -self.tensor[:, 0::7]
if self.with_yaw: if self.with_yaw:
self.tensor[:, 6] = -self.tensor[:, 6] self.tensor[:, 6] = -self.tensor[:, 6] + np.pi
if points is not None: if points is not None:
assert isinstance(points, (torch.Tensor, np.ndarray, BasePoints)) assert isinstance(points, (torch.Tensor, np.ndarray, BasePoints))
...@@ -200,40 +174,20 @@ class LiDARInstance3DBoxes(BaseInstance3DBoxes): ...@@ -200,40 +174,20 @@ class LiDARInstance3DBoxes(BaseInstance3DBoxes):
points.flip(bev_direction) points.flip(bev_direction)
return points return points
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:
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:
torch.Tensor: 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 convert_to(self, dst, rt_mat=None): def convert_to(self, dst, rt_mat=None):
"""Convert self to ``dst`` mode. """Convert self to ``dst`` mode.
Args: Args:
dst (:obj:`Box3DMode`): the target Box mode dst (:obj:`Box3DMode`): the target Box mode
rt_mat (np.ndarray | torch.Tensor): The rotation and translation rt_mat (np.ndarray | torch.Tensor, optional): The rotation and
matrix between different coordinates. Defaults to None. translation matrix between different coordinates.
Defaults to None.
The conversion from ``src`` coordinates to ``dst`` coordinates The conversion from ``src`` coordinates to ``dst`` coordinates
usually comes along the change of sensors, e.g., from camera usually comes along the change of sensors, e.g., from camera
to LiDAR. This requires a transformation matrix. to LiDAR. This requires a transformation matrix.
Returns: Returns:
:obj:`BaseInstance3DBoxes`: \ :obj:`BaseInstance3DBoxes`:
The converted box of the same type in the ``dst`` mode. The converted box of the same type in the ``dst`` mode.
""" """
from .box_3d_mode import Box3DMode from .box_3d_mode import Box3DMode
...@@ -254,17 +208,3 @@ class LiDARInstance3DBoxes(BaseInstance3DBoxes): ...@@ -254,17 +208,3 @@ class LiDARInstance3DBoxes(BaseInstance3DBoxes):
# bottom center z minus extra_width # bottom center z minus extra_width
enlarged_boxes[:, 2] -= extra_width enlarged_boxes[:, 2] -= extra_width
return self.new_box(enlarged_boxes) return self.new_box(enlarged_boxes)
def points_in_boxes(self, points):
"""Find the box which the points are in.
Args:
points (torch.Tensor): Points in shape (N, 3).
Returns:
torch.Tensor: The index of box where each point are in.
"""
box_idx = points_in_boxes_gpu(
points.unsqueeze(0),
self.tensor.unsqueeze(0).to(points.device)).squeeze(0)
return box_idx
# Copyright (c) OpenMMLab. All rights reserved. # Copyright (c) OpenMMLab. All rights reserved.
from logging import warning
import numpy as np import numpy as np
import torch import torch
from logging import warning
from mmdet3d.core.utils import array_converter
@array_converter(apply_to=('val', ))
def limit_period(val, offset=0.5, period=np.pi): def limit_period(val, offset=0.5, period=np.pi):
"""Limit the value into a period for periodic function. """Limit the value into a period for periodic function.
Args: Args:
val (torch.Tensor): The value to be converted. val (torch.Tensor | np.ndarray): The value to be converted.
offset (float, optional): Offset to set the value range. \ offset (float, optional): Offset to set the value range.
Defaults to 0.5. Defaults to 0.5.
period ([type], optional): Period of the value. Defaults to np.pi. period ([type], optional): Period of the value. Defaults to np.pi.
Returns: Returns:
torch.Tensor: Value in the range of \ (torch.Tensor | np.ndarray): Value in the range of
[-offset * period, (1-offset) * period] [-offset * period, (1-offset) * period]
""" """
return val - torch.floor(val / period + offset) * period limited_val = val - torch.floor(val / period + offset) * period
return limited_val
def rotation_3d_in_axis(points, angles, axis=0): @array_converter(apply_to=('points', 'angles'))
def rotation_3d_in_axis(points,
angles,
axis=0,
return_mat=False,
clockwise=False):
"""Rotate points by angles according to axis. """Rotate points by angles according to axis.
Args: Args:
points (torch.Tensor): Points of shape (N, M, 3). points (np.ndarray | torch.Tensor | list | tuple ):
angles (torch.Tensor): Vector of angles in shape (N,) Points of shape (N, M, 3).
angles (np.ndarray | torch.Tensor | list | tuple | float):
Vector of angles in shape (N,)
axis (int, optional): The axis to be rotated. Defaults to 0. axis (int, optional): The axis to be rotated. Defaults to 0.
return_mat: Whether or not return the rotation matrix (transposed).
Defaults to False.
clockwise: Whether the rotation is clockwise. Defaults to False.
Raises: Raises:
ValueError: when the axis is not in range [0, 1, 2], it will \ ValueError: when the axis is not in range [0, 1, 2], it will
raise value error. raise value error.
Returns: Returns:
torch.Tensor: Rotated points in shape (N, M, 3) (torch.Tensor | np.ndarray): Rotated points in shape (N, M, 3).
""" """
batch_free = len(points.shape) == 2
if batch_free:
points = points[None]
if isinstance(angles, float) or len(angles.shape) == 0:
angles = torch.full(points.shape[:1], angles)
assert len(points.shape) == 3 and len(angles.shape) == 1 \
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_sin = torch.sin(angles) rot_sin = torch.sin(angles)
rot_cos = torch.cos(angles) rot_cos = torch.cos(angles)
ones = torch.ones_like(rot_cos) ones = torch.ones_like(rot_cos)
zeros = torch.zeros_like(rot_cos) zeros = torch.zeros_like(rot_cos)
if axis == 1:
rot_mat_T = torch.stack([ if points.shape[-1] == 3:
torch.stack([rot_cos, zeros, -rot_sin]), if axis == 1 or axis == -2:
torch.stack([zeros, ones, zeros]), rot_mat_T = torch.stack([
torch.stack([rot_sin, zeros, rot_cos]) torch.stack([rot_cos, zeros, -rot_sin]),
]) torch.stack([zeros, ones, zeros]),
elif axis == 2 or axis == -1: torch.stack([rot_sin, zeros, rot_cos])
rot_mat_T = torch.stack([ ])
torch.stack([rot_cos, -rot_sin, zeros]), elif axis == 2 or axis == -1:
torch.stack([rot_sin, rot_cos, zeros]), rot_mat_T = torch.stack([
torch.stack([zeros, zeros, ones]) torch.stack([rot_cos, rot_sin, zeros]),
]) torch.stack([-rot_sin, rot_cos, zeros]),
elif axis == 0: torch.stack([zeros, zeros, ones])
])
elif axis == 0 or axis == -3:
rot_mat_T = torch.stack([
torch.stack([ones, zeros, zeros]),
torch.stack([zeros, rot_cos, rot_sin]),
torch.stack([zeros, -rot_sin, rot_cos])
])
else:
raise ValueError(f'axis should in range '
f'[-3, -2, -1, 0, 1, 2], got {axis}')
else:
rot_mat_T = torch.stack([ rot_mat_T = torch.stack([
torch.stack([zeros, rot_cos, -rot_sin]), torch.stack([rot_cos, rot_sin]),
torch.stack([zeros, rot_sin, rot_cos]), torch.stack([-rot_sin, rot_cos])
torch.stack([ones, zeros, zeros])
]) ])
if clockwise:
rot_mat_T = rot_mat_T.transpose(0, 1)
if points.shape[0] == 0:
points_new = points
else: else:
raise ValueError(f'axis should in range [0, 1, 2], got {axis}') points_new = torch.einsum('aij,jka->aik', points, rot_mat_T)
if batch_free:
points_new = points_new.squeeze(0)
return torch.einsum('aij,jka->aik', (points, rot_mat_T)) if return_mat:
rot_mat_T = torch.einsum('jka->ajk', rot_mat_T)
if batch_free:
rot_mat_T = rot_mat_T.squeeze(0)
return points_new, rot_mat_T
else:
return points_new
@array_converter(apply_to=('boxes_xywhr', ))
def xywhr2xyxyr(boxes_xywhr): def xywhr2xyxyr(boxes_xywhr):
"""Convert a rotated boxes in XYWHR format to XYXYR format. """Convert a rotated boxes in XYWHR format to XYXYR format.
Args: Args:
boxes_xywhr (torch.Tensor): Rotated boxes in XYWHR format. boxes_xywhr (torch.Tensor | np.ndarray): Rotated boxes in XYWHR format.
Returns: Returns:
torch.Tensor: Converted boxes in XYXYR format. (torch.Tensor | np.ndarray): Converted boxes in XYXYR format.
""" """
boxes = torch.zeros_like(boxes_xywhr) boxes = torch.zeros_like(boxes_xywhr)
half_w = boxes_xywhr[:, 2] / 2 half_w = boxes_xywhr[..., 2] / 2
half_h = boxes_xywhr[:, 3] / 2 half_h = boxes_xywhr[..., 3] / 2
boxes[:, 0] = boxes_xywhr[:, 0] - half_w boxes[..., 0] = boxes_xywhr[..., 0] - half_w
boxes[:, 1] = boxes_xywhr[:, 1] - half_h boxes[..., 1] = boxes_xywhr[..., 1] - half_h
boxes[:, 2] = boxes_xywhr[:, 0] + half_w boxes[..., 2] = boxes_xywhr[..., 0] + half_w
boxes[:, 3] = boxes_xywhr[:, 1] + half_h boxes[..., 3] = boxes_xywhr[..., 1] + half_h
boxes[:, 4] = boxes_xywhr[:, 4] boxes[..., 4] = boxes_xywhr[..., 4]
return boxes return boxes
...@@ -91,6 +146,10 @@ def get_box_type(box_type): ...@@ -91,6 +146,10 @@ def get_box_type(box_type):
box_type (str): The type of box structure. box_type (str): The type of box structure.
The valid value are "LiDAR", "Camera", or "Depth". The valid value are "LiDAR", "Camera", or "Depth".
Raises:
ValueError: A ValueError is raised when `box_type`
does not belong to the three valid types.
Returns: Returns:
tuple: Box type and box mode. tuple: Box type and box mode.
""" """
...@@ -113,21 +172,24 @@ def get_box_type(box_type): ...@@ -113,21 +172,24 @@ def get_box_type(box_type):
return box_type_3d, box_mode_3d return box_type_3d, box_mode_3d
@array_converter(apply_to=('points_3d', 'proj_mat'))
def points_cam2img(points_3d, proj_mat, with_depth=False): def points_cam2img(points_3d, proj_mat, with_depth=False):
"""Project points from camera coordicates to image coordinates. """Project points in camera coordinates to image coordinates.
Args: Args:
points_3d (torch.Tensor): Points in shape (N, 3). points_3d (torch.Tensor | np.ndarray): Points in shape (N, 3)
proj_mat (torch.Tensor): Transformation matrix between coordinates. proj_mat (torch.Tensor | np.ndarray):
Transformation matrix between coordinates.
with_depth (bool, optional): Whether to keep depth in the output. with_depth (bool, optional): Whether to keep depth in the output.
Defaults to False. Defaults to False.
Returns: Returns:
torch.Tensor: Points in image coordinates with shape [N, 2]. (torch.Tensor | np.ndarray): Points in image coordinates,
with shape [N, 2] if `with_depth=False`, else [N, 3].
""" """
points_num = list(points_3d.shape)[:-1] points_shape = list(points_3d.shape)
points_shape[-1] = 1
points_shape = np.concatenate([points_num, [1]], axis=0).tolist()
assert len(proj_mat.shape) == 2, 'The dimension of the projection'\ assert len(proj_mat.shape) == 2, 'The dimension of the projection'\
f' matrix should be 2 instead of {len(proj_mat.shape)}.' f' matrix should be 2 instead of {len(proj_mat.shape)}.'
d1, d2 = proj_mat.shape[:2] d1, d2 = proj_mat.shape[:2]
...@@ -140,17 +202,52 @@ def points_cam2img(points_3d, proj_mat, with_depth=False): ...@@ -140,17 +202,52 @@ def points_cam2img(points_3d, proj_mat, with_depth=False):
proj_mat_expanded[:d1, :d2] = proj_mat proj_mat_expanded[:d1, :d2] = proj_mat
proj_mat = proj_mat_expanded proj_mat = proj_mat_expanded
# previous implementation use new_zeros, new_one yeilds better results # previous implementation use new_zeros, new_one yields better results
points_4 = torch.cat( points_4 = torch.cat([points_3d, points_3d.new_ones(points_shape)], dim=-1)
[points_3d, points_3d.new_ones(*points_shape)], dim=-1)
point_2d = torch.matmul(points_4, proj_mat.t()) point_2d = points_4 @ proj_mat.T
point_2d_res = point_2d[..., :2] / point_2d[..., 2:3] point_2d_res = point_2d[..., :2] / point_2d[..., 2:3]
if with_depth: if with_depth:
return torch.cat([point_2d_res, point_2d[..., 2:3]], dim=-1) point_2d_res = torch.cat([point_2d_res, point_2d[..., 2:3]], dim=-1)
return point_2d_res return point_2d_res
@array_converter(apply_to=('points', 'cam2img'))
def points_img2cam(points, cam2img):
"""Project points in image coordinates to camera coordinates.
Args:
points (torch.Tensor): 2.5D points in 2D images, [N, 3],
3 corresponds with x, y in the image and depth.
cam2img (torch.Tensor): Camera intrinsic matrix. The shape can be
[3, 3], [3, 4] or [4, 4].
Returns:
torch.Tensor: points in 3D space. [N, 3],
3 corresponds with x, y, z in 3D space.
"""
assert cam2img.shape[0] <= 4
assert cam2img.shape[1] <= 4
assert points.shape[1] == 3
xys = points[:, :2]
depths = points[:, 2].view(-1, 1)
unnormed_xys = torch.cat([xys * depths, depths], dim=1)
pad_cam2img = torch.eye(4, dtype=xys.dtype, device=xys.device)
pad_cam2img[:cam2img.shape[0], :cam2img.shape[1]] = cam2img
inv_pad_cam2img = torch.inverse(pad_cam2img).transpose(0, 1)
# Do operation in homogeneous coordinates.
num_points = unnormed_xys.shape[0]
homo_xys = torch.cat([unnormed_xys, xys.new_ones((num_points, 1))], dim=1)
points3D = torch.mm(homo_xys, inv_pad_cam2img)[:, :3]
return points3D
def mono_cam_box2vis(cam_box): def mono_cam_box2vis(cam_box):
"""This is a post-processing function on the bboxes from Mono-3D task. If """This is a post-processing function on the bboxes from Mono-3D task. If
we want to perform projection visualization, we need to: we want to perform projection visualization, we need to:
...@@ -162,9 +259,9 @@ def mono_cam_box2vis(cam_box): ...@@ -162,9 +259,9 @@ def mono_cam_box2vis(cam_box):
After applying this function, we can project and draw it on 2D images. After applying this function, we can project and draw it on 2D images.
Args: Args:
cam_box (:obj:`CameraInstance3DBoxes`): 3D bbox in camera coordinate \ cam_box (:obj:`CameraInstance3DBoxes`): 3D bbox in camera coordinate
system before conversion. Could be gt bbox loaded from dataset or \ system before conversion. Could be gt bbox loaded from dataset
network prediction output. or network prediction output.
Returns: Returns:
:obj:`CameraInstance3DBoxes`: Box after conversion. :obj:`CameraInstance3DBoxes`: Box after conversion.
...@@ -212,3 +309,27 @@ def get_proj_mat_by_coord_type(img_meta, coord_type): ...@@ -212,3 +309,27 @@ def get_proj_mat_by_coord_type(img_meta, coord_type):
mapping = {'LIDAR': 'lidar2img', 'DEPTH': 'depth2img', 'CAMERA': 'cam2img'} mapping = {'LIDAR': 'lidar2img', 'DEPTH': 'depth2img', 'CAMERA': 'cam2img'}
assert coord_type in mapping.keys() assert coord_type in mapping.keys()
return img_meta[mapping[coord_type]] return img_meta[mapping[coord_type]]
def yaw2local(yaw, loc):
"""Transform global yaw to local yaw (alpha in kitti) in camera
coordinates, ranges from -pi to pi.
Args:
yaw (torch.Tensor): A vector with local yaw of each box.
shape: (N, )
loc (torch.Tensor): gravity center of each box.
shape: (N, 3)
Returns:
torch.Tensor: local yaw (alpha in kitti).
"""
local_yaw = yaw - torch.atan2(loc[:, 0], loc[:, 2])
larger_idx = (local_yaw > np.pi).nonzero(as_tuple=False)
small_idx = (local_yaw < -np.pi).nonzero(as_tuple=False)
if len(larger_idx) != 0:
local_yaw[larger_idx] -= 2 * np.pi
if len(small_idx) != 0:
local_yaw[small_idx] += 2 * np.pi
return local_yaw
...@@ -32,7 +32,7 @@ def bbox3d2roi(bbox_list): ...@@ -32,7 +32,7 @@ def bbox3d2roi(bbox_list):
corresponding to a batch of images. corresponding to a batch of images.
Returns: Returns:
torch.Tensor: Region of interests in shape (n, c), where \ torch.Tensor: Region of interests in shape (n, c), where
the channels are in order of [batch_ind, x, y ...]. the channels are in order of [batch_ind, x, y ...].
""" """
rois_list = [] rois_list = []
...@@ -51,10 +51,10 @@ def bbox3d2result(bboxes, scores, labels, attrs=None): ...@@ -51,10 +51,10 @@ def bbox3d2result(bboxes, scores, labels, attrs=None):
"""Convert detection results to a list of numpy arrays. """Convert detection results to a list of numpy arrays.
Args: Args:
bboxes (torch.Tensor): Bounding boxes with shape of (n, 5). bboxes (torch.Tensor): Bounding boxes with shape (N, 5).
labels (torch.Tensor): Labels with shape of (n, ). labels (torch.Tensor): Labels with shape (N, ).
scores (torch.Tensor): Scores with shape of (n, ). scores (torch.Tensor): Scores with shape (N, ).
attrs (torch.Tensor, optional): Attributes with shape of (n, ). \ attrs (torch.Tensor, optional): Attributes with shape (N, ).
Defaults to None. Defaults to None.
Returns: Returns:
......
...@@ -9,9 +9,9 @@ def average_precision(recalls, precisions, mode='area'): ...@@ -9,9 +9,9 @@ def average_precision(recalls, precisions, mode='area'):
"""Calculate average precision (for single or multiple scales). """Calculate average precision (for single or multiple scales).
Args: Args:
recalls (np.ndarray): Recalls with shape of (num_scales, num_dets) \ recalls (np.ndarray): Recalls with shape of (num_scales, num_dets)
or (num_dets, ). or (num_dets, ).
precisions (np.ndarray): Precisions with shape of \ precisions (np.ndarray): Precisions with shape of
(num_scales, num_dets) or (num_dets, ). (num_scales, num_dets) or (num_dets, ).
mode (str): 'area' or '11points', 'area' means calculating the area mode (str): 'area' or '11points', 'area' means calculating the area
under precision-recall curve, '11points' means calculating under precision-recall curve, '11points' means calculating
...@@ -58,13 +58,13 @@ def eval_det_cls(pred, gt, iou_thr=None): ...@@ -58,13 +58,13 @@ def eval_det_cls(pred, gt, iou_thr=None):
single class. single class.
Args: Args:
pred (dict): Predictions mapping from image id to bounding boxes \ pred (dict): Predictions mapping from image id to bounding boxes
and scores. and scores.
gt (dict): Ground truths mapping from image id to bounding boxes. gt (dict): Ground truths mapping from image id to bounding boxes.
iou_thr (list[float]): A list of iou thresholds. iou_thr (list[float]): A list of iou thresholds.
Return: Return:
tuple (np.ndarray, np.ndarray, float): Recalls, precisions and \ tuple (np.ndarray, np.ndarray, float): Recalls, precisions and
average precision. average precision.
""" """
...@@ -170,10 +170,9 @@ def eval_map_recall(pred, gt, ovthresh=None): ...@@ -170,10 +170,9 @@ def eval_map_recall(pred, gt, ovthresh=None):
Args: Args:
pred (dict): Information of detection results, pred (dict): Information of detection results,
which maps class_id and predictions. which maps class_id and predictions.
gt (dict): Information of ground truths, which maps class_id and \ gt (dict): Information of ground truths, which maps class_id and
ground truths. ground truths.
ovthresh (list[float]): iou threshold. ovthresh (list[float], optional): iou threshold. Default: None.
Default: None.
Return: Return:
tuple[dict]: dict results of recall, AP, and precision for all classes. tuple[dict]: dict results of recall, AP, and precision for all classes.
...@@ -218,12 +217,12 @@ def indoor_eval(gt_annos, ...@@ -218,12 +217,12 @@ def indoor_eval(gt_annos,
includes the following keys includes the following keys
- labels_3d (torch.Tensor): Labels of boxes. - labels_3d (torch.Tensor): Labels of boxes.
- boxes_3d (:obj:`BaseInstance3DBoxes`): \ - boxes_3d (:obj:`BaseInstance3DBoxes`):
3D bounding boxes in Depth coordinate. 3D bounding boxes in Depth coordinate.
- scores_3d (torch.Tensor): Scores of boxes. - scores_3d (torch.Tensor): Scores of boxes.
metric (list[float]): IoU thresholds for computing average precisions. metric (list[float]): IoU thresholds for computing average precisions.
label2cat (dict): Map from label to category. label2cat (dict): Map from label to category.
logger (logging.Logger | str | None): The way to print the mAP logger (logging.Logger | str, optional): The way to print the mAP
summary. See `mmdet.utils.print_log()` for details. Default: None. summary. See `mmdet.utils.print_log()` for details. Default: None.
Return: Return:
......
# Copyright (c) OpenMMLab. All rights reserved. # Copyright (c) OpenMMLab. All rights reserved.
import gc import gc
import io as sysio import io as sysio
import numba import numba
import numpy as np import numpy as np
...@@ -569,13 +570,20 @@ def eval_class(gt_annos, ...@@ -569,13 +570,20 @@ def eval_class(gt_annos,
return ret_dict return ret_dict
def get_mAP(prec): def get_mAP11(prec):
sums = 0 sums = 0
for i in range(0, prec.shape[-1], 4): for i in range(0, prec.shape[-1], 4):
sums = sums + prec[..., i] sums = sums + prec[..., i]
return sums / 11 * 100 return sums / 11 * 100
def get_mAP40(prec):
sums = 0
for i in range(1, prec.shape[-1]):
sums = sums + prec[..., i]
return sums / 40 * 100
def print_str(value, *arg, sstream=None): def print_str(value, *arg, sstream=None):
if sstream is None: if sstream is None:
sstream = sysio.StringIO() sstream = sysio.StringIO()
...@@ -592,8 +600,10 @@ def do_eval(gt_annos, ...@@ -592,8 +600,10 @@ def do_eval(gt_annos,
eval_types=['bbox', 'bev', '3d']): eval_types=['bbox', 'bev', '3d']):
# min_overlaps: [num_minoverlap, metric, num_class] # min_overlaps: [num_minoverlap, metric, num_class]
difficultys = [0, 1, 2] difficultys = [0, 1, 2]
mAP_bbox = None mAP11_bbox = None
mAP_aos = None mAP11_aos = None
mAP40_bbox = None
mAP40_aos = None
if 'bbox' in eval_types: if 'bbox' in eval_types:
ret = eval_class( ret = eval_class(
gt_annos, gt_annos,
...@@ -604,22 +614,29 @@ def do_eval(gt_annos, ...@@ -604,22 +614,29 @@ def do_eval(gt_annos,
min_overlaps, min_overlaps,
compute_aos=('aos' in eval_types)) compute_aos=('aos' in eval_types))
# ret: [num_class, num_diff, num_minoverlap, num_sample_points] # ret: [num_class, num_diff, num_minoverlap, num_sample_points]
mAP_bbox = get_mAP(ret['precision']) mAP11_bbox = get_mAP11(ret['precision'])
mAP40_bbox = get_mAP40(ret['precision'])
if 'aos' in eval_types: if 'aos' in eval_types:
mAP_aos = get_mAP(ret['orientation']) mAP11_aos = get_mAP11(ret['orientation'])
mAP40_aos = get_mAP40(ret['orientation'])
mAP_bev = None mAP11_bev = None
mAP40_bev = None
if 'bev' in eval_types: if 'bev' in eval_types:
ret = eval_class(gt_annos, dt_annos, current_classes, difficultys, 1, ret = eval_class(gt_annos, dt_annos, current_classes, difficultys, 1,
min_overlaps) min_overlaps)
mAP_bev = get_mAP(ret['precision']) mAP11_bev = get_mAP11(ret['precision'])
mAP40_bev = get_mAP40(ret['precision'])
mAP_3d = None mAP11_3d = None
mAP40_3d = None
if '3d' in eval_types: if '3d' in eval_types:
ret = eval_class(gt_annos, dt_annos, current_classes, difficultys, 2, ret = eval_class(gt_annos, dt_annos, current_classes, difficultys, 2,
min_overlaps) min_overlaps)
mAP_3d = get_mAP(ret['precision']) mAP11_3d = get_mAP11(ret['precision'])
return mAP_bbox, mAP_bev, mAP_3d, mAP_aos mAP40_3d = get_mAP40(ret['precision'])
return (mAP11_bbox, mAP11_bev, mAP11_3d, mAP11_aos, mAP40_bbox, mAP40_bev,
mAP40_3d, mAP40_aos)
def do_coco_style_eval(gt_annos, dt_annos, current_classes, overlap_ranges, def do_coco_style_eval(gt_annos, dt_annos, current_classes, overlap_ranges,
...@@ -629,9 +646,10 @@ def do_coco_style_eval(gt_annos, dt_annos, current_classes, overlap_ranges, ...@@ -629,9 +646,10 @@ def do_coco_style_eval(gt_annos, dt_annos, current_classes, overlap_ranges,
for i in range(overlap_ranges.shape[1]): for i in range(overlap_ranges.shape[1]):
for j in range(overlap_ranges.shape[2]): for j in range(overlap_ranges.shape[2]):
min_overlaps[:, i, j] = np.linspace(*overlap_ranges[:, i, j]) min_overlaps[:, i, j] = np.linspace(*overlap_ranges[:, i, j])
mAP_bbox, mAP_bev, mAP_3d, mAP_aos = do_eval(gt_annos, dt_annos, mAP_bbox, mAP_bev, mAP_3d, mAP_aos, _, _, \
current_classes, min_overlaps, _, _ = do_eval(gt_annos, dt_annos,
compute_aos) current_classes, min_overlaps,
compute_aos)
# ret: [num_class, num_diff, num_minoverlap] # ret: [num_class, num_diff, num_minoverlap]
mAP_bbox = mAP_bbox.mean(-1) mAP_bbox = mAP_bbox.mean(-1)
mAP_bev = mAP_bev.mean(-1) mAP_bev = mAP_bev.mean(-1)
...@@ -703,33 +721,109 @@ def kitti_eval(gt_annos, ...@@ -703,33 +721,109 @@ def kitti_eval(gt_annos,
if compute_aos: if compute_aos:
eval_types.append('aos') eval_types.append('aos')
mAPbbox, mAPbev, mAP3d, mAPaos = do_eval(gt_annos, dt_annos, mAP11_bbox, mAP11_bev, mAP11_3d, mAP11_aos, mAP40_bbox, mAP40_bev, \
current_classes, min_overlaps, mAP40_3d, mAP40_aos = do_eval(gt_annos, dt_annos,
eval_types) current_classes, min_overlaps,
eval_types)
ret_dict = {} ret_dict = {}
difficulty = ['easy', 'moderate', 'hard'] difficulty = ['easy', 'moderate', 'hard']
# calculate AP11
result += '\n----------- AP11 Results ------------\n\n'
for j, curcls in enumerate(current_classes): for j, curcls in enumerate(current_classes):
# mAP threshold array: [num_minoverlap, metric, class] # mAP threshold array: [num_minoverlap, metric, class]
# mAP result: [num_class, num_diff, num_minoverlap] # mAP result: [num_class, num_diff, num_minoverlap]
curcls_name = class_to_name[curcls] curcls_name = class_to_name[curcls]
for i in range(min_overlaps.shape[0]): for i in range(min_overlaps.shape[0]):
# prepare results for print # prepare results for print
result += ('{} AP@{:.2f}, {:.2f}, {:.2f}:\n'.format( result += ('{} AP11@{:.2f}, {:.2f}, {:.2f}:\n'.format(
curcls_name, *min_overlaps[i, :, j])) curcls_name, *min_overlaps[i, :, j]))
if mAPbbox is not None: if mAP11_bbox is not None:
result += 'bbox AP:{:.4f}, {:.4f}, {:.4f}\n'.format( result += 'bbox AP11:{:.4f}, {:.4f}, {:.4f}\n'.format(
*mAPbbox[j, :, i]) *mAP11_bbox[j, :, i])
if mAPbev is not None: if mAP11_bev is not None:
result += 'bev AP:{:.4f}, {:.4f}, {:.4f}\n'.format( result += 'bev AP11:{:.4f}, {:.4f}, {:.4f}\n'.format(
*mAPbev[j, :, i]) *mAP11_bev[j, :, i])
if mAP3d is not None: if mAP11_3d is not None:
result += '3d AP:{:.4f}, {:.4f}, {:.4f}\n'.format( result += '3d AP11:{:.4f}, {:.4f}, {:.4f}\n'.format(
*mAP3d[j, :, i]) *mAP11_3d[j, :, i])
if compute_aos:
result += 'aos AP11:{:.2f}, {:.2f}, {:.2f}\n'.format(
*mAP11_aos[j, :, i])
# prepare results for logger
for idx in range(3):
if i == 0:
postfix = f'{difficulty[idx]}_strict'
else:
postfix = f'{difficulty[idx]}_loose'
prefix = f'KITTI/{curcls_name}'
if mAP11_3d is not None:
ret_dict[f'{prefix}_3D_AP11_{postfix}'] =\
mAP11_3d[j, idx, i]
if mAP11_bev is not None:
ret_dict[f'{prefix}_BEV_AP11_{postfix}'] =\
mAP11_bev[j, idx, i]
if mAP11_bbox is not None:
ret_dict[f'{prefix}_2D_AP11_{postfix}'] =\
mAP11_bbox[j, idx, i]
# calculate mAP11 over all classes if there are multiple classes
if len(current_classes) > 1:
# prepare results for print
result += ('\nOverall AP11@{}, {}, {}:\n'.format(*difficulty))
if mAP11_bbox is not None:
mAP11_bbox = mAP11_bbox.mean(axis=0)
result += 'bbox AP11:{:.4f}, {:.4f}, {:.4f}\n'.format(
*mAP11_bbox[:, 0])
if mAP11_bev is not None:
mAP11_bev = mAP11_bev.mean(axis=0)
result += 'bev AP11:{:.4f}, {:.4f}, {:.4f}\n'.format(
*mAP11_bev[:, 0])
if mAP11_3d is not None:
mAP11_3d = mAP11_3d.mean(axis=0)
result += '3d AP11:{:.4f}, {:.4f}, {:.4f}\n'.format(*mAP11_3d[:,
0])
if compute_aos:
mAP11_aos = mAP11_aos.mean(axis=0)
result += 'aos AP11:{:.2f}, {:.2f}, {:.2f}\n'.format(
*mAP11_aos[:, 0])
# prepare results for logger
for idx in range(3):
postfix = f'{difficulty[idx]}'
if mAP11_3d is not None:
ret_dict[f'KITTI/Overall_3D_AP11_{postfix}'] = mAP11_3d[idx, 0]
if mAP11_bev is not None:
ret_dict[f'KITTI/Overall_BEV_AP11_{postfix}'] =\
mAP11_bev[idx, 0]
if mAP11_bbox is not None:
ret_dict[f'KITTI/Overall_2D_AP11_{postfix}'] =\
mAP11_bbox[idx, 0]
# Calculate AP40
result += '\n----------- AP40 Results ------------\n\n'
for j, curcls in enumerate(current_classes):
# mAP threshold array: [num_minoverlap, metric, class]
# mAP result: [num_class, num_diff, num_minoverlap]
curcls_name = class_to_name[curcls]
for i in range(min_overlaps.shape[0]):
# prepare results for print
result += ('{} AP40@{:.2f}, {:.2f}, {:.2f}:\n'.format(
curcls_name, *min_overlaps[i, :, j]))
if mAP40_bbox is not None:
result += 'bbox AP40:{:.4f}, {:.4f}, {:.4f}\n'.format(
*mAP40_bbox[j, :, i])
if mAP40_bev is not None:
result += 'bev AP40:{:.4f}, {:.4f}, {:.4f}\n'.format(
*mAP40_bev[j, :, i])
if mAP40_3d is not None:
result += '3d AP40:{:.4f}, {:.4f}, {:.4f}\n'.format(
*mAP40_3d[j, :, i])
if compute_aos: if compute_aos:
result += 'aos AP:{:.2f}, {:.2f}, {:.2f}\n'.format( result += 'aos AP40:{:.2f}, {:.2f}, {:.2f}\n'.format(
*mAPaos[j, :, i]) *mAP40_aos[j, :, i])
# prepare results for logger # prepare results for logger
for idx in range(3): for idx in range(3):
...@@ -738,39 +832,48 @@ def kitti_eval(gt_annos, ...@@ -738,39 +832,48 @@ def kitti_eval(gt_annos,
else: else:
postfix = f'{difficulty[idx]}_loose' postfix = f'{difficulty[idx]}_loose'
prefix = f'KITTI/{curcls_name}' prefix = f'KITTI/{curcls_name}'
if mAP3d is not None: if mAP40_3d is not None:
ret_dict[f'{prefix}_3D_{postfix}'] = mAP3d[j, idx, i] ret_dict[f'{prefix}_3D_AP40_{postfix}'] =\
if mAPbev is not None: mAP40_3d[j, idx, i]
ret_dict[f'{prefix}_BEV_{postfix}'] = mAPbev[j, idx, i] if mAP40_bev is not None:
if mAPbbox is not None: ret_dict[f'{prefix}_BEV_AP40_{postfix}'] =\
ret_dict[f'{prefix}_2D_{postfix}'] = mAPbbox[j, idx, i] mAP40_bev[j, idx, i]
if mAP40_bbox is not None:
# calculate mAP over all classes if there are multiple classes ret_dict[f'{prefix}_2D_AP40_{postfix}'] =\
mAP40_bbox[j, idx, i]
# calculate mAP40 over all classes if there are multiple classes
if len(current_classes) > 1: if len(current_classes) > 1:
# prepare results for print # prepare results for print
result += ('\nOverall AP@{}, {}, {}:\n'.format(*difficulty)) result += ('\nOverall AP40@{}, {}, {}:\n'.format(*difficulty))
if mAPbbox is not None: if mAP40_bbox is not None:
mAPbbox = mAPbbox.mean(axis=0) mAP40_bbox = mAP40_bbox.mean(axis=0)
result += 'bbox AP:{:.4f}, {:.4f}, {:.4f}\n'.format(*mAPbbox[:, 0]) result += 'bbox AP40:{:.4f}, {:.4f}, {:.4f}\n'.format(
if mAPbev is not None: *mAP40_bbox[:, 0])
mAPbev = mAPbev.mean(axis=0) if mAP40_bev is not None:
result += 'bev AP:{:.4f}, {:.4f}, {:.4f}\n'.format(*mAPbev[:, 0]) mAP40_bev = mAP40_bev.mean(axis=0)
if mAP3d is not None: result += 'bev AP40:{:.4f}, {:.4f}, {:.4f}\n'.format(
mAP3d = mAP3d.mean(axis=0) *mAP40_bev[:, 0])
result += '3d AP:{:.4f}, {:.4f}, {:.4f}\n'.format(*mAP3d[:, 0]) if mAP40_3d is not None:
mAP40_3d = mAP40_3d.mean(axis=0)
result += '3d AP40:{:.4f}, {:.4f}, {:.4f}\n'.format(*mAP40_3d[:,
0])
if compute_aos: if compute_aos:
mAPaos = mAPaos.mean(axis=0) mAP40_aos = mAP40_aos.mean(axis=0)
result += 'aos AP:{:.2f}, {:.2f}, {:.2f}\n'.format(*mAPaos[:, 0]) result += 'aos AP40:{:.2f}, {:.2f}, {:.2f}\n'.format(
*mAP40_aos[:, 0])
# prepare results for logger # prepare results for logger
for idx in range(3): for idx in range(3):
postfix = f'{difficulty[idx]}' postfix = f'{difficulty[idx]}'
if mAP3d is not None: if mAP40_3d is not None:
ret_dict[f'KITTI/Overall_3D_{postfix}'] = mAP3d[idx, 0] ret_dict[f'KITTI/Overall_3D_AP40_{postfix}'] = mAP40_3d[idx, 0]
if mAPbev is not None: if mAP40_bev is not None:
ret_dict[f'KITTI/Overall_BEV_{postfix}'] = mAPbev[idx, 0] ret_dict[f'KITTI/Overall_BEV_AP40_{postfix}'] =\
if mAPbbox is not None: mAP40_bev[idx, 0]
ret_dict[f'KITTI/Overall_2D_{postfix}'] = mAPbbox[idx, 0] if mAP40_bbox is not None:
ret_dict[f'KITTI/Overall_2D_AP40_{postfix}'] =\
mAP40_bbox[idx, 0]
return result, ret_dict return result, ret_dict
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
# Author: yanyan, scrin@foxmail.com # Author: yanyan, scrin@foxmail.com
##################### #####################
import math import math
import numba import numba
import numpy as np import numpy as np
from numba import cuda from numba import cuda
...@@ -15,13 +16,13 @@ def div_up(m, n): ...@@ -15,13 +16,13 @@ def div_up(m, n):
return m // n + (m % n > 0) return m // n + (m % n > 0)
@cuda.jit('(float32[:], float32[:], float32[:])', device=True, inline=True) @cuda.jit(device=True, inline=True)
def trangle_area(a, b, c): def trangle_area(a, b, c):
return ((a[0] - c[0]) * (b[1] - c[1]) - (a[1] - c[1]) * return ((a[0] - c[0]) * (b[1] - c[1]) - (a[1] - c[1]) *
(b[0] - c[0])) / 2.0 (b[0] - c[0])) / 2.0
@cuda.jit('(float32[:], int32)', device=True, inline=True) @cuda.jit(device=True, inline=True)
def area(int_pts, num_of_inter): def area(int_pts, num_of_inter):
area_val = 0.0 area_val = 0.0
for i in range(num_of_inter - 2): for i in range(num_of_inter - 2):
...@@ -31,7 +32,7 @@ def area(int_pts, num_of_inter): ...@@ -31,7 +32,7 @@ def area(int_pts, num_of_inter):
return area_val return area_val
@cuda.jit('(float32[:], int32)', device=True, inline=True) @cuda.jit(device=True, inline=True)
def sort_vertex_in_convex_polygon(int_pts, num_of_inter): def sort_vertex_in_convex_polygon(int_pts, num_of_inter):
if num_of_inter > 0: if num_of_inter > 0:
center = cuda.local.array((2, ), dtype=numba.float32) center = cuda.local.array((2, ), dtype=numba.float32)
...@@ -71,10 +72,7 @@ def sort_vertex_in_convex_polygon(int_pts, num_of_inter): ...@@ -71,10 +72,7 @@ def sort_vertex_in_convex_polygon(int_pts, num_of_inter):
int_pts[j * 2 + 1] = ty int_pts[j * 2 + 1] = ty
@cuda.jit( @cuda.jit(device=True, inline=True)
'(float32[:], float32[:], int32, int32, float32[:])',
device=True,
inline=True)
def line_segment_intersection(pts1, pts2, i, j, temp_pts): def line_segment_intersection(pts1, pts2, i, j, temp_pts):
A = cuda.local.array((2, ), dtype=numba.float32) A = cuda.local.array((2, ), dtype=numba.float32)
B = cuda.local.array((2, ), dtype=numba.float32) B = cuda.local.array((2, ), dtype=numba.float32)
...@@ -117,10 +115,7 @@ def line_segment_intersection(pts1, pts2, i, j, temp_pts): ...@@ -117,10 +115,7 @@ def line_segment_intersection(pts1, pts2, i, j, temp_pts):
return False return False
@cuda.jit( @cuda.jit(device=True, inline=True)
'(float32[:], float32[:], int32, int32, float32[:])',
device=True,
inline=True)
def line_segment_intersection_v1(pts1, pts2, i, j, temp_pts): def line_segment_intersection_v1(pts1, pts2, i, j, temp_pts):
a = cuda.local.array((2, ), dtype=numba.float32) a = cuda.local.array((2, ), dtype=numba.float32)
b = cuda.local.array((2, ), dtype=numba.float32) b = cuda.local.array((2, ), dtype=numba.float32)
...@@ -159,7 +154,7 @@ def line_segment_intersection_v1(pts1, pts2, i, j, temp_pts): ...@@ -159,7 +154,7 @@ def line_segment_intersection_v1(pts1, pts2, i, j, temp_pts):
return True return True
@cuda.jit('(float32, float32, float32[:])', device=True, inline=True) @cuda.jit(device=True, inline=True)
def point_in_quadrilateral(pt_x, pt_y, corners): def point_in_quadrilateral(pt_x, pt_y, corners):
ab0 = corners[2] - corners[0] ab0 = corners[2] - corners[0]
ab1 = corners[3] - corners[1] ab1 = corners[3] - corners[1]
...@@ -178,7 +173,7 @@ def point_in_quadrilateral(pt_x, pt_y, corners): ...@@ -178,7 +173,7 @@ def point_in_quadrilateral(pt_x, pt_y, corners):
return abab >= abap and abap >= 0 and adad >= adap and adap >= 0 return abab >= abap and abap >= 0 and adad >= adap and adap >= 0
@cuda.jit('(float32[:], float32[:], float32[:])', device=True, inline=True) @cuda.jit(device=True, inline=True)
def quadrilateral_intersection(pts1, pts2, int_pts): def quadrilateral_intersection(pts1, pts2, int_pts):
num_of_inter = 0 num_of_inter = 0
for i in range(4): for i in range(4):
...@@ -202,7 +197,7 @@ def quadrilateral_intersection(pts1, pts2, int_pts): ...@@ -202,7 +197,7 @@ def quadrilateral_intersection(pts1, pts2, int_pts):
return num_of_inter return num_of_inter
@cuda.jit('(float32[:], float32[:])', device=True, inline=True) @cuda.jit(device=True, inline=True)
def rbbox_to_corners(corners, rbbox): def rbbox_to_corners(corners, rbbox):
# generate clockwise corners and rotate it clockwise # generate clockwise corners and rotate it clockwise
angle = rbbox[4] angle = rbbox[4]
...@@ -228,7 +223,7 @@ def rbbox_to_corners(corners, rbbox): ...@@ -228,7 +223,7 @@ def rbbox_to_corners(corners, rbbox):
1] = -a_sin * corners_x[i] + a_cos * corners_y[i] + center_y 1] = -a_sin * corners_x[i] + a_cos * corners_y[i] + center_y
@cuda.jit('(float32[:], float32[:])', device=True, inline=True) @cuda.jit(device=True, inline=True)
def inter(rbbox1, rbbox2): def inter(rbbox1, rbbox2):
"""Compute intersection of two rotated boxes. """Compute intersection of two rotated boxes.
...@@ -254,7 +249,7 @@ def inter(rbbox1, rbbox2): ...@@ -254,7 +249,7 @@ def inter(rbbox1, rbbox2):
return area(intersection_corners, num_intersection) return area(intersection_corners, num_intersection)
@cuda.jit('(float32[:], float32[:], int32)', device=True, inline=True) @cuda.jit(device=True, inline=True)
def devRotateIoUEval(rbox1, rbox2, criterion=-1): def devRotateIoUEval(rbox1, rbox2, criterion=-1):
"""Compute rotated iou on device. """Compute rotated iou on device.
...@@ -291,7 +286,8 @@ def rotate_iou_kernel_eval(N, ...@@ -291,7 +286,8 @@ def rotate_iou_kernel_eval(N,
dev_query_boxes, dev_query_boxes,
dev_iou, dev_iou,
criterion=-1): criterion=-1):
"""Kernel of computing rotated iou. """Kernel of computing rotated IoU. This function is for bev boxes in
camera coordinate system ONLY (the rotation is clockwise).
Args: Args:
N (int): The number of boxes. N (int): The number of boxes.
...@@ -343,10 +339,14 @@ def rotate_iou_gpu_eval(boxes, query_boxes, criterion=-1, device_id=0): ...@@ -343,10 +339,14 @@ def rotate_iou_gpu_eval(boxes, query_boxes, criterion=-1, device_id=0):
in one example with numba.cuda code). convert from [this project]( in one example with numba.cuda code). convert from [this project](
https://github.com/hongzhenwang/RRPN-revise/tree/master/lib/rotation). https://github.com/hongzhenwang/RRPN-revise/tree/master/lib/rotation).
This function is for bev boxes in camera coordinate system ONLY
(the rotation is clockwise).
Args: Args:
boxes (torch.Tensor): rbboxes. format: centers, dims, boxes (torch.Tensor): rbboxes. format: centers, dims,
angles(clockwise when positive) with the shape of [N, 5]. angles(clockwise when positive) with the shape of [N, 5].
query_boxes (float tensor: [K, 5]): rbboxes to compute iou with boxes. query_boxes (torch.FloatTensor, shape=(K, 5)):
rbboxes to compute iou with boxes.
device_id (int, optional): Defaults to 0. Device to use. device_id (int, optional): Defaults to 0. Device to use.
criterion (int, optional): Indicate different type of iou. criterion (int, optional): Indicate different type of iou.
-1 indicate `area_inter / (area1 + area2 - area_inter)`, -1 indicate `area_inter / (area1 + area2 - area_inter)`,
......
# Copyright (c) OpenMMLab. All rights reserved. # Copyright (c) OpenMMLab. All rights reserved.
from os import path as osp
import mmcv import mmcv
import numpy as np import numpy as np
from lyft_dataset_sdk.eval.detection.mAP_evaluation import (Box3D, get_ap, from lyft_dataset_sdk.eval.detection.mAP_evaluation import (Box3D, get_ap,
...@@ -7,7 +9,6 @@ from lyft_dataset_sdk.eval.detection.mAP_evaluation import (Box3D, get_ap, ...@@ -7,7 +9,6 @@ from lyft_dataset_sdk.eval.detection.mAP_evaluation import (Box3D, get_ap,
group_by_key, group_by_key,
wrap_in_box) wrap_in_box)
from mmcv.utils import print_log from mmcv.utils import print_log
from os import path as osp
from terminaltables import AsciiTable from terminaltables import AsciiTable
...@@ -18,7 +19,7 @@ def load_lyft_gts(lyft, data_root, eval_split, logger=None): ...@@ -18,7 +19,7 @@ def load_lyft_gts(lyft, data_root, eval_split, logger=None):
lyft (:obj:`LyftDataset`): Lyft class in the sdk. lyft (:obj:`LyftDataset`): Lyft class in the sdk.
data_root (str): Root of data for reading splits. data_root (str): Root of data for reading splits.
eval_split (str): Name of the split for evaluation. eval_split (str): Name of the split for evaluation.
logger (logging.Logger | str | None): Logger used for printing logger (logging.Logger | str, optional): Logger used for printing
related information during evaluation. Default: None. related information during evaluation. Default: None.
Returns: Returns:
...@@ -96,7 +97,7 @@ def lyft_eval(lyft, data_root, res_path, eval_set, output_dir, logger=None): ...@@ -96,7 +97,7 @@ def lyft_eval(lyft, data_root, res_path, eval_set, output_dir, logger=None):
res_path (str): Path of result json file recording detections. res_path (str): Path of result json file recording detections.
eval_set (str): Name of the split for evaluation. eval_set (str): Name of the split for evaluation.
output_dir (str): Output directory for output json files. output_dir (str): Output directory for output json files.
logger (logging.Logger | str | None): Logger used for printing logger (logging.Logger | str, optional): Logger used for printing
related information during evaluation. Default: None. related information during evaluation. Default: None.
Returns: Returns:
...@@ -202,9 +203,9 @@ def get_single_class_aps(gt, predictions, iou_thresholds): ...@@ -202,9 +203,9 @@ def get_single_class_aps(gt, predictions, iou_thresholds):
Args: Args:
gt (list[dict]): list of dictionaries in the format described above. gt (list[dict]): list of dictionaries in the format described above.
predictions (list[dict]): list of dictionaries in the format \ predictions (list[dict]): list of dictionaries in the format
described below. described below.
iou_thresholds (list[float]): IOU thresholds used to calculate \ iou_thresholds (list[float]): IOU thresholds used to calculate
TP / FN TP / FN
Returns: Returns:
......
...@@ -77,7 +77,7 @@ def seg_eval(gt_labels, seg_preds, label2cat, ignore_index, logger=None): ...@@ -77,7 +77,7 @@ def seg_eval(gt_labels, seg_preds, label2cat, ignore_index, logger=None):
seg_preds (list[torch.Tensor]): Predictions. seg_preds (list[torch.Tensor]): Predictions.
label2cat (dict): Map from label to category name. label2cat (dict): Map from label to category name.
ignore_index (int): Index that will be ignored in evaluation. ignore_index (int): Index that will be ignored in evaluation.
logger (logging.Logger | str | None): The way to print the mAP logger (logging.Logger | str, optional): The way to print the mAP
summary. See `mmdet.utils.print_log()` for details. Default: None. summary. See `mmdet.utils.print_log()` for details. Default: None.
Returns: Returns:
......
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