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):
"""Encode ground truth to prediction targets.
Args:
gt_bboxes_3d (BaseInstance3DBoxes): Ground truth bboxes \
gt_bboxes_3d (BaseInstance3DBoxes): Ground truth bboxes
with shape (n, 7).
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):
between each aligned pair of bboxes1 and bboxes2.
Args:
bboxes1 (torch.Tensor): shape (N, 7+N) [x, y, z, h, w, l, ry, v].
bboxes2 (torch.Tensor): shape (M, 7+N) [x, y, z, h, w, l, ry, v].
bboxes1 (torch.Tensor): shape (N, 7+N)
[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
(intersection over foreground).
is_aligned (bool): Whether the calculation is aligned.
Return:
torch.Tensor: If ``is_aligned`` is ``True``, return ious between \
bboxes1 and bboxes2 with shape (M, N). If ``is_aligned`` is \
torch.Tensor: If ``is_aligned`` is ``True``, return ious between
bboxes1 and bboxes2 with shape (M, N). If ``is_aligned`` is
``False``, return shape is M.
"""
return bbox_overlaps_nearest_3d(bboxes1, bboxes2, mode, is_aligned,
......@@ -74,13 +76,15 @@ class BboxOverlaps3D(object):
calculate the actual 3D IoUs of boxes.
Args:
bboxes1 (torch.Tensor): shape (N, 7+C) [x, y, z, h, w, l, ry].
bboxes2 (torch.Tensor): shape (M, 7+C) [x, y, z, h, w, l, ry].
bboxes1 (torch.Tensor): with shape (N, 7+C),
(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 (intersection over foreground).
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).
"""
return bbox_overlaps_3d(bboxes1, bboxes2, mode, self.coordinate)
......@@ -102,7 +106,7 @@ def bbox_overlaps_nearest_3d(bboxes1,
Note:
This function first finds the nearest 2D boxes in bird eye view
(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.
If ``is_aligned`` is ``False``, then it calculates the ious between
......@@ -110,15 +114,17 @@ def bbox_overlaps_nearest_3d(bboxes1,
aligned pair of bboxes1 and bboxes2.
Args:
bboxes1 (torch.Tensor): shape (N, 7+C) [x, y, z, h, w, l, ry, v].
bboxes2 (torch.Tensor): shape (M, 7+C) [x, y, z, h, w, l, ry, v].
bboxes1 (torch.Tensor): with shape (N, 7+C),
(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
(intersection over foreground).
is_aligned (bool): Whether the calculation is aligned
Return:
torch.Tensor: If ``is_aligned`` is ``True``, return ious between \
bboxes1 and bboxes2 with shape (M, N). If ``is_aligned`` is \
torch.Tensor: If ``is_aligned`` is ``True``, return ious between
bboxes1 and bboxes2 with shape (M, N). If ``is_aligned`` is
``False``, return shape is M.
"""
assert bboxes1.size(-1) == bboxes2.size(-1) >= 7
......@@ -148,14 +154,16 @@ def bbox_overlaps_3d(bboxes1, bboxes2, mode='iou', coordinate='camera'):
calculate the actual IoUs of boxes.
Args:
bboxes1 (torch.Tensor): shape (N, 7+C) [x, y, z, h, w, l, ry].
bboxes2 (torch.Tensor): shape (M, 7+C) [x, y, z, h, w, l, ry].
bboxes1 (torch.Tensor): with shape (N, 7+C),
(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 (intersection over foreground).
coordinate (str): 'camera' or 'lidar' coordinate system.
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).
"""
assert bboxes1.size(-1) == bboxes2.size(-1) >= 7
......@@ -185,7 +193,7 @@ class AxisAlignedBboxOverlaps3D(object):
mode (str): "iou" (intersection over union) or "giou" (generalized
intersection over union).
is_aligned (bool, optional): If True, then m and n must be equal.
Default False.
Defaults to False.
Returns:
Tensor: shape (m, n) if ``is_aligned`` is False else shape (m,)
"""
......@@ -219,9 +227,9 @@ def axis_aligned_bbox_overlaps_3d(bboxes1,
mode (str): "iou" (intersection over union) or "giou" (generalized
intersection over union).
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
stability. Default 1e-6.
stability. Defaults to 1e-6.
Returns:
Tensor: shape (m, n) if ``is_aligned`` is False else shape (m,)
......@@ -250,7 +258,7 @@ def axis_aligned_bbox_overlaps_3d(bboxes1,
"""
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 (bboxes2.size(-1) == 6 or bboxes2.size(0) == 0)
......
......@@ -9,8 +9,8 @@ from . import RandomSampler, SamplingResult
class IoUNegPiecewiseSampler(RandomSampler):
"""IoU Piece-wise Sampling.
Sampling negtive proposals according to a list of IoU thresholds.
The negtive proposals are divided into several pieces according
Sampling negative proposals according to a list of IoU thresholds.
The negative proposals are divided into several pieces according
to `neg_iou_piece_thrs`. And the ratio of each piece is indicated
by `neg_piece_fractions`.
......@@ -18,11 +18,11 @@ class IoUNegPiecewiseSampler(RandomSampler):
num (int): Number of proposals.
pos_fraction (float): The fraction of positive proposals.
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
indicate the upper bound of this piece.
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.
"""
......@@ -59,8 +59,8 @@ class IoUNegPiecewiseSampler(RandomSampler):
neg_inds = torch.nonzero(assign_result.gt_inds == 0, as_tuple=False)
if neg_inds.numel() != 0:
neg_inds = neg_inds.squeeze(1)
if len(neg_inds) <= num_expected:
return neg_inds
if len(neg_inds) <= 0:
return neg_inds.squeeze(1)
else:
neg_inds_choice = neg_inds.new_zeros([0])
extend_num = 0
......@@ -88,12 +88,38 @@ class IoUNegPiecewiseSampler(RandomSampler):
neg_inds_choice = torch.cat(
[neg_inds_choice, neg_inds[piece_neg_inds]], dim=0)
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:
piece_choice = self.random_choice(piece_neg_inds,
piece_expected_num)
neg_inds_choice = torch.cat(
[neg_inds_choice, neg_inds[piece_choice]], dim=0)
extend_num = 0
assert len(neg_inds_choice) == num_expected
return neg_inds_choice
def sample(self,
......@@ -111,7 +137,7 @@ class IoUNegPiecewiseSampler(RandomSampler):
assign_result (:obj:`AssignResult`): Bbox assigning results.
bboxes (torch.Tensor): Boxes to be sampled from.
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.
Returns:
......@@ -145,7 +171,6 @@ class IoUNegPiecewiseSampler(RandomSampler):
num_expected_neg = neg_upper_bound
neg_inds = self.neg_sampler._sample_neg(
assign_result, num_expected_neg, bboxes=bboxes, **kwargs)
neg_inds = neg_inds.unique()
sampling_result = SamplingResult(pos_inds, neg_inds, bboxes, gt_bboxes,
assign_result, gt_flags)
......
......@@ -6,12 +6,13 @@ from .coord_3d_mode import Coord3DMode
from .depth_box3d import DepthInstance3DBoxes
from .lidar_box3d import LiDARInstance3DBoxes
from .utils import (get_box_type, get_proj_mat_by_coord_type, limit_period,
mono_cam_box2vis, points_cam2img, rotation_3d_in_axis,
xywhr2xyxyr)
mono_cam_box2vis, points_cam2img, points_img2cam,
rotation_3d_in_axis, xywhr2xyxyr)
__all__ = [
'Box3DMode', 'BaseInstance3DBoxes', 'LiDARInstance3DBoxes',
'CameraInstance3DBoxes', 'DepthInstance3DBoxes', 'xywhr2xyxyr',
'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.
import warnings
from abc import abstractmethod
import numpy as np
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 .utils import limit_period, xywhr2xyxyr
......@@ -18,12 +21,12 @@ class BaseInstance3DBoxes(object):
tensor (torch.Tensor | np.ndarray | list): a N x box_dim matrix.
box_dim (int): Number of the dimension of a box.
Each row is (x, y, z, x_size, y_size, z_size, yaw).
Default to 7.
Defaults to 7.
with_yaw (bool): Whether the box is with yaw rotation.
If False, the value of yaw will be set to 0 as minmax boxes.
Default to True.
origin (tuple[float]): The relative position of origin in the box.
Default to (0.5, 0.5, 0). This will guide the box be converted to
Defaults to True.
origin (tuple[float], optional): Relative position of the box origin.
Defaults to (0.5, 0.5, 0). This will guide the box be converted to
(0.5, 0.5, 0) mode.
Attributes:
......@@ -72,27 +75,29 @@ class BaseInstance3DBoxes(object):
@property
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]
@property
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]
@property
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]
@property
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
@property
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]
@property
......@@ -100,58 +105,114 @@ class BaseInstance3DBoxes(object):
"""Calculate the center of all the boxes.
Note:
In the MMDetection3D's convention, the bottom center is
In MMDetection3D's convention, the bottom center is
usually taken as the default center.
The relative position of the centers in different kinds of
boxes are different, e.g., the relative center of a boxes is
(0.5, 1.0, 0.5) in camera and (0.5, 0.5, 0) in lidar.
It is recommended to use ``bottom_center`` or ``gravity_center``
for more clear usage.
for clearer usage.
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
@property
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]
@property
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
@property
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
@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
def rotate(self, angle, points=None):
"""Rotate boxes with points (optional) with the given angle or \
rotation matrix.
"""Rotate boxes with points (optional) with the given angle or rotation
matrix.
Args:
angle (float | torch.Tensor | np.ndarray):
Rotation angle or rotation matrix.
points (torch.Tensor, numpy.ndarray, :obj:`BasePoints`, optional):
points (torch.Tensor | numpy.ndarray |
:obj:`BasePoints`, optional):
Points to rotate. Defaults to None.
"""
pass
@abstractmethod
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
def translate(self, trans_vector):
"""Translate boxes with the given translation vector.
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):
trans_vector = self.tensor.new_tensor(trans_vector)
......@@ -170,7 +231,7 @@ class BaseInstance3DBoxes(object):
polygon, we try to reduce the burden for simpler cases.
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.
"""
in_range_flags = ((self.tensor[:, 0] > box_range[0])
......@@ -181,34 +242,21 @@ class BaseInstance3DBoxes(object):
& (self.tensor[:, 2] < box_range[5]))
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
def convert_to(self, dst, rt_mat=None):
"""Convert self to ``dst`` mode.
Args:
dst (:obj:`Box3DMode`): The target Box mode.
rt_mat (np.ndarray | torch.Tensor): The rotation and translation
matrix between different coordinates. Defaults to None.
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.
Returns:
:obj:`BaseInstance3DBoxes`: The converted box of the same type \
:obj:`BaseInstance3DBoxes`: The converted box of the same type
in the `dst` mode.
"""
pass
......@@ -220,28 +268,29 @@ class BaseInstance3DBoxes(object):
scale_factors (float): Scale factors to scale the boxes.
"""
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):
"""Limit the yaw to a given period and offset.
Args:
offset (float): The offset of the yaw.
period (float): The expected period.
offset (float, optional): The offset of the yaw. Defaults to 0.5.
period (float, optional): The expected period. Defaults to np.pi.
"""
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.
A box is considered empty,
if either of its side is no larger than threshold.
Args:
threshold (float): The threshold of minimal sizes.
threshold (float, optional): The threshold of minimal sizes.
Defaults to 0.0.
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 = self.tensor
......@@ -267,8 +316,8 @@ class BaseInstance3DBoxes(object):
subject to Pytorch's indexing semantics.
Returns:
:obj:`BaseInstance3DBoxes`: A new object of \
:class:`BaseInstances3DBoxes` after indexing.
:obj:`BaseInstance3DBoxes`: A new object of
:class:`BaseInstance3DBoxes` after indexing.
"""
original_type = type(self)
if isinstance(item, int):
......@@ -319,7 +368,7 @@ class BaseInstance3DBoxes(object):
device (str | :obj:`torch.device`): The name of the device.
Returns:
:obj:`BaseInstance3DBoxes`: A new boxes object on the \
:obj:`BaseInstance3DBoxes`: A new boxes object on the
specific device.
"""
original_type = type(self)
......@@ -332,7 +381,7 @@ class BaseInstance3DBoxes(object):
"""Clone the Boxes.
Returns:
:obj:`BaseInstance3DBoxes`: Box object with the same properties \
:obj:`BaseInstance3DBoxes`: Box object with the same properties
as self.
"""
original_type = type(self)
......@@ -363,7 +412,7 @@ class BaseInstance3DBoxes(object):
Args:
boxes1 (:obj:`BaseInstance3DBoxes`): Boxes 1 contain N 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:
torch.Tensor: Calculated iou of boxes.
......@@ -444,14 +493,14 @@ class BaseInstance3DBoxes(object):
def new_box(self, 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.
Args:
data (torch.Tensor | numpy.array | list): Data to be copied.
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``.
"""
new_tensor = self.tensor.new_tensor(data) \
......@@ -459,3 +508,75 @@ class BaseInstance3DBoxes(object):
original_type = type(self)
return original_type(
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.
from enum import IntEnum, unique
import numpy as np
import torch
from enum import IntEnum, unique
from .base_box3d import BaseInstance3DBoxes
from .cam_box3d import CameraInstance3DBoxes
from .depth_box3d import DepthInstance3DBoxes
from .lidar_box3d import LiDARInstance3DBoxes
from .utils import limit_period
@unique
......@@ -61,23 +63,28 @@ class Box3DMode(IntEnum):
DEPTH = 2
@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.
Args:
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.
src (:obj:`Box3DMode`): The src Box mode.
dst (:obj:`Box3DMode`): The target Box mode.
rt_mat (np.ndarray | torch.Tensor): The rotation and translation
matrix between different coordinates. Defaults to None.
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, optional): If `box` is an instance of
:obj:`BaseInstance3DBoxes`, whether or not it has a yaw angle.
Defaults to True.
Returns:
(tuple | list | np.ndarray | torch.Tensor | BaseInstance3DBoxes): \
(tuple | list | np.ndarray | torch.Tensor |
:obj:`BaseInstance3DBoxes`):
The converted box of the same type.
"""
if src == dst:
......@@ -100,32 +107,53 @@ class Box3DMode(IntEnum):
else:
arr = box.clone()
if is_Instance3DBoxes:
with_yaw = box.with_yaw
# convert box from `src` mode to `dst` mode.
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 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)
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:
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)
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:
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)
if with_yaw:
yaw = -yaw
elif src == Box3DMode.CAM and dst == Box3DMode.DEPTH:
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)
if with_yaw:
yaw = -yaw
elif src == Box3DMode.LIDAR and dst == Box3DMode.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)
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:
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)
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:
raise NotImplementedError(
f'Conversion from Box3DMode {src} to {dst} '
......@@ -135,13 +163,17 @@ class Box3DMode(IntEnum):
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)
[arr[..., :3], arr.new_ones(arr.size(0), 1)], dim=-1)
xyz = extended_xyz @ rt_mat.t()
else:
xyz = arr[:, :3] @ rt_mat.t()
xyz = arr[..., :3] @ rt_mat.t()
remains = arr[..., 6:]
arr = torch.cat([xyz[:, :3], xyz_size, remains], dim=-1)
if with_yaw:
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
original_type = type(box)
......@@ -160,7 +192,6 @@ class Box3DMode(IntEnum):
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)
return target_type(arr, box_dim=arr.size(-1), with_yaw=with_yaw)
else:
return arr
......@@ -2,9 +2,9 @@
import numpy as np
import torch
from mmdet3d.core.points import BasePoints
from ...points import BasePoints
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):
......@@ -28,16 +28,14 @@ class CameraInstance3DBoxes(BaseInstance3DBoxes):
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.
A refactor is ongoing to make the three coordinate systems
easier to understand and convert between each other.
Attributes:
tensor (torch.Tensor): Float matrix of N x box_dim.
box_dim (int): Integer indicates the dimension of a box
tensor (torch.Tensor): Float matrix in shape (N, box_dim).
box_dim (int): Integer indicating the dimension of a box
Each row is (x, y, z, x_size, y_size, z_size, yaw, ...).
with_yaw (bool): If True, the value of yaw will be set to 0 as minmax
boxes.
with_yaw (bool): If True, the value of yaw will be set to 0 as
axis-aligned boxes tightly enclosing the original boxes.
"""
YAW_AXIS = 1
def __init__(self,
tensor,
......@@ -76,23 +74,39 @@ class CameraInstance3DBoxes(BaseInstance3DBoxes):
@property
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]
@property
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
return self.bottom_height - self.height
@property
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]
@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
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
gravity_center = torch.zeros_like(bottom_center)
gravity_center[:, [0, 2]] = bottom_center[:, [0, 2]]
......@@ -137,82 +151,66 @@ class CameraInstance3DBoxes(BaseInstance3DBoxes):
corners_norm = corners_norm - dims.new_tensor([0.5, 1, 0.5])
corners = dims.view([-1, 1, 3]) * corners_norm.reshape([1, 8, 3])
# rotate around y axis
corners = rotation_3d_in_axis(corners, self.tensor[:, 6], axis=1)
corners = rotation_3d_in_axis(
corners, self.tensor[:, 6], axis=self.YAW_AXIS)
corners += self.tensor[:, :3].view(-1, 1, 3)
return corners
@property
def bev(self):
"""torch.Tensor: A n x 5 tensor of 2D BEV box of each box
with rotation in XYWHR format."""
return self.tensor[:, [0, 2, 3, 5, 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 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
"""torch.Tensor: 2D BEV box of each box with rotation
in XYWHR format, in shape (N, 5)."""
bev = self.tensor[:, [0, 2, 3, 5, 6]].clone()
# positive direction of the gravity axis
# in cam coord system points to the earth
# so the bev yaw angle needs to be reversed
bev[:, -1] = -bev[:, -1]
return bev
def rotate(self, angle, points=None):
"""Rotate boxes with points (optional) with the given angle or \
rotation matrix.
"""Rotate boxes with points (optional) with the given angle or rotation
matrix.
Args:
angle (float | torch.Tensor | np.ndarray):
Rotation angle or rotation matrix.
points (torch.Tensor, numpy.ndarray, :obj:`BasePoints`, optional):
points (torch.Tensor | np.ndarray | :obj:`BasePoints`, optional):
Points to rotate. Defaults to None.
Returns:
tuple or None: When ``points`` is None, the function returns \
None, otherwise it returns the rotated points and the \
tuple or None: When ``points`` is None, the function returns
None, otherwise it returns the rotated points and the
rotation matrix ``rot_mat_T``.
"""
if not isinstance(angle, torch.Tensor):
angle = self.tensor.new_tensor(angle)
assert angle.shape == torch.Size([3, 3]) or angle.numel() == 1, \
f'invalid rotation angle shape {angle.shape}'
if angle.numel() == 1:
rot_sin = torch.sin(angle)
rot_cos = torch.cos(angle)
rot_mat_T = self.tensor.new_tensor([[rot_cos, 0, -rot_sin],
[0, 1, 0],
[rot_sin, 0, rot_cos]])
self.tensor[:, 0:3], rot_mat_T = rotation_3d_in_axis(
self.tensor[:, 0:3],
angle,
axis=self.YAW_AXIS,
return_mat=True)
else:
rot_mat_T = angle
rot_sin = rot_mat_T[2, 0]
rot_cos = rot_mat_T[0, 0]
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
if points is not None:
if isinstance(points, torch.Tensor):
points[:, :3] = points[:, :3] @ rot_mat_T
elif isinstance(points, np.ndarray):
rot_mat_T = rot_mat_T.numpy()
rot_mat_T = rot_mat_T.cpu().numpy()
points[:, :3] = np.dot(points[:, :3], rot_mat_T)
elif isinstance(points, BasePoints):
# clockwise
points.rotate(-angle)
points.rotate(rot_mat_T)
else:
raise ValueError
return points, rot_mat_T
......@@ -224,7 +222,7 @@ class CameraInstance3DBoxes(BaseInstance3DBoxes):
Args:
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.
Returns:
......@@ -251,28 +249,6 @@ class CameraInstance3DBoxes(BaseInstance3DBoxes):
points.flip(bev_direction)
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
def height_overlaps(cls, boxes1, boxes2, mode='iou'):
"""Calculate height overlaps of two boxes.
......@@ -296,8 +272,8 @@ class CameraInstance3DBoxes(BaseInstance3DBoxes):
boxes2_top_height = boxes2.top_height.view(1, -1)
boxes2_bottom_height = boxes2.bottom_height.view(1, -1)
# In camera coordinate system
# from up to down is the positive direction
# positive direction of the gravity axis
# in cam coord system points to the earth
heighest_of_bottom = torch.min(boxes1_bottom_height,
boxes2_bottom_height)
lowest_of_top = torch.max(boxes1_top_height, boxes2_top_height)
......@@ -309,16 +285,70 @@ class CameraInstance3DBoxes(BaseInstance3DBoxes):
Args:
dst (:obj:`Box3DMode`): The target Box mode.
rt_mat (np.ndarray | torch.Tensor): The rotation and translation
matrix between different coordinates. Defaults to None.
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.
Returns:
:obj:`BaseInstance3DBoxes`: \
:obj:`BaseInstance3DBoxes`:
The converted box of the same type in the ``dst`` mode.
"""
from .box_3d_mode import Box3DMode
return Box3DMode.convert(
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.
from enum import IntEnum, unique
import numpy as np
import torch
from enum import IntEnum, unique
from mmdet3d.core.points import (BasePoints, CameraPoints, DepthPoints,
LiDARPoints)
from ...points import BasePoints, CameraPoints, DepthPoints, LiDARPoints
from .base_box3d import BaseInstance3DBoxes
from .cam_box3d import CameraInstance3DBoxes
from .depth_box3d import DepthInstance3DBoxes
from .lidar_box3d import LiDARInstance3DBoxes
from .box_3d_mode import Box3DMode
@unique
......@@ -64,119 +62,75 @@ class Coord3DMode(IntEnum):
DEPTH = 2
@staticmethod
def convert(input, src, dst, rt_mat=None):
"""Convert boxes or points from `src` mode to `dst` mode."""
def convert(input, src, dst, rt_mat=None, with_yaw=True, is_point=True):
"""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):
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):
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:
raise NotImplementedError
@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.
Args:
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.
src (:obj:`CoordMode`): The src Box mode.
dst (:obj:`CoordMode`): The target Box mode.
rt_mat (np.ndarray | torch.Tensor): The rotation and translation
matrix between different coordinates. Defaults to None.
src (:obj:`Box3DMode`): The src Box mode.
dst (:obj:`Box3DMode`): The target Box 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.
Returns:
(tuple | list | np.ndarray | torch.Tensor | BaseInstance3DBoxes): \
(tuple | list | np.ndarray | torch.Tensor |
:obj:`BaseInstance3DBoxes`):
The converted box of the same type.
"""
if src == dst:
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
return Box3DMode.convert(box, src, dst, rt_mat=rt_mat)
@staticmethod
def convert_point(point, src, dst, rt_mat=None):
......@@ -184,18 +138,19 @@ class Coord3DMode(IntEnum):
Args:
point (tuple | list | np.ndarray |
torch.Tensor | BasePoints):
torch.Tensor | :obj:`BasePoints`):
Can be a k-tuple, k-list or an Nxk array/tensor.
src (:obj:`CoordMode`): The src Point mode.
dst (:obj:`CoordMode`): The target Point mode.
rt_mat (np.ndarray | torch.Tensor): The rotation and translation
matrix between different coordinates. Defaults to None.
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.
Returns:
(tuple | list | np.ndarray | torch.Tensor | BasePoints): \
(tuple | list | np.ndarray | torch.Tensor | :obj:`BasePoints`):
The converted point of the same type.
"""
if src == dst:
......@@ -219,8 +174,6 @@ class Coord3DMode(IntEnum):
arr = point.clone()
# 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 rt_mat is None:
rt_mat = arr.new_tensor([[0, -1, 0], [0, 0, -1], [1, 0, 0]])
......@@ -248,13 +201,13 @@ class Coord3DMode(IntEnum):
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)
[arr[..., :3], arr.new_ones(arr.size(0), 1)], dim=-1)
xyz = extended_xyz @ rt_mat.t()
else:
xyz = arr[:, :3] @ rt_mat.t()
xyz = arr[..., :3] @ rt_mat.t()
remains = arr[:, 3:]
arr = torch.cat([xyz[:, :3], remains], dim=-1)
remains = arr[..., 3:]
arr = torch.cat([xyz[..., :3], remains], dim=-1)
# convert arr to the original type
original_type = type(point)
......
......@@ -3,9 +3,8 @@ import numpy as np
import torch
from mmdet3d.core.points import BasePoints
from mmdet3d.ops import points_in_boxes_batch
from .base_box3d import BaseInstance3DBoxes
from .utils import limit_period, rotation_3d_in_axis
from .utils import rotation_3d_in_axis
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
boxes.
"""
YAW_AXIS = 2
@property
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
gravity_center = torch.zeros_like(bottom_center)
gravity_center[:, :2] = bottom_center[:, :2]
......@@ -85,73 +85,50 @@ class DepthInstance3DBoxes(BaseInstance3DBoxes):
corners = dims.view([-1, 1, 3]) * corners_norm.reshape([1, 8, 3])
# 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)
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):
"""Rotate boxes with points (optional) with the given angle or \
rotation matrix.
"""Rotate boxes with points (optional) with the given angle or rotation
matrix.
Args:
angle (float | torch.Tensor | np.ndarray):
Rotation angle or rotation matrix.
points (torch.Tensor, numpy.ndarray, :obj:`BasePoints`, optional):
points (torch.Tensor | np.ndarray | :obj:`BasePoints`, optional):
Points to rotate. Defaults to None.
Returns:
tuple or None: When ``points`` is None, the function returns \
None, otherwise it returns the rotated points and the \
tuple or None: When ``points`` is None, the function returns
None, otherwise it returns the rotated points and the
rotation matrix ``rot_mat_T``.
"""
if not isinstance(angle, torch.Tensor):
angle = self.tensor.new_tensor(angle)
assert angle.shape == torch.Size([3, 3]) or angle.numel() == 1, \
f'invalid rotation angle shape {angle.shape}'
if angle.numel() == 1:
rot_sin = torch.sin(angle)
rot_cos = torch.cos(angle)
rot_mat_T = self.tensor.new_tensor([[rot_cos, -rot_sin, 0],
[rot_sin, rot_cos, 0],
[0, 0, 1]]).T
self.tensor[:, 0:3], rot_mat_T = rotation_3d_in_axis(
self.tensor[:, 0:3],
angle,
axis=self.YAW_AXIS,
return_mat=True)
else:
rot_mat_T = angle.T
rot_mat_T = angle
rot_sin = rot_mat_T[0, 1]
rot_cos = rot_mat_T[0, 0]
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:
self.tensor[:, 6] -= angle
self.tensor[:, 6] += angle
else:
# for axis-aligned boxes, we take the new
# enclosing axis-aligned boxes after rotation
corners_rot = self.corners @ rot_mat_T
new_x_size = corners_rot[..., 0].max(
dim=1, keepdim=True)[0] - corners_rot[..., 0].min(
......@@ -165,11 +142,10 @@ class DepthInstance3DBoxes(BaseInstance3DBoxes):
if isinstance(points, torch.Tensor):
points[:, :3] = points[:, :3] @ rot_mat_T
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)
elif isinstance(points, BasePoints):
# anti-clockwise
points.rotate(angle)
points.rotate(rot_mat_T)
else:
raise ValueError
return points, rot_mat_T
......@@ -180,8 +156,9 @@ class DepthInstance3DBoxes(BaseInstance3DBoxes):
In Depth coordinates, it flips x (horizontal) or y (vertical) axis.
Args:
bev_direction (str): Flip direction (horizontal or vertical).
points (torch.Tensor, numpy.ndarray, :obj:`BasePoints`, None):
bev_direction (str, optional): Flip direction
(horizontal or vertical). Defaults to 'horizontal'.
points (torch.Tensor | np.ndarray | :obj:`BasePoints`, optional):
Points to flip. Defaults to None.
Returns:
......@@ -208,75 +185,26 @@ class DepthInstance3DBoxes(BaseInstance3DBoxes):
points.flip(bev_direction)
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):
"""Convert self to ``dst`` mode.
Args:
dst (:obj:`Box3DMode`): The target Box mode.
rt_mat (np.ndarray | torch.Tensor): The rotation and translation
matrix between different coordinates. Defaults to None.
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.
Returns:
:obj:`DepthInstance3DBoxes`: \
:obj:`DepthInstance3DBoxes`:
The converted box of the same type in the ``dst`` mode.
"""
from .box_3d_mode import Box3DMode
return Box3DMode.convert(
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):
"""Enlarge the length, width and height boxes.
......@@ -284,7 +212,7 @@ class DepthInstance3DBoxes(BaseInstance3DBoxes):
extra_width (float | torch.Tensor): Extra width to enlarge the box.
Returns:
:obj:`LiDARInstance3DBoxes`: Enlarged boxes.
:obj:`DepthInstance3DBoxes`: Enlarged boxes.
"""
enlarged_boxes = self.tensor.clone()
enlarged_boxes[:, 3:6] += extra_width * 2
......@@ -331,13 +259,12 @@ class DepthInstance3DBoxes(BaseInstance3DBoxes):
-1, 3)
surface_rot = rot_mat_T.repeat(6, 1, 1)
surface_3d = torch.matmul(
surface_3d.unsqueeze(-2), surface_rot.transpose(2, 1)).squeeze(-2)
surface_3d = torch.matmul(surface_3d.unsqueeze(-2),
surface_rot).squeeze(-2)
surface_center = center.repeat(1, 6, 1).reshape(-1, 3) + surface_3d
line_rot = rot_mat_T.repeat(12, 1, 1)
line_3d = torch.matmul(
line_3d.unsqueeze(-2), line_rot.transpose(2, 1)).squeeze(-2)
line_3d = torch.matmul(line_3d.unsqueeze(-2), line_rot).squeeze(-2)
line_center = center.repeat(1, 12, 1).reshape(-1, 3) + line_3d
return surface_center, line_center
......@@ -3,9 +3,8 @@ import numpy as np
import torch
from mmdet3d.core.points import BasePoints
from mmdet3d.ops.roiaware_pool3d import points_in_boxes_gpu
from .base_box3d import BaseInstance3DBoxes
from .utils import limit_period, rotation_3d_in_axis
from .utils import rotation_3d_in_axis
class LiDARInstance3DBoxes(BaseInstance3DBoxes):
......@@ -15,16 +14,16 @@ class LiDARInstance3DBoxes(BaseInstance3DBoxes):
.. code-block:: none
up z x front (yaw=-0.5*pi)
^ ^
| /
| /
(yaw=-pi) left y <------ 0 -------- (yaw=0)
up z x front (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),
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 negative direction of y to the positive direction of x.
The yaw is 0 at the positive direction of x axis, and increases from
the positive direction of x to the positive direction of y.
A refactor is ongoing to make the three coordinate systems
easier to understand and convert between each other.
......@@ -36,10 +35,11 @@ class LiDARInstance3DBoxes(BaseInstance3DBoxes):
with_yaw (bool): If True, the value of yaw will be set to 0 as minmax
boxes.
"""
YAW_AXIS = 2
@property
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
gravity_center = torch.zeros_like(bottom_center)
gravity_center[:, :2] = bottom_center[:, :2]
......@@ -83,70 +83,45 @@ class LiDARInstance3DBoxes(BaseInstance3DBoxes):
corners = dims.view([-1, 1, 3]) * corners_norm.reshape([1, 8, 3])
# 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)
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):
"""Rotate boxes with points (optional) with the given angle or \
rotation matrix.
"""Rotate boxes with points (optional) with the given angle or rotation
matrix.
Args:
angles (float | torch.Tensor | np.ndarray):
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.
Returns:
tuple or None: When ``points`` is None, the function returns \
None, otherwise it returns the rotated points and the \
tuple or None: When ``points`` is None, the function returns
None, otherwise it returns the rotated points and the
rotation matrix ``rot_mat_T``.
"""
if not isinstance(angle, torch.Tensor):
angle = self.tensor.new_tensor(angle)
assert angle.shape == torch.Size([3, 3]) or angle.numel() == 1, \
f'invalid rotation angle shape {angle.shape}'
if angle.numel() == 1:
rot_sin = torch.sin(angle)
rot_cos = torch.cos(angle)
rot_mat_T = self.tensor.new_tensor([[rot_cos, -rot_sin, 0],
[rot_sin, rot_cos, 0],
[0, 0, 1]])
self.tensor[:, 0:3], rot_mat_T = rotation_3d_in_axis(
self.tensor[:, 0:3],
angle,
axis=self.YAW_AXIS,
return_mat=True)
else:
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]
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
if self.tensor.shape[1] == 9:
......@@ -157,11 +132,10 @@ class LiDARInstance3DBoxes(BaseInstance3DBoxes):
if isinstance(points, torch.Tensor):
points[:, :3] = points[:, :3] @ rot_mat_T
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)
elif isinstance(points, BasePoints):
# clockwise
points.rotate(-angle)
points.rotate(rot_mat_T)
else:
raise ValueError
return points, rot_mat_T
......@@ -173,7 +147,7 @@ class LiDARInstance3DBoxes(BaseInstance3DBoxes):
Args:
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.
Returns:
......@@ -183,11 +157,11 @@ class LiDARInstance3DBoxes(BaseInstance3DBoxes):
if bev_direction == 'horizontal':
self.tensor[:, 1::7] = -self.tensor[:, 1::7]
if self.with_yaw:
self.tensor[:, 6] = -self.tensor[:, 6] + np.pi
self.tensor[:, 6] = -self.tensor[:, 6]
elif bev_direction == 'vertical':
self.tensor[:, 0::7] = -self.tensor[:, 0::7]
if self.with_yaw:
self.tensor[:, 6] = -self.tensor[:, 6]
self.tensor[:, 6] = -self.tensor[:, 6] + np.pi
if points is not None:
assert isinstance(points, (torch.Tensor, np.ndarray, BasePoints))
......@@ -200,40 +174,20 @@ class LiDARInstance3DBoxes(BaseInstance3DBoxes):
points.flip(bev_direction)
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):
"""Convert self to ``dst`` mode.
Args:
dst (:obj:`Box3DMode`): the target Box mode
rt_mat (np.ndarray | torch.Tensor): The rotation and translation
matrix between different coordinates. Defaults to None.
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.
Returns:
:obj:`BaseInstance3DBoxes`: \
:obj:`BaseInstance3DBoxes`:
The converted box of the same type in the ``dst`` mode.
"""
from .box_3d_mode import Box3DMode
......@@ -254,17 +208,3 @@ class LiDARInstance3DBoxes(BaseInstance3DBoxes):
# bottom center z minus extra_width
enlarged_boxes[:, 2] -= extra_width
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.
from logging import warning
import numpy as np
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):
"""Limit the value into a period for periodic function.
Args:
val (torch.Tensor): The value to be converted.
offset (float, optional): Offset to set the value range. \
val (torch.Tensor | np.ndarray): The value to be converted.
offset (float, optional): Offset to set the value range.
Defaults to 0.5.
period ([type], optional): Period of the value. Defaults to np.pi.
Returns:
torch.Tensor: Value in the range of \
(torch.Tensor | np.ndarray): Value in the range of
[-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.
Args:
points (torch.Tensor): Points of shape (N, M, 3).
angles (torch.Tensor): Vector of angles in shape (N,)
points (np.ndarray | torch.Tensor | list | tuple ):
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.
return_mat: Whether or not return the rotation matrix (transposed).
Defaults to False.
clockwise: Whether the rotation is clockwise. Defaults to False.
Raises:
ValueError: when the axis is not in range [0, 1, 2], it will \
ValueError: when the axis is not in range [0, 1, 2], it will
raise value error.
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_cos = torch.cos(angles)
ones = torch.ones_like(rot_cos)
zeros = torch.zeros_like(rot_cos)
if axis == 1:
rot_mat_T = torch.stack([
torch.stack([rot_cos, zeros, -rot_sin]),
torch.stack([zeros, ones, zeros]),
torch.stack([rot_sin, zeros, rot_cos])
])
elif axis == 2 or axis == -1:
rot_mat_T = torch.stack([
torch.stack([rot_cos, -rot_sin, zeros]),
torch.stack([rot_sin, rot_cos, zeros]),
torch.stack([zeros, zeros, ones])
])
elif axis == 0:
if points.shape[-1] == 3:
if axis == 1 or axis == -2:
rot_mat_T = torch.stack([
torch.stack([rot_cos, zeros, -rot_sin]),
torch.stack([zeros, ones, zeros]),
torch.stack([rot_sin, zeros, rot_cos])
])
elif axis == 2 or axis == -1:
rot_mat_T = torch.stack([
torch.stack([rot_cos, rot_sin, zeros]),
torch.stack([-rot_sin, rot_cos, zeros]),
torch.stack([zeros, zeros, ones])
])
elif axis == 0 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([
torch.stack([zeros, rot_cos, -rot_sin]),
torch.stack([zeros, rot_sin, rot_cos]),
torch.stack([ones, zeros, zeros])
torch.stack([rot_cos, rot_sin]),
torch.stack([-rot_sin, rot_cos])
])
if clockwise:
rot_mat_T = rot_mat_T.transpose(0, 1)
if points.shape[0] == 0:
points_new = points
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):
"""Convert a rotated boxes in XYWHR format to XYXYR format.
Args:
boxes_xywhr (torch.Tensor): Rotated boxes in XYWHR format.
boxes_xywhr (torch.Tensor | np.ndarray): Rotated boxes in XYWHR format.
Returns:
torch.Tensor: Converted boxes in XYXYR format.
(torch.Tensor | np.ndarray): Converted boxes in XYXYR format.
"""
boxes = torch.zeros_like(boxes_xywhr)
half_w = boxes_xywhr[:, 2] / 2
half_h = boxes_xywhr[:, 3] / 2
boxes[:, 0] = boxes_xywhr[:, 0] - half_w
boxes[:, 1] = boxes_xywhr[:, 1] - half_h
boxes[:, 2] = boxes_xywhr[:, 0] + half_w
boxes[:, 3] = boxes_xywhr[:, 1] + half_h
boxes[:, 4] = boxes_xywhr[:, 4]
half_w = boxes_xywhr[..., 2] / 2
half_h = boxes_xywhr[..., 3] / 2
boxes[..., 0] = boxes_xywhr[..., 0] - half_w
boxes[..., 1] = boxes_xywhr[..., 1] - half_h
boxes[..., 2] = boxes_xywhr[..., 0] + half_w
boxes[..., 3] = boxes_xywhr[..., 1] + half_h
boxes[..., 4] = boxes_xywhr[..., 4]
return boxes
......@@ -91,6 +146,10 @@ def get_box_type(box_type):
box_type (str): The type of box structure.
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:
tuple: Box type and box mode.
"""
......@@ -113,21 +172,24 @@ def get_box_type(box_type):
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):
"""Project points from camera coordicates to image coordinates.
"""Project points in camera coordinates to image coordinates.
Args:
points_3d (torch.Tensor): Points in shape (N, 3).
proj_mat (torch.Tensor): Transformation matrix between coordinates.
points_3d (torch.Tensor | np.ndarray): Points in shape (N, 3)
proj_mat (torch.Tensor | np.ndarray):
Transformation matrix between coordinates.
with_depth (bool, optional): Whether to keep depth in the output.
Defaults to False.
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'\
f' matrix should be 2 instead of {len(proj_mat.shape)}.'
d1, d2 = proj_mat.shape[:2]
......@@ -140,17 +202,52 @@ def points_cam2img(points_3d, proj_mat, with_depth=False):
proj_mat_expanded[:d1, :d2] = proj_mat
proj_mat = proj_mat_expanded
# previous implementation use new_zeros, new_one yeilds better results
points_4 = torch.cat(
[points_3d, points_3d.new_ones(*points_shape)], dim=-1)
point_2d = torch.matmul(points_4, proj_mat.t())
# previous implementation use new_zeros, new_one yields better results
points_4 = torch.cat([points_3d, points_3d.new_ones(points_shape)], dim=-1)
point_2d = points_4 @ proj_mat.T
point_2d_res = point_2d[..., :2] / point_2d[..., 2:3]
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
@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):
"""This is a post-processing function on the bboxes from Mono-3D task. If
we want to perform projection visualization, we need to:
......@@ -162,9 +259,9 @@ def mono_cam_box2vis(cam_box):
After applying this function, we can project and draw it on 2D images.
Args:
cam_box (:obj:`CameraInstance3DBoxes`): 3D bbox in camera coordinate \
system before conversion. Could be gt bbox loaded from dataset or \
network prediction output.
cam_box (:obj:`CameraInstance3DBoxes`): 3D bbox in camera coordinate
system before conversion. Could be gt bbox loaded from dataset
or network prediction output.
Returns:
:obj:`CameraInstance3DBoxes`: Box after conversion.
......@@ -212,3 +309,27 @@ def get_proj_mat_by_coord_type(img_meta, coord_type):
mapping = {'LIDAR': 'lidar2img', 'DEPTH': 'depth2img', 'CAMERA': 'cam2img'}
assert coord_type in mapping.keys()
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):
corresponding to a batch of images.
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 ...].
"""
rois_list = []
......@@ -51,10 +51,10 @@ def bbox3d2result(bboxes, scores, labels, attrs=None):
"""Convert detection results to a list of numpy arrays.
Args:
bboxes (torch.Tensor): Bounding boxes with shape of (n, 5).
labels (torch.Tensor): Labels with shape of (n, ).
scores (torch.Tensor): Scores with shape of (n, ).
attrs (torch.Tensor, optional): Attributes with shape of (n, ). \
bboxes (torch.Tensor): Bounding boxes with shape (N, 5).
labels (torch.Tensor): Labels with shape (N, ).
scores (torch.Tensor): Scores with shape (N, ).
attrs (torch.Tensor, optional): Attributes with shape (N, ).
Defaults to None.
Returns:
......
......@@ -9,9 +9,9 @@ def average_precision(recalls, precisions, mode='area'):
"""Calculate average precision (for single or multiple scales).
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, ).
precisions (np.ndarray): Precisions with shape of \
precisions (np.ndarray): Precisions with shape of
(num_scales, num_dets) or (num_dets, ).
mode (str): 'area' or '11points', 'area' means calculating the area
under precision-recall curve, '11points' means calculating
......@@ -58,13 +58,13 @@ def eval_det_cls(pred, gt, iou_thr=None):
single class.
Args:
pred (dict): Predictions mapping from image id to bounding boxes \
pred (dict): Predictions mapping from image id to bounding boxes
and scores.
gt (dict): Ground truths mapping from image id to bounding boxes.
iou_thr (list[float]): A list of iou thresholds.
Return:
tuple (np.ndarray, np.ndarray, float): Recalls, precisions and \
tuple (np.ndarray, np.ndarray, float): Recalls, precisions and
average precision.
"""
......@@ -170,10 +170,9 @@ def eval_map_recall(pred, gt, ovthresh=None):
Args:
pred (dict): Information of detection results,
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.
ovthresh (list[float]): iou threshold.
Default: None.
ovthresh (list[float], optional): iou threshold. Default: None.
Return:
tuple[dict]: dict results of recall, AP, and precision for all classes.
......@@ -218,12 +217,12 @@ def indoor_eval(gt_annos,
includes the following keys
- labels_3d (torch.Tensor): Labels of boxes.
- boxes_3d (:obj:`BaseInstance3DBoxes`): \
- boxes_3d (:obj:`BaseInstance3DBoxes`):
3D bounding boxes in Depth coordinate.
- scores_3d (torch.Tensor): Scores of boxes.
metric (list[float]): IoU thresholds for computing average precisions.
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.
Return:
......
# Copyright (c) OpenMMLab. All rights reserved.
import gc
import io as sysio
import numba
import numpy as np
......@@ -569,13 +570,20 @@ def eval_class(gt_annos,
return ret_dict
def get_mAP(prec):
def get_mAP11(prec):
sums = 0
for i in range(0, prec.shape[-1], 4):
sums = sums + prec[..., i]
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):
if sstream is None:
sstream = sysio.StringIO()
......@@ -592,8 +600,10 @@ def do_eval(gt_annos,
eval_types=['bbox', 'bev', '3d']):
# min_overlaps: [num_minoverlap, metric, num_class]
difficultys = [0, 1, 2]
mAP_bbox = None
mAP_aos = None
mAP11_bbox = None
mAP11_aos = None
mAP40_bbox = None
mAP40_aos = None
if 'bbox' in eval_types:
ret = eval_class(
gt_annos,
......@@ -604,22 +614,29 @@ def do_eval(gt_annos,
min_overlaps,
compute_aos=('aos' in eval_types))
# 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:
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:
ret = eval_class(gt_annos, dt_annos, current_classes, difficultys, 1,
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:
ret = eval_class(gt_annos, dt_annos, current_classes, difficultys, 2,
min_overlaps)
mAP_3d = get_mAP(ret['precision'])
return mAP_bbox, mAP_bev, mAP_3d, mAP_aos
mAP11_3d = get_mAP11(ret['precision'])
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,
......@@ -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 j in range(overlap_ranges.shape[2]):
min_overlaps[:, i, j] = np.linspace(*overlap_ranges[:, i, j])
mAP_bbox, mAP_bev, mAP_3d, mAP_aos = do_eval(gt_annos, dt_annos,
current_classes, min_overlaps,
compute_aos)
mAP_bbox, mAP_bev, mAP_3d, mAP_aos, _, _, \
_, _ = do_eval(gt_annos, dt_annos,
current_classes, min_overlaps,
compute_aos)
# ret: [num_class, num_diff, num_minoverlap]
mAP_bbox = mAP_bbox.mean(-1)
mAP_bev = mAP_bev.mean(-1)
......@@ -703,33 +721,109 @@ def kitti_eval(gt_annos,
if compute_aos:
eval_types.append('aos')
mAPbbox, mAPbev, mAP3d, mAPaos = do_eval(gt_annos, dt_annos,
current_classes, min_overlaps,
eval_types)
mAP11_bbox, mAP11_bev, mAP11_3d, mAP11_aos, mAP40_bbox, mAP40_bev, \
mAP40_3d, mAP40_aos = do_eval(gt_annos, dt_annos,
current_classes, min_overlaps,
eval_types)
ret_dict = {}
difficulty = ['easy', 'moderate', 'hard']
# calculate AP11
result += '\n----------- AP11 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 += ('{} AP@{:.2f}, {:.2f}, {:.2f}:\n'.format(
result += ('{} AP11@{:.2f}, {:.2f}, {:.2f}:\n'.format(
curcls_name, *min_overlaps[i, :, j]))
if mAPbbox is not None:
result += 'bbox AP:{:.4f}, {:.4f}, {:.4f}\n'.format(
*mAPbbox[j, :, i])
if mAPbev is not None:
result += 'bev AP:{:.4f}, {:.4f}, {:.4f}\n'.format(
*mAPbev[j, :, i])
if mAP3d is not None:
result += '3d AP:{:.4f}, {:.4f}, {:.4f}\n'.format(
*mAP3d[j, :, i])
if mAP11_bbox is not None:
result += 'bbox AP11:{:.4f}, {:.4f}, {:.4f}\n'.format(
*mAP11_bbox[j, :, i])
if mAP11_bev is not None:
result += 'bev AP11:{:.4f}, {:.4f}, {:.4f}\n'.format(
*mAP11_bev[j, :, i])
if mAP11_3d is not None:
result += '3d AP11:{:.4f}, {:.4f}, {:.4f}\n'.format(
*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:
result += 'aos AP:{:.2f}, {:.2f}, {:.2f}\n'.format(
*mAPaos[j, :, i])
result += 'aos AP40:{:.2f}, {:.2f}, {:.2f}\n'.format(
*mAP40_aos[j, :, i])
# prepare results for logger
for idx in range(3):
......@@ -738,39 +832,48 @@ def kitti_eval(gt_annos,
else:
postfix = f'{difficulty[idx]}_loose'
prefix = f'KITTI/{curcls_name}'
if mAP3d is not None:
ret_dict[f'{prefix}_3D_{postfix}'] = mAP3d[j, idx, i]
if mAPbev is not None:
ret_dict[f'{prefix}_BEV_{postfix}'] = mAPbev[j, idx, i]
if mAPbbox is not None:
ret_dict[f'{prefix}_2D_{postfix}'] = mAPbbox[j, idx, i]
# calculate mAP over all classes if there are multiple classes
if mAP40_3d is not None:
ret_dict[f'{prefix}_3D_AP40_{postfix}'] =\
mAP40_3d[j, idx, i]
if mAP40_bev is not None:
ret_dict[f'{prefix}_BEV_AP40_{postfix}'] =\
mAP40_bev[j, idx, i]
if mAP40_bbox is not None:
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:
# prepare results for print
result += ('\nOverall AP@{}, {}, {}:\n'.format(*difficulty))
if mAPbbox is not None:
mAPbbox = mAPbbox.mean(axis=0)
result += 'bbox AP:{:.4f}, {:.4f}, {:.4f}\n'.format(*mAPbbox[:, 0])
if mAPbev is not None:
mAPbev = mAPbev.mean(axis=0)
result += 'bev AP:{:.4f}, {:.4f}, {:.4f}\n'.format(*mAPbev[:, 0])
if mAP3d is not None:
mAP3d = mAP3d.mean(axis=0)
result += '3d AP:{:.4f}, {:.4f}, {:.4f}\n'.format(*mAP3d[:, 0])
result += ('\nOverall AP40@{}, {}, {}:\n'.format(*difficulty))
if mAP40_bbox is not None:
mAP40_bbox = mAP40_bbox.mean(axis=0)
result += 'bbox AP40:{:.4f}, {:.4f}, {:.4f}\n'.format(
*mAP40_bbox[:, 0])
if mAP40_bev is not None:
mAP40_bev = mAP40_bev.mean(axis=0)
result += 'bev AP40:{:.4f}, {:.4f}, {:.4f}\n'.format(
*mAP40_bev[:, 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:
mAPaos = mAPaos.mean(axis=0)
result += 'aos AP:{:.2f}, {:.2f}, {:.2f}\n'.format(*mAPaos[:, 0])
mAP40_aos = mAP40_aos.mean(axis=0)
result += 'aos AP40:{:.2f}, {:.2f}, {:.2f}\n'.format(
*mAP40_aos[:, 0])
# prepare results for logger
for idx in range(3):
postfix = f'{difficulty[idx]}'
if mAP3d is not None:
ret_dict[f'KITTI/Overall_3D_{postfix}'] = mAP3d[idx, 0]
if mAPbev is not None:
ret_dict[f'KITTI/Overall_BEV_{postfix}'] = mAPbev[idx, 0]
if mAPbbox is not None:
ret_dict[f'KITTI/Overall_2D_{postfix}'] = mAPbbox[idx, 0]
if mAP40_3d is not None:
ret_dict[f'KITTI/Overall_3D_AP40_{postfix}'] = mAP40_3d[idx, 0]
if mAP40_bev is not None:
ret_dict[f'KITTI/Overall_BEV_AP40_{postfix}'] =\
mAP40_bev[idx, 0]
if mAP40_bbox is not None:
ret_dict[f'KITTI/Overall_2D_AP40_{postfix}'] =\
mAP40_bbox[idx, 0]
return result, ret_dict
......
......@@ -5,6 +5,7 @@
# Author: yanyan, scrin@foxmail.com
#####################
import math
import numba
import numpy as np
from numba import cuda
......@@ -15,13 +16,13 @@ def div_up(m, n):
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):
return ((a[0] - c[0]) * (b[1] - c[1]) - (a[1] - c[1]) *
(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):
area_val = 0.0
for i in range(num_of_inter - 2):
......@@ -31,7 +32,7 @@ def area(int_pts, num_of_inter):
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):
if num_of_inter > 0:
center = cuda.local.array((2, ), dtype=numba.float32)
......@@ -71,10 +72,7 @@ def sort_vertex_in_convex_polygon(int_pts, num_of_inter):
int_pts[j * 2 + 1] = ty
@cuda.jit(
'(float32[:], float32[:], int32, int32, float32[:])',
device=True,
inline=True)
@cuda.jit(device=True, inline=True)
def line_segment_intersection(pts1, pts2, i, j, temp_pts):
A = 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):
return False
@cuda.jit(
'(float32[:], float32[:], int32, int32, float32[:])',
device=True,
inline=True)
@cuda.jit(device=True, inline=True)
def line_segment_intersection_v1(pts1, pts2, i, j, temp_pts):
a = 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):
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):
ab0 = corners[2] - corners[0]
ab1 = corners[3] - corners[1]
......@@ -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
@cuda.jit('(float32[:], float32[:], float32[:])', device=True, inline=True)
@cuda.jit(device=True, inline=True)
def quadrilateral_intersection(pts1, pts2, int_pts):
num_of_inter = 0
for i in range(4):
......@@ -202,7 +197,7 @@ def quadrilateral_intersection(pts1, pts2, int_pts):
return num_of_inter
@cuda.jit('(float32[:], float32[:])', device=True, inline=True)
@cuda.jit(device=True, inline=True)
def rbbox_to_corners(corners, rbbox):
# generate clockwise corners and rotate it clockwise
angle = rbbox[4]
......@@ -228,7 +223,7 @@ def rbbox_to_corners(corners, rbbox):
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):
"""Compute intersection of two rotated boxes.
......@@ -254,7 +249,7 @@ def inter(rbbox1, rbbox2):
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):
"""Compute rotated iou on device.
......@@ -291,7 +286,8 @@ def rotate_iou_kernel_eval(N,
dev_query_boxes,
dev_iou,
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:
N (int): The number of boxes.
......@@ -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](
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:
boxes (torch.Tensor): rbboxes. format: centers, dims,
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.
criterion (int, optional): Indicate different type of iou.
-1 indicate `area_inter / (area1 + area2 - area_inter)`,
......
# Copyright (c) OpenMMLab. All rights reserved.
from os import path as osp
import mmcv
import numpy as np
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,
wrap_in_box)
from mmcv.utils import print_log
from os import path as osp
from terminaltables import AsciiTable
......@@ -18,7 +19,7 @@ def load_lyft_gts(lyft, data_root, eval_split, logger=None):
lyft (:obj:`LyftDataset`): Lyft class in the sdk.
data_root (str): Root of data for reading splits.
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.
Returns:
......@@ -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.
eval_set (str): Name of the split for evaluation.
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.
Returns:
......@@ -202,9 +203,9 @@ def get_single_class_aps(gt, predictions, iou_thresholds):
Args:
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.
iou_thresholds (list[float]): IOU thresholds used to calculate \
iou_thresholds (list[float]): IOU thresholds used to calculate
TP / FN
Returns:
......
......@@ -77,7 +77,7 @@ def seg_eval(gt_labels, seg_preds, label2cat, ignore_index, logger=None):
seg_preds (list[torch.Tensor]): Predictions.
label2cat (dict): Map from label to category name.
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.
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