HangboY commited on
Commit
7957cc3
·
verified ·
1 Parent(s): 95ca8f3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +107 -65
app.py CHANGED
@@ -1,38 +1,70 @@
1
-
2
- import io, math
3
  from PIL import Image, ImageChops, ImageStat, ExifTags
4
  import gradio as gr
5
 
 
6
  GENERATOR_KEYWORDS = [
7
  "stable diffusion", "stability.ai", "sdxl", "midjourney", "dall", "openai",
8
  "novelai", "leonardo", "kaiber", "flux", "comfyui", "automatic1111", "invokeai"
9
  ]
10
 
11
- def to_rgb(img):
12
- if img.mode in ["RGBA", "P"]:
13
- return img.convert("RGB")
14
- if img.mode != "RGB":
15
- return img.convert("RGB")
16
- return img
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
  def compute_ela_score(img, quality=95):
19
- # Error Level Analysis: recompress at given JPEG quality, then compute difference stats
20
- img_rgb = to_rgb(img)
21
- buf = io.BytesIO()
22
- img_rgb.save(buf, "JPEG", quality=quality, optimize=True)
23
- buf.seek(0)
24
- recompressed = Image.open(buf).convert("RGB")
25
- ela = ImageChops.difference(img_rgb, recompressed)
26
- stat = ImageStat.Stat(ela)
27
- # mean and std over RGB channels
28
- mean = sum(stat.mean) / len(stat.mean)
29
- std = sum(stat.stddev) / len(stat.stddev)
30
- # normalize to 0-1 rough scale (not used for decision, but could be returned)
31
- mean_norm = min(mean / 10.0, 1.0)
32
- std_norm = min(std / 10.0, 1.0)
33
- return mean, std, mean_norm, std_norm
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
  def extract_exif_flags(img):
 
36
  exif = {}
37
  try:
38
  raw = img.getexif()
@@ -44,69 +76,79 @@ def extract_exif_flags(img):
44
 
45
  exif_str = " ".join([str(v).lower() for v in exif.values()]) if exif else ""
46
  has_camera_fields = any(tag in exif for tag in ["Make", "Model", "LensModel", "DateTimeOriginal"])
47
- has_software = "Software" in exif
48
  has_generator_kw = any(kw in exif_str for kw in GENERATOR_KEYWORDS)
49
  empty_exif = (len(exif) == 0)
50
 
51
  preview = {}
52
- for k in ["Make","Model","LensModel","Software","DateTimeOriginal"]:
53
  if k in exif:
54
  preview[k] = str(exif[k])
55
 
56
  return {
57
  "has_camera_fields": has_camera_fields,
58
- "has_software": has_software,
59
  "has_generator_kw": has_generator_kw,
60
  "empty_exif": empty_exif,
61
  "exif_preview": preview
62
  }
63
 
64
  def ai_likelihood(img):
65
- if img is None:
66
- return {"error": "No image uploaded."}
67
- info = extract_exif_flags(img)
68
- mean, std, mean_norm, std_norm = compute_ela_score(img)
69
-
70
- score = 0.5
71
- reasons = []
72
-
73
- if info["has_generator_kw"]:
74
- score += 0.4
75
- reasons.append("Metadata contains generator keywords (e.g., Stable Diffusion/Midjourney).")
76
- if info["has_camera_fields"]:
77
- score -= 0.2
78
- reasons.append("Camera EXIF fields found (Make/Model/Lens), suggesting real capture.")
79
- if info["empty_exif"]:
80
- score += 0.1
81
- reasons.append("No EXIF found (common in exported AI images or screenshots).")
82
- # ELA heuristic: AI images often show uniform reconstruction errors
83
- if mean < 2.0 and std < 2.0:
84
- score += 0.15
85
- reasons.append("ELA mean/std are very low → uniform compression error (AI-like).")
86
- elif mean > 4.0 or std > 4.0:
87
- score -= 0.05
88
- reasons.append("ELA mean/std are higher → natural camera/post-processing artifacts (Real-like).")
89
-
90
- score = max(0.0, min(1.0, score))
91
- label = "Likely AI" if score >= 0.6 else ("Uncertain" if 0.4 <= score < 0.6 else "Likely Real")
92
-
93
- explanation = {
94
- "label": label,
95
- "ai_probability": round(score, 3),
96
- "ela_mean": round(mean, 3),
97
- "ela_std": round(std, 3),
98
- "exif": info["exif_preview"],
99
- "notes": reasons or ["No strong signals; result uncertain."]
100
- }
101
- return explanation
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
  with gr.Blocks() as demo:
104
  gr.Markdown("""
105
  # 🕵️ FakeSpotter (Heuristic Demo)
106
  Upload an image to estimate whether it is **AI-generated** or **Real** using simple FREE heuristics:
107
  - Metadata scan (generator keywords vs. camera EXIF)
108
- - ELA (Error Level Analysis) statistics
109
- > ⚠️ This is a classroom demo, **not a forensic tool**.
110
  """)
111
  inp = gr.Image(type="pil", label="Upload image")
112
  out = gr.JSON(label="Result")
 
1
+ import io
 
2
  from PIL import Image, ImageChops, ImageStat, ExifTags
3
  import gradio as gr
4
 
5
+ # 可能出现在 EXIF 里的生成器关键词(可自行扩展)
6
  GENERATOR_KEYWORDS = [
7
  "stable diffusion", "stability.ai", "sdxl", "midjourney", "dall", "openai",
8
  "novelai", "leonardo", "kaiber", "flux", "comfyui", "automatic1111", "invokeai"
9
  ]
10
 
11
+ def to_rgb_flat(img, bg=(255, 255, 255)):
12
+ """确保得到 RGB;遇到 RGBA/带透明通道时做白底合成,避免 JPEG 保存时报错。"""
13
+ if img.mode == "RGB":
14
+ return img
15
+ if img.mode in ("RGBA", "LA", "P"):
16
+ bg_img = Image.new("RGB", img.size, bg)
17
+ if img.mode == "P":
18
+ img = img.convert("RGBA")
19
+ bg_img.paste(img, mask=img.split()[-1] if "A" in img.getbands() else None)
20
+ return bg_img
21
+ return img.convert("RGB")
22
+
23
+ def resize_max(img, max_side=1024):
24
+ """把最长边限制到 1024,降低内存占用并避免某些编码错误。"""
25
+ w, h = img.size
26
+ m = max(w, h)
27
+ if m <= max_side:
28
+ return img
29
+ scale = max_side / float(m)
30
+ return img.resize((int(w * scale), int(h * scale)), Image.LANCZOS)
31
 
32
  def compute_ela_score(img, quality=95):
33
+ """
34
+ ELA(误差层分析):以给定 JPEG 质量重压一遍,然后计算差异图的均值/方差。
35
+ 若失败(例如非 JPEG 友好的模式/编解码异常),返回 (None, None) 并由上层降级处理。
36
+ """
37
+ try:
38
+ img_rgb = to_rgb_flat(img)
39
+ img_rgb = resize_max(img_rgb, 1024)
40
+ buf = io.BytesIO()
41
+ img_rgb.save(buf, "JPEG", quality=quality, optimize=True)
42
+ buf.seek(0)
43
+ recompressed = Image.open(buf).convert("RGB")
44
+ ela = ImageChops.difference(img_rgb, recompressed)
45
+ stat = ImageStat.Stat(ela)
46
+ mean = float(sum(stat.mean) / len(stat.mean))
47
+ std = float(sum(stat.stddev) / len(stat.stddev))
48
+ return mean, std
49
+ except Exception:
50
+ # 再尝试一次更保守的质量设置
51
+ try:
52
+ img_rgb = to_rgb_flat(img)
53
+ img_rgb = resize_max(img_rgb, 1024)
54
+ buf = io.BytesIO()
55
+ img_rgb.save(buf, "JPEG", quality=85)
56
+ buf.seek(0)
57
+ recompressed = Image.open(buf).convert("RGB")
58
+ ela = ImageChops.difference(img_rgb, recompressed)
59
+ stat = ImageStat.Stat(ela)
60
+ mean = float(sum(stat.mean) / len(stat.mean))
61
+ std = float(sum(stat.stddev) / len(stat.stddev))
62
+ return mean, std
63
+ except Exception:
64
+ return None, None # 彻底放弃 ELA,交由上层“降级”
65
 
66
  def extract_exif_flags(img):
67
+ """读取少量常见 EXIF 字段,并搜寻生成器关键词。异常直接吞掉,返回尽量多的信息。"""
68
  exif = {}
69
  try:
70
  raw = img.getexif()
 
76
 
77
  exif_str = " ".join([str(v).lower() for v in exif.values()]) if exif else ""
78
  has_camera_fields = any(tag in exif for tag in ["Make", "Model", "LensModel", "DateTimeOriginal"])
 
79
  has_generator_kw = any(kw in exif_str for kw in GENERATOR_KEYWORDS)
80
  empty_exif = (len(exif) == 0)
81
 
82
  preview = {}
83
+ for k in ["Make", "Model", "LensModel", "Software", "DateTimeOriginal"]:
84
  if k in exif:
85
  preview[k] = str(exif[k])
86
 
87
  return {
88
  "has_camera_fields": has_camera_fields,
 
89
  "has_generator_kw": has_generator_kw,
90
  "empty_exif": empty_exif,
91
  "exif_preview": preview
92
  }
93
 
94
  def ai_likelihood(img):
95
+ """
96
+ 主入口:任何异常都捕获,返回 JSON 友好信息而不是让前端报“错误”。
97
+ """
98
+ try:
99
+ if img is None:
100
+ return {"label": "Error", "message": "No image uploaded."}
101
+
102
+ info = extract_exif_flags(img)
103
+ ela_mean, ela_std = compute_ela_score(img)
104
+
105
+ # 初始分数(0.5 = 不确定)
106
+ score = 0.5
107
+ reasons = []
108
+
109
+ if info["has_generator_kw"]:
110
+ score += 0.4
111
+ reasons.append("Metadata contains generator keywords (e.g., Stable Diffusion/Midjourney).")
112
+ if info["has_camera_fields"]:
113
+ score -= 0.2
114
+ reasons.append("Camera EXIF fields found (Make/Model/Lens/DateTimeOriginal).")
115
+ if info["empty_exif"]:
116
+ score += 0.1
117
+ reasons.append("No EXIF found (common in exported AI images or screenshots).")
118
+
119
+ if ela_mean is not None and ela_std is not None:
120
+ if ela_mean < 2.0 and ela_std < 2.0:
121
+ score += 0.15
122
+ reasons.append("ELA mean/std are very low → uniform compression error (AI-like).")
123
+ elif ela_mean > 4.0 or ela_std > 4.0:
124
+ score -= 0.05
125
+ reasons.append("ELA mean/std are higher → natural camera/post-processing artifacts (Real-like).")
126
+ else:
127
+ reasons.append("ELA failed (unsupported format/codec); decision based on metadata only.")
128
+
129
+ score = max(0.0, min(1.0, score))
130
+ label = "Likely AI" if score >= 0.6 else ("Uncertain" if 0.4 <= score < 0.6 else "Likely Real")
131
+
132
+ return {
133
+ "label": label,
134
+ "ai_probability": round(score, 3),
135
+ "ela_mean": None if ela_mean is None else round(ela_mean, 3),
136
+ "ela_std": None if ela_std is None else round(ela_std, 3),
137
+ "exif": info["exif_preview"],
138
+ "notes": reasons or ["No strong signals; result uncertain."]
139
+ }
140
+
141
+ except Exception as e:
142
+ # 兜底:把异常显示在 JSON 里,便于你在前端看到具体原因
143
+ return {"label": "Error", "message": str(e)}
144
 
145
  with gr.Blocks() as demo:
146
  gr.Markdown("""
147
  # 🕵️ FakeSpotter (Heuristic Demo)
148
  Upload an image to estimate whether it is **AI-generated** or **Real** using simple FREE heuristics:
149
  - Metadata scan (generator keywords vs. camera EXIF)
150
+ - ELA (Error Level Analysis) statistics
151
+ > ⚠️ Classroom demo, **not** a forensic tool.
152
  """)
153
  inp = gr.Image(type="pil", label="Upload image")
154
  out = gr.JSON(label="Result")