docforensics / fusion /decision.py
Suryakarthik-1
Deploy DocForensics to Hugging Face Spaces
70520f0
Raw
History Blame Contribute Delete
3.27 kB
import cv2
import numpy as np
from core.config import FUSION_WEIGHTS, TAMPER_THRESHOLD
from core.types import Detection, Verdict
LABELS = {
'ela': 'ELA anomaly (pixel error inconsistency)',
'noise': 'Noise fingerprint mismatch',
'copy_move': 'Cloned/duplicated region detected',
'double_jpeg': 'Double compression artifact',
'font': 'Font or text baseline inconsistency',
'metadata': 'Suspicious metadata',
'ai_generated': 'AI-generated image signature',
'model': 'CNN tampering localization',
}
def build_evidence(detections: list[Detection]) -> list[str]:
messages = []
for det in detections:
if det.score > 0.3:
base = LABELS.get(det.detector_name, det.detector_name)
flags = det.details.get('flags', [])
if flags:
messages.append(f'{base}: {"; ".join(flags)}')
else:
messages.append(f'{base} (score={det.score:.0%})')
return messages or ['No significant tampering signals detected']
def fuse(detections: list[Detection], model_confidence: float = 0.0,
model_heatmap: np.ndarray | None = None,
weights: dict | None = None) -> Verdict:
w = weights or FUSION_WEIGHTS
scores = {d.detector_name: d.score for d in detections}
scores['model'] = model_confidence
# ── Base: weighted average of every signal ──
weighted = sum(w.get(name, 0.0) * s for name, s in scores.items())
# ── Strong-signal boost: a single confident specialist shouldn't be diluted
# to nothing by the calmer detectors. Only the RELIABLE detectors get to
# drive this (copy_move / double_jpeg are too noisy on documents).
strongest = max(scores.get('ai_generated', 0.0),
scores.get('model', 0.0),
scores.get('ela', 0.0))
fused_score = float(np.clip(max(weighted, 0.55 * strongest + 0.45 * weighted), 0, 1))
# ── Decide a human-readable label ──
ai = scores.get('ai_generated', 0.0)
is_flagged = fused_score >= TAMPER_THRESHOLD
if is_flagged and ai >= 0.6 and ai >= strongest - 1e-6:
label = 'AI-GENERATED'
elif is_flagged:
label = 'TAMPERED'
else:
label = 'AUTHENTIC'
# ── Merge heatmaps (weighted) ──
heatmaps = [(d.heatmap, w.get(d.detector_name, 0.0))
for d in detections if d.heatmap is not None]
if model_heatmap is not None:
heatmaps.append((model_heatmap, w.get('model', 0.2)))
if heatmaps:
ref_h, ref_w = heatmaps[0][0].shape
merged = np.zeros((ref_h, ref_w), dtype=np.float32)
total_w = 0.0
for hmap, hw in heatmaps:
resized = cv2.resize(hmap.astype(np.float32), (ref_w, ref_h))
merged += resized * hw
total_w += hw
if total_w > 0:
merged /= total_w
merged = np.clip(np.nan_to_num(merged, nan=0.0), 0, 1)
else:
merged = np.zeros((256, 256), dtype=np.float32)
return Verdict(
is_tampered=is_flagged,
confidence=fused_score,
fused_heatmap=merged,
evidence=build_evidence(detections),
per_detector=detections,
label=label,
)