deepguard / metadata_extractor.py
BADRzs's picture
Upload folder using huggingface_hub
9dc05f8 verified
Raw History Blame Contribute Delete
8.52 kB
"""
Stage 2.5 - Metadata Forensic Analysis.
Hardened version: downweighted to minor corroborating signal only.
Missing metadata is NOT evidence of manipulation.
"""
import io
from typing import Dict, Any, List, Optional
from PIL import Image
import piexif
def extract_metadata(image_bytes: bytes) -> Dict[str, Any]:
"""
Extract EXIF metadata and analyze for forensic signals.
IMPORTANT: Metadata is a MINOR CORROBORATING SIGNAL ONLY.
- Absence of EXIF is common (social media, screenshots, editing)
- Missing metadata does NOT push verdict toward FAKE
- Software signatures are informative but not definitive
"""
try:
image = Image.open(io.BytesIO(image_bytes))
image.load() # Force load to access EXIF
except Exception as e:
return _empty_metadata(f"Failed to open image: {e}")
# Extract raw EXIF
exif_data = _extract_exif(image)
# Analyze signals
has_exif = len(exif_data) > 0
camera_model = _get_camera_model(exif_data)
software = _get_software(exif_data)
gps_info = _get_gps(exif_data)
dimensions = f"{image.width}x{image.height}"
is_standard_square = _is_standard_square(image.width, image.height)
# Build suspicious flags (INFORMATIVE ONLY - weight: minor)
suspicious_flags = []
if not has_exif:
suspicious_flags.append({
"flag": "NO_EXIF",
"description": "No EXIF metadata found",
"weight": "minor",
"note": "Common for social media uploads, screenshots, and edited images. NOT evidence of AI generation."
})
else:
# Check for AI software signatures
ai_software = _check_ai_software(software)
if ai_software:
suspicious_flags.append({
"flag": "AI_SOFTWARE",
"description": f"Software signature suggests AI tool: {ai_software}",
"weight": "moderate",
"note": "Software tags can be stripped or spoofed. Corroborating only."
})
# Check for editing software
editing_software = _check_editing_software(software)
if editing_software:
suspicious_flags.append({
"flag": "EDITING_SOFTWARE",
"description": f"Edited with: {editing_software}",
"weight": "minor",
"note": "Legitimate editing is common. Not evidence of fakery."
})
# Standard generator dimensions
if is_standard_square:
suspicious_flags.append({
"flag": "STANDARD_DIMENSIONS",
"description": f"Dimensions {dimensions} match common AI generator defaults",
"weight": "minor",
"note": "Many real photos are also square or standard sizes. Weak signal alone."
})
# Missing camera hardware info (when EXIF exists but no camera data)
if not camera_model and has_exif:
suspicious_flags.append({
"flag": "NO_CAMERA_INFO",
"description": "EXIF present but no camera make/model recorded",
"weight": "minor",
"note": "Some cameras/devices don't write full EXIF. Common in edited images."
})
# Summary
if not has_exif:
summary = "No EXIF metadata available. This is normal for images from social media, messaging apps, or screenshots. Metadata absence is not evidence of manipulation."
elif ai_software:
summary = f"Metadata indicates creation with {ai_software}. This is a moderate corroborating signal for AI generation."
elif editing_software:
summary = f"Image edited with {editing_software}. Legitimate editing is common; this does not indicate fakery."
elif is_standard_square:
summary = f"Image has standard dimensions ({dimensions}) but camera metadata is present. Weak signal."
else:
summary = f"Camera metadata present: {camera_model or 'unknown device'}. Consistent with authentic capture."
return {
"has_exif": has_exif,
"camera_model": camera_model,
"software": software,
"gps_present": gps_info is not None,
"dimensions": dimensions,
"is_standard_square": is_standard_square,
"suspicious_flags": suspicious_flags,
"summary": summary,
"weight": "minor", # EXPLICIT: metadata is minor corroborating signal only
"metadata_absent": not has_exif, # Explicit flag for downstream use
}
def _empty_metadata(reason: str) -> Dict[str, Any]:
return {
"has_exif": False,
"camera_model": None,
"software": None,
"gps_present": False,
"dimensions": "unknown",
"is_standard_square": False,
"suspicious_flags": [{
"flag": "EXTRACTION_FAILED",
"description": reason,
"weight": "none",
"note": "Metadata extraction failed; no signal available."
}],
"summary": f"Metadata extraction failed: {reason}",
"weight": "minor",
"metadata_absent": True,
}
def _extract_exif(image: Image.Image) -> Dict:
"""Extract EXIF data using piexif for robust parsing."""
exif = {}
try:
if "exif" in image.info:
exif_dict = piexif.load(image.info["exif"])
for ifd_name in ["0th", "Exif", "GPS", "1st", "Interop"]:
if ifd_name in exif_dict:
for tag, value in exif_dict[ifd_name].items():
try:
exif[tag] = value
except:
pass
except Exception:
pass
return exif
def _get_camera_model(exif: Dict) -> Optional[str]:
"""Extract camera make/model from EXIF."""
make = exif.get(271) or exif.get(272) # Make/Model
model = exif.get(272) or exif.get(271)
if make and model:
return f"{make} {model}".strip()
return make or model
def _get_software(exif: Dict) -> Optional[str]:
"""Extract software tag from EXIF."""
# Tag 305 = Software
software = exif.get(305)
if isinstance(software, bytes):
software = software.decode('utf-8', errors='ignore')
return software
def _get_gps(exif: Dict) -> Optional[Dict]:
"""Extract GPS info if present."""
gps_tags = [k for k in exif.keys() if 'gps' in str(k).lower() or k in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36]]
if gps_tags:
return {"tags_present": len(gps_tags)}
return None
def _is_standard_square(width: int, height: int) -> bool:
"""Check if dimensions match common AI generator defaults."""
standard_sizes = {
(512, 512), (768, 768), (1024, 1024), (2048, 2048),
(512, 768), (768, 512), (768, 1024), (1024, 768),
(896, 896), (1152, 1152),
}
return (width, height) in standard_sizes or (height, width) in standard_sizes
def _check_ai_software(software: Optional[str]) -> Optional[str]:
"""Check for AI generation software signatures."""
if not software:
return None
software_lower = software.lower()
ai_signatures = {
"midjourney": "Midjourney",
"stable diffusion": "Stable Diffusion",
"stablediffusion": "Stable Diffusion",
"dall-e": "DALL-E",
"dalle": "DALL-E",
"novelai": "NovelAI",
"comfyui": "ComfyUI",
"automatic1111": "Automatic1111",
"invokeai": "InvokeAI",
"krita-ai": "Krita AI",
"photoshop ai": "Photoshop AI",
"firefly": "Adobe Firefly",
"flux": "Flux",
}
for key, name in ai_signatures.items():
if key in software_lower:
return name
return None
def _check_editing_software(software: Optional[str]) -> Optional[str]:
"""Check for traditional editing software."""
if not software:
return None
software_lower = software.lower()
editing_signatures = {
"photoshop": "Adobe Photoshop",
"lightroom": "Adobe Lightroom",
"gimp": "GIMP",
"affinity": "Affinity Photo",
"paint.net": "Paint.NET",
"snapseed": "Snapseed",
"vsco": "VSCO",
"lightroom": "Lightroom",
}
for key, name in editing_signatures.items():
if key in software_lower:
return name
return None