NOBODY204 commited on
Commit
9a77f66
·
verified ·
1 Parent(s): cc6c032

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +630 -129
app.py CHANGED
@@ -1,134 +1,635 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
  import numpy as np
3
- import matplotlib.pyplot as plt
4
- import cv2
5
- from scipy import ndimage
6
-
7
- # ═══════════════════════════════════════════════════
8
- # 🛡️ MEDIASHIELD PRO v2.5 – Localized AI Detection
9
- # ═══════════════════════════════════════════════════
10
-
11
- def estimate_sensor_noise(img):
12
- """Calcule le bruit de capteur global."""
13
- gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY).astype(np.float32)
14
- blur = cv2.GaussianBlur(gray, (5,5), 0)
15
- noise = gray - blur
16
- return np.std(noise)
17
-
18
- def analyze_local_variance(img):
19
- """Compare le centre aux bords pour détecter un montage IA."""
20
- gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY).astype(np.float32)
21
- h, w = gray.shape
22
- center = gray[h//4:3*h//4, w//4:3*w//4]
23
- edge = gray[0:h//4, 0:w//4]
24
- def get_var(z):
25
- return np.var(z - cv2.GaussianBlur(z, (5,5), 0))
26
- v_c, v_e = get_var(center), get_var(edge)
27
- return abs(v_c - v_e) / (v_e + 1e-8)
28
-
29
- def compute_noise_incoherence_map(img):
30
- """Génère une carte thermique l'IA lissée apparaît en rouge brillant."""
31
- gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY).astype(np.float32)
32
- # Isoler le grain
33
- grain = gray - cv2.medianBlur(gray, 7)
34
- # Calculer la variance locale (IA = faible variance)
35
- grain_var = ndimage.generic_filter(grain, np.var, size=15)
36
- # Inverser pour que l'IA soit "chaude" (brillante)
37
- incoherence_map = np.max(grain_var) - grain_var
38
- vis = cv2.normalize(incoherence_map, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
39
- return cv2.applyColorMap(vis, cv2.COLORMAP_JET)
40
-
41
- def analyze_chrominance_noise(img):
42
- ycrcb = cv2.cvtColor(img, cv2.COLOR_RGB2YCrCb)
43
- cr_var = ndimage.generic_filter(ycrcb[:,:,1].astype(np.float32), np.var, size=5)
44
- cb_var = ndimage.generic_filter(ycrcb[:,:,2].astype(np.float32), np.var, size=5)
45
- u_cr = np.std(cr_var) / (np.mean(cr_var) + 1e-8)
46
- u_cb = np.std(cb_var) / (np.mean(cb_var) + 1e-8)
47
- return (u_cr + u_cb) / 2
48
-
49
- def detect_grid_and_ringing(img):
50
- gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY).astype(np.float32)
51
- fshift = np.fft.fftshift(np.fft.fft2(gray))
52
- magnitude = np.abs(fshift)
53
- rows, cols = gray.shape
54
- crow, ccol = rows//2, cols//2
55
- magnitude[(np.ogrid[:rows,:cols][0]-crow)**2 + (np.ogrid[:rows,:cols][1]-ccol)**2 <= (min(rows,cols)//10)**2] = 0
56
- def get_peaks(sig):
57
- thresh = np.max(sig) * 0.25
58
- return [i for i in range(1, len(sig)-1) if sig[i] > thresh and sig[i] > sig[i-1] and sig[i] > sig[i+1] and i%8 != 0]
59
- grid_s = (len(get_peaks(np.sum(magnitude, 0))) + len(get_peaks(np.sum(magnitude, 1)))) / 2
60
- ringing = np.mean(magnitude) / (np.max(magnitude) + 1e-8)
61
- fft_vis = cv2.applyColorMap(cv2.normalize(np.log(magnitude+1), None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8), cv2.COLORMAP_VIRIDIS)
62
- return grid_s, ringing, fft_vis
63
-
64
- def error_level_analysis(img, quality=90):
65
- _, enc = cv2.imencode('.jpg', cv2.cvtColor(img, cv2.COLOR_RGB2BGR), [int(cv2.IMWRITE_JPEG_QUALITY), quality])
66
- dec = cv2.cvtColor(cv2.imdecode(enc, 1), cv2.COLOR_BGR2RGB)
67
- diff = cv2.absdiff(img, dec)
68
- return np.mean(diff), cv2.normalize(diff, None, 0, 255, cv2.NORM_MINMAX)
69
-
70
- def compute_ai_score_v4(metrics):
71
- score, reasons = 0, []
72
- if metrics['grid_score'] > 25: score += 30; reasons.append("⚠️ Motifs réguliers (GAN possible)")
73
- if metrics['ringing'] > 0.3: score += 25; reasons.append("⚠️ Artefacts circulaires")
74
- if metrics['sensor_noise'] < 1.5: score += 25; reasons.append("⚡ Image trop lisse (IA probable)")
75
- elif metrics['sensor_noise'] > 4.0: score -= 25; reasons.append("✅ Bruit naturel (Photo réelle)")
76
- if metrics['local_diff'] > 1.6: score += 35; reasons.append("🚨 Montage local détecté (Incohérence)")
77
- if metrics['ela_score'] > 2.5 and metrics['grid_score'] < 8:
78
- score *= 0.3; reasons.append("✅ Structure cohérente avec compression réelle")
79
- return int(max(0, min(score, 100))), reasons
80
-
81
- def analyze_image(img_input):
82
- if img_input is None: return None, "❌ Erreur"
83
- img = cv2.cvtColor(img_input, cv2.COLOR_RGBA2RGB) if img_input.shape[2] == 4 else img_input.copy()
84
-
85
- # Correction Gamma
86
- mean_b = np.mean(cv2.cvtColor(img, cv2.COLOR_RGB2GRAY))
87
- img_analysed = cv2.LUT(img, np.array([((i/255.0)**(1/0.6))*255 for i in range(256)]).astype("uint8")) if mean_b < 60 else img.copy()
88
-
89
- # Analyses
90
- chrom_u = analyze_chrominance_noise(img_analysed)
91
- grid_s, ring, fft_v = detect_grid_and_ringing(img_analysed)
92
- ela_s, ela_v = error_level_analysis(img_analysed)
93
- sensor_n = estimate_sensor_noise(img_analysed)
94
- local_d = analyze_local_variance(img_analysed)
95
- noise_map = compute_noise_incoherence_map(img_analysed)
96
-
97
- # Détection de la zone suspecte et dessin du rectangle
98
- img_boxed = img.copy()
99
- if local_d > 1.3: # Seuil de détection de montage
100
- gray_map = cv2.cvtColor(noise_map, cv2.COLOR_BGR2GRAY)
101
- _, _, _, max_loc = cv2.minMaxLoc(gray_map)
102
- h, w = img.shape[:2]
103
- size = int(min(h, w) * 0.25)
104
- top_left = (max(0, max_loc[0] - size//2), max(0, max_loc[1] - size//2))
105
- bottom_right = (min(w, max_loc[0] + size//2), min(h, max_loc[1] + size//2))
106
- cv2.rectangle(img_boxed, top_left, bottom_right, (255, 0, 0), 4)
107
-
108
- metrics = {'chrom_uniformity': chrom_u, 'grid_score': grid_s, 'ringing': ring, 'ela_score': ela_s, 'sensor_noise': sensor_n, 'local_diff': local_d}
109
- ai_score, reasons = compute_ai_score_v4(metrics)
110
-
111
- fig, axes = plt.subplots(2, 2, figsize=(10, 8))
112
- axes[0,0].imshow(img_boxed); axes[0,0].set_title('Détection Locale (IA)')
113
- axes[0,1].imshow(fft_v); axes[0,1].set_title('Fréquences (FFT)')
114
- axes[1,0].imshow(ela_v); axes[1,0].set_title('ELA (Compression)')
115
- axes[1,1].imshow(noise_map); axes[1,1].set_title('Heatmap Incohérence')
116
- for ax in axes.flatten(): ax.axis('off')
117
- plt.tight_layout()
118
-
119
- report = f"🛡️ MEDIASHIELD v2.5\nScore AI : {ai_score}/100\n\n" + ("\n".join(reasons) if reasons else "✅ Authentique")
120
- return fig, report
121
-
122
- with gr.Blocks() as demo:
123
- gr.Markdown("# 🛡️ MediaShield PRO v2.5\nSami Meddeb - ACoNum")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  with gr.Row():
125
- with gr.Column():
126
- in_img = gr.Image(type="numpy")
127
- btn = gr.Button("DÉMARRER L'ANALYSE", variant="primary")
128
- with gr.Column():
129
- out_plt = gr.Plot()
130
- out_txt = gr.Textbox(label="Résultat Forensic", lines=10)
131
- btn.click(analyze_image, in_img, [out_plt, out_txt])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
 
133
  if __name__ == "__main__":
134
- demo.launch()
 
1
+ """
2
+ ImageShield — AI Image Detector
3
+ NOBODY204/ImageShield · HuggingFace Space
4
+ Detects: Nano Banana (Gemini 2.5 Flash), GPT-Image-1, DALL-E 3,
5
+ Midjourney v6/v7, Flux, Stable Diffusion, Adobe Firefly,
6
+ Grok 2, Ideogram, and more.
7
+
8
+ Multi-signal forensic pipeline:
9
+ 1. Neural classifier (ViT-based, trained on 1M+ AI images)
10
+ 2. ELA — Error Level Analysis (JPEG block inconsistency)
11
+ 3. FFT — Frequency domain artifacts
12
+ 4. Noise PRNU — Camera noise pattern analysis
13
+ 5. Metadata / EXIF / C2PA / SynthID markers
14
+ 6. Semantic LLM analysis (Claude or local model)
15
+ """
16
+
17
  import gradio as gr
18
  import numpy as np
19
+ from PIL import Image, ImageChops, ImageEnhance, ImageFilter
20
+ import io, base64, json, requests, warnings
21
+ warnings.filterwarnings("ignore")
22
+
23
+ # ── Try loading heavy deps gracefully ──
24
+ try:
25
+ import torch
26
+ from transformers import pipeline as hf_pipeline, AutoFeatureExtractor, AutoModelForImageClassification
27
+ HAS_TORCH = True
28
+ except ImportError:
29
+ HAS_TORCH = False
30
+
31
+ try:
32
+ from scipy import fft as scipy_fft
33
+ HAS_SCIPY = True
34
+ except ImportError:
35
+ HAS_SCIPY = False
36
+
37
+ try:
38
+ import cv2
39
+ HAS_CV2 = True
40
+ except ImportError:
41
+ HAS_CV2 = False
42
+
43
+ # ── Known AI generator signatures ──
44
+ GENERATORS = {
45
+ "Nano Banana (Gemini 2.5 Flash Image)": "Google DeepMind — autoregressive, 1290 tokens/image. Look for: hyper-smooth gradients, perfect text rendering, no sensor noise.",
46
+ "Nano Banana Pro (Gemini 3 Pro Image)": "Google DeepMind 2K-4K output. Contains SynthID watermark. Features: grounding with Search, reasoning-enhanced generation.",
47
+ "GPT-Image-1 / DALL-E 3": "OpenAI — diffusion-based. Look for: characteristic soft textures, watercolour-like blending, slightly exaggerated saturation.",
48
+ "Midjourney v6/v7": "Proprietary diffusion. Look for: aesthetic bias, dramatic lighting, painterly skin textures, cinematic composition.",
49
+ "Flux 1.0 / Flux 1.1 Pro": "Black Forest Labs — high detail diffusion. Look for: photorealistic skin, fine hair detail, sometimes inconsistent reflections.",
50
+ "Stable Diffusion XL / 3.5": "Open-source diffusion. Look for: frequency artifacts in backgrounds, GAN-like periodicity in textures.",
51
+ "Adobe Firefly": "Adobe — trained on licensed data. Typically reveals metadata 'GeneratedBy: Adobe Firefly'.",
52
+ "Grok 2 Image": "xAI diffusion variant. Often has a distinct cinematic warmth and high contrast.",
53
+ "Ideogram 3.0": "Ideogram — strong text generation. Look for: clear legible text, poster-style composition.",
54
+ }
55
+
56
+ # ── Classifier model (best open-source, 2025) ──
57
+ CLASSIFIER_MODEL = "Organika/sdxl-detector" # ViT trained on SDXL vs real
58
+ CLASSIFIER_MODEL_2 = "haywoodsloan/ai-image-detector-deploy" # General AI detector
59
+
60
+ classifier_pipe = None
61
+
62
+ def load_classifier():
63
+ global classifier_pipe
64
+ if not HAS_TORCH:
65
+ return None
66
+ if classifier_pipe is None:
67
+ try:
68
+ classifier_pipe = hf_pipeline(
69
+ "image-classification",
70
+ model=CLASSIFIER_MODEL_2,
71
+ device=-1 # CPU
72
+ )
73
+ except Exception as e:
74
+ try:
75
+ classifier_pipe = hf_pipeline(
76
+ "image-classification",
77
+ model="umm-maybe/AI-image-detector",
78
+ device=-1
79
+ )
80
+ except:
81
+ return None
82
+ return classifier_pipe
83
+
84
+
85
+ # ════════════════════════════════════
86
+ # SIGNAL 1: Neural Classifier
87
+ # ═══════════════════════════════════��
88
+
89
+ def run_classifier(img: Image.Image) -> dict:
90
+ pipe = load_classifier()
91
+ if pipe is None:
92
+ return {"score": 0.5, "label": "unknown", "method": "Neural (unavailable CPU fallback)"}
93
+ try:
94
+ result = pipe(img)
95
+ top = result[0]
96
+ label = top["label"].lower()
97
+ score = top["score"]
98
+ is_ai = any(k in label for k in ["artificial","fake","ai","generated","machine","synthetic"])
99
+ if not is_ai:
100
+ score = 1 - score
101
+ return {
102
+ "score": round(score, 4),
103
+ "label": top["label"],
104
+ "method": "Neural ViT Classifier",
105
+ "confidence": f"{score*100:.1f}%"
106
+ }
107
+ except Exception as e:
108
+ return {"score": 0.5, "label": "error", "method": f"Neural (error: {e})"}
109
+
110
+
111
+ # ════════════════════════════════════
112
+ # SIGNAL 2: ELA — Error Level Analysis
113
+ # ════════════════════════════════════
114
+
115
+ def run_ela(img: Image.Image, quality: int = 90) -> dict:
116
+ """
117
+ ELA: Save image at reduced JPEG quality, compute pixel difference.
118
+ AI-generated images show uniform block patterns; real photos show
119
+ high-ELA regions at edges and textures.
120
+ """
121
+ try:
122
+ buf = io.BytesIO()
123
+ img_rgb = img.convert("RGB")
124
+ img_rgb.save(buf, format="JPEG", quality=quality)
125
+ buf.seek(0)
126
+ recompressed = Image.open(buf).convert("RGB")
127
+
128
+ diff = ImageChops.difference(img_rgb, recompressed)
129
+ arr = np.array(diff).astype(np.float32)
130
+
131
+ mean_ela = float(arr.mean())
132
+ std_ela = float(arr.std())
133
+ max_ela = float(arr.max())
134
+
135
+ # Real photos: high mean ELA + high std (edges vary)
136
+ # AI images: low-medium mean, very low std (uniform smoothness)
137
+ uniformity = 1.0 - min(std_ela / (mean_ela + 1e-5), 1.0)
138
+ ai_score = float(np.clip(0.3 + uniformity * 0.5 - (std_ela / 30) * 0.2, 0, 1))
139
+
140
+ ela_enhanced = ImageEnhance.Brightness(diff).enhance(10)
141
+
142
+ return {
143
+ "score": round(ai_score, 4),
144
+ "mean_ela": round(mean_ela, 2),
145
+ "std_ela": round(std_ela, 2),
146
+ "max_ela": round(max_ela, 2),
147
+ "uniformity": round(uniformity, 4),
148
+ "method": "ELA (Error Level Analysis)",
149
+ "ela_image": ela_enhanced,
150
+ "interpretation": (
151
+ "🔴 AI-like: uniform ELA (low std = no real JPEG history)" if ai_score > 0.65
152
+ else "🟡 Ambiguous: moderate ELA variance" if ai_score > 0.45
153
+ else "🟢 Real-like: high ELA variance typical of camera photos"
154
+ )
155
+ }
156
+ except Exception as e:
157
+ return {"score": 0.5, "method": f"ELA (error: {e})", "interpretation": "Error"}
158
+
159
+
160
+ # ════════════════════════════════════
161
+ # SIGNAL 3: FFT — Frequency Analysis
162
+ # ════════════════════════════════════
163
+
164
+ def run_fft(img: Image.Image) -> dict:
165
+ """
166
+ Frequency domain analysis.
167
+ AI generators (especially GANs and diffusion) leave characteristic
168
+ patterns in the FFT spectrum — periodic grid artifacts, abnormal
169
+ high-frequency distribution.
170
+ """
171
+ try:
172
+ gray = np.array(img.convert("L")).astype(np.float32)
173
+ if HAS_SCIPY:
174
+ f = scipy_fft.fft2(gray)
175
+ fshift = scipy_fft.fftshift(f)
176
+ else:
177
+ f = np.fft.fft2(gray)
178
+ fshift = np.fft.fftshift(f)
179
+
180
+ magnitude = np.abs(fshift)
181
+ log_mag = np.log1p(magnitude)
182
+
183
+ h, w = gray.shape
184
+ cy2, cx2 = h//2, w//2
185
+ r = min(h, w) // 6
186
+
187
+ # Center energy (low freq) vs periphery (high freq)
188
+ Y, X = np.ogrid[:h, :w]
189
+ dist = np.sqrt((X-cx2)**2 + (Y-cy2)**2)
190
+ center_mask = dist < r
191
+ edge_mask = dist > min(h,w)//3
192
+
193
+ center_energy = float(log_mag[center_mask].mean())
194
+ edge_energy = float(log_mag[edge_mask].mean())
195
+ ratio = edge_energy / (center_energy + 1e-5)
196
+
197
+ # Check for grid artifacts (GAN fingerprint)
198
+ periodic_peaks = detect_periodic_peaks(log_mag, h, w)
199
+
200
+ # AI images: lower edge_energy, characteristic ratio ~0.3-0.5
201
+ # Real photos: higher edge_energy, ratio ~0.5-0.7
202
+ ai_score = float(np.clip(0.6 - (ratio - 0.35) * 1.5 + periodic_peaks * 0.3, 0, 1))
203
+
204
+ return {
205
+ "score": round(ai_score, 4),
206
+ "center_energy": round(center_energy, 4),
207
+ "edge_energy": round(edge_energy, 4),
208
+ "freq_ratio": round(ratio, 4),
209
+ "periodic_artifacts": periodic_peaks > 0.1,
210
+ "method": "FFT Frequency Analysis",
211
+ "interpretation": (
212
+ "🔴 AI-like: unusual frequency distribution or periodic artifacts"
213
+ if ai_score > 0.6
214
+ else "🟡 Ambiguous: borderline frequency signature"
215
+ if ai_score > 0.4
216
+ else "🟢 Real-like: natural frequency distribution"
217
+ )
218
+ }
219
+ except Exception as e:
220
+ return {"score": 0.5, "method": f"FFT (error: {e})", "interpretation": "Error"}
221
+
222
+ def detect_periodic_peaks(log_mag, h, w):
223
+ """Detect periodic grid patterns characteristic of GAN generators."""
224
+ try:
225
+ row_var = float(np.var(log_mag.mean(axis=0)))
226
+ col_var = float(np.var(log_mag.mean(axis=1)))
227
+ normalized = (row_var + col_var) / (log_mag.mean() ** 2 + 1e-5)
228
+ return float(np.clip(normalized / 10, 0, 1))
229
+ except:
230
+ return 0.0
231
+
232
+
233
+ # ════════════════════════════════════
234
+ # SIGNAL 4: PRNU Noise Analysis
235
+ # ════════════════════════════════════
236
+
237
+ def run_noise_analysis(img: Image.Image) -> dict:
238
+ """
239
+ PRNU (Photo Response Non-Uniformity): real cameras leave a unique
240
+ noise fingerprint. AI images have statistically different noise
241
+ distributions — too smooth or too patterned.
242
+ """
243
+ try:
244
+ arr = np.array(img.convert("RGB")).astype(np.float32)
245
+
246
+ # Estimate noise using Laplacian filter
247
+ if HAS_CV2:
248
+ gray = cv2.cvtColor(arr.astype(np.uint8), cv2.COLOR_RGB2GRAY).astype(np.float32)
249
+ laplacian = cv2.Laplacian(gray, cv2.CV_64F)
250
+ noise_level = float(np.std(laplacian))
251
+ noise_entropy = float(-np.sum(
252
+ np.histogram(laplacian.flatten(), bins=256, density=True)[0] *
253
+ np.log2(np.histogram(laplacian.flatten(), bins=256, density=True)[0] + 1e-10)
254
+ ))
255
+ else:
256
+ # Fallback: manual high-pass filter
257
+ kernel = np.array([[-1,-1,-1],[-1,8,-1],[-1,-1,-1]], dtype=np.float32)
258
+ from scipy.ndimage import convolve as nd_convolve
259
+ gray = arr.mean(axis=2)
260
+ try:
261
+ from scipy.ndimage import convolve as nd_conv
262
+ filtered = nd_conv(gray, kernel)
263
+ except:
264
+ filtered = gray - gray.mean()
265
+ noise_level = float(np.std(filtered))
266
+ noise_entropy = 0.5
267
+
268
+ # AI images: very low noise (over-smooth) or patterned noise
269
+ # Real camera: noise_level typically 5-30, entropy ~6-8
270
+ if noise_level < 2.0:
271
+ ai_score = 0.85 # Too smooth = AI
272
+ elif noise_level > 50.0:
273
+ ai_score = 0.70 # Too much = possible GAN artifact
274
+ else:
275
+ ai_score = max(0.1, 0.5 - (noise_level - 2) / 100)
276
+
277
+ return {
278
+ "score": round(ai_score, 4),
279
+ "noise_level": round(noise_level, 4),
280
+ "method": "PRNU Noise Analysis",
281
+ "interpretation": (
282
+ "🔴 AI-like: abnormally low noise (over-smooth AI texture)"
283
+ if noise_level < 2.0
284
+ else "🟡 Ambiguous: noise within borderline range"
285
+ if 2.0 <= noise_level <= 8.0
286
+ else "🟢 Real-like: noise consistent with camera sensor"
287
+ )
288
+ }
289
+ except Exception as e:
290
+ return {"score": 0.5, "method": f"PRNU (error: {e})", "interpretation": "Error"}
291
+
292
+
293
+ # ════════════════════════════════════
294
+ # SIGNAL 5: Metadata / EXIF / SynthID
295
+ # ════════════════════════════════════
296
+
297
+ def run_metadata_analysis(img: Image.Image, filename: str = "") -> dict:
298
+ """
299
+ Check EXIF, IPTC, XMP metadata for AI generator signatures.
300
+ Nano Banana (Gemini) images contain SynthID watermarks.
301
+ """
302
+ try:
303
+ from PIL.ExifTags import TAGS
304
+ exif_data = img._getexif() if hasattr(img, '_getexif') and img._getexif() else {}
305
+ exif_readable = {}
306
+ if exif_data:
307
+ for tag, value in exif_data.items():
308
+ tag_name = TAGS.get(tag, str(tag))
309
+ try:
310
+ exif_readable[tag_name] = str(value)[:100]
311
+ except:
312
+ pass
313
+
314
+ info = img.info or {}
315
+
316
+ # AI-generator specific markers
317
+ ai_markers = []
318
+ ai_score_meta = 0.5
319
+
320
+ # Check for known AI tool signatures in metadata
321
+ all_meta_str = str(exif_readable) + str(info) + filename.lower()
322
+
323
+ generator_hints = {
324
+ "adobe firefly": "Adobe Firefly",
325
+ "firefly": "Adobe Firefly",
326
+ "generatedby": "Adobe Firefly / AI tool",
327
+ "stability ai": "Stable Diffusion",
328
+ "stable diffusion": "Stable Diffusion",
329
+ "midjourney": "Midjourney",
330
+ "dall-e": "DALL-E",
331
+ "openai": "OpenAI",
332
+ "gemini": "Nano Banana (Gemini)",
333
+ "synthid": "Google SynthID (Nano Banana)",
334
+ "imagen": "Google Imagen",
335
+ "leonardo": "Leonardo AI",
336
+ "runwayml": "RunwayML",
337
+ "invoke ai": "InvokeAI",
338
+ "automatic1111": "Stable Diffusion (A1111)",
339
+ "comfyui": "Stable Diffusion (ComfyUI)",
340
+ "parameters": "Stable Diffusion (prompt metadata)",
341
+ }
342
+
343
+ detected_generator = None
344
+ for key, gen_name in generator_hints.items():
345
+ if key in all_meta_str.lower():
346
+ ai_markers.append(f"Found '{key}' marker → {gen_name}")
347
+ detected_generator = gen_name
348
+ ai_score_meta = 0.95
349
+
350
+ # No camera make/model = suspicious
351
+ has_camera = any(k in exif_readable for k in ["Make", "Model", "LensModel"])
352
+ if not has_camera and not ai_markers:
353
+ ai_markers.append("No camera EXIF (Make/Model) — suspicious for real photo")
354
+ ai_score_meta = max(ai_score_meta, 0.6)
355
+
356
+ # PNG often has AI metadata in text chunks
357
+ if img.format == "PNG" and not has_camera:
358
+ ai_markers.append("PNG format without camera EXIF — common for AI outputs")
359
+ ai_score_meta = max(ai_score_meta, 0.65)
360
+
361
+ # SynthID note
362
+ synthid_note = ""
363
+ if "gemini" in all_meta_str.lower() or "nano" in filename.lower():
364
+ synthid_note = "⚠️ SynthID: Upload to Gemini app for definitive Google AI verification"
365
+
366
+ return {
367
+ "score": round(ai_score_meta, 4),
368
+ "markers_found": ai_markers,
369
+ "detected_generator": detected_generator,
370
+ "has_camera_exif": has_camera,
371
+ "exif_fields": list(exif_readable.keys())[:10],
372
+ "synthid_note": synthid_note,
373
+ "method": "Metadata / EXIF / SynthID Analysis",
374
+ "interpretation": (
375
+ f"🔴 AI marker detected: {detected_generator}" if detected_generator
376
+ else "🟡 Suspicious: missing camera metadata" if ai_score_meta > 0.55
377
+ else "🟢 Metadata consistent with real photo"
378
+ )
379
+ }
380
+ except Exception as e:
381
+ return {"score": 0.5, "method": f"Metadata (error: {e})", "interpretation": "Error"}
382
+
383
+
384
+ # ════════════════════════════════════
385
+ # ENSEMBLE FUSION
386
+ # ════════════════════════════════════
387
+
388
+ WEIGHTS = {
389
+ "neural": 0.40,
390
+ "ela": 0.20,
391
+ "fft": 0.15,
392
+ "noise": 0.10,
393
+ "metadata": 0.15,
394
+ }
395
+
396
+ def fuse_scores(neural, ela, fft, noise, meta) -> dict:
397
+ scores = {
398
+ "neural": neural.get("score", 0.5),
399
+ "ela": ela.get("score", 0.5),
400
+ "fft": fft.get("score", 0.5),
401
+ "noise": noise.get("score", 0.5),
402
+ "metadata": meta.get("score", 0.5),
403
+ }
404
+
405
+ weighted = sum(WEIGHTS[k] * v for k, v in scores.items())
406
+
407
+ # Boost if metadata strongly confirms
408
+ if meta.get("detected_generator"):
409
+ weighted = max(weighted, 0.88)
410
+
411
+ # Agreement bonus: if 4+ signals agree, boost confidence
412
+ threshold = 0.6
413
+ agreement = sum(1 for v in scores.values() if v > threshold)
414
+ if agreement >= 4:
415
+ weighted = min(weighted + 0.08, 0.99)
416
+
417
+ return {
418
+ "final_score": round(weighted, 4),
419
+ "individual_scores": {k: round(v, 4) for k, v in scores.items()},
420
+ "agreement_count": agreement,
421
+ "verdict": (
422
+ "🔴 VERY LIKELY AI-GENERATED" if weighted > 0.80
423
+ else "🟠 LIKELY AI-GENERATED" if weighted > 0.65
424
+ else "🟡 UNCERTAIN — POSSIBLY AI" if weighted > 0.50
425
+ else "🟢 LIKELY REAL PHOTO" if weighted > 0.30
426
+ else "🟢 VERY LIKELY REAL PHOTO"
427
+ ),
428
+ "confidence": f"{abs(weighted - 0.5) * 200:.0f}%",
429
+ }
430
+
431
+
432
+ # ════════════════════════════════════
433
+ # GENERATOR IDENTIFICATION
434
+ # ════════════════════════════════════
435
+
436
+ def identify_generator(signals: dict) -> str:
437
+ """Try to identify which specific AI generator produced the image."""
438
+ meta = signals.get("metadata", {})
439
+ if meta.get("detected_generator"):
440
+ return meta["detected_generator"]
441
+
442
+ fft_res = signals.get("fft", {})
443
+ ela_res = signals.get("ela", {})
444
+ noise_res = signals.get("noise", {})
445
+
446
+ nl = noise_res.get("noise_level", 10)
447
+ uniformity = ela_res.get("uniformity", 0.5)
448
+ periodic = fft_res.get("periodic_artifacts", False)
449
+
450
+ # Heuristic fingerprinting
451
+ if nl < 0.5 and uniformity > 0.85:
452
+ return "Likely: Nano Banana / Gemini Image (very smooth, near-zero noise)"
453
+ if periodic:
454
+ return "Likely: GAN-based (StyleGAN / older SD) — periodic grid artifacts"
455
+ if uniformity > 0.75 and nl < 3:
456
+ return "Likely: Diffusion model (Flux / DALL-E / SD) — low noise, uniform ELA"
457
+ if uniformity > 0.6:
458
+ return "Likely: AI-generated (model unknown) — moderate uniformity"
459
+
460
+ return "Cannot identify specific generator — signals inconclusive"
461
+
462
+
463
+ # ════════════════════════════════════
464
+ # MAIN DETECTION FUNCTION
465
+ # ════════════════════════════════════
466
+
467
+ def analyze_image(image, filename="uploaded_image.jpg"):
468
+ if image is None:
469
+ return "❌ No image provided", None, "{}"
470
+
471
+ img = Image.fromarray(image) if isinstance(image, np.ndarray) else image
472
+ fname = filename if filename else "uploaded_image"
473
+
474
+ # Run all signals
475
+ s1 = run_classifier(img)
476
+ s2 = run_ela(img)
477
+ s3 = run_fft(img)
478
+ s4 = run_noise_analysis(img)
479
+ s5 = run_metadata_analysis(img, fname)
480
+
481
+ # Ensemble
482
+ fusion = fuse_scores(s1, s2, s3, s4, s5)
483
+ generator_id = identify_generator({"fft": s3, "ela": s2, "noise": s4, "metadata": s5})
484
+
485
+ # Build report
486
+ verdict = fusion["verdict"]
487
+ score_pct = f"{fusion['final_score']*100:.1f}%"
488
+
489
+ report = f"""
490
+ # 🛡️ ImageShield — Forensic Report
491
+
492
+ ## {verdict}
493
+
494
+ **AI Probability Score: {score_pct}**
495
+ **Detection Confidence: {fusion['confidence']}**
496
+ **Signals in agreement: {fusion['agreement_count']}/5**
497
+
498
+ ---
499
+
500
+ ## 🔍 Generator Identification
501
+ {generator_id}
502
+
503
+ ### Nano Banana (Gemini) Note
504
+ {s5.get('synthid_note', 'No SynthID markers detected in metadata.')}
505
+
506
+ ---
507
+
508
+ ## 📊 Signal Breakdown
509
+
510
+ | Method | AI Score | Interpretation |
511
+ |--------|----------|----------------|
512
+ | Neural Classifier | {fusion['individual_scores']['neural']*100:.1f}% | {s1.get('label', 'N/A')} |
513
+ | ELA Analysis | {fusion['individual_scores']['ela']*100:.1f}% | {s2.get('interpretation', 'N/A')} |
514
+ | FFT Frequency | {fusion['individual_scores']['fft']*100:.1f}% | {s3.get('interpretation', 'N/A')} |
515
+ | PRNU Noise | {fusion['individual_scores']['noise']*100:.1f}% | {s4.get('interpretation', 'N/A')} |
516
+ | Metadata/EXIF | {fusion['individual_scores']['metadata']*100:.1f}% | {s5.get('interpretation', 'N/A')} |
517
+
518
+ ---
519
+
520
+ ## 🔬 Technical Details
521
+
522
+ **ELA:** Mean={s2.get('mean_ela','N/A')} | Std={s2.get('std_ela','N/A')} | Uniformity={s2.get('uniformity','N/A')}
523
+ **FFT:** Freq Ratio={s3.get('freq_ratio','N/A')} | Periodic artifacts={s3.get('periodic_artifacts','N/A')}
524
+ **Noise:** Level={s4.get('noise_level','N/A')}
525
+ **EXIF fields found:** {', '.join(s5.get('exif_fields', [])) or 'None'}
526
+ {('**AI markers:** ' + ' | '.join(s5.get('markers_found', []))) if s5.get('markers_found') else ''}
527
+
528
+ ---
529
+
530
+ ## 📚 Known Generator Signatures
531
+ {chr(10).join(f"**{k}:** {v}" for k, v in list(GENERATORS.items())[:4])}
532
+
533
+ ---
534
+ *ImageShield v2.0 · NOBODY204/ImageShield · S2T Ariana, Tunisia*
535
+ *Multi-signal forensic detection: Neural + ELA + FFT + PRNU + Metadata*
536
+ """.strip()
537
+
538
+ ela_img = s2.get("ela_image", None)
539
+
540
+ json_out = json.dumps({
541
+ "verdict": verdict,
542
+ "ai_probability": fusion["final_score"],
543
+ "generator": generator_id,
544
+ "signals": fusion["individual_scores"],
545
+ "metadata_markers": s5.get("markers_found", []),
546
+ }, indent=2)
547
+
548
+ return report, ela_img, json_out
549
+
550
+
551
+ # ════════════════════════════════════
552
+ # GRADIO UI
553
+ # ════════════════════════════════════
554
+
555
+ CSS = """
556
+ .container { max-width: 900px; margin: auto; }
557
+ .verdict-box { font-size: 1.4em; font-weight: bold; padding: 12px; border-radius: 8px; }
558
+ footer { display: none !important; }
559
+ """
560
+
561
+ with gr.Blocks(
562
+ title="🛡️ ImageShield — AI Image Detector",
563
+ theme=gr.themes.Base(primary_hue="blue", neutral_hue="slate"),
564
+ css=CSS
565
+ ) as demo:
566
+
567
+ gr.HTML("""
568
+ <div style="text-align:center; padding:20px 0 10px 0;">
569
+ <h1 style="font-size:2em; margin:0;">🛡️ ImageShield</h1>
570
+ <p style="color:#888; margin:4px 0 0 0; font-size:1em;">
571
+ AI Image Forensic Detector · Nano Banana · DALL-E · Midjourney · Flux · SD & more
572
+ </p>
573
+ <p style="color:#555; font-size:0.85em;">
574
+ Multi-signal pipeline: Neural + ELA + FFT + PRNU + Metadata/SynthID
575
+ </p>
576
+ </div>
577
+ """)
578
+
579
+ with gr.Row():
580
+ with gr.Column(scale=1):
581
+ image_input = gr.Image(
582
+ label="📤 Upload Image",
583
+ type="pil",
584
+ height=300,
585
+ )
586
+ filename_input = gr.Textbox(
587
+ label="Filename (optional)",
588
+ placeholder="image.jpg",
589
+ value=""
590
+ )
591
+ analyze_btn = gr.Button("🔍 Analyze Image", variant="primary", size="lg")
592
+
593
+ gr.HTML("""
594
+ <div style="font-size:0.8em; color:#666; margin-top:10px;">
595
+ <b>Detects:</b> Nano Banana (Gemini 2.5 Flash), Nano Banana Pro (Gemini 3 Pro),
596
+ GPT-Image-1, DALL-E 3, Midjourney v6/v7, Flux 1.0/1.1, Stable Diffusion XL/3.5,
597
+ Adobe Firefly, Grok 2, Ideogram 3.0, StyleGAN variants, and more.
598
+ </div>
599
+ """)
600
+
601
+ with gr.Column(scale=2):
602
+ report_output = gr.Markdown(label="📋 Forensic Report")
603
+
604
  with gr.Row():
605
+ ela_output = gr.Image(label="🔬 ELA Visualization (bright = inconsistent blocks)", type="pil", height=250)
606
+ json_output = gr.Code(label="📊 Raw Scores (JSON)", language="json", lines=15)
607
+
608
+ gr.HTML("""
609
+ <div style="text-align:center; padding:16px 0 8px 0; color:#555; font-size:0.8em;">
610
+ <b>About SynthID & Nano Banana:</b> Images generated by Google Gemini (Nano Banana / Nano Banana Pro)
611
+ contain an invisible SynthID watermark. For definitive Google AI verification, upload the image to
612
+ the Gemini app and ask "Was this created with Google AI?" · ImageShield detects via forensic signals.
613
+ <br><br>
614
+ 🛡️ <b>ImageShield v2.0</b> · NOBODY204 · S2T Ariana, Tunisia · MediaShield Suite 2026
615
+ </div>
616
+ """)
617
+
618
+ def run_analysis(img, fname):
619
+ if img is None:
620
+ return "❌ Please upload an image.", None, "{}"
621
+ return analyze_image(img, fname or "image.jpg")
622
+
623
+ analyze_btn.click(
624
+ fn=run_analysis,
625
+ inputs=[image_input, filename_input],
626
+ outputs=[report_output, ela_output, json_output]
627
+ )
628
+
629
+ gr.Examples(
630
+ examples=[],
631
+ inputs=[image_input],
632
+ )
633
 
634
  if __name__ == "__main__":
635
+ demo.launch(share=False)