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