Spaces:
Sleeping
Sleeping
| """ | |
| HybridDeepfakeDetector β Dual-branch Spatial + Frequency CNN | |
| Architecture for IEEE Research Paper on Deepfake Video Detection | |
| """ | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| try: | |
| import timm | |
| TIMM_AVAILABLE = True | |
| except ImportError: | |
| TIMM_AVAILABLE = False | |
| import torchvision.models as tv_models | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # Frequency Analysis Branch | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| class FrequencyBranch(nn.Module): | |
| """ | |
| Extracts GAN fingerprint artifacts from the frequency domain. | |
| GAN generators leave periodic patterns in the DCT/FFT spectrum | |
| that are invisible to the human eye but detectable by CNNs. | |
| """ | |
| def __init__(self, out_dim: int = 128): | |
| super().__init__() | |
| self.conv_layers = nn.Sequential( | |
| nn.Conv2d(1, 32, kernel_size=3, padding=1, bias=False), | |
| nn.BatchNorm2d(32), | |
| nn.ReLU(inplace=True), | |
| nn.MaxPool2d(2), # 112x112 | |
| nn.Conv2d(32, 64, kernel_size=3, padding=1, bias=False), | |
| nn.BatchNorm2d(64), | |
| nn.ReLU(inplace=True), | |
| nn.MaxPool2d(2), # 56x56 | |
| nn.Conv2d(64, 128, kernel_size=3, padding=1, bias=False), | |
| nn.BatchNorm2d(128), | |
| nn.ReLU(inplace=True), | |
| nn.MaxPool2d(2), # 28x28 | |
| nn.Conv2d(128, out_dim, kernel_size=3, padding=1, bias=False), | |
| nn.BatchNorm2d(out_dim), | |
| nn.ReLU(inplace=True), | |
| nn.AdaptiveAvgPool2d((1, 1)), # 1x1 | |
| ) | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| # Convert to grayscale: (B, 1, H, W) | |
| gray = 0.299 * x[:, 0:1] + 0.587 * x[:, 1:2] + 0.114 * x[:, 2:3] | |
| # 2D FFT β log-magnitude spectrum | |
| fft = torch.fft.fft2(gray) | |
| magnitude = torch.abs(fft) | |
| magnitude = torch.log(magnitude + 1e-8) | |
| # Normalize per sample | |
| b = magnitude.shape[0] | |
| m = magnitude.view(b, -1) | |
| mn = m.mean(dim=1, keepdim=True).view(b, 1, 1, 1) | |
| std = m.std(dim=1, keepdim=True).view(b, 1, 1, 1) + 1e-8 | |
| magnitude = (magnitude - mn) / std | |
| return self.conv_layers(magnitude).flatten(1) # (B, out_dim) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # Spatial Branch (EfficientNet-B0 backbone) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| class SpatialBranch(nn.Module): | |
| def __init__(self, pretrained: bool = True): | |
| super().__init__() | |
| if TIMM_AVAILABLE: | |
| self.backbone = timm.create_model( | |
| "efficientnet_b0", | |
| pretrained=pretrained, | |
| num_classes=0, # Remove classifier head | |
| global_pool="avg", | |
| ) | |
| self.out_dim = self.backbone.num_features # 1280 | |
| else: | |
| # Fallback: MobileNetV3-Small from torchvision | |
| backbone = tv_models.mobilenet_v3_small(pretrained=pretrained) | |
| self.backbone = nn.Sequential(*list(backbone.children())[:-2], | |
| nn.AdaptiveAvgPool2d(1), nn.Flatten()) | |
| self.out_dim = 576 | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| return self.backbone(x) # (B, out_dim) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # Fusion Classifier | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| class FusionClassifier(nn.Module): | |
| def __init__(self, spatial_dim: int, freq_dim: int): | |
| super().__init__() | |
| combined = spatial_dim + freq_dim | |
| self.fc = nn.Sequential( | |
| nn.Linear(combined, 512), | |
| nn.BatchNorm1d(512), | |
| nn.ReLU(inplace=True), | |
| nn.Dropout(0.5), | |
| nn.Linear(512, 128), | |
| nn.ReLU(inplace=True), | |
| nn.Dropout(0.3), | |
| nn.Linear(128, 1), | |
| ) | |
| def forward(self, spatial_feat, freq_feat): | |
| x = torch.cat([spatial_feat, freq_feat], dim=1) | |
| return self.fc(x) # (B, 1) β raw logits | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # HybridDeepfakeDetector (Main Model) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| class HybridDeepfakeDetector(nn.Module): | |
| """ | |
| Novel dual-branch architecture combining: | |
| - Spatial branch (EfficientNet-B0): captures texture/semantic artifacts | |
| - Frequency branch (FFT-CNN): captures GAN frequency fingerprints | |
| Fused via FC layers for binary Real/Fake classification. | |
| """ | |
| def __init__(self, pretrained: bool = True, freq_dim: int = 128): | |
| super().__init__() | |
| self.spatial = SpatialBranch(pretrained=pretrained) | |
| self.freq = FrequencyBranch(out_dim=freq_dim) | |
| self.fusion = FusionClassifier(self.spatial.out_dim, freq_dim) | |
| def forward(self, x: torch.Tensor): | |
| s = self.spatial(x) | |
| f = self.freq(x) | |
| return self.fusion(s, f) # (B, 1) logits | |
| def predict_proba(self, x: torch.Tensor) -> torch.Tensor: | |
| """Returns fake probability in [0, 1].""" | |
| with torch.no_grad(): | |
| logits = self.forward(x) | |
| return torch.sigmoid(logits).squeeze(1) # (B,) | |
| def load(path: str, device: str = "cpu") -> "HybridDeepfakeDetector": | |
| model = HybridDeepfakeDetector(pretrained=False) | |
| state = torch.load(path, map_location=device) | |
| model.load_state_dict(state) | |
| model.eval() | |
| return model | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # Quick sanity check | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| if __name__ == "__main__": | |
| model = HybridDeepfakeDetector(pretrained=False) | |
| dummy = torch.randn(4, 3, 224, 224) | |
| out = model.predict_proba(dummy) | |
| total = sum(p.numel() for p in model.parameters()) | |
| print(f"Output shape : {out.shape}") | |
| print(f"Total params : {total:,}") | |
| print("Model OK β") | |