ResistAI
Robust detection of AI-generated images under real-world transformations.
Inspiration
Almost every AI-image detector we looked at is benchmarked on pristine images: the raw output of the generator, straight from the model, untouched. That is not how AI-generated images actually reach people. By the time a synthetic image lands in a group chat or on a timeline, it has been JPEG-compressed by an upload pipeline, screenshotted, resized for a thumbnail, re-uploaded, maybe cropped and colour-corrected along the way. Every one of those steps destroys exactly the kind of high-frequency generator fingerprint that detectors quietly rely on.
So the question we wanted to answer was not "can a model tell real from fake?" It was "does that ability survive contact with the real internet?" We suspected the honest answer was no, and we wanted numbers instead of a suspicion.
What it does
ResistAI takes an image and predicts real (0) or AI-generated (1), and it is built to stay accurate after the image has been degraded.
Specifically, it is evaluated under six families of real-world transformation:
- JPEG compression (quality 90, 70, 50, 30)
- Gaussian blur (sigma 0.5, 1.0, 2.0)
- Resize down then back up (0.5x, 0.25x)
- Gaussian noise (sigma 0.02, 0.05, 0.10)
- Colour jitter (brightness, contrast, saturation, plus or minus 20%)
- Centre crop (keep 80%, resize back)
The repo ships two trained models so the comparison is explicit rather than assumed. Baseline v1 is a straightforward fine-tune on clean images. Robust v1 is the same model, same optimiser, same frozen data split, with real-world corruptions applied on the fly during training only. Everything else is machinery for making that comparison trustworthy: a frozen 80/10/10 split committed to the repo, a 17-condition robustness benchmark, a CLI that produces predictions.json for submission, and a Streamlit dashboard for poking at single images.
How we built it
Backbone. Pretrained EfficientNet-B0 via timm, around 5.3M parameters, well under the 2B limit, with a 2-class head. We deliberately picked something small. The interesting variable was the training distribution, not the parameter count, and a small model meant we could run many more experiments in a hackathon window.
Data. CIFAKE: 50,000 real CIFAR-10 photos and 50,000 Stable Diffusion v1.4 fakes, pulled from the HuggingFace mirror yanbax/CIFAKE_autotrain_compatible (MIT, no login, single zip).
The split, frozen and committed. The mirror has no train/test split, so we defined one once with scripts/make_splits.py: stratified per class, 80/10/10, seeded, verified disjoint by scripts/verify_splits.py, and the resulting CSV manifests are committed to the repo. train.py only ever reads train.csv and val.csv. evaluate.py only ever reads test.csv. Neither script can re-split, and make_splits.py refuses to overwrite existing manifests without --force. Images on disk are never copied or modified.
Transformation-aware training. During training only, each image has probability --aug_prob of getting one randomly chosen transform from src/augmentations.py applied on the fly. Validation and test stay clean. The label never changes. Without the --augment flag, train.py is byte-identical to the baseline, which is what makes the two runs genuinely comparable.
Benchmarking. scripts/evaluate_robustness.py scores a checkpoint on the same 10,000-image held-out test set under 17 conditions (clean plus 16 transformed), with every image transformed at a fixed severity. Outputs land in reports/benchmark_results.csv, with figures regenerated by reports/make_figures.py.
Reproducibility plumbing. Every run writes run_config.json with the exact settings used, metrics.csv per epoch, and metrics.json plus a confusion matrix at evaluation. Device selection is automatic (CUDA, then MPS, then CPU), so the same code ran on a MacBook for iteration and a Colab T4 for full runs with no changes.
Challenges we ran into
The result was not the one we wanted. Robust v1 is not universally better. It gives up clean-image accuracy to buy robustness. The temptation to bury that, or to quietly report only the transformed averages, was real. We put the trade-off in the README table instead, and it made the project stronger.
Making the comparison actually fair. Two models are only comparable if literally everything else is held constant. That drove a lot of unglamorous work: freezing and committing the split, refusing to overwrite manifests, recording run configs, keeping the augmentation path completely inert when the flag is off.
32x32 images. CIFAKE is tiny. Applying blur sigma 2.0 or a 0.25x downscale to a 32-pixel image is brutal, and it upscales to 64px for the model, which means it never sees native high-frequency detail. This shaped our expectations about what the absolute accuracy numbers mean.
Diagnosing the failure mode, not just the number. The baseline does not degrade gracefully under blur and resize. It collapses toward predicting "real" for everything. Accuracy alone hides this, since a 55% accuracy looks like mild damage. The F1 gap is what exposed it, which is why the benchmark tracks accuracy, F1, and ROC-AUC per condition rather than a single headline metric.
Hackathon scope discipline. The plan said "extra robustness tricks only if time permits, do not over-engineer." We stuck to it, and shipped a complete comparison instead of a half-finished clever idea.
Accomplishments that we're proud of
The headline numbers, all from the same 10,000-image held-out test set:
| Baseline v1 | Robust v1 | Δ | |
|---|---|---|---|
| Clean accuracy / F1 / ROC-AUC | 0.915 / 0.915 / 0.969 | 0.897 / 0.899 / 0.957 | -1.8 / -1.6 / -1.2 pp |
| Transformed (mean of 16) accuracy | 0.754 | 0.833 | +7.9 pp |
| Transformed (mean of 16) F1 | 0.676 | 0.821 | +14.5 pp |
| Transformed (mean of 16) ROC-AUC | 0.836 | 0.905 | +6.9 pp |
| Accuracy retention under corruption | ~0.82 | ~0.93 |
The biggest wins are exactly where the baseline falls apart:
| Condition | Baseline | Robust | Gain |
|---|---|---|---|
| Blur sigma 1.0 | 0.569 | 0.819 | +25.0 pp (F1 +55.6) |
| Resize 0.5x | 0.554 | 0.803 | +24.9 pp (F1 +55.8) |
| Blur sigma 2.0 | 0.531 | 0.728 | +19.7 pp |
| Resize 0.25x | 0.524 | 0.721 | +19.7 pp |
| Centre crop 80% | 0.664 | 0.833 | +16.9 pp |
| Noise sigma 0.10 | 0.533 | 0.660 | +12.7 pp |
A 1.8 point cost on clean images bought roughly 25 points on blur and resize, and lifted accuracy retention under corruption from about 0.82 to about 0.93.
Beyond the numbers, we are proud of the honesty of the writeup. We documented where Robust v1 loses (clean, all four JPEG levels, light noise, all three colour-jitter conditions, all within about 2 points), and we wrote a Limitations section listing single-seed runs, a 3-epoch and 8,000-image budget, one generator family, and simulated rather than measured corruptions. A result you can trust is worth more than a result that looks good.
What we learned
Robustness is a distributional problem, not an architectural one. We changed nothing about the model. We changed what it saw during training, and the failure modes moved by 25 points. That is a much cheaper lever than a bigger backbone.
Detectors fail asymmetrically. Under heavy degradation, the baseline does not become randomly wrong. It becomes systematically biased toward "real," because blur and downscaling erase the generator artefacts that push it toward "fake." A detector whose errors all point one direction is more dangerous than one that is simply less accurate, because it fails silently in exactly the direction that lets synthetic media through.
Aggregate accuracy hides everything interesting. Only the per-condition breakdown showed us which transforms are cheap (JPEG, colour jitter, light noise) and which are devastating (blur, resize, crop). The mean of 16 conditions would have told us "somewhat worse" and nothing else.
Freezing the split early paid for itself repeatedly. Every experiment after that point was directly comparable to every earlier one, with no doubt about leakage and no re-running to check.
There is no free lunch, and saying so is a feature. The clean-versus-robust trade-off is real and measurable. Naming it means a deployer can make an informed choice: if your inputs are pristine, use the baseline; if your inputs came off the internet, use the robust model.
What's next for ResistAI
Close the trade-off instead of accepting it. The obvious next experiment is mixing clean and augmented batches rather than transforming with a fixed probability, so the model keeps clean-image performance while gaining corruption robustness. Consistency regularisation between the clean and transformed views of the same image is the other candidate.
Spend the budget we did not have. Both models were still improving at epoch 3, with validation loss still decreasing. Longer training, a larger --image_size, and multiple seeds with confidence intervals would tell us how much of the trade-off is real and how much is undertrained noise.
Escape 32x32. Retrain on a native-resolution dataset so the model can actually see the high-frequency detail that resolution-preserving detectors depend on. We expect the transform rankings to shift once JPEG and blur have real detail to destroy.
Test generalisation across generators. Every fake in CIFAKE is Stable Diffusion v1.4. Nothing here proves the model transfers to SDXL, Midjourney, FLUX, or GAN outputs. Cross-generator evaluation is the single most important gap in the current results.
Real corruptions, not simulated ones. Push images through actual platform pipelines (upload, screenshot, re-upload) and benchmark against those rather than our approximations of them.
Ship the demo. The Streamlit dashboard exists; a recorded demo video and per-condition confidence visualisation would make the robustness story visible in ten seconds rather than in a results table.
Built With
- claude
- python
Log in or sign up for Devpost to join the conversation.