https://github.com/systemslibrarian/steg-arena
https://github.com/systemslibrarian/steg-arena
Last synced: 2 days ago
JSON representation
- Host: GitHub
- URL: https://github.com/systemslibrarian/steg-arena
- Owner: systemslibrarian
- Created: 2026-04-13T19:18:19.000Z (about 2 months ago)
- Default Branch: main
- Last Pushed: 2026-04-13T19:57:41.000Z (about 2 months ago)
- Last Synced: 2026-04-13T21:26:25.294Z (about 2 months ago)
- Language: Python
- Homepage: https://systemslibrarian.github.io/steg-arena/
- Size: 235 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Steg Arena
**Adversarial Neural Steganography — research platform + browser demo**
Two neural networks compete in an evolutionary arms race:
| Network | Role | Goal |
|---------|------|------|
| **Encoder** (Stegger) | Hides a secret payload in a cover image | Maximize payload capacity and invisibility while fooling the Warden |
| **Warden** (Detector) | Classifies images as clean or stego | Detect the Encoder's output with maximum accuracy |
| **Decoder** (Extractor) | Recovers the hidden payload | Prove the payload survived embedding with low BER |
Architecture based on **HiDDeN** (Zhu et al., ECCV 2018). See `REFERENCES.md` for full bibliography.
---
## Full Workflow
```
Step 1 → Train the arms race python arena.py --image_dir ./images
Step 2 → Export to browser-ready python export_onnx.py
Step 3 → Push demo/ to GitHub git push (GitHub Pages serves it)
Step 4 → Users embed/detect/extract https://your-username.github.io/steg-arena/demo/
```
---
## Quick Start (no images needed)
```bash
# Install CPU-only PyTorch
pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
pip install -r requirements.txt
# Step 1: Train with synthetic data (verifies pipeline, real images give better results)
python arena.py
# Step 2: Export to ONNX
python export_onnx.py --weights_dir ./output --out_dir ./demo
# Step 3: Open demo/index.html in a browser (needs a local HTTP server)
cd demo && python -m http.server 8080
# Then visit http://localhost:8080
```
> **Note:** `index.html` must be served over HTTP (not opened as a file) because
> ONNX Runtime Web uses WebAssembly workers that require a server context.
---
## File Structure
```
steg-arena/
├── arena.py ← Main training loop (Step 1)
├── export_onnx.py ← Export weights → .onnx files (Step 2)
├── models.py ← Encoder, Decoder, Warden architectures
├── dataset.py ← Image loader with synthetic fallback
├── metrics.py ← SSIM, PSNR, BER, detection accuracy
├── visualize.py ← Arms race plot generator
├── inference.py ← CLI embed / detect / extract
├── requirements.txt
├── REFERENCES.md
└── demo/
├── index.html ← Browser demo (loads .onnx files at runtime)
├── encoder.onnx ← Generated by export_onnx.py (add after training)
├── decoder.onnx
└── warden.onnx
```
---
## How the Arms Race Works
```
Round N
┌─────────────────────────────────────────────┐
│ 1. Warden trains │
│ → sees clean covers + stego from │
│ frozen Encoder │
│ → learns to classify via BCELoss │
│ │
│ 2. Encoder trains │
│ → three-part loss: │
│ LAMBDA_IMAGE × MSE(stego, cover) │ imperceptible
│ LAMBDA_DECODE × BCE(decoded, payload) │ recoverable
│ LAMBDA_ADV × BCE(warden, clean) │ evade detection
└─────────────────────────────────────────────┘
```
---
## How the Encoder Learns to Evade the Warden
The encoder doesn't just hide data blindly — it uses the warden's own neural network
as a differentiable guide to learn exactly what to avoid.
### The three-part loss function
During the encoder's training phase each round, every stego image is scored by three
criteria, combined into a single loss that backpropagation optimises:
| Loss component | Formula | What it teaches the encoder |
|----------------|---------|----------------------------|
| **Image loss** | `MSE(stego, cover)` | Keep pixel changes as small as possible — stay invisible to human eyes |
| **Decode loss** | `BCE(decoder(stego), payload)` | Embed the payload so the decoder can recover it — don't lose the message |
| **Adversarial loss** | `BCE(warden(stego), 0)` | Make the warden classify stego as **clean** (label 0) — fool the detector |
The three components are weighted by `LAMBDA_IMAGE`, `LAMBDA_DECODE`, and
`LAMBDA_ADVERSARIAL` (default 1.0, 1.0, 0.5) to balance invisibility, recoverability,
and evasion.
### The gradient trick
The adversarial loss is where the real learning happens:
1. The **warden is frozen** (`warden.eval()`) — its weights don't change during encoder training
2. The encoder produces a stego image and passes it through the frozen warden
3. The loss is computed as if the stego image were **clean** (target label = 0)
4. **Backpropagation flows through the warden** back into the encoder — the gradients
tell the encoder exactly which pixel patterns the warden is currently using to detect
stego, and in which direction to adjust to avoid them
The encoder literally reads the warden's mind through its gradients, then learns to
produce images that exploit the warden's blind spots.
### The arms race alternation
Each round repeats two phases:
1. **Warden trains** (encoder frozen) — sees the encoder's latest stego output alongside
clean covers, learns to tell them apart
2. **Encoder trains** (warden frozen) — uses the warden's updated detection network as
a guide to learn new hiding strategies that evade the latest detection
This creates an evolutionary arms race: the warden gets better at detecting → the encoder
adapts to hide differently → the warden adapts again → and so on. Over many rounds, both
networks become increasingly sophisticated.
---
## Network Architecture
These are **not** pretrained foundation models or LLMs — they are small custom
convolutional neural networks (CNNs) defined in `models.py`, trained from scratch
on your images. The design is based on the
[HiDDeN](https://arxiv.org/abs/1807.09937) paper (Zhu et al., ECCV 2018), scaled
down for CPU training.
### Building block
Every network is built from `ConvBnRelu` — a single repeating unit:
```
Conv2d → BatchNorm2d → ReLU
```
### Network details
| Network | Layers | Parameters | Architecture summary |
|---------|--------|------------|----------------------|
| **Encoder** | 5 conv + Tanh | 168,451 | Two-branch CNN. Cover branch extracts image features (2 layers). Payload is tiled spatially and concatenated with features. Joint branch (3 layers + 1×1 conv) produces a residual that is scaled by 0.1 and added to the cover image. |
| **Decoder** | 4 conv + pool + FC | ~66K | Four ConvBnRelu layers → adaptive average pooling → linear head outputs one logit per payload bit. |
| **Warden** | 4 conv + MaxPool + FC | 130,593 | Progressively pools with MaxPool2d(2) after each layer (64→32→16→8), then adaptive pool → linear binary classifier. Hidden dims double each stage: 32→64→64→128. |
**Total: ~365K parameters** — roughly 10,000× smaller than a typical LLM.
### Why the architecture matters
The balance between encoder and warden capacity directly shapes the arms race:
- **Warden too strong** → encoder can never learn to evade; training stalls with
warden_acc stuck near 1.0
- **Warden too weak** → encoder fools it immediately; it never learns subtlety
because there's no pressure to improve
- **Well matched** → both sides push each other, producing genuinely evolved strategies
The current design is deliberately balanced and small so the arms race dynamic works
on CPU hardware.
### Ideas for improving the architecture
If you have GPU access or want to push research further, here are concrete upgrades
to try in `models.py`:
| Upgrade | Where | What it does | Expected impact |
|---------|-------|--------------|-----------------|
| **ResNet blocks** | Encoder & Decoder | Replace `ConvBnRelu` with residual blocks (`x + conv(conv(x))`) | Better gradient flow → faster convergence, higher SSIM |
| **U-Net encoder** | Encoder | Add skip connections from cover branch to joint branch at multiple resolutions | Preserves fine spatial detail → more imperceptible embedding |
| **Attention layers** | Warden | Add channel or spatial attention (SE blocks, CBAM) | Warden focuses on diagnostic regions → forces encoder to hide more uniformly |
| **SRNet / Zhu-Net warden** | Warden | Use a steganalysis-specific architecture from the literature | Much stronger detection → pushes encoder to develop advanced strategies |
| **Larger hidden dims** | All networks | Increase `hidden_dim` from 64/32 to 128/64 or higher | More capacity for both sides, but slower training |
| **Higher resolution** | All | Train at 128×128 or 256×256 (`--image_size 256`) | More spatial area to hide data, more realistic images |
| **Longer payloads** | Encoder & Decoder | Increase `--payload_len` from 32 to 64 or 128 bits | More data hidden per image, harder to keep invisible |
**Important:** When changing architecture in `models.py`, delete `output/` and retrain
from scratch — old checkpoints won't be compatible with new network shapes.
---
## Reading the Results
| Metric | Encoder winning | Warden winning |
|--------|----------------|----------------|
| Warden accuracy | < 0.60 (near random) | > 0.80 |
| SSIM | > 0.95 (invisible) | — |
| BER | < 0.10 (recoverable) | — |
---
## CLI Usage (inference.py)
```bash
# Embed a message (Python, no browser needed)
python inference.py embed \
--image cover.png \
--message "HELP" \
--encoder output/encoder_final.pt \
--out stego.png
# Detect steganography
python inference.py detect \
--image stego.png \
--warden output/warden_final.pt
# Extract the payload
python inference.py extract \
--image stego.png \
--decoder output/decoder_final.pt
```
---
## Key Hyperparameters
| Parameter | Default | Notes |
|-----------|---------|-------|
| `--image_size` | 64 | 64×64 recommended for CPU. 128 is slower but higher capacity. |
| `--payload_len` | 32 | Bits per image. 32 = 4 ASCII chars. Match in export_onnx.py and index.html. |
| `--batch_size` | 4 | Keep 4–8 for CPU. |
| `--rounds` | 20 | More rounds = more evolved strategies. 30+ recommended for research. |
| `LAMBDA_ADVERSARIAL` | 0.5 | Raise to make encoder more aggressive at evasion. |
---
## Recommended Datasets
| Dataset | Images | URL |
|---------|--------|-----|
| BOSS Base 1.01 | 10,000 | http://agents.fel.cvut.cz/boss/ |
| BOWS2 | 10,000 | http://bows2.ec-lille.fr/ |
| ALASKA2 | 80,000 | https://www.kaggle.com/c/alaska2-image-steganalysis |
---
## GitHub Pages Deployment
```bash
# In your crypto-lab repo:
cp -r /path/to/steg-arena/demo ./steg-arena
# Edit demo/index.html if your image_size or payload_len differ from defaults
# Then push — GitHub Pages serves the .onnx files automatically
git add steg-arena/ && git commit -m "add steg-arena demo" && git push
```
The browser loads `encoder.onnx`, `decoder.onnx`, and `warden.onnx` from the same folder as `index.html`.
---
*"Whatever you do, do it all for the glory of God." — 1 Corinthians 10:31*