deepshield-ai / tta.py
pseudonomasorg's picture
Upload folder using huggingface_hub
b0c6daf verified
Raw
History Blame Contribute Delete
5.33 kB
"""
Test-Time Augmentation (TTA) β€” adversarial robustness for deepfake detection.
Runs multiple augmented versions of each face through the model and
averages predictions. This makes the system resistant to:
- Adversarial perturbations (small pixel changes that flip predictions)
- Compression artifacts that might confuse single-pass inference
- Orientation-dependent biases in the model
Usage (from main.py):
from tta import predict_with_tta
result = predict_with_tta(face_pil, model, transform, device)
# result = {"tta_confidence": 0.91, "tta_agreement": 0.8, ...}
"""
import io
import random
import torch
import numpy as np
from PIL import Image, ImageFilter
# ──────────────────────────────────────
# Augmentation functions (PIL β†’ PIL)
# ──────────────────────────────────────
def _identity(img: Image.Image) -> Image.Image:
"""No-op augmentation (original image)."""
return img
def _horizontal_flip(img: Image.Image) -> Image.Image:
"""Mirror the face horizontally."""
return img.transpose(Image.FLIP_LEFT_RIGHT)
def _slight_rotate(img: Image.Image) -> Image.Image:
"""Small rotation (Β±3Β°) β€” tests orientation robustness."""
angle = random.choice([-3, -2, 2, 3])
return img.rotate(angle, resample=Image.BILINEAR, expand=False, fillcolor=(0, 0, 0))
def _jpeg_compress(img: Image.Image) -> Image.Image:
"""
Re-encode as JPEG at quality 60 β€” destroys adversarial perturbations
while preserving genuine facial features.
"""
buffer = io.BytesIO()
img.save(buffer, format="JPEG", quality=60)
buffer.seek(0)
return Image.open(buffer).convert("RGB")
def _gaussian_blur(img: Image.Image) -> Image.Image:
"""Light Gaussian blur β€” smooths out adversarial noise."""
return img.filter(ImageFilter.GaussianBlur(radius=0.8))
# Default augmentation pipeline
DEFAULT_AUGMENTATIONS = [
_identity,
_horizontal_flip,
_slight_rotate,
_jpeg_compress,
_gaussian_blur,
]
def predict_with_tta(
face_pil: Image.Image,
model,
transform,
device,
augmentations=None,
) -> dict:
"""
Run test-time augmentation on a single face crop.
Args:
face_pil: PIL Image β€” the cropped face
model: DeepfakeClassifier instance (eval mode)
transform: torchvision transform pipeline
device: torch.device
augmentations: list of (PIL→PIL) functions, or None for defaults
Returns:
dict with keys:
- tta_label: str β€” consensus label ("FAKE" or "REAL")
- tta_pred_idx: int β€” consensus class index
- tta_confidence: float β€” average max confidence across augmentations
- tta_agreement: float β€” fraction of augmentations that agree with consensus
- tta_probs: list β€” per-augmentation [fake_prob, real_prob]
- tta_embedding: torch.Tensor β€” embedding from the original (unaugmented) pass
"""
if augmentations is None:
augmentations = DEFAULT_AUGMENTATIONS
CLASS_NAMES = {0: "FAKE", 1: "REAL"}
all_probs = []
all_preds = []
original_embedding = None
model.eval()
for i, aug_fn in enumerate(augmentations):
try:
aug_face = aug_fn(face_pil)
inp = transform(aug_face).unsqueeze(0).to(device)
with torch.no_grad():
embedding = model.backbone(inp)
logits = model.head(embedding)
probs = torch.softmax(logits, dim=1)[0]
# Save the original pass's embedding for trust model
if i == 0:
original_embedding = embedding
pred_idx = int(torch.argmax(probs))
all_probs.append(probs.cpu())
all_preds.append(pred_idx)
except Exception as e:
print(f"[TTA] Augmentation {aug_fn.__name__} failed: {e}")
continue
if not all_probs:
# Fallback: return empty result
return {
"tta_label": "UNKNOWN",
"tta_pred_idx": -1,
"tta_confidence": 0.0,
"tta_agreement": 0.0,
"tta_probs": [],
"tta_embedding": None,
}
# Stack all probability vectors and compute consensus
prob_stack = torch.stack(all_probs) # (N, 2)
avg_probs = prob_stack.mean(dim=0) # (2,)
consensus_idx = int(torch.argmax(avg_probs))
consensus_conf = float(avg_probs[consensus_idx])
# Agreement: fraction of augmentations that predict the consensus class
agreement = sum(1 for p in all_preds if p == consensus_idx) / len(all_preds)
return {
"tta_label": CLASS_NAMES.get(consensus_idx, "UNKNOWN"),
"tta_pred_idx": consensus_idx,
"tta_confidence": round(consensus_conf, 4),
"tta_agreement": round(agreement, 4),
"tta_probs": [[round(float(p[0]), 4), round(float(p[1]), 4)] for p in all_probs],
"tta_embedding": original_embedding,
}