group_points.py 7.83 KB
Newer Older
1
# Copyright (c) OpenMMLab. All rights reserved.
2
3
from typing import Tuple

wuyuefeng's avatar
wuyuefeng committed
4
import torch
zhangwenwei's avatar
zhangwenwei committed
5
from torch import nn as nn
wuyuefeng's avatar
wuyuefeng committed
6
7
8
from torch.autograd import Function

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


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

    Groups with a ball query of radius

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

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

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

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

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

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

        ret = [new_features]
        if self.return_grouped_xyz:
            ret.append(grouped_xyz)
        if self.return_unique_cnt:
            ret.append(unique_cnt)
126
127
        if self.return_grouped_idx:
            ret.append(idx)
wuyuefeng's avatar
wuyuefeng committed
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
187
        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.
188
            indices (Tensor): (B, npoint, nsample) the indices of
wuyuefeng's avatar
wuyuefeng committed
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
230
                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