NOBODY204 commited on
Commit
c1906a4
ยท
verified ยท
1 Parent(s): 9a77f66

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +92 -627
app.py CHANGED
@@ -1,635 +1,100 @@
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)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
  import numpy as np
3
+ import cv2
4
+ import matplotlib.pyplot as plt
5
+ from PIL import Image, ImageChops, ImageEnhance
6
+ from scipy import ndimage
7
+ import io, json, warnings
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
+ warnings.filterwarnings("ignore")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
12
+ # ๐Ÿ›ก๏ธ MEDIASHIELD X IMAGESHIELD (FUSION v3.0)
13
+ # Calibration spรฉciale : Archives & Mode Sombre
14
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
15
+
16
+ def apply_gamma_correction(img):
17
+ """Prรฉpare l'image pour l'analyse si elle est trop sombre."""
18
+ gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
19
+ brightness = np.mean(gray)
20
+ if brightness < 65:
21
+ # Correction pour rรฉvรฉler les artefacts de compression dans l'ombre
22
+ gamma = 0.6
23
+ invGamma = 1.0 / gamma
24
+ table = np.array([((i / 255.0) ** invGamma) * 255 for i in np.arange(0, 256)]).astype("uint8")
25
+ return cv2.LUT(img, table), f"๐ŸŒ™ Mode Sombre (Lumiรจre: {brightness:.1f})"
26
+ return img, "โ˜€๏ธ Lumiรจre Standard"
27
+
28
+ def analyze_forensics(img_input):
29
+ if img_input is None: return None, "โŒ Aucune image"
30
+
31
+ # 1. Prรฉparation (Correction de lumiรจre S2T)
32
+ img_cv = cv2.cvtColor(img_input, cv2.COLOR_RGBA2RGB) if img_input.shape[2] == 4 else img_input.copy()
33
+ img_ready, light_msg = apply_gamma_correction(img_cv)
34
+ pil_img = Image.fromarray(img_ready)
35
+
36
+ # 2. Analyse de Chrominance (Signatures Nano Banana)
37
+ ycrcb = cv2.cvtColor(img_ready, cv2.COLOR_RGB2YCrCb)
38
+ cr_var = ndimage.generic_filter(ycrcb[:,:,1], np.var, size=5)
39
+ chroma_score = np.std(cr_var) / (np.mean(cr_var) + 1e-8)
40
+
41
+ # 3. FFT & Grid Patterns (Signatures GAN/Diffusion)
42
+ gray = cv2.cvtColor(img_ready, cv2.COLOR_RGB2GRAY).astype(np.float32)
43
+ f_shift = np.fft.fftshift(np.fft.fft2(gray))
44
+ magnitude = np.abs(f_shift)
45
+ fft_vis = np.log(magnitude + 1)
46
+
47
+ # 4. ELA (Error Level Analysis)
48
+ quality = 90
49
+ buf = io.BytesIO()
50
+ pil_img.save(buf, format="JPEG", quality=quality)
51
+ recomp = Image.open(io.BytesIO(buf.getvalue())).convert("RGB")
52
+ ela_diff = ImageChops.difference(pil_img, recomp)
53
+ ela_vis = ImageEnhance.Brightness(ela_diff).enhance(10)
54
+
55
+ # 5. Calcul du Score Final
56
+ score = 0
57
+ reasons = []
58
+ if chroma_score < 0.9:
59
+ score += 35
60
+ reasons.append("โš ๏ธ Bruit chromatique trop uniforme (Typique IA)")
61
+ if np.std(magnitude) > 100000: # Seuil simplifiรฉ
62
+ score += 30
63
+ reasons.append("๐Ÿ”ฎ Artefacts de frรฉquence dรฉtectรฉs (FFT)")
64
+ if np.mean(np.array(ela_diff)) < 1.2:
65
+ score += 25
66
+ reasons.append("๐Ÿ” Compression suspecte (ELA trop lisse)")
67
+
68
+ final_verdict = "๐Ÿ”ด TRรˆS PROBABLEMENT IA" if score > 60 else "๐ŸŸข PROBABLEMENT Rร‰EL"
69
+
70
+ # Visualisation
71
+ fig, axes = plt.subplots(1, 3, figsize=(15, 5))
72
+ axes[0].imshow(img_cv); axes[0].set_title(f'Original ({light_msg})')
73
+ axes[1].imshow(fft_vis, cmap='viridis'); axes[1].set_title('Frรฉquences (FFT)')
74
+ axes[2].imshow(ela_vis); axes[2].set_title('Analyse ELA')
75
+ for ax in axes: ax.axis('off')
76
+
77
+ report = f"๐Ÿ›ก๏ธ MEDIASHIELD v3.0\nVERDICT: {final_verdict}\nScore: {score}/100\n\n{light_msg}\n\n" + "\n".join(reasons)
78
+ return fig, report
79
+
80
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
81
+ # UI Ariana Edition
82
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
83
+
84
+ with gr.Blocks(title="MediaShield v3.0", theme=gr.themes.Soft()) as demo:
85
+ gr.Markdown("# ๐Ÿ›ก๏ธ MediaShield v3.0 โ€” Forensic Archive Suite")
86
+ gr.Markdown("Fusion du moteur ImageShield et du correcteur optique MediaShield.")
87
+
88
  with gr.Row():
89
+ with gr.Column():
90
+ input_file = gr.Image(label="Dรฉpรดt d'archive numรฉrique", type="numpy")
91
+ run_btn = gr.Button("Dร‰MARRER L'ANALYSE FORENSIC", variant="primary")
92
+ with gr.Column():
93
+ output_plot = gr.Plot(label="Cartographie des signaux")
94
+ output_text = gr.Textbox(label="Rapport d'expertise S2T", lines=8)
 
 
 
 
 
 
95
 
96
+ run_btn.click(analyze_forensics, inputs=input_file, outputs=[output_plot, output_text])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
  if __name__ == "__main__":
99
+ demo.launch()
100
+