vaniv commited on
Commit
39aa383
·
verified ·
1 Parent(s): f3ba0c5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +33 -41
app.py CHANGED
@@ -14,9 +14,6 @@ def _enhance_for_display(pil_img, scale: float):
14
  return Image.fromarray(arr)
15
 
16
  def error_level_analysis(pil_img: Image.Image, quality: int = 90):
17
- """
18
- Save as JPEG at given quality, diff vs original, return (ela_image, mean_intensity in [0,1]).
19
- """
20
  img = pil_img.convert("RGB")
21
  with io.BytesIO() as buf:
22
  img.save(buf, "JPEG", quality=quality)
@@ -32,23 +29,15 @@ def error_level_analysis(pil_img: Image.Image, quality: int = 90):
32
  return ela_vis, mean_intensity
33
 
34
  def ela_sweep_mean(pil_img, qualities=(95, 90, 85)):
35
- """
36
- Try multiple JPEG qualities and take the max ELA response.
37
- Returns (peak, avg) for a small compression-aware adjustment later.
38
- """
39
  vals = []
40
  for q in qualities:
41
  _, m = error_level_analysis(pil_img, quality=q)
42
  vals.append(m)
43
  return float(max(vals)), float(np.mean(vals))
44
 
45
- # ====================== Frequency & Noise (accept a face mask) ======================
46
 
47
  def fft_high_freq_ratio(pil_img: Image.Image, mask=None):
48
- """
49
- High-frequency energy ratio from 2D FFT magnitude on Y (luma) channel.
50
- If mask provided (float [0,1]), analyze only masked region.
51
- """
52
  y = pil_img.convert("YCbCr").split()[0]
53
  gray = np.array(y, dtype=np.float32) / 255.0
54
  if mask is not None:
@@ -70,10 +59,6 @@ def fft_high_freq_ratio(pil_img: Image.Image, mask=None):
70
  return None, float(hf_ratio)
71
 
72
  def noise_inconsistency(pil_img: Image.Image, mask=None):
73
- """
74
- Laplacian sharpness variability on Y channel. Higher => more inconsistent texture.
75
- If mask provided, restrict to masked region.
76
- """
77
  y = pil_img.convert("YCbCr").split()[0]
78
  img = np.array(y, dtype=np.float32)
79
  if mask is not None:
@@ -82,7 +67,6 @@ def noise_inconsistency(pil_img: Image.Image, mask=None):
82
  lap = cv2.Laplacian(img, cv2.CV_32F, ksize=3)
83
  lap_abs = np.abs(lap)
84
 
85
- # Light normalization for stability (visual not used here)
86
  _ = exposure.equalize_adapthist(
87
  (lap_abs / (lap_abs.max() + 1e-9)).astype("float32"), clip_limit=0.01
88
  )
@@ -99,7 +83,7 @@ def noise_inconsistency(pil_img: Image.Image, mask=None):
99
  return None, 0.0
100
  vals = np.array(vals, dtype=np.float32)
101
  score = float(vals.std() / (vals.mean() + 1e-9))
102
- return None, float(np.tanh(score / 5.0)) # squash to ~[0,1]
103
 
104
  # ====================== Face crop + oval mask ======================
105
 
@@ -108,9 +92,6 @@ _mp_face = mp.solutions.face_detection.FaceDetection(
108
  )
109
 
110
  def crop_face(pil_img, pad=0.25):
111
- """
112
- Crop to the largest detected face and add a margin (pad). Fallback to original if none.
113
- """
114
  img = np.array(pil_img.convert("RGB"))
115
  h, w = img.shape[:2]
116
  res = _mp_face.process(cv2.cvtColor(img, cv2.COLOR_RGB2BGR))
@@ -119,15 +100,12 @@ def crop_face(pil_img, pad=0.25):
119
  det = max(res.detections, key=lambda d: d.location_data.relative_bounding_box.width)
120
  b = det.location_data.relative_bounding_box
121
  x, y, bw, bh = b.xmin, b.ymin, b.width, b.height
122
- x1 = int(max(0, (x - pad * bw) * w)); y1 = int(max(0, (y - pad * bh) * h))
123
- x2 = int(min(w, (x + bw + pad * bw) * w)); y2 = int(min(h, (y + bh + pad * bh) * h))
124
  face = Image.fromarray(img[y1:y2, x1:x2])
125
  return face if face.size[0] > 20 and face.size[1] > 20 else pil_img
126
 
127
- def face_oval_mask(img_pil, shrink=0.88):
128
- """
129
- Create an elliptical face mask (float [0,1]) to ignore hair/neck/jewelry/background.
130
- """
131
  w, h = img_pil.size
132
  mask = Image.new("L", (w, h), 0)
133
  draw = ImageDraw.Draw(mask)
@@ -135,19 +113,32 @@ def face_oval_mask(img_pil, shrink=0.88):
135
  draw.ellipse((dx, dy, w - dx, h - dy), fill=255)
136
  return np.array(mask, dtype=np.float32) / 255.0
137
 
138
- # ====================== Decision layer ======================
139
 
140
- def combine_scores(ela_mean, hf_ratio, noise_incons_score):
141
  """
142
- Weighted aggregation -> 'manipulated' confidence in [0,1].
143
- Tuned to reduce false positives from naturally sharp clothing/jewelry.
144
  """
145
- w1, w2, w3 = 0.30, 0.45, 0.25
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  s_ela = np.clip(ela_mean * 3.0, 0, 1)
147
- s_hf = np.clip((hf_ratio - 0.62) / 0.23, 0, 1)
148
  s_noi = np.clip(noise_incons_score, 0, 1)
149
- suspect = float(w1 * s_ela + w2 * s_hf + w3 * s_noi)
150
- label = "Likely Manipulated" if suspect >= 0.55 else "Likely Authentic"
151
  return label, suspect
152
 
153
  # ====================== Gradio handler ======================
@@ -156,23 +147,24 @@ def analyze_simple(pil_img: Image.Image):
156
  if pil_img is None:
157
  return "Upload an image."
158
 
159
- # 1) Face crop + normalize size
160
  pil_img = crop_face(pil_img)
161
  pil_img = pil_img.convert("RGB").resize((512, 512))
162
 
163
- # 2) Build an oval face mask to ignore clothes/jewelry/background
164
- oval = face_oval_mask(pil_img, shrink=0.88)
165
 
166
  # 3) Features
167
  ela_peak, ela_avg = ela_sweep_mean(pil_img)
168
- # soften ELA if image recompresses extremely cleanly
169
  ela_mean = ela_peak * (0.85 if ela_avg < 0.06 else 1.0)
170
 
171
  _, hf_ratio = fft_high_freq_ratio(pil_img, mask=oval)
172
  _, noi_score = noise_inconsistency(pil_img, mask=oval)
173
 
174
- # 4) Decision
175
- label, conf = combine_scores(ela_mean, hf_ratio, noi_score)
 
 
176
  return f"Deepfake likelihood: {conf*100:.1f}% — {label}"
177
 
178
  # ====================== UI ======================
 
14
  return Image.fromarray(arr)
15
 
16
  def error_level_analysis(pil_img: Image.Image, quality: int = 90):
 
 
 
17
  img = pil_img.convert("RGB")
18
  with io.BytesIO() as buf:
19
  img.save(buf, "JPEG", quality=quality)
 
29
  return ela_vis, mean_intensity
30
 
31
  def ela_sweep_mean(pil_img, qualities=(95, 90, 85)):
 
 
 
 
32
  vals = []
33
  for q in qualities:
34
  _, m = error_level_analysis(pil_img, quality=q)
35
  vals.append(m)
36
  return float(max(vals)), float(np.mean(vals))
37
 
38
+ # ====================== Frequency & Noise (support face masks) ======================
39
 
40
  def fft_high_freq_ratio(pil_img: Image.Image, mask=None):
 
 
 
 
41
  y = pil_img.convert("YCbCr").split()[0]
42
  gray = np.array(y, dtype=np.float32) / 255.0
43
  if mask is not None:
 
59
  return None, float(hf_ratio)
60
 
61
  def noise_inconsistency(pil_img: Image.Image, mask=None):
 
 
 
 
62
  y = pil_img.convert("YCbCr").split()[0]
63
  img = np.array(y, dtype=np.float32)
64
  if mask is not None:
 
67
  lap = cv2.Laplacian(img, cv2.CV_32F, ksize=3)
68
  lap_abs = np.abs(lap)
69
 
 
70
  _ = exposure.equalize_adapthist(
71
  (lap_abs / (lap_abs.max() + 1e-9)).astype("float32"), clip_limit=0.01
72
  )
 
83
  return None, 0.0
84
  vals = np.array(vals, dtype=np.float32)
85
  score = float(vals.std() / (vals.mean() + 1e-9))
86
+ return None, float(np.tanh(score / 5.0))
87
 
88
  # ====================== Face crop + oval mask ======================
89
 
 
92
  )
93
 
94
  def crop_face(pil_img, pad=0.25):
 
 
 
95
  img = np.array(pil_img.convert("RGB"))
96
  h, w = img.shape[:2]
97
  res = _mp_face.process(cv2.cvtColor(img, cv2.COLOR_RGB2BGR))
 
100
  det = max(res.detections, key=lambda d: d.location_data.relative_bounding_box.width)
101
  b = det.location_data.relative_bounding_box
102
  x, y, bw, bh = b.xmin, b.ymin, b.width, b.height
103
+ x1 = int(max(0, (x - pad*bw) * w)); y1 = int(max(0, (y - pad*bh) * h))
104
+ x2 = int(min(w, (x + bw + pad*bw) * w)); y2 = int(min(h, (y + bh + pad*bh) * h))
105
  face = Image.fromarray(img[y1:y2, x1:x2])
106
  return face if face.size[0] > 20 and face.size[1] > 20 else pil_img
107
 
108
+ def face_oval_mask(img_pil, shrink=0.80):
 
 
 
109
  w, h = img_pil.size
110
  mask = Image.new("L", (w, h), 0)
111
  draw = ImageDraw.Draw(mask)
 
113
  draw.ellipse((dx, dy, w - dx, h - dy), fill=255)
114
  return np.array(mask, dtype=np.float32) / 255.0
115
 
116
+ # ====================== Natural texture correction ======================
117
 
118
+ def natural_texture_correction(pil_img: Image.Image):
119
  """
120
+ Down-weight suspicion for clean studio portraits with smooth gradients.
121
+ Returns factor in [0.7, 1.0]; lower => more likely real.
122
  """
123
+ gray = np.array(pil_img.convert("L"), dtype=np.float32) / 255.0
124
+ grad_x = cv2.Sobel(gray, cv2.CV_32F, 1, 0, ksize=3)
125
+ grad_y = cv2.Sobel(gray, cv2.CV_32F, 0, 1, ksize=3)
126
+ edge_strength = np.mean(np.sqrt(grad_x**2 + grad_y**2))
127
+ flatness = np.std(gray)
128
+ ratio = edge_strength / (flatness + 1e-6) # small -> smooth/realistic
129
+ corr = 1.0 - np.clip((0.15 - ratio) * 2.5, 0, 0.3)
130
+ return float(np.clip(corr, 0.7, 1.0))
131
+
132
+ # ====================== Decision layer ======================
133
+
134
+ def combine_scores(ela_mean, hf_ratio, noise_incons_score, texture_corr=1.0):
135
+ # Balanced, with stricter cutoff to curb false positives on real portraits
136
+ w1, w2, w3 = 0.30, 0.40, 0.30
137
  s_ela = np.clip(ela_mean * 3.0, 0, 1)
138
+ s_hf = np.clip((hf_ratio - 0.65) / 0.25, 0, 1)
139
  s_noi = np.clip(noise_incons_score, 0, 1)
140
+ suspect = float((w1*s_ela + w2*s_hf + w3*s_noi) * texture_corr)
141
+ label = "Likely Manipulated" if suspect >= 0.65 else "Likely Authentic"
142
  return label, suspect
143
 
144
  # ====================== Gradio handler ======================
 
147
  if pil_img is None:
148
  return "Upload an image."
149
 
150
+ # 1) Face crop + normalize
151
  pil_img = crop_face(pil_img)
152
  pil_img = pil_img.convert("RGB").resize((512, 512))
153
 
154
+ # 2) Face-only oval mask to ignore hair/neck/jewelry/background
155
+ oval = face_oval_mask(pil_img, shrink=0.80)
156
 
157
  # 3) Features
158
  ela_peak, ela_avg = ela_sweep_mean(pil_img)
 
159
  ela_mean = ela_peak * (0.85 if ela_avg < 0.06 else 1.0)
160
 
161
  _, hf_ratio = fft_high_freq_ratio(pil_img, mask=oval)
162
  _, noi_score = noise_inconsistency(pil_img, mask=oval)
163
 
164
+ # 4) Natural texture correction + decision
165
+ texture_corr = natural_texture_correction(pil_img)
166
+ label, conf = combine_scores(ela_mean, hf_ratio, noi_score, texture_corr)
167
+
168
  return f"Deepfake likelihood: {conf*100:.1f}% — {label}"
169
 
170
  # ====================== UI ======================