| |
| import sys |
| from pathlib import Path |
| sys.path.insert(0, str(Path(__file__).parent.parent / 'src')) |
|
|
| import cv2 |
| import gradio as gr |
| import numpy as np |
| import torch |
| from PIL import Image |
|
|
| from dataset import VAL_TRANSFORMS |
| from forensic_text import generate_forensic_report |
| from gradcam import GradCAM, get_top_zones |
| from model import DeepfakeClassifier |
|
|
| try: |
| from facenet_pytorch import MTCNN as _MTCNN |
| _mtcnn = _MTCNN(image_size=224, margin=20, keep_all=False, device='cpu') |
| except Exception: |
| _mtcnn = None |
|
|
| DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu' |
| CHECKPOINT = Path(__file__).parent.parent / 'checkpoints' / 'best_model.pt' |
|
|
| model = DeepfakeClassifier(freeze_blocks=5) |
| if CHECKPOINT.exists(): |
| model.load_state_dict(torch.load(str(CHECKPOINT), map_location=DEVICE, weights_only=True)) |
| model = model.to(DEVICE).eval() |
| grad_cam = GradCAM(model) |
|
|
|
|
| def _crop_face(pil_img: Image.Image): |
| """Returns (224x224 face crop as PIL, crop tensor) or (None, None) if no face.""" |
| if _mtcnn is None: |
| resized = pil_img.resize((224, 224)) |
| tensor = VAL_TRANSFORMS(resized).unsqueeze(0).to(DEVICE) |
| return resized, tensor |
|
|
| crop_tensor = _mtcnn(pil_img) |
| if crop_tensor is None: |
| return None, None |
|
|
| crop_np = ((crop_tensor.permute(1, 2, 0).numpy() + 1) / 2 * 255).clip(0, 255).astype(np.uint8) |
| crop_pil = Image.fromarray(crop_np) |
| img_tensor = VAL_TRANSFORMS(crop_pil).unsqueeze(0).to(DEVICE) |
| return crop_pil, img_tensor |
|
|
|
|
| def analyze_image(pil_img: Image.Image): |
| if pil_img is None: |
| return None, "No image provided.", "Upload an image to begin." |
|
|
| crop_pil, img_tensor = _crop_face(pil_img) |
| if crop_pil is None: |
| return None, "No face detected.", "Unable to analyze β no face found in the image." |
|
|
| heatmap, confidence = grad_cam.compute(img_tensor) |
| overlay = grad_cam.overlay(crop_pil, heatmap) |
| zones = get_top_zones(heatmap, top_k=2) |
| report = generate_forensic_report(confidence, zones[0], zones[1]) |
| label = "FAKE" if confidence >= 0.5 else "REAL" |
| verdict = f"{label} β Confidence: {confidence:.1%}" |
| return overlay, verdict, report |
|
|
|
|
| def analyze_video(video_path: str): |
| if video_path is None: |
| return None, "No video provided.", "Upload a video to begin." |
|
|
| cap = cv2.VideoCapture(video_path) |
| total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) |
| if total == 0: |
| cap.release() |
| return None, "Could not read video.", "File may be corrupt or unsupported format." |
|
|
| indices = [int(i * total / 15) for i in range(15)] |
| all_confidences = [] |
| best_conf, best_overlay, best_report = 0.0, None, "" |
|
|
| for idx in indices: |
| cap.set(cv2.CAP_PROP_POS_FRAMES, idx) |
| ret, frame = cap.read() |
| if not ret: |
| continue |
| pil_frame = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) |
| crop_pil, img_tensor = _crop_face(pil_frame) |
| if crop_pil is None: |
| continue |
| heatmap, conf = grad_cam.compute(img_tensor) |
| overlay = grad_cam.overlay(crop_pil, heatmap) |
| zones = get_top_zones(heatmap, top_k=2) |
| report = generate_forensic_report(conf, zones[0], zones[1]) |
| all_confidences.append(conf) |
| if conf > best_conf: |
| best_conf, best_overlay, best_report = conf, overlay, report |
|
|
| cap.release() |
|
|
| if not all_confidences: |
| return None, "No faces detected in video.", "Could not analyze any frames." |
|
|
| mean_conf = np.mean(all_confidences) |
| label = "FAKE" if mean_conf >= 0.5 else "REAL" |
| verdict = f"[VIDEO] {label} β Mean confidence: {mean_conf:.1%} over {len(all_confidences)} frames" |
| return best_overlay, verdict, best_report |
|
|
|
|
| |
| with gr.Blocks(title="Deepfake Detector") as demo: |
| gr.Markdown("# Deepfake Detection with Forensic Explainability") |
| gr.Markdown( |
| "Upload an image or short video. The model detects manipulation artifacts, " |
| "highlights suspicious facial regions with Grad-CAM, and generates a forensic report." |
| ) |
|
|
| with gr.Tab("Image"): |
| with gr.Row(): |
| img_in = gr.Image(type="pil", label="Input Image") |
| img_overlay = gr.Image(label="Grad-CAM Heatmap") |
| img_verdict = gr.Textbox(label="Verdict", interactive=False) |
| img_report = gr.Textbox(label="Forensic Report", lines=4, interactive=False) |
| gr.Button("Analyze Image").click( |
| analyze_image, |
| inputs=img_in, |
| outputs=[img_overlay, img_verdict, img_report], |
| ) |
|
|
| with gr.Tab("Video"): |
| vid_in = gr.Video(label="Input Video (β€ 60 s recommended)") |
| with gr.Row(): |
| vid_overlay = gr.Image(label="Highest-Confidence Frame β Grad-CAM") |
| with gr.Column(): |
| vid_verdict = gr.Textbox(label="Video Verdict", interactive=False) |
| vid_report = gr.Textbox(label="Forensic Report", lines=4, interactive=False) |
| gr.Button("Analyze Video").click( |
| analyze_video, |
| inputs=vid_in, |
| outputs=[vid_overlay, vid_verdict, vid_report], |
| ) |
|
|
| if __name__ == "__main__": |
| demo.launch() |
|
|