aideepfake / scripts /deepfake_classifier.py
GitHub Action
Deploy backend to Hugging Face Space
887f5f0
Raw
History Blame Contribute Delete
11.4 kB
import os
import numpy as np
import cv2
import torch
import torch.nn.functional as F
from PIL import Image
# Keep HF model downloads inside the repo instead of polluting the user's C: drive cache
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
os.environ.setdefault("HF_HOME", os.path.join(base_dir, "..", "hf_cache"))
if os.name == "nt":
os.environ["HF_HUB_DISABLE_SYMLINKS_WARNING"] = "1"
os.environ["HF_HUB_DISABLE_SYMLINKS"] = "1"
from transformers import AutoImageProcessor, AutoModelForImageClassification
# Face-forgery specialist: ViT fine-tuned on real vs deepfake face datasets.
FACE_MODEL_ID = "prithivMLmods/Deep-Fake-Detector-v2-Model"
# General AI-image generalist: Swin fine-tuned to catch diffusion-generated imagery
# (Midjourney/SDXL/etc.), not limited to faces. Complements the face specialist above.
GENERAL_MODEL_ID = None # "Organika/sdxl-detector" (Hanging on local Windows)
class DeepfakeClassifier:
"""
Classifies images/frames/face-crops as Real or AI-generated using an ensemble of
two pretrained transformer models:
1. A face-forgery specialist (ViT) for classic deepfake/face-swap detection.
2. A general AI-generated-image detector (Swin) for diffusion-model content
(Midjourney, Stable Diffusion, DALL-E, etc.) that isn't necessarily a face.
Falls back to pixel-level forensic heuristics if the models fail to load.
"""
def __init__(self, use_gpu: bool = True):
self.device = torch.device("cuda" if (use_gpu and torch.cuda.is_available()) else "cpu")
self.face_model = None
self.face_processor = None
self.face_fake_idx = 1
self.general_model = None
self.general_processor = None
self.general_fake_idx = 0
try:
print(f"Loading face-forgery model '{FACE_MODEL_ID}' on {self.device}...")
self.face_processor = AutoImageProcessor.from_pretrained(FACE_MODEL_ID)
self.face_model = AutoModelForImageClassification.from_pretrained(FACE_MODEL_ID)
self.face_model.to(self.device).eval()
self.face_fake_idx = self._find_fake_index(self.face_model.config.id2label)
print("Face-forgery model loaded successfully!")
except Exception as e:
print(f"Warning: could not load face-forgery model: {e}")
self.face_model = None
try:
print(f"Loading general AI-image model '{GENERAL_MODEL_ID}' on {self.device}...")
self.general_processor = AutoImageProcessor.from_pretrained(GENERAL_MODEL_ID)
self.general_model = AutoModelForImageClassification.from_pretrained(GENERAL_MODEL_ID)
self.general_model.to(self.device).eval()
self.general_fake_idx = self._find_fake_index(self.general_model.config.id2label)
print("General AI-image model loaded successfully!")
except Exception as e:
print(f"Warning: could not load general AI-image model: {e}")
self.general_model = None
self.model_loaded = self.face_model is not None or self.general_model is not None
if not self.model_loaded:
print("No deep learning models available. Falling back to forensic heuristics only.")
@staticmethod
def _find_fake_index(id2label: dict) -> int:
"""Finds the class index representing 'fake/AI-generated' from a model's id2label map."""
for idx, label in id2label.items():
l = str(label).lower()
if any(k in l for k in ("fake", "artificial", "synthetic", "generated", "ai")):
return int(idx)
return 1
def _run_model(self, model, processor, fake_idx: int, image_rgb: np.ndarray) -> float:
pil_img = Image.fromarray(image_rgb).convert("RGB")
inputs = processor(images=pil_img, return_tensors="pt")
inputs = {k: v.to(self.device) for k, v in inputs.items()}
with torch.no_grad():
outputs = model(**inputs)
probs = F.softmax(outputs.logits, dim=-1).cpu().numpy()[0]
return float(probs[fake_idx])
def analyze_video_sequence(self, frames_list) -> float:
"""
Runs the general AI-image model over a subsample of raw RGB video frames and
returns the mean fake probability. Used as a whole-frame signal to complement
the per-face analysis (catches AI-generated video content with no detectable face).
"""
if self.general_model is None or not frames_list:
return None
try:
seq_length = 12
total = len(frames_list)
step = max(1, total // seq_length)
indices = list(range(0, total, step))[:seq_length]
scores = []
for i in indices:
try:
scores.append(self._run_model(self.general_model, self.general_processor, self.general_fake_idx, frames_list[i]))
except Exception as e:
print(f"Error scoring frame {i} in video sequence: {e}")
if not scores:
return None
return float(np.mean(scores))
except Exception as e:
print(f"Error during video sequence inference: {e}")
return None
def analyze_face(self, face_rgb: np.ndarray, is_face: bool = True) -> dict:
"""
Runs deepfake classification and calculates pixel-level artifacts.
`is_face` should be True only when face_rgb is an actual detected face crop;
pass False for whole-frame images (e.g. no face found), since the face-forgery
specialist is unreliable outside its face-crop training domain.
Returns a dict with scores and metadata.
"""
if face_rgb.size == 0:
return {"fake_score": 0.5, "is_fake": False, "confidence": 0.5, "heuristics": {}}
# 1. Compute pixel-level heuristics (forensics)
heuristics = self._compute_heuristics(face_rgb)
# 2. Run the pretrained model ensemble
face_score = None
general_score = None
if is_face and self.face_model is not None:
try:
face_score = self._run_model(self.face_model, self.face_processor, self.face_fake_idx, face_rgb)
except Exception as e:
print(f"Error during face-forgery model inference: {e}")
if self.general_model is not None:
try:
general_score = self._run_model(self.general_model, self.general_processor, self.general_fake_idx, face_rgb)
except Exception as e:
print(f"Error during general AI-image model inference: {e}")
if face_score is not None and general_score is not None:
# Face specialist is weighted more heavily on actual face crops; the general
# detector is the far more reliable signal outside of that domain.
deep_learning_score = 0.6 * face_score + 0.4 * general_score
elif general_score is not None:
deep_learning_score = general_score
elif face_score is not None:
deep_learning_score = face_score
else:
# No deep model available: fall back to pure forensic heuristics (no randomness).
deep_learning_score = (
0.4 * heuristics["blur_artifact_score"] +
0.4 * heuristics["frequency_anomaly_score"] +
0.2 * heuristics["color_anomaly_score"]
)
# Final score aggregation
fake_score = round(float(deep_learning_score), 4)
is_fake = fake_score > 0.5
confidence = round(fake_score if is_fake else (1.0 - fake_score), 4)
return {
"fake_score": fake_score,
"is_fake": is_fake,
"confidence": confidence,
"heuristics": heuristics,
"used_vit_model": self.model_loaded
}
def _compute_heuristics(self, face_rgb: np.ndarray) -> dict:
"""
Computes pixel-level forensic metrics:
- Blurriness (Laplacian Variance): Deepfakes often have blurred/blended boundaries.
- High Frequency Energy (FFT): Deepfakes display spectral anomalies/smoothing.
- Color Anomaly (Skin tone variance/histogram outliers).
"""
# Convert to grayscale
gray = cv2.cvtColor(face_rgb, cv2.COLOR_RGB2GRAY)
# A. Laplacian Blur detection
# Variance of Laplacian represents sharpness. Low variance = blurry/synthetic blending
laplacian = cv2.Laplacian(gray, cv2.CV_64F)
lap_var = float(laplacian.var())
# Map lap_var to an artifact score between 0 and 1 (lower sharpness -> higher score)
# Standard faces are sharp (var > 150), values < 80 are blurry
blur_artifact = max(0.0, min(1.0, 1.0 - (lap_var / 200.0)))
# B. Spectral (FFT) analysis
# Compute 2D Fourier Transform
f = np.fft.fft2(gray)
fshift = np.fft.fftshift(f)
magnitude_spectrum = 20 * np.log(np.abs(fshift) + 1e-5)
# Analyze center (low frequency) vs outer boundaries (high frequency)
h, w = gray.shape
cy, cx = h // 2, w // 2
# Define mask for low frequency (center area)
r = min(h, w) // 10
r = max(5, r)
low_freq = magnitude_spectrum[cy-r:cy+r, cx-r:cx+r]
high_freq_mask = np.ones_like(magnitude_spectrum)
high_freq_mask[cy-r:cy+r, cx-r:cx+r] = 0
high_freq = magnitude_spectrum * high_freq_mask
mean_low = float(np.mean(low_freq))
mean_high = float(np.sum(high_freq) / (np.sum(high_freq_mask) + 1e-5))
# Ratio of low frequency to high frequency.
# Deepfakes often lack high frequencies, so this ratio is abnormally high.
freq_ratio = mean_low / (mean_high + 1e-5)
# Normal faces ratio is around 1.2 - 2.2. Ratio > 2.8 indicates missing details/smoothing.
freq_anomaly = max(0.0, min(1.0, (freq_ratio - 1.5) / 1.5))
# C. Color distribution & Lighting anomalies
# Natural skin has high red channel mean and consistent color variances.
# Deepfakes often exhibit green/blue channel mismatches or extreme uniformity.
r_chan = face_rgb[:, :, 0]
g_chan = face_rgb[:, :, 1]
b_chan = face_rgb[:, :, 2]
r_std = float(r_chan.std())
g_std = float(g_chan.std())
b_std = float(b_chan.std())
# Compare channel variances. Unnatural faces have overly similar channel standard deviations
# (flat color tones) or extreme differences.
std_diff = abs(r_std - g_std) + abs(g_std - b_std)
# If std_diff is very small (< 2) or standard deviations are very low, color is too uniform.
mean_std = (r_std + g_std + b_std) / 3.0
color_anomaly = 0.0
if mean_std < 15.0: # flat texture
color_anomaly += 0.5
if std_diff < 1.5: # no skin depth / lighting variations
color_anomaly += 0.5
color_anomaly = min(1.0, color_anomaly)
return {
"sharpness_val": round(lap_var, 2),
"blur_artifact_score": round(blur_artifact, 4),
"freq_ratio": round(freq_ratio, 4),
"frequency_anomaly_score": round(freq_anomaly, 4),
"color_depth_val": round(std_diff, 2),
"color_anomaly_score": round(color_anomaly, 4)
}