Spaces:
Sleeping
Sleeping
| """ | |
| 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", | |
| } | |