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

Upload agents/optical_agent.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. agents/optical_agent.py +253 -28
agents/optical_agent.py CHANGED
@@ -292,39 +292,264 @@ def analyze_bokeh(img: Image.Image) -> Dict[str, Any]:
292
  }
293
 
294
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
  # ─── Main Agent Entry Point ─────────────────────────────────────────
296
  def run_optical_agent(img: Image.Image) -> AgentEvidence:
297
  """Run all optical physics tests and produce structured evidence."""
298
  findings = []
299
  scores = []
300
 
301
- try:
302
- ca = analyze_chromatic_aberration(img)
303
- findings.append(ca)
304
- scores.append(ca["score"])
305
- except Exception as e:
306
- findings.append({"test": "Chromatic Aberration", "error": str(e), "score": 0})
307
-
308
- try:
309
- vig = analyze_vignetting(img)
310
- findings.append(vig)
311
- scores.append(vig["score"])
312
- except Exception as e:
313
- findings.append({"test": "Vignetting", "error": str(e), "score": 0})
314
-
315
- try:
316
- dof = analyze_dof_consistency(img)
317
- findings.append(dof)
318
- scores.append(dof["score"])
319
- except Exception as e:
320
- findings.append({"test": "DoF Consistency", "error": str(e), "score": 0})
321
-
322
- try:
323
- bokeh = analyze_bokeh(img)
324
- findings.append(bokeh)
325
- scores.append(bokeh["score"])
326
- except Exception as e:
327
- findings.append({"test": "Bokeh Microstructure", "error": str(e), "score": 0})
328
 
329
  if scores:
330
  avg_score = float(np.mean(scores))
@@ -352,7 +577,7 @@ def run_optical_agent(img: Image.Image) -> AgentEvidence:
352
  agent_name="Optical Physics Agent",
353
  violation_score=np.clip(avg_score, -1, 1),
354
  confidence=confidence,
355
- failure_prob=max(0.0, 1.0 - len(scores) / 4),
356
  rationale=rationale,
357
  sub_findings=findings,
358
  )
 
292
  }
293
 
294
 
295
+ # ─── Lens Distortion Analysis ────────────────────────────────────────
296
+ def analyze_lens_distortion(img: Image.Image) -> Dict[str, Any]:
297
+ """
298
+ Real lenses produce barrel/pincushion distortion following Brown-Conrady model.
299
+ AI images often have perfectly rectilinear geometry or impossible distortion.
300
+ """
301
+ gray = _to_gray(img)
302
+ h, w = gray.shape
303
+
304
+ # Edge detection
305
+ ex = sobel(gray, axis=1)
306
+ ey = sobel(gray, axis=0)
307
+ edge_mag = np.hypot(ex, ey)
308
+
309
+ # Threshold strong edges
310
+ threshold = np.percentile(edge_mag, 90)
311
+ strong_edges = edge_mag > threshold
312
+
313
+ # Analyze edge straightness in radial bands
314
+ cy, cx = h / 2, w / 2
315
+ Y, X = np.mgrid[0:h, 0:w]
316
+ R = np.sqrt((X - cx) ** 2 + (Y - cy) ** 2)
317
+ R_max = np.sqrt(cx ** 2 + cy ** 2)
318
+ R_norm = R / R_max
319
+
320
+ # Compare edge density at different radial distances
321
+ inner_edges = float(np.mean(strong_edges[R_norm < 0.3]))
322
+ mid_edges = float(np.mean(strong_edges[(R_norm >= 0.3) & (R_norm < 0.7)]))
323
+ outer_edges = float(np.mean(strong_edges[R_norm >= 0.7]))
324
+
325
+ # Real lenses: edges slightly softer at corners due to distortion
326
+ # AI: uniform edge sharpness across frame
327
+ edge_ratio = outer_edges / (inner_edges + 1e-9)
328
+
329
+ if 0.5 < edge_ratio < 0.9:
330
+ score = -0.3
331
+ note = f"Natural edge falloff at periphery (ratio={edge_ratio:.3f}, lens distortion present)"
332
+ elif edge_ratio > 0.95:
333
+ score = 0.3
334
+ note = f"Unnaturally uniform edges across frame (ratio={edge_ratio:.3f}, no lens distortion)"
335
+ else:
336
+ score = 0.1
337
+ note = f"Edge distribution ratio={edge_ratio:.3f}"
338
+
339
+ return {
340
+ "test": "Lens Distortion",
341
+ "edge_ratio_outer_inner": round(edge_ratio, 4),
342
+ "inner_edge_density": round(inner_edges, 4),
343
+ "outer_edge_density": round(outer_edges, 4),
344
+ "score": score,
345
+ "note": note,
346
+ }
347
+
348
+
349
+ # ─── CA Radial Pattern Analysis ─────────────────────────────────────
350
+ def analyze_ca_radial_pattern(img: Image.Image) -> Dict[str, Any]:
351
+ """
352
+ Real chromatic aberration increases radially from center (more at corners).
353
+ AI images have spatially uniform or random channel misregistration.
354
+ """
355
+ rgb = _to_rgb(img)
356
+ h, w, _ = rgb.shape
357
+ cy, cx = h / 2, w / 2
358
+
359
+ r, g, b = rgb[:, :, 0], rgb[:, :, 1], rgb[:, :, 2]
360
+
361
+ # Compute local channel difference in blocks
362
+ block_size = max(32, min(h, w) // 8)
363
+ center_diffs = []
364
+ edge_diffs = []
365
+
366
+ Y, X = np.mgrid[0:h, 0:w]
367
+ R = np.sqrt((X - cx) ** 2 + (Y - cy) ** 2)
368
+ R_max = np.sqrt(cx ** 2 + cy ** 2)
369
+
370
+ for bi in range(0, h - block_size, block_size):
371
+ for bj in range(0, w - block_size, block_size):
372
+ block_r = r[bi:bi + block_size, bj:bj + block_size]
373
+ block_g = g[bi:bi + block_size, bj:bj + block_size]
374
+ block_b = b[bi:bi + block_size, bj:bj + block_size]
375
+
376
+ # Local RG difference as proxy for CA
377
+ rg_diff = float(np.std(block_r - block_g))
378
+ rb_diff = float(np.std(block_r - block_b))
379
+ ca_magnitude = (rg_diff + rb_diff) / 2
380
+
381
+ block_center_r = R[bi + block_size // 2, bj + block_size // 2] / R_max
382
+
383
+ if block_center_r < 0.4:
384
+ center_diffs.append(ca_magnitude)
385
+ elif block_center_r > 0.6:
386
+ edge_diffs.append(ca_magnitude)
387
+
388
+ if center_diffs and edge_diffs:
389
+ center_ca = float(np.mean(center_diffs))
390
+ edge_ca = float(np.mean(edge_diffs))
391
+ ca_increase = edge_ca / (center_ca + 1e-9)
392
+
393
+ # Real lenses: CA increases toward edges (ratio > 1.1)
394
+ if ca_increase > 1.15:
395
+ score = -0.3
396
+ note = f"CA increases radially (edge/center={ca_increase:.2f}, natural lens behavior)"
397
+ elif ca_increase < 0.9:
398
+ score = 0.3
399
+ note = f"CA decreases toward edges (ratio={ca_increase:.2f}, unnatural)"
400
+ else:
401
+ score = 0.1
402
+ note = f"Flat CA distribution (ratio={ca_increase:.2f})"
403
+ else:
404
+ ca_increase = 1.0
405
+ score = 0.0
406
+ note = "Insufficient data for radial CA analysis"
407
+
408
+ return {
409
+ "test": "CA Radial Pattern",
410
+ "ca_edge_center_ratio": round(ca_increase, 4),
411
+ "score": score,
412
+ "note": note,
413
+ }
414
+
415
+
416
+ # ─── Specular Reflection Map ────────────────────────────────────────
417
+ def analyze_specular_reflections(img: Image.Image) -> Dict[str, Any]:
418
+ """
419
+ Real specular reflections follow Phong/Blinn-Phong model with
420
+ consistent highlight shapes. AI often has inconsistent specularity.
421
+ """
422
+ rgb = _to_rgb(img)
423
+ gray = np.mean(rgb, axis=-1)
424
+
425
+ # Detect specular highlights (very bright, near-white pixels)
426
+ highlight_threshold = np.percentile(gray, 98)
427
+ highlight_mask = gray > highlight_threshold
428
+
429
+ # Compute saturation (low saturation = specular)
430
+ max_c = np.max(rgb, axis=-1)
431
+ min_c = np.min(rgb, axis=-1)
432
+ saturation = (max_c - min_c) / (max_c + 1e-9)
433
+
434
+ specular_mask = highlight_mask & (saturation < 0.2)
435
+ n_specular = int(np.sum(specular_mask))
436
+ specular_fraction = float(n_specular / (gray.size + 1e-9))
437
+
438
+ if n_specular < 50:
439
+ return {
440
+ "test": "Specular Reflections",
441
+ "score": 0.0,
442
+ "note": "Insufficient specular highlights for analysis",
443
+ "specular_count": n_specular,
444
+ }
445
+
446
+ # Check if specular highlights are compact (real) vs diffuse (AI)
447
+ from scipy.ndimage import label
448
+ labeled, n_features = label(specular_mask)
449
+ if n_features > 0:
450
+ sizes = [int(np.sum(labeled == i)) for i in range(1, min(n_features + 1, 100))]
451
+ avg_size = float(np.mean(sizes))
452
+ size_std = float(np.std(sizes))
453
+ size_cv = size_std / (avg_size + 1e-9) # coefficient of variation
454
+ else:
455
+ size_cv = 0.0
456
+ avg_size = 0.0
457
+
458
+ # Real highlights: varied sizes (large CV), AI: uniform sizes
459
+ if size_cv > 1.0:
460
+ score = -0.2
461
+ note = f"Varied specular highlight sizes (CV={size_cv:.2f}, natural)"
462
+ elif size_cv < 0.3 and n_features > 3:
463
+ score = 0.3
464
+ note = f"Suspiciously uniform highlight sizes (CV={size_cv:.2f})"
465
+ else:
466
+ score = 0.0
467
+ note = f"Specular analysis neutral (CV={size_cv:.2f})"
468
+
469
+ return {
470
+ "test": "Specular Reflections",
471
+ "specular_count": n_specular,
472
+ "n_highlights": n_features,
473
+ "size_cv": round(size_cv, 4),
474
+ "avg_size": round(avg_size, 2),
475
+ "score": score,
476
+ "note": note,
477
+ }
478
+
479
+
480
+ # ─── Purple Fringing Detection ──────────────────────────────────────
481
+ def analyze_purple_fringing(img: Image.Image) -> Dict[str, Any]:
482
+ """
483
+ Real cameras exhibit purple/magenta fringing at high-contrast edges
484
+ due to chromatic aberration. AI images rarely reproduce this artifact.
485
+ """
486
+ rgb = _to_rgb(img)
487
+ gray = np.mean(rgb, axis=-1)
488
+
489
+ # Find high-contrast edges
490
+ edge = np.hypot(sobel(gray, axis=0), sobel(gray, axis=1))
491
+ edge_mask = edge > np.percentile(edge, 95)
492
+
493
+ # Check for purple/magenta hue at edges
494
+ r, g, b = rgb[:, :, 0], rgb[:, :, 1], rgb[:, :, 2]
495
+
496
+ # Purple = high R, low G, high B
497
+ purple_score_map = (r + b - 2 * g) / (r + g + b + 1e-9)
498
+ edge_purple = purple_score_map[edge_mask]
499
+
500
+ if len(edge_purple) < 100:
501
+ return {
502
+ "test": "Purple Fringing",
503
+ "score": 0.0,
504
+ "note": "Insufficient high-contrast edges for fringing analysis",
505
+ }
506
+
507
+ mean_purple = float(np.mean(edge_purple))
508
+ purple_fraction = float(np.mean(edge_purple > 0.1))
509
+
510
+ if purple_fraction > 0.05:
511
+ score = -0.3
512
+ note = f"Purple fringing detected at {purple_fraction:.1%} of edges (real lens artifact)"
513
+ elif purple_fraction < 0.01 and mean_purple < 0.02:
514
+ score = 0.2
515
+ note = "No purple fringing (uncommon in real photography, possible AI)"
516
+ else:
517
+ score = 0.0
518
+ note = f"Minimal fringing (fraction={purple_fraction:.3f})"
519
+
520
+ return {
521
+ "test": "Purple Fringing",
522
+ "purple_fraction": round(purple_fraction, 4),
523
+ "mean_purple_score": round(mean_purple, 4),
524
+ "score": score,
525
+ "note": note,
526
+ }
527
+
528
+
529
  # ─── Main Agent Entry Point ─────────────────────────────────────────
530
  def run_optical_agent(img: Image.Image) -> AgentEvidence:
531
  """Run all optical physics tests and produce structured evidence."""
532
  findings = []
533
  scores = []
534
 
535
+ tests = [
536
+ analyze_chromatic_aberration,
537
+ analyze_vignetting,
538
+ analyze_dof_consistency,
539
+ analyze_bokeh,
540
+ analyze_lens_distortion,
541
+ analyze_ca_radial_pattern,
542
+ analyze_specular_reflections,
543
+ analyze_purple_fringing,
544
+ ]
545
+
546
+ for fn in tests:
547
+ try:
548
+ result = fn(img)
549
+ findings.append(result)
550
+ scores.append(result["score"])
551
+ except Exception as e:
552
+ findings.append({"test": fn.__name__, "error": str(e), "score": 0})
 
 
 
 
 
 
 
 
 
553
 
554
  if scores:
555
  avg_score = float(np.mean(scores))
 
577
  agent_name="Optical Physics Agent",
578
  violation_score=np.clip(avg_score, -1, 1),
579
  confidence=confidence,
580
+ failure_prob=max(0.0, 1.0 - len(scores) / len(tests)),
581
  rationale=rationale,
582
  sub_findings=findings,
583
  )