anky2002 commited on
Commit
7690770
Β·
verified Β·
1 Parent(s): 8917df2

Upload agents/sensor_agent.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. agents/sensor_agent.py +147 -2
agents/sensor_agent.py CHANGED
@@ -198,13 +198,158 @@ def analyze_bayer_demosaicing(img: Image.Image) -> Dict[str, Any]:
198
  }
199
 
200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  # ─── Main Agent Entry Point ─────────────────────────────────────────
202
  def run_sensor_agent(img: Image.Image) -> AgentEvidence:
203
  """Run all sensor characteristic tests."""
204
  findings = []
205
  scores = []
206
 
207
- for fn in [analyze_prnu, analyze_noise_structure, analyze_bayer_demosaicing]:
 
208
  try:
209
  result = fn(img)
210
  findings.append(result)
@@ -233,7 +378,7 @@ def run_sensor_agent(img: Image.Image) -> AgentEvidence:
233
  agent_name="Sensor Characteristics Agent",
234
  violation_score=np.clip(avg_score, -1, 1),
235
  confidence=confidence,
236
- failure_prob=max(0.0, 1.0 - len(scores) / 3),
237
  rationale=rationale,
238
  sub_findings=findings,
239
  )
 
198
  }
199
 
200
 
201
+ # ─── CFA Pattern Verification ────────────────────────────────────────
202
+ def analyze_cfa_pattern(img: Image.Image) -> Dict[str, Any]:
203
+ """
204
+ Real camera images retain traces of CFA (Color Filter Array) interpolation.
205
+ Detect periodic patterns in cross-channel differences at Nyquist frequency.
206
+ """
207
+ rgb = np.array(img.convert("RGB")).astype(np.float64)
208
+ r, g, b = rgb[:, :, 0], rgb[:, :, 1], rgb[:, :, 2]
209
+
210
+ # Compute RG and BG differences
211
+ rg = r - g
212
+ bg = b - g
213
+
214
+ # 2D FFT of difference channels
215
+ fft_rg = np.abs(np.fft.fftshift(np.fft.fft2(rg)))
216
+ fft_bg = np.abs(np.fft.fftshift(np.fft.fft2(bg)))
217
+
218
+ h, w = fft_rg.shape
219
+ cy, cx = h // 2, w // 2
220
+
221
+ # Check for Bayer CFA signature: peaks at (N/2, 0), (0, N/2), (N/2, N/2)
222
+ nyquist_energy_rg = float(
223
+ fft_rg[cy, 0] + fft_rg[0, cx] + fft_rg[0, 0]
224
+ ) / 3
225
+ center_energy_rg = float(np.mean(fft_rg[cy - 5:cy + 5, cx - 5:cx + 5]))
226
+ cfa_ratio = nyquist_energy_rg / (center_energy_rg + 1e-9)
227
+
228
+ if cfa_ratio > 1.5:
229
+ score = -0.3
230
+ note = f"CFA interpolation traces detected (ratio={cfa_ratio:.2f}, real camera)"
231
+ elif cfa_ratio < 0.5:
232
+ score = 0.3
233
+ note = f"No CFA traces (ratio={cfa_ratio:.2f}, possible AI generation)"
234
+ else:
235
+ score = 0.0
236
+ note = f"Ambiguous CFA analysis (ratio={cfa_ratio:.2f})"
237
+
238
+ return {
239
+ "test": "CFA Pattern Verification",
240
+ "cfa_nyquist_ratio": round(cfa_ratio, 4),
241
+ "score": score,
242
+ "note": note,
243
+ }
244
+
245
+
246
+ # ─── Hot/Dead Pixel Analysis ────────────────────────────────────────
247
+ def analyze_hot_dead_pixels(img: Image.Image) -> Dict[str, Any]:
248
+ """
249
+ Real sensors have hot (stuck bright) and dead (stuck dark) pixels.
250
+ AI images lack these sensor defects entirely.
251
+ """
252
+ gray = np.array(img.convert("L")).astype(np.float64)
253
+ h, w = gray.shape
254
+
255
+ # Local median filter
256
+ from scipy.ndimage import median_filter
257
+ med = median_filter(gray, size=5)
258
+
259
+ diff = np.abs(gray - med)
260
+
261
+ # Hot pixels: much brighter than neighbors
262
+ hot_threshold = np.percentile(diff, 99.9)
263
+ hot_pixels = int(np.sum(diff > hot_threshold))
264
+
265
+ # Dead pixels: much darker than neighbors AND very low absolute value
266
+ dark_mask = (gray < 5) & (diff > hot_threshold * 0.5)
267
+ dead_pixels = int(np.sum(dark_mask))
268
+
269
+ total_defects = hot_pixels + dead_pixels
270
+ defect_rate = total_defects / (h * w)
271
+
272
+ # Real cameras: typically 0.001%-0.01% defective pixels
273
+ if 0.00001 < defect_rate < 0.001:
274
+ score = -0.2
275
+ note = f"Sensor defects detected ({total_defects} pixels, rate={defect_rate:.6f}, real camera)"
276
+ elif defect_rate < 0.000001:
277
+ score = 0.2
278
+ note = f"No sensor defects ({total_defects} pixels, possible AI generation)"
279
+ else:
280
+ score = 0.0
281
+ note = f"Defect rate={defect_rate:.6f} ({total_defects} pixels)"
282
+
283
+ return {
284
+ "test": "Hot/Dead Pixel Analysis",
285
+ "hot_pixels": hot_pixels,
286
+ "dead_pixels": dead_pixels,
287
+ "defect_rate": round(defect_rate, 8),
288
+ "score": score,
289
+ "note": note,
290
+ }
291
+
292
+
293
+ # ─── JPEG Quantization Table Analysis ───────────────────────────────
294
+ def analyze_jpeg_quantization(img: Image.Image) -> Dict[str, Any]:
295
+ """
296
+ Real JPEG images have specific quantization tables from camera firmware.
297
+ AI-generated images saved as JPEG have generic tables.
298
+ Double-compressed images show quantization table mismatches.
299
+ """
300
+ try:
301
+ qtables = img.quantization
302
+ if qtables:
303
+ # Standard JPEG quality tables
304
+ tables = list(qtables.values())
305
+ n_tables = len(tables)
306
+
307
+ # Analyze first table (luminance)
308
+ if tables:
309
+ t = np.array(list(tables[0].values()) if isinstance(tables[0], dict) else list(tables[0]))
310
+ if len(t) == 64:
311
+ # Check for standard Photoshop/camera patterns
312
+ is_uniform = float(np.std(t)) < 5
313
+ max_q = int(np.max(t))
314
+ min_q = int(np.min(t))
315
+
316
+ if is_uniform:
317
+ score = 0.2
318
+ note = f"Unusual uniform quantization table (std={np.std(t):.1f})"
319
+ elif max_q > 100:
320
+ score = -0.2
321
+ note = f"Heavy compression quantization (max={max_q}, camera-typical)"
322
+ else:
323
+ score = -0.1
324
+ note = f"Standard quantization table ({n_tables} tables, range=[{min_q},{max_q}])"
325
+ else:
326
+ score = 0.0
327
+ note = "Non-standard quantization table size"
328
+ else:
329
+ score = 0.1
330
+ note = "No luminance quantization table found"
331
+ else:
332
+ score = 0.1
333
+ note = "No quantization tables (not JPEG or tables stripped)"
334
+ except Exception:
335
+ score = 0.0
336
+ note = "Unable to read quantization tables"
337
+
338
+ return {
339
+ "test": "JPEG Quantization Table",
340
+ "score": score,
341
+ "note": note,
342
+ }
343
+
344
+
345
  # ─── Main Agent Entry Point ─────────────────────────────────────────
346
  def run_sensor_agent(img: Image.Image) -> AgentEvidence:
347
  """Run all sensor characteristic tests."""
348
  findings = []
349
  scores = []
350
 
351
+ for fn in [analyze_prnu, analyze_noise_structure, analyze_bayer_demosaicing,
352
+ analyze_cfa_pattern, analyze_hot_dead_pixels, analyze_jpeg_quantization]:
353
  try:
354
  result = fn(img)
355
  findings.append(result)
 
378
  agent_name="Sensor Characteristics Agent",
379
  violation_score=np.clip(avg_score, -1, 1),
380
  confidence=confidence,
381
+ failure_prob=max(0.0, 1.0 - len(scores) / 6),
382
  rationale=rationale,
383
  sub_findings=findings,
384
  )