| import torch |
| import torch.nn as nn |
| from torchvision import models |
|
|
| class L2CS(nn.Module): |
| def __init__(self, backbone_name='resnet50', num_bins=90, pretrained=True): |
| """ |
| L2CS-Net Teacher Model Wrapper. |
| |
| Args: |
| backbone_name (str): ResNet variant to use ('resnet18', 'resnet34', 'resnet50'). |
| num_bins (int): Number of bins for pitch and yaw classification (default 90). |
| pretrained (bool): Whether to use ImageNet weights for the backbone. |
| """ |
| super(L2CS, self).__init__() |
| |
| |
| if backbone_name == 'resnet18': |
| self.backbone = models.resnet18(weights='IMAGENET1K_V1' if pretrained else None) |
| feat_dim = 512 |
| elif backbone_name == 'resnet34': |
| self.backbone = models.resnet34(weights='IMAGENET1K_V1' if pretrained else None) |
| feat_dim = 512 |
| else: |
| self.backbone = models.resnet50(weights='IMAGENET1K_V2' if pretrained else None) |
| feat_dim = 2048 |
| |
| |
| self.backbone = nn.Sequential(*(list(self.backbone.children())[:-1])) |
| |
| |
| self.fc_pitch = nn.Linear(feat_dim, num_bins) |
| self.fc_yaw = nn.Linear(feat_dim, num_bins) |
| |
| |
| self.num_bins = num_bins |
| |
| |
| |
| self.register_buffer('idx_tensor', torch.arange(num_bins).float()) |
|
|
| def forward(self, x): |
| """ |
| Forward pass. |
| Returns: |
| pitch_logits, yaw_logits: Raw scores for each bin. |
| """ |
| |
| features = self.backbone(x) |
| features = features.view(features.size(0), -1) |
| |
| |
| pitch_logits = self.fc_pitch(features) |
| yaw_logits = self.fc_yaw(features) |
| |
| return pitch_logits, yaw_logits |
|
|
| def get_angles(self, pitch_logits, yaw_logits): |
| """ |
| Convert logits to continuous angles (degrees) using soft-argmax (expectation). |
| Assumes bins represent -90 to 90 degrees with a 2-degree step for 90 bins. |
| """ |
| pitch_softmax = nn.functional.softmax(pitch_logits, dim=1) |
| yaw_softmax = nn.functional.softmax(yaw_logits, dim=1) |
| |
| |
| pitch_idx = torch.sum(pitch_softmax * self.idx_tensor, dim=1) |
| yaw_idx = torch.sum(yaw_softmax * self.idx_tensor, dim=1) |
| |
| |
| |
| pitch_deg = pitch_idx * 2 - 90 |
| yaw_deg = yaw_idx * 2 - 90 |
| |
| return pitch_deg, yaw_deg |
|
|
| def load_teacher_model(path=None, backbone='resnet50', num_bins=90, device='cpu'): |
| """ |
| Utility to load the teacher model with pre-trained weights. |
| """ |
| model = L2CS(backbone_name=backbone, num_bins=num_bins, pretrained=True) |
| if path: |
| |
| checkpoint = torch.load(path, map_location=device) |
| |
| state_dict = checkpoint.get('state_dict', checkpoint) |
| model.load_state_dict(state_dict, strict=False) |
| |
| model.to(device) |
| model.eval() |
| return model |
|
|