transforms_3d.py 13.5 KB
Newer Older
zhangwenwei's avatar
zhangwenwei committed
1
import numpy as np
2
from mmcv.utils import build_from_cfg
zhangwenwei's avatar
zhangwenwei committed
3
4

from mmdet3d.core.bbox import box_np_ops
5
from mmdet.datasets.builder import PIPELINES
zhangwenwei's avatar
zhangwenwei committed
6
from mmdet.datasets.pipelines import RandomFlip
zhangwenwei's avatar
zhangwenwei committed
7
8
9
10
from ..registry import OBJECTSAMPLERS
from .data_augment_utils import noise_per_object_v3_


11
@PIPELINES.register_module()
zhangwenwei's avatar
zhangwenwei committed
12
13
14
15
16
17
18
19
class RandomFlip3D(RandomFlip):
    """Flip the points & bbox.

    If the input dict contains the key "flip", then the flag will be used,
    otherwise it will be randomly decided by a ratio specified in the init
    method.

    Args:
zhangwenwei's avatar
zhangwenwei committed
20
21
22
23
        sync_2d (bool, optional): Whether to apply flip according to the 2D
            images. If True, it will apply the same flip as that to 2D images.
            If False, it will decide whether to flip randomly and independently
            to that of 2D images.
zhangwenwei's avatar
zhangwenwei committed
24
25
26
27
28
29
30
        flip_ratio (float, optional): The flipping probability.
    """

    def __init__(self, sync_2d=True, **kwargs):
        super(RandomFlip3D, self).__init__(**kwargs)
        self.sync_2d = sync_2d

zhangwenwei's avatar
zhangwenwei committed
31
32
33
34
    def random_flip_data_3d(self, input_dict):
        input_dict['points'][:, 1] = -input_dict['points'][:, 1]
        for key in input_dict['bbox3d_fields']:
            input_dict[key].flip()
zhangwenwei's avatar
zhangwenwei committed
35
36

    def __call__(self, input_dict):
zhangwenwei's avatar
zhangwenwei committed
37
        # filp 2D image and its annotations
zhangwenwei's avatar
zhangwenwei committed
38
        super(RandomFlip3D, self).__call__(input_dict)
zhangwenwei's avatar
zhangwenwei committed
39

zhangwenwei's avatar
zhangwenwei committed
40
41
42
43
44
        if self.sync_2d:
            input_dict['pcd_flip'] = input_dict['flip']
        else:
            flip = True if np.random.rand() < self.flip_ratio else False
            input_dict['pcd_flip'] = flip
zhangwenwei's avatar
zhangwenwei committed
45

zhangwenwei's avatar
zhangwenwei committed
46
        if input_dict['pcd_flip']:
zhangwenwei's avatar
zhangwenwei committed
47
            self.random_flip_data_3d(input_dict)
zhangwenwei's avatar
zhangwenwei committed
48
49
        return input_dict

zhangwenwei's avatar
zhangwenwei committed
50
51
52
53
    def __repr__(self):
        return self.__class__.__name__ + '(flip_ratio={}, sync_2d={})'.format(
            self.flip_ratio, self.sync_2d)

zhangwenwei's avatar
zhangwenwei committed
54

55
@PIPELINES.register_module()
zhangwenwei's avatar
zhangwenwei committed
56
class ObjectSample(object):
zhangwenwei's avatar
zhangwenwei committed
57
58
59
60
61
62
63
    """Sample GT objects to the data

    Args:
        db_sampler (dict): Config dict of the database sampler.
        sample_2d (bool): Whether to also paste 2D image patch to the images
            This should be true when applying multi-modality cut-and-paste.
    """
zhangwenwei's avatar
zhangwenwei committed
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79

    def __init__(self, db_sampler, sample_2d=False):
        self.sampler_cfg = db_sampler
        self.sample_2d = sample_2d
        if 'type' not in db_sampler.keys():
            db_sampler['type'] = 'DataBaseSampler'
        self.db_sampler = build_from_cfg(db_sampler, OBJECTSAMPLERS)

    @staticmethod
    def remove_points_in_boxes(points, boxes):
        masks = box_np_ops.points_in_rbbox(points, boxes)
        points = points[np.logical_not(masks.any(-1))]
        return points

    def __call__(self, input_dict):
        gt_bboxes_3d = input_dict['gt_bboxes_3d']
zhangwenwei's avatar
zhangwenwei committed
80
81
        gt_labels_3d = input_dict['gt_labels_3d']

zhangwenwei's avatar
zhangwenwei committed
82
83
84
        # change to float for blending operation
        points = input_dict['points']
        if self.sample_2d:
wuyuefeng's avatar
wuyuefeng committed
85
            img = input_dict['img']
zhangwenwei's avatar
zhangwenwei committed
86
87
88
            gt_bboxes_2d = input_dict['gt_bboxes']
            # Assume for now 3D & 2D bboxes are the same
            sampled_dict = self.db_sampler.sample_all(
89
90
91
92
                gt_bboxes_3d.tensor.numpy(),
                gt_labels_3d,
                gt_bboxes_2d=gt_bboxes_2d,
                img=img)
zhangwenwei's avatar
zhangwenwei committed
93
94
        else:
            sampled_dict = self.db_sampler.sample_all(
95
                gt_bboxes_3d.tensor.numpy(), gt_labels_3d, img=None)
zhangwenwei's avatar
zhangwenwei committed
96
97
98
99

        if sampled_dict is not None:
            sampled_gt_bboxes_3d = sampled_dict['gt_bboxes_3d']
            sampled_points = sampled_dict['points']
zhangwenwei's avatar
zhangwenwei committed
100
            sampled_gt_labels = sampled_dict['gt_labels_3d']
zhangwenwei's avatar
zhangwenwei committed
101

zhangwenwei's avatar
zhangwenwei committed
102
103
            gt_labels_3d = np.concatenate([gt_labels_3d, sampled_gt_labels],
                                          axis=0)
104
105
106
            gt_bboxes_3d = gt_bboxes_3d.new_box(
                np.concatenate(
                    [gt_bboxes_3d.tensor.numpy(), sampled_gt_bboxes_3d]))
zhangwenwei's avatar
zhangwenwei committed
107

zhangwenwei's avatar
zhangwenwei committed
108
109
110
111
112
113
114
115
116
117
            points = self.remove_points_in_boxes(points, sampled_gt_bboxes_3d)
            # check the points dimension
            dim_inds = points.shape[-1]
            points = np.concatenate([sampled_points[:, :dim_inds], points],
                                    axis=0)

            if self.sample_2d:
                sampled_gt_bboxes_2d = sampled_dict['gt_bboxes_2d']
                gt_bboxes_2d = np.concatenate(
                    [gt_bboxes_2d, sampled_gt_bboxes_2d]).astype(np.float32)
zhangwenwei's avatar
zhangwenwei committed
118

zhangwenwei's avatar
zhangwenwei committed
119
                input_dict['gt_bboxes'] = gt_bboxes_2d
wuyuefeng's avatar
wuyuefeng committed
120
                input_dict['img'] = sampled_dict['img']
zhangwenwei's avatar
zhangwenwei committed
121
122

        input_dict['gt_bboxes_3d'] = gt_bboxes_3d
zhangwenwei's avatar
zhangwenwei committed
123
        input_dict['gt_labels_3d'] = gt_labels_3d
zhangwenwei's avatar
zhangwenwei committed
124
        input_dict['points'] = points
zhangwenwei's avatar
zhangwenwei committed
125

zhangwenwei's avatar
zhangwenwei committed
126
127
128
129
130
131
        return input_dict

    def __repr__(self):
        return self.__class__.__name__


132
@PIPELINES.register_module()
zhangwenwei's avatar
zhangwenwei committed
133
class ObjectNoise(object):
zhangwenwei's avatar
zhangwenwei committed
134
135
136
137
138
139
140
141
142
143
144
145
146
    """Apply noise to each GT objects in the scene

    Args:
        translation_std (list, optional): Standard deviation of the
            distribution where translation noise are sampled from.
            Defaults to [0.25, 0.25, 0.25].
        global_rot_range (list, optional): Global rotation to the scene.
            Defaults to [0.0, 0.0].
        rot_range (list, optional): Object rotation range.
            Defaults to [-0.15707963267, 0.15707963267].
        num_try (int, optional): Number of times to try if the noise applied is
            invalid. Defaults to 100.
    """
zhangwenwei's avatar
zhangwenwei committed
147
148

    def __init__(self,
zhangwenwei's avatar
zhangwenwei committed
149
                 translation_std=[0.25, 0.25, 0.25],
zhangwenwei's avatar
zhangwenwei committed
150
                 global_rot_range=[0.0, 0.0],
zhangwenwei's avatar
zhangwenwei committed
151
                 rot_range=[-0.15707963267, 0.15707963267],
zhangwenwei's avatar
zhangwenwei committed
152
                 num_try=100):
zhangwenwei's avatar
zhangwenwei committed
153
        self.translation_std = translation_std
zhangwenwei's avatar
zhangwenwei committed
154
        self.global_rot_range = global_rot_range
zhangwenwei's avatar
zhangwenwei committed
155
        self.rot_range = rot_range
zhangwenwei's avatar
zhangwenwei committed
156
157
158
159
160
        self.num_try = num_try

    def __call__(self, input_dict):
        gt_bboxes_3d = input_dict['gt_bboxes_3d']
        points = input_dict['points']
zhangwenwei's avatar
zhangwenwei committed
161

zhangwenwei's avatar
zhangwenwei committed
162
        # TODO: check this inplace function
163
        numpy_box = gt_bboxes_3d.tensor.numpy()
zhangwenwei's avatar
zhangwenwei committed
164
        noise_per_object_v3_(
165
            numpy_box,
zhangwenwei's avatar
zhangwenwei committed
166
            points,
zhangwenwei's avatar
zhangwenwei committed
167
168
            rotation_perturb=self.rot_range,
            center_noise_std=self.translation_std,
zhangwenwei's avatar
zhangwenwei committed
169
170
            global_random_rot_range=self.global_rot_range,
            num_try=self.num_try)
171
172

        input_dict['gt_bboxes_3d'] = gt_bboxes_3d.new_box(numpy_box)
zhangwenwei's avatar
zhangwenwei committed
173
174
175
176
177
178
        input_dict['points'] = points
        return input_dict

    def __repr__(self):
        repr_str = self.__class__.__name__
        repr_str += '(num_try={},'.format(self.num_try)
zhangwenwei's avatar
zhangwenwei committed
179
        repr_str += ' translation_std={},'.format(self.translation_std)
zhangwenwei's avatar
zhangwenwei committed
180
        repr_str += ' global_rot_range={},'.format(self.global_rot_range)
zhangwenwei's avatar
zhangwenwei committed
181
        repr_str += ' rot_range={})'.format(self.rot_range)
zhangwenwei's avatar
zhangwenwei committed
182
183
184
        return repr_str


185
@PIPELINES.register_module()
zhangwenwei's avatar
zhangwenwei committed
186
187
188
189
190
191
192
193
194
195
196
197
198
class GlobalRotScaleTrans(object):
    """Apply global rotation, scaling and translation to a 3D scene

    Args:
        rot_range (list[float]): Range of rotation angle.
            Default to [-0.78539816, 0.78539816] (close to [-pi/4, pi/4]).
        scale_ratio_range (list[float]): Range of scale ratio.
            Default to [0.95, 1.05].
        translation_std (list[float]): The standard deviation of ranslation
            noise. This apply random translation to a scene by a noise, which
            is sampled from a gaussian distribution whose standard deviation
            is set by ``translation_std``. Default to [0, 0, 0]
    """
zhangwenwei's avatar
zhangwenwei committed
199
200

    def __init__(self,
zhangwenwei's avatar
zhangwenwei committed
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
                 rot_range=[-0.78539816, 0.78539816],
                 scale_ratio_range=[0.95, 1.05],
                 translation_std=[0, 0, 0]):
        self.rot_range = rot_range
        self.scale_ratio_range = scale_ratio_range
        self.translation_std = translation_std

    def _trans_bbox_points(self, input_dict):
        if not isinstance(self.translation_std, (list, tuple, np.ndarray)):
            translation_std = [
                self.translation_std, self.translation_std,
                self.translation_std
            ]
        else:
            translation_std = self.translation_std
        translation_std = np.array(translation_std, dtype=np.float32)
        trans_factor = np.random.normal(scale=translation_std, size=3).T

        input_dict['points'][:, :3] += trans_factor
        input_dict['pcd_trans'] = trans_factor
        for key in input_dict['bbox3d_fields']:
            input_dict[key].translate(trans_factor)

    def _rot_bbox_points(self, input_dict):
        rotation = self.rot_range
zhangwenwei's avatar
zhangwenwei committed
226
227
228
        if not isinstance(rotation, list):
            rotation = [-rotation, rotation]
        noise_rotation = np.random.uniform(rotation[0], rotation[1])
zhangwenwei's avatar
zhangwenwei committed
229
230

        points = input_dict['points']
zhangwenwei's avatar
zhangwenwei committed
231
232
        points[:, :3], rot_mat_T = box_np_ops.rotation_points_single_angle(
            points[:, :3], noise_rotation, axis=2)
zhangwenwei's avatar
zhangwenwei committed
233
234
235
236
237
        input_dict['points'] = points
        input_dict['pcd_rotation'] = rot_mat_T

        for key in input_dict['bbox3d_fields']:
            input_dict[key].rotate(noise_rotation)
238

zhangwenwei's avatar
zhangwenwei committed
239
240
241
242
243
    def _scale_bbox_points(self, input_dict):
        scale = input_dict['pcd_scale_factor']
        input_dict['points'][:, :3] *= scale
        for key in input_dict['bbox3d_fields']:
            input_dict[key].scale(scale)
zhangwenwei's avatar
zhangwenwei committed
244

zhangwenwei's avatar
zhangwenwei committed
245
246
247
248
    def _random_scale(self, input_dict):
        scale_factor = np.random.uniform(self.scale_ratio_range[0],
                                         self.scale_ratio_range[1])
        input_dict['pcd_scale_factor'] = scale_factor
zhangwenwei's avatar
zhangwenwei committed
249
250

    def __call__(self, input_dict):
zhangwenwei's avatar
zhangwenwei committed
251
        self._rot_bbox_points(input_dict)
zhangwenwei's avatar
zhangwenwei committed
252

zhangwenwei's avatar
zhangwenwei committed
253
254
255
        if 'pcd_scale_factor' not in input_dict:
            self._random_scale(input_dict)
        self._scale_bbox_points(input_dict)
zhangwenwei's avatar
zhangwenwei committed
256

zhangwenwei's avatar
zhangwenwei committed
257
        self._trans_bbox_points(input_dict)
zhangwenwei's avatar
zhangwenwei committed
258
259
260
261
        return input_dict

    def __repr__(self):
        repr_str = self.__class__.__name__
zhangwenwei's avatar
zhangwenwei committed
262
263
264
        repr_str += '(rot_range={},'.format(self.rot_range)
        repr_str += ' scale_ratio_range={},'.format(self.scale_ratio_range)
        repr_str += ' translation_std={})'.format(self.translation_std)
zhangwenwei's avatar
zhangwenwei committed
265
266
267
        return repr_str


268
@PIPELINES.register_module()
zhangwenwei's avatar
zhangwenwei committed
269
270
271
272
273
274
275
276
277
278
class PointShuffle(object):

    def __call__(self, input_dict):
        np.random.shuffle(input_dict['points'])
        return input_dict

    def __repr__(self):
        return self.__class__.__name__


279
@PIPELINES.register_module()
zhangwenwei's avatar
zhangwenwei committed
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
class ObjectRangeFilter(object):

    def __init__(self, point_cloud_range):
        self.pcd_range = np.array(point_cloud_range, dtype=np.float32)
        self.bev_range = self.pcd_range[[0, 1, 3, 4]]

    @staticmethod
    def filter_gt_box_outside_range(gt_bboxes_3d, limit_range):
        """remove gtbox outside training range.
        this function should be applied after other prep functions
        Args:
            gt_bboxes_3d ([type]): [description]
            limit_range ([type]): [description]
        """
        gt_bboxes_3d_bv = box_np_ops.center_to_corner_box2d(
            gt_bboxes_3d[:, [0, 1]], gt_bboxes_3d[:, [3, 3 + 1]],
            gt_bboxes_3d[:, 6])
        bounding_box = box_np_ops.minmax_to_corner_2d(
            np.asarray(limit_range)[np.newaxis, ...])
        ret = box_np_ops.points_in_convex_polygon_jit(
            gt_bboxes_3d_bv.reshape(-1, 2), bounding_box)
        return np.any(ret.reshape(-1, 4), axis=1)

    def __call__(self, input_dict):
        gt_bboxes_3d = input_dict['gt_bboxes_3d']
zhangwenwei's avatar
zhangwenwei committed
305
        gt_labels_3d = input_dict['gt_labels_3d']
306
        mask = gt_bboxes_3d.in_range_bev(self.bev_range)
zhangwenwei's avatar
zhangwenwei committed
307
        gt_bboxes_3d = gt_bboxes_3d[mask]
ZwwWayne's avatar
ZwwWayne committed
308
309
310
311
312
        # mask is a torch tensor but gt_labels_3d is still numpy array
        # using mask to index gt_labels_3d will cause bug when
        # len(gt_labels_3d) == 1, where mask=1 will be interpreted
        # as gt_labels_3d[1] and cause out of index error
        gt_labels_3d = gt_labels_3d[mask.numpy().astype(np.bool)]
zhangwenwei's avatar
zhangwenwei committed
313
314

        # limit rad to [-pi, pi]
315
316
        gt_bboxes_3d.limit_yaw(offset=0.5, period=2 * np.pi)
        input_dict['gt_bboxes_3d'] = gt_bboxes_3d
zhangwenwei's avatar
zhangwenwei committed
317
318
        input_dict['gt_labels_3d'] = gt_labels_3d

zhangwenwei's avatar
zhangwenwei committed
319
320
321
322
323
324
325
326
        return input_dict

    def __repr__(self):
        repr_str = self.__class__.__name__
        repr_str += '(point_cloud_range={})'.format(self.pcd_range.tolist())
        return repr_str


327
@PIPELINES.register_module()
zhangwenwei's avatar
zhangwenwei committed
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
class PointsRangeFilter(object):

    def __init__(self, point_cloud_range):
        self.pcd_range = np.array(
            point_cloud_range, dtype=np.float32)[np.newaxis, :]

    def __call__(self, input_dict):
        points = input_dict['points']
        points_mask = ((points[:, :3] >= self.pcd_range[:, :3])
                       & (points[:, :3] < self.pcd_range[:, 3:]))
        points_mask = points_mask[:, 0] & points_mask[:, 1] & points_mask[:, 2]
        clean_points = points[points_mask, :]
        input_dict['points'] = clean_points
        return input_dict

    def __repr__(self):
        repr_str = self.__class__.__name__
        repr_str += '(point_cloud_range={})'.format(self.pcd_range.tolist())
        return repr_str
zhangwenwei's avatar
zhangwenwei committed
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373


@PIPELINES.register_module()
class ObjectNameFilter(object):
    """Filter GT objects by their names

    Args:
        classes (list[str]): list of class names to be kept for training
    """

    def __init__(self, classes):
        self.classes = classes
        self.labels = list(range(len(self.classes)))

    def __call__(self, input_dict):
        gt_labels_3d = input_dict['gt_labels_3d']
        gt_bboxes_mask = np.array([n in self.labels for n in gt_labels_3d],
                                  dtype=np.bool_)
        input_dict['gt_bboxes_3d'] = input_dict['gt_bboxes_3d'][gt_bboxes_mask]
        input_dict['gt_labels_3d'] = input_dict['gt_labels_3d'][gt_bboxes_mask]

        return input_dict

    def __repr__(self):
        repr_str = self.__class__.__name__
        repr_str += f'(classes={self.classes})'
        return repr_str