SPAR Reproduction — complete step-by-step · arXiv:2604.02457 · lab path: /home/ubuntu/spar/
A printed border that goes behind your license plate (not covering any text) that fools ALPR systems. The paper achieved:
/home/ubuntu/spar/data/raw/Option A — Web GUI (recommended for teams):
cd /home/ubuntu/spar
source venv/bin/activate
python web_labeler/labeler_server.py --images data/raw/ --labels data/labels/ --port 5050
Open http://YOUR_SERVER_IP:5050 in any browser. Everyone enters their name, clicks corners simultaneously, images auto-assign so no overlap. Full controls on the Web Labeler page.
Option B — Desktop tool (single user):
python label_corners.py -i data/raw/
Click the 4 corners in order: Top-Left → Top-Right → Bottom-Right → Bottom-Left. ~3 seconds per image = 300 images in ~15 minutes. Output: JSON files in data/labels/.
Verify labels:
python check_labels.py -i data/raw/ -l data/labels/
python train_patch.py \
--mode disruption \
--epochs 100 \
--batch-size 64 \
--lr 0.01 \
--tv-weight 0.01
What happens:
Outputs in patches/:
| File | Description |
|---|---|
patch_disruption_epoch100.pt | PyTorch checkpoint |
patch_disruption_epoch100_preview.jpg | Low-res preview |
patch_disruption_epoch100_printable.png | 300 DPI — send this to the printer |
python train_patch.py \
--mode impersonation \
--target "ABC1234" \
--epochs 100 \
--batch-size 64
Makes the ALPR read your plate as a different plate text instead of failing to read it.
patches/patch_disruption_epoch100_printable.png to a professional print shop (FedEx Office, Staples, local shop).python evaluate.py \
--patch patches/patch_disruption_epoch100.pt \
--images data/eval/ \
--labels data/labels/ \
--output results/
What to put in data/eval/:
What you get back (results/):
| Metric | What it measures |
|---|---|
| Detection suppressed completely | % of images where YOLO found no plate at all |
| Avg confidence reduction | How much YOLO's confidence dropped |
| Correct read rate | % where ALPR still read the plate correctly |
| Per-image breakdown | CSV with each image's results |
To render a standalone patch later:
python render_patch.py --patch patches/patch_disruption.pt --output patches/new_print.png
# Activate environment (every new terminal)
source /home/ubuntu/spar/venv/bin/activate
cd /home/ubuntu/spar
# 0. Start web labeler (team can label simultaneously)
python web_labeler/labeler_server.py --images data/raw/ --labels data/labels/ --port 5050
# 1. Label corners via web GUI — http://YOUR_SERVER_IP:5050
# 2. Verify labels
python check_labels.py -i data/raw/ -l data/labels/
# 3. Train disruption
python train_patch.py --mode disruption --epochs 100 --batch-size 64
# 4. Train impersonation (optional)
python train_patch.py --mode impersonation --target "ABC1234" --epochs 100
# 5. Render printable
python render_patch.py --patch patches/patch_disruption_epoch100.pt
# 6. Evaluate
python evaluate.py --patch patches/patch_disruption_epoch100.pt --images data/eval/
Your photo (300x) Random patch
│ │
│ Click 4 corners │
│ of the plate │ Xavier init
▼ ▼
Corner JSON Trainable tensor [3, 400, 800]
│ │
└──────┬────────────────┘
│ kornia find_homography_dlt()
│ warp_perspective()
▼
Patched image (rim composited around plate)
│
┌──────┴──────┐
▼ ▼
YOLO-v9 CCT-OCR
(detection) (reading)
│ │
▼ ▼
det_loss ocr_loss
(lower conf. (max entropy or
+ expand CE to target
bbox) plate text)
│ │
└──────┬──────┘
│ + TV loss (smoothness)
▼
AdamW optimizer
→ updates patch
Key: Both YOLO and OCR run in PyTorch (converted from ONNX via onnx2torch), so gradients flow all the way back to the patch pixels. The homography warp via kornia is also differentiable. The whole thing is one end-to-end gradient descent loop.
| Component | Version | Path |
|---|---|---|
| Python | 3.11 | spar/venv/ |
| PyTorch | 2.13.0 (CPU) | via pip |
| kornia | 0.8.1 | via pip |
| fast-alpr | 0.4.0 | spar/fast-alpr/ |
| fast-plate-ocr | 1.1.0 | spar/fast-plate-ocr/ |
| open-image-models | 0.5.1 | spar/open-image-models/ |
| YOLO model | yolo-v9-t-384 | cached in ~/.cache/ |
| OCR model | cct-xs-v2-global | cached in ~/.cache/ |
| Synthetic test data | 50 train + 10 eval | data/raw/ + data/eval/ |