Inspiration
Detecting AI-generated images is easy in a lab and hard on a platform. By the time an image reaches a feed it has been re-compressed, cropped to a thumbnail, auto-enhanced, and screenshotted — and most published detectors quietly fall apart under exactly that treatment.
So before writing any model code, we measured our own training data. What we found reframed the whole project: the hard part isn't the transforms, it's the shortcuts hiding in the dataset.
Two of them, both measured rather than assumed:
- Synthetic images are smoother. The FLUX-generated fakes carry 41% less high-frequency energy than real photos. That's a cheap, tempting signal — and Gaussian blur destroys it, which is precisely what the evaluation tests.
- Compression history leaks the label outright. The real images are Flickr-sourced and already JPEG-compressed; the fakes are pristine PNGs. A single hand-written 8×8 blockiness statistic — no machine learning at all — separates the two classes at AUROC 0.830.
A model trained naively on that data would report ~99% validation accuracy having learned nothing but "JPEG artifacts ⇒ real", then invert the instant someone compressed a fake. VerifAI exists to not be that model.
What it does
VerifAI scores any directory of images for the likelihood each one is
AI-generated, outputting a calibrated confidence in [0,1] as JSON
(image_path, pred).
It is built for messy real-world input: a corrupt file returns 0.5 with a warning instead of crashing the run, and it handles EXIF rotation, greyscale and CMYK, alpha channels, animated formats and truncated files. It runs on CPU if no GPU is available.
An interactive demo lets you upload an image and apply any of 16 judged transforms — JPEG q30–90, blur σ0.5–2.0, resize 0.25–0.5×, noise σ0.02–0.10, ±20% colour jitter, 80% centre crop — and watch the prediction stay stable.
How we built it
One model, roughly a thousand trained parameters. A frozen CLIP ViT-L/14 vision tower (303M params, well under the 2B limit and asserted in code) maps each image to a 1024-d feature; a linear SVM draws a single hyperplane through that space.
The backbone never receives a gradient, and that is the central design decision rather than a shortcut. Following Ojha et al. (CVPR 2023) and Cozzolino et al. (CVPRW 2024): a fine-tuned network becomes asymmetrically tuned to its training generator and dumps anything unfamiliar into the "real" class. A frozen, general-purpose feature space never learned that asymmetry, so real and fake stay separable across generators. Our train→test gap is FLUX → DALL·E 3, so that property is everything.
Augmentation is the engineering, not a garnish. Every training image — both classes identically — is randomly JPEG re-encoded (QF 30–95), resized, cropped, jittered, noised and sometimes blurred, composed together rather than applied in isolation. This is what kills the compression-history shortcut: our blockiness probe drops from AUROC 0.830 to 0.498, pure chance. We verified that before training, so the robustness is designed in rather than hoped for.
Evaluation was pre-registered. We fixed a 16-cell transform grid, four metrics (AUROC; accuracy at a threshold calibrated once on clean validation and never re-tuned; TPR@1%FPR; TPR@5%FPR) and numeric targets in advance, anchored to published results — then ran the ablation and reported what came out.
Challenges we ran into
The PNG trap. We converted the whole dataset to PNG specifically to avoid format leakage — and it didn't work. JPEG blocking survives in the pixel values, not the file container. Only measuring it exposed this; the fix (randomly re-encoding both classes) came from evidence rather than intuition.
A reference benchmark that grades itself. Auditing the provided evaluation set, we found its default config is perfectly separable by image size alone — every real image is exactly 200×200 and no fake is, giving AUROC 1.000 with no model whatsoever. It is also 36/64 imbalanced, so always answering "fake" scores 64% accuracy. We report our headline numbers on the resolution-matched config instead, where only genuine detection signal can score.
Environment reality. Kaggle's transformers build silently ignores
output_hidden_states, so the penultimate CLIP layer came back empty and
extraction crashed. We now capture that tensor with a forward pre-hook on the
final encoder layer — verified bit-identical to the documented path, and
immune to version drift.
Accomplishments that we're proud of
Every pre-registered target met, with the numbers fixed before we saw them:
| Metric | Target (set in advance) | Achieved |
|---|---|---|
| Clean in-distribution AUROC | > 0.95 | 1.000 |
| Worst cell of 16 transforms | > 0.85 | 0.995 |
| Unseen generator (headline) | > 0.80 | 0.952 |
| Four unseen generators | > 0.75 | 0.845 |
| Augmentation ablation | > +0.05 | +0.060 |
The ablation is the result we care about most. Identical architecture, identical data — one head trained with the augmentation pipeline, one without. The augmented head wins by +0.060 AUROC on unseen generators, and on the harshest cells at the fixed threshold the gap is stark: noise σ0.10 accuracy 0.772 → 0.967, JPEG q30 0.939 → 0.982. That is quantified evidence the robustness was engineered rather than lucky.
Our 0.952 on the unseen-generator condition also exceeds the comparable published reference (Cozzolino et al., 0.821).
We report TPR at 1% FPR because it is the number that actually matters operationally — a platform cannot flag one in twenty genuine photos. At that budget we catch 46.5% of fakes on unseen generators (76.9% at 5% FPR), and we state it plainly rather than hiding behind headline accuracy.
What we learned
Audit the data before touching the model. Both shortcuts we found would have produced a beautiful validation curve and a worthless detector. An hour of measurement changed the entire architecture.
A frozen backbone can beat a fine-tuned one when the test distribution is a generator you have never seen — the opposite of the usual instinct to train everything end to end.
More data doesn't substitute for better augmentation. The literature showed 1k images with augmentation beating 10k without, and our ablation reproduced the same direction on our own data.
Cheap models scale. Training costs about 15 minutes of T4 time in total, because the frozen backbone runs once per image and everything downstream works on cached vectors. The deployed artifact is a 9 KB weight vector.
What's next for VerifAI
- Late fusion with a pretrained low-level forensic detector — published gains of +3.6 AUC stack on top of CLIP features, since the two signals are near-orthogonal.
- Larger frozen backbones — ViT-H/14 at 986M params still fits the budget, and a bigger pretraining corpus is worth roughly ten points in the literature.
- Per-surface threshold calibration against a live false-positive budget: strict for ranking demotion, lenient for user-facing labels.
- Continuous generator coverage. Because retraining is a linear fit taking seconds, the response time to a newly released image generator is bounded by data collection rather than engineering — which is what makes this viable as an actual platform defence rather than a snapshot.
Development tools used
- VSCode with the WSL2 (Ubuntu) remote extension — all development, editing and local testing.
- Kaggle Notebooks with a Tesla T4 GPU — CLIP feature extraction, training both classifier heads, and the full evaluation sweep. Total GPU time for the project was about fifteen minutes.
- WSL2 / Ubuntu on Windows — the authoritative Python environment, isolated
in a
venv. - Git and GitHub — version control and team collaboration.
- Gradio — the interactive demo interface used in our video.
Models or APIs used
openai/clip-vit-large-patch14(Hugging Face) — used as a frozen vision backbone. Only the vision tower is loaded: 303,179,776 parameters, none of which are trained. We take the penultimate hidden layer's CLS token, giving a 1024-dimensional feature per image. The parameter count is asserted in code at model load, so the <2B limit is enforced rather than assumed.- scikit-learn
LinearSVC— the only trained component, roughly 1,025 parameters (1024 weights plus one intercept), fitted on the cached CLIP features. - Hugging Face Hub API — for downloading the backbone and the evaluation dataset. Both resources are public and ungated; no API key is required.
No paid APIs, no external inference services, and no commercial detection endpoints were used. The entire system runs locally from a fresh clone.
Libraries and frameworks used
| Library | Role |
|---|---|
| PyTorch | Model execution, fp16 autocast inference on the T4 |
Hugging Face transformers |
Loading and running CLIP ViT-L/14 |
Hugging Face datasets |
Streaming SID_Set and the evaluation subset |
huggingface_hub |
Model and dataset resolution/caching |
| scikit-learn | LinearSVC head, ROC/AUROC metrics, threshold calibration |
| Pillow (PIL) | All image I/O and the transform suite — JPEG re-encoding, blur, resize, noise, colour jitter, cropping |
| NumPy | Feature arrays, metric computation, seeded RNG for reproducibility |
| joblib | Serialising the trained heads (~9 KB each) |
| Gradio | Demo web interface |
| Matplotlib | Error-analysis visualisations |
Datasets and assets used
Training data — 12,000 images, balanced:
- SID_Set (Hugging Face,
saberzl/SID_Set). We streamed a balanced subset: 6,000 real images (OpenImages V7, Flickr-sourced) and 6,000 fully synthetic images (FLUX-generated). The dataset's third class — locally tampered images — was deliberately excluded, since region-level tampering is a localisation task rather than the binary whole-image question posed here. - Images were centre-cropped to 512×512 rather than resized, because resampling is a low-pass filter that smears the high-frequency generator traces a detector depends on. All were stored as PNG.
Evaluation data — never used for training, tuning, or model selection:
techjam-aigc/wildfake-eval-subset, a parquet repackaging of the organisers' reference benchmark (13,841 rows = 4,998 COCO val2017 real + 8,843 DALL·E 3). We report on two of its configs:laion_matched(7,652 images) — our headline metric. Real and fake are resolution-matched, so only genuine detection signal can score.cross_generator(5,494 images) — real photographs versus DALL·E 3, Midjourney v5, SDXL and GigaGAN.
- We audited this benchmark before using it and found its default config is trivially gameable: every real image is exactly 200×200 and no fake is, so a one-line size check scores AUROC 1.000 with no model at all. It is also 36/64 imbalanced. We therefore report the resolution-matched configs and use AUROC rather than raw accuracy.
Generated assets (in the repository):
artifacts/head_aug.joblibandhead_noaug.joblib— the two trained classifier heads forming our augmentation ablation.docs/RESULTS.md— the full 16-cell robustness table.docs/error_analysis/— rendered contact sheets of the highest-confidence false positives and false negatives.
Built With
- ai-detection
- clip
- computer-vision
- data-augmentation
- deepfake-detection
- gradio
- huggingface
- image-classification
- joblib
- machine-learning
- pytorch
- vision-transformer
Log in or sign up for Devpost to join the conversation.