AI‑generated portraits have reached a level of realism that can fool even attentive human observers. For developers building media‑processing pipelines, content moderation systems, or forensic tools, knowing how to separate synthetic faces from genuine photographs is increasingly important. Below is a practical guide that walks through the tell‑tale signs of AI‑crafted faces, the underlying reasons they appear, and a ready‑to‑run code sketch you can adapt to your own detector.
Why AI faces are hard to spot
Modern generative models (StyleGAN2/3, Diffusion‑based generators, or large‑scale transformer‑based image syntheses) learn the statistical distribution of millions of real faces. They reproduce:
- Global geometry – pose, lighting, and facial proportions that match the training data.
- Local texture – skin pores, fine wrinkles, and hair strands that are sampled from real‑world examples.
- Semantic coherence – eyes, nose, and mouth are correctly aligned because the generator conditions on facial landmarks.
Because the generator optimizes for pixel‑level likelihood, many traditional artifacts (blurred eyes, mismatched teeth, warped background) have been largely eliminated. However, the generation process still leaves subtle statistical traces that differ from the distribution of authentic camera‑captured images.
Common forensic cues
| Cue | What to look for | Why it appears in AI faces |
|---|---|---|
| Frequency‑domain inconsistencies | Examine the amplitude spectrum; AI images often show excess energy at mid‑high frequencies due to up‑sampling artifacts. | Generators typically use transpose convolutions or pixel‑shuffle layers that create checkerboard‑like patterns in the Fourier domain. |
| Eye reflection symmetry | Real eyes exhibit complex, view‑dependent specular highlights; synthetic eyes may have perfectly mirrored or missing reflections. | The generator may not model the precise physics of corneal reflectance, leading to overly uniform highlights. |
| Texture granularity | Compute local binary patterns (LBP) or Gabor filter responses; synthetic skin can be overly smooth or show periodic patterns. | Training on limited texture patches can cause the model to repeat learned micro‑structures. |
| 3D shape cues | Estimate depth from shading or use a pretrained face‑shape regressor; inconsistent depth maps reveal warping. | The generator optimizes 2D appearance without enforcing true 3D consistency. |
| Biological signals | Eye‑blink rate, pupil dilation, or subtle blood‑flow changes (via remote photoplethysmography) are often static in generated video. | Static frames lack the physiological dynamics present in real capture. |
No single cue is foolproof; the most robust detectors combine several of them into a learned classifier.
Building a lightweight detector
Below is a minimal PyTorch‑based pipeline that extracts frequency‑domain features and feeds them to a small convolutional network. It’s intentionally simple so you can replace the backbone with a more powerful model (e.g., Xception, EfficientNet) if you need higher accuracy.
python
# detector.py
import torch
import torch.nn as nn
import torchvision.transforms as T
from PIL import Image
import numpy as np
import cv2
# ---- 1. Pre‑processing -------------------------------------------------
def load_and_preprocess(path: str, size: int = 224) -> torch.Tensor:
img = Image.open(path).convert("RGB")
img = img.resize((size, size))
# Convert to numpy for FFT
img_np = np.array(img).astype(np.float32) / 255.0
# Apply FFT on each channel, shift zero freq to center, take log magnitude
fft_img = []
for c in range(3):
f = np.fft.fft2(img_np[:, :, c])
fshift = np.fft.fftshift(f)
magnitude = np.log(np.abs(fshift) + 1)
fft_img.append(magnitude)
fft_img = np.stack(fft_img, axis=-1) # (H, W, 3)
# Normalize per channel (mean/std from ImageNet works OK)
transform = T.Compose([
T.ToTensor(),
T.Normalize(mean=[0.485, 0.456, 0.406],
std =[0.229, 0.224, 0.225])
])
return transform(fft_img).unsqueeze(0) # (1, 3, H, W)
# ---- 2. Simple classifier ------------------------------------------------
class FreqNet(nn.Module):
def __init__(self):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 32, 3, stride=2, padding=1), nn.ReLU(inplace=True),
nn.Conv2d(32, 64, 3, stride=2, padding=1), nn.ReLU(inplace=True),
nn.Conv2d(64, 128, 3, stride=2, padding=1), nn.ReLU(inplace=True),
nn.AdaptiveAvgPool2d(1)
)
self.classifier = nn.Linear(128, 2) # 0 = real, 1 = AI‑generated
def forward(self, x):
x = self.features(x)
x =
Top comments (0)