group_points.py 7.97 KB
Newer Older
1
# Copyright (c) OpenMMLab. All rights reserved.
ChaimZhu's avatar
ChaimZhu committed
2

3
4
from typing import Tuple

wuyuefeng's avatar
wuyuefeng committed
5
import torch
6
from mmcv.runner import force_fp32
zhangwenwei's avatar
zhangwenwei committed
7
from torch import nn as nn
wuyuefeng's avatar
wuyuefeng committed
8
9
10
from torch.autograd import Function

from ..ball_query import ball_query
11
from ..knn import knn
wuyuefeng's avatar
wuyuefeng committed
12
13
14
15
16
17
18
19
20
from . import group_points_ext


class QueryAndGroup(nn.Module):
    """Query and Group.

    Groups with a ball query of radius

    Args:
21
        max_radius (float): The maximum radius of the balls.
22
            If None is given, we will use kNN sampling instead of ball query.
wuyuefeng's avatar
wuyuefeng committed
23
        sample_num (int): Maximum number of features to gather in the ball.
24
25
26
        min_radius (float, optional): The minimum radius of the balls.
            Default: 0.
        use_xyz (bool, optional): Whether to use xyz.
wuyuefeng's avatar
wuyuefeng committed
27
            Default: True.
28
        return_grouped_xyz (bool, optional): Whether to return grouped xyz.
wuyuefeng's avatar
wuyuefeng committed
29
            Default: False.
30
        normalize_xyz (bool, optional): Whether to normalize xyz.
wuyuefeng's avatar
wuyuefeng committed
31
            Default: False.
32
        uniform_sample (bool, optional): Whether to sample uniformly.
wuyuefeng's avatar
wuyuefeng committed
33
            Default: False
34
35
36
        return_unique_cnt (bool, optional): Whether to return the count of
            unique samples. Default: False.
        return_grouped_idx (bool, optional): Whether to return grouped idx.
37
            Default: False.
wuyuefeng's avatar
wuyuefeng committed
38
39
40
    """

    def __init__(self,
41
                 max_radius,
wuyuefeng's avatar
wuyuefeng committed
42
                 sample_num,
43
                 min_radius=0,
wuyuefeng's avatar
wuyuefeng committed
44
45
46
47
                 use_xyz=True,
                 return_grouped_xyz=False,
                 normalize_xyz=False,
                 uniform_sample=False,
48
49
                 return_unique_cnt=False,
                 return_grouped_idx=False):
wuyuefeng's avatar
wuyuefeng committed
50
        super(QueryAndGroup, self).__init__()
51
52
        self.max_radius = max_radius
        self.min_radius = min_radius
wuyuefeng's avatar
wuyuefeng committed
53
54
55
56
57
58
        self.sample_num = sample_num
        self.use_xyz = use_xyz
        self.return_grouped_xyz = return_grouped_xyz
        self.normalize_xyz = normalize_xyz
        self.uniform_sample = uniform_sample
        self.return_unique_cnt = return_unique_cnt
59
        self.return_grouped_idx = return_grouped_idx
wuyuefeng's avatar
wuyuefeng committed
60
        if self.return_unique_cnt:
61
62
63
64
65
66
            assert self.uniform_sample, \
                'uniform_sample should be True when ' \
                'returning the count of unique samples'
        if self.max_radius is None:
            assert not self.normalize_xyz, \
                'can not normalize grouped xyz when max_radius is None'
67
        self.fp16_enabled = False
wuyuefeng's avatar
wuyuefeng committed
68

69
    @force_fp32()
wuyuefeng's avatar
wuyuefeng committed
70
    def forward(self, points_xyz, center_xyz, features=None):
zhangwenwei's avatar
zhangwenwei committed
71
        """forward.
wuyuefeng's avatar
wuyuefeng committed
72
73
74
75
76
77
78
79
80

        Args:
            points_xyz (Tensor): (B, N, 3) xyz coordinates of the features.
            center_xyz (Tensor): (B, npoint, 3) Centriods.
            features (Tensor): (B, C, N) Descriptors of the features.

        Return:
            Tensor: (B, 3 + C, npoint, sample_num) Grouped feature.
        """
81
82
83
84
85
86
87
88
        # if self.max_radius is None, we will perform kNN instead of ball query
        # idx is of shape [B, npoint, sample_num]
        if self.max_radius is None:
            idx = knn(self.sample_num, points_xyz, center_xyz, False)
            idx = idx.transpose(1, 2).contiguous()
        else:
            idx = ball_query(self.min_radius, self.max_radius, self.sample_num,
                             points_xyz, center_xyz)
wuyuefeng's avatar
wuyuefeng committed
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106

        if self.uniform_sample:
            unique_cnt = torch.zeros((idx.shape[0], idx.shape[1]))
            for i_batch in range(idx.shape[0]):
                for i_region in range(idx.shape[1]):
                    unique_ind = torch.unique(idx[i_batch, i_region, :])
                    num_unique = unique_ind.shape[0]
                    unique_cnt[i_batch, i_region] = num_unique
                    sample_ind = torch.randint(
                        0,
                        num_unique, (self.sample_num - num_unique, ),
                        dtype=torch.long)
                    all_ind = torch.cat((unique_ind, unique_ind[sample_ind]))
                    idx[i_batch, i_region, :] = all_ind

        xyz_trans = points_xyz.transpose(1, 2).contiguous()
        # (B, 3, npoint, sample_num)
        grouped_xyz = grouping_operation(xyz_trans, idx)
107
108
        grouped_xyz_diff = grouped_xyz - \
            center_xyz.transpose(1, 2).unsqueeze(-1)  # relative offsets
wuyuefeng's avatar
wuyuefeng committed
109
        if self.normalize_xyz:
110
            grouped_xyz_diff /= self.max_radius
wuyuefeng's avatar
wuyuefeng committed
111
112
113
114
115

        if features is not None:
            grouped_features = grouping_operation(features, idx)
            if self.use_xyz:
                # (B, C + 3, npoint, sample_num)
116
                new_features = torch.cat([grouped_xyz_diff, grouped_features],
wuyuefeng's avatar
wuyuefeng committed
117
118
119
120
121
122
                                         dim=1)
            else:
                new_features = grouped_features
        else:
            assert (self.use_xyz
                    ), 'Cannot have not features and not use xyz as a feature!'
123
            new_features = grouped_xyz_diff
wuyuefeng's avatar
wuyuefeng committed
124
125
126
127
128
129

        ret = [new_features]
        if self.return_grouped_xyz:
            ret.append(grouped_xyz)
        if self.return_unique_cnt:
            ret.append(unique_cnt)
130
131
        if self.return_grouped_idx:
            ret.append(idx)
wuyuefeng's avatar
wuyuefeng committed
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
        if len(ret) == 1:
            return ret[0]
        else:
            return tuple(ret)


class GroupAll(nn.Module):
    """Group All.

    Group xyz with feature.

    Args:
        use_xyz (bool): Whether to use xyz.
    """

    def __init__(self, use_xyz: bool = True):
        super().__init__()
        self.use_xyz = use_xyz
150
        self.fp16_enabled = False
wuyuefeng's avatar
wuyuefeng committed
151

152
    @force_fp32()
wuyuefeng's avatar
wuyuefeng committed
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
    def forward(self,
                xyz: torch.Tensor,
                new_xyz: torch.Tensor,
                features: torch.Tensor = None):
        """forward.

        Args:
            xyz (Tensor): (B, N, 3) xyz coordinates of the features.
            new_xyz (Tensor): Ignored.
            features (Tensor): (B, C, N) features to group.

        Return:
            Tensor: (B, C + 3, 1, N) Grouped feature.
        """
        grouped_xyz = xyz.transpose(1, 2).unsqueeze(2)
        if features is not None:
            grouped_features = features.unsqueeze(2)
            if self.use_xyz:
                new_features = torch.cat([grouped_xyz, grouped_features],
                                         dim=1)  # (B, 3 + C, 1, N)
            else:
                new_features = grouped_features
        else:
            new_features = grouped_xyz

        return new_features


class GroupingOperation(Function):
    """Grouping Operation.

    Group feature with given index.
    """

    @staticmethod
    def forward(ctx, features: torch.Tensor,
                indices: torch.Tensor) -> torch.Tensor:
        """forward.

        Args:
            features (Tensor): (B, C, N) tensor of features to group.
194
            indices (Tensor): (B, npoint, nsample) the indices of
wuyuefeng's avatar
wuyuefeng committed
195
196
197
198
199
200
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
226
227
228
229
230
231
232
233
234
235
236
                features to group with.

        Returns:
            Tensor: (B, C, npoint, nsample) Grouped features.
        """
        assert features.is_contiguous()
        assert indices.is_contiguous()

        B, nfeatures, nsample = indices.size()
        _, C, N = features.size()
        output = torch.cuda.FloatTensor(B, C, nfeatures, nsample)

        group_points_ext.forward(B, C, N, nfeatures, nsample, features,
                                 indices, output)

        ctx.for_backwards = (indices, N)
        return output

    @staticmethod
    def backward(ctx,
                 grad_out: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
        """backward.

        Args:
            grad_out (Tensor): (B, C, npoint, nsample) tensor of the gradients
                of the output from forward.

        Returns:
            Tensor: (B, C, N) gradient of the features.
        """
        idx, N = ctx.for_backwards

        B, C, npoint, nsample = grad_out.size()
        grad_features = torch.cuda.FloatTensor(B, C, N).zero_()

        grad_out_data = grad_out.data.contiguous()
        group_points_ext.backward(B, C, N, npoint, nsample, grad_out_data, idx,
                                  grad_features.data)
        return grad_features, None


grouping_operation = GroupingOperation.apply