| import torch |
| import torch.nn as nn |
| import torchvision.ops as ops |
| from torchvision.models import resnet50, ResNet50_Weights |
|
|
| class DetectionHead(nn.Module): |
| def __init__(self, num_classes=20, pretrained=True): |
| super().__init__() |
|
|
| self.num_classes = num_classes |
|
|
| weights = ResNet50_Weights.DEFAULT if pretrained else None |
| self.conv5_x = resnet50(weights=weights).layer4 |
|
|
| self.fc_cls = nn.Linear(2048, num_classes + 1) |
| self.fc_reg = nn.Linear(2048, num_classes * 4) |
|
|
| self.fc_cls.weight.data.normal_(0, 0.01) |
| self.fc_reg.weight.data.normal_(0, 0.001) |
| self.fc_cls.bias.data.zero_() |
| self.fc_reg.bias.data.zero_() |
|
|
| for module in self.conv5_x.modules(): |
| if isinstance(module, nn.BatchNorm2d): |
| module.weight.requires_grad = False |
| module.bias.requires_grad = False |
|
|
| def train(self, mode=True): |
| super().train(mode) |
| |
| |
| |
| for module in self.conv5_x.modules(): |
| if isinstance(module, nn.BatchNorm2d): |
| module.eval() |
| |
| return self |
|
|
| def forward(self, pooled_batch): |
| |
| counts = [pooled.shape[0] for pooled in pooled_batch] |
|
|
| |
| |
| if sum(counts) == 0: |
| cls_logits = [pooled.new_zeros((0, self.num_classes + 1)) for pooled in pooled_batch] |
| box_deltas = [pooled.new_zeros((0, self.num_classes * 4)) for pooled in pooled_batch] |
| return cls_logits, box_deltas |
|
|
| |
| x = torch.cat(pooled_batch, dim=0) |
| x = self.conv5_x(x) |
| x = x.mean(dim=(2, 3)) |
|
|
| cls_logits = self.fc_cls(x) |
| box_deltas = self.fc_reg(x) |
|
|
| return list(cls_logits.split(counts)), list(box_deltas.split(counts)) |
|
|
|
|
| class DetectionLoss(nn.Module): |
| def __init__(self): |
| super().__init__() |
|
|
| def forward(self, feature_maps, batch_proposals, batch_gt_boxes, batch_gt_labels, |
| batch_img_height, batch_img_width, roi_pool, detection_head, |
| num_samples=64, pos_fraction=0.25, background_label=20): |
| |
| |
| |
| |
| device = feature_maps.device |
| batch_size = len(batch_proposals) |
|
|
| batch_sampled_proposals = [] |
| batch_labels = [] |
| batch_reg_targets = [] |
|
|
| |
| for i in range(batch_size): |
| |
| |
| |
| |
| proposals = batch_proposals[i].detach() |
| gt_boxes = batch_gt_boxes[i].to(device) |
| gt_labels = batch_gt_labels[i].to(device) |
|
|
| labels, matched_gt_idx = self.assign_proposal_labels(proposals, gt_boxes, gt_labels, background_label=background_label) |
| sampled_idx = self.sample_proposals(labels, num_samples, pos_fraction, background_label) |
|
|
| sampled_proposals = proposals[sampled_idx] |
| sampled_labels = labels[sampled_idx] |
|
|
| if gt_boxes.shape[0] == 0: |
| |
| |
| reg_targets = torch.zeros((sampled_idx.numel(), 4), dtype=sampled_proposals.dtype, device=device) |
| else: |
| matched_gt_boxes = gt_boxes[matched_gt_idx[sampled_idx]] |
| reg_targets = self.encode_box_targets(sampled_proposals, matched_gt_boxes) |
|
|
| batch_sampled_proposals.append(sampled_proposals) |
| batch_labels.append(sampled_labels) |
| batch_reg_targets.append(reg_targets) |
|
|
| pooled_batch = roi_pool(feature_maps, batch_sampled_proposals, batch_img_height, batch_img_width) |
| batch_cls_logits, batch_box_deltas = detection_head(pooled_batch) |
|
|
| total_cls_loss = 0.0 |
| total_reg_loss = 0.0 |
|
|
| for i in range(batch_size): |
| cls_logits = batch_cls_logits[i] |
| box_deltas = batch_box_deltas[i] |
| labels = batch_labels[i] |
| reg_targets = batch_reg_targets[i] |
|
|
| num_sampled = labels.numel() |
| if num_sampled == 0: |
| |
| continue |
|
|
| cls_loss = nn.functional.cross_entropy(cls_logits, labels, reduction='mean') |
|
|
| |
| positive_mask = labels != background_label |
|
|
| if positive_mask.sum() == 0: |
| |
| reg_loss = torch.tensor(0.0, dtype=box_deltas.dtype, device=device) |
| else: |
| |
| num_classes = box_deltas.shape[1] // 4 |
| positive_labels = labels[positive_mask] |
| positive_deltas = box_deltas[positive_mask].view(-1, num_classes, 4) |
| |
| |
| predicted_deltas = positive_deltas[torch.arange(positive_labels.numel(), device=device), positive_labels] |
|
|
| reg_loss = nn.functional.smooth_l1_loss(predicted_deltas, reg_targets[positive_mask], reduction='sum') |
|
|
| reg_loss = reg_loss / num_sampled |
|
|
| total_cls_loss += cls_loss |
| total_reg_loss += reg_loss |
|
|
| return total_cls_loss / batch_size, total_reg_loss / batch_size |
|
|
| def compute_iou_matrix_corners(self, boxes_a, boxes_b): |
| a_x1, a_y1, a_x2, a_y2 = boxes_a[:, 0], boxes_a[:, 1], boxes_a[:, 2], boxes_a[:, 3] |
| b_x1, b_y1, b_x2, b_y2 = boxes_b[:, 0], boxes_b[:, 1], boxes_b[:, 2], boxes_b[:, 3] |
|
|
| inter_x1 = torch.max(a_x1.unsqueeze(1), b_x1.unsqueeze(0)) |
| inter_y1 = torch.max(a_y1.unsqueeze(1), b_y1.unsqueeze(0)) |
| inter_x2 = torch.min(a_x2.unsqueeze(1), b_x2.unsqueeze(0)) |
| inter_y2 = torch.min(a_y2.unsqueeze(1), b_y2.unsqueeze(0)) |
|
|
| inter_area = torch.clamp(inter_x2 - inter_x1, min=0) * torch.clamp(inter_y2 - inter_y1, min=0) |
|
|
| area_a = (a_x2 - a_x1) * (a_y2 - a_y1) |
| area_b = (b_x2 - b_x1) * (b_y2 - b_y1) |
|
|
| union_area = area_a.unsqueeze(1) + area_b.unsqueeze(0) - inter_area |
|
|
| return inter_area / union_area |
|
|
| def assign_proposal_labels(self, proposals, gt_boxes, gt_labels, pos_iou_thresh=0.5, neg_iou_lo=0.1, background_label=20): |
| |
| |
| |
| |
| num_proposals = proposals.shape[0] |
|
|
| if gt_boxes.shape[0] == 0: |
| |
| labels = torch.full((num_proposals,), background_label, dtype=torch.long, device=proposals.device) |
| matched_gt_idx = torch.zeros((num_proposals,), dtype=torch.long, device=proposals.device) |
| return labels, matched_gt_idx |
|
|
| iou_matrix = self.compute_iou_matrix_corners(proposals, gt_boxes) |
| max_iou_per_proposal, matched_gt_idx = iou_matrix.max(dim=1) |
|
|
| labels = torch.full((num_proposals,), -1, dtype=torch.long, device=proposals.device) |
| labels[max_iou_per_proposal >= neg_iou_lo] = background_label |
|
|
| positive_mask = max_iou_per_proposal >= pos_iou_thresh |
| labels[positive_mask] = gt_labels[matched_gt_idx[positive_mask]] |
|
|
| return labels, matched_gt_idx |
|
|
| def sample_proposals(self, labels, num_samples=64, pos_fraction=0.25, background_label=20): |
| |
| positive_idx = torch.where((labels >= 0) & (labels != background_label))[0] |
| negative_idx = torch.where(labels == background_label)[0] |
|
|
| num_pos = min(int(num_samples * pos_fraction), positive_idx.numel()) |
| num_neg = min(num_samples - num_pos, negative_idx.numel()) |
|
|
| perm_pos = torch.randperm(positive_idx.numel(), device=labels.device)[:num_pos] |
| perm_neg = torch.randperm(negative_idx.numel(), device=labels.device)[:num_neg] |
|
|
| sampled_pos_idx = positive_idx[perm_pos] |
| sampled_neg_idx = negative_idx[perm_neg] |
|
|
| |
| return torch.cat([sampled_pos_idx, sampled_neg_idx]) |
|
|
| 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 encode_box_targets(self, proposals, gt_boxes, delta_std=(0.1, 0.1, 0.2, 0.2)): |
| proposals_centered = self.corners_to_center(proposals) |
| gt_centered = self.corners_to_center(gt_boxes) |
|
|
| p_xc, p_yc, p_w, p_h = proposals_centered[:, 0], proposals_centered[:, 1], proposals_centered[:, 2], proposals_centered[:, 3] |
| gt_xc, gt_yc, gt_w, gt_h = gt_centered[:, 0], gt_centered[:, 1], gt_centered[:, 2], gt_centered[:, 3] |
|
|
| target_dx = (gt_xc - p_xc) / p_w |
| target_dy = (gt_yc - p_yc) / p_h |
| target_dw = torch.log(gt_w / p_w) |
| target_dh = torch.log(gt_h / p_h) |
|
|
| target_deltas = torch.stack((target_dx, target_dy, target_dw, target_dh), dim=1) |
|
|
| |
| std = torch.tensor(delta_std, dtype=target_deltas.dtype, device=target_deltas.device) |
|
|
| |
| |
| return target_deltas / std |
|
|
|
|
| class DetectionNet(nn.Module): |
| def __init__(self, detection_head, background_label=20, delta_std=(0.1, 0.1, 0.2, 0.2), |
| score_thresh=0.05, nms_iou_thresh=0.5, max_detections_per_image=100, |
| min_box_size=1.0): |
| super().__init__() |
|
|
| self.detection_head = detection_head |
| self.background_label = background_label |
| self.delta_std = delta_std |
|
|
| |
| |
| |
| |
| self.score_thresh = score_thresh |
| self.nms_iou_thresh = nms_iou_thresh |
| self.max_detections_per_image = max_detections_per_image |
| self.min_box_size = min_box_size |
|
|
| 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 select_foreground(self, cls_logits): |
| |
| softmax_scores = nn.functional.softmax(cls_logits, dim=-1) |
| fg_scores = softmax_scores[:, :self.background_label] |
|
|
| proposal_idx, labels = (fg_scores > self.score_thresh).nonzero(as_tuple=True) |
| scores = fg_scores[proposal_idx, labels] |
|
|
| return proposal_idx, labels, scores |
|
|
| def decode_box_deltas(self, proposals_center, box_deltas): |
| |
| |
| std = torch.tensor(self.delta_std, dtype=box_deltas.dtype, device=box_deltas.device) |
| dx, dy, dw, dh = (box_deltas * std).unbind(dim=1) |
|
|
| xc_p, yc_p, w_p, h_p = proposals_center[:, 0], proposals_center[:, 1], proposals_center[:, 2], proposals_center[:, 3] |
|
|
| xc = dx * w_p + xc_p |
| yc = dy * h_p + yc_p |
| w = torch.exp(dw) * w_p |
| h = torch.exp(dh) * h_p |
|
|
| 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 |
|
|
| def clip_boxes_to_image(self, decoded_boxes, img_height, img_width): |
| |
| 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 forward(self, rpn_proposals, pooled_proposals, img_sizes_before_pad): |
| |
| |
| batch_cls_logits, batch_box_deltas = self.detection_head(pooled_proposals) |
|
|
| batch_size = len(rpn_proposals) |
|
|
| labels_list = [] |
| 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] |
| proposals = rpn_proposals[i] |
|
|
| proposal_idx, labels, scores = self.select_foreground(cls_logits) |
|
|
| if proposal_idx.numel() == 0: |
| |
| labels_list.append(labels) |
| scores_list.append(scores) |
| boxes_list.append(proposals.new_zeros((0, 4))) |
| continue |
|
|
| |
| num_classes = box_deltas.shape[1] // 4 |
| selected_proposals = proposals[proposal_idx] |
| predicted_deltas = box_deltas.view(-1, num_classes, 4)[proposal_idx, labels] |
|
|
| proposals_center = self.corners_to_center(selected_proposals) |
| decoded_boxes = self.decode_box_deltas(proposals_center, predicted_deltas) |
| clipped_boxes = self.clip_boxes_to_image(decoded_boxes, img_height, img_width) |
|
|
| |
| widths = clipped_boxes[:, 2] - clipped_boxes[:, 0] |
| heights = clipped_boxes[:, 3] - clipped_boxes[:, 1] |
| keep = (widths >= self.min_box_size) & (heights >= self.min_box_size) |
| clipped_boxes, labels, scores = clipped_boxes[keep], labels[keep], scores[keep] |
|
|
|
|
| if labels.numel() == 0: |
| labels_list.append(labels) |
| scores_list.append(scores) |
| boxes_list.append(clipped_boxes) |
| continue |
|
|
| |
| keep_idx = ops.batched_nms(clipped_boxes, scores, labels, self.nms_iou_thresh) |
| keep_idx = keep_idx[:self.max_detections_per_image] |
|
|
| labels_list.append(labels[keep_idx]) |
| scores_list.append(scores[keep_idx]) |
| boxes_list.append(clipped_boxes[keep_idx]) |
|
|
| return labels_list, scores_list, boxes_list |
|
|