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
| Property | Value |
|---|---|
| Architecture | ImprovedDetector (multi-branch CNN) |
| Task | Binary image classification (FAKE / REAL) |
| Input | RGB image, resized to 32 Γ 32, normalized to [-1, 1] |
| Output | (logit, gates) β scalar logit + 3-weight gate vector |
| Decision threshold | sigmoid(logit) > 0.5 β FAKE |
| Format | TorchScript (.pt) |
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)
β
logit
Usage
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):
| Output | Shape | Description |
|---|---|---|
logit |
(1,) |
Raw pre-sigmoid score. Pass through sigmoid() to get fake_prob. |
gates |
(1, 3) |
Softmax-normalized branch weights: [spatial, frequency, noise] |
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
| File | Description |
|---|---|
detector_scripted.pt |
TorchScript model β load with torch.jit.load() |
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).