cam_box3d.py 13.4 KB
Newer Older
dingchang's avatar
dingchang committed
1
# Copyright (c) OpenMMLab. All rights reserved.
zhangwenwei's avatar
zhangwenwei committed
2
3
4
import numpy as np
import torch

zhangshilong's avatar
zhangshilong committed
5
from mmdet3d.structures.points import BasePoints
zhangwenwei's avatar
zhangwenwei committed
6
from .base_box3d import BaseInstance3DBoxes
7
from .utils import rotation_3d_in_axis, yaw2local
zhangwenwei's avatar
zhangwenwei committed
8
9


zhangwenwei's avatar
zhangwenwei committed
10
class CameraInstance3DBoxes(BaseInstance3DBoxes):
liyinhao's avatar
liyinhao committed
11
    """3D boxes of instances in CAM coordinates.
zhangwenwei's avatar
zhangwenwei committed
12
13

    Coordinates in camera:
14

zhangwenwei's avatar
zhangwenwei committed
15
    .. code-block:: none
zhangwenwei's avatar
zhangwenwei committed
16

17
                z front (yaw=-0.5*pi)
zhangwenwei's avatar
zhangwenwei committed
18
19
               /
              /
wuyuefeng's avatar
wuyuefeng committed
20
             0 ------> x right (yaw=0)
zhangwenwei's avatar
zhangwenwei committed
21
22
23
24
             |
             |
             v
        down y
zhangwenwei's avatar
zhangwenwei committed
25

wuyuefeng's avatar
wuyuefeng committed
26
    The relative coordinate of bottom center in a CAM box is (0.5, 1.0, 0.5),
zhangwenwei's avatar
zhangwenwei committed
27
    and the yaw is around the y axis, thus the rotation axis=1.
28
    The yaw is 0 at the positive direction of x axis, and decreases from
wuyuefeng's avatar
wuyuefeng committed
29
    the positive direction of x to the positive direction of z.
zhangwenwei's avatar
zhangwenwei committed
30
31

    Attributes:
32
33
        tensor (torch.Tensor): Float matrix in shape (N, box_dim).
        box_dim (int): Integer indicating the dimension of a box
zhangwenwei's avatar
zhangwenwei committed
34
            Each row is (x, y, z, x_size, y_size, z_size, yaw, ...).
35
36
        with_yaw (bool): If True, the value of yaw will be set to 0 as
            axis-aligned boxes tightly enclosing the original boxes.
zhangwenwei's avatar
zhangwenwei committed
37
    """
38
    YAW_AXIS = 1
zhangwenwei's avatar
zhangwenwei committed
39

40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
    def __init__(self,
                 tensor,
                 box_dim=7,
                 with_yaw=True,
                 origin=(0.5, 1.0, 0.5)):
        if isinstance(tensor, torch.Tensor):
            device = tensor.device
        else:
            device = torch.device('cpu')
        tensor = torch.as_tensor(tensor, dtype=torch.float32, device=device)
        if tensor.numel() == 0:
            # Use reshape, so we don't end up creating a new tensor that
            # does not depend on the inputs (and consequently confuses jit)
            tensor = tensor.reshape((0, box_dim)).to(
                dtype=torch.float32, device=device)
        assert tensor.dim() == 2 and tensor.size(-1) == box_dim, tensor.size()

        if tensor.shape[-1] == 6:
            # If the dimension of boxes is 6, we expand box_dim by padding
            # 0 as a fake yaw and set with_yaw to False.
            assert box_dim == 6
            fake_rot = tensor.new_zeros(tensor.shape[0], 1)
            tensor = torch.cat((tensor, fake_rot), dim=-1)
            self.box_dim = box_dim + 1
            self.with_yaw = False
        else:
            self.box_dim = box_dim
            self.with_yaw = with_yaw
68
        self.tensor = tensor.clone()
69
70
71
72
73
74

        if origin != (0.5, 1.0, 0.5):
            dst = self.tensor.new_tensor((0.5, 1.0, 0.5))
            src = self.tensor.new_tensor(origin)
            self.tensor[:, :3] += self.tensor[:, 3:6] * (dst - src)

75
76
    @property
    def height(self):
77
        """torch.Tensor: A vector with height of each box in shape (N, )."""
78
79
        return self.tensor[:, 4]

80
81
    @property
    def top_height(self):
82
83
        """torch.Tensor:
            A vector with the top height of each box in shape (N, )."""
84
85
86
87
88
        # the positive direction is down rather than up
        return self.bottom_height - self.height

    @property
    def bottom_height(self):
89
90
        """torch.Tensor:
            A vector with bottom's height of each box in shape (N, )."""
91
92
        return self.tensor[:, 1]

93
94
95
96
97
98
99
100
101
102
103
104
105
106
    @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

zhangwenwei's avatar
zhangwenwei committed
107
108
    @property
    def gravity_center(self):
109
        """torch.Tensor: A tensor with center of each box in shape (N, 3)."""
zhangwenwei's avatar
zhangwenwei committed
110
111
112
        bottom_center = self.bottom_center
        gravity_center = torch.zeros_like(bottom_center)
        gravity_center[:, [0, 2]] = bottom_center[:, [0, 2]]
zhangwenwei's avatar
zhangwenwei committed
113
        gravity_center[:, 1] = bottom_center[:, 1] - self.tensor[:, 4] * 0.5
zhangwenwei's avatar
zhangwenwei committed
114
115
116
117
        return gravity_center

    @property
    def corners(self):
118
119
        """torch.Tensor: Coordinates of corners of all the boxes in
                         shape (N, 8, 3).
zhangwenwei's avatar
zhangwenwei committed
120
121

        Convert the boxes to  in clockwise order, in the form of
wuyuefeng's avatar
wuyuefeng committed
122
        (x0y0z0, x0y0z1, x0y1z1, x0y1z0, x1y0z0, x1y0z1, x1y1z1, x1y1z0)
zhangwenwei's avatar
zhangwenwei committed
123
124

        .. code-block:: none
zhangwenwei's avatar
zhangwenwei committed
125

zhangwenwei's avatar
zhangwenwei committed
126
127
128
129
130
131
                         front z
                              /
                             /
               (x0, y0, z1) + -----------  + (x1, y0, z1)
                           /|            / |
                          / |           /  |
wuyuefeng's avatar
wuyuefeng committed
132
            (x0, y0, z0) + ----------- +   + (x1, y1, z1)
zhangwenwei's avatar
zhangwenwei committed
133
                         |  /      .   |  /
134
                         | / origin    | /
zhangwenwei's avatar
zhangwenwei committed
135
136
137
138
139
140
            (x0, y1, z0) + ----------- + -------> x right
                         |             (x1, y1, z0)
                         |
                         v
                    down y
        """
141
142
143
        if self.tensor.numel() == 0:
            return torch.empty([0, 8, 3], device=self.tensor.device)

zhangwenwei's avatar
zhangwenwei committed
144
145
146
147
148
149
150
151
152
153
        dims = self.dims
        corners_norm = torch.from_numpy(
            np.stack(np.unravel_index(np.arange(8), [2] * 3), axis=1)).to(
                device=dims.device, dtype=dims.dtype)

        corners_norm = corners_norm[[0, 1, 3, 2, 4, 5, 7, 6]]
        # use relative origin [0.5, 1, 0.5]
        corners_norm = corners_norm - dims.new_tensor([0.5, 1, 0.5])
        corners = dims.view([-1, 1, 3]) * corners_norm.reshape([1, 8, 3])

154
        corners = rotation_3d_in_axis(
Yezhen Cong's avatar
Yezhen Cong committed
155
            corners, self.tensor[:, 6], axis=self.YAW_AXIS)
zhangwenwei's avatar
zhangwenwei committed
156
157
158
        corners += self.tensor[:, :3].view(-1, 1, 3)
        return corners

159
    @property
160
    def bev(self):
161
162
        """torch.Tensor: 2D BEV box of each box with rotation
            in XYWHR format, in shape (N, 5)."""
163
164
165
166
167
168
        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
169

wuyuefeng's avatar
wuyuefeng committed
170
    def rotate(self, angle, points=None):
171
172
        """Rotate boxes with points (optional) with the given angle or rotation
        matrix.
zhangwenwei's avatar
zhangwenwei committed
173
174

        Args:
175
176
            angle (float | torch.Tensor | np.ndarray):
                Rotation angle or rotation matrix.
177
            points (torch.Tensor | np.ndarray | :obj:`BasePoints`, optional):
178
                Points to rotate. Defaults to None.
wuyuefeng's avatar
wuyuefeng committed
179
180

        Returns:
181
182
            tuple or None: When ``points`` is None, the function returns
                None, otherwise it returns the rotated points and the
wuyuefeng's avatar
wuyuefeng committed
183
                rotation matrix ``rot_mat_T``.
zhangwenwei's avatar
zhangwenwei committed
184
185
186
        """
        if not isinstance(angle, torch.Tensor):
            angle = self.tensor.new_tensor(angle)
187

188
189
190
191
        assert angle.shape == torch.Size([3, 3]) or angle.numel() == 1, \
            f'invalid rotation angle shape {angle.shape}'

        if angle.numel() == 1:
192
193
194
195
            self.tensor[:, 0:3], rot_mat_T = rotation_3d_in_axis(
                self.tensor[:, 0:3],
                angle,
                axis=self.YAW_AXIS,
Yezhen Cong's avatar
Yezhen Cong committed
196
                return_mat=True)
197
198
199
200
201
        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)
202
            self.tensor[:, 0:3] = self.tensor[:, 0:3] @ rot_mat_T
zhangwenwei's avatar
zhangwenwei committed
203
204
205

        self.tensor[:, 6] += angle

wuyuefeng's avatar
wuyuefeng committed
206
207
208
209
        if points is not None:
            if isinstance(points, torch.Tensor):
                points[:, :3] = points[:, :3] @ rot_mat_T
            elif isinstance(points, np.ndarray):
210
                rot_mat_T = rot_mat_T.cpu().numpy()
wuyuefeng's avatar
wuyuefeng committed
211
                points[:, :3] = np.dot(points[:, :3], rot_mat_T)
212
            elif isinstance(points, BasePoints):
213
                points.rotate(rot_mat_T)
wuyuefeng's avatar
wuyuefeng committed
214
215
216
217
218
            else:
                raise ValueError
            return points, rot_mat_T

    def flip(self, bev_direction='horizontal', points=None):
zhangwenwei's avatar
zhangwenwei committed
219
        """Flip the boxes in BEV along given BEV direction.
zhangwenwei's avatar
zhangwenwei committed
220

wuyuefeng's avatar
wuyuefeng committed
221
222
223
224
        In CAM coordinates, it flips the x (horizontal) or z (vertical) axis.

        Args:
            bev_direction (str): Flip direction (horizontal or vertical).
225
            points (torch.Tensor | np.ndarray | :obj:`BasePoints`, optional):
226
                Points to flip. Defaults to None.
wuyuefeng's avatar
wuyuefeng committed
227
228
229

        Returns:
            torch.Tensor, numpy.ndarray or None: Flipped points.
zhangwenwei's avatar
zhangwenwei committed
230
        """
wuyuefeng's avatar
wuyuefeng committed
231
232
233
234
235
236
237
238
239
        assert bev_direction in ('horizontal', 'vertical')
        if bev_direction == 'horizontal':
            self.tensor[:, 0::7] = -self.tensor[:, 0::7]
            if self.with_yaw:
                self.tensor[:, 6] = -self.tensor[:, 6] + np.pi
        elif bev_direction == 'vertical':
            self.tensor[:, 2::7] = -self.tensor[:, 2::7]
            if self.with_yaw:
                self.tensor[:, 6] = -self.tensor[:, 6]
zhangwenwei's avatar
zhangwenwei committed
240

wuyuefeng's avatar
wuyuefeng committed
241
        if points is not None:
242
243
244
245
246
247
248
249
            assert isinstance(points, (torch.Tensor, np.ndarray, BasePoints))
            if isinstance(points, (torch.Tensor, np.ndarray)):
                if bev_direction == 'horizontal':
                    points[:, 0] = -points[:, 0]
                elif bev_direction == 'vertical':
                    points[:, 2] = -points[:, 2]
            elif isinstance(points, BasePoints):
                points.flip(bev_direction)
wuyuefeng's avatar
wuyuefeng committed
250
251
            return points

252
    @classmethod
253
    def height_overlaps(cls, boxes1, boxes2, mode='iou'):
zhangwenwei's avatar
zhangwenwei committed
254
        """Calculate height overlaps of two boxes.
255

Wenwei Zhang's avatar
Wenwei Zhang committed
256
257
        This function calculates the height overlaps between ``boxes1`` and
        ``boxes2``, where ``boxes1`` and ``boxes2`` should be in the same type.
258
259

        Args:
Wenwei Zhang's avatar
Wenwei Zhang committed
260
261
            boxes1 (:obj:`CameraInstance3DBoxes`): Boxes 1 contain N boxes.
            boxes2 (:obj:`CameraInstance3DBoxes`): Boxes 2 contain M boxes.
liyinhao's avatar
liyinhao committed
262
            mode (str, optional): Mode of iou calculation. Defaults to 'iou'.
263
264

        Returns:
Wenwei Zhang's avatar
Wenwei Zhang committed
265
            torch.Tensor: Calculated iou of boxes' heights.
266
        """
Wenwei Zhang's avatar
Wenwei Zhang committed
267
268
        assert isinstance(boxes1, CameraInstance3DBoxes)
        assert isinstance(boxes2, CameraInstance3DBoxes)
269
270
271
272
273
274

        boxes1_top_height = boxes1.top_height.view(-1, 1)
        boxes1_bottom_height = boxes1.bottom_height.view(-1, 1)
        boxes2_top_height = boxes2.top_height.view(1, -1)
        boxes2_bottom_height = boxes2.bottom_height.view(1, -1)

275
276
        # positive direction of the gravity axis
        # in cam coord system points to the earth
277
278
279
280
281
        heighest_of_bottom = torch.min(boxes1_bottom_height,
                                       boxes2_bottom_height)
        lowest_of_top = torch.max(boxes1_top_height, boxes2_top_height)
        overlaps_h = torch.clamp(heighest_of_bottom - lowest_of_top, min=0)
        return overlaps_h
282
283

    def convert_to(self, dst, rt_mat=None):
Wenwei Zhang's avatar
Wenwei Zhang committed
284
        """Convert self to ``dst`` mode.
285
286

        Args:
287
            dst (:obj:`Box3DMode`): The target Box mode.
288
289
290
            rt_mat (np.ndarray | torch.Tensor, optional): The rotation and
                translation matrix between different coordinates.
                Defaults to None.
Wenwei Zhang's avatar
Wenwei Zhang committed
291
                The conversion from ``src`` coordinates to ``dst`` coordinates
292
293
294
295
                usually comes along the change of sensors, e.g., from camera
                to LiDAR. This requires a transformation matrix.

        Returns:
296
            :obj:`BaseInstance3DBoxes`:
Wenwei Zhang's avatar
Wenwei Zhang committed
297
                The converted box of the same type in the ``dst`` mode.
298
299
300
301
        """
        from .box_3d_mode import Box3DMode
        return Box3DMode.convert(
            box=self, src=Box3DMode.CAM, dst=dst, rt_mat=rt_mat)
302

303
304
    def points_in_boxes_part(self, points, boxes_override=None):
        """Find the box in which each point is.
305
306

        Args:
307
308
309
310
            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.
311
312

        Returns:
313
314
315
            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).
316
317
318
319
320
        """
        from .coord_3d_mode import Coord3DMode

        points_lidar = Coord3DMode.convert(points, Coord3DMode.CAM,
                                           Coord3DMode.LIDAR)
321
322
323
324
325
        if boxes_override is not None:
            boxes_lidar = boxes_override
        else:
            boxes_lidar = Coord3DMode.convert(self.tensor, Coord3DMode.CAM,
                                              Coord3DMode.LIDAR)
326

327
        box_idx = super().points_in_boxes_part(points_lidar, boxes_lidar)
328
329
        return box_idx

330
331
    def points_in_boxes_all(self, points, boxes_override=None):
        """Find all boxes in which each point is.
332
333

        Args:
334
335
336
337
            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.
338
339

        Returns:
340
341
            torch.Tensor: The index of all boxes in which each point is,
                in shape (B, M, T).
342
343
344
345
346
        """
        from .coord_3d_mode import Coord3DMode

        points_lidar = Coord3DMode.convert(points, Coord3DMode.CAM,
                                           Coord3DMode.LIDAR)
347
348
349
350
351
        if boxes_override is not None:
            boxes_lidar = boxes_override
        else:
            boxes_lidar = Coord3DMode.convert(self.tensor, Coord3DMode.CAM,
                                              Coord3DMode.LIDAR)
352

353
        box_idx = super().points_in_boxes_all(points_lidar, boxes_lidar)
354
        return box_idx