face-intel / providers /forensics /image_integrity.py
Marwan
Restructure + add reverse face search (PimEyes-style)
f5eeb1c
Raw
History Blame Contribute Delete
4.01 kB
"""
Image integrity forensics provider.
Delegates SHA-256 hashing to cores.vision.hashing and Pillow parsing to
cores.metadata.extractor — no duplicated hashing or Pillow-open logic.
"""
from __future__ import annotations
from typing import Any
import numpy as np
from config.settings import Settings, settings as _default_settings
from cores.metadata import extract_all
from cores.vision import sha256_bytes
from pipeline.feature_extraction import PipelineOutput
from providers.base import BaseProvider, ProviderCapability
class ImageIntegrityProvider(BaseProvider):
name = "image_integrity"
capability = ProviderCapability.FORENSICS
def __init__(self, settings: Settings | None = None) -> None:
super().__init__(settings=settings or _default_settings)
def is_available(self) -> bool:
return True
def _run(self, pipeline_output: PipelineOutput) -> tuple[dict, dict]:
original = pipeline_output.original_bytes
img = pipeline_output.image
h, w = img.shape[:2]
checks: dict[str, Any] = {}
integrity_issues: list[str] = []
# Check 1: Pillow decode (via cores.metadata to avoid double-open)
meta = extract_all(original) if original else {"error": "no bytes", "format": None}
pillow_ok = meta.get("error") is None
pillow_format = meta.get("format")
if not pillow_ok:
integrity_issues.append(f"Pillow decode: {meta.get('error')}")
checks["pillow_decode"] = pillow_ok
checks["pillow_format"] = pillow_format
# Check 2: dimensions sanity
checks["opencv_dimensions"] = {"width": w, "height": h}
# Check 3: file size sanity
size_bytes = len(original) if original else 0
pixels = w * h
if pixels > 0 and size_bytes > 0:
bpp = size_bytes / pixels
if bpp < 0.1:
integrity_issues.append(f"Unusually compressed: {bpp:.3f} bytes/pixel")
elif bpp > 50:
integrity_issues.append(f"Unusually large: {bpp:.2f} bytes/pixel (possible embedded payload)")
checks["size_bytes"] = size_bytes
checks["bytes_per_pixel"] = round(size_bytes / pixels, 4) if pixels else 0
# Check 4: SHA-256 (via cores.vision.hashing)
sha256 = sha256_bytes(original) if original else None
checks["sha256"] = sha256
# Check 5: JPEG APP markers count
app_markers = _count_jpeg_app_markers(original)
checks["jpeg_app_markers"] = app_markers
if app_markers > 5:
integrity_issues.append(f"High APP markers ({app_markers}) — possible embedded data")
integrity_score = max(0.0, 1.0 - 0.15 * len(integrity_issues))
raw = {
"checks": checks,
"integrity_issues": integrity_issues,
"integrity_score": integrity_score,
}
normalized = {
"integrity_score": round(integrity_score, 4),
"manipulation_indicators": integrity_issues,
"details": {
"sha256": sha256,
"format": pillow_format,
"size_bytes": size_bytes,
"bytes_per_pixel": checks["bytes_per_pixel"],
"app_markers": app_markers,
},
}
return raw, normalized
def _count_jpeg_app_markers(data: bytes | None) -> int:
"""Count JPEG APPn markers — quick steganography heuristic."""
if not data or data[:2] != b"\xff\xd8":
return 0
count = 0
i = 2
while i < len(data) - 1:
if data[i] != 0xFF:
break
marker = data[i + 1]
if marker in (0xD8, 0xD9):
i += 2
continue
if marker == 0x00 or 0xD0 <= marker <= 0xD7:
i += 2
continue
if i + 4 > len(data):
break
seg_len = (data[i + 2] << 8) | data[i + 3]
if 0xE0 <= marker <= 0xEF:
count += 1
i += 2 + seg_len
return count