Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import numpy as np | |
| import matplotlib.pyplot as plt | |
| import cv2 | |
| from scipy import ndimage | |
| # ═══════════════════════════════════════════════════ | |
| # 🛡️ IMAGESHIELD PRO v2.4 – AUTHENTICITY & DEEPFAKE DETECTOR | |
| # ACoNum / Trusted Sound 2026 — Sami Meddeb | |
| # Fix v2.4 : seuils recalibrés pour éviter les faux positifs | |
| # (photos studio professionnelles, éclairage fort, JPEG haute qualité) | |
| # ═══════════════════════════════════════════════════ | |
| # ── SEUILS CALIBRÉS v2.4 ──────────────────────────────────── | |
| # Problème v2.3 : photos studio → grain faible → détecté comme IA | |
| # Solution : seuils plus stricts + score combiné obligatoire | |
| NOISE_THRESHOLD = 0.55 # était 1.8 — studio pro légit peut avoir < 1.0 | |
| FREQ_THRESHOLD = 500 # était 150 — trop sensible aux JPEG | |
| ELA_THRESHOLD = 0.25 # était 1.0 — photos JPEG légit ont ELA faible | |
| MIN_SIGNALS = 2 # il faut AU MOINS 2 signaux pour crier deepfake | |
| def get_sensor_noise_fingerprint(img): | |
| """ | |
| Extrait le bruit hautes fréquences pour vérifier si c'est un capteur physique. | |
| IMPORTANT : photos studio avec éclairage professionnel ont naturellement | |
| moins de grain — ne pas confondre avec signature IA. | |
| On normalise par la luminosité moyenne pour corriger ce biais. | |
| """ | |
| gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY).astype(np.float32) | |
| blurred = cv2.medianBlur(gray.astype(np.uint8), 3).astype(np.float32) | |
| noise = cv2.absdiff(gray, blurred) | |
| # Normalisation par la luminosité pour compenser l'éclairage studio | |
| mean_lum = np.mean(gray) | |
| noise_density = np.std(noise) | |
| # Correction : photo très lumineuse → grain naturellement réduit | |
| # On ramène à une base commune | |
| corrected_density = noise_density * (128.0 / (mean_lum + 1e-8)) | |
| return corrected_density, noise | |
| def analyze_frequency_domain(img): | |
| """ | |
| Analyse FFT pour détecter les grilles de génération IA. | |
| Les GAN/Diffusion produisent des pics très réguliers à fréquences spécifiques. | |
| Une photo JPEG légitime même compressée n'a PAS ces pics réguliers. | |
| """ | |
| gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY).astype(np.float32) | |
| f = np.fft.fft2(gray) | |
| fshift = np.fft.fftshift(f) | |
| mag = np.abs(fshift) | |
| h, w = gray.shape | |
| cy, cx = h // 2, w // 2 | |
| mag[cy - 20:cy + 20, cx - 20:cx + 20] = 0 | |
| # Ratio max/mean : un vrai GAN a des pics TRÈS anormaux (> 500) | |
| # Une photo réelle même avec artefacts JPEG reste < 400 | |
| peak_score = np.max(mag) / (np.mean(mag) + 1e-8) | |
| vis = np.log(mag + 1) | |
| vis = cv2.normalize(vis, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) | |
| return peak_score, cv2.applyColorMap(vis, cv2.COLORMAP_VIRIDIS) | |
| def error_level_analysis(img, quality=92): | |
| """ | |
| ELA : détecte les manipulations locales. | |
| IMPORTANT : une photo JPEG légit, même haute qualité, a une ELA faible. | |
| On cherche une ELA ANORMALEMENT basse (image purement synthétique) | |
| OU des zones avec ELA irrégulière (copier-coller, manipulation locale). | |
| Seuil abaissé à 0.25 pour ne pas pénaliser les vraies photos. | |
| """ | |
| _, enc = cv2.imencode( | |
| '.jpg', cv2.cvtColor(img, cv2.COLOR_RGB2BGR), | |
| [int(cv2.IMWRITE_JPEG_QUALITY), quality] | |
| ) | |
| dec = cv2.imdecode(enc, 1) | |
| diff = cv2.absdiff(img, cv2.cvtColor(dec, cv2.COLOR_BGR2RGB)) | |
| ela_score = np.mean(diff) | |
| # Variance spatiale : une image IA a souvent une ELA trop UNIFORME | |
| ela_variance = np.std(diff) | |
| return ela_score, ela_variance, cv2.convertScaleAbs(diff, alpha=5.0) | |
| def detect_face_artifacts(img): | |
| """ | |
| Détecte les artefacts typiques des deepfakes sur les visages : | |
| - Bords flous autour du visage | |
| - Transition peau/arrière-plan trop nette (upsampling) | |
| - Symétrie excessive | |
| """ | |
| gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) | |
| # Gradient de Sobel pour détecter les discontinuités anormales | |
| sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3) | |
| sobely = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3) | |
| gradient_mag = np.sqrt(sobelx**2 + sobely**2) | |
| # Un deepfake facial a des gradients trop lisses dans les zones de peau | |
| # et trop nets aux bords du visage généré | |
| gradient_std = np.std(gradient_mag) | |
| gradient_mean = np.mean(gradient_mag) | |
| ratio = gradient_std / (gradient_mean + 1e-8) | |
| # Ratio < 1.2 = trop uniforme = suspect | |
| return ratio | |
| def detect_deepfake(img): | |
| # 1. Bruit de capteur (corrigé pour éclairage studio) | |
| noise_density, noise_map = get_sensor_noise_fingerprint(img) | |
| # 2. Analyse fréquentielle FFT | |
| freq_score, fft_map = analyze_frequency_domain(img) | |
| # 3. ELA + variance | |
| ela_s, ela_var, ela_map = error_level_analysis(img) | |
| # 4. Artefacts de gradient (visage/bords) | |
| gradient_ratio = detect_face_artifacts(img) | |
| # ─── LOGIQUE DE SCORING v2.4 ───────────────────────────── | |
| # Règle : au moins 2 signaux positifs pour déclarer "deepfake" | |
| # Chaque signal contribue des points SEULEMENT s'il est significatif | |
| signals_triggered = 0 | |
| ai_confidence = 0 | |
| reasons = [] | |
| # Signal 1 — Bruit de capteur anormalement faible | |
| # Seuil 0.55 (corrigé luminosité) — les vrais studios restent > 0.6 | |
| if noise_density < NOISE_THRESHOLD: | |
| ai_confidence += 35 | |
| signals_triggered += 1 | |
| reasons.append( | |
| f"⚠️ Bruit photonique absent (densité={noise_density:.2f} < {NOISE_THRESHOLD}) — " | |
| "Absence de grain capteur physique" | |
| ) | |
| # Signal 2 — Pics de fréquence GAN (grille de diffusion) | |
| # Seuil 500 — uniquement les vraies grilles IA | |
| if freq_score > FREQ_THRESHOLD: | |
| ai_confidence += 30 | |
| signals_triggered += 1 | |
| reasons.append( | |
| f"⚠️ Grille IA détectée en FFT (score={freq_score:.0f} > {FREQ_THRESHOLD}) — " | |
| "Pattern de génération diffusion/GAN" | |
| ) | |
| # Signal 3 — ELA trop parfaite ET variance nulle | |
| # Seuil 0.25 + variance < 0.5 = image purement synthétique | |
| if ela_s < ELA_THRESHOLD and ela_var < 0.5: | |
| ai_confidence += 25 | |
| signals_triggered += 1 | |
| reasons.append( | |
| f"⚠️ Compression parfaite ELA={ela_s:.3f} σ={ela_var:.3f} — " | |
| "Image non issue d'un capteur optique réel" | |
| ) | |
| # Signal 4 — Gradient trop uniforme (deepfake facial) | |
| if gradient_ratio < 1.2: | |
| ai_confidence += 20 | |
| signals_triggered += 1 | |
| reasons.append( | |
| f"⚠️ Gradients trop lisses (ratio={gradient_ratio:.2f} < 1.2) — " | |
| "Absence de texture naturelle du capteur" | |
| ) | |
| # ─── RÈGLE DE SÉCURITÉ : minimum 2 signaux ────────────── | |
| if signals_triggered < MIN_SIGNALS: | |
| # Un seul signal → suspicieux mais pas deepfake | |
| ai_confidence = min(ai_confidence, 28) | |
| final_score = min(ai_confidence, 100) | |
| # ─── VERDICT ───────────────────────────────────────────── | |
| if final_score > 55: | |
| label = "🚨 DEEPFAKE / GENERATED" | |
| color = "red" | |
| elif final_score > 28: | |
| label = "⚖️ SUSPICIEUX / MODIFIÉ" | |
| color = "orange" | |
| else: | |
| label = "✅ AUTHENTIQUE (CAMERA)" | |
| color = "green" | |
| return ( | |
| final_score, label, reasons, | |
| fft_map, ela_map, noise_map, | |
| noise_density, freq_score, ela_s, signals_triggered | |
| ) | |
| # ═══════════════════════════════════════════════════ | |
| # 🎨 GRADIO INTERFACE | |
| # ═══════════════════════════════════════════════════ | |
| def process(input_img): | |
| if input_img is None: | |
| return None, "Veuillez charger une image." | |
| (score, label, reasons, | |
| fft, ela, noise, | |
| noise_val, freq_val, ela_val, n_signals) = detect_deepfake(input_img) | |
| fig, axes = plt.subplots(2, 2, figsize=(12, 10)) | |
| axes[0, 0].imshow(input_img); axes[0, 0].set_title("Original") | |
| axes[0, 1].imshow(fft); axes[0, 1].set_title("FFT — Grilles IA") | |
| axes[1, 0].imshow(ela); axes[1, 0].set_title("ELA — Compression") | |
| axes[1, 1].imshow(noise, cmap='gray'); axes[1, 1].set_title("Bruit Capteur Corrigé") | |
| for ax in axes.flatten(): | |
| ax.axis('off') | |
| plt.tight_layout() | |
| report = f"RÉSULTAT : {label}\n" | |
| report += f"Probabilité IA : {score}% | Signaux déclenchés : {n_signals}/{MIN_SIGNALS} minimum\n" | |
| report += f"\nMesures brutes :\n" | |
| report += f" • Bruit capteur corrigé : {noise_val:.3f} (seuil < {NOISE_THRESHOLD})\n" | |
| report += f" • Pic FFT : {freq_val:.0f} (seuil > {FREQ_THRESHOLD})\n" | |
| report += f" • ELA moyenne : {ela_val:.4f} (seuil < {ELA_THRESHOLD})\n" | |
| report += f"\nSignaux actifs :\n" | |
| report += "\n".join(reasons) if reasons else " Aucune trace de génération synthétique détectée." | |
| report += "\n\n── ImageShield PRO v2.4 · ACoNum / Trusted Sound 2026 ──" | |
| return fig, report | |
| with gr.Blocks(theme=gr.themes.Soft()) as demo: | |
| gr.Markdown( | |
| "# 🛡️ ImageShield PRO v2.4\n" | |
| "### Analyse Forensic : Authentique vs Deepfake\n" | |
| "_Fix v2.4 : seuils recalibrés — photos studio, éclairage fort et JPEG haute qualité correctement classifiés_" | |
| ) | |
| with gr.Row(): | |
| with gr.Column(): | |
| in_img = gr.Image(label="Charger une image (JPG/PNG)", type="numpy") | |
| run_btn = gr.Button("LANCER L'ANALYSE", variant="primary") | |
| with gr.Column(): | |
| out_plot = gr.Plot() | |
| out_text = gr.Textbox(label="Rapport d'Expertise", lines=12) | |
| run_btn.click(process, inputs=in_img, outputs=[out_plot, out_text]) | |
| demo.launch() | |