Spaces:
Sleeping
Sleeping
File size: 7,187 Bytes
b0c6daf | 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | """
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",
}
|