deepshield-ai / frequency_analyzer.py
pseudonomasorg's picture
Upload folder using huggingface_hub
b0c6daf verified
Raw
History Blame Contribute Delete
7.19 kB
"""
Frequency Domain Analyzer β€” DCT/FFT spectral fingerprinting for deepfake detection.
GANs and diffusion models leave characteristic artifacts in the frequency domain
that are invisible in pixel space. This module extracts those signatures and
provides a secondary signal alongside the primary CNN classifier.
Key insight: Real faces have smooth, natural frequency roll-off. GAN-generated
faces exhibit abnormal high-frequency peaks (checkerboard artifacts from
transposed convolutions) or unnatural spectral uniformity (from diffusion
post-processing).
Usage (from main.py):
from frequency_analyzer import analyze_frequency
result = analyze_frequency(face_pil_image)
# result = {
# "spectral_score": 0.85, # 0=fake-like spectrum, 1=natural spectrum
# "high_freq_energy": 0.032, # normalized high-freq energy ratio
# "spectral_anomaly": False, # True if spectrum looks unnatural
# "spectral_details": "natural", # human-readable summary
# }
"""
import numpy as np
from PIL import Image
# ──────────────────────────────────────
# DCT-based frequency analysis
# ──────────────────────────────────────
def _dct_2d(block: np.ndarray) -> np.ndarray:
"""Compute 2D DCT using scipy if available, else numpy approximation."""
try:
from scipy.fft import dctn
return dctn(block, type=2, norm='ortho')
except ImportError:
# Fallback: use FFT-based approximation
from numpy.fft import fft2, fftshift
return np.abs(fftshift(fft2(block)))
def _compute_azimuthal_average(spectrum_2d: np.ndarray) -> np.ndarray:
"""
Compute the azimuthal (radial) average of a 2D power spectrum.
This collapses the 2D spectrum into a 1D curve showing energy vs frequency.
"""
h, w = spectrum_2d.shape
cy, cx = h // 2, w // 2
# Build distance matrix from center
Y, X = np.ogrid[:h, :w]
dist = np.sqrt((X - cx)**2 + (Y - cy)**2).astype(int)
max_radius = min(cy, cx)
radial_profile = np.zeros(max_radius)
count = np.zeros(max_radius)
for r in range(max_radius):
mask = dist == r
radial_profile[r] = spectrum_2d[mask].sum()
count[r] = mask.sum()
# Avoid division by zero
count[count == 0] = 1
return radial_profile / count
def analyze_frequency(face_pil: Image.Image) -> dict:
"""
Analyze the frequency domain characteristics of a face crop.
Args:
face_pil: PIL Image of the cropped face (any size, will be resized)
Returns:
dict with spectral_score, high_freq_energy, spectral_anomaly, spectral_details
"""
try:
# Convert to grayscale and resize to standard analysis size
gray = face_pil.convert('L').resize((256, 256), Image.LANCZOS)
pixels = np.array(gray, dtype=np.float64) / 255.0
# ── Step 1: Compute 2D DCT/FFT spectrum ──
spectrum = _dct_2d(pixels)
power_spectrum = np.abs(spectrum) ** 2
# ── Step 2: Compute radial energy profile ──
radial = _compute_azimuthal_average(power_spectrum)
if len(radial) < 10:
return _default_result()
# Normalize
total_energy = radial.sum()
if total_energy < 1e-10:
return _default_result()
radial_norm = radial / total_energy
# ── Step 3: Analyze frequency distribution ──
n = len(radial_norm)
low_band = radial_norm[:n // 4].sum() # 0-25% of spectrum
mid_band = radial_norm[n // 4:n // 2].sum() # 25-50%
high_band = radial_norm[n // 2:].sum() # 50-100% (high frequencies)
# ── Step 4: Compute spectral slope ──
# Natural images follow a 1/f^Ξ± power law with Ξ± β‰ˆ 2.0
# GAN images deviate from this β€” either flatter (Ξ± < 1.5) or steeper
log_freqs = np.log(np.arange(1, n) + 1)
log_power = np.log(radial_norm[1:] + 1e-12)
# Linear regression in log-log space to estimate spectral slope
if len(log_freqs) > 2:
coeffs = np.polyfit(log_freqs, log_power, 1)
spectral_slope = abs(coeffs[0])
else:
spectral_slope = 2.0 # default natural slope
# ── Step 5: Detect GAN checkerboard artifacts ──
# Look for periodic peaks in mid-high frequencies
if n > 20:
mid_high = radial_norm[n // 3:]
if len(mid_high) > 3:
# Compute local variance (peak detection proxy)
local_var = np.var(mid_high)
peak_ratio = np.max(mid_high) / (np.mean(mid_high) + 1e-12)
else:
local_var = 0.0
peak_ratio = 1.0
else:
local_var = 0.0
peak_ratio = 1.0
# ── Step 6: Score computation ──
# Factors that indicate FAKE-like spectrum:
# - Unusually high "high_band" energy (GAN artifacts)
# - Spectral slope far from natural (Ξ± β‰ˆ 2.0)
# - High peak_ratio (periodic artifacts)
# Slope deviation from natural (2.0)
slope_score = max(0, 1.0 - abs(spectral_slope - 2.0) / 2.0)
# High-frequency energy penalty (real faces have low high-freq energy)
hf_score = max(0, 1.0 - high_band * 10) # penalize if high_band > 0.1
# Peak ratio penalty (periodic artifacts)
peak_score = max(0, 1.0 - (peak_ratio - 3.0) / 10.0) if peak_ratio > 3.0 else 1.0
# Weighted combination
spectral_score = round(
0.40 * slope_score + 0.35 * hf_score + 0.25 * peak_score,
3
)
spectral_score = max(0.0, min(1.0, spectral_score))
# ── Step 7: Determine anomaly and details ──
anomaly = spectral_score < 0.5
if spectral_score >= 0.75:
details = "natural"
elif spectral_score >= 0.50:
details = "minor_artifacts"
elif spectral_score >= 0.30:
details = "suspicious_spectrum"
else:
details = "gan_signature_detected"
return {
"spectral_score": spectral_score,
"high_freq_energy": round(float(high_band), 4),
"spectral_anomaly": anomaly,
"spectral_details": details,
}
except Exception as e:
print(f"[FREQUENCY] Analysis error: {e}")
return _default_result()
def _default_result() -> dict:
"""Returns safe default when analysis fails or input is insufficient."""
return {
"spectral_score": 0.5,
"high_freq_energy": 0.0,
"spectral_anomaly": False,
"spectral_details": "unavailable",
}