deepshield-api / backend /image_detector.py
Venkatkalyan21
Deploy clean backend to Hugging Face
1970a5b
Raw
History Blame Contribute Delete
9.83 kB
"""
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,)
@staticmethod
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 βœ“")