{"id":50486870,"url":"https://github.com/systemslibrarian/steg-arena","last_synced_at":"2026-06-01T23:02:42.254Z","repository":{"id":351152085,"uuid":"1209779889","full_name":"systemslibrarian/steg-arena","owner":"systemslibrarian","description":null,"archived":false,"fork":false,"pushed_at":"2026-04-13T19:57:41.000Z","size":241,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-13T21:26:25.294Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://systemslibrarian.github.io/steg-arena/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/systemslibrarian.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-13T19:18:19.000Z","updated_at":"2026-04-13T19:59:18.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/systemslibrarian/steg-arena","commit_stats":null,"previous_names":["systemslibrarian/steg-arena"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/systemslibrarian/steg-arena","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/systemslibrarian%2Fsteg-arena","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/systemslibrarian%2Fsteg-arena/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/systemslibrarian%2Fsteg-arena/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/systemslibrarian%2Fsteg-arena/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/systemslibrarian","download_url":"https://codeload.github.com/systemslibrarian/steg-arena/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/systemslibrarian%2Fsteg-arena/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33797128,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-01T02:00:06.963Z","response_time":115,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-01T23:02:40.617Z","updated_at":"2026-06-01T23:02:42.248Z","avatar_url":"https://github.com/systemslibrarian.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Steg Arena\n\n**Adversarial Neural Steganography — research platform + browser demo**\n\nTwo neural networks compete in an evolutionary arms race:\n\n| Network | Role | Goal |\n|---------|------|------|\n| **Encoder** (Stegger) | Hides a secret payload in a cover image | Maximize payload capacity and invisibility while fooling the Warden |\n| **Warden** (Detector) | Classifies images as clean or stego | Detect the Encoder's output with maximum accuracy |\n| **Decoder** (Extractor) | Recovers the hidden payload | Prove the payload survived embedding with low BER |\n\nArchitecture based on **HiDDeN** (Zhu et al., ECCV 2018). See `REFERENCES.md` for full bibliography.\n\n---\n\n## Full Workflow\n\n```\nStep 1  →  Train the arms race         python arena.py --image_dir ./images\nStep 2  →  Export to browser-ready     python export_onnx.py\nStep 3  →  Push demo/ to GitHub        git push (GitHub Pages serves it)\nStep 4  →  Users embed/detect/extract  https://your-username.github.io/steg-arena/demo/\n```\n\n---\n\n## Quick Start (no images needed)\n\n```bash\n# Install CPU-only PyTorch\npip install torch torchvision --index-url https://download.pytorch.org/whl/cpu\npip install -r requirements.txt\n\n# Step 1: Train with synthetic data (verifies pipeline, real images give better results)\npython arena.py\n\n# Step 2: Export to ONNX\npython export_onnx.py --weights_dir ./output --out_dir ./demo\n\n# Step 3: Open demo/index.html in a browser (needs a local HTTP server)\ncd demo \u0026\u0026 python -m http.server 8080\n# Then visit http://localhost:8080\n```\n\n\u003e **Note:** `index.html` must be served over HTTP (not opened as a file) because\n\u003e ONNX Runtime Web uses WebAssembly workers that require a server context.\n\n---\n\n## File Structure\n\n```\nsteg-arena/\n├── arena.py          ← Main training loop (Step 1)\n├── export_onnx.py    ← Export weights → .onnx files (Step 2)\n├── models.py         ← Encoder, Decoder, Warden architectures\n├── dataset.py        ← Image loader with synthetic fallback\n├── metrics.py        ← SSIM, PSNR, BER, detection accuracy\n├── visualize.py      ← Arms race plot generator\n├── inference.py      ← CLI embed / detect / extract\n├── requirements.txt\n├── REFERENCES.md\n└── demo/\n    ├── index.html    ← Browser demo (loads .onnx files at runtime)\n    ├── encoder.onnx  ← Generated by export_onnx.py (add after training)\n    ├── decoder.onnx\n    └── warden.onnx\n```\n\n---\n\n## How the Arms Race Works\n\n```\nRound N\n  ┌─────────────────────────────────────────────┐\n  │  1. Warden trains                           │\n  │     → sees clean covers + stego from        │\n  │       frozen Encoder                        │\n  │     → learns to classify via BCELoss        │\n  │                                             │\n  │  2. Encoder trains                          │\n  │     → three-part loss:                      │\n  │       LAMBDA_IMAGE  × MSE(stego, cover)     │  imperceptible\n  │       LAMBDA_DECODE × BCE(decoded, payload) │  recoverable\n  │       LAMBDA_ADV    × BCE(warden, clean)    │  evade detection\n  └─────────────────────────────────────────────┘\n```\n\n---\n\n## How the Encoder Learns to Evade the Warden\n\nThe encoder doesn't just hide data blindly — it uses the warden's own neural network\nas a differentiable guide to learn exactly what to avoid.\n\n### The three-part loss function\n\nDuring the encoder's training phase each round, every stego image is scored by three\ncriteria, combined into a single loss that backpropagation optimises:\n\n| Loss component | Formula | What it teaches the encoder |\n|----------------|---------|----------------------------|\n| **Image loss** | `MSE(stego, cover)` | Keep pixel changes as small as possible — stay invisible to human eyes |\n| **Decode loss** | `BCE(decoder(stego), payload)` | Embed the payload so the decoder can recover it — don't lose the message |\n| **Adversarial loss** | `BCE(warden(stego), 0)` | Make the warden classify stego as **clean** (label 0) — fool the detector |\n\nThe three components are weighted by `LAMBDA_IMAGE`, `LAMBDA_DECODE`, and\n`LAMBDA_ADVERSARIAL` (default 1.0, 1.0, 0.5) to balance invisibility, recoverability,\nand evasion.\n\n### The gradient trick\n\nThe adversarial loss is where the real learning happens:\n\n1. The **warden is frozen** (`warden.eval()`) — its weights don't change during encoder training\n2. The encoder produces a stego image and passes it through the frozen warden\n3. The loss is computed as if the stego image were **clean** (target label = 0)\n4. **Backpropagation flows through the warden** back into the encoder — the gradients\n   tell the encoder exactly which pixel patterns the warden is currently using to detect\n   stego, and in which direction to adjust to avoid them\n\nThe encoder literally reads the warden's mind through its gradients, then learns to\nproduce images that exploit the warden's blind spots.\n\n### The arms race alternation\n\nEach round repeats two phases:\n\n1. **Warden trains** (encoder frozen) — sees the encoder's latest stego output alongside\n   clean covers, learns to tell them apart\n2. **Encoder trains** (warden frozen) — uses the warden's updated detection network as\n   a guide to learn new hiding strategies that evade the latest detection\n\nThis creates an evolutionary arms race: the warden gets better at detecting → the encoder\nadapts to hide differently → the warden adapts again → and so on. Over many rounds, both\nnetworks become increasingly sophisticated.\n\n---\n\n## Network Architecture\n\nThese are **not** pretrained foundation models or LLMs — they are small custom\nconvolutional neural networks (CNNs) defined in `models.py`, trained from scratch\non your images. The design is based on the\n[HiDDeN](https://arxiv.org/abs/1807.09937) paper (Zhu et al., ECCV 2018), scaled\ndown for CPU training.\n\n### Building block\n\nEvery network is built from `ConvBnRelu` — a single repeating unit:\n\n```\nConv2d → BatchNorm2d → ReLU\n```\n\n### Network details\n\n| Network | Layers | Parameters | Architecture summary |\n|---------|--------|------------|----------------------|\n| **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. |\n| **Decoder** | 4 conv + pool + FC | ~66K | Four ConvBnRelu layers → adaptive average pooling → linear head outputs one logit per payload bit. |\n| **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. |\n\n**Total: ~365K parameters** — roughly 10,000× smaller than a typical LLM.\n\n### Why the architecture matters\n\nThe balance between encoder and warden capacity directly shapes the arms race:\n\n- **Warden too strong** → encoder can never learn to evade; training stalls with\n  warden_acc stuck near 1.0\n- **Warden too weak** → encoder fools it immediately; it never learns subtlety\n  because there's no pressure to improve\n- **Well matched** → both sides push each other, producing genuinely evolved strategies\n\nThe current design is deliberately balanced and small so the arms race dynamic works\non CPU hardware.\n\n### Ideas for improving the architecture\n\nIf you have GPU access or want to push research further, here are concrete upgrades\nto try in `models.py`:\n\n| Upgrade | Where | What it does | Expected impact |\n|---------|-------|--------------|-----------------|\n| **ResNet blocks** | Encoder \u0026 Decoder | Replace `ConvBnRelu` with residual blocks (`x + conv(conv(x))`) | Better gradient flow → faster convergence, higher SSIM |\n| **U-Net encoder** | Encoder | Add skip connections from cover branch to joint branch at multiple resolutions | Preserves fine spatial detail → more imperceptible embedding |\n| **Attention layers** | Warden | Add channel or spatial attention (SE blocks, CBAM) | Warden focuses on diagnostic regions → forces encoder to hide more uniformly |\n| **SRNet / Zhu-Net warden** | Warden | Use a steganalysis-specific architecture from the literature | Much stronger detection → pushes encoder to develop advanced strategies |\n| **Larger hidden dims** | All networks | Increase `hidden_dim` from 64/32 to 128/64 or higher | More capacity for both sides, but slower training |\n| **Higher resolution** | All | Train at 128×128 or 256×256 (`--image_size 256`) | More spatial area to hide data, more realistic images |\n| **Longer payloads** | Encoder \u0026 Decoder | Increase `--payload_len` from 32 to 64 or 128 bits | More data hidden per image, harder to keep invisible |\n\n**Important:** When changing architecture in `models.py`, delete `output/` and retrain\nfrom scratch — old checkpoints won't be compatible with new network shapes.\n\n---\n\n## Reading the Results\n\n| Metric | Encoder winning | Warden winning |\n|--------|----------------|----------------|\n| Warden accuracy | \u003c 0.60 (near random) | \u003e 0.80 |\n| SSIM | \u003e 0.95 (invisible) | — |\n| BER  | \u003c 0.10 (recoverable) | — |\n\n---\n\n## CLI Usage (inference.py)\n\n```bash\n# Embed a message (Python, no browser needed)\npython inference.py embed \\\n    --image cover.png \\\n    --message \"HELP\" \\\n    --encoder output/encoder_final.pt \\\n    --out stego.png\n\n# Detect steganography\npython inference.py detect \\\n    --image stego.png \\\n    --warden output/warden_final.pt\n\n# Extract the payload\npython inference.py extract \\\n    --image stego.png \\\n    --decoder output/decoder_final.pt\n```\n\n---\n\n## Key Hyperparameters\n\n| Parameter | Default | Notes |\n|-----------|---------|-------|\n| `--image_size` | 64 | 64×64 recommended for CPU. 128 is slower but higher capacity. |\n| `--payload_len` | 32 | Bits per image. 32 = 4 ASCII chars. Match in export_onnx.py and index.html. |\n| `--batch_size` | 4 | Keep 4–8 for CPU. |\n| `--rounds` | 20 | More rounds = more evolved strategies. 30+ recommended for research. |\n| `LAMBDA_ADVERSARIAL` | 0.5 | Raise to make encoder more aggressive at evasion. |\n\n---\n\n## Recommended Datasets\n\n| Dataset | Images | URL |\n|---------|--------|-----|\n| BOSS Base 1.01 | 10,000 | http://agents.fel.cvut.cz/boss/ |\n| BOWS2 | 10,000 | http://bows2.ec-lille.fr/ |\n| ALASKA2 | 80,000 | https://www.kaggle.com/c/alaska2-image-steganalysis |\n\n---\n\n## GitHub Pages Deployment\n\n```bash\n# In your crypto-lab repo:\ncp -r /path/to/steg-arena/demo ./steg-arena\n\n# Edit demo/index.html if your image_size or payload_len differ from defaults\n# Then push — GitHub Pages serves the .onnx files automatically\ngit add steg-arena/ \u0026\u0026 git commit -m \"add steg-arena demo\" \u0026\u0026 git push\n```\n\nThe browser loads `encoder.onnx`, `decoder.onnx`, and `warden.onnx` from the same folder as `index.html`.\n\n---\n\n*\"Whatever you do, do it all for the glory of God.\" — 1 Corinthians 10:31*\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsystemslibrarian%2Fsteg-arena","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsystemslibrarian%2Fsteg-arena","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsystemslibrarian%2Fsteg-arena/lists"}