Inspiration
Most AI-image detectors are evaluated on pristine files. No image on a social platform is pristine. Between the generator and the viewer sits an upload pipeline that re-encodes, resizes, strips metadata and re-compresses — often several times over, as content is screenshotted and reshared.
What made the problem click was realising the failure is not incidental but mechanical. Detectors key on high-frequency generator fingerprints: the periodic traces left by upsampling layers, which live in the top octave of the spectrum. JPEG quantisation and downscaling are low-pass operations. They attack precisely the band the detector depends on. A detector and a compression codec are, in a real sense, fighting over the same coefficients — and the codec runs last.
So I wanted to build something that measures the collapse rather than reporting another clean-benchmark accuracy.
What it does
Afterimage detects AI-generated images and keeps working after the image has been through the things that happen to every image on a platform: JPEG re-encoding, blur, downscaling, sensor noise, filter apps and cropping.
Point it at a directory and it returns a JSON file with a confidence score in $[0,1]$ that each image is AI-generated:
python -m src.predict --image-dir photos/ --output preds.json [{"image_path": "photos/0001.jpg", "pred": 0.93}]
Underneath it is a 1,562,353-parameter CNN — three orders of magnitude under the 2B limit — plus the thing that makes the claim meaningful: an evaluation harness that applies six families of real-world degradation at 15 severities and reports every severity separately.
The headline is not the accuracy, it's the cliff. Trained on clean images only, the detector scores 96.69% clean and falls to 61.32% under blur. Trained with the same transforms as augmentation, it holds 87.94% worst-case across every condition. The mean accuracy drop under post-processing falls from 15.65 points to 2.52 — a $6.2\times$ reduction — for 1.11 points of clean accuracy.
How I built it
The whole project is one controlled comparison, and everything else exists to make it trustworthy.
The measuring instrument came first. Before training anything, I built the transform suite — JPEG at $q \in {90,70,50,30}$, Gaussian blur at $\sigma \in {0.5,1.0,2.0}$, downscale-and-restore at $0.5\times$ and $0.25\times$, Gaussian noise at $\sigma \in {0.02,0.05,0.10}$, $\pm 20%$ colour jitter, and an 80% centre crop — plus a self-test that verifies it. Every claim in the project is a claim about this code, so it is checked rather than assumed. Two checks turned out to be load-bearing:
Noise $\sigma$ is in normalised $[0,1]$ units, applied before normalisation and clamped back. Measured RMSE against the clean image is $0.0198 / 0.0490 / 0.0965$ for requested $\sigma$ of $0.02/0.05/0.10$ — within 2%. Interpreting $\sigma$ in 0–255 units would have made every perturbation $255\times$ too large and every noise result meaningless. JPEG is a real encode/decode round trip, not a blur approximation. Re-encoding an already-$q50$ image at $q50$ changes it by RMSE $0.00000$, which only a real codec does. Since JPEG's high-frequency quantisation is the phenomenon under study, approximating it would have deleted the subject matter.
Then two models, one variable. Identical architecture, seed, schedule and data split, trained twice — once on clean images, once with the transform suite as training-time augmentation — and evaluated across all 15 conditions on the same held-out 20,000-image CIFAKE test split. Stochastic conditions are seeded per image index, so both models are scored on byte-identical inputs and the comparison is exact rather than statistical.
The architecture is a residual CNN trained from scratch, not a fine-tuned ImageNet backbone. Pretrained networks open with a stride-2 convolution and stride-2 pool that discard three quarters of the spatial signal before the first block — they throw away the evidence. The stem here is stride-1 for that reason, and the network is fully convolutional with global average pooling so it accepts any input size.
Augmentation samples continuous severity ranges that span the test severities without containing them ($q \sim \mathcal{U}[30,95]$, blur $\sigma \sim \mathcal{U}[0.3,2.0]$), chaining up to two transforms per image. Training on exactly the test settings would have inflated the table without demonstrating anything general; sampling the interval makes every evaluation point an interpolation rather than a memorised case.
Challenges I ran into
Silent numerical corruption. Computing channel statistics over $\approx 10^8$ pixels in float32 returned $(0.1638, 0.1638, 0.1638)$ — suspicious because the three channels were identical. Once the accumulator passes $2^{25}$ the ULP is $4$, so adding a value near $0.4$ is a no-op and the sum silently stops growing. In float64 the answer is $(0.4720, 0.4629, 0.4178)$, consistent with CIFAR-10. Nothing crashed. I caught it only because I had an expectation to check the output against.
A bug that announced itself, and the one that wouldn't have. Evaluating on a subsample produced nan AUC. The test set is class-ordered, so a head slice was single-class and AUC was undefined. The fix was stratified sampling — but the real lesson was the near-miss: a subsample that was merely imbalanced rather than single-class would have produced plausible, wrong accuracies and no error at all.
Evaluating a 32×32 model at 1024px honestly. CIFAKE is $32\times32$, where "resize $0.25\times$" is an $8\times8$ thumbnail and an 80% centre crop is 25 pixels wide — several conditions are close to degenerate. So I validated on 600 full-resolution SID_Set images, pulled via the HuggingFace datasets-server row API to avoid a 140GB download. Applying the model means tiling at native resolution and averaging per-patch logits before the sigmoid, $p = \sigma!\left(\frac{1}{N}\sum_i z_i\right)$, since averaging probabilities lets a few saturated patches dominate. The first implementation cropped patches one at a time and projected to 2.5 hours; rewriting extraction as a single reshape over the array made it $4\times$ faster with bit-identical output.
The result I did not want. At full resolution the robustness finding transfers — augmentation lifts mean transformed AUC from 64.20 to 67.54 and worst-case from 44.20 to 55.63 — but clean AUC falls from 99.48 to 77.17 with no transform applied at all. That 22.31-point gap is generator and resolution transfer, comparable in size to the worst post-processing effect measured anywhere in the project. Robustness to post-processing and generalisation to unseen generators are separate problems, and this solves only the first.
Accomplishments that I'm proud of
Building the baseline that makes the result legible. The clean-trained model costs a full training run and doesn't improve the headline number — it exists purely so the improvement can be attributed to augmentation and nothing else. It's the first thing a time-pressured build cuts, and without it a single robust model is just a number with nothing to compare against.
Every figure traces to a file. No estimates anywhere. The README, the robustness table and the error analysis are all generated from committed JSON, and a verification pass near the end caught three of my own overstated claims — including one where I'd written that the transfer gap exceeded every transform effect when the data said otherwise. Correcting those mattered more than adding another point of accuracy.
Naming the limitation before a judge could. The full-resolution results are weak in absolute terms, and the writeup leads with that rather than burying it.
It runs. The prediction script works from a clean clone with no dataset download and no retraining, because the checkpoints ship with the repo.
What I learned
Averaging destroys the most interesting structure. The baseline's failure is one-sided at moderate severity — at blur $\sigma=0.5$ it scores TPR $0.613$ against TNR $0.998$, still recognising real images almost perfectly while generated ones slip past. But at $\sigma=2.0$ it reverses to TPR $0.902$ / TNR $0.324$: heavy smoothing makes authentic photographs look synthetic, because unnatural smoothness is itself a cue the model learned. Averaging the three blur severities yields ~68%, which reads as uniform mild degradation and describes nothing that is actually happening.
Accuracy and AUC answer different questions, and the gap between them is diagnostic. Under an 80% centre crop the baseline scores 71.74% accuracy but 94.91 AUC — the ranking survived while the score distribution slid across the 0.5 threshold. That failure is calibration, recoverable by re-thresholding (+16.10 points), not lost signal. Under blur $\sigma=1.0$ only +8.27 points come back, so that one is genuine. For the robust model the recoverable gap is at most +0.35 points in any condition: augmentation didn't just raise accuracy, it made the model's confidence mean the same thing across transforms.
A configuration can look best and be worst. At full resolution, applying the detector by downscaling the image gives a model that varies by only 1.74 AUC points across all 15 conditions — by far the flattest robustness profile in the project, and the least useful one. Downscaling has already destroyed the high-frequency evidence that blur, noise and compression destroy, so the transforms have nothing left to take. Its flatness is a floor, not resilience, and patch-based inference beats it by 12.11 AUC on clean images. Robustness measured without regard to absolute performance can be maximised by making the model useless.
The detector isn't measuring authenticity. Both failure classes lie on one axis — scene texture density, measured as mean absolute Laplacian $\frac{1}{HW}\sum |\nabla^2 I|$. False positives are isolated subjects on smooth backgrounds (aircraft against blank sky, boats on flat water) at $0.1257$ against a test-set mean of $0.1325$; false negatives are busy textured scenes — foliage, fur, clutter — at $0.1363$, above the mean. Low texture reads as synthetic; high texture masks the fingerprint. That's the same axis blur moves images along, which is why blur breaks the model and why the breakage reverses.
What's next for Afterimage
Train at full resolution on multiple generators. The measurements point at one binding constraint, and it isn't robustness — it's the 22-point transfer gap. Training on native-resolution patches drawn from several generator families, rather than transferring a $32\times32$ model trained on Stable Diffusion v1.4 alone, is the single highest-value change and the one the evidence most clearly supports.
Degradation-aware inference. Patch inference beats downscaling in 11 of 15 conditions for the robust model but only 8 of 15 for the baseline — the crossover is real and measurable. A system that estimates input degradation could switch modes on it, using native-resolution patches on clean uploads and falling back under heavy blur.
Per-condition thresholds. The calibration analysis shows how much accuracy a better-placed threshold recovers. A deployed system could set its threshold from an estimate of what the image has been through, rather than running one fixed 0.5 across everything.
The transforms not yet tested: adversarial perturbation, screenshotting, re-photography, platform-specific pipelines, and compositions deeper than two.
Log in or sign up for Devpost to join the conversation.