"tools/imglab/vscode:/vscode.git/clone" did not exist on "ba72c2f95c759cc95b8db9f75ec8a5f7677c3258"
Commit d7ade147 authored by zhangwenwei's avatar zhangwenwei
Browse files

Merge branch 'add-flop-counter' into 'master'

update docstrings in core

See merge request open-mmlab/mmdet.3d!103
parents 398f541e 9732a488
...@@ -28,7 +28,7 @@ linting: ...@@ -28,7 +28,7 @@ linting:
script: script:
- echo "Start building..." - echo "Start building..."
- pip install -q "git+https://github.com/open-mmlab/cocoapi.git#subdirectory=pycocotools" - pip install -q "git+https://github.com/open-mmlab/cocoapi.git#subdirectory=pycocotools"
- pip install -q git+https://github.com/open-mmlab/mmcv.git - pip install -q git+https://github.com/open-mmlab/mmcv.git@v0.6.2
- pip install -q git+https://github.com/open-mmlab/mmdetection.git - pip install -q git+https://github.com/open-mmlab/mmdetection.git
- python -c "import mmdet; print(mmdet.__version__)" - python -c "import mmdet; print(mmdet.__version__)"
- pip install -e .[all] - pip install -e .[all]
......
...@@ -409,31 +409,6 @@ average iter time: 1.1959 s/iter ...@@ -409,31 +409,6 @@ average iter time: 1.1959 s/iter
``` ```
### Get the FLOPs and params (experimental)
We provide a script adapted from [flops-counter.pytorch](https://github.com/sovrasov/flops-counter.pytorch) to compute the FLOPs and params of a given model.
```shell
python tools/get_flops.py ${CONFIG_FILE} [--shape ${INPUT_SHAPE}]
```
You will get the result like this.
```
==============================
Input shape: (3, 1280, 800)
Flops: 239.32 GMac
Params: 37.74 M
==============================
```
**Note**: This tool is still experimental and we do not guarantee that the number is correct. You may well use the result for simple comparisons, but double check it before you adopt it in technical reports or papers.
(1) FLOPs are related to the input shape while parameters are not. The default input shape is (1, 3, 1280, 800).
(2) Some operators are not counted into FLOPs like GN and custom operators.
You can add support for new operators by modifying [`mmdet/utils/flops_counter.py`](https://github.com/open-mmlab/mmdetection/blob/master/mmdet/utils/flops_counter.py).
(3) The FLOPs of two-stage detectors is dependent on the number of proposals.
### Publish a model ### Publish a model
Before you upload a model to AWS, you may want to Before you upload a model to AWS, you may want to
......
...@@ -69,12 +69,14 @@ class Anchor3DRangeGenerator(object): ...@@ -69,12 +69,14 @@ class Anchor3DRangeGenerator(object):
@property @property
def num_base_anchors(self): def num_base_anchors(self):
"""list[int]: total number of base anchors in a feature grid"""
num_rot = len(self.rotations) num_rot = len(self.rotations)
num_size = torch.tensor(self.sizes).reshape(-1, 3).size(0) num_size = torch.tensor(self.sizes).reshape(-1, 3).size(0)
return num_rot * num_size return num_rot * num_size
@property @property
def num_levels(self): def num_levels(self):
"""int: number of feature levels that the generator will be applied"""
return len(self.scales) return len(self.scales)
def grid_anchors(self, featmap_sizes, device='cuda'): def grid_anchors(self, featmap_sizes, device='cuda'):
...@@ -103,6 +105,20 @@ class Anchor3DRangeGenerator(object): ...@@ -103,6 +105,20 @@ class Anchor3DRangeGenerator(object):
return multi_level_anchors return multi_level_anchors
def single_level_grid_anchors(self, featmap_size, scale, device='cuda'): def single_level_grid_anchors(self, featmap_size, scale, device='cuda'):
"""Generate grid anchors of a single level feature map.
Note:
This function is usually called by method ``self.grid_anchors``.
Args:
featmap_size (tuple[int]): Size of the feature map.
scale (float): Scale factor of the anchors in the current level.
device (str, optional): Device the tensor will be put on.
Defaults to 'cuda'.
Returns:
torch.Tensor: Anchors in the overall feature map.
"""
# We reimplement the anchor generator using torch in cuda # We reimplement the anchor generator using torch in cuda
# torch: 0.6975 s for 1000 times # torch: 0.6975 s for 1000 times
# numpy: 4.3345 s for 1000 times # numpy: 4.3345 s for 1000 times
...@@ -139,11 +155,21 @@ class Anchor3DRangeGenerator(object): ...@@ -139,11 +155,21 @@ class Anchor3DRangeGenerator(object):
"""Generate anchors in a single range """Generate anchors in a single range
Args: Args:
feature_size: list [D, H, W](zyx) feature_size (list[float] | tuple[float]): Feature map size. It is
sizes: [N, 3] list of list or array, size of anchors, xyz either a list of a tuple of [D, H, W](in order of z, y, and x).
anchor_range (torch.Tensor | list[float]): Range of anchors with
shape [6]. The order is consistent with that of anchors, i.e.,
(x_min, y_min, z_min, x_max, y_max, z_max).
scale (float | int, optional): The scale factor of anchors.
sizes (list[list] | np.ndarray | torch.Tensor): Anchor size with
shape [N, 3], in order of x, y, z.
rotations (list[float] | np.ndarray | torch.Tensor): Rotations of
anchors in a single feature grid.
device (str): Devices that the anchors will be put on.
Returns: Returns:
anchors: [*feature_size, num_sizes, num_rots, 7] tensor. torch.Tensor: anchors with shape
[*feature_size, num_sizes, num_rots, 7].
""" """
if len(feature_size) == 2: if len(feature_size) == 2:
feature_size = [1, feature_size[0], feature_size[1]] feature_size = [1, feature_size[0], feature_size[1]]
......
...@@ -12,15 +12,10 @@ from .structures import (BaseInstance3DBoxes, Box3DMode, CameraInstance3DBoxes, ...@@ -12,15 +12,10 @@ from .structures import (BaseInstance3DBoxes, Box3DMode, CameraInstance3DBoxes,
xywhr2xyxyr) xywhr2xyxyr)
from .transforms import bbox3d2result, bbox3d2roi, bbox3d_mapping_back from .transforms import bbox3d2result, bbox3d2roi, bbox3d_mapping_back
from .assign_sampling import ( # isort:skip, avoid recursive imports
build_bbox_coder, # temporally settings
assign_and_sample, build_assigner, build_sampler)
__all__ = [ __all__ = [
'BaseSampler', 'AssignResult', 'BaseAssigner', 'MaxIoUAssigner', 'BaseSampler', 'AssignResult', 'BaseAssigner', 'MaxIoUAssigner',
'PseudoSampler', 'RandomSampler', 'InstanceBalancedPosSampler', 'PseudoSampler', 'RandomSampler', 'InstanceBalancedPosSampler',
'IoUBalancedNegSampler', 'CombinedSampler', 'SamplingResult', 'IoUBalancedNegSampler', 'CombinedSampler', 'SamplingResult',
'build_assigner', 'build_sampler', 'assign_and_sample', 'build_bbox_coder',
'DeltaXYZWLHRBBoxCoder', 'BboxOverlapsNearest3D', 'BboxOverlaps3D', 'DeltaXYZWLHRBBoxCoder', 'BboxOverlapsNearest3D', 'BboxOverlaps3D',
'bbox_overlaps_nearest_3d', 'bbox_overlaps_3d', 'Box3DMode', 'bbox_overlaps_nearest_3d', 'bbox_overlaps_3d', 'Box3DMode',
'LiDARInstance3DBoxes', 'CameraInstance3DBoxes', 'bbox3d2roi', 'LiDARInstance3DBoxes', 'CameraInstance3DBoxes', 'bbox3d2roi',
......
import mmcv
from . import assigners, coders, samplers
def build_assigner(cfg, **kwargs):
if isinstance(cfg, assigners.BaseAssigner):
return cfg
elif isinstance(cfg, dict):
return mmcv.runner.obj_from_dict(cfg, assigners, default_args=kwargs)
else:
raise TypeError('Invalid type {} for building a sampler'.format(
type(cfg)))
def build_bbox_coder(cfg, **kwargs):
if isinstance(cfg, coders.DeltaXYZWLHRBBoxCoder):
return cfg
elif isinstance(cfg, dict):
return mmcv.runner.obj_from_dict(cfg, coders, default_args=kwargs)
else:
raise TypeError('Invalid type {} for building a sampler'.format(
type(cfg)))
def build_sampler(cfg, **kwargs):
if isinstance(cfg, samplers.BaseSampler):
return cfg
elif isinstance(cfg, dict):
return mmcv.runner.obj_from_dict(cfg, samplers, default_args=kwargs)
else:
raise TypeError('Invalid type {} for building a sampler'.format(
type(cfg)))
def assign_and_sample(bboxes, gt_bboxes, gt_bboxes_ignore, gt_labels, cfg):
bbox_assigner = build_assigner(cfg.assigner)
bbox_sampler = build_sampler(cfg.sampler)
assign_result = bbox_assigner.assign(bboxes, gt_bboxes, gt_bboxes_ignore,
gt_labels)
sampling_result = bbox_sampler.sample(assign_result, bboxes, gt_bboxes,
gt_labels)
return assign_result, sampling_result
...@@ -545,7 +545,7 @@ def points_in_convex_polygon_jit(points, polygon, clockwise=True): ...@@ -545,7 +545,7 @@ def points_in_convex_polygon_jit(points, polygon, clockwise=True):
Args: Args:
points (np.ndarray): Input points with the shape of [num_points, 2]. points (np.ndarray): Input points with the shape of [num_points, 2].
polygon (dnarray): Input polygon with the shape of polygon (np.ndarray): Input polygon with the shape of
[num_polygon, num_points_of_polygon, 2]. [num_polygon, num_points_of_polygon, 2].
clockwise (bool): Indicate polygon is clockwise. clockwise (bool): Indicate polygon is clockwise.
......
...@@ -6,6 +6,11 @@ from mmdet.core.bbox.builder import BBOX_CODERS ...@@ -6,6 +6,11 @@ from mmdet.core.bbox.builder import BBOX_CODERS
@BBOX_CODERS.register_module() @BBOX_CODERS.register_module()
class DeltaXYZWLHRBBoxCoder(BaseBBoxCoder): class DeltaXYZWLHRBBoxCoder(BaseBBoxCoder):
"""Bbox Coder for 3D boxes
Args:
code_size (int): The dimension of boxes to be encoded.
"""
def __init__(self, code_size=7): def __init__(self, code_size=7):
super(DeltaXYZWLHRBBoxCoder, self).__init__() super(DeltaXYZWLHRBBoxCoder, self).__init__()
......
...@@ -20,10 +20,30 @@ class BboxOverlapsNearest3D(object): ...@@ -20,10 +20,30 @@ class BboxOverlapsNearest3D(object):
self.coordinate = coordinate self.coordinate = coordinate
def __call__(self, bboxes1, bboxes2, mode='iou', is_aligned=False): def __call__(self, bboxes1, bboxes2, mode='iou', is_aligned=False):
"""Calculate nearest 3D IoU
Note:
If ``is_aligned`` is ``False``, then it calculates the ious between
each bbox of bboxes1 and bboxes2, otherwise it calculates the ious
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].
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
``False``, return shape is M.
"""
return bbox_overlaps_nearest_3d(bboxes1, bboxes2, mode, is_aligned, return bbox_overlaps_nearest_3d(bboxes1, bboxes2, mode, is_aligned,
self.coordinate) self.coordinate)
def __repr__(self): def __repr__(self):
"""str: return a string that describes the module"""
repr_str = self.__class__.__name__ repr_str = self.__class__.__name__
repr_str += f'(coordinate={self.coordinate}' repr_str += f'(coordinate={self.coordinate}'
return repr_str return repr_str
...@@ -34,7 +54,8 @@ class BboxOverlaps3D(object): ...@@ -34,7 +54,8 @@ class BboxOverlaps3D(object):
"""3D IoU Calculator """3D IoU Calculator
Args: Args:
coordinate (str): 'camera', 'lidar', or 'depth' coordinate system coordinate (str): The coordinate system, valid options are
'camera', 'lidar', and 'depth'.
""" """
def __init__(self, coordinate): def __init__(self, coordinate):
...@@ -42,9 +63,27 @@ class BboxOverlaps3D(object): ...@@ -42,9 +63,27 @@ class BboxOverlaps3D(object):
self.coordinate = coordinate self.coordinate = coordinate
def __call__(self, bboxes1, bboxes2, mode='iou'): def __call__(self, bboxes1, bboxes2, mode='iou'):
"""Calculate 3D IoU using cuda implementation
Note:
This function calculate the IoU of 3D boxes based on their volumes.
IoU calculator ``:class:BboxOverlaps3D`` uses this function to
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].
mode (str): "iou" (intersection over union) or
iof (intersection over foreground).
Return:
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) return bbox_overlaps_3d(bboxes1, bboxes2, mode, self.coordinate)
def __repr__(self): def __repr__(self):
"""str: return a string that describes the module"""
repr_str = self.__class__.__name__ repr_str = self.__class__.__name__
repr_str += f'(coordinate={self.coordinate}' repr_str += f'(coordinate={self.coordinate}'
return repr_str return repr_str
...@@ -68,8 +107,8 @@ def bbox_overlaps_nearest_3d(bboxes1, ...@@ -68,8 +107,8 @@ def bbox_overlaps_nearest_3d(bboxes1,
aligned pair of bboxes1 and bboxes2. aligned pair of bboxes1 and bboxes2.
Args: Args:
bboxes1 (torch.Tensor): shape (N, 7+N) [x, y, z, h, w, l, ry, v]. bboxes1 (torch.Tensor): shape (N, 7+C) [x, y, z, h, w, l, ry, v].
bboxes2 (torch.Tensor): shape (M, 7+N) [x, y, z, h, w, l, ry, v]. bboxes2 (torch.Tensor): shape (M, 7+C) [x, y, z, h, w, l, ry, v].
mode (str): "iou" (intersection over union) or iof mode (str): "iou" (intersection over union) or iof
(intersection over foreground). (intersection over foreground).
is_aligned (bool): Whether the calculation is aligned is_aligned (bool): Whether the calculation is aligned
...@@ -80,8 +119,7 @@ def bbox_overlaps_nearest_3d(bboxes1, ...@@ -80,8 +119,7 @@ def bbox_overlaps_nearest_3d(bboxes1,
``False``, return shape is M. ``False``, return shape is M.
""" """
assert bboxes1.size(-1) >= 7 assert bboxes1.size(-1) == bboxes2.size(-1) >= 7
assert bboxes2.size(-1) >= 7
box_type, _ = get_box_type(coordinate) box_type, _ = get_box_type(coordinate)
...@@ -108,8 +146,8 @@ def bbox_overlaps_3d(bboxes1, bboxes2, mode='iou', coordinate='camera'): ...@@ -108,8 +146,8 @@ def bbox_overlaps_3d(bboxes1, bboxes2, mode='iou', coordinate='camera'):
calculate the actual IoUs of boxes. calculate the actual IoUs of boxes.
Args: Args:
bboxes1 (torch.Tensor): shape (N, 7) [x, y, z, h, w, l, ry]. bboxes1 (torch.Tensor): shape (N, 7+C) [x, y, z, h, w, l, ry].
bboxes2 (torch.Tensor): shape (M, 7) [x, y, z, h, w, l, ry]. bboxes2 (torch.Tensor): shape (M, 7+C) [x, y, z, h, w, l, ry].
mode (str): "iou" (intersection over union) or mode (str): "iou" (intersection over union) or
iof (intersection over foreground). iof (intersection over foreground).
coordinate (str): 'camera' or 'lidar' coordinate system. coordinate (str): 'camera' or 'lidar' coordinate system.
...@@ -118,7 +156,7 @@ def bbox_overlaps_3d(bboxes1, bboxes2, mode='iou', coordinate='camera'): ...@@ -118,7 +156,7 @@ def bbox_overlaps_3d(bboxes1, bboxes2, mode='iou', coordinate='camera'):
torch.Tensor: Bbox overlaps results of bboxes1 and bboxes2 torch.Tensor: Bbox overlaps results of bboxes1 and bboxes2
with shape (M, N) (aligned mode is not supported currently). with shape (M, N) (aligned mode is not supported currently).
""" """
assert bboxes1.size(-1) == bboxes2.size(-1) == 7 assert bboxes1.size(-1) == bboxes2.size(-1) >= 7
box_type, _ = get_box_type(coordinate) box_type, _ = get_box_type(coordinate)
......
...@@ -54,6 +54,7 @@ class IoUNegPiecewiseSampler(RandomSampler): ...@@ -54,6 +54,7 @@ class IoUNegPiecewiseSampler(RandomSampler):
return self.random_choice(pos_inds, num_expected) return self.random_choice(pos_inds, num_expected)
def _sample_neg(self, assign_result, num_expected, **kwargs): def _sample_neg(self, assign_result, num_expected, **kwargs):
"""Randomly sample some negative samples."""
neg_inds = torch.nonzero(assign_result.gt_inds == 0) neg_inds = torch.nonzero(assign_result.gt_inds == 0)
if neg_inds.numel() != 0: if neg_inds.numel() != 0:
neg_inds = neg_inds.squeeze(1) neg_inds = neg_inds.squeeze(1)
......
...@@ -312,9 +312,11 @@ class BaseInstance3DBoxes(object): ...@@ -312,9 +312,11 @@ class BaseInstance3DBoxes(object):
return original_type(b, box_dim=self.box_dim, with_yaw=self.with_yaw) return original_type(b, box_dim=self.box_dim, with_yaw=self.with_yaw)
def __len__(self): def __len__(self):
"""int: Number of boxes in the current object"""
return self.tensor.shape[0] return self.tensor.shape[0]
def __repr__(self): def __repr__(self):
"""str: Return a strings that describes the object"""
return self.__class__.__name__ + '(\n ' + str(self.tensor) + ')' return self.__class__.__name__ + '(\n ' + str(self.tensor) + ')'
@classmethod @classmethod
...@@ -341,6 +343,15 @@ class BaseInstance3DBoxes(object): ...@@ -341,6 +343,15 @@ class BaseInstance3DBoxes(object):
return cat_boxes return cat_boxes
def to(self, device): def to(self, device):
"""Convert current boxes to a specific device
Args:
device (str | :obj:`torch.device`): The name of the device.
Returns:
:obj:`BaseInstance3DBoxes`: A new boxes object in the
specific device.
"""
original_type = type(self) original_type = type(self)
return original_type( return original_type(
self.tensor.to(device), self.tensor.to(device),
...@@ -359,6 +370,7 @@ class BaseInstance3DBoxes(object): ...@@ -359,6 +370,7 @@ class BaseInstance3DBoxes(object):
@property @property
def device(self): def device(self):
"""str: The device of the boxes are in."""
return self.tensor.device return self.tensor.device
def __iter__(self): def __iter__(self):
......
...@@ -62,6 +62,14 @@ def rotation_3d_in_axis(points, angles, axis=0): ...@@ -62,6 +62,14 @@ def rotation_3d_in_axis(points, angles, axis=0):
def xywhr2xyxyr(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.
Returns:
torch.Tensor: Converted boxes in XYXYR format.
"""
boxes = torch.zeros_like(boxes_xywhr) boxes = torch.zeros_like(boxes_xywhr)
half_w = boxes_xywhr[:, 2] / 2 half_w = boxes_xywhr[:, 2] / 2
half_h = boxes_xywhr[:, 3] / 2 half_h = boxes_xywhr[:, 3] / 2
...@@ -104,6 +112,15 @@ def get_box_type(box_type): ...@@ -104,6 +112,15 @@ def get_box_type(box_type):
def points_cam2img(points_3d, proj_mat): def points_cam2img(points_3d, proj_mat):
"""Project points from camera coordicates to image coordinates
Args:
points_3d (torch.Tensor): Points in shape (N, 3)
proj_mat (torch.Tensor): Transformation matrix between coordinates.
Returns:
torch.Tensor: Points in image coordinates with shape [N, 2].
"""
points_num = list(points_3d.shape)[:-1] points_num = list(points_3d.shape)[:-1]
points_shape = np.concatenate([points_num, [1]], axis=0).tolist() points_shape = np.concatenate([points_num, [1]], axis=0).tolist()
# previous implementation use new_zeros, new_one yeilds better results # previous implementation use new_zeros, new_one yeilds better results
......
...@@ -10,6 +10,25 @@ def box3d_multiclass_nms(mlvl_bboxes, ...@@ -10,6 +10,25 @@ def box3d_multiclass_nms(mlvl_bboxes,
max_num, max_num,
cfg, cfg,
mlvl_dir_scores=None): mlvl_dir_scores=None):
"""Multi-class nms for 3D boxes
Args:
mlvl_bboxes (torch.Tensor): Multi-level boxes with shape (N, M).
M is the dimensions of boxes.
mlvl_bboxes_for_nms (torch.Tensor): Multi-level boxes with shape
(N, 4), N is the number of boxes.
mlvl_scores (torch.Tensor): Multi-level boxes with shape
(N, ), N is the number of boxes.
score_thr (float): Score thredhold to filter boxes with low
confidence.
max_num (int): Maximum number of boxes will be kept.
cfg (dict): Config dict of NMS.
mlvl_dir_scores (torch.Tensor, optional): Multi-level scores
of direction classifier. Defaults to None.
Returns:
tuple: Return (bboxes, scores, labels, dir_scores).
"""
# do multi class nms # do multi class nms
# the fg class id range: [0, num_classes-1] # the fg class id range: [0, num_classes-1]
num_classes = mlvl_scores.shape[1] - 1 num_classes = mlvl_scores.shape[1] - 1
......
...@@ -6,6 +6,12 @@ import trimesh ...@@ -6,6 +6,12 @@ import trimesh
def _write_ply(points, out_filename): def _write_ply(points, out_filename):
"""Write points into ply format for meshlab visualization
Args:
points (np.ndarray): Points in shape (N, dim).
out_filename (str): Filename to be saved.
"""
N = points.shape[0] N = points.shape[0]
fout = open(out_filename, 'w') fout = open(out_filename, 'w')
for i in range(N): for i in range(N):
...@@ -64,6 +70,15 @@ def _write_oriented_bbox(scene_bbox, out_filename): ...@@ -64,6 +70,15 @@ def _write_oriented_bbox(scene_bbox, out_filename):
def show_result(points, gt_bboxes, pred_bboxes, out_dir, filename): def show_result(points, gt_bboxes, pred_bboxes, out_dir, filename):
"""Convert results into format that is directly readable for meshlab.
Args:
points (np.ndarray): Points.
gt_bboxes (np.ndarray): Ground truth boxes.
pred_bboxes (np.ndarray): Predicted boxes.
out_dir (str): Path of output directory
filename (str): Filename of the current frame.
"""
mmcv.mkdir_or_exist(out_dir) mmcv.mkdir_or_exist(out_dir)
if gt_bboxes is not None: if gt_bboxes is not None:
......
...@@ -4,6 +4,7 @@ from . import voxel_generator ...@@ -4,6 +4,7 @@ from . import voxel_generator
def build_voxel_generator(cfg, **kwargs): def build_voxel_generator(cfg, **kwargs):
"""Builder of voxel generator"""
if isinstance(cfg, voxel_generator.VoxelGenerator): if isinstance(cfg, voxel_generator.VoxelGenerator):
return cfg return cfg
elif isinstance(cfg, dict): elif isinstance(cfg, dict):
......
...@@ -3,21 +3,22 @@ import numpy as np ...@@ -3,21 +3,22 @@ import numpy as np
class VoxelGenerator(object): class VoxelGenerator(object):
"""Voxel generator in numpy implementation""" """Voxel generator in numpy implementation
Args:
voxel_size (list[float]): Size of a single voxel
point_cloud_range (list[float]): Range of points
max_num_points (int): Maximum number of points in a single voxel
max_voxels (int, optional): Maximum number of voxels.
Defaults to 20000.
"""
def __init__(self, def __init__(self,
voxel_size, voxel_size,
point_cloud_range, point_cloud_range,
max_num_points, max_num_points,
max_voxels=20000): max_voxels=20000):
"""
Args:
voxel_size (list[float]): Size of a single voxel
point_cloud_range (list[float]): Range of points
max_num_points (int): Maximum number of points in a single voxel
max_voxels (int, optional): Maximum number of voxels.
Defaults to 20000.
"""
point_cloud_range = np.array(point_cloud_range, dtype=np.float32) point_cloud_range = np.array(point_cloud_range, dtype=np.float32)
# [0, -40, -3, 70.4, 40, 1] # [0, -40, -3, 70.4, 40, 1]
voxel_size = np.array(voxel_size, dtype=np.float32) voxel_size = np.array(voxel_size, dtype=np.float32)
...@@ -32,24 +33,29 @@ class VoxelGenerator(object): ...@@ -32,24 +33,29 @@ class VoxelGenerator(object):
self._grid_size = grid_size self._grid_size = grid_size
def generate(self, points): def generate(self, points):
"""Generate voxels given points"""
return points_to_voxel(points, self._voxel_size, return points_to_voxel(points, self._voxel_size,
self._point_cloud_range, self._max_num_points, self._point_cloud_range, self._max_num_points,
True, self._max_voxels) True, self._max_voxels)
@property @property
def voxel_size(self): def voxel_size(self):
"""list[float]: size of a single voxel"""
return self._voxel_size return self._voxel_size
@property @property
def max_num_points_per_voxel(self): def max_num_points_per_voxel(self):
"""int: maximum number of points per voxel"""
return self._max_num_points return self._max_num_points
@property @property
def point_cloud_range(self): def point_cloud_range(self):
"""list[float]: range of point cloud"""
return self._point_cloud_range return self._point_cloud_range
@property @property
def grid_size(self): def grid_size(self):
"""np.ndarray: The size of grids"""
return self._grid_size return self._grid_size
...@@ -59,15 +65,13 @@ def points_to_voxel(points, ...@@ -59,15 +65,13 @@ def points_to_voxel(points,
max_points=35, max_points=35,
reverse_index=True, reverse_index=True,
max_voxels=20000): max_voxels=20000):
"""convert kitti points(N, >=3) to voxels. This version calculate """convert kitti points(N, >=3) to voxels.
everything in one loop. now it takes only 4.2ms(complete point cloud)
with jit and 3.2ghz cpu.(don't calculate other features)
Args: Args:
points (np.ndarray): [N, ndim]. points[:, :3] contain xyz points and points (np.ndarray): [N, ndim]. points[:, :3] contain xyz points and
points[:, 3:] contain other information such as reflectivity. points[:, 3:] contain other information such as reflectivity.
voxel_size (list, tuple, np.ndarray): [3] xyz, indicate voxel size voxel_size (list, tuple, np.ndarray): [3] xyz, indicate voxel size
coors_range: [6] list/tuple or array, float. indicate voxel range. coors_range (list[float | tuple[float] | ndarray]): Voxel range.
format: xyzxyz, minmax format: xyzxyz, minmax
max_points (int): Indicate maximum points contained in a voxel. max_points (int): Indicate maximum points contained in a voxel.
reverse_index (bool): Whether return reversed coordinates. reverse_index (bool): Whether return reversed coordinates.
...@@ -126,6 +130,31 @@ def _points_to_voxel_reverse_kernel(points, ...@@ -126,6 +130,31 @@ def _points_to_voxel_reverse_kernel(points,
coors, coors,
max_points=35, max_points=35,
max_voxels=20000): max_voxels=20000):
"""convert kitti points(N, >=3) to voxels.
Args:
points (np.ndarray): [N, ndim]. points[:, :3] contain xyz points and
points[:, 3:] contain other information such as reflectivity.
voxel_size (list, tuple, np.ndarray): [3] xyz, indicate voxel size
coors_range (list[float | tuple[float] | ndarray]): Range of voxels.
format: xyzxyz, minmax
num_points_per_voxel (int): Number of points per voxel.
coor_to_voxel_idx (np.ndarray): A voxel grid of shape (D, H, W), which
has the same shape as the complete voxel map. It indicates the
index of each corresponding voxel.
voxels (np.ndarray): Created empty voxels.
coors (np.ndarray): Created coordinates of each voxel.
max_points (int): Indicate maximum points contained in a voxel.
max_voxels (int): Maximum number of voxels this function create.
for second, 20000 is a good choice. Points should be shuffled for
randomness before this function because max_voxels drops points.
Returns:
tuple[np.ndarray]:
voxels: Shape [M, max_points, ndim], only contain points.
coordinates: Shape [M, 3].
num_points_per_voxel: Shape [M].
"""
# put all computations to one loop. # put all computations to one loop.
# we shouldn't create large array in main jit code, otherwise # we shouldn't create large array in main jit code, otherwise
# reduce performance # reduce performance
...@@ -175,11 +204,31 @@ def _points_to_voxel_kernel(points, ...@@ -175,11 +204,31 @@ def _points_to_voxel_kernel(points,
coors, coors,
max_points=35, max_points=35,
max_voxels=20000): max_voxels=20000):
# need mutex if write in cuda, but numba.cuda don't support mutex. """convert kitti points(N, >=3) to voxels.
# in addition, pytorch don't support cuda in dataloader.
# put all computations to one loop. Args:
# we shouldn't create large array in main jit code, otherwise points (np.ndarray): [N, ndim]. points[:, :3] contain xyz points and
# decrease performance points[:, 3:] contain other information such as reflectivity.
voxel_size (list, tuple, np.ndarray): [3] xyz, indicate voxel size
coors_range (list[float | tuple[float] | ndarray]): Range of voxels.
format: xyzxyz, minmax
num_points_per_voxel (int): Number of points per voxel.
coor_to_voxel_idx (np.ndarray): A voxel grid of shape (D, H, W), which
has the same shape as the complete voxel map. It indicates the
index of each corresponding voxel.
voxels (np.ndarray): Created empty voxels.
coors (np.ndarray): Created coordinates of each voxel.
max_points (int): Indicate maximum points contained in a voxel.
max_voxels (int): Maximum number of voxels this function create.
for second, 20000 is a good choice. Points should be shuffled for
randomness before this function because max_voxels drops points.
Returns:
tuple[np.ndarray]:
voxels: Shape [M, max_points, ndim], only contain points.
coordinates: Shape [M, 3].
num_points_per_voxel: Shape [M].
"""
N = points.shape[0] N = points.shape[0]
# ndim = points.shape[1] - 1 # ndim = points.shape[1] - 1
ndim = 3 ndim = 3
......
...@@ -3,11 +3,10 @@ import torch ...@@ -3,11 +3,10 @@ import torch
import torch.nn as nn import torch.nn as nn
from mmcv.cnn import bias_init_with_prob, normal_init from mmcv.cnn import bias_init_with_prob, normal_init
from mmdet3d.core import (PseudoSampler, box3d_multiclass_nms, from mmdet3d.core import (PseudoSampler, box3d_multiclass_nms, limit_period,
build_anchor_generator, build_assigner,
build_bbox_coder, build_sampler, limit_period,
xywhr2xyxyr) xywhr2xyxyr)
from mmdet.core import multi_apply from mmdet.core import (build_anchor_generator, build_assigner,
build_bbox_coder, build_sampler, multi_apply)
from mmdet.models import HEADS from mmdet.models import HEADS
from ..builder import build_loss from ..builder import build_loss
from .train_mixins import AnchorTrainMixin from .train_mixins import AnchorTrainMixin
......
...@@ -4,13 +4,12 @@ import torch.nn as nn ...@@ -4,13 +4,12 @@ import torch.nn as nn
import torch.nn.functional as F import torch.nn.functional as F
from mmcv.cnn import ConvModule from mmcv.cnn import ConvModule
from mmdet3d.core import build_bbox_coder
from mmdet3d.core.post_processing import aligned_3d_nms from mmdet3d.core.post_processing import aligned_3d_nms
from mmdet3d.models.builder import build_loss from mmdet3d.models.builder import build_loss
from mmdet3d.models.losses import chamfer_distance from mmdet3d.models.losses import chamfer_distance
from mmdet3d.models.model_utils import VoteModule from mmdet3d.models.model_utils import VoteModule
from mmdet3d.ops import PointSAModule, furthest_point_sample from mmdet3d.ops import PointSAModule, furthest_point_sample
from mmdet.core import multi_apply from mmdet.core import build_bbox_coder, multi_apply
from mmdet.models import HEADS from mmdet.models import HEADS
......
...@@ -7,6 +7,8 @@ from .voxelnet import VoxelNet ...@@ -7,6 +7,8 @@ from .voxelnet import VoxelNet
@DETECTORS.register_module() @DETECTORS.register_module()
class DynamicVoxelNet(VoxelNet): class DynamicVoxelNet(VoxelNet):
"""VoxelNet using `dynamic voxelization
<https://arxiv.org/abs/1910.06528>`_."""
def __init__(self, def __init__(self,
voxel_layer, voxel_layer,
...@@ -31,6 +33,7 @@ class DynamicVoxelNet(VoxelNet): ...@@ -31,6 +33,7 @@ class DynamicVoxelNet(VoxelNet):
) )
def extract_feat(self, points, img_metas): def extract_feat(self, points, img_metas):
"""Extract features from points"""
voxels, coors = self.voxelize(points) voxels, coors = self.voxelize(points)
voxel_features, feature_coors = self.voxel_encoder(voxels, coors) voxel_features, feature_coors = self.voxel_encoder(voxels, coors)
batch_size = coors[-1, 0].item() + 1 batch_size = coors[-1, 0].item() + 1
...@@ -42,6 +45,7 @@ class DynamicVoxelNet(VoxelNet): ...@@ -42,6 +45,7 @@ class DynamicVoxelNet(VoxelNet):
@torch.no_grad() @torch.no_grad()
def voxelize(self, points): def voxelize(self, points):
"""Apply dynamic voxelization to points"""
coors = [] coors = []
# dynamic voxelization only provide a coors mapping # dynamic voxelization only provide a coors mapping
for res in points: for res in points:
......
...@@ -34,6 +34,7 @@ class VoxelNet(SingleStage3DDetector): ...@@ -34,6 +34,7 @@ class VoxelNet(SingleStage3DDetector):
self.middle_encoder = builder.build_middle_encoder(middle_encoder) self.middle_encoder = builder.build_middle_encoder(middle_encoder)
def extract_feat(self, points, img_metas): def extract_feat(self, points, img_metas):
"""Extract features from points"""
voxels, num_points, coors = self.voxelize(points) voxels, num_points, coors = self.voxelize(points)
voxel_features = self.voxel_encoder(voxels, num_points, coors) voxel_features = self.voxel_encoder(voxels, num_points, coors)
batch_size = coors[-1, 0].item() + 1 batch_size = coors[-1, 0].item() + 1
...@@ -45,6 +46,7 @@ class VoxelNet(SingleStage3DDetector): ...@@ -45,6 +46,7 @@ class VoxelNet(SingleStage3DDetector):
@torch.no_grad() @torch.no_grad()
def voxelize(self, points): def voxelize(self, points):
"""Apply hard voxelization to points"""
voxels, coors, num_points = [], [], [] voxels, coors, num_points = [], [], []
for res in points: for res in points:
res_voxels, res_coors, res_num_points = self.voxel_layer(res) res_voxels, res_coors, res_num_points = self.voxel_layer(res)
......
...@@ -6,25 +6,24 @@ from ..registry import MIDDLE_ENCODERS ...@@ -6,25 +6,24 @@ from ..registry import MIDDLE_ENCODERS
@MIDDLE_ENCODERS.register_module() @MIDDLE_ENCODERS.register_module()
class PointPillarsScatter(nn.Module): class PointPillarsScatter(nn.Module):
"""Point Pillar's Scatter.
def __init__(self, in_channels, output_shape): Converts learned features from dense tensor to sparse pseudo image.
"""
Point Pillar's Scatter.
Converts learned features from dense tensor to sparse pseudo image.
Args: Args:
output_shape (list[int]): Required output shape of features. in_channels (int): Channels of input features.
in_channels (int): Number of input features. output_shape (list[int]): Required output shape of features.
""" """
def __init__(self, in_channels, output_shape):
super().__init__() super().__init__()
self.name = 'PointPillarsScatter'
self.output_shape = output_shape self.output_shape = output_shape
self.ny = output_shape[0] self.ny = output_shape[0]
self.nx = output_shape[1] self.nx = output_shape[1]
self.nchannels = in_channels self.in_channels = in_channels
def forward(self, voxel_features, coors, batch_size=None): def forward(self, voxel_features, coors, batch_size=None):
"""Foraward function to scatter features"""
# TODO: rewrite the function in a batch manner # TODO: rewrite the function in a batch manner
# no need to deal with different batch cases # no need to deal with different batch cases
if batch_size is not None: if batch_size is not None:
...@@ -33,9 +32,16 @@ class PointPillarsScatter(nn.Module): ...@@ -33,9 +32,16 @@ class PointPillarsScatter(nn.Module):
return self.forward_single(voxel_features, coors) return self.forward_single(voxel_features, coors)
def forward_single(self, voxel_features, coors): def forward_single(self, voxel_features, coors):
"""Scatter features of single sample
Args:
voxel_features (torch.Tensor): Voxel features in shape (N, M, C).
coors (torch.Tensor): Coordinates of each voxel.
The first column indicates the sample ID.
"""
# Create the canvas for this sample # Create the canvas for this sample
canvas = torch.zeros( canvas = torch.zeros(
self.nchannels, self.in_channels,
self.nx * self.ny, self.nx * self.ny,
dtype=voxel_features.dtype, dtype=voxel_features.dtype,
device=voxel_features.device) device=voxel_features.device)
...@@ -46,17 +52,24 @@ class PointPillarsScatter(nn.Module): ...@@ -46,17 +52,24 @@ class PointPillarsScatter(nn.Module):
# Now scatter the blob back to the canvas. # Now scatter the blob back to the canvas.
canvas[:, indices] = voxels canvas[:, indices] = voxels
# Undo the column stacking to final 4-dim tensor # Undo the column stacking to final 4-dim tensor
canvas = canvas.view(1, self.nchannels, self.ny, self.nx) canvas = canvas.view(1, self.in_channels, self.ny, self.nx)
return [canvas] return [canvas]
def forward_batch(self, voxel_features, coors, batch_size): def forward_batch(self, voxel_features, coors, batch_size):
"""Scatter features of single sample
Args:
voxel_features (torch.Tensor): Voxel features in shape (N, M, C).
coors (torch.Tensor): Coordinates of each voxel in shape (N, 4).
The first column indicates the sample ID.
batch_size (int): Number of samples in the current batch.
"""
# batch_canvas will be the final output. # batch_canvas will be the final output.
batch_canvas = [] batch_canvas = []
for batch_itt in range(batch_size): for batch_itt in range(batch_size):
# Create the canvas for this sample # Create the canvas for this sample
canvas = torch.zeros( canvas = torch.zeros(
self.nchannels, self.in_channels,
self.nx * self.ny, self.nx * self.ny,
dtype=voxel_features.dtype, dtype=voxel_features.dtype,
device=voxel_features.device) device=voxel_features.device)
...@@ -75,11 +88,11 @@ class PointPillarsScatter(nn.Module): ...@@ -75,11 +88,11 @@ class PointPillarsScatter(nn.Module):
# Append to a list for later stacking. # Append to a list for later stacking.
batch_canvas.append(canvas) batch_canvas.append(canvas)
# Stack to 3-dim tensor (batch-size, nchannels, nrows*ncols) # Stack to 3-dim tensor (batch-size, in_channels, nrows*ncols)
batch_canvas = torch.stack(batch_canvas, 0) batch_canvas = torch.stack(batch_canvas, 0)
# Undo the column stacking to final 4-dim tensor # Undo the column stacking to final 4-dim tensor
batch_canvas = batch_canvas.view(batch_size, self.nchannels, self.ny, batch_canvas = batch_canvas.view(batch_size, self.in_channels, self.ny,
self.nx) self.nx)
return batch_canvas return batch_canvas
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