Spaces:
Sleeping
Sleeping
| """ | |
| ImageDeepfakeDetector β EfficientNetV2-S + MTCNN for static image deepfake detection | |
| Detects facial inconsistencies, texture/pixel artifacts in images (JPG, PNG, WEBP). | |
| """ | |
| import torch | |
| import torch.nn as nn | |
| import numpy as np | |
| import cv2 | |
| from PIL import Image | |
| from pathlib import Path | |
| from typing import Optional, Tuple | |
| import torchvision.transforms as T | |
| try: | |
| import timm | |
| TIMM_AVAILABLE = True | |
| except ImportError: | |
| TIMM_AVAILABLE = False | |
| try: | |
| # facenet-pytorch works functionally even with version warning vs torch>=2.4 | |
| import warnings | |
| with warnings.catch_warnings(): | |
| warnings.simplefilter("ignore") | |
| from facenet_pytorch import MTCNN as FacenetMTCNN | |
| MTCNN_AVAILABLE = True | |
| except (ImportError, Exception): | |
| MTCNN_AVAILABLE = False | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Image pre-processing | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| MEAN = [0.485, 0.456, 0.406] | |
| STD = [0.229, 0.224, 0.225] | |
| inference_transform = T.Compose([ | |
| T.Resize((224, 224)), | |
| T.ToTensor(), | |
| T.Normalize(mean=MEAN, std=STD), | |
| ]) | |
| train_transform = T.Compose([ | |
| T.Resize((256, 256)), | |
| T.RandomCrop(224), | |
| T.RandomHorizontalFlip(), | |
| T.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.1), | |
| T.ToTensor(), | |
| T.Normalize(mean=MEAN, std=STD), | |
| ]) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # EfficientNetV2-S Backbone + Classifier Head | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class EfficientNetV2Detector(nn.Module): | |
| """ | |
| EfficientNetV2-S backbone for fake image detection. | |
| ImageNet-pretrained β fine-tuned on deepfake face datasets. | |
| Detects: | |
| - Facial boundary blending artifacts | |
| - Texture inconsistencies (blurriness, over-smoothing) | |
| - GAN frequency fingerprints in pixel space | |
| """ | |
| def __init__(self, pretrained: bool = True): | |
| super().__init__() | |
| if TIMM_AVAILABLE: | |
| # Use tf_efficientnetv2_s which has pretrained weights in all timm versions | |
| try: | |
| self.backbone = timm.create_model( | |
| "tf_efficientnetv2_s", | |
| pretrained=pretrained, | |
| num_classes=0, | |
| global_pool="avg", | |
| ) | |
| except Exception: | |
| self.backbone = timm.create_model( | |
| "efficientnet_b4", | |
| pretrained=pretrained, | |
| num_classes=0, | |
| global_pool="avg", | |
| ) | |
| self.feat_dim = self.backbone.num_features # 1280 | |
| else: | |
| # Fallback: EfficientNet-B0 via torchvision | |
| import torchvision.models as tv | |
| net = tv.efficientnet_b0(pretrained=pretrained) | |
| self.backbone = nn.Sequential( | |
| net.features, | |
| nn.AdaptiveAvgPool2d(1), | |
| nn.Flatten(), | |
| ) | |
| self.feat_dim = 1280 | |
| self.classifier = nn.Sequential( | |
| nn.Linear(self.feat_dim, 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, x: torch.Tensor) -> torch.Tensor: | |
| features = self.backbone(x) | |
| return self.classifier(features) # (B, 1) logits | |
| def predict_proba(self, x: torch.Tensor) -> torch.Tensor: | |
| with torch.no_grad(): | |
| return torch.sigmoid(self.forward(x)).squeeze(1) # (B,) | |
| def load(path: str, device: str = "cpu") -> "EfficientNetV2Detector": | |
| model = EfficientNetV2Detector(pretrained=False) | |
| state = torch.load(path, map_location=device, weights_only=True) | |
| model.load_state_dict(state) | |
| model.eval() | |
| return model | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # MTCNN Face Extractor Wrapper | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class MTCNNExtractor: | |
| """ | |
| MTCNN-based face extractor using facenet-pytorch. | |
| Localises and aligns faces before passing to the classifier. | |
| """ | |
| def __init__(self, device: str = "cpu"): | |
| if MTCNN_AVAILABLE: | |
| self.mtcnn = FacenetMTCNN( | |
| image_size=224, | |
| margin=30, | |
| keep_all=False, | |
| post_process=False, | |
| device=device, | |
| ) | |
| else: | |
| self.mtcnn = None | |
| print("[MTCNN] facenet-pytorch not installed β using full image fallback.") | |
| def extract(self, image_rgb: np.ndarray) -> Tuple[np.ndarray, bool]: | |
| """ | |
| Returns: | |
| face_rgb : (H, W, 3) uint8 numpy array β face crop or full image | |
| detected : bool β True if MTCNN found a face | |
| """ | |
| if self.mtcnn is None: | |
| return image_rgb, False | |
| pil_img = Image.fromarray(image_rgb) | |
| try: | |
| face_tensor = self.mtcnn(pil_img) # (C, H, W) float in [0, 255] | |
| if face_tensor is not None: | |
| face_np = face_tensor.permute(1, 2, 0).byte().numpy() | |
| return face_np, True | |
| except Exception: | |
| pass | |
| return image_rgb, False | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Full Image Detection Pipeline | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class ImageDeepfakeDetector: | |
| """ | |
| End-to-end pipeline: | |
| 1. Load image from disk | |
| 2. Detect & crop face with MTCNN | |
| 3. Classify with EfficientNetV2-S | |
| 4. Return verdict + confidence | |
| """ | |
| def __init__( | |
| self, | |
| model_path: Optional[str] = None, | |
| device: Optional[str] = None, | |
| threshold: float = 0.5, | |
| ): | |
| self.device = device or ("cuda" if torch.cuda.is_available() else "cpu") | |
| self.threshold = threshold | |
| # EfficientNetV2 model | |
| self.model = EfficientNetV2Detector(pretrained=(model_path is None)) | |
| if model_path and Path(model_path).exists(): | |
| state = torch.load(model_path, map_location=self.device, weights_only=True) | |
| self.model.load_state_dict(state) | |
| print(f"[ImageDetector] Loaded weights from {model_path}") | |
| else: | |
| print("[ImageDetector] Using ImageNet pretrained weights (demo mode).") | |
| self.model.to(self.device).eval() | |
| # MTCNN face extractor | |
| self.extractor = MTCNNExtractor(device=self.device) | |
| def analyze(self, image_path: str) -> dict: | |
| """Run deepfake detection on a single image file.""" | |
| # ββ Load ββββββββββββββββββββββββββββββββββββββββββββββ | |
| img_bgr = cv2.imread(str(image_path)) | |
| if img_bgr is None: | |
| return { | |
| "verdict": "ERROR", | |
| "error": "Cannot read image file.", | |
| "confidence": 0.0, | |
| "fake_prob": 0.0, | |
| "face_detected": False, | |
| "modality": "image", | |
| } | |
| img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) | |
| h, w = img_rgb.shape[:2] | |
| # ββ Face extraction βββββββββββββββββββββββββββββββββββ | |
| face_rgb, face_detected = self.extractor.extract(img_rgb) | |
| # ββ Pre-process & infer βββββββββββββββββββββββββββββββ | |
| pil_img = Image.fromarray(face_rgb) | |
| tensor = inference_transform(pil_img).unsqueeze(0).to(self.device) | |
| with torch.no_grad(): | |
| prob = self.model.predict_proba(tensor).item() | |
| verdict = "FAKE" if prob >= self.threshold else "REAL" | |
| return { | |
| "verdict": verdict, | |
| "confidence": round(prob, 4), | |
| "fake_prob": round(prob, 4), | |
| "faces_detected": 1 if face_detected else 0, | |
| "image_size": f"{w}Γ{h}", | |
| "modality": "image", | |
| } | |
| # ββ Quick sanity check ββββββββββββββββββββββββββββββββββββββββββββ | |
| if __name__ == "__main__": | |
| model = EfficientNetV2Detector(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("EfficientNetV2Detector OK β") | |