File size: 8,268 Bytes
fdf3c02
 
 
 
 
825e068
fdf3c02
 
 
 
825e068
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fdf3c02
 
825e068
fdf3c02
 
 
825e068
fdf3c02
 
 
 
 
 
825e068
 
 
 
 
 
 
 
 
 
 
 
 
fdf3c02
 
 
 
 
 
 
825e068
 
fdf3c02
 
 
 
825e068
fdf3c02
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8bcf2e0
fdf3c02
 
 
8bcf2e0
fdf3c02
 
8bcf2e0
 
fdf3c02
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8bcf2e0
511b8a3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
import os, io, tempfile, warnings
import numpy as np
import gradio as gr

# =========================
# TensorFlow / Keras Setup
# =========================
import tensorflow as tf
from tensorflow.keras.applications.resnet50 import preprocess_input as resnet_preprocess

# Limit TensorFlow thread usage (prevents OOM)
os.environ["OMP_NUM_THREADS"] = "2"
os.environ["MKL_NUM_THREADS"] = "2"
os.environ["OPENBLAS_NUM_THREADS"] = "2"
os.environ["NUMEXPR_NUM_THREADS"] = "2"
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"

# Try to enable GPU safely for TensorFlow
try:
    gpus = tf.config.experimental.list_physical_devices("GPU")
    if gpus:
        for gpu in gpus:
            tf.config.experimental.set_memory_growth(gpu, True)
        tf.keras.mixed_precision.set_global_policy("mixed_float16")
        print("[INFO] ✅ TensorFlow GPU detected & configured (mixed precision ON).")
    else:
        print("[WARN] ⚠️ No TensorFlow GPU detected, running on CPU.")
except Exception as e:
    print(f"[WARN] TensorFlow GPU init skipped: {e}")

# =========================
# PyTorch / MTCNN Setup
# =========================
import torch
from facenet_pytorch import MTCNN
import cv2
import dlib
from imutils import face_utils
from scipy.spatial import distance as dist

warnings.filterwarnings("ignore")

# Use GPU for MTCNN if available
if torch.cuda.is_available():
    _torch_device = torch.device("cuda")
    torch.backends.cudnn.benchmark = True
    try:
        torch.set_float32_matmul_precision("medium")
    except Exception:
        pass
    print("[INFO] ✅ PyTorch GPU detected — MTCNN will use CUDA.")
else:
    _torch_device = torch.device("cpu")
    print("[WARN] ⚠️ No GPU detected — MTCNN running on CPU.")

# =========================
# Paths / Config
# =========================
VIDEO_MODEL_PATH = "models/video_model.h5"
DLIB_LANDMARK_MODEL = "shape_predictor_68_face_landmarks.dat"

IMG_SIZE = (224, 224)
FRAME_STEP = 5
NUM_MAX_FACES = 300
EAR_THRESHOLD = 0.25
EAR_CONSEC_FRAMES = 3
PRED_THRESHOLD = 0.5

# Lazy-loaded globals
_video_model = None
_mtcnn = None
_dlib_detector = None
_dlib_predictor = None


def lazy_load():
    global _video_model, _mtcnn, _dlib_detector, _dlib_predictor

    if _video_model is None:
        if not os.path.exists(VIDEO_MODEL_PATH):
            raise FileNotFoundError(f"Missing: {VIDEO_MODEL_PATH}")
        # Allow tf to place on GPU if available
        _video_model = tf.keras.models.load_model(VIDEO_MODEL_PATH)
        print("[INFO] Video model loaded.")

    if _mtcnn is None:
        _mtcnn = MTCNN(keep_all=True, device=_torch_device, image_size=IMG_SIZE[0])
        print(f"[INFO] MTCNN ready on {_torch_device}.")

    if _dlib_detector is None or _dlib_predictor is None:
        if not os.path.exists(DLIB_LANDMARK_MODEL):
            raise FileNotFoundError(
                f"Missing dlib predictor: {DLIB_LANDMARK_MODEL}. Place it beside app.py."
            )
        _dlib_detector = dlib.get_frontal_face_detector()
        _dlib_predictor = dlib.shape_predictor(DLIB_LANDMARK_MODEL)
        print("[INFO] dlib detector + predictor ready.")

# =========================
# VIDEO: faces + blink features
# =========================
def _eye_aspect_ratio(eye_pts):
    A = dist.euclidean(eye_pts[1], eye_pts[5])
    B = dist.euclidean(eye_pts[2], eye_pts[4])
    C = dist.euclidean(eye_pts[0], eye_pts[3])
    return (A + B) / (2.0 * C + 1e-9)

def extract_faces_all(path):
    """Sample every 5th frame, keep ALL faces per frame, resize to 224x224."""
    cap = cv2.VideoCapture(path)
    total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    if total <= 0:
        cap.release()
        return None

    faces = []
    for idx in range(0, total, FRAME_STEP):
        if len(faces) >= NUM_MAX_FACES:
            break
        cap.set(cv2.CAP_PROP_POS_FRAMES, idx)
        ok, frame = cap.read()
        if not ok:
            break
        rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        boxes, _ = _mtcnn.detect(rgb)
        if boxes is None:
            continue
        for (x1, y1, x2, y2) in boxes.astype(int):
            x1, y1 = max(0, x1), max(0, y1)
            x2, y2 = min(frame.shape[1], x2), min(frame.shape[0], y2)
            crop = frame[y1:y2, x1:x2]
            if crop.size > 0:
                faces.append(cv2.resize(crop, IMG_SIZE))
            if len(faces) >= NUM_MAX_FACES:
                break
    cap.release()
    if not faces:
        return None
    return np.stack(faces, axis=0).astype(np.uint8)

def blink_features_from_crops(faces):
    """Compute EAR on cropped faces; denominator = #frames with valid EAR."""
    (lStart, lEnd) = face_utils.FACIAL_LANDMARKS_IDXS["left_eye"]
    (rStart, rEnd) = face_utils.FACIAL_LANDMARKS_IDXS["right_eye"]

    ear_values = []
    blink_counter = 0
    total_blinks = 0

    for face in faces:
        gray = cv2.cvtColor(face, cv2.COLOR_BGR2GRAY)
        rects = _dlib_detector(gray, 0)
        if len(rects) == 0:
            continue
        shape = _dlib_predictor(gray, rects[0])
        shape = face_utils.shape_to_np(shape)

        left_eye = shape[lStart:lEnd]
        right_eye = shape[rStart:rEnd]
        ear = 0.5 * (_eye_aspect_ratio(left_eye) + _eye_aspect_ratio(right_eye))
        ear_values.append(ear)

        if ear < EAR_THRESHOLD:
            blink_counter += 1
        else:
            if blink_counter >= EAR_CONSEC_FRAMES:
                total_blinks += 1
            blink_counter = 0

    n_ear = len(ear_values)
    if n_ear == 0:
        return np.array([0, 0.0, 0.0], dtype=np.float32)

    blink_freq = total_blinks / float(n_ear)
    ear_var = float(np.var(np.array(ear_values, dtype=np.float32)))
    return np.array([total_blinks, blink_freq, ear_var], dtype=np.float32)

def predict_video_prob(video_path):
    faces = extract_faces_all(video_path)
    if faces is None or len(faces) == 0:
        return None, "No faces detected."

    blink_feats = blink_features_from_crops(faces)
    # Tile to per-face samples
    tiled = np.tile(blink_feats, (faces.shape[0], 1)).astype(np.float32)
    imgs = resnet_preprocess(faces.astype(np.float32))

    # Batch size heuristic (bigger if GPU is present)
    has_tf_gpu = len(tf.config.experimental.list_physical_devices("GPU")) > 0
    bs = 128 if has_tf_gpu else 32

    preds = []
    for i in range(0, imgs.shape[0], bs):
        p = _video_model.predict([imgs[i:i+bs], tiled[i:i+bs]], verbose=0)
        preds.append(p.reshape(-1))
    return float(np.mean(np.concatenate(preds, axis=0))), None

def to_verdict(score):
    return "DEEPFAKE" if score >= PRED_THRESHOLD else "REAL"

# =========================
# Inference entry
# =========================
def run_inference(video_file):
    lazy_load()
    if video_file is None:
        return None, None, "Please upload a video file."

    video_prob, vmsg = predict_video_prob(video_file)
    if video_prob is None:
        return None, None, vmsg or "Unable to process the video."

    verdict = f"VIDEO ONLY: {to_verdict(video_prob)}"
    fmt = lambda x: None if x is None else round(float(x), 4)
    return fmt(video_prob), verdict, None

# =========================
# Gradio UI
# =========================
with gr.Blocks(title="Deepfake Detector — Video Only (GPU-ready)") as demo:
    gr.Markdown(
        "### 🎭 Deepfake Detector — **Video Only**\n"
        "- **Compute**: Uses GPU automatically if available (PyTorch MTCNN, TensorFlow model; mixed precision on TF).\n"
        "- **Video path**: samples every 5th frame, keeps **all** faces, resizes to 224×224.\n"
        "- **Features**: ResNet50 preprocessing + blink EAR features on cropped faces via dlib.\n"
        f"- **Threshold**: {PRED_THRESHOLD} (≥ means DEEPFAKE).\n"
    )
    with gr.Row():
        video_in = gr.Video(label="Video")
    go = gr.Button("Analyze")
    with gr.Row():
        v_out = gr.Number(label="Video probability (deepfake)", precision=4)
        verdict_out = gr.Textbox(label="Verdict", interactive=False)
    msg_out = gr.Textbox(label="Message / Warnings", interactive=False)

    go.click(run_inference, inputs=[video_in], outputs=[v_out, verdict_out, msg_out])

if __name__ == "__main__":
    lazy_load()
    demo.launch(server_name="0.0.0.0", server_port=7860)