CoolFace
Apppublic

hiimwsta/AI-Source-Forensics

sourceHugging Facemitupdated 6mo agoView on Hugging Face
0likes
app.py321 linesDownload Raw Back to root
1import os2import json3import torch4import torch.nn as nn5from flask import Flask, request, jsonify, render_template, send_from_directory6from werkzeug.utils import secure_filename7from torchvision import transforms8from PIL import Image9import io10import numpy as np11import cv212import time13import mediapipe as mp 14from model_architecture import MultiBranchDetector15 16app = Flask(__name__)17project_root = os.path.dirname(os.path.abspath(__file__))18app.config['UPLOAD_FOLDER'] = os.path.join(project_root, 'static', 'uploads')19os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)20 21MODEL_PATH = "multi_branch_detector_genimage.pth"22CLASSES_JSON_PATH = "class_names.json"23IMAGE_SIZE = 22424device = torch.device("cuda" if torch.cuda.is_available() else "cpu")25 26mp_face_detection = mp.solutions.face_detection27face_detection = mp_face_detection.FaceDetection(model_selection=0, min_detection_confidence=0.5)28 29# --- Load Class Names ---30def load_classes():31    if os.path.exists(CLASSES_JSON_PATH):32        with open(CLASSES_JSON_PATH, 'r') as f:33            return json.load(f)34    return ['ai-gen', 'real'] 35 36CLASS_NAMES = load_classes()37NUM_CLASSES = len(CLASS_NAMES)38 39# --- Feature Extraction Tools ---40class FFTTransform:41    def __call__(self, img):42        img_np = np.array(img.convert('L'))43        f = np.fft.fft2(img_np)44        fshift = np.fft.fftshift(f)45        magnitude_spectrum = 20 * np.log(np.abs(fshift) + 1e-9)46        magnitude_spectrum = np.clip(magnitude_spectrum, 0, 255).astype(np.uint8)47        return Image.fromarray(cv2.merge([magnitude_spectrum]*3))48 49class LBPTransform:50    def __call__(self, img):51        img_np = np.array(img.convert('L'), dtype=np.uint8)52        h, w = img_np.shape53        lbp = np.zeros((h, w), dtype=np.uint8)54        offsets = [(-1, -1), (-1, 0), (-1, 1), (0, 1), (1, 1), (1, 0), (1, -1), (0, -1)]55        for i, (dy, dx) in enumerate(offsets):56            shifted = np.roll(img_np, shift=(-dy, -dx), axis=(0, 1))57            lbp += ((shifted >= img_np).astype(np.uint8) << (7 - i))58        return Image.fromarray(cv2.merge([lbp]*3))59 60# --- Load Model ---61def load_model():62    model = MultiBranchDetector(num_classes=NUM_CLASSES)63    if os.path.exists(MODEL_PATH):64        model.load_state_dict(torch.load(MODEL_PATH, map_location=device, weights_only=True))65    model = model.to(device)66    model.eval()67    return model68 69global_model = load_model()70 71# --- Dynamic Diagnostic Log Generator ---72def generate_diagnostic_log(class_name, highest_domain):73    target = class_name.lower().replace(' ', '_').replace('-', '')74    if 'real' in target:75        return "Success: Natural optical sensor noise profile verified. No synthetic upsampling artifacts detected."76 77    diagnostics = {78        'midjourney': {79            'Spatial': "High-contrast structural anomalies and unnatural edge gradients detected.",80            'Frequency': "High-frequency spectral artifacts consistent with diffusion denoising steps.",81            'Texture': "Unnatural micro-texture uniformity and hyper-smoothness detected in LBP analysis."82        },83        'stable_diffusion': {84            'Spatial': "Latent space decoding artifacts and pixel-level spatial inconsistencies identified.",85            'Frequency': "Periodic frequency anomalies indicative of VAE (Variational Autoencoder) upsampling.",86            'Texture': "Synthetic local binary patterns suggesting artificial texture mapping."87        },88        'stylegan': {89            'Spatial': "Asymmetric feature alignment and GAN-specific spatial droplet artifacts found.",90            'Frequency': "Pronounced checkerboard artifacts in the frequency spectrum characteristic of GAN upsampling.",91            'Texture': "Phase discrepancies and unnatural texture repetition detected."92        },93        'deepfacelab': {94            'Spatial': "Color blending inconsistencies and resolution mismatch along facial boundaries detected.",95            'Frequency': "Frequency spectrum mismatch between central facial region and background image.",96            'Texture': "Texture discontinuity along facial landmarks and blending mask edges."97        }98    }99    default_log = f"Synthetic generation patterns detected with primary anomalies in the {highest_domain} domain."100    specific_log = diagnostics.get(target, {}).get(highest_domain, default_log)101    return f"Warning: Source attributed to {class_name.upper()}. {specific_log}"102 103# ==========================================104# ๐Ÿ’ก Smart Crop using Google MediaPipe105# ==========================================106def smart_crop(img_pil):107    img_np = np.array(img_pil)108    results = face_detection.process(img_np)109    110    if results.detections:111        largest_detection = max(results.detections, 112                                key=lambda d: d.location_data.relative_bounding_box.width * d.location_data.relative_bounding_box.height)113        114        bbox = largest_detection.location_data.relative_bounding_box115        ih, iw, _ = img_np.shape116        117        x = int(bbox.xmin * iw)118        y = int(bbox.ymin * ih)119        w = int(bbox.width * iw)120        h = int(bbox.height * ih)121        122        pad_x, pad_y = int(w * 0.3), int(h * 0.3)123        x1, y1 = max(0, x - pad_x), max(0, y - pad_y)124        x2, y2 = min(iw, x + w + pad_x), min(ih, y + h + pad_y)125        126        face_crop = img_pil.crop((x1, y1, x2, y2))127        return face_crop.resize((IMAGE_SIZE, IMAGE_SIZE))128    else:129        fallback = transforms.Compose([130            transforms.Resize(IMAGE_SIZE),131            transforms.CenterCrop(IMAGE_SIZE)132        ])133        return fallback(img_pil)134 135# --- Save XAI Feature Maps ---136def save_feature_visualizations(img_cropped, base_filename):137    fft_img = FFTTransform()(img_cropped)138    lbp_img = LBPTransform()(img_cropped)139    140    timestamp = int(time.time())141    crop_name = f"crop_{timestamp}_{base_filename}.png"142    fft_name = f"fft_{timestamp}_{base_filename}.png"143    lbp_name = f"lbp_{timestamp}_{base_filename}.png"144    145    img_cropped.save(os.path.join(app.config['UPLOAD_FOLDER'], crop_name))146    fft_img.save(os.path.join(app.config['UPLOAD_FOLDER'], fft_name))147    lbp_img.save(os.path.join(app.config['UPLOAD_FOLDER'], lbp_name))148    149    return crop_name, fft_name, lbp_name150 151# --- Core Inference Logic ---152def predict_pil_image(img_cropped, model):153    to_tensor = transforms.ToTensor()154    norm = transforms.Normalize([0.5]*3, [0.5]*3)155    156    rgb_t = norm(to_tensor(img_cropped)).unsqueeze(0).to(device)157    fft_t = norm(to_tensor(FFTTransform()(img_cropped))).unsqueeze(0).to(device)158    lbp_t = norm(to_tensor(LBPTransform()(img_cropped))).unsqueeze(0).to(device)159    160    output, (s_rgb, s_fft, s_lbp) = model.get_domain_contribution(rgb_t, fft_t, lbp_t)161    probs = torch.nn.functional.softmax(output, dim=1).squeeze()162    163    top_prob, top_idx = torch.max(probs, 0)164    top_class_name = CLASS_NAMES[top_idx.item()]165    total_score = s_rgb + s_fft + s_lbp + 1e-9166    167    return {168        "source": top_class_name.upper(), 169        "confidence": top_prob.item() * 100,170        "p_rgb": (s_rgb / total_score) * 100,171        "p_fft": (s_fft / total_score) * 100,172        "p_lbp": (s_lbp / total_score) * 100173    }174 175def predict_image(image_bytes, model, filename):176    try:177        img = Image.open(io.BytesIO(image_bytes)).convert("RGB")178        img_cropped = smart_crop(img)179        180        crop_file, fft_file, lbp_file = save_feature_visualizations(img_cropped, secure_filename(filename))181        182        res = predict_pil_image(img_cropped, model)183        184        is_real = 'REAL' in res['source']185        prediction_status = 'real' if is_real else 'ai-gen'186        highest_domain = max([('Spatial', res['p_rgb']), ('Frequency', res['p_fft']), ('Texture', res['p_lbp'])], key=lambda x: x[1])[0]187        log_msg = generate_diagnostic_log(res['source'], highest_domain)188 189        return {190            "prediction": prediction_status, "source": res['source'], "confidence": res['confidence'],191            "rgb_score": res['confidence'] * (res['p_rgb']/33.3), "fft_score": res['confidence'] * (res['p_fft']/33.3), "lbp_score": res['confidence'] * (res['p_lbp']/33.3),192            "log": log_msg, "is_video": False, "frames": 1,193            "crop_file": crop_file, "fft_file": fft_file, "lbp_file": lbp_file194        }195    except Exception as e:196        print(f"Image Prediction Error: {e}")197        return None198 199def analyze_video(video_path, model, filename, num_frames=12):200    try:201        cap = cv2.VideoCapture(video_path)202        total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))203        if total_frames == 0: return None204        205        step = max(1, total_frames // num_frames)206        frame_results = []207        crop_file, fft_file, lbp_file = None, None, None208        209        for i in range(num_frames):210            frame_idx = i * step211            if frame_idx >= total_frames: break212            213            cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)214            ret, frame = cap.read()215            if not ret: break216            217            frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)218            pil_img = Image.fromarray(frame_rgb)219            img_cropped = smart_crop(pil_img)220            221            if i == num_frames // 2:222                crop_file, fft_file, lbp_file = save_feature_visualizations(img_cropped, secure_filename(filename))223                224            res = predict_pil_image(img_cropped, model)225            frame_results.append(res)226            227        cap.release()228        if not frame_results: return None229        230        source_counts = {}231        total_conf, total_rgb, total_fft, total_lbp = 0, 0, 0, 0232        n = len(frame_results)233        calibrated_count = 0234        235        for res in frame_results:236            src = res['source']237            conf = res['confidence']238            239            if src in ['STABLE DIFFUSION', 'MIDJOURNEY', 'STYLEGAN'] and conf < 85.0:240                src = 'REAL'241                conf = 100.0 - conf242                calibrated_count += 1243                244            source_counts[src] = source_counts.get(src, 0) + 1245            total_conf += conf246            total_rgb += res['p_rgb']247            total_fft += res['p_fft']248            total_lbp += res['p_lbp']249 250        ai_threats = {k: v for k, v in source_counts.items() if 'REAL' not in k}251        if ai_threats:252            highest_ai_threat = max(ai_threats, key=ai_threats.get)253            if ai_threats[highest_ai_threat] >= (n // 4):254                top_source = highest_ai_threat255            else:256                top_source = max(source_counts, key=source_counts.get)257        else:258            top_source = 'REAL'259            260        avg_conf = total_conf / n261        avg_p_rgb = total_rgb / n262        avg_p_fft = total_fft / n263        avg_p_lbp = total_lbp / n264        265        is_real = 'REAL' in top_source266        prediction_status = 'real' if is_real else 'ai-gen'267        highest_domain = max([('Spatial', avg_p_rgb), ('Frequency', avg_p_fft), ('Texture', avg_p_lbp)], key=lambda x: x[1])[0]268        base_log = generate_diagnostic_log(top_source, highest_domain)269        270        if is_real and calibrated_count > 0:271            log_msg = f"[Video Analysis | Auto-Calibrated] MP4 compression filtered. " + base_log272        elif not is_real:273            log_msg = f"[Video Analysis | Threat Priority Triggered] Deepfake temporal anomalies detected. " + base_log274        else:275            log_msg = f"[Video Analysis | Examined {n} Keyframes] " + base_log276 277        return {278            "prediction": prediction_status, "source": top_source, "confidence": avg_conf,279            "rgb_score": avg_conf * (avg_p_rgb/33.3), "fft_score": avg_conf * (avg_p_fft/33.3), "lbp_score": avg_conf * (avg_p_lbp/33.3),280            "log": log_msg, "is_video": True, "frames": n,281            "crop_file": crop_file, "fft_file": fft_file, "lbp_file": lbp_file282        }283    except Exception as e:284        print(f"Video Processing Error: {e}")285        return None286 287# --- Routing ---288@app.route('/', methods=['GET', 'POST'])289def index():290    if request.method == 'POST':291        if 'media' not in request.files: return jsonify({"error": "No file part"}), 400292        file = request.files['media']293        if file.filename == '': return jsonify({"error": "No selected file"}), 400294        295        if file:296            filename = secure_filename(file.filename)297            path = os.path.join(app.config['UPLOAD_FOLDER'], filename)298            file.save(path)299            300            ext = filename.rsplit('.', 1)[1].lower() if '.' in filename else ''301            302            if ext in ['mp4', 'avi', 'mov', 'webm']:303                res = analyze_video(path, global_model, filename, num_frames=12)304            else:305                file.seek(0)306                res = predict_image(file.read(), global_model, filename)307            308            if res:309                res['media_file'] = filename310                return jsonify(res)311            else:312                return jsonify({"error": "Error processing media"}), 500313                                   314    return render_template('index.html', prediction=None)315 316@app.route('/static/uploads/<filename>')317def uploaded_file(filename):318    return send_from_directory(app.config['UPLOAD_FOLDER'], filename)319 320if __name__ == '__main__':321    app.run(host='0.0.0.0', port=7860)