| <<<<<<< HEAD |
| import io |
| import json |
| from PIL import Image |
| import numpy as np |
| import gradio as gr |
| import matplotlib |
| matplotlib.use('Agg') |
| import matplotlib.pyplot as plt |
| import joblib |
| from pathlib import Path |
| from datetime import datetime |
| from sklearn.metrics import accuracy_score, roc_auc_score, classification_report, roc_curve |
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
| from torchvision import transforms, models |
| import torchvision.transforms.functional as TF |
| import cv2 |
| import tempfile |
| import os |
|
|
| from detector import sliding_patch_scores, reconstruct_heatmap, rgb_to_gray, extract_residual, fft_stats, lbp_entropy |
| from ethical_assessment import EthicalAssessment, format_ethical_report, get_simple_status |
| from video_model import ResNetLSTM, GradCAM, overlay_cam |
| from video_data import read_video_frames, FaceCropper |
|
|
|
|
| def get_enhanced_ethical_status(assessment): |
| """Generate enhanced status string with prominent flag display""" |
| status = assessment.get('status', 'UNKNOWN') |
| risk = assessment.get('risk_score', 0) |
| flags = assessment.get('flags', []) |
| checks = assessment.get('checks', {}) |
|
|
| lines = [] |
|
|
| |
| if assessment.get('is_ethical'): |
| lines.append(f"STATUS: {status}") |
| else: |
| lines.append(f"STATUS: {status}") |
|
|
| lines.append(f"Risk Score: {risk:.1%}") |
|
|
| |
| if flags: |
| lines.append("") |
| lines.append("FLAGS RAISED:") |
| for flag in flags: |
| flag_desc = { |
| "NSFW_CONTENT": "Explicit/NSFW content detected", |
| "POTENTIAL_MINOR": "CRITICAL: Potential minor detected", |
| "POTENTIAL_CELEBRITY": "Celebrity impersonation risk", |
| "EMOTIONAL_MANIPULATION": "High emotional manipulation", |
| "AI_METADATA_MARKERS": "AI generation markers in metadata", |
| "WATERMARK_REMOVAL": "Signs of watermark removal", |
| "POTENTIAL_HATE_SYMBOL": "Potential hate symbol detected", |
| "MISLEADING_TEXT": "Misleading text overlay", |
| "DOCUMENT_DETECTED": "Document/ID forgery risk" |
| }.get(flag, flag) |
| lines.append(f" - {flag_desc}") |
|
|
| |
| if checks: |
| lines.append("") |
| if 'nsfw' in checks and checks['nsfw'].get('nsfw_score', 0) > 0.3: |
| lines.append(f"NSFW: {checks['nsfw'].get('severity', 'N/A')}") |
| if 'age_estimation' in checks and checks['age_estimation'].get('is_minor_risk'): |
| lines.append(f"Age: {checks['age_estimation'].get('estimated_age_range', 'N/A')}") |
| if 'document' in checks and checks['document'].get('is_document'): |
| lines.append(f"Document: {checks['document'].get('document_type', 'detected')}") |
|
|
| return "\n".join(lines) |
|
|
| |
| try: |
| import timm |
| HAS_TIMM = True |
| except ImportError: |
| HAS_TIMM = False |
|
|
|
|
| |
| |
| |
|
|
| class FFTFeatureExtractor(nn.Module): |
| """Extract FFT features for frequency domain analysis""" |
| def __init__(self, output_dim=512): |
| super().__init__() |
| self.fft_processor = nn.Sequential( |
| nn.Linear(12, 64), |
| nn.BatchNorm1d(64), |
| nn.ReLU(inplace=True), |
| nn.Dropout(0.1), |
| nn.Linear(64, 128), |
| nn.BatchNorm1d(128), |
| nn.ReLU(inplace=True), |
| nn.Linear(128, output_dim), |
| ) |
|
|
| @torch.no_grad() |
| def _extract_fft_features(self, x): |
| B, C, H, W = x.shape |
| device = x.device |
| x_f32 = x.float() |
| if C == 3: |
| gray = 0.299 * x_f32[:, 0] + 0.587 * x_f32[:, 1] + 0.114 * x_f32[:, 2] |
| else: |
| gray = x_f32[:, 0] |
| fft_img = torch.fft.fft2(gray) |
| fft_shift = torch.fft.fftshift(fft_img) |
| mag = torch.abs(fft_shift) + 1e-8 |
| mag = mag / (mag.max(dim=-1, keepdim=True)[0].max(dim=-2, keepdim=True)[0] + 1e-8) |
| fft_features = [] |
| for i in range(B): |
| m = mag[i].flatten() |
| feat = torch.stack([ |
| m.mean(), m.std().clamp(min=1e-8), m.max(), m.min(), |
| (m > m.mean()).float().mean(), m.median(), |
| mag[i][:H//4, :].mean(), mag[i][H//4:H//2, :].mean(), |
| mag[i][H//2:3*H//4, :].mean(), mag[i][3*H//4:, :].mean(), |
| (m > 0.5).float().mean(), (m > 0.1).float().mean(), |
| ]) |
| feat = torch.clamp(feat, min=-10, max=10) |
| fft_features.append(feat) |
| return torch.stack(fft_features, dim=0) |
|
|
| def forward(self, x): |
| fft_feat = self._extract_fft_features(x) |
| fft_feat = fft_feat.to(x.dtype).detach() |
| fft_feat.requires_grad_(True) |
| return self.fft_processor(fft_feat) |
|
|
|
|
| class EfficientNetFFTFusion(nn.Module): |
| """EfficientNet-B4 backbone with FFT feature fusion""" |
| def __init__(self, num_classes=2, dropout=0.4, backbone='efficientnet_b4'): |
| super().__init__() |
| if HAS_TIMM: |
| self.backbone = timm.create_model(backbone, pretrained=False, num_classes=0) |
| backbone_dim = self.backbone.num_features |
| else: |
| self.backbone = models.efficientnet_b4(weights=None) |
| backbone_dim = self.backbone.classifier[1].in_features |
| self.backbone.classifier = nn.Identity() |
| fft_dim = 512 |
| self.fft_extractor = FFTFeatureExtractor(output_dim=fft_dim) |
| fusion_dim = backbone_dim + fft_dim |
| self.fusion = nn.Sequential( |
| nn.Linear(fusion_dim, 1024), |
| nn.LayerNorm(1024), |
| nn.GELU(), |
| nn.Dropout(dropout), |
| nn.Linear(1024, 512), |
| nn.LayerNorm(512), |
| nn.GELU(), |
| nn.Dropout(dropout * 0.5), |
| ) |
| self.classifier = nn.Linear(512, num_classes) |
|
|
| def forward(self, x): |
| backbone_feat = self.backbone(x) |
| fft_feat = self.fft_extractor(x) |
| fused = torch.cat([backbone_feat, fft_feat], dim=1) |
| fused = self.fusion(fused) |
| return self.classifier(fused) |
|
|
|
|
| def make_overlay_pil(img_arr, heatmap, alpha=0.5, cmap='jet'): |
| |
| plt.figure(figsize=(6, 6), dpi=100) |
| plt.imshow(np.clip(img_arr, 0, 1)) |
| plt.imshow(heatmap, cmap=cmap, alpha=alpha, vmin=0, vmax=1) |
| plt.axis('off') |
| buf = io.BytesIO() |
| plt.savefig(buf, format='png', bbox_inches='tight', pad_inches=0) |
| plt.close() |
| buf.seek(0) |
| return Image.open(buf).convert('RGB') |
|
|
|
|
| def pad_to_min_size(img, size): |
| w, h = img.size |
| pad_w = max(0, size - w) |
| pad_h = max(0, size - h) |
| if pad_w or pad_h: |
| left = pad_w // 2 |
| right = pad_w - left |
| top = pad_h // 2 |
| bottom = pad_h - top |
| img = TF.pad(img, [left, top, right, bottom], padding_mode='reflect') |
| return img |
|
|
|
|
| |
| MODEL_PATH = None |
| MODEL = None |
| MODEL_INFO = None |
| |
| |
| |
| |
| |
| AUTO_THRESHOLD = 0.50 |
|
|
| |
| |
| TEMPERATURE_SCALE = 1.2 |
|
|
| def apply_temperature_scaling(prob): |
| """Apply temperature scaling to reduce overconfidence""" |
| import math |
| prob = max(1e-7, min(1 - 1e-7, prob)) |
| logit = math.log(prob / (1 - prob)) |
| scaled_logit = logit / TEMPERATURE_SCALE |
| return 1 / (1 + math.exp(-scaled_logit)) |
|
|
| |
| VIDEO_MODEL = None |
| VIDEO_MODEL_CONFIG = None |
| VIDEO_MODEL_PATH = 'video_resnet_lstm.pt' |
|
|
| TARGET_REAL_FPR = 0.02 |
| MAX_CALIB_IMAGES = 200 |
|
|
|
|
| def _pick_dataset_root(): |
| """Pick the first validation dataset that exists on disk.""" |
| candidates = [ |
| Path('C:/Users/DESHNA/Downloads/UAIDE_enhanced/CIFAKE'), |
| Path('DeepfakeVsReal/Dataset'), |
| Path('AI vs Real img'), |
| ] |
| for cand in candidates: |
| if (cand / 'Validation').exists(): |
| return str(cand) |
| return None |
|
|
| def _get_validation_files(dataset_root, max_val_images=None): |
| val_root = Path(dataset_root) / 'Validation' |
| if not val_root.exists(): |
| return None, None |
|
|
| real_files = list((val_root / 'Real').rglob('*.jpg')) + list((val_root / 'Real').rglob('*.png')) |
| fake_files = list((val_root / 'Fake').rglob('*.jpg')) + list((val_root / 'Fake').rglob('*.png')) |
| real_files = sorted([str(x) for x in real_files]) |
| fake_files = sorted([str(x) for x in fake_files]) |
|
|
| if max_val_images: |
| real_files = real_files[:max_val_images] |
| fake_files = fake_files[:max_val_images] |
|
|
| files = real_files + fake_files |
| labels = [0] * len(real_files) + [1] * len(fake_files) |
| if len(files) == 0: |
| return None, None |
| return files, labels |
|
|
|
|
| def _get_transform(model_type): |
| if model_type in ['resnet', 'fusion', 'fusion_improved']: |
| size = 224 |
| else: |
| size = 128 |
| return transforms.Compose([ |
| transforms.Lambda(lambda img: pad_to_min_size(img, size)), |
| transforms.CenterCrop(size), |
| transforms.ToTensor(), |
| transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) |
| ]) |
|
|
|
|
| def _load_video_model(checkpoint_path): |
| """Load video ResNetLSTM model from checkpoint.""" |
| if not Path(checkpoint_path).exists(): |
| return None, None |
| |
| try: |
| device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') |
| ckpt = torch.load(checkpoint_path, map_location=device) |
| config = ckpt.get('config', {}) |
| |
| model = ResNetLSTM( |
| hidden_size=config.get('hidden_size', 256), |
| num_layers=config.get('num_layers', 1), |
| bidirectional=config.get('bidirectional', True), |
| temporal_pool=config.get('temporal_pool', 'mean'), |
| pretrained=config.get('pretrained', False), |
| ) |
| model.load_state_dict(ckpt['model_state'], strict=True) |
| model.to(device) |
| model.eval() |
| |
| print(f'=== Loaded Video Model ===') |
| print(f"Checkpoint: {checkpoint_path}") |
| print(f"Config: {config}") |
| return model, config |
| except Exception as e: |
| print(f'Failed to load video model from {checkpoint_path}: {e}') |
| return None, None |
|
|
|
|
| def _build_video_transform(): |
| """Build transform for video frames.""" |
| return transforms.Compose([ |
| transforms.Resize((224, 224)), |
| transforms.ToTensor(), |
| transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), |
| ]) |
|
|
|
|
| def _extract_video_frames(video_path, frames_per_video=16, frame_stride=4, face_detection=False): |
| """Extract frames from video file.""" |
| try: |
| face_cropper = FaceCropper() if face_detection else None |
| frames = read_video_frames( |
| Path(video_path), |
| frames_per_video=frames_per_video, |
| frame_stride=frame_stride, |
| face_cropper=face_cropper, |
| ) |
| return frames |
| except Exception as e: |
| print(f'Video frame extraction failed: {e}') |
| return None |
|
|
|
|
| def _predict_video_model(model, config, video_path, return_frames=False): |
| """Predict deepfake probability using video model.""" |
| if model is None: |
| return None, None, None |
| |
| try: |
| device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') |
| |
| frames_per_video = config.get('frames_per_video', 16) |
| frame_stride = config.get('frame_stride', 4) |
| face_detection = config.get('face_detection', True) |
| |
| frames = _extract_video_frames(video_path, frames_per_video, frame_stride, face_detection) |
| if frames is None: |
| return None, None, None |
| |
| transform = _build_video_transform() |
| video_tensor = torch.stack([transform(Image.fromarray(f)) for f in frames], dim=0) |
| video_tensor = video_tensor.unsqueeze(0).to(device) |
| |
| model.eval() |
| with torch.no_grad(): |
| frame_logits, video_logits = model(video_tensor) |
| video_probs = torch.softmax(video_logits, dim=1).cpu().numpy()[0] |
| frame_probs = torch.softmax(frame_logits.squeeze(0), dim=1).cpu().numpy() |
| |
| prob_fake = float(video_probs[1]) |
| |
| if return_frames: |
| return prob_fake, frame_probs, frames |
| return prob_fake, frame_probs, None |
| except Exception as e: |
| print(f'Video model prediction failed: {e}') |
| return None, None, None |
|
|
|
|
|
|
| def _evaluate_deep_model(model, model_type, dataset_root, max_val_images=None): |
| """Evaluate deep learning models on validation set.""" |
| from train import ImageDataset |
| from torch.utils.data import DataLoader |
|
|
| files, labels = _get_validation_files(dataset_root, max_val_images=max_val_images) |
| if files is None: |
| return None |
|
|
| transform = _get_transform(model_type) |
| dataset = ImageDataset(files, labels, transform=transform) |
| dataloader = DataLoader(dataset, batch_size=8, shuffle=False, num_workers=0) |
|
|
| device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') |
| model.eval() |
|
|
| all_probs = [] |
| all_labels = [] |
| with torch.no_grad(): |
| for inputs, lbls in dataloader: |
| inputs = inputs.to(device) |
| outputs = model(inputs) |
| probs = torch.softmax(outputs, dim=1)[:, 1].cpu().numpy() |
| all_probs.extend(probs.tolist()) |
| all_labels.extend(lbls) |
|
|
| if len(all_labels) == 0: |
| return None |
|
|
| y_true = np.array(all_labels) |
| y_prob = np.array(all_probs) |
| y_pred = (y_prob >= 0.5).astype(int) |
|
|
| acc = accuracy_score(y_true, y_pred) |
| try: |
| auc = roc_auc_score(y_true, y_prob) |
| except Exception: |
| auc = None |
| report = classification_report(y_true, y_pred, zero_division=0) |
| return { |
| 'accuracy': acc, |
| 'auc': auc, |
| 'report': report, |
| 'count': len(y_true) |
| } |
|
|
|
|
| def _evaluate_ml_model(model, dataset_root, max_val_images=None, patch_size=128, n_patches=4): |
| from train import collect_features |
|
|
| val_root = Path(dataset_root) / 'Validation' |
| if not val_root.exists(): |
| return None |
|
|
| real_val = val_root / 'Real' |
| fake_val = val_root / 'Fake' |
| Xrv, yrv = collect_features(real_val, 0, max_images=max_val_images, patch_size=patch_size, n_patches=n_patches) |
| Xfv, yfv = collect_features(fake_val, 1, max_images=max_val_images, patch_size=patch_size, n_patches=n_patches) |
| if len(Xrv) + len(Xfv) == 0: |
| return None |
|
|
| Xv = np.array(Xrv + Xfv) |
| yv = np.array(yrv + yfv) |
| if hasattr(model, 'predict_proba'): |
| probs = model.predict_proba(Xv)[:, 1] |
| else: |
| probs = model.predict(Xv).astype(float) |
| preds = (probs >= 0.5).astype(int) |
|
|
| acc = accuracy_score(yv, preds) |
| try: |
| auc = roc_auc_score(yv, probs) |
| except Exception: |
| auc = None |
| report = classification_report(yv, preds, zero_division=0) |
| return { |
| 'accuracy': acc, |
| 'auc': auc, |
| 'report': report, |
| 'count': len(yv) |
| } |
|
|
|
|
| def _calibrate_threshold(model, model_info, dataset_root, target_real_fpr=0.05, max_val_images=200): |
| """Calibrate detection threshold using ROC/Youden J; fallback to real-FPR quantile.""" |
| if model is None or model_info is None: |
| return None |
|
|
| val_root = Path(dataset_root) / 'Validation' |
| if not val_root.exists(): |
| return None |
|
|
| mtype = model_info.get('model_type', 'unknown') if isinstance(model_info, dict) else 'unknown' |
| all_probs = [] |
| all_labels = [] |
|
|
| try: |
| if mtype in ['cnn', 'cnn_kfold', 'resnet', 'fusion', 'fusion_improved']: |
| from train import ImageDataset |
| from torch.utils.data import DataLoader |
|
|
| files, labels = _get_validation_files(dataset_root, max_val_images=max_val_images) |
| if files is None: |
| return None |
|
|
| transform = _get_transform(mtype) |
| dataset = ImageDataset(files, labels, transform=transform) |
| dataloader = DataLoader(dataset, batch_size=16, shuffle=False, num_workers=0) |
|
|
| device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') |
| model.eval() |
| with torch.no_grad(): |
| for inputs, lbls in dataloader: |
| inputs = inputs.to(device) |
| outputs = model(inputs) |
| probs = torch.softmax(outputs, dim=1)[:, 1].cpu().numpy() |
| all_probs.extend(probs.tolist()) |
| all_labels.extend(lbls.tolist()) |
| else: |
| from train import collect_features |
|
|
| real_val = val_root / 'Real' |
| fake_val = val_root / 'Fake' |
| Xrv, yrv = collect_features(real_val, 0, max_images=max_val_images, patch_size=128, n_patches=4) |
| Xfv, yfv = collect_features(fake_val, 1, max_images=max_val_images, patch_size=128, n_patches=4) |
| X = np.array(Xrv + Xfv) |
| y = np.array(yrv + yfv) |
| if len(X) == 0: |
| return None |
| if hasattr(model, 'predict_proba'): |
| probs = model.predict_proba(X)[:, 1] |
| else: |
| probs = model.predict(X).astype(float) |
| all_probs.extend(probs.tolist()) |
| all_labels.extend(y.tolist()) |
|
|
| if len(all_labels) == 0 or len(np.unique(all_labels)) < 2: |
| return None |
|
|
| |
| fpr, tpr, thresholds = roc_curve(np.array(all_labels), np.array(all_probs)) |
| j_scores = tpr - fpr |
| best_idx = int(np.argmax(j_scores)) |
| best_thresh = float(thresholds[best_idx]) |
| print(f'Calibrated threshold via Youden J (balanced): {best_thresh:.3f} (TPR={tpr[best_idx]:.3f}, FPR={fpr[best_idx]:.3f})') |
| return best_thresh |
| except Exception as e: |
| print(f'Youden calibration failed, falling back to real-FPR quantile: {e}') |
|
|
| |
| real_probs = [] |
| if mtype in ['cnn', 'cnn_kfold', 'resnet', 'fusion', 'fusion_improved']: |
| from train import ImageDataset |
| from torch.utils.data import DataLoader |
|
|
| files, labels = _get_validation_files(dataset_root, max_val_images=max_val_images) |
| if files is None: |
| return None |
| real_files = [f for f, lbl in zip(files, labels) if lbl == 0] |
| if len(real_files) == 0: |
| return None |
|
|
| transform = _get_transform(mtype) |
| dataset = ImageDataset(real_files, [0] * len(real_files), transform=transform) |
| dataloader = DataLoader(dataset, batch_size=16, shuffle=False, num_workers=0) |
|
|
| device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') |
| model.eval() |
| with torch.no_grad(): |
| for inputs, _ in dataloader: |
| inputs = inputs.to(device) |
| outputs = model(inputs) |
| probs = torch.softmax(outputs, dim=1)[:, 1].cpu().numpy() |
| real_probs.extend(probs.tolist()) |
| else: |
| from train import collect_features |
| real_val = val_root / 'Real' |
| Xrv, _ = collect_features(real_val, 0, max_images=max_val_images, patch_size=128, n_patches=4) |
| if len(Xrv) == 0: |
| return None |
| X = np.array(Xrv) |
| if hasattr(model, 'predict_proba'): |
| probs = model.predict_proba(X)[:, 1] |
| else: |
| probs = model.predict(X).astype(float) |
| real_probs.extend(probs.tolist()) |
|
|
| if len(real_probs) == 0: |
| return None |
|
|
| real_probs = np.array(real_probs) |
| target = max(0.0, min(1.0, float(target_real_fpr))) |
| if target <= 0.0: |
| return float(np.max(real_probs)) |
| return float(np.quantile(real_probs, 1.0 - target)) |
|
|
|
|
| def _load_model_from_info(info_path): |
| info = joblib.load(str(info_path)) |
| if not isinstance(info, dict) or 'model_type' not in info: |
| return None, None |
|
|
| def _model_base_from_info_path(path_obj): |
| path_str = str(path_obj) |
| if path_str.endswith('_improved_info.pkl'): |
| return path_str[:-len('_improved_info.pkl')] |
| if path_str.endswith('_info.pkl'): |
| return path_str[:-len('_info.pkl')] |
| return path_str |
|
|
| mtype = info['model_type'] |
| model = None |
|
|
| if mtype in ['cnn', 'cnn_kfold', 'resnet', 'fusion', 'fusion_improved']: |
| if mtype == 'resnet': |
| from train import DeepfakeResNet as _ModelClass |
| elif mtype in ['fusion', 'fusion_improved']: |
| from train import DeepfakeFeatureFusion as _ModelClass |
| else: |
| from train import DeepfakeCNN as _ModelClass |
|
|
| model = _ModelClass() |
| state_path = info.get('state_dict_path') |
| if state_path is None: |
| base = _model_base_from_info_path(info_path) |
| candidates = [base + '_best_improved', base + '_best', base] |
| for c in candidates: |
| if Path(c).exists(): |
| state_path = c |
| break |
| if state_path is None: |
| raise FileNotFoundError('state_dict_path not found in model info and no candidate file exists') |
|
|
| model.load_state_dict(torch.load(state_path, map_location='cpu')) |
| model.eval() |
| if torch.cuda.is_available(): |
| model.to(torch.device('cuda')) |
| else: |
| base = _model_base_from_info_path(info_path) |
| joblib_candidates = [base, base + '.joblib'] if base.endswith('.joblib') else [base + '.joblib', base] |
| for joblib_path in joblib_candidates: |
| if Path(joblib_path).exists(): |
| model = joblib.load(joblib_path) |
| break |
| if model is None: |
| model = info |
|
|
| return model, info |
|
|
|
|
| def load_model_fast(): |
| """Load a model quickly without validation evaluation for faster startup.""" |
| |
| info_candidates = list(Path('.').glob('*_improved_info.pkl')) + list(Path('.').glob('*_info.pkl')) |
| info_candidates = sorted(info_candidates, key=lambda p: p.name, reverse=True) |
| |
| if not info_candidates: |
| return None, None, None |
| |
| |
| for info_path in info_candidates: |
| try: |
| model, info = _load_model_from_info(info_path) |
| mtype = info.get('model_type', 'unknown') if isinstance(info, dict) else 'unknown' |
| print(f'=== Loaded Model (fast mode) ===') |
| print(f"Model: {info_path.name}") |
| print(f"Type: {mtype}") |
| return model, info, str(info_path) |
| except Exception as e: |
| print(f"Skipping {info_path.name}: failed to load model ({e})") |
| continue |
| |
| return None, None, None |
|
|
|
|
| def select_best_model(dataset_root='C:\\Users\\DESHNA\\Downloads\\UAIDE_enhanced\\CIFAKE', max_val_images=None): |
| info_candidates = list(Path('.').glob('*_improved_info.pkl')) + list(Path('.').glob('*_info.pkl')) |
| info_candidates = sorted(info_candidates, key=lambda p: p.name, reverse=True) |
| if not info_candidates: |
| return None, None, None |
|
|
| scored = [] |
| for info_path in info_candidates: |
| try: |
| model, info = _load_model_from_info(info_path) |
| except Exception as e: |
| print(f"Skipping {info_path.name}: failed to load model ({e})") |
| continue |
|
|
| mtype = info.get('model_type', 'unknown') if isinstance(info, dict) else 'unknown' |
| stats = None |
|
|
| if mtype in ['cnn', 'cnn_kfold', 'resnet', 'fusion', 'fusion_improved']: |
| try: |
| stats = _evaluate_deep_model(model, mtype, dataset_root, max_val_images=max_val_images) |
| except Exception as e: |
| print(f"Failed to evaluate deep model: {e}") |
| stats = None |
| else: |
| patch_size = info.get('patch_size', 128) if isinstance(info, dict) else 128 |
| n_patches = info.get('patches_per_image', 4) if isinstance(info, dict) else 4 |
| stats = _evaluate_ml_model(model, dataset_root, max_val_images=max_val_images, patch_size=patch_size, n_patches=n_patches) |
|
|
| if stats is None: |
| print(f"Skipping {info_path.name}: no validation stats") |
| continue |
|
|
| scored.append((info_path, model, info, stats)) |
| auc = stats.get('auc') |
| acc = stats.get('accuracy') |
| print('\n=== Model Evaluation ===') |
| print(f"Model: {info_path.name} | type: {mtype}") |
| print(f"Val samples: {stats.get('count', 0)}") |
| print(f"Accuracy: {acc:.4f}") |
| if auc is not None: |
| print(f"ROC AUC: {auc:.4f}") |
| print('Classification report:') |
| print(stats.get('report', 'N/A')) |
|
|
| if not scored: |
| return None, None, None |
|
|
| def _score_key(item): |
| _, _, _, st = item |
| auc = st.get('auc') |
| acc = st.get('accuracy') |
| return (auc if auc is not None else -1.0, acc) |
|
|
| scored.sort(key=_score_key, reverse=True) |
| best_path, best_model, best_info, best_stats = scored[0] |
| print('\n=== Selected Best Model ===') |
| print(f"Model: {best_path.name}") |
| print(f"Type: {best_info.get('model_type', 'unknown') if isinstance(best_info, dict) else 'unknown'}") |
| print(f"Accuracy: {best_stats.get('accuracy'):.4f}") |
| if best_stats.get('auc') is not None: |
| print(f"ROC AUC: {best_stats.get('auc'):.4f}") |
| return best_model, best_info, str(best_path) |
|
|
|
|
|
|
| |
| |
| |
|
|
| def calculate_optimal_threshold_from_metrics(metrics): |
| """ |
| Calculate optimal threshold using ROC curve analysis (Youden's J statistic). |
| Uses sensitivity and specificity from the classification report. |
| |
| Youden's J = Sensitivity + Specificity - 1 |
| |
| When model is biased (sensitivity >> specificity), we need to raise threshold |
| aggressively to reduce false positives (real images detected as AI). |
| """ |
| if not metrics: |
| return 0.5 |
|
|
| sensitivity = metrics.get('sensitivity', 0.5) |
| specificity = metrics.get('specificity', 0.5) |
| fpr = metrics.get('fpr', 0.5) |
| fnr = metrics.get('fnr', 0.5) |
|
|
| |
| youden_j = sensitivity + specificity - 1 |
|
|
| |
| imbalance = sensitivity - specificity |
|
|
| |
| |
| |
| if fpr > 0.15: |
| |
| fpr_adjustment = fpr * 1.2 + (fpr - 0.15) * 0.5 |
| else: |
| fpr_adjustment = fpr * 0.8 |
|
|
| |
| |
| if imbalance > 0: |
| |
| imbalance_adjustment = imbalance * (0.4 + 0.3 * imbalance) |
| else: |
| imbalance_adjustment = imbalance * 0.3 |
|
|
| |
| |
| if specificity < 0.85: |
| specificity_boost = (0.85 - specificity) * 0.5 |
| else: |
| specificity_boost = 0.0 |
|
|
| |
| adjustment = 0.5 * fpr_adjustment + 0.3 * imbalance_adjustment + 0.2 * specificity_boost |
|
|
| optimal_threshold = 0.5 + adjustment |
|
|
| |
| optimal_threshold = max(0.40, min(0.85, optimal_threshold)) |
|
|
| print(f" ROC Analysis (Youden's J Method):") |
| print(f" Sensitivity (TPR): {sensitivity:.3f} (detect fakes)") |
| print(f" Specificity (TNR): {specificity:.3f} (recognize real)") |
| print(f" False Positive Rate: {fpr:.3f} (real -> fake error)") |
| print(f" False Negative Rate: {fnr:.3f} (fake -> real error)") |
| print(f" Youden's J: {youden_j:.3f}") |
| print(f" Imbalance: {imbalance:+.3f}") |
| print(f" FPR Adjustment: +{fpr_adjustment:.3f}") |
| print(f" Imbalance Adjustment: +{imbalance_adjustment:.3f}") |
| print(f" Specificity Boost: +{specificity_boost:.3f}") |
| print(f" Combined Adjustment: +{adjustment:.3f}") |
| print(f" Optimal Threshold: {optimal_threshold:.3f}") |
|
|
| return optimal_threshold |
|
|
|
|
| def get_confidence_tier(probability, threshold): |
| """ |
| Get confidence tier instead of binary decision. |
| Returns tier name and confidence level. |
| """ |
| if probability >= threshold + 0.25: |
| return "HIGH_CONFIDENCE_AI", "Very likely AI-generated" |
| elif probability >= threshold + 0.10: |
| return "MEDIUM_CONFIDENCE_AI", "Likely AI-generated" |
| elif probability >= threshold: |
| return "LOW_CONFIDENCE_AI", "Possibly AI-generated (borderline)" |
| elif probability >= threshold - 0.15: |
| return "UNCERTAIN", "Uncertain - could be either" |
| elif probability >= threshold - 0.30: |
| return "LOW_CONFIDENCE_REAL", "Likely authentic" |
| else: |
| return "HIGH_CONFIDENCE_REAL", "Very likely authentic" |
|
|
|
|
| |
| |
|
|
| MODEL = None |
| MODEL_PATH = None |
| MODEL_INFO = None |
|
|
| |
| |
| |
| ADV_MODEL_PATH = Path('models_adv/best_model_weights.pt') |
| ADV_CONFIG_PATH = Path('models_adv/config.json') |
|
|
| if ADV_MODEL_PATH.exists(): |
| try: |
| device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') |
|
|
| |
| adv_config = {} |
| if ADV_CONFIG_PATH.exists(): |
| with open(ADV_CONFIG_PATH) as f: |
| adv_config = json.load(f) |
|
|
| |
| state_dict = torch.load(ADV_MODEL_PATH, map_location=device, weights_only=False) |
|
|
| |
| |
| fusion_in_dim = state_dict['fusion.0.weight'].shape[1] |
| backbone_dim = fusion_in_dim - 512 |
|
|
| |
| backbone_map = {1280: 'efficientnet_b0', 1408: 'efficientnet_b2', 1792: 'efficientnet_b4'} |
| backbone = backbone_map.get(backbone_dim, 'efficientnet_b2') |
|
|
| |
| if not HAS_TIMM: |
| raise ImportError("timm is required for EfficientNet models. Install with: pip install timm") |
|
|
| |
| MODEL = EfficientNetFFTFusion(num_classes=2, dropout=0.4, backbone=backbone) |
| MODEL.load_state_dict(state_dict) |
| MODEL.to(device) |
| MODEL.eval() |
|
|
| MODEL_PATH = str(ADV_MODEL_PATH) |
| MODEL_INFO = { |
| 'model_type': 'efficientnet_fft', |
| 'backbone': backbone, |
| 'accuracy': adv_config.get('best_metrics', {}).get('accuracy', 86.0), |
| 'auc': adv_config.get('best_metrics', {}).get('auc_roc', 0.9394), |
| 'optimal_threshold': 0.50, |
| **adv_config |
| } |
|
|
| mod_time = datetime.fromtimestamp(ADV_MODEL_PATH.stat().st_mtime).strftime('%Y-%m-%d %H:%M:%S') |
| print(f'\n=== Loaded Recent Model (PRIORITY 1) ===') |
| print(f"Model: {ADV_MODEL_PATH.name}") |
| print(f"Backbone: {MODEL_INFO.get('backbone', 'unknown')}") |
| print(f"Accuracy: {MODEL_INFO.get('accuracy', 'unknown')}%") |
| print(f"AUC: {MODEL_INFO.get('auc', 'unknown')}") |
| print(f"Modified: {mod_time}") |
| print(f"Status: Ready for inference ✓") |
|
|
| except Exception as e: |
| print(f'Failed to load models_adv model: {e}') |
| MODEL = None |
|
|
| |
| |
| |
| EFFICIENTNET_MODEL_PATH = Path('models_v2/best_model.pt') |
| EFFICIENTNET_WEIGHTS_PATH = Path('models_v2/best_model_weights.pt') |
| EFFICIENTNET_CONFIG_PATH = Path('models_v2/config.json') |
| EFFICIENTNET_REPORT_PATH = Path('models_v2/classification_report.json') |
|
|
| if MODEL is None and (EFFICIENTNET_MODEL_PATH.exists() or EFFICIENTNET_WEIGHTS_PATH.exists()): |
| try: |
| device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') |
|
|
| |
| config = {} |
| if EFFICIENTNET_CONFIG_PATH.exists(): |
| with open(EFFICIENTNET_CONFIG_PATH) as f: |
| config = json.load(f) |
|
|
| |
| report = {} |
| if EFFICIENTNET_REPORT_PATH.exists(): |
| with open(EFFICIENTNET_REPORT_PATH) as f: |
| report = json.load(f) |
|
|
| |
| backbone = config.get('backbone', 'efficientnet_b4') |
| MODEL = EfficientNetFFTFusion(num_classes=2, dropout=0.4, backbone=backbone) |
|
|
| |
| if EFFICIENTNET_MODEL_PATH.exists(): |
| checkpoint = torch.load(EFFICIENTNET_MODEL_PATH, map_location=device, weights_only=False) |
| if 'model_state_dict' in checkpoint: |
| MODEL.load_state_dict(checkpoint['model_state_dict']) |
| else: |
| MODEL.load_state_dict(checkpoint) |
| else: |
| MODEL.load_state_dict(torch.load(EFFICIENTNET_WEIGHTS_PATH, map_location=device, weights_only=False)) |
|
|
| MODEL.to(device) |
| MODEL.eval() |
|
|
| |
| metrics = report.get('metrics', {}) |
|
|
| |
| AUTO_THRESHOLD = calculate_optimal_threshold_from_metrics(metrics) |
|
|
| MODEL_INFO = { |
| 'model_type': 'efficientnet_fft', |
| 'backbone': backbone, |
| 'image_size': config.get('image_size', 224), |
| 'accuracy': metrics.get('accuracy', 0.89), |
| 'auc_roc': metrics.get('auc_roc', 0.96), |
| 'optimal_threshold': AUTO_THRESHOLD, |
| } |
| MODEL_PATH = str(EFFICIENTNET_MODEL_PATH) |
|
|
| print('=' * 60) |
| print(' LOADED: EfficientNet-B4 + FFT Fusion Model') |
| print('=' * 60) |
| print(f" Path: {MODEL_PATH}") |
| print(f" Backbone: {backbone}") |
| print(f" Accuracy: {metrics.get('accuracy', 'N/A'):.2%}" if metrics.get('accuracy') else " Accuracy: N/A") |
| print(f" AUC-ROC: {metrics.get('auc_roc', 'N/A'):.4f}" if metrics.get('auc_roc') else " AUC-ROC: N/A") |
| print(f" Threshold (ROC-optimized): {AUTO_THRESHOLD:.3f}") |
| print('=' * 60) |
|
|
| except Exception as e: |
| print(f'Failed to load EfficientNet model: {e}') |
| MODEL = None |
|
|
| |
| |
| |
| if MODEL is None: |
| |
| model_candidates = sorted( |
| list(Path('.').glob('model*.joblib')) + |
| list(Path('.').glob('model*.pt')) + |
| [Path('model_fusion_best_improved')] + |
| [Path('model_fusion_best_improved_best_improved')], |
| key=lambda p: p.stat().st_mtime if p.exists() else 0, |
| reverse=True |
| ) |
|
|
| if model_candidates: |
| for model_path in model_candidates: |
| try: |
| |
| try: |
| MODEL = joblib.load(str(model_path)) |
| print(f'[OK] Loaded joblib model: {model_path.name}') |
| except Exception as joblib_err: |
| |
| try: |
| state_dict = torch.load(str(model_path), map_location='cpu') |
|
|
| |
| model_info_path = Path(str(model_path).rsplit('.', 1)[0] + '_info.pkl') |
| if model_info_path.exists(): |
| MODEL_INFO = joblib.load(str(model_info_path)) |
| else: |
| MODEL_INFO = {'model_type': 'fusion_improved'} |
|
|
| model_type = MODEL_INFO.get('model_type', 'fusion_improved') |
|
|
| |
| if model_type in ['fusion', 'fusion_improved']: |
| from train import DeepfakeFeatureFusion |
| MODEL = DeepfakeFeatureFusion() |
| MODEL.load_state_dict(state_dict) |
| MODEL.eval() |
| if torch.cuda.is_available(): |
| MODEL.cuda() |
| print(f'[OK] Loaded PyTorch fusion model: {model_path.name}') |
| elif model_type == 'resnet': |
| from train import DeepfakeResNet |
| MODEL = DeepfakeResNet() |
| MODEL.load_state_dict(state_dict) |
| MODEL.eval() |
| print(f'[OK] Loaded PyTorch ResNet model: {model_path.name}') |
| elif model_type == 'cnn': |
| from train import DeepfakeCNN |
| MODEL = DeepfakeCNN() |
| MODEL.load_state_dict(state_dict) |
| MODEL.eval() |
| print(f'[OK] Loaded PyTorch CNN model: {model_path.name}') |
| else: |
| raise ValueError(f"Unknown model type: {model_type}") |
| except Exception as torch_err: |
| raise Exception(f"Joblib failed: {joblib_err}; PyTorch failed: {torch_err}") |
|
|
| MODEL_PATH = str(model_path) |
|
|
| |
| if MODEL_INFO is None: |
| model_info_path = Path(str(model_path).rsplit('.', 1)[0] + '_info.pkl') |
| if not model_info_path.exists(): |
| model_base = str(model_path).replace('.joblib', '').replace('_best_improved', '').replace('_best', '') |
| for info_candidate in [model_base + '_info.pkl', model_base + '_improved_info.pkl']: |
| if Path(info_candidate).exists(): |
| model_info_path = Path(info_candidate) |
| break |
|
|
| if model_info_path.exists(): |
| MODEL_INFO = joblib.load(str(model_info_path)) |
| else: |
| MODEL_INFO = {'model_type': 'unknown'} |
|
|
| |
| mod_time = datetime.fromtimestamp(model_path.stat().st_mtime).strftime('%Y-%m-%d %H:%M:%S') |
| print(f'=== Loaded Latest Model ===') |
| print(f"Model: {model_path.name}") |
| print(f"Modified: {mod_time}") |
| model_type = MODEL_INFO.get("model_type", "unknown") if isinstance(MODEL_INFO, dict) else "unknown" |
| print(f"Type: {model_type}") |
| print(f"Path: {model_path.resolve()}") |
| break |
| except Exception as e: |
| print(f'Failed to load {model_path.name}: {e}') |
| continue |
|
|
| if MODEL is None: |
| print('No valid model found; falling back to heuristic detection.') |
| MODEL_INFO = None |
| MODEL_PATH = None |
| else: |
| |
| |
| print(f'Using fixed threshold: {AUTO_THRESHOLD:.3f} (high confidence mode)') |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| |
| if Path(VIDEO_MODEL_PATH).exists(): |
| VIDEO_MODEL, VIDEO_MODEL_CONFIG = _load_video_model(VIDEO_MODEL_PATH) |
| else: |
| print(f'Video model not found: {VIDEO_MODEL_PATH}') |
|
|
|
|
| def extract_image_features_from_array(img_arr, patch_size=128, n_patches=8, random_state=None): |
| |
| H, W, _ = img_arr.shape |
| patches = [] |
| rng = np.random.RandomState(random_state) |
| for _ in range(n_patches): |
| if H <= patch_size or W <= patch_size: |
| y0 = max(0, (H - patch_size) // 2) |
| x0 = max(0, (W - patch_size) // 2) |
| else: |
| y0 = int(rng.randint(0, H - patch_size + 1)) |
| x0 = int(rng.randint(0, W - patch_size + 1)) |
| patch = img_arr[y0:y0 + patch_size, x0:x0 + patch_size] |
| if patch.shape[0] != patch_size or patch.shape[1] != patch_size: |
| ph = np.zeros((patch_size, patch_size, 3), dtype=patch.dtype) |
| ph[:patch.shape[0], :patch.shape[1]] = patch |
| patch = ph |
| patches.append(patch) |
|
|
| feats = [] |
| for p in patches: |
| g = rgb_to_gray(p) |
| res = extract_residual(g) |
| res_std = float(np.std(res)) |
| _, hf = fft_stats(g) |
| |
| ent = lbp_entropy((g * 255).astype(np.uint8)) |
| feats.append([res_std, hf, ent]) |
| feats = np.array(feats) |
| mean = feats.mean(axis=0) |
| std = feats.std(axis=0) |
| return np.concatenate([mean, std])[None, :] |
| |
| def evaluate_model_on_validation(model, dataset_root='C:\\Users\\DESHNA\\Downloads\\UAIDE_enhanced\\CIFAKE'): |
| p = Path(dataset_root) |
| val_root = p / 'Validation' |
| if not val_root.exists(): |
| return 'Validation folder not found under ' + str(p) |
|
|
| real_folder = val_root / 'Real' |
| fake_folder = val_root / 'Fake' |
| files_real = sorted([str(x) for x in real_folder.rglob('*.jpg')] + [str(x) for x in real_folder.rglob('*.png')]) |
| files_fake = sorted([str(x) for x in fake_folder.rglob('*.jpg')] + [str(x) for x in fake_folder.rglob('*.png')]) |
| files = [(f, 0) for f in files_real] + [(f, 1) for f in files_fake] |
| X = [] |
| y = [] |
| for f, lbl in files: |
| try: |
| pil = Image.open(f).convert('RGB') |
| arr = np.asarray(pil).astype(np.float32) / 255.0 |
| feat = extract_image_features_from_array(arr, patch_size=128, n_patches=8, random_state=123) |
| X.append(feat[0]) |
| y.append(lbl) |
| except Exception as e: |
| continue |
| if len(X) == 0: |
| return 'No validation images found or feature extraction failed.' |
| X = np.array(X) |
| y = np.array(y) |
| try: |
| if hasattr(model, 'predict_proba'): |
| probs = model.predict_proba(X)[:, 1] |
| else: |
| probs = model.predict(X).astype(float) |
| preds = (probs >= 0.5).astype(int) |
| acc = accuracy_score(y, preds) |
| try: |
| auc = roc_auc_score(y, probs) |
| except Exception: |
| auc = None |
| report = classification_report(y, preds) |
| lines = [f'Val accuracy: {acc:.4f}'] |
| if auc is not None: |
| lines.append(f'Val ROC AUC: {auc:.4f}') |
| lines.append('\n'+report) |
| return '\n'.join(lines) |
| except Exception as e: |
| return f'Evaluation failed: {e}' |
|
|
|
|
| def predict_video_gradio(video_file, ethical_threshold=0.5, show_raw_features=False): |
| """Predict deepfake probability for video input.""" |
| ethical_status = "N/A" |
| ethical_report = "" |
| |
| if VIDEO_MODEL is None or VIDEO_MODEL_CONFIG is None: |
| return "Model Error", 0.0, None, "Video model not loaded", "Please ensure video_resnet_lstm.pt exists" |
| |
| try: |
| |
| if isinstance(video_file, str): |
| video_path = video_file |
| else: |
| video_path = video_file.name if hasattr(video_file, 'name') else str(video_file) |
| |
| |
| prob_fake, frame_probs, frames = _predict_video_model( |
| VIDEO_MODEL, |
| VIDEO_MODEL_CONFIG, |
| video_path, |
| return_frames=True |
| ) |
|
|
| if prob_fake is None: |
| return "Error", 0.0, None, "Failed to process video", "Video processing failed" |
|
|
| |
| prob_fake = apply_temperature_scaling(prob_fake) |
|
|
| |
| threshold = AUTO_THRESHOLD if AUTO_THRESHOLD is not None else VIDEO_MODEL_CONFIG.get('optimal_threshold', 0.85) |
| is_ai = prob_fake >= threshold |
| label = 'AI-generated' if is_ai else 'Real (camera)' |
| |
| |
| visualization = None |
| if frames is not None and len(frames) > 0: |
| try: |
| |
| top_frame_idx = int(np.argmax(frame_probs[:, 1])) |
| frame = frames[top_frame_idx] |
| |
| |
| device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') |
| target_layer = VIDEO_MODEL.backbone.layer4[-1].conv3 |
| grad_cam = GradCAM(VIDEO_MODEL, target_layer) |
| |
| transform = _build_video_transform() |
| frame_tensor = transform(Image.fromarray(frame)).unsqueeze(0).unsqueeze(0).to(device) |
| cam = grad_cam.generate(frame_tensor, class_idx=1) |
| |
| |
| visualization = overlay_cam(frame, cam, alpha=0.5) |
| except Exception as e: |
| print(f"Grad-CAM visualization failed: {e}") |
| |
| if frames and len(frames) > 0: |
| visualization = frames[0] |
| |
| |
| if visualization is not None: |
| overlay_pil = Image.fromarray(visualization) |
| else: |
| |
| overlay_pil = Image.new('RGB', (224, 224), color=(128, 128, 128)) |
| |
| |
| if is_ai and frames and len(frames) > 0: |
| |
| img_arr = np.asarray(frames[0]).astype(np.float32) / 255.0 |
| assessment = EthicalAssessment.assess(img_arr, threshold=ethical_threshold) |
| ethical_status = get_enhanced_ethical_status(assessment) |
| ethical_report = format_ethical_report(assessment) |
| if not show_raw_features: |
| idx = ethical_report.find('\nRAW FEATURES:') |
| if idx != -1: |
| ethical_report = ethical_report[:idx] |
| |
| return label, prob_fake, overlay_pil, ethical_status, ethical_report |
| |
| except Exception as e: |
| print(f"Video prediction failed: {e}") |
| return "Error", 0.0, None, "Prediction failed", str(e) |
|
|
|
|
| def predict_gradio(pil_img, ethical_threshold=0.5, show_raw_features=False): |
| |
| img = np.asarray(pil_img).astype(np.float32) / 255.0 |
|
|
| |
| ethical_status = "N/A" |
| ethical_report = "" |
|
|
| if MODEL is not None and MODEL_INFO is not None: |
| try: |
| if MODEL_INFO['model_type'] in ['cnn', 'cnn_kfold', 'resnet', 'fusion', 'fusion_improved', 'efficientnet_fft']: |
| |
| device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') |
|
|
| |
| if MODEL_INFO['model_type'] == 'efficientnet_fft': |
| |
| img_size = MODEL_INFO.get('image_size', 224) |
| transform = transforms.Compose([ |
| transforms.Resize((img_size, img_size)), |
| transforms.ToTensor(), |
| transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) |
| ]) |
| elif MODEL_INFO['model_type'] in ['resnet', 'fusion', 'fusion_improved']: |
| transform = transforms.Compose([ |
| transforms.Lambda(lambda img: pad_to_min_size(img, 224)), |
| transforms.CenterCrop(224), |
| transforms.ToTensor(), |
| transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) |
| ]) |
| else: |
| transform = transforms.Compose([ |
| transforms.Lambda(lambda img: pad_to_min_size(img, 128)), |
| transforms.CenterCrop(128), |
| transforms.ToTensor(), |
| transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) |
| ]) |
|
|
| input_tensor = transform(pil_img).unsqueeze(0).to(device) |
|
|
| |
| with torch.no_grad(): |
| outputs = MODEL(input_tensor) |
| probs = torch.softmax(outputs, dim=1) |
| prob_fake_raw = float(probs[0, 1]) |
|
|
| |
| prob_fake = apply_temperature_scaling(prob_fake_raw) |
|
|
| |
| threshold = AUTO_THRESHOLD if AUTO_THRESHOLD is not None else MODEL_INFO.get('optimal_threshold', 0.85) |
| pred_class = 1 if prob_fake >= threshold else 0 |
|
|
| label = 'AI-generated' if pred_class == 1 else 'Real (camera)' |
| is_ai = pred_class == 1 |
|
|
| |
| try: |
| overlay_img = apply_gradcam_overlay_from_pil(pil_img, MODEL, MODEL_INFO['model_type']) |
| overlay_pil = Image.fromarray(overlay_img) |
| except Exception as e: |
| print(f"Grad-CAM failed: {e}") |
| |
| patch_scores, coords, img_shape, ps, st = sliding_patch_scores(img, patch_size=128, stride=64) |
| heat = reconstruct_heatmap(patch_scores, coords, img_shape, ps, st) |
| overlay_pil = make_overlay_pil(img, heat) |
|
|
| |
| if is_ai: |
| assessment = EthicalAssessment.assess(img, threshold=ethical_threshold) |
| ethical_status = get_enhanced_ethical_status(assessment) |
| ethical_report = format_ethical_report(assessment) |
| if not show_raw_features: |
| idx = ethical_report.find('\nRAW FEATURES:') |
| if idx != -1: |
| ethical_report = ethical_report[:idx] |
|
|
| return label, prob_fake, overlay_pil, ethical_status, ethical_report |
|
|
| else: |
| |
| X = extract_image_features_from_array(img, patch_size=128, n_patches=8, random_state=0) |
| if hasattr(MODEL, 'predict_proba'): |
| prob_raw = float(MODEL.predict_proba(X)[:, 1][0]) |
| else: |
| pred = MODEL.predict(X)[0] |
| prob_raw = float(pred) |
|
|
| |
| prob = apply_temperature_scaling(prob_raw) |
|
|
| |
| threshold = AUTO_THRESHOLD if AUTO_THRESHOLD is not None else MODEL_INFO.get('optimal_threshold', 0.85) |
| is_ai = prob >= threshold |
| label = 'AI-generated' if is_ai else 'Real (camera)' |
|
|
| |
| patch_scores, coords, img_shape, ps, st = sliding_patch_scores(img, patch_size=128, stride=64) |
| heat = reconstruct_heatmap(patch_scores, coords, img_shape, ps, st) |
| overlay = make_overlay_pil(img, heat) |
|
|
| |
| if is_ai: |
| assessment = EthicalAssessment.assess(img, threshold=ethical_threshold) |
| ethical_status = get_enhanced_ethical_status(assessment) |
| ethical_report = format_ethical_report(assessment) |
| if not show_raw_features: |
| idx = ethical_report.find('\nRAW FEATURES:') |
| if idx != -1: |
| ethical_report = ethical_report[:idx] |
|
|
| return label, prob, overlay, ethical_status, ethical_report |
|
|
| except Exception as e: |
| print(f"Model prediction failed: {e}") |
| |
|
|
| |
| patch_scores, coords, img_shape, ps, st = sliding_patch_scores(img, patch_size=128, stride=64) |
| heat = reconstruct_heatmap(patch_scores, coords, img_shape, ps, st) |
| overlay = make_overlay_pil(img, heat) |
|
|
| ai_score_raw = float(np.mean(heat)) |
| |
| ai_score = apply_temperature_scaling(ai_score_raw) |
| threshold = AUTO_THRESHOLD if AUTO_THRESHOLD is not None else 0.85 |
| is_ai = ai_score >= threshold |
| label = 'AI-generated' if is_ai else 'Real (camera)' |
| |
| |
| if is_ai: |
| assessment = EthicalAssessment.assess(img, threshold=ethical_threshold) |
| ethical_status = get_enhanced_ethical_status(assessment) |
| ethical_report = format_ethical_report(assessment) |
| if not show_raw_features: |
| idx = ethical_report.find('\nRAW FEATURES:') |
| if idx != -1: |
| ethical_report = ethical_report[:idx] |
| |
| return label, ai_score, overlay, ethical_status, ethical_report |
|
|
|
|
| def apply_gradcam_overlay_from_pil(pil_img, model, model_type): |
| """Apply Grad-CAM to PIL image for deep learning models""" |
| device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') |
|
|
| |
| if model_type == 'efficientnet_fft': |
| img_size = MODEL_INFO.get('image_size', 224) if MODEL_INFO else 224 |
| transform = transforms.Compose([ |
| transforms.Resize((img_size, img_size)), |
| transforms.ToTensor(), |
| transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) |
| ]) |
| target_size = (img_size, img_size) |
| elif model_type in ['resnet', 'fusion', 'fusion_improved']: |
| transform = transforms.Compose([ |
| transforms.Lambda(lambda img: pad_to_min_size(img, 224)), |
| transforms.CenterCrop(224), |
| transforms.ToTensor(), |
| transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) |
| ]) |
| target_size = (224, 224) |
| else: |
| transform = transforms.Compose([ |
| transforms.Lambda(lambda img: pad_to_min_size(img, 128)), |
| transforms.CenterCrop(128), |
| transforms.ToTensor(), |
| transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) |
| ]) |
| target_size = (128, 128) |
|
|
| input_tensor = transform(pil_img).unsqueeze(0).to(device) |
|
|
| |
| if model_type == 'efficientnet_fft': |
| |
| if HAS_TIMM: |
| |
| target_layer = model.backbone.conv_head |
| else: |
| |
| target_layer = model.backbone.features[-1] |
| elif model_type == 'resnet': |
| target_layer = model.resnet.layer4[-1].conv3 |
| elif model_type in ['fusion', 'fusion_improved']: |
| target_layer = model.resnet[7][-1].conv3 |
| else: |
| target_layer = model.conv4 |
|
|
| |
| if model_type == 'efficientnet_fft': |
| |
| activations = [] |
| gradients = [] |
|
|
| def forward_hook(module, input, output): |
| activations.append(output) |
|
|
| def backward_hook(module, grad_in, grad_out): |
| gradients.append(grad_out[0]) |
|
|
| handle_f = target_layer.register_forward_hook(forward_hook) |
| handle_b = target_layer.register_full_backward_hook(backward_hook) |
|
|
| model.eval() |
| output = model(input_tensor) |
| model.zero_grad() |
| output[0, 1].backward() |
|
|
| handle_f.remove() |
| handle_b.remove() |
|
|
| |
| act = activations[0].detach() |
| grad = gradients[0].detach() |
| weights = grad.mean(dim=(2, 3), keepdim=True) |
| cam = (weights * act).sum(dim=1, keepdim=True) |
| cam = F.relu(cam) |
| cam = cam - cam.min() |
| cam = cam / (cam.max() + 1e-8) |
| cam = F.interpolate(cam, size=target_size, mode='bilinear', align_corners=False) |
| cam = cam.squeeze().cpu().numpy() |
| else: |
| |
| from train import GradCAM |
| grad_cam = GradCAM(model, target_layer) |
| cam = grad_cam.generate_cam(input_tensor, target_class=1) |
|
|
| |
| heatmap = cv2.applyColorMap(np.uint8(255 * cam), cv2.COLORMAP_JET) |
| heatmap = cv2.cvtColor(heatmap, cv2.COLOR_BGR2RGB) |
|
|
| |
| original = cv2.resize(np.array(pil_img), target_size) |
|
|
| |
| overlay = cv2.addWeighted(original, 0.6, heatmap, 0.4, 0) |
|
|
| return overlay |
|
|
|
|
| title = "Advanced Deepfake Detection System with Ethical Assessment" |
|
|
| |
| with gr.Blocks(title=title) as iface: |
| gr.Markdown(f""" |
| # {title} |
| |
| Upload an image or video to detect if it's AI-generated and assess its ethical status. |
| |
| **Image Model:** {MODEL_INFO['model_type'].upper() if MODEL_INFO else 'Heuristic-based'} |
| **Video Model:** {'ResNetLSTM (Video)' if VIDEO_MODEL else 'Not loaded'} |
| """) |
| |
| with gr.Tabs(): |
| |
| with gr.Tab("Image Detection"): |
| with gr.Row(): |
| |
| with gr.Column(scale=1): |
| gr.Markdown("### Input") |
| input_image = gr.Image(type='pil', label='Upload Image', height=400) |
| |
| gr.Markdown("### Settings") |
| ethical_threshold_img = gr.Slider( |
| minimum=0.0, |
| maximum=1.0, |
| step=0.01, |
| value=0.5, |
| label='Ethical Risk Threshold', |
| info='Lower = more strict classification' |
| ) |
| show_raw_features_img = gr.Checkbox( |
| label='Show raw feature values', |
| value=False |
| ) |
| |
| analyze_img_btn = gr.Button("Analyze Image", variant="primary", size="lg") |
| |
| gr.Markdown(""" |
| --- |
| **Features:** |
| - Deep Learning: CNN/ResNet with transfer learning |
| - Grad-CAM visualization highlights suspicious regions |
| - Ethical assessment evaluates privacy and misuse risks |
| - Real-time GPU-accelerated inference |
| """) |
| |
| |
| with gr.Column(scale=1): |
| gr.Markdown("### Detection Results") |
| |
| with gr.Row(): |
| detection_result_img = gr.Label(num_top_classes=2, label='Classification') |
| ai_score_img = gr.Number(label='AI-likelihood Score', precision=4) |
| |
| heatmap_img = gr.Image(label='Detection Heatmap', height=400) |
| |
| gr.Markdown("### Ethical Assessment") |
| ethical_status_img = gr.Textbox(label='Status', lines=2) |
| |
| with gr.Accordion("Full Report", open=False): |
| ethical_report_img = gr.Textbox( |
| label='Detailed Assessment', |
| lines=30 |
| ) |
| |
| |
| analyze_img_btn.click( |
| fn=predict_gradio, |
| inputs=[input_image, ethical_threshold_img, show_raw_features_img], |
| outputs=[detection_result_img, ai_score_img, heatmap_img, ethical_status_img, ethical_report_img] |
| ) |
| |
| |
| with gr.Tab("Video Detection"): |
| with gr.Row(): |
| |
| with gr.Column(scale=1): |
| gr.Markdown("### Input") |
| input_video = gr.Video(label='Upload Video', height=400) |
| |
| gr.Markdown("### Settings") |
| ethical_threshold_vid = gr.Slider( |
| minimum=0.0, |
| maximum=1.0, |
| step=0.01, |
| value=0.5, |
| label='Ethical Risk Threshold', |
| info='Lower = more strict classification' |
| ) |
| show_raw_features_vid = gr.Checkbox( |
| label='Show raw feature values', |
| value=False |
| ) |
| |
| analyze_vid_btn = gr.Button("Analyze Video", variant="primary", size="lg") |
| |
| gr.Markdown(""" |
| --- |
| **Features:** |
| - ResNetLSTM: temporal modeling with LSTM |
| - Frame-level & video-level predictions |
| - Grad-CAM on most suspicious frame |
| - Ethical assessment from frame content |
| - Supports MP4, AVI, MOV, MKV, WebM |
| """) |
| |
| |
| with gr.Column(scale=1): |
| gr.Markdown("### Detection Results") |
| |
| with gr.Row(): |
| detection_result_vid = gr.Label(num_top_classes=2, label='Classification') |
| ai_score_vid = gr.Number(label='AI-likelihood Score', precision=4) |
| |
| heatmap_vid = gr.Image(label='Suspicious Frame (with Grad-CAM)', height=400) |
| |
| gr.Markdown("### Ethical Assessment") |
| ethical_status_vid = gr.Textbox(label='Status', lines=2) |
| |
| with gr.Accordion("Full Report", open=False): |
| ethical_report_vid = gr.Textbox( |
| label='Detailed Assessment', |
| lines=30 |
| ) |
| |
| |
| analyze_vid_btn.click( |
| fn=predict_video_gradio, |
| inputs=[input_video, ethical_threshold_vid, show_raw_features_vid], |
| outputs=[detection_result_vid, ai_score_vid, heatmap_vid, ethical_status_vid, ethical_report_vid] |
| ) |
| |
| gr.Markdown(""" |
| --- |
| **How it works:** |
| - **Image**: The heatmap overlay shows regions the model considers suspicious for deepfake artifacts. |
| - **Video**: Frames are processed temporally, and the most suspicious frame is highlighted with Grad-CAM. |
| - Ethical classification is based on artifact detectability and human face presence. |
| |
| *Powered by FHIBE Dataset concepts for face authenticity verification.* |
| """) |
|
|
|
|
| if __name__ == '__main__': |
| iface.launch() |
| ======= |
| import io
|
| from PIL import Image
|
| import numpy as np
|
| import gradio as gr
|
| import matplotlib
|
| matplotlib.use('Agg')
|
| import matplotlib.pyplot as plt
|
| import joblib
|
| from pathlib import Path
|
| from sklearn.metrics import accuracy_score, roc_auc_score, classification_report
|
| import torch
|
| import torch.nn as nn
|
| from torchvision import transforms
|
| import torchvision.transforms.functional as TF
|
| import cv2
|
|
|
| from detector import sliding_patch_scores, reconstruct_heatmap, rgb_to_gray, extract_residual, fft_stats, lbp_entropy
|
| from ethical_assessment import EthicalAssessment, format_ethical_report, get_simple_status
|
|
|
|
|
| def make_overlay_pil(img_arr, heatmap, alpha=0.5, cmap='jet'):
|
|
|
| plt.figure(figsize=(6, 6), dpi=100)
|
| plt.imshow(np.clip(img_arr, 0, 1))
|
| plt.imshow(heatmap, cmap=cmap, alpha=alpha, vmin=0, vmax=1)
|
| plt.axis('off')
|
| buf = io.BytesIO()
|
| plt.savefig(buf, format='png', bbox_inches='tight', pad_inches=0)
|
| plt.close()
|
| buf.seek(0)
|
| return Image.open(buf).convert('RGB')
|
|
|
|
|
| def pad_to_min_size(img, size):
|
| w, h = img.size
|
| pad_w = max(0, size - w)
|
| pad_h = max(0, size - h)
|
| if pad_w or pad_h:
|
| left = pad_w // 2
|
| right = pad_w - left
|
| top = pad_h // 2
|
| bottom = pad_h - top
|
| img = TF.pad(img, [left, top, right, bottom], padding_mode='reflect')
|
| return img
|
|
|
|
|
|
|
| MODEL_PATH = None
|
| MODEL = None
|
| MODEL_INFO = None
|
| AUTO_THRESHOLD = None
|
|
|
| TARGET_REAL_FPR = 0.05
|
| MAX_CALIB_IMAGES = 200
|
|
|
| def _get_validation_files(dataset_root, max_val_images=None):
|
| val_root = Path(dataset_root) / 'Validation'
|
| if not val_root.exists():
|
| return None, None
|
|
|
| real_files = list((val_root / 'Real').rglob('*.jpg')) + list((val_root / 'Real').rglob('*.png'))
|
| fake_files = list((val_root / 'Fake').rglob('*.jpg')) + list((val_root / 'Fake').rglob('*.png'))
|
| real_files = sorted([str(x) for x in real_files])
|
| fake_files = sorted([str(x) for x in fake_files])
|
|
|
| if max_val_images:
|
| real_files = real_files[:max_val_images]
|
| fake_files = fake_files[:max_val_images]
|
|
|
| files = real_files + fake_files
|
| labels = [0] * len(real_files) + [1] * len(fake_files)
|
| if len(files) == 0:
|
| return None, None
|
| return files, labels
|
|
|
|
|
| def _get_transform(model_type):
|
| if model_type in ['resnet', 'fusion', 'fusion_improved', 'fusion_dual']:
|
| size = 224
|
| else:
|
| size = 128
|
| return transforms.Compose([
|
| transforms.Lambda(lambda img: pad_to_min_size(img, size)),
|
| transforms.CenterCrop(size),
|
| transforms.ToTensor(),
|
| transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
|
| ])
|
|
|
|
|
| def _evaluate_deep_model(model, model_type, dataset_root, max_val_images=None):
|
| from train import ImageDataset
|
| from torch.utils.data import DataLoader
|
|
|
| files, labels = _get_validation_files(dataset_root, max_val_images=max_val_images)
|
| if files is None:
|
| return None
|
|
|
| transform = _get_transform(model_type)
|
| dataset = ImageDataset(files, labels, transform=transform)
|
| dataloader = DataLoader(dataset, batch_size=8, shuffle=False, num_workers=0)
|
|
|
| device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
| model.eval()
|
|
|
| all_probs = []
|
| all_labels = []
|
| with torch.no_grad():
|
| for inputs, lbls in dataloader:
|
| inputs = inputs.to(device)
|
| outputs = model(inputs)
|
| probs = torch.softmax(outputs, dim=1)[:, 1].cpu().numpy()
|
| all_probs.extend(probs.tolist())
|
| all_labels.extend(lbls)
|
|
|
| if len(all_labels) == 0:
|
| return None
|
|
|
| y_true = np.array(all_labels)
|
| y_prob = np.array(all_probs)
|
| y_pred = (y_prob >= 0.5).astype(int)
|
|
|
| acc = accuracy_score(y_true, y_pred)
|
| try:
|
| auc = roc_auc_score(y_true, y_prob)
|
| except Exception:
|
| auc = None
|
| report = classification_report(y_true, y_pred, zero_division=0)
|
| return {
|
| 'accuracy': acc,
|
| 'auc': auc,
|
| 'report': report,
|
| 'count': len(y_true)
|
| }
|
|
|
|
|
| def _evaluate_ml_model(model, dataset_root, max_val_images=None, patch_size=128, n_patches=4):
|
| from train import collect_features
|
|
|
| val_root = Path(dataset_root) / 'Validation'
|
| if not val_root.exists():
|
| return None
|
|
|
| real_val = val_root / 'Real'
|
| fake_val = val_root / 'Fake'
|
| Xrv, yrv = collect_features(real_val, 0, max_images=max_val_images, patch_size=patch_size, n_patches=n_patches)
|
| Xfv, yfv = collect_features(fake_val, 1, max_images=max_val_images, patch_size=patch_size, n_patches=n_patches)
|
| if len(Xrv) + len(Xfv) == 0:
|
| return None
|
|
|
| Xv = np.array(Xrv + Xfv)
|
| yv = np.array(yrv + yfv)
|
| if hasattr(model, 'predict_proba'):
|
| probs = model.predict_proba(Xv)[:, 1]
|
| else:
|
| probs = model.predict(Xv).astype(float)
|
| preds = (probs >= 0.5).astype(int)
|
|
|
| acc = accuracy_score(yv, preds)
|
| try:
|
| auc = roc_auc_score(yv, probs)
|
| except Exception:
|
| auc = None
|
| report = classification_report(yv, preds, zero_division=0)
|
| return {
|
| 'accuracy': acc,
|
| 'auc': auc,
|
| 'report': report,
|
| 'count': len(yv)
|
| }
|
|
|
|
|
| def _calibrate_threshold(model, model_info, dataset_root, target_real_fpr=0.05, max_val_images=200):
|
| if model is None or model_info is None:
|
| return None
|
|
|
| val_root = Path(dataset_root) / 'Validation'
|
| if not val_root.exists():
|
| return None
|
|
|
| mtype = model_info.get('model_type', 'unknown') if isinstance(model_info, dict) else 'unknown'
|
| real_probs = []
|
|
|
| if mtype in ['cnn', 'cnn_kfold', 'resnet', 'fusion', 'fusion_improved', 'fusion_dual']:
|
| from train import ImageDataset
|
| from torch.utils.data import DataLoader
|
|
|
| files, labels = _get_validation_files(dataset_root, max_val_images=max_val_images)
|
| if files is None:
|
| return None
|
|
|
| real_files = [f for f, lbl in zip(files, labels) if lbl == 0]
|
| if len(real_files) == 0:
|
| return None
|
|
|
| transform = _get_transform(mtype)
|
| dataset = ImageDataset(real_files, [0] * len(real_files), transform=transform)
|
| dataloader = DataLoader(dataset, batch_size=8, shuffle=False, num_workers=0)
|
|
|
| device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
| model.eval()
|
| with torch.no_grad():
|
| for inputs, _ in dataloader:
|
| inputs = inputs.to(device)
|
| outputs = model(inputs)
|
| probs = torch.softmax(outputs, dim=1)[:, 1].cpu().numpy()
|
| real_probs.extend(probs.tolist())
|
| else:
|
| from train import collect_features
|
|
|
| real_val = val_root / 'Real'
|
| Xrv, _ = collect_features(real_val, 0, max_images=max_val_images, patch_size=128, n_patches=4)
|
| if len(Xrv) == 0:
|
| return None
|
| X = np.array(Xrv)
|
| if hasattr(model, 'predict_proba'):
|
| probs = model.predict_proba(X)[:, 1]
|
| else:
|
| probs = model.predict(X).astype(float)
|
| real_probs.extend(probs.tolist())
|
|
|
| if len(real_probs) == 0:
|
| return None
|
|
|
| real_probs = np.array(real_probs)
|
| target = max(0.0, min(1.0, float(target_real_fpr)))
|
| if target <= 0.0:
|
| return float(np.max(real_probs))
|
| return float(np.quantile(real_probs, 1.0 - target))
|
|
|
|
|
| def _load_model_from_info(info_path):
|
| info = joblib.load(str(info_path))
|
| if not isinstance(info, dict) or 'model_type' not in info:
|
| return None, None
|
|
|
| mtype = info['model_type']
|
| model = None
|
|
|
| if mtype in ['cnn', 'cnn_kfold', 'resnet', 'fusion', 'fusion_improved', 'fusion_dual']:
|
| if mtype == 'resnet':
|
| from train import DeepfakeResNet as _ModelClass
|
| elif mtype == 'fusion_dual':
|
| from train import DeepfakeDualStream as _ModelClass
|
| elif mtype in ['fusion', 'fusion_improved']:
|
| from train import DeepfakeFeatureFusion as _ModelClass
|
| else:
|
| from train import DeepfakeCNN as _ModelClass
|
|
|
| model = _ModelClass()
|
| state_path = info.get('state_dict_path')
|
| if state_path is None:
|
| base = str(info_path).replace('_improved_info.pkl', '').replace('_info.pkl', '')
|
| candidates = [base + '_best_improved', base + '_best', base]
|
| for c in candidates:
|
| if Path(c).exists():
|
| state_path = c
|
| break
|
| if state_path is None:
|
| raise FileNotFoundError('state_dict_path not found in model info and no candidate file exists')
|
|
|
| model.load_state_dict(torch.load(state_path, map_location='cpu'))
|
| model.eval()
|
| if torch.cuda.is_available():
|
| model.to(torch.device('cuda'))
|
| else:
|
| base = str(info_path).replace('_improved_info.pkl', '').replace('_info.pkl', '')
|
| joblib_path = base + '.joblib'
|
| if Path(joblib_path).exists():
|
| model = joblib.load(joblib_path)
|
| else:
|
| model = info
|
|
|
| return model, info
|
|
|
|
|
| def load_model_fast():
|
| """Load a model quickly without validation evaluation for faster startup."""
|
|
|
| info_candidates = list(Path('.').glob('*_improved_info.pkl')) + list(Path('.').glob('*_info.pkl'))
|
| info_candidates = sorted(info_candidates, key=lambda p: p.name, reverse=True)
|
|
|
| if not info_candidates:
|
| return None, None, None
|
|
|
|
|
| for info_path in info_candidates:
|
| try:
|
| model, info = _load_model_from_info(info_path)
|
| mtype = info.get('model_type', 'unknown') if isinstance(info, dict) else 'unknown'
|
| print(f'=== Loaded Model (fast mode) ===')
|
| print(f"Model: {info_path.name}")
|
| print(f"Type: {mtype}")
|
| return model, info, str(info_path)
|
| except Exception as e:
|
| print(f"Skipping {info_path.name}: failed to load model ({e})")
|
| continue
|
|
|
| return None, None, None
|
|
|
|
|
| def select_best_model(dataset_root='DeepfakeVsReal/Dataset', max_val_images=None):
|
| info_candidates = list(Path('.').glob('*_improved_info.pkl')) + list(Path('.').glob('*_info.pkl'))
|
| info_candidates = sorted(info_candidates, key=lambda p: p.name, reverse=True)
|
| if not info_candidates:
|
| return None, None, None
|
|
|
| scored = []
|
| for info_path in info_candidates:
|
| try:
|
| model, info = _load_model_from_info(info_path)
|
| except Exception as e:
|
| print(f"Skipping {info_path.name}: failed to load model ({e})")
|
| continue
|
|
|
| mtype = info.get('model_type', 'unknown') if isinstance(info, dict) else 'unknown'
|
| stats = None
|
|
|
| if mtype in ['cnn', 'cnn_kfold', 'resnet', 'fusion', 'fusion_improved', 'fusion_dual']:
|
| stats = _evaluate_deep_model(model, mtype, dataset_root, max_val_images=max_val_images)
|
| else:
|
| patch_size = info.get('patch_size', 128) if isinstance(info, dict) else 128
|
| n_patches = info.get('patches_per_image', 4) if isinstance(info, dict) else 4
|
| stats = _evaluate_ml_model(model, dataset_root, max_val_images=max_val_images, patch_size=patch_size, n_patches=n_patches)
|
|
|
| if stats is None:
|
| print(f"Skipping {info_path.name}: no validation stats")
|
| continue
|
|
|
| scored.append((info_path, model, info, stats))
|
| auc = stats.get('auc')
|
| acc = stats.get('accuracy')
|
| print('\n=== Model Evaluation ===')
|
| print(f"Model: {info_path.name} | type: {mtype}")
|
| print(f"Val samples: {stats.get('count', 0)}")
|
| print(f"Accuracy: {acc:.4f}")
|
| if auc is not None:
|
| print(f"ROC AUC: {auc:.4f}")
|
| print('Classification report:')
|
| print(stats.get('report', 'N/A'))
|
|
|
| if not scored:
|
| return None, None, None
|
|
|
| def _score_key(item):
|
| _, _, _, st = item
|
| auc = st.get('auc')
|
| acc = st.get('accuracy')
|
| return (auc if auc is not None else -1.0, acc)
|
|
|
| scored.sort(key=_score_key, reverse=True)
|
| best_path, best_model, best_info, best_stats = scored[0]
|
| print('\n=== Selected Best Model ===')
|
| print(f"Model: {best_path.name}")
|
| print(f"Type: {best_info.get('model_type', 'unknown') if isinstance(best_info, dict) else 'unknown'}")
|
| print(f"Accuracy: {best_stats.get('accuracy'):.4f}")
|
| if best_stats.get('auc') is not None:
|
| print(f"ROC AUC: {best_stats.get('auc'):.4f}")
|
| return best_model, best_info, str(best_path)
|
|
|
|
|
|
|
| MODEL_INFO_PATH = Path('model_fusion_best.joblib_info.pkl')
|
| if MODEL_INFO_PATH.exists():
|
| try:
|
| MODEL, MODEL_INFO = _load_model_from_info(MODEL_INFO_PATH)
|
| MODEL_PATH = str(MODEL_INFO_PATH)
|
| print(f'=== Loaded Fixed Model ===')
|
| print(f"Model: {MODEL_INFO_PATH.name}")
|
| print(f"Type: {MODEL_INFO.get('model_type', 'unknown') if isinstance(MODEL_INFO, dict) else 'unknown'}")
|
| except Exception as e:
|
| print(f'Failed to load fixed model {MODEL_INFO_PATH.name}: {e}')
|
| MODEL = None
|
| MODEL_INFO = None
|
| MODEL_PATH = None
|
| else:
|
| print(f'Fixed model info not found: {MODEL_INFO_PATH.name}')
|
|
|
| if MODEL is None:
|
| print('No valid model loaded; falling back to heuristic detection.')
|
| else:
|
| try:
|
| AUTO_THRESHOLD = _calibrate_threshold(
|
| MODEL,
|
| MODEL_INFO,
|
| dataset_root='DeepfakeVsReal/Dataset',
|
| target_real_fpr=TARGET_REAL_FPR,
|
| max_val_images=MAX_CALIB_IMAGES,
|
| )
|
| if AUTO_THRESHOLD is not None:
|
| print(f'Auto-threshold (target real FPR {TARGET_REAL_FPR:.2%}): {AUTO_THRESHOLD:.3f}')
|
| except Exception as e:
|
| print(f'Auto-threshold calibration failed: {e}')
|
|
|
|
|
| def extract_image_features_from_array(img_arr, patch_size=128, n_patches=8, random_state=None):
|
|
|
| H, W, _ = img_arr.shape
|
| patches = []
|
| rng = np.random.RandomState(random_state)
|
| for _ in range(n_patches):
|
| if H <= patch_size or W <= patch_size:
|
| y0 = max(0, (H - patch_size) // 2)
|
| x0 = max(0, (W - patch_size) // 2)
|
| else:
|
| y0 = int(rng.randint(0, H - patch_size + 1))
|
| x0 = int(rng.randint(0, W - patch_size + 1))
|
| patch = img_arr[y0:y0 + patch_size, x0:x0 + patch_size]
|
| if patch.shape[0] != patch_size or patch.shape[1] != patch_size:
|
| ph = np.zeros((patch_size, patch_size, 3), dtype=patch.dtype)
|
| ph[:patch.shape[0], :patch.shape[1]] = patch
|
| patch = ph
|
| patches.append(patch)
|
|
|
| feats = []
|
| for p in patches:
|
| g = rgb_to_gray(p)
|
| res = extract_residual(g)
|
| res_std = float(np.std(res))
|
| _, hf = fft_stats(g)
|
|
|
| ent = lbp_entropy((g * 255).astype(np.uint8))
|
| feats.append([res_std, hf, ent])
|
| feats = np.array(feats)
|
| mean = feats.mean(axis=0)
|
| std = feats.std(axis=0)
|
| return np.concatenate([mean, std])[None, :]
|
|
|
| def evaluate_model_on_validation(model, dataset_root='DeepfakeVsReal/Dataset'):
|
| p = Path(dataset_root)
|
| val_root = p / 'Validation'
|
| if not val_root.exists():
|
| return 'Validation folder not found under ' + str(p)
|
|
|
| real_folder = val_root / 'Real'
|
| fake_folder = val_root / 'Fake'
|
| files_real = sorted([str(x) for x in real_folder.rglob('*.jpg')] + [str(x) for x in real_folder.rglob('*.png')])
|
| files_fake = sorted([str(x) for x in fake_folder.rglob('*.jpg')] + [str(x) for x in fake_folder.rglob('*.png')])
|
| files = [(f, 0) for f in files_real] + [(f, 1) for f in files_fake]
|
| X = []
|
| y = []
|
| for f, lbl in files:
|
| try:
|
| pil = Image.open(f).convert('RGB')
|
| arr = np.asarray(pil).astype(np.float32) / 255.0
|
| feat = extract_image_features_from_array(arr, patch_size=128, n_patches=8, random_state=123)
|
| X.append(feat[0])
|
| y.append(lbl)
|
| except Exception as e:
|
| continue
|
| if len(X) == 0:
|
| return 'No validation images found or feature extraction failed.'
|
| X = np.array(X)
|
| y = np.array(y)
|
| try:
|
| if hasattr(model, 'predict_proba'):
|
| probs = model.predict_proba(X)[:, 1]
|
| else:
|
| probs = model.predict(X).astype(float)
|
| preds = (probs >= 0.5).astype(int)
|
| acc = accuracy_score(y, preds)
|
| try:
|
| auc = roc_auc_score(y, probs)
|
| except Exception:
|
| auc = None
|
| report = classification_report(y, preds)
|
| lines = [f'Val accuracy: {acc:.4f}']
|
| if auc is not None:
|
| lines.append(f'Val ROC AUC: {auc:.4f}')
|
| lines.append('\n'+report)
|
| return '\n'.join(lines)
|
| except Exception as e:
|
| return f'Evaluation failed: {e}'
|
|
|
|
|
| def predict_gradio(pil_img, ethical_threshold=0.5, show_raw_features=False):
|
|
|
| img = np.asarray(pil_img).astype(np.float32) / 255.0
|
|
|
|
|
| ethical_status = "N/A"
|
| ethical_report = ""
|
|
|
| if MODEL is not None and MODEL_INFO is not None:
|
| try:
|
| if MODEL_INFO['model_type'] in ['cnn', 'cnn_kfold', 'resnet', 'fusion', 'fusion_improved', 'fusion_dual']:
|
|
|
| device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
|
|
|
|
| if MODEL_INFO['model_type'] in ['resnet', 'fusion', 'fusion_improved']:
|
| transform = transforms.Compose([
|
| transforms.Lambda(lambda img: pad_to_min_size(img, 224)),
|
| transforms.CenterCrop(224),
|
| transforms.ToTensor(),
|
| transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
|
| ])
|
| else:
|
| transform = transforms.Compose([
|
| transforms.Lambda(lambda img: pad_to_min_size(img, 128)),
|
| transforms.CenterCrop(128),
|
| transforms.ToTensor(),
|
| transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
|
| ])
|
|
|
| input_tensor = transform(pil_img).unsqueeze(0).to(device)
|
|
|
|
|
| with torch.no_grad():
|
| outputs = MODEL(input_tensor)
|
| probs = torch.softmax(outputs, dim=1)
|
| prob_fake = float(probs[0, 1])
|
|
|
|
|
| threshold = AUTO_THRESHOLD if AUTO_THRESHOLD is not None else MODEL_INFO.get('optimal_threshold', 0.5)
|
| pred_class = 1 if prob_fake >= threshold else 0
|
|
|
| label = 'AI-generated' if pred_class == 1 else 'Real (camera)'
|
| is_ai = pred_class == 1
|
|
|
|
|
| try:
|
| overlay_img = apply_gradcam_overlay_from_pil(pil_img, MODEL, MODEL_INFO['model_type'])
|
| overlay_pil = Image.fromarray(overlay_img)
|
| except Exception as e:
|
| print(f"Grad-CAM failed: {e}")
|
|
|
| patch_scores, coords, img_shape, ps, st = sliding_patch_scores(img, patch_size=128, stride=64)
|
| heat = reconstruct_heatmap(patch_scores, coords, img_shape, ps, st)
|
| overlay_pil = make_overlay_pil(img, heat)
|
|
|
|
|
| if is_ai:
|
| assessment = EthicalAssessment.assess(img, threshold=ethical_threshold)
|
| ethical_status = get_simple_status(assessment)
|
| ethical_report = format_ethical_report(assessment)
|
| if not show_raw_features:
|
| idx = ethical_report.find('\nRAW FEATURES:')
|
| if idx != -1:
|
| ethical_report = ethical_report[:idx]
|
|
|
| return label, prob_fake, overlay_pil, ethical_status, ethical_report
|
|
|
| else:
|
|
|
| X = extract_image_features_from_array(img, patch_size=128, n_patches=8, random_state=0)
|
| if hasattr(MODEL, 'predict_proba'):
|
| prob = float(MODEL.predict_proba(X)[:, 1][0])
|
| else:
|
| pred = MODEL.predict(X)[0]
|
| prob = float(pred)
|
|
|
|
|
| threshold = AUTO_THRESHOLD if AUTO_THRESHOLD is not None else MODEL_INFO.get('optimal_threshold', 0.5)
|
| is_ai = prob >= threshold
|
| label = 'AI-generated' if is_ai else 'Real (camera)'
|
|
|
|
|
| patch_scores, coords, img_shape, ps, st = sliding_patch_scores(img, patch_size=128, stride=64)
|
| heat = reconstruct_heatmap(patch_scores, coords, img_shape, ps, st)
|
| overlay = make_overlay_pil(img, heat)
|
|
|
|
|
| if is_ai:
|
| assessment = EthicalAssessment.assess(img, threshold=ethical_threshold)
|
| ethical_status = get_simple_status(assessment)
|
| ethical_report = format_ethical_report(assessment)
|
| if not show_raw_features:
|
| idx = ethical_report.find('\nRAW FEATURES:')
|
| if idx != -1:
|
| ethical_report = ethical_report[:idx]
|
|
|
| return label, prob, overlay, ethical_status, ethical_report
|
|
|
| except Exception as e:
|
| print(f"Model prediction failed: {e}")
|
|
|
|
|
|
|
| patch_scores, coords, img_shape, ps, st = sliding_patch_scores(img, patch_size=128, stride=64)
|
| heat = reconstruct_heatmap(patch_scores, coords, img_shape, ps, st)
|
| overlay = make_overlay_pil(img, heat)
|
|
|
| ai_score = float(np.mean(heat))
|
| threshold = AUTO_THRESHOLD if AUTO_THRESHOLD is not None else 0.5
|
| is_ai = ai_score >= threshold
|
| label = 'AI-generated' if is_ai else 'Real (camera)'
|
|
|
|
|
| if is_ai:
|
| assessment = EthicalAssessment.assess(img, threshold=ethical_threshold)
|
| ethical_status = get_simple_status(assessment)
|
| ethical_report = format_ethical_report(assessment)
|
| if not show_raw_features:
|
| idx = ethical_report.find('\nRAW FEATURES:')
|
| if idx != -1:
|
| ethical_report = ethical_report[:idx]
|
|
|
| return label, ai_score, overlay, ethical_status, ethical_report
|
|
|
|
|
| def apply_gradcam_overlay_from_pil(pil_img, model, model_type):
|
| """Apply Grad-CAM to PIL image for deep learning models"""
|
| device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
|
|
|
|
| if model_type in ['resnet', 'fusion', 'fusion_improved']:
|
| transform = transforms.Compose([
|
| transforms.Lambda(lambda img: pad_to_min_size(img, 224)),
|
| transforms.CenterCrop(224),
|
| transforms.ToTensor(),
|
| transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
|
| ])
|
| target_size = (224, 224)
|
| else:
|
| transform = transforms.Compose([
|
| transforms.Lambda(lambda img: pad_to_min_size(img, 128)),
|
| transforms.CenterCrop(128),
|
| transforms.ToTensor(),
|
| transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
|
| ])
|
| target_size = (128, 128)
|
|
|
| input_tensor = transform(pil_img).unsqueeze(0).to(device)
|
|
|
|
|
| from train import GradCAM
|
| if model_type == 'resnet':
|
| target_layer = model.resnet.layer4[-1].conv3
|
| elif model_type in ['fusion', 'fusion_improved']:
|
|
|
| target_layer = model.resnet[7][-1].conv3
|
| else:
|
| target_layer = model.conv4
|
|
|
| grad_cam = GradCAM(model, target_layer)
|
| cam = grad_cam.generate_cam(input_tensor, target_class=1)
|
|
|
|
|
| heatmap = cv2.applyColorMap(np.uint8(255 * cam), cv2.COLORMAP_JET)
|
| heatmap = cv2.cvtColor(heatmap, cv2.COLOR_BGR2RGB)
|
|
|
|
|
| original = cv2.resize(np.array(pil_img), target_size)
|
|
|
|
|
| overlay = cv2.addWeighted(original, 0.6, heatmap, 0.4, 0)
|
|
|
| return overlay
|
|
|
|
|
| title = "Advanced Deepfake Detection System with Ethical Assessment"
|
|
|
|
|
| with gr.Blocks(title=title) as iface:
|
| gr.Markdown(f"""
|
| # {title}
|
|
|
| Upload an image to detect if it's AI-generated and assess its ethical status.
|
|
|
| **Current Model:** {MODEL_INFO['model_type'].upper() if MODEL_INFO else 'Heuristic-based'}
|
| """)
|
|
|
| with gr.Row():
|
|
|
| with gr.Column(scale=1):
|
| gr.Markdown("### Input")
|
| input_image = gr.Image(type='pil', label='Upload Image', height=400)
|
|
|
| gr.Markdown("### Settings")
|
| ethical_threshold = gr.Slider(
|
| minimum=0.0,
|
| maximum=1.0,
|
| step=0.01,
|
| value=0.5,
|
| label='Ethical Risk Threshold',
|
| info='Lower = more strict classification'
|
| )
|
| show_raw_features = gr.Checkbox(
|
| label='Show raw feature values',
|
| value=False
|
| )
|
|
|
| analyze_btn = gr.Button("Analyze Image", variant="primary", size="lg")
|
|
|
| gr.Markdown("""
|
| ---
|
| **Features:**
|
| - Deep Learning: CNN/ResNet with transfer learning
|
| - Grad-CAM visualization highlights suspicious regions
|
| - Ethical assessment evaluates privacy and misuse risks
|
| - Real-time GPU-accelerated inference
|
| """)
|
|
|
|
|
| with gr.Column(scale=1):
|
| gr.Markdown("### Detection Results")
|
|
|
| with gr.Row():
|
| detection_result = gr.Label(num_top_classes=2, label='Classification')
|
| ai_score = gr.Number(label='AI-likelihood Score', precision=4)
|
|
|
| heatmap = gr.Image(label='Detection Heatmap', height=400)
|
|
|
| gr.Markdown("### Ethical Assessment")
|
| ethical_status = gr.Textbox(label='Status', lines=2)
|
|
|
| with gr.Accordion("Full Report", open=False):
|
| ethical_report = gr.Textbox(
|
| label='Detailed Assessment',
|
| lines=30
|
| )
|
|
|
| gr.Markdown("""
|
| ---
|
| **How it works:** The heatmap overlay shows regions the model considers suspicious for deepfake artifacts.
|
| Ethical classification is based on artifact detectability and human face presence.
|
|
|
| *Powered by FHIBE Dataset concepts for face authenticity verification.*
|
| """)
|
|
|
|
|
| analyze_btn.click(
|
| fn=predict_gradio,
|
| inputs=[input_image, ethical_threshold, show_raw_features],
|
| outputs=[detection_result, ai_score, heatmap, ethical_status, ethical_report]
|
| )
|
|
|
|
|
| if __name__ == '__main__':
|
| iface.launch() |
| >>>>>>> 65ab9814191b6bb448da441c53a768594e7d1d59 |
|
|