Binary Real (0) vs AI-Generated (1) image detection in PyTorch. This is the code behind the undergraduate thesis "DeepGuard: Towards AI-Generated Image Detection" (Computer Engineering, University of Brasília — UnB), and it doubles as the reproducibility artifact for that work.
Status: archived / thesis complete. All eleven experimental phases are finished. Treat this repository as a settled, reproducible archive: reproduce, evaluate, or extend, but the method decisions are documented in the thesis.
The persistent ~83 % accuracy / ~0.92 AUC ceiling reached by every CNN and by full fine-tuning was broken only by LoRA-adapting a DINOv2 ViT-L/14:
| Model | Preprocess | NTIRE test Acc | NTIRE test AUC | Trainable params | Train time |
|---|---|---|---|---|---|
| DINOv2 ViT-L/14 + LoRA (r=16, α=32) | RGB | 90.37 % | 0.9705 | ~2.4 M / 305 M | 16.8 h |
Single-stream RGB, trained on a single NVIDIA RTX 2060 Super (8 GB). Reported
figures are Clean AUC (undistorted); robustness under the NTIRE degradation
pipeline was not evaluated. See all_evaluations.md for the
full per-run record across all phases.
- Python 3.12 (pinned to 3.12.12 in
.python-version) - NVIDIA GPU with CUDA 12.6 for the pinned Torch wheels (the project was developed on an 8 GB RTX 2060 Super; the code is device-agnostic and will fall back to CPU, but training the ViT-L/14 without a GPU is impractical)
- Dependencies pinned in
requirements.txt
requirements.txt pins the CUDA 12.6 build of Torch/torchvision via an
--extra-index-url. On a machine with a different CUDA version (or CPU-only),
install torch/torchvision first from the appropriate index at
https://pytorch.org/get-started/locally/, then install the rest.
git clone https://github.com/ArthurAntero/project-tcc.git
cd project-tcc
python3.12 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txttimm (DINOv2 backbones) and peft (LoRA) are pinned in requirements.txt and
imported dynamically inside src/models.py; no extra steps are
needed once the requirements are installed.
The project uses two datasets, each laid out as {split}/{REAL,FAKE}/:
| Dataset | Splits (REAL / FAKE) | Total | Role |
|---|---|---|---|
| KAGGLE-AI-VS-REAL-IMAGES | train 31 980 / 31 980 · test 7 995 / 7 995 |
79 950 | Curated, in-distribution (calibration ceiling) |
| NTIRE-AI-VS-REAL-IMAGES | train 99 765 / 177 220 · test 5 000 / 5 000 · test-hard 1 250 / 1 250 |
289 485 | In-the-wild benchmark (42 generators) |
Download links:
- KAGGLE-AI-VS-REAL-IMAGES — https://www.kaggle.com/competitions/detect-ai-vs-human-generated-images
- NTIRE-AI-VS-REAL-IMAGES — https://www.codabench.org/competitions/12761/#/pages-tab
After downloading, arrange each dataset on disk as:
<datasets-root>/
├── KAGGLE-AI-VS-REAL-IMAGES/
│ ├── train/{REAL,FAKE}/
│ └── test/{REAL,FAKE}/
└── NTIRE-AI-VS-REAL-IMAGES/
├── train/{REAL,FAKE}/
├── test/{REAL,FAKE}/
└── test-hard/{REAL,FAKE}/
Paths are resolved in src/config.py and can be overridden
without editing code, via environment variables:
export AI_VS_REAL_DATASETS_ROOT=/path/to/datasets # folder holding both datasets
export AI_VS_REAL_DATA_DIR=/path/to/datasets/KAGGLE-AI-VS-REAL-IMAGES # default single datasettrain.py / test.py also accept --datasets-root and --train-dataset /
--test-dataset (a folder name under the root, or an absolute path). Run either
script with --list-datasets to see what was discovered.
All three entry points fall back to interactive numbered prompts when required flags are omitted.
python train.py --model dinov2_vitl14 --epochs 10 --batch 8 --lr 1e-4 \
--train-dataset NTIRE-AI-VS-REAL-IMAGES \
--preprocess-method none \
--freeze-backbone lora--model— one of:alexnet,convnext_tiny,densenet121,dinov2_vitb14,dinov2_vitl14,efficientnet_b0,efficientnet_b4,efficientnet_v2_s,regnet_y_3_2gf,resnet50,swin_t,vgg16_bn.--preprocess-method—none(RGB),lota_nbc(LOTA bit-plane noise), orgradient_full(per-channel directional gradients). All three are 3-channel.--freeze-backbone—none(full fine-tune, default),all(freeze backbone, legacy), orlora(freeze weights, inject LoRA adapters into DINOv2 attention — the winning recipe).
Stream A is always RGB; stream B carries the forensic preprocessing.
python train.py --mix \
--model-a dinov2_vitl14 --model-b resnet50 \
--mix-stream-b gradient_full \
--freeze-backbone-a lora --freeze-backbone-b none \
--train-dataset NTIRE-AI-VS-REAL-IMAGESpython test.py --checkpoint-experiment dinov2_vitl14_20260511_003834 \
--test-dataset NTIRE-AI-VS-REAL-IMAGES --test-split test \
--tta none --class-one-label auto--checkpoint-experiment— experiment id undercheckpoints/(or--checkpoint-pathfor an explicitbest_model.pth).--test-split—testor (NTIRE only)test-hard.--threshold— omit to reuse the threshold saved at training time (grid-searched on validation to maximize balanced accuracy).--tta—none,hflip,5crop, or10crop.--class-one-label—autoprobes both orientations and picks the best (compatibility with pre-convention checkpoints).
python app.pyLaunches a Gradio UI that scans checkpoints/ for any experiment with a
best_model.pth, supports both single and mix checkpoints, and reports
P(REAL) and P(FAKE (AI)) explicitly.
Every run writes reproducible artifacts (see src/utils.py):
- Training →
results/training/{model}/{exp_id}/—train-results.json(full config + final metrics + freeze policy + LoRA params + class map +best_threshold), a per-epoch metrics CSV, and a training-curve plot. The checkpoint is saved tocheckpoints/{model}/{exp_id}/best_model.pth. - Evaluation →
results/evaluations/{model}/{exp_id}/{dataset}__{split}/—eval-results.json+ a confusion matrix.
Every number in the thesis traces to one of these records; the consolidated
per-run log is all_evaluations.md.
- Class mapping is fixed:
REAL = 0,FAKE = 1(explicitly remapped insrc/dataset.py; recorded in everytrain-results.json). - Single-logit output (
NUM_CLASSES = 1), trained withBCEWithLogitsLoss; inference issigmoid+ threshold. - Reproducibility:
RANDOM_SEED = 42, stratified 80/20 split. - Faithful paper reimplementations. LOTA (Wang et al., 2025) is reimplemented
literally in
src/preprocess_lota.py— BGNIG (3 LSBs → noise image) + MGPS (single 32×32 max-gradient patch at native resolution).
- LOTA — Wang et al. (2025), Bit-Planes Guided AI-Generated Image Detection.
- DINOv2 — Oquab et al. (2024), DINOv2: Learning Robust Visual Features without Supervision.
- LoRA — Hu et al. (2022), LoRA: Low-Rank Adaptation of Large Language Models.
- Benchmark — NTIRE 2026 Robust AI-Generated Image Detection in the Wild
(42 generators, 36-step degradation pipeline; the public validation splits are
used here locally as
test/test-hard).
If you use this code, please cite the thesis:
@thesis{antero2026deepguard,
author = {Arthur Antero},
title = {DeepGuard: Towards AI-Generated Image Detection},
school = {University of Brasília (UnB)},
type = {Undergraduate thesis},
year = {2026}
}