Create inference.py
Browse files- inference.py +120 -0
inference.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import numpy as np
|
| 3 |
+
from PIL import Image
|
| 4 |
+
import torch
|
| 5 |
+
import torch.nn as nn
|
| 6 |
+
from torchvision import models, transforms
|
| 7 |
+
from retinaface import RetinaFace
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Optional
|
| 10 |
+
|
| 11 |
+
# --- Configuration ---
|
| 12 |
+
CHECKPOINT_PATH = Path("pytorch_model.bin") # Updated to match your new HF filename
|
| 13 |
+
_IN_FEATURES = 1408
|
| 14 |
+
_DROPOUT = 0.3
|
| 15 |
+
_NUM_CLASSES = 2
|
| 16 |
+
_INPUT_SIZE = 260
|
| 17 |
+
_CONFIDENCE_THRESHOLD = 0.90
|
| 18 |
+
_MIN_FACE_PX = 50
|
| 19 |
+
_PADDING = 20
|
| 20 |
+
|
| 21 |
+
# --- Transform ---
|
| 22 |
+
_transform = transforms.Compose([
|
| 23 |
+
transforms.Resize((_INPUT_SIZE, _INPUT_SIZE), interpolation=transforms.InterpolationMode.BICUBIC),
|
| 24 |
+
transforms.ToTensor(),
|
| 25 |
+
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
|
| 26 |
+
])
|
| 27 |
+
|
| 28 |
+
# --- 1. Load Architecture ---
|
| 29 |
+
def load_model() -> tuple[nn.Module, torch.device]:
|
| 30 |
+
# Universal Hardware Routing
|
| 31 |
+
if torch.cuda.is_available():
|
| 32 |
+
device = torch.device("cuda")
|
| 33 |
+
elif torch.backends.mps.is_available():
|
| 34 |
+
device = torch.device("mps")
|
| 35 |
+
else:
|
| 36 |
+
device = torch.device("cpu")
|
| 37 |
+
|
| 38 |
+
net = models.efficientnet_b2(weights=None)
|
| 39 |
+
net.classifier = nn.Sequential(
|
| 40 |
+
nn.Dropout(_DROPOUT),
|
| 41 |
+
nn.Linear(_IN_FEATURES, _NUM_CLASSES),
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
checkpoint = torch.load(CHECKPOINT_PATH, map_location=device, weights_only=False)
|
| 45 |
+
net.load_state_dict(checkpoint["model_state_dict"])
|
| 46 |
+
|
| 47 |
+
net.to(device)
|
| 48 |
+
net.eval()
|
| 49 |
+
|
| 50 |
+
return net, device
|
| 51 |
+
|
| 52 |
+
MODEL, DEVICE = load_model()
|
| 53 |
+
|
| 54 |
+
# --- 2. Extract & Preprocess ---
|
| 55 |
+
def detect_and_crop_face(image_path: str) -> Optional[torch.Tensor]:
|
| 56 |
+
image_bgr = cv2.imread(image_path)
|
| 57 |
+
if image_bgr is None:
|
| 58 |
+
raise ValueError(f"Could not load image at {image_path}")
|
| 59 |
+
|
| 60 |
+
detections = RetinaFace.detect_faces(image_bgr)
|
| 61 |
+
if not isinstance(detections, dict):
|
| 62 |
+
return None
|
| 63 |
+
|
| 64 |
+
best_conf = -1.0
|
| 65 |
+
best_box = None
|
| 66 |
+
|
| 67 |
+
for face_data in detections.values():
|
| 68 |
+
conf = float(face_data.get("score", 0.0))
|
| 69 |
+
if conf < _CONFIDENCE_THRESHOLD:
|
| 70 |
+
continue
|
| 71 |
+
|
| 72 |
+
x1, y1, x2, y2 = face_data["facial_area"]
|
| 73 |
+
w, h = x2 - x1, y2 - y1
|
| 74 |
+
if w < _MIN_FACE_PX or h < _MIN_FACE_PX:
|
| 75 |
+
continue
|
| 76 |
+
|
| 77 |
+
if conf > best_conf:
|
| 78 |
+
best_conf = conf
|
| 79 |
+
best_box = (x1, y1, x2, y2)
|
| 80 |
+
|
| 81 |
+
if best_box is None:
|
| 82 |
+
return None
|
| 83 |
+
|
| 84 |
+
H, W = image_bgr.shape[:2]
|
| 85 |
+
x1, y1, x2, y2 = best_box
|
| 86 |
+
x1 = max(0, x1 - _PADDING)
|
| 87 |
+
y1 = max(0, y1 - _PADDING)
|
| 88 |
+
x2 = min(W, x2 + _PADDING)
|
| 89 |
+
y2 = min(H, y2 + _PADDING)
|
| 90 |
+
|
| 91 |
+
crop_bgr = image_bgr[y1:y2, x1:x2]
|
| 92 |
+
crop_rgb = cv2.cvtColor(crop_bgr, cv2.COLOR_BGR2RGB)
|
| 93 |
+
pil_face = Image.fromarray(crop_rgb)
|
| 94 |
+
|
| 95 |
+
return _transform(pil_face).unsqueeze(0)
|
| 96 |
+
|
| 97 |
+
# --- 3. Execute Prediction ---
|
| 98 |
+
def predict_deepfake(image_path: str) -> dict:
|
| 99 |
+
face_tensor = detect_and_crop_face(image_path)
|
| 100 |
+
|
| 101 |
+
if face_tensor is None:
|
| 102 |
+
return {"error": "No face detected in the image meeting confidence thresholds."}
|
| 103 |
+
|
| 104 |
+
face_tensor = face_tensor.to(DEVICE)
|
| 105 |
+
|
| 106 |
+
with torch.no_grad():
|
| 107 |
+
logits = MODEL(face_tensor)
|
| 108 |
+
probs = torch.softmax(logits, dim=1)[0]
|
| 109 |
+
|
| 110 |
+
fake_prob = probs[0].item() * 100
|
| 111 |
+
real_prob = probs[1].item() * 100
|
| 112 |
+
predicted_idx = int(torch.argmax(probs).item())
|
| 113 |
+
|
| 114 |
+
prediction = "REAL" if predicted_idx == 1 else "FAKE"
|
| 115 |
+
|
| 116 |
+
return {
|
| 117 |
+
"prediction": prediction,
|
| 118 |
+
"fake_confidence": f"{fake_prob:.2f}%",
|
| 119 |
+
"real_confidence": f"{real_prob:.2f}%"
|
| 120 |
+
}
|