File size: 18,770 Bytes
6ca1e94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
import torch
import torch.nn as nn
import torchvision.ops as ops

class RPN_Head(nn.Module):
    def __init__(self, in_channels, mid_channels):
        super(RPN_Head, self).__init__()
        self.num_anchors = 9
        self.conv1 = nn.Conv2d(in_channels, mid_channels, kernel_size=3, stride=1, padding=1)
        self.conv_cls = nn.Conv2d(mid_channels, self.num_anchors * 2, kernel_size=1, stride=1)
        self.conv_reg = nn.Conv2d(mid_channels, self.num_anchors * 4, kernel_size=1, stride=1)


        self.conv1.weight.data.normal_(0, 0.01)
        self.conv_cls.weight.data.normal_(0, 0.01)
        self.conv_reg.weight.data.normal_(0, 0.01)
        self.conv1.bias.data.zero_()
        self.conv_cls.bias.data.zero_()
        self.conv_reg.bias.data.zero_()

        self.scales = torch.tensor([128, 256, 512])
        self.ratios = torch.tensor([0.5, 1, 2])

    def forward(self, feature_map, batch_img_height=None, batch_img_width=None):
        self.img_height = batch_img_height
        self.img_width = batch_img_width

        x = torch.relu(self.conv1(feature_map))
        batch_cls_logits = self.conv_cls(x).permute(0, 2, 3, 1)
        batch_box_deltas = self.conv_reg(x).permute(0, 2, 3, 1)

        batch_cls_logits = batch_cls_logits.reshape(batch_cls_logits.shape[0], -1, 2)
        batch_box_deltas = batch_box_deltas.reshape(batch_box_deltas.shape[0], -1, 4)

        batch_anchors = self.generate_anchors(feature_map)

        return batch_cls_logits, batch_box_deltas, batch_anchors

    def generate_anchors(self, feature_map):
        # This function should generate anchors based on the feature map size and predefined scales/aspect ratios

        batch_size, _, height, width = feature_map.shape

        # Calculating stride for the feature map relative to the input image
        total_stride_x = self.img_width // width   
        total_stride_y = self.img_height // height    

        # For each position in the feature map, calculate the center of the anchor box
        # center_x = (x_coord * total_stride_x) + (0.5 * total_stride_x)
        # center_y = (y_coord * total_stride_y) + (0.5 * total_stride_y)

        # Create a grid of center positions
        grid_x = torch.arange(width).float() + 0.5
        grid_y = torch.arange(height).float() + 0.5

        # Create a meshgrid of x_center, y_center positions for each anchor for each value of width and height in feature map
        grid_y, grid_x = torch.meshgrid(grid_y, grid_x, indexing='ij')      # (H, W) format

        # Converting grid positions to the scale from the feature map to the image size
        grid_x = grid_x * total_stride_x
        grid_y = grid_y * total_stride_y


        # Now, for each center position, we need to generate anchors based on the scales and aspect ratios
        widths = self.scales.view(-1, 1) * torch.sqrt(self.ratios).view(1, -1)
        heights = self.scales.view(-1, 1) / torch.sqrt(self.ratios).view(1, -1)


        centres = torch.stack((grid_x, grid_y), dim=-1).unsqueeze(dim=-2)  # Shape: [height, width, 1, 2]
        sizes = torch.stack((widths.flatten(), heights.flatten()), dim=-1)[None, None, : ]  # Shape: [1, 1, num_scales * num_ratios, 2]

        anchors = torch.cat((centres.expand(-1, -1, sizes.shape[2], -1), sizes.expand(centres.shape[0], centres.shape[1], -1, -1)), dim=-1)  # Shape: [height, width, num_anchors, 4]
        anchors = anchors.view(-1, 4)  # Flatten to [num_anchors_total, 4]
        
        batch_anchors = anchors.repeat(batch_size, 1, 1)  # Repeat for each image in the batch
        batch_anchors = batch_anchors.to(feature_map.device)  # Move to the same device as the feature map
        
        return batch_anchors 


class RPN_Loss(nn.Module):
    def __init__(self):
        super(RPN_Loss, self).__init__()

    def forward(self, batch_cls_logits, batch_box_deltas, batch_anchors, batch_gt_boxes, img_sizes_before_pad):
        batch_size = batch_cls_logits.shape[0]


        total_cls_loss = 0.0
        total_reg_loss = 0.0

        for i in range(batch_size):
            img_height, img_width = img_sizes_before_pad[i]

            cls_logits = batch_cls_logits[i]
            box_deltas = batch_box_deltas[i]
            anchors = batch_anchors[i]

            gt_boxes = batch_gt_boxes[i]
            gt_boxes = gt_boxes.to(anchors.device)  # Move gt_boxes to the same device as anchors

            inside_indices = self.anchors_inside_image(anchors, img_height, img_width)

            anchors = anchors[inside_indices]
            cls_logits = cls_logits[inside_indices]
            box_deltas = box_deltas[inside_indices]

            if gt_boxes.shape[0] == 0:
            # No objects in this image: every anchor is negative by definition,
            # there's nothing to regress toward.
                anchor_labels = torch.full((anchors.shape[0],), -1, dtype=torch.long, device=anchors.device)
                matched_gt_indices = torch.zeros((anchors.shape[0],), dtype=torch.long, device=anchors.device)  #unused placeholder
            else:
                anchor_labels, matched_gt_indices = self.anchor_labelling(anchors, gt_boxes)

            sample_mask, sampled_pos_idx, sampled_neg_idx = self.create_sample_mask_per_img(anchor_labels)

            cls_loss = self.cls_loss_fn_per_img(cls_logits, anchor_labels, sample_mask)

            if sampled_pos_idx.numel() == 0:
                reg_loss = torch.tensor(0.0, device=box_deltas.device, dtype=box_deltas.dtype)
            else:
                reg_loss = self.reg_loss_fn_per_img(box_deltas, gt_boxes, anchors, sampled_pos_idx, matched_gt_indices)

            total_cls_loss += cls_loss
            total_reg_loss += reg_loss

        return total_cls_loss / batch_size, total_reg_loss / batch_size  # Return average losses over the batch


    def cls_loss_fn_per_img(self, cls_logits, anchor_labels, sample_mask):
        selected_cls_logits = cls_logits[sample_mask]
        selected_anchor_labels = anchor_labels[sample_mask]

        targets = (selected_anchor_labels == 1).long()  # Convert to binary targets (1 for positive, 0 for negative)

        cls_loss = nn.functional.cross_entropy(selected_cls_logits, targets, reduction='mean')      # reduction='mean' to get the average loss over the selected samples i.e., divide by num_samples = Ncls
        return cls_loss
    
    def corners_to_center(self, boxes):
        x1, y1, x2, y2 = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
        w = x2 - x1
        h = y2 - y1
        x_c = x1 + w / 2
        y_c = y1 + h / 2
        return torch.stack([x_c, y_c, w, h], dim=1)


    def reg_loss_fn_per_img(self, box_deltas, gt_boxes, valid_anchors, sampled_pos_idx, matched_gt_indices, N_reg=2400):
        pred_box_deltas = box_deltas[sampled_pos_idx]
        selected_gt_boxes = gt_boxes[matched_gt_indices[sampled_pos_idx]]  # Select the corresponding gt_boxes for the sampled positive anchors
        anchors = valid_anchors[sampled_pos_idx]

        gt_boxes_centered = self.corners_to_center(selected_gt_boxes)

        xc_a, yc_a, w_a, h_a = anchors[:, 0], anchors[:, 1], anchors[:, 2], anchors[:, 3]
        gt_xc, gt_yc, gt_w, gt_h = gt_boxes_centered[:, 0], gt_boxes_centered[:, 1], gt_boxes_centered[:, 2], gt_boxes_centered[:, 3]

        # Calculate the regression targets (deltas) for the selected anchors
        target_dx = (gt_xc - xc_a) / w_a
        target_dy = (gt_yc - yc_a) / h_a
        target_dw = torch.log(gt_w / w_a)
        target_dh = torch.log(gt_h / h_a)

        target_box_deltas = torch.stack((target_dx, target_dy, target_dw, target_dh), dim=1)

        reg_loss = nn.functional.smooth_l1_loss(pred_box_deltas, target_box_deltas, reduction='sum')
        reg_loss /= N_reg
        return reg_loss

    def create_sample_mask_per_img(self, anchor_labels, num_samples=256, pos_fraction=0.5):
        positive_idx = torch.where(anchor_labels == 1)[0]       # ...[0] as it is a single valued tuple
        negative_idx = torch.where(anchor_labels == -1)[0]

        num_pos = min(int(num_samples * pos_fraction), positive_idx.numel())    # numel() returns the number of elements in the tensor
        num_neg = min(num_samples - num_pos, negative_idx.numel())

        perm_pos = torch.randperm(positive_idx.numel(), device=anchor_labels.device)[:num_pos]
        perm_neg = torch.randperm(negative_idx.numel(), device=anchor_labels.device)[:num_neg]

        sampled_pos_idx = positive_idx[perm_pos]
        sampled_neg_idx = negative_idx[perm_neg]

        # Ignoring i.e., making the anchor labels of the unsampled anchors to 0 (ignore)
        sample_mask = torch.zeros_like(anchor_labels, dtype=torch.bool, device=anchor_labels.device)
        sample_mask[sampled_pos_idx] = True
        sample_mask[sampled_neg_idx] = True

        return sample_mask, sampled_pos_idx, sampled_neg_idx

    def anchor_labelling(self, anchors, gt_boxes, pos_iou_threshold=0.7, neg_iou_threshold=0.3):
        iou_matrix = self.compute_iou_matrix(anchors, gt_boxes)

        num_anchors, num_gt_boxes = iou_matrix.shape
        labels = torch.zeros((num_anchors,), dtype=torch.long, device=anchors.device)  # Initialize all labels to 0 (ignore)

        max_iou_per_anchor, matched_gt_indices = iou_matrix.max(dim=1)

        labels[max_iou_per_anchor < neg_iou_threshold] = -1  # Negative labels
        labels[max_iou_per_anchor >= pos_iou_threshold] = 1  # Positive labels

        max_iou_per_gt, matched_anchor_indices = iou_matrix.max(dim=0)

        labels[matched_anchor_indices] = 1  # Ensure each gt box has at least one positive anchor

        return labels, matched_gt_indices

    def anchors_inside_image(self, anchors, img_height, img_width):
        # Check if anchors are inside the image boundaries
        xc, yc, w, h = anchors[:, 0], anchors[:, 1], anchors[:, 2], anchors[:, 3]

        x1 = xc - w / 2
        y1 = yc - h / 2
        x2 = xc + w / 2
        y2 = yc + h / 2

        inside_indices = (0<=x1) & (0<=y1) & (x2 <= img_width-1) & (y2 <= img_height-1)           # anchors[:, 0] is xc, anchors[:, 1] is yc, anchors[:, 2] is w, anchors[:, 3] is h
        return inside_indices
    
    def compute_iou_matrix(self, anchors, gt_boxes):
        # Compute the IoU matrix between anchors and ground truth boxes
        # anchors: [num_anchors, 4], gt_boxes: [num_gt_boxes, 4]
        # Returns: IoU matrix of shape [num_anchors, num_gt_boxes]
        
        # Convert anchors and gt_boxes to (x1, y1, x2, y2) format
        anchor_x1 = anchors[:, 0] - anchors[:, 2] / 2
        anchor_y1 = anchors[:, 1] - anchors[:, 3] / 2
        anchor_x2 = anchors[:, 0] + anchors[:, 2] / 2
        anchor_y2 = anchors[:, 1] + anchors[:, 3] / 2
        
        gt_x1 = gt_boxes[:, 0]
        gt_y1 = gt_boxes[:, 1]
        gt_x2 = gt_boxes[:, 2]
        gt_y2 = gt_boxes[:, 3]
        
        # Calculate intersection
        inter_x1 = torch.max(anchor_x1.unsqueeze(1), gt_x1.unsqueeze(0))        # Unsqueeze to align dimensions for broadcasting
        inter_y1 = torch.max(anchor_y1.unsqueeze(1), gt_y1.unsqueeze(0))
        inter_x2 = torch.min(anchor_x2.unsqueeze(1), gt_x2.unsqueeze(0))
        inter_y2 = torch.min(anchor_y2.unsqueeze(1), gt_y2.unsqueeze(0))
        
        inter_area = torch.clamp(inter_x2 - inter_x1, min=0) * torch.clamp(inter_y2 - inter_y1, min=0)
        
        # Calculate union
        anchor_area = (anchor_x2 - anchor_x1) * (anchor_y2 - anchor_y1)
        gt_area = (gt_x2 - gt_x1) * (gt_y2 - gt_y1)
        
        union_area = anchor_area.unsqueeze(dim=1) + gt_area.unsqueeze(dim=0) - inter_area           # Unsqueeze to align dimensions for broadcasting
        
        # Compute IoU
        iou_matrix = inter_area / union_area
        
        return iou_matrix


import torchvision.ops as ops

class RegionProposalNetwork(nn.Module):
    def __init__(self, rpn_head=None,  in_channels=1024, mid_channels=512):
        super(RegionProposalNetwork, self).__init__()
        if rpn_head is not None:
            self.rpn_head = rpn_head
        else:
            self.rpn_head = RPN_Head(in_channels, mid_channels)

    def forward(self, feature_map, batch_img_height=None,  batch_img_width=None, img_sizes_before_pad=None, pre_nms_top_n=6000, post_nms_top_n=300):
        # pre_nms_top_n: proposals kept per image (by score) before NMS.
        # post_nms_top_n: proposals kept per image after NMS. Paper uses 2000 for
        # Step-2 Fast R-CNN training and 300 for inference/eval; defaults to 300.
        batch_cls_logits, batch_box_deltas, batch_anchors = self.rpn_head(feature_map, batch_img_height=batch_img_height, batch_img_width=batch_img_width)

        batch_size = batch_cls_logits.shape[0]

        scores_list = []
        boxes_list = []

        for i in range(batch_size):
            img_height, img_width = img_sizes_before_pad[i]

            cls_logits = batch_cls_logits[i]
            box_deltas = batch_box_deltas[i]
            anchors = batch_anchors[i]

            scores = nn.functional.softmax(cls_logits, dim=-1)[:, 1]  # Get the foreground class scores for each box

            decoded_boxes = self.decode_box_deltas(anchors, box_deltas)

            clipped_boxes = self.clip_boxes_to_image(decoded_boxes, img_height, img_width)

            filtered_boxes, filtered_scores = self.filter_small_boxes(scores, clipped_boxes, min_size=16)

            if filtered_boxes.shape[0] == 0:
                # No proposals survived filtering for this image — return empty, well-shaped tensors
                scores_list.append(torch.zeros((0,), device=feature_map.device))        # shape (0,) to not break further tensor operations in the pipeline
                boxes_list.append(torch.zeros((0, 4), device=feature_map.device))        # shape (0, 4) to not break further tensor operations in the pipeline
                continue

            pre_nms_top_n_boxes, pre_nms_top_n_scores = self.pre_nms_top_n(filtered_boxes, filtered_scores, pre_nms_top_n=pre_nms_top_n)
            post_nms_indices = self.nms(pre_nms_top_n_boxes, pre_nms_top_n_scores, iou_threshold=0.7)
            post_nms_top_n_boxes, post_nms_top_n_scores = self.post_nms_top_n(pre_nms_top_n_boxes, pre_nms_top_n_scores, post_nms_indices, post_nms_top_n=post_nms_top_n)

            scores_list.append(post_nms_top_n_scores)
            boxes_list.append(post_nms_top_n_boxes)

        return scores_list, boxes_list

    # Decoding box deltas to get the final bounding boxes per img
    def decode_box_deltas(self, anchors, box_deltas):
        # Decode the predicted box deltas to get the final bounding boxes
        # anchors: [num_anchors, 4], box_deltas: [num_anchors, 4]
        # Returns: decoded_boxes of shape [num_anchors, 4]

        xc_a, yc_a, w_a, h_a = anchors[:, 0], anchors[:, 1], anchors[:, 2], anchors[:, 3]
        dx, dy, dw, dh = box_deltas[:, 0], box_deltas[:, 1], box_deltas[:, 2], box_deltas[:, 3]

        # Apply the inverse of the encoding transformation
        xc = dx * w_a + xc_a
        yc = dy * h_a + yc_a
        w = torch.exp(dw) * w_a
        h = torch.exp(dh) * h_a

        # Convert back to (xmin, ymin, xmax, ymax) format
        xmin = xc - w / 2
        ymin = yc - h / 2
        xmax = xc + w / 2
        ymax = yc + h / 2

        decoded_boxes = torch.stack((xmin, ymin, xmax, ymax), dim=1)
        return decoded_boxes
    
    # Clip the decoded boxes to ensure they are within the image boundaries per img
    def clip_boxes_to_image(self, decoded_boxes, img_height, img_width):
        # Clip the decoded boxes to ensure they are within the image boundaries
        # decoded_boxes: [num_anchors, 4]
        # Returns: clipped_boxes of shape [num_anchors, 4]

        xmin = torch.clamp(decoded_boxes[:, 0], min=0, max=img_width - 1)
        ymin = torch.clamp(decoded_boxes[:, 1], min=0, max=img_height - 1)
        xmax = torch.clamp(decoded_boxes[:, 2], min=0, max=img_width - 1)
        ymax = torch.clamp(decoded_boxes[:, 3], min=0, max=img_height - 1)

        clipped_boxes = torch.stack((xmin, ymin, xmax, ymax), dim=1)
        return clipped_boxes
    
    def filter_small_boxes(self, scores, decoded_boxes, min_size=16):
        # Filter out boxes that are smaller than a specified minimum size
        # decoded_boxes: [num_anchors, 4]
        # Returns: filtered_boxes of shape [num_filtered_anchors, 4]

        widths = decoded_boxes[:, 2] - decoded_boxes[:, 0]
        heights = decoded_boxes[:, 3] - decoded_boxes[:, 1]

        keep_indices = (widths >= min_size) & (heights >= min_size)
        filtered_boxes = decoded_boxes[keep_indices]

        # also need to remove the scores corresponding to the filtered boxes, but that will be handled in the pre_nms_top_n function
        filtered_scores = scores[keep_indices]  # Assuming scores is available in the context where this function is called

        return filtered_boxes, filtered_scores

    # Get the top N boxes based on their scores before applying NMS per img
    def pre_nms_top_n(self, decoded_boxes, scores, pre_nms_top_n=6000):
        # Select the top N boxes based on their scores before applying NMS
        # decoded_boxes: [num_anchors, 4], scores: [num_anchors]
        # Returns: top_decoded_boxes of shape [pre_nms_top_n, 4], top_scores of shape [pre_nms_top_n]

        # scores = nn.functional.softmax(scores, dim=-1)[:1]  # Get the foreground class scores for each box, since 1:obj, 0: background, therefore [:1] gives the foreground class scores for each box

        top_scores, top_indices = torch.topk(scores, k=min(pre_nms_top_n, scores.size(0)))
        top_decoded_boxes = decoded_boxes[top_indices]

        return top_decoded_boxes, top_scores
    
    def nms(self, decoded_boxes, scores, iou_threshold=0.7):
        # Apply Non-Maximum Suppression (NMS) to remove overlapping boxes
        # decoded_boxes: [num_anchors, 4], scores: [num_anchors]
        # Returns: keep_indices of shape [num_kept_anchors]

        keep_indices = ops.nms(decoded_boxes, scores, iou_threshold)
        return keep_indices
    
    def post_nms_top_n(self, bboxes, scores, keep_indices, post_nms_top_n=2000):
        # Select the top N boxes after applying NMS
        # decoded_boxes_post_nms: [num_kept_anchors, 4], scores_post_nms: [num_kept_anchors]
        # Returns: final_decoded_boxes of shape [post_nms_top_n, 4], final_scores of shape [post_nms_top_n]

        scores_post_nms = scores[keep_indices]
        boxes_post_nms = bboxes[keep_indices]

        top_scores_post_nms, top_indices_post_nms = torch.topk(scores_post_nms, k=min(post_nms_top_n, scores_post_nms.size(0)))
        final_decoded_boxes = boxes_post_nms[top_indices_post_nms]

        return final_decoded_boxes, top_scores_post_nms