Medsa/ai-image-authenticity-detector
TruthLens — AI Image Authenticity Detector
Classifies images as AI-generated (FAKE) or authentic (REAL) using a multi-branch convolutional neural network that analyses spatial, frequency, and noise-level features simultaneously.
The model is exported as a self-contained TorchScript file — no architecture definition is required to run it.
Model Details
Architecture
The model is built around three independent branches that each look for a different class of evidence, feeding into a shared gated fusion layer.
Spatial Branch
Processes raw pixel data through a stem convolution followed by three residual blocks. Looks for visual artifacts, unnatural textures, and lighting inconsistencies that generative models leave behind.
Frequency Branch
Operates in the frequency domain by computing the FFT magnitude, FFT phase, and a DCT approximation of the input — each processed by its own convolutional block. Detects the spectral fingerprints that generative models produce but real camera sensors don't.
Noise Branch
Applies a fixed high-pass filter to isolate the high-frequency noise layer of the image, then analyses it with a small CNN. Real photographs carry consistent sensor noise; AI images tend to have suspiciously smooth or periodically structured noise patterns.
Gated Fusion
All three branch embeddings are concatenated and passed through a two-layer gate network with a Softmax output, producing three scalar weights that sum to 1.0. These weights are used to compute a weighted sum of projected branch features before the final classifier. The gate weights are returned alongside the logit, giving per-image explainability.
Input image (3 × 32 × 32)
│
┌────┴────────────────────┐
│ │ │
Spatial Frequency Noise
(512-d) (512-d) (256-d)
│ │ │
└────────────┴────────────┘
│
Gated Fusion
(learned weights → 512-d)
│
Classifier
(512 → 256 → 64 → 1)
│
logitUsage
Direct inference (Python)
import torch
from PIL import Image
import torchvision.transforms as T
from huggingface_hub import hf_hub_download
# Load model
path = hf_hub_download("Medsa/ai-image-authenticity-detector", "detector_scripted.pt")
model = torch.jit.load(path, map_location="cpu")
model.eval()
# Preprocess
transform = T.Compose([
T.Resize((32, 32)),
T.ToTensor(),
T.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]),
])
img = Image.open("photo.jpg").convert("RGB")
tensor = transform(img).unsqueeze(0) # (1, 3, 32, 32)
# Inference
with torch.no_grad():
logit, gates = model(tensor)
fake_prob = torch.sigmoid(logit).item()
verdict = "FAKE" if fake_prob > 0.5 else "REAL"
print(f"Verdict : {verdict}")
print(f"Fake prob : {fake_prob:.4f}")
print(f"Real prob : {1 - fake_prob:.4f}")
print(f"Gates : spatial={gates[0,0]:.3f} freq={gates[0,1]:.3f} noise={gates[0,2]:.3f}")Via the TruthLens API
The model is also served by the TruthLens web application:
curl -X POST http://localhost:5000/predict \
-F "files=@photo.jpg"Output Format
The model returns a tuple (logit, gates):
fake_prob = torch.sigmoid(logit).item() # probability image is AI-generated
real_prob = 1 - fake_prob # probability image is authentic
spatial_weight = gates[0, 0].item()
frequency_weight = gates[0, 1].item()
noise_weight = gates[0, 2].item()Gate weights indicate which branch was most influential for a given image. They always sum to 1.0.
Preprocessing
The model expects images preprocessed exactly as follows:
transforms.Compose([
transforms.Resize((32, 32)),
transforms.ToTensor(), # scales to [0, 1]
transforms.Normalize([0.5, 0.5, 0.5], # shifts to [-1, 1]
[0.5, 0.5, 0.5]),
])Inputs that are not normalized to [-1, 1] will produce unreliable results.
Files
Training
Dataset: CIFAKE — a balanced dataset of real CIFAR-10 images and AI-generated counterparts at 32 × 32 resolution.
Optimizer: AdamW (lr=3e-4, weight_decay=1e-4) with cosine annealing down to 1e-6 over 50 epochs, batch size 64, gradient clipping at norm 1.0. Early stopping with patience 5.
Data augmentation (train only): random horizontal/vertical flips, color jitter (brightness, contrast, saturation), and small random rotation (±10°).
Loss function: standard BCE plus a gate diversity penalty to prevent branch collapse — an issue where the gate learns to ignore one or more branches entirely and routes everything through a single one:
L = BCE(logit, label) − λ · H(gates)where H(gates) is the mean gate entropy across the batch. Maximising entropy keeps all three branches actively contributing. The weight λ starts at 0.3 and decays by 0.9 per epoch (floored at 0.05), so the penalty is strong early on to force branch diversity, then relaxes as the gate is allowed to specialise.
Limitations
- Input resolution is fixed at 32 × 32. Fine spatial detail is lost; the model relies on statistical patterns rather than high-frequency pixel-level artifacts.
- Performance may degrade on image types significantly underrepresented in training data (e.g. medical images, satellite imagery).
- Like all detectors, it can be fooled by adversarial post-processing (JPEG compression, noise injection, resizing pipelines).
