group_points.py 7.83 KB
Newer Older
1
# Copyright (c) OpenMMLab. All rights reserved.
wuyuefeng's avatar
wuyuefeng committed
2
import torch
zhangwenwei's avatar
zhangwenwei committed
3
from torch import nn as nn
wuyuefeng's avatar
wuyuefeng committed
4
from torch.autograd import Function
zhangwenwei's avatar
zhangwenwei committed
5
from typing import Tuple
wuyuefeng's avatar
wuyuefeng committed
6
7

from ..ball_query import ball_query
8
from ..knn import knn
wuyuefeng's avatar
wuyuefeng committed
9
10
11
12
13
14
15
16
17
from . import group_points_ext


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

    Groups with a ball query of radius

    Args:
18
        max_radius (float): The maximum radius of the balls.
19
            If None is given, we will use kNN sampling instead of ball query.
wuyuefeng's avatar
wuyuefeng committed
20
        sample_num (int): Maximum number of features to gather in the ball.
21
22
23
        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
24
            Default: True.
25
        return_grouped_xyz (bool, optional): Whether to return grouped xyz.
wuyuefeng's avatar
wuyuefeng committed
26
            Default: False.
27
        normalize_xyz (bool, optional): Whether to normalize xyz.
wuyuefeng's avatar
wuyuefeng committed
28
            Default: False.
29
        uniform_sample (bool, optional): Whether to sample uniformly.
wuyuefeng's avatar
wuyuefeng committed
30
            Default: False
31
32
33
        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.
34
            Default: False.
wuyuefeng's avatar
wuyuefeng committed
35
36
37
    """

    def __init__(self,
38
                 max_radius,
wuyuefeng's avatar
wuyuefeng committed
39
                 sample_num,
40
                 min_radius=0,
wuyuefeng's avatar
wuyuefeng committed
41
42
43
44
                 use_xyz=True,
                 return_grouped_xyz=False,
                 normalize_xyz=False,
                 uniform_sample=False,
45
46
                 return_unique_cnt=False,
                 return_grouped_idx=False):
wuyuefeng's avatar
wuyuefeng committed
47
        super(QueryAndGroup, self).__init__()
48
49
        self.max_radius = max_radius
        self.min_radius = min_radius
wuyuefeng's avatar
wuyuefeng committed
50
51
52
53
54
55
        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
56
        self.return_grouped_idx = return_grouped_idx
wuyuefeng's avatar
wuyuefeng committed
57
        if self.return_unique_cnt:
58
59
60
61
62
63
            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'
wuyuefeng's avatar
wuyuefeng committed
64
65

    def forward(self, points_xyz, center_xyz, features=None):
zhangwenwei's avatar
zhangwenwei committed
66
        """forward.
wuyuefeng's avatar
wuyuefeng committed
67
68
69
70
71
72
73
74
75

        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.
        """
76
77
78
79
80
81
82
83
        # 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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101

        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)
102
103
        grouped_xyz_diff = grouped_xyz - \
            center_xyz.transpose(1, 2).unsqueeze(-1)  # relative offsets
wuyuefeng's avatar
wuyuefeng committed
104
        if self.normalize_xyz:
105
            grouped_xyz_diff /= self.max_radius
wuyuefeng's avatar
wuyuefeng committed
106
107
108
109
110

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

        ret = [new_features]
        if self.return_grouped_xyz:
            ret.append(grouped_xyz)
        if self.return_unique_cnt:
            ret.append(unique_cnt)
125
126
        if self.return_grouped_idx:
            ret.append(idx)
wuyuefeng's avatar
wuyuefeng committed
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
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
        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

    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.
187
            indices (Tensor): (B, npoint, nsample) the indices of
wuyuefeng's avatar
wuyuefeng committed
188
189
190
191
192
193
194
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
                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