File size: 16,747 Bytes
3e06e1c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
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
187
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
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
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
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
# Copyright (c) OpenMMLab. All rights reserved.
from typing import Optional, Tuple

import torch
from mmengine.structures import InstanceData
from torch import Tensor

from mmdet.registry import TASK_UTILS
from mmdet.utils import ConfigType
from .assign_result import AssignResult
from .base_assigner import BaseAssigner


def scale_boxes(bboxes: Tensor, scale: float) -> Tensor:
    """Expand an array of boxes by a given scale.

    Args:
        bboxes (Tensor): Shape (m, 4)
        scale (float): The scale factor of bboxes

    Returns:
        Tensor: Shape (m, 4). Scaled bboxes
    """
    assert bboxes.size(1) == 4
    w_half = (bboxes[:, 2] - bboxes[:, 0]) * .5
    h_half = (bboxes[:, 3] - bboxes[:, 1]) * .5
    x_c = (bboxes[:, 2] + bboxes[:, 0]) * .5
    y_c = (bboxes[:, 3] + bboxes[:, 1]) * .5

    w_half *= scale
    h_half *= scale

    boxes_scaled = torch.zeros_like(bboxes)
    boxes_scaled[:, 0] = x_c - w_half
    boxes_scaled[:, 2] = x_c + w_half
    boxes_scaled[:, 1] = y_c - h_half
    boxes_scaled[:, 3] = y_c + h_half
    return boxes_scaled


def is_located_in(points: Tensor, bboxes: Tensor) -> Tensor:
    """Are points located in bboxes.

    Args:
        points (Tensor): Points, shape: (m, 2).
        bboxes (Tensor): Bounding boxes, shape: (n, 4).

    Return:
        Tensor: Flags indicating if points are located in bboxes,
        shape: (m, n).
    """
    assert points.size(1) == 2
    assert bboxes.size(1) == 4
    return (points[:, 0].unsqueeze(1) > bboxes[:, 0].unsqueeze(0)) & \
           (points[:, 0].unsqueeze(1) < bboxes[:, 2].unsqueeze(0)) & \
           (points[:, 1].unsqueeze(1) > bboxes[:, 1].unsqueeze(0)) & \
           (points[:, 1].unsqueeze(1) < bboxes[:, 3].unsqueeze(0))


def bboxes_area(bboxes: Tensor) -> Tensor:
    """Compute the area of an array of bboxes.

    Args:
        bboxes (Tensor): The coordinates ox bboxes. Shape: (m, 4)

    Returns:
        Tensor: Area of the bboxes. Shape: (m, )
    """
    assert bboxes.size(1) == 4
    w = (bboxes[:, 2] - bboxes[:, 0])
    h = (bboxes[:, 3] - bboxes[:, 1])
    areas = w * h
    return areas


@TASK_UTILS.register_module()
class CenterRegionAssigner(BaseAssigner):
    """Assign pixels at the center region of a bbox as positive.

    Each proposals will be assigned with `-1`, `0`, or a positive integer
    indicating the ground truth index.
    - -1: negative samples
    - semi-positive numbers: positive sample, index (0-based) of assigned gt

    Args:
        pos_scale (float): Threshold within which pixels are
            labelled as positive.
        neg_scale (float): Threshold above which pixels are
            labelled as positive.
        min_pos_iof (float): Minimum iof of a pixel with a gt to be
            labelled as positive. Default: 1e-2
        ignore_gt_scale (float): Threshold within which the pixels
            are ignored when the gt is labelled as shadowed. Default: 0.5
        foreground_dominate (bool): If True, the bbox will be assigned as
            positive when a gt's kernel region overlaps with another's shadowed
            (ignored) region, otherwise it is set as ignored. Default to False.
        iou_calculator (:obj:`ConfigDict` or dict): Config of overlaps
            Calculator.
    """

    def __init__(
        self,
        pos_scale: float,
        neg_scale: float,
        min_pos_iof: float = 1e-2,
        ignore_gt_scale: float = 0.5,
        foreground_dominate: bool = False,
        iou_calculator: ConfigType = dict(type='BboxOverlaps2D')
    ) -> None:
        self.pos_scale = pos_scale
        self.neg_scale = neg_scale
        self.min_pos_iof = min_pos_iof
        self.ignore_gt_scale = ignore_gt_scale
        self.foreground_dominate = foreground_dominate
        self.iou_calculator = TASK_UTILS.build(iou_calculator)

    def get_gt_priorities(self, gt_bboxes: Tensor) -> Tensor:
        """Get gt priorities according to their areas.

        Smaller gt has higher priority.

        Args:
            gt_bboxes (Tensor): Ground truth boxes, shape (k, 4).

        Returns:
            Tensor: The priority of gts so that gts with larger priority is
            more likely to be assigned. Shape (k, )
        """
        gt_areas = bboxes_area(gt_bboxes)
        # Rank all gt bbox areas. Smaller objects has larger priority
        _, sort_idx = gt_areas.sort(descending=True)
        sort_idx = sort_idx.argsort()
        return sort_idx

    def assign(self,
               pred_instances: InstanceData,
               gt_instances: InstanceData,
               gt_instances_ignore: Optional[InstanceData] = None,
               **kwargs) -> AssignResult:
        """Assign gt to bboxes.

        This method assigns gts to every prior (proposal/anchor), each prior
        will be assigned with -1, or a semi-positive number. -1 means
        negative sample, semi-positive number is the index (0-based) of
        assigned gt.

        Args:
            pred_instances (:obj:`InstanceData`): Instances of model
                predictions. It includes ``priors``, and the priors can
                be anchors or points, or the bboxes predicted by the
                previous stage, has shape (n, 4). The bboxes predicted by
                the current model or stage will be named ``bboxes``,
                ``labels``, and ``scores``, the same as the ``InstanceData``
                in other places.
            gt_instances (:obj:`InstanceData`): Ground truth of instance
                annotations. It usually includes ``bboxes``, with shape (k, 4),
                and ``labels``, with shape (k, ).
            gt_instances_ignore (:obj:`InstanceData`, optional): Instances
                to be ignored during training. It includes ``bboxes``
                attribute data that is ignored during training and testing.
                Defaults to None.

        Returns:
            :obj:`AssignResult`: The assigned result. Note that shadowed_labels
            of shape (N, 2) is also added as an `assign_result` attribute.
            `shadowed_labels` is a tensor composed of N pairs of anchor_ind,
            class_label], where N is the number of anchors that lie in the
            outer region of a gt, anchor_ind is the shadowed anchor index
            and class_label is the shadowed class label.

        Example:
            >>> from mmengine.structures import InstanceData
            >>> self = CenterRegionAssigner(0.2, 0.2)
            >>> pred_instances.priors = torch.Tensor([[0, 0, 10, 10],
            ...                                      [10, 10, 20, 20]])
            >>> gt_instances = InstanceData()
            >>> gt_instances.bboxes = torch.Tensor([[0, 0, 10, 10]])
            >>> gt_instances.labels = torch.Tensor([0])
            >>> assign_result = self.assign(pred_instances, gt_instances)
            >>> expected_gt_inds = torch.LongTensor([1, 0])
            >>> assert torch.all(assign_result.gt_inds == expected_gt_inds)
        """
        # There are in total 5 steps in the pixel assignment
        # 1. Find core (the center region, say inner 0.2)
        #     and shadow (the relatively ourter part, say inner 0.2-0.5)
        #     regions of every gt.
        # 2. Find all prior bboxes that lie in gt_core and gt_shadow regions
        # 3. Assign prior bboxes in gt_core with a one-hot id of the gt in
        #      the image.
        #    3.1. For overlapping objects, the prior bboxes in gt_core is
        #           assigned with the object with smallest area
        # 4. Assign prior bboxes with class label according to its gt id.
        #    4.1. Assign -1 to prior bboxes lying in shadowed gts
        #    4.2. Assign positive prior boxes with the corresponding label
        # 5. Find pixels lying in the shadow of an object and assign them with
        #      background label, but set the loss weight of its corresponding
        #      gt to zero.

        # TODO not extract bboxes in assign.
        gt_bboxes = gt_instances.bboxes
        priors = pred_instances.priors
        gt_labels = gt_instances.labels

        assert priors.size(1) == 4, 'priors must have size of 4'
        # 1. Find core positive and shadow region of every gt
        gt_core = scale_boxes(gt_bboxes, self.pos_scale)
        gt_shadow = scale_boxes(gt_bboxes, self.neg_scale)

        # 2. Find prior bboxes that lie in gt_core and gt_shadow regions
        prior_centers = (priors[:, 2:4] + priors[:, 0:2]) / 2
        # The center points lie within the gt boxes
        is_prior_in_gt = is_located_in(prior_centers, gt_bboxes)
        # Only calculate prior and gt_core IoF. This enables small prior bboxes
        #   to match large gts
        prior_and_gt_core_overlaps = self.iou_calculator(
            priors, gt_core, mode='iof')
        # The center point of effective priors should be within the gt box
        is_prior_in_gt_core = is_prior_in_gt & (
            prior_and_gt_core_overlaps > self.min_pos_iof)  # shape (n, k)

        is_prior_in_gt_shadow = (
            self.iou_calculator(priors, gt_shadow, mode='iof') >
            self.min_pos_iof)
        # Rule out center effective positive pixels
        is_prior_in_gt_shadow &= (~is_prior_in_gt_core)

        num_gts, num_priors = gt_bboxes.size(0), priors.size(0)
        if num_gts == 0 or num_priors == 0:
            # If no gts exist, assign all pixels to negative
            assigned_gt_ids = \
                is_prior_in_gt_core.new_zeros((num_priors,),
                                              dtype=torch.long)
            pixels_in_gt_shadow = assigned_gt_ids.new_empty((0, 2))
        else:
            # Step 3: assign a one-hot gt id to each pixel, and smaller objects
            #    have high priority to assign the pixel.
            sort_idx = self.get_gt_priorities(gt_bboxes)
            assigned_gt_ids, pixels_in_gt_shadow = \
                self.assign_one_hot_gt_indices(is_prior_in_gt_core,
                                               is_prior_in_gt_shadow,
                                               gt_priority=sort_idx)

        if (gt_instances_ignore is not None
                and gt_instances_ignore.bboxes.numel() > 0):
            # No ground truth or boxes, return empty assignment
            gt_bboxes_ignore = gt_instances_ignore.bboxes
            gt_bboxes_ignore = scale_boxes(
                gt_bboxes_ignore, scale=self.ignore_gt_scale)
            is_prior_in_ignored_gts = is_located_in(prior_centers,
                                                    gt_bboxes_ignore)
            is_prior_in_ignored_gts = is_prior_in_ignored_gts.any(dim=1)
            assigned_gt_ids[is_prior_in_ignored_gts] = -1

        # 4. Assign prior bboxes with class label according to its gt id.
        # Default assigned label is the background (-1)
        assigned_labels = assigned_gt_ids.new_full((num_priors, ), -1)
        pos_inds = torch.nonzero(assigned_gt_ids > 0, as_tuple=False).squeeze()
        if pos_inds.numel() > 0:
            assigned_labels[pos_inds] = gt_labels[assigned_gt_ids[pos_inds] -
                                                  1]
        # 5. Find pixels lying in the shadow of an object
        shadowed_pixel_labels = pixels_in_gt_shadow.clone()
        if pixels_in_gt_shadow.numel() > 0:
            pixel_idx, gt_idx =\
                pixels_in_gt_shadow[:, 0], pixels_in_gt_shadow[:, 1]
            assert (assigned_gt_ids[pixel_idx] != gt_idx).all(), \
                'Some pixels are dually assigned to ignore and gt!'
            shadowed_pixel_labels[:, 1] = gt_labels[gt_idx - 1]
            override = (
                assigned_labels[pixel_idx] == shadowed_pixel_labels[:, 1])
            if self.foreground_dominate:
                # When a pixel is both positive and shadowed, set it as pos
                shadowed_pixel_labels = shadowed_pixel_labels[~override]
            else:
                # When a pixel is both pos and shadowed, set it as shadowed
                assigned_labels[pixel_idx[override]] = -1
                assigned_gt_ids[pixel_idx[override]] = 0

        assign_result = AssignResult(
            num_gts, assigned_gt_ids, None, labels=assigned_labels)
        # Add shadowed_labels as assign_result property. Shape: (num_shadow, 2)
        assign_result.set_extra_property('shadowed_labels',
                                         shadowed_pixel_labels)
        return assign_result

    def assign_one_hot_gt_indices(
            self,
            is_prior_in_gt_core: Tensor,
            is_prior_in_gt_shadow: Tensor,
            gt_priority: Optional[Tensor] = None) -> Tuple[Tensor, Tensor]:
        """Assign only one gt index to each prior box.

        Gts with large gt_priority are more likely to be assigned.

        Args:
            is_prior_in_gt_core (Tensor): Bool tensor indicating the prior
                center is in the core area of a gt (e.g. 0-0.2).
                Shape: (num_prior, num_gt).
            is_prior_in_gt_shadow (Tensor): Bool tensor indicating the prior
                center is in the shadowed area of a gt (e.g. 0.2-0.5).
                Shape: (num_prior, num_gt).
            gt_priority (Tensor): Priorities of gts. The gt with a higher
                priority is more likely to be assigned to the bbox when the
                bbox match with multiple gts. Shape: (num_gt, ).

        Returns:
            tuple: Returns (assigned_gt_inds, shadowed_gt_inds).

            - assigned_gt_inds: The assigned gt index of each prior bbox \
            (i.e. index from 1 to num_gts). Shape: (num_prior, ).
            - shadowed_gt_inds: shadowed gt indices. It is a tensor of \
            shape (num_ignore, 2) with first column being the shadowed prior \
            bbox indices and the second column the shadowed gt \
            indices (1-based).
        """
        num_bboxes, num_gts = is_prior_in_gt_core.shape

        if gt_priority is None:
            gt_priority = torch.arange(
                num_gts, device=is_prior_in_gt_core.device)
        assert gt_priority.size(0) == num_gts
        # The bigger gt_priority, the more preferable to be assigned
        # The assigned inds are by default 0 (background)
        assigned_gt_inds = is_prior_in_gt_core.new_zeros((num_bboxes, ),
                                                         dtype=torch.long)
        # Shadowed bboxes are assigned to be background. But the corresponding
        #   label is ignored during loss calculation, which is done through
        #   shadowed_gt_inds
        shadowed_gt_inds = torch.nonzero(is_prior_in_gt_shadow, as_tuple=False)
        if is_prior_in_gt_core.sum() == 0:  # No gt match
            shadowed_gt_inds[:, 1] += 1  # 1-based. For consistency issue
            return assigned_gt_inds, shadowed_gt_inds

        # The priority of each prior box and gt pair. If one prior box is
        #  matched bo multiple gts. Only the pair with the highest priority
        #  is saved
        pair_priority = is_prior_in_gt_core.new_full((num_bboxes, num_gts),
                                                     -1,
                                                     dtype=torch.long)

        # Each bbox could match with multiple gts.
        # The following codes deal with this situation
        # Matched  bboxes (to any gt). Shape: (num_pos_anchor, )
        inds_of_match = torch.any(is_prior_in_gt_core, dim=1)
        # The matched gt index of each positive bbox. Length >= num_pos_anchor
        #   , since one bbox could match multiple gts
        matched_bbox_gt_inds = torch.nonzero(
            is_prior_in_gt_core, as_tuple=False)[:, 1]
        # Assign priority to each bbox-gt pair.
        pair_priority[is_prior_in_gt_core] = gt_priority[matched_bbox_gt_inds]
        _, argmax_priority = pair_priority[inds_of_match].max(dim=1)
        assigned_gt_inds[inds_of_match] = argmax_priority + 1  # 1-based
        # Zero-out the assigned anchor box to filter the shadowed gt indices
        is_prior_in_gt_core[inds_of_match, argmax_priority] = 0
        # Concat the shadowed indices due to overlapping with that out side of
        #   effective scale. shape: (total_num_ignore, 2)
        shadowed_gt_inds = torch.cat(
            (shadowed_gt_inds,
             torch.nonzero(is_prior_in_gt_core, as_tuple=False)),
            dim=0)
        # Change `is_prior_in_gt_core` back to keep arguments intact.
        is_prior_in_gt_core[inds_of_match, argmax_priority] = 1
        # 1-based shadowed gt indices, to be consistent with `assigned_gt_inds`
        if shadowed_gt_inds.numel() > 0:
            shadowed_gt_inds[:, 1] += 1
        return assigned_gt_inds, shadowed_gt_inds