MohsinElis commited on
Commit
8de36d4
·
verified ·
1 Parent(s): 7eba895

feat: add predict_ensemble, conservative forensic fusion, and exif harvester

Browse files
Files changed (1) hide show
  1. api/model_server.py +187 -0
api/model_server.py CHANGED
@@ -59,6 +59,63 @@ TURBO_LUT = np.array([
59
  ], dtype=np.uint8)
60
 
61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  def check_photographic_domain(
63
  image: Image.Image,
64
  filename: Optional[str] = None,
@@ -252,6 +309,7 @@ class TamperingModelServer:
252
  self,
253
  image: Image.Image,
254
  filename: Optional[str] = None,
 
255
  ) -> Dict[str, Any]:
256
  start = time.time()
257
  tensor = self._preprocess(image)
@@ -314,6 +372,12 @@ class TamperingModelServer:
314
  blockiness = ood_info.get("compression_blockiness", 1.0)
315
  warning = ood_info["warning"]
316
 
 
 
 
 
 
 
317
  conf = round(probs[pred_idx].item(), 4)
318
  if conf < 0.60:
319
  entropy_msg = f"Low triage certainty ({conf * 100:.1f}%). Model exhibits high entropy across forensic classes."
@@ -335,5 +399,128 @@ class TamperingModelServer:
335
  "domain_status": domain_status,
336
  "is_social_media_compressed": is_sm_compressed,
337
  "compression_blockiness": blockiness,
 
 
 
 
 
 
338
  "warning": warning,
339
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  ], dtype=np.uint8)
60
 
61
 
62
+ def extract_exif_metadata(image: Image.Image) -> Dict[str, Any]:
63
+ """
64
+ Extracts camera hardware metadata and detects AI generation software tags
65
+ embedded within EXIF, XMP, or PNG text segments.
66
+ """
67
+ exif_summary = {}
68
+ generative_tags = []
69
+ has_exif = False
70
+
71
+ gen_keywords = [
72
+ "firefly", "stable diffusion", "midjourney", "dall-e", "dalle",
73
+ "comfyui", "automatic1111", "novelai", "invokeai", "photoshop",
74
+ "generative", "canva", "bing creator"
75
+ ]
76
+
77
+ try:
78
+ from PIL import ExifTags
79
+ raw_exif = image.getexif()
80
+ if raw_exif:
81
+ for tag_id, val in raw_exif.items():
82
+ tag_name = ExifTags.TAGS.get(tag_id, str(tag_id))
83
+ val_str = str(val).strip()
84
+ if val_str and len(val_str) < 120:
85
+ exif_summary[tag_name] = val_str
86
+ has_exif = True
87
+ for kw in gen_keywords:
88
+ if kw in val_str.lower() and kw not in generative_tags:
89
+ generative_tags.append(kw)
90
+ except Exception:
91
+ pass
92
+
93
+ for k, v in image.info.items():
94
+ if isinstance(v, (str, bytes)):
95
+ v_str = str(v).strip()
96
+ if len(v_str) < 150:
97
+ exif_summary[f"info_{k}"] = v_str
98
+ for kw in gen_keywords:
99
+ if kw in v_str.lower() and kw not in generative_tags:
100
+ generative_tags.append(kw)
101
+
102
+ # Focus on standard photographic capture audit parameters
103
+ key_tags = [
104
+ "Make", "Model", "Software", "DateTimeOriginal", "LensModel",
105
+ "ExposureTime", "FNumber", "ISOSpeedRatings", "FocalLength"
106
+ ]
107
+ filtered_exif = {k: exif_summary[k] for k in key_tags if k in exif_summary}
108
+ if not filtered_exif and exif_summary:
109
+ filtered_exif = {k: v for i, (k, v) in enumerate(exif_summary.items()) if i < 8}
110
+
111
+ return {
112
+ "has_exif": has_exif,
113
+ "exif_summary": filtered_exif if filtered_exif else None,
114
+ "generative_software_tag": ", ".join(generative_tags) if generative_tags else None,
115
+ "is_generative_exif": len(generative_tags) > 0,
116
+ }
117
+
118
+
119
  def check_photographic_domain(
120
  image: Image.Image,
121
  filename: Optional[str] = None,
 
309
  self,
310
  image: Image.Image,
311
  filename: Optional[str] = None,
312
+ sha256_hash: Optional[str] = None,
313
  ) -> Dict[str, Any]:
314
  start = time.time()
315
  tensor = self._preprocess(image)
 
372
  blockiness = ood_info.get("compression_blockiness", 1.0)
373
  warning = ood_info["warning"]
374
 
375
+ # EXIF Hardware & Software metadata audit
376
+ exif_res = extract_exif_metadata(image)
377
+ if exif_res["is_generative_exif"]:
378
+ gen_tag_msg = f"AI Generation Metadata Detected in EXIF ({exif_res['generative_software_tag']})"
379
+ warning = f"{warning} | {gen_tag_msg}" if warning else gen_tag_msg
380
+
381
  conf = round(probs[pred_idx].item(), 4)
382
  if conf < 0.60:
383
  entropy_msg = f"Low triage certainty ({conf * 100:.1f}%). Model exhibits high entropy across forensic classes."
 
399
  "domain_status": domain_status,
400
  "is_social_media_compressed": is_sm_compressed,
401
  "compression_blockiness": blockiness,
402
+ "consensus_status": "single_model",
403
+ "consensus_message": None,
404
+ "exif_summary": exif_res["exif_summary"],
405
+ "generative_software_tag": exif_res["generative_software_tag"],
406
+ "sha256_hash": sha256_hash,
407
+ "cached": False,
408
  "warning": warning,
409
  }
410
+
411
+
412
+ def predict_ensemble(
413
+ server_base: TamperingModelServer,
414
+ server_large: TamperingModelServer,
415
+ image: Image.Image,
416
+ filename: Optional[str] = None,
417
+ sha256_hash: Optional[str] = None,
418
+ ) -> Dict[str, Any]:
419
+ """
420
+ SOTA Conservative Forensic Fusion Ensemble combining Patch-MFFT Base (3.28M)
421
+ and Patch-MFFT Large (13.94M). Applies tampering priority gating and element-wise
422
+ maximum likelihood localization mask fusion.
423
+ """
424
+ start = time.time()
425
+
426
+ res_b = server_base.predict(image, filename=filename, sha256_hash=sha256_hash)
427
+ res_l = server_large.predict(image, filename=filename, sha256_hash=sha256_hash)
428
+
429
+ pb = res_b["class_probabilities"]
430
+ pl = res_l["class_probabilities"]
431
+
432
+ # Conservative Forensic Fusion (Tampering Priority Gating)
433
+ # If Base detected localized inpainting (p_spliced > 0.45), preserve localized tampering
434
+ if pb["spliced_tampered"] > 0.45:
435
+ prediction_label = "spliced_tampered"
436
+ conf = pb["spliced_tampered"]
437
+ # If Large or Base detected Full AI with high confidence, preserve generative evidence
438
+ elif pl["full_ai"] > 0.70 or pb["full_ai"] > 0.70:
439
+ prediction_label = "full_ai"
440
+ conf = max(pl["full_ai"], pb["full_ai"])
441
+ else:
442
+ ens_auth = 0.40 * pb["authentic"] + 0.60 * pl["authentic"]
443
+ ens_ai = 0.40 * pb["full_ai"] + 0.60 * pl["full_ai"]
444
+ ens_splice = 0.40 * pb["spliced_tampered"] + 0.60 * pl["spliced_tampered"]
445
+ candidates = [("authentic", ens_auth), ("full_ai", ens_ai), ("spliced_tampered", ens_splice)]
446
+ candidates.sort(key=lambda x: x[1], reverse=True)
447
+ prediction_label = candidates[0][0]
448
+ conf = candidates[0][1]
449
+
450
+ # Consensus Status
451
+ if res_b["prediction"] == res_l["prediction"]:
452
+ consensus_status = "verified_consensus"
453
+ consensus_message = f"Multi-Model Consensus: Both Base ({res_b['confidence']*100:.1f}%) and Large ({res_l['confidence']*100:.1f}%) agree unanimously on '{prediction_label}'."
454
+ else:
455
+ consensus_status = "disputed"
456
+ consensus_message = (
457
+ f"Disputed Forensic Case: Base ({res_b['prediction']}, {res_b['confidence']*100:.1f}%) "
458
+ f"vs Large ({res_l['prediction']}, {res_l['confidence']*100:.1f}%). "
459
+ f"Conservative forensic fusion prioritized the higher tampering indicator."
460
+ )
461
+
462
+ # Element-Wise Maximum Likelihood Heatmap Fusion
463
+ t_b = server_base._preprocess(image)
464
+ t_l = server_large._preprocess(image)
465
+ with torch.no_grad():
466
+ _, mask_b = server_base.model(t_b, return_mask=True)
467
+ _, mask_l = server_large.model(t_l, return_mask=True)
468
+ m_b = torch.sigmoid(mask_b)[0, 0].cpu().numpy()
469
+ m_l = torch.sigmoid(mask_l)[0, 0].cpu().numpy()
470
+ fused_mask = np.maximum(m_b, m_l)
471
+
472
+ peak_score = round(float(fused_mask.max()), 4)
473
+ coverage_ratio = round(float(np.mean(fused_mask > 0.50)), 4)
474
+
475
+ lut_idx = np.clip(fused_mask * 255.0, 0, 255).astype(np.int32)
476
+ heatmap_rgb = TURBO_LUT[lut_idx]
477
+ mask_pil = Image.fromarray(heatmap_rgb, mode="RGB")
478
+ buf = io.BytesIO()
479
+ mask_pil.save(buf, format="PNG")
480
+ mask_b64 = base64.b64encode(buf.getvalue()).decode("utf-8")
481
+
482
+ overlay_rgba = np.zeros((server_base.img_size, server_base.img_size, 4), dtype=np.uint8)
483
+ overlay_rgba[:, :, :3] = heatmap_rgb
484
+ alpha_map = np.clip((fused_mask - 0.12) / 0.25 * 210.0, 0, 210).astype(np.uint8)
485
+ overlay_rgba[:, :, 3] = alpha_map
486
+ overlay_pil = Image.fromarray(overlay_rgba, mode="RGBA")
487
+ buf_o = io.BytesIO()
488
+ overlay_pil.save(buf_o, format="PNG")
489
+ overlay_b64 = base64.b64encode(buf_o.getvalue()).decode("utf-8")
490
+
491
+ class_probs = {
492
+ "authentic": round(0.40 * pb["authentic"] + 0.60 * pl["authentic"], 4),
493
+ "full_ai": round(0.40 * pb["full_ai"] + 0.60 * pl["full_ai"], 4),
494
+ "spliced_tampered": round(0.40 * pb["spliced_tampered"] + 0.60 * pl["spliced_tampered"], 4),
495
+ }
496
+
497
+ warning = res_b.get("warning") or res_l.get("warning")
498
+ if consensus_status == "disputed":
499
+ warning = f"{warning} | {consensus_message}" if warning else consensus_message
500
+
501
+ elapsed_ms = (time.time() - start) * 1000.0
502
+
503
+ return {
504
+ "prediction": prediction_label,
505
+ "confidence": round(float(conf), 4),
506
+ "tampering_detected": prediction_label in ["full_ai", "spliced_tampered"],
507
+ "class_probabilities": class_probs,
508
+ "tampering_heatmap_b64": mask_b64,
509
+ "tampering_overlay_b64": overlay_b64,
510
+ "tampering_peak_score": peak_score,
511
+ "tampering_coverage_ratio": coverage_ratio,
512
+ "frequency_band_contributions": res_l["frequency_band_contributions"],
513
+ "processing_time_ms": round(elapsed_ms, 2),
514
+ "model_variant": "ensemble",
515
+ "is_non_photographic_graphic": res_b["is_non_photographic_graphic"],
516
+ "domain_status": res_b["domain_status"],
517
+ "is_social_media_compressed": res_b["is_social_media_compressed"],
518
+ "compression_blockiness": res_b["compression_blockiness"],
519
+ "consensus_status": consensus_status,
520
+ "consensus_message": consensus_message,
521
+ "exif_summary": res_b.get("exif_summary"),
522
+ "generative_software_tag": res_b.get("generative_software_tag"),
523
+ "sha256_hash": sha256_hash,
524
+ "cached": False,
525
+ "warning": warning,
526
+ }