Metadata-Version: 2.4
Name: nsf5stego
Version: 1.8.4
Summary: nsF5 steganography tool: syndrome matrix coding (Hamming) + wet-paper coding + image-hash keying + blind steganalysis + GUI
Author-email: nsf5stego <dev@example.com>
License: Apache-2.0
Project-URL: Homepage, https://github.com/Yukinoshita-lin/nsf5-steganography
Project-URL: Repository, https://github.com/Yukinoshita-lin/nsf5-steganography
Keywords: steganography,nsf5,f5,hamming,steganalysis,image
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Security :: Cryptography
Classifier: Topic :: Multimedia :: Graphics
Classifier: License :: OSI Approved :: Apache Software License
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
Requires-Dist: numpy
Requires-Dist: Pillow
Requires-Dist: matplotlib
Requires-Dist: joblib
Requires-Dist: lightgbm
Requires-Dist: scikit-learn<2,>=1.5
Requires-Dist: pandas
Requires-Dist: xgboost
Provides-Extra: gpu
Requires-Dist: torch; extra == "gpu"
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Requires-Dist: build; extra == "dev"
Provides-Extra: notebooks
Requires-Dist: jupyter; extra == "notebooks"
Requires-Dist: ipykernel; extra == "notebooks"
Provides-Extra: experiments
Requires-Dist: seaborn; extra == "experiments"
Requires-Dist: lightgbm; extra == "experiments"
Requires-Dist: torch; extra == "experiments"
Requires-Dist: pandas; extra == "experiments"
Requires-Dist: scikit-learn; extra == "experiments"
Dynamic: license-file

# nsF5 图像隐写工具 (Steganography)

![CI](https://github.com/Yukinoshita-lin/nsf5-steganography/actions/workflows/ci.yml/badge.svg)
![version](https://img.shields.io/badge/version-1.8.4-blue)
![license](https://img.shields.io/badge/license-Apache_2.0-blue)
![python](https://img.shields.io/badge/python-3.9%2B-blue)
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.22543628.svg)](https://doi.org/10.5281/zenodo.22543628)

<!-- TOC:BEGIN 由 scripts/readme_toc.py 生成, 勿手改 -->

## 目录

- [English Overview](#english-overview)
- [互动教学网站](#互动教学网站)
- [功能总览](#功能总览)
- [学习手册](#学习手册)
- [更正记录：曾经出现过的错误](#更正记录曾经出现过的错误)
- [安装与运行](#安装与运行)
- [GUI 使用流程](#gui-使用流程)
- [目录结构](#目录结构)
- [技术细节](#技术细节)
- [有监督 ML 隐写分析（C++ 特征提取 + 校园照片训练）](#有监督-ml-隐写分析c-特征提取--校园照片训练)
- [GPU 版 (v1.2)：PyTorch 批量向量化的统计特征分析](#gpu-版-v12pytorch-批量向量化的统计特征分析)
- [持续集成 & 发版](#持续集成--发版)
- [版本历史](#版本历史)
- [许可](#许可)

<!-- TOC:END -->

## English Overview

**nsF5 Steganography** is an open-source teaching and research toolkit for image
steganography and steganalysis. It implements the classic nsF5 algorithm -
Hamming syndrome-matrix coding combined with wet paper coding - plus blind
steganalysis (chi-square and RS), SHA-256 content keying, and supervised
machine-learning detection with two deployable LightGBM models.

The project is pure Python at its core and runs on **Windows, Linux, macOS, and
Colab**. Optional C++ accelerators speed up feature extraction, embedding, and
shuffling; build them with `make cpp` (or `python scripts/build_cpp.py`). They
are never shipped as binaries - the same sources build a `.dll`, `.so` or
`.dylib` as appropriate - and when they are absent the code falls back to
equivalent pure-Python implementations, so notebooks, Docker, and cloud
environments work out of the box. (Feature extraction is bit-identical between
the two; the nsF5 embedder is interoperable but not pixel-identical -- see the
note in the Chinese section.)

### Highlights

- **Embedding / decoding** - ASCII messages hidden in 8-bit grayscale or color
  images with password keying and self-synchronizing SHA-256 content hashing;
- **nsF5 core** - binary Hamming codes `[n=2^p-1, k, 3]` with syndrome matrix
  embedding, F5-style magnitude decrease, and wet paper coding (no shrinkage,
  no retries);
- **Blind steganalysis** - Westfeld chi-square and Fridrich RS analysis with a
  content-aware verdict and three sensitivity modes;
- **ML steganalysis** - 11-D statistical features (v1) and 143-D v2 features
  (SRM residuals + prefix chi-square statistics), trained with photo-grouped
  cross-validation; ships both a **143-D robust model** and a **53-D
  interpretable model**;
- **Cross-platform** - pure-Python fallbacks for features, embedding, and
  model inference; CI verifies Ubuntu, macOS, and Windows on every change;
- **Teaching-first** - GUI matrix-coding animation, one-click Colab/Jupyter
  notebooks per chapter, dataset downloader, Docker/JupyterLab image, and a
  bilingual web handbook.

### Quick Start (Linux / macOS / Windows)

```bash
git clone https://github.com/Yukinoshita-lin/nsf5-steganography.git
cd nsf5-steganography
python3 -m venv .venv
source .venv/bin/activate            # Windows: .venv\Scripts\activate
python -m pip install -e .

python src/test_core.py              # algorithm self-tests
python src/test_steg.py              # steganalysis self-tests
python src/run_e2e.py                # full embed -> decode -> analyze demo
nsf5stego embed cover.png -m "msg"   # CLI: embed / extract / analyze / gui
python src/gui.py                    # GUI (requires tkinter)
```

Or use the bundled `Makefile`: `make install`, `make test`, `make e2e`,
`make notebooks`, `make dataset`, `make docker`.

> **Windows installer**: no Python needed — download
> `nsf5stego-setup-<version>.exe` from the
> [Releases](https://github.com/Yukinoshita-lin/nsf5-steganography/releases/latest)
> page (Start-menu shortcut, optional PATH entry, Chinese wizard).
> See the Chinese section "方式 0：Windows 安装版".

### Learning Resources

- English handbook (PDF):
  [Learning-Handbook-From-Zero-to-nsF5-Steganography.pdf](docs/Learning-Handbook-From-Zero-to-nsF5-Steganography.pdf)
- Chinese handbook (PDF):
  [学习手册-从零读懂nsF5隐写项目.pdf](docs/学习手册-从零读懂nsF5隐写项目.pdf)
- Online bilingual handbook:
  [Interactive learning lab](https://yukinoshita-lin.github.io/nsf5-steganography/) ·
  [zh](https://yukinoshita-lin.github.io/nsf5-steganography/zh/content/intro.html) ·
  [en](https://yukinoshita-lin.github.io/nsf5-steganography/en/content/intro.html)
- Per-chapter Colab/Jupyter notebooks and Docker instructions:
  [teaching/README.md](teaching/README.md)
- Dataset downloader (BOSSbase 1.01):
  `python scripts/download_datasets.py --out data/BOSSbase_1.01`

### License

Apache-2.0 - see [LICENSE](LICENSE) and [NOTICE](NOTICE).

---

针对 **8bit 灰度/彩色图像** 的隐写研究工具，实现了基于**伴随式矩阵编码（二元汉明码）** 的
nsF5 隐写算法，并附带**盲隐写分析**、**图像哈希键控**与**码族/嵌入效率可视化**。

项目核心为纯 Python（依赖 `numpy`/`Pillow`，GUI 使用标准库 `tkinter`），可在
**Windows / Linux / macOS / Colab** 直接运行。另提供**可选的 C++ 加速库**
（`cpp/fsfeatures.cpp` 特征提取、`cpp/nsf5embed.cpp` 嵌入热路径 + 确定性置乱
`nsf5_permute`）：三平台同一份源码，用 `make cpp` 编译成 `.dll`/`.so`/`.dylib`
（**不入库**，见 `scripts/build_cpp.py`）。库缺失时自动回退到同算法纯 Python
实现：嵌入/解码仍可逆、11-D/143-D 特征与双版本 ML 模型均可使用。

## 互动教学网站

GitHub Pages 首页已升级为**交互式双语教学网站**（不依赖手册即可动手理解项目）：

- 🔗 主站: https://yukinoshita-lin.github.io/nsf5-steganography/
- 🌐 语言切换: 页面右上角一键中英切换，或使用 `?lang=zh` / `?lang=en`
- 🧪 交互实验: LSB 位平面（可抽出单层观察 + 按权重叠加还原）/ LSB 嵌入→解码闭环
  + 卡方/RS 实时自检 + 改动像素掩码 / 汉明伴随式找位 / 湿纸干点求解(自动演示) /
  **nsF5 全流程对比**（朴素 LSB vs 项目真实 nsF5Pixel：矩阵编码 + 减幅修改 + 湿点避让，
  改动统计 / PSNR / 卡方·RS 指纹一图对比，含解码往返与固定种子置乱路径）/
  ML 阈值判别 / 载荷扫描（真实项目统计）/ 10 题双语自测计分
- 🗺 教学层次: 首页"本页导航"把整页分成动手实验/原理/学习路径/FAQ+自测 四部分，
  每节带部分徽章，末尾附双语 FAQ 手风琴答疑
- 🖼 可视化: 16 张教学示意图，覆盖 cover↔stego、位平面分层加权叠加、统计直方图、
  湿纸、效率曲线、ROC 与双模型对比
- 📚 原手册仍保留: [`/zh`](https://yukinoshita-lin.github.io/nsf5-steganography/zh/content/intro.html)
  与 [`/en`](https://yukinoshita-lin.github.io/nsf5-steganography/en/content/intro.html)
- 💻 本地体验: 仓库根目录 `make webapp`（即 `python -m http.server 8080 --directory webapp`）
  后打开 <http://127.0.0.1:8080>。直接双击 `webapp/index.html` 时 `file://` 下
  fetch 本地 JSON 会被浏览器拦, 载荷扫描实验拿不到数据 —— 所以要起 http 服务
- 🧪 自动化回归: `webapp/tests/`（DOM 冒烟 + 交互 + 桌面/移动布局），
  由 `.github/workflows/webapp-tests.yml` 在每次改动 `webapp/**` 时执行

---

## 功能总览

| 模块 | 说明 |
|------|------|
| **嵌入 / 解码** | 将 ASCII 字符串嵌入图像 LSB，解码还原；支持口令键控 |
| **伴随式矩阵编码** | nsF5 + F5 / LSB 矩阵编码，二元汉明码 `[n=2^p-1, k, 3]`，块内至多改 1 系数 |
| **湿纸编码** | nsF5 核心：预标记"减幅归零=湿"位置，在干位解 GF(2) 线性方程，无收缩 |
| **图像哈希键控** | 载入时计算 SHA-256；隐藏路径由"内容哈希+口令"唯一决定，解码端自同步并感知篡改 |
| **盲隐写分析** | 卡方检验(Westfeld) + RS 分析(Fridrich)，输出 0–1 隐写倾向概率与判读 |
| **ML 隐写分类器(双版本)** | 143d 稳健版(默认) + 53d 可解释版,详见下文"双版本部署策略" |
| **绘图** | 绘制码族(嵌入率 α vs 载荷)理论曲线 与 实测嵌入效率对比 |
| **GUI** | 载入图 → 嵌入/解码 → 分析 → 绘图 一体化界面 |

## 学习手册

项目提供**中英文双语学习手册**，从零基础开始，12 周快速入门；想深入可预留 6–12 个月（见手册附录 F 的完整路线）:

- 🖥 网页版: [中文](https://yukinoshita-lin.github.io/nsf5-steganography/zh/content/intro.html) · [English](https://yukinoshita-lin.github.io/nsf5-steganography/en/content/intro.html)
- 🇨🇳 [`docs/学习手册-从零读懂nsF5隐写项目.pdf`](docs/学习手册-从零读懂nsF5隐写项目.pdf) — 中文版, 67 页
- 🇬🇧 [`docs/Learning-Handbook-From-Zero-to-nsF5-Steganography.pdf`](docs/Learning-Handbook-From-Zero-to-nsF5-Steganography.pdf) — English, 76 pages
- 📓 按章 Colab/Jupyter Notebook: 见 [`teaching/README.md`](teaching/README.md)
- 🐳 Docker/JupyterLab 教学镜像: `docker compose up --build`

**姊妹项目**：[`yccstego`](https://github.com/Yukinoshita-lin/yccstego)（`pip install yccstego`）——
把 nsF5 搬到 JPEG 量化 DCT 系数（Y 通道）上的压缩域实现，含自写的 DCT/Huffman 编解码。
它**不在本仓库内**（独立仓库与 PyPI 包），是第 11 章与附录 F 推荐的下一步方向；手册里
提到它的地方都给出了地址。

涵盖: 数字图像基础 → Python 入门 → LSB 隐写 → 卡方/RS 分析 → 汉明矩阵编码 → F5/nsF5 → 湿纸编码 → 哈希键控 → 机器学习基础 → v1/v2 特征工程 → SRM 滤波 → 143d/53d 双版本模型 → C++/GPU 加速 → 综合实验。每章配有"动手做"实验与"想一想"思考题, 适合本科毕设自学。

### 模型双版本(2026-09-06)

项目保留两套训练好的 LGB 模型,默认加载 **143d 稳健版**,可切换到 **53d 可解释版**:

```python
from src.ml_predict import MLPredictor

# 默认 143d (稳健, 部署推荐)
pred = MLPredictor()  # models/stego_classifier.joblib

# 切换 53d 可解释版 (AUC 略低, 但每一维都能解释; 教学/答辩推荐)
pred = MLPredictor(model_path='models/stego_classifier_v2_jpeg_lgb_51d.joblib',
                   clip_outliers=False)

# 预测
r = pred.predict(image)
# r = {'probability': 0.83, 'verdict': '含密(stego)', 'threshold': 0.168}
```

#### 指标口径（重要）：头条只引用 BOSSbase

同一个 143d 模型在 BOSSbase 上是 0.8062，在自建校园语料上是 0.8939 —— 差值反映
的是**语料**（难度、负样本构成都不同），不是模型强弱。对外引用、论文对比一律用
下面主表的 BOSSbase 数字：

| 语料 | 模型 | AUC | 协议 | 说明 |
|---|---|---|---|---|
| **BOSSbase 1.01**（领域标准基准，**头条口径**） | LGB-143d | **0.8062** [0.7894, 0.8225] | 按源图 holdout | 对外一律引用这一行 |
| | LGB-53d | 0.7172 [0.6943, 0.7406] | 同上 | 同一份划分，可与 CNN 对比 |
| | LGB-11d | 0.7128 [0.6913, 0.7360] | 同上 | 11 维基线 |
| 校园照片（自建语料，**附表**） | 143d 部署模型 | 0.8939；8-split 均值 0.8980 | 按源图 holdout | 含 414 张真实 JPEG 干净图，与上表不可并列 |
| | 53d 部署模型 | 0.8391；8-split 均值 0.8461 | 同上 | 同上 |

> ⚠ **2026-09-14 审计更正**：校园语料此前的数字（0.8946 / 0.9100，8-split
> 0.9085 / 0.9227）含**源图泄漏** —— 414 个 `clean_jpeg` 行被赋予了独立
> photo_id，使"按源图划分"失效；同一批 GPU 产出的 SRM 特征尺度也与推理端
> 不一致。两处均已修复并重跑，详见 [CHANGELOG](CHANGELOG.md) 1.6.2。
> 修复后 **143d 优于 53d**（此前结论相反：SRM 90 维在错误尺度下才是"噪声"）。

完整权威表（逐行标注语料 / 协议 / 是否可溯源）见 **[`docs/RESULTS.md`](docs/RESULTS.md)**，
由 `experiments/build_results_table.py` 生成；README 内任何口径冲突都以它为准。

| 版本 | 文件 | 校园语料 AUC (8-split 均值) | 弱档 nsF5 p3 d=0.25 | 推荐场景 |
|---|---|---|---|---|
| **143d 默认** | `stego_classifier.joblib` | **0.8980** | **50.0%** | 通用部署 / 真实图 |
| **53d 可解释** | `stego_classifier_v2_jpeg_lgb_51d.joblib` | 0.8461 | 41.4% | 论文 / 答辩 / 教学（逐维可解释） |

> 本表是**校园照片**语料（自建，含真实 JPEG 干净图）上的部署指标；与 BOSSbase
> 的数字分属不同语料，不能并列。
>
> **OOD（真实干净照片）误报率**（2026-09-14 重建，1514 张 = 校园 414 + DIV2K 100 +
> ALASKA#2 1000，可追到 `experiments/data/ood_summary.csv`）：
> **143d 9.58%** [8.20, 11.16]、**53d 28.86%** [26.64, 31.20]。
> 旧的"1/8 / 3/8"只有 8 张样本、且生产者已丢失，已作废。
> 详见下文的 **双版本部署策略** 与 [`docs/RESULTS.md`](docs/RESULTS.md) 第 5 节。

## 更正记录：曾经出现过的错误

这一节列的是**本项目真实犯过、并且已经修复**的错误。写在这里的动机很直接：隐写分析
项目如果连自己的评测数字都不可信，它的教学价值就是负的。逐条细节见
[`CHANGELOG.md`](CHANGELOG.md) 的 1.6.1–1.6.4，权威数字见
[`docs/RESULTS.md`](docs/RESULTS.md)。

| # | 曾经的错误 | 影响（实测） | 现状 |
|---|---|---|---|
| 1 | **源图泄漏**：414 个 `clean_jpeg` 行被赋了独立 photo_id，而其特征与对应 `clean` 行逐位相同（是副本） | "按源图划分"名存实亡 —— 同一张源图可以跨训练/验证两侧。单独修分组后 143d held-out AUC **0.8946 → 0.7555**、弱档检出 **85.3% → 44.2%** | 已修：`experiments/add_jpeg_clean.py` 让 photo_id 继承源图；`train_deploy_models.py` 开训前强制校验分组不变量 |
| 2 | **SRM 特征尺度不一致**：GPU 端在高通滤波前把像素 `/255`，残差缩小 255 倍、`clip` 形同虚设 | 语料用 GPU 特征、单图推理用 CPU 特征，两者相差约 **30 倍**，143d 模型一直吃分布外输入。修好后 BOSSbase 143d **0.7529 → 0.8062** | 已修：CPU/GPU 143 维特征逐项一致（max\|Δ\|≈3e-5），并有回归测试与 CI job |
| 3 | **C++ 与 Python 特征不一致，且被自检掩盖**：卡方自由度用了 `n` 而非 `n−1`（p 值差 **26%**）、20 段中位数取"上中位"；自检把它误诊成"MinGW lgamma 精度偏移"，容差放宽到 **0.2** | Windows（带 DLL）与 Linux（纯 Python）对同一张图给出不同的 `chi2_pvalue` / `median_prefix_p` | 已修：11 维逐项一致（max\|d\|≈6e-14），自检容差收回 **1e-9** |
| 4 | **结论建立在错误数据上**：曾写"53d 精简版 AUC 更高""SRM 90 维是噪声特征、去掉反而更好" | 该结论完全来自第 1、2 条缺陷 | 已推翻并重跑：**143d 更准**（8-split 0.8980 vs 0.8461）；SRM 占 LightGBM gain **52.6%**，去掉它 OOF AUC 掉 0.05 |
| 5 | **OOD 数字只有 8 张样本**（"1/8、3/8"），且产生它的脚本与数据一起丢失 | 无法复核，也没有统计意义 | 已重建 `experiments/ood_eval.py`：**1514 张**真实干净照片，143d **9.58%**、53d **28.86%**（Wilson 95% CI） |
| 6 | **同一指标名跨语料/跨协议混用**："8-split" 指过两种协议；校园语料数字与 BOSSbase 数字被并列比较 | 读者会看到互相矛盾的数值 | 已修：`docs/RESULTS.md` 把**语料**与**协议**做成每行必填字段，并规定 README 头条只引用 BOSSbase |
| 7 | **多处数字只打印不落盘**（GPU 管线 0.790/0.644/0.712、train_model 家族、gain importance） | 不可溯源，无法复核 | 已修：四个生产者补齐并重跑，不可溯源行 **9 → 0**（0.7903 / 0.6438 / 0.8889 等逐位复现） |
| 8 | **部署模型不可复现**：仓库里没有能产出两个随包 `.joblib` 的脚本 | clone 之后无法重建模型，"可用但不可复现" | 已补 `experiments/train_deploy_models.py`，并在全部 5796 个样本上验证其产出与随包模型预测一致 |
| 9 | **教学手册带着已被推翻的结论**（DOCX、网页版、PDF 三处都有） | 教学材料带错结论比没有结论更糟 | 已修：`teaching/handbook_facts.py` 统一口径并纳入 CI；PDF 按新口径重新导出（中文 67 页 / 英文 74 页） |
| 10 | **教学 notebook 从未被执行过** | 03 号让学员嵌入 5000 字符，而封面图容量只有 3494 字节 —— 这个 cell 一直在抛 `ValueError` | 已修并进 CI：10/10 逐本执行通过，另有"入库 notebook 与生成器一致"的漂移检查 |
| 11 | **LICENSE 缺 APPENDIX 段**，结尾被换成自定义版权块 | GitHub 把 Apache-2.0 识别成 `NOASSERTION`，与徽章不符 | 已恢复标准 Apache-2.0 全文 |
| 12 | **若干使用即踩的缺陷**：`python src/run_e2e.py` 在中文 Windows 控制台崩溃（`✓` 无法用 GBK 编码）；`make_dataset` 打印的样本数恒比真实值多 1；`train_model` 遇到空环境变量直接崩溃；README 引用过从未存在的 `src/_add_jpeg_clean.py`；wheel 安装示例版本过期 | 使用者直接踩到 | 均已修复，并新增控制台编码护栏测试 |
| 13 | **wheel 里没有模型文件**：两个部署模型只在仓库里，`pyproject` 没有把它们打进包，而 `ml_predict` 也只按仓库布局找路径 | `pip install nsf5stego` 之后依赖里装着 lightgbm、README 写着有 ML 判定，但 `MLPredictor.available` **恒为 False**（真实验证：安装布局下 `FileNotFoundError`） | 已修（2026-09-15）：`models/` 以 `nsf5_models` 包打进 wheel，`ml_predict` 按"仓库布局 → 安装布局"查找；CI 的 build 作业在**干净 venv** 里断言模型可用并能打出概率 |
| 14 | **事实校验器把一处陈旧引用放过去了**：图 9-2 的图注写着"论文图 / thesis figure"，而论文稿 2026-09-14 已删除；`FORBIDDEN` 用的是 `"（论文图"` / `"(thesis figure"`（左括号紧贴），实际文本是 `"（log–log 坐标，论文图）"`，子串匹配被绕过 —— 于是这句话同时留在 DOCX、网页版和**入库 PDF** 里 | 教学材料指向不存在的文件；而"进了 CI 就不会再犯"的假设也因此不成立 | 已修：匹配放宽到 `"论文图"` / `"thesis figure"`（另禁 `"project thesis"`，注意不能用裸 `"thesis"` —— 会命中 `"hypothesis"`），三处共 6 处修正并重新导出 PDF（67 / 74 页）；PDF 的导出步骤也补成了脚本 `teaching/export_handbook_pdf_word.py`（`make handbook-pdf`） |
| 15 | **手册把姊妹项目说成本项目的一部分**：中英文 ch03 / ch11 / 附录 F 都写"项目 `yccstego` 扩展"，而 `yccstego` 是独立仓库与 PyPI 包（`pip install yccstego`），本仓库里没有它的代码、也没有任何链接 | 读者按手册去找，什么也找不到 —— 与第 14 条同类：教学材料指向不存在的东西 | 已修（1.7.1）：中英各 5 处改成"姊妹项目 `yccstego`"并给出仓库地址，README 增"姊妹项目"一行，PDF 重新导出（英文 75 页）；`handbook_facts.py` 的 `REQUIRED` 加上该地址，三份材料缺它就红 |

> **为什么保留这些记录，而不是悄悄把数字改掉：** 第 1 条和第 3 条恰好是"评测设计本身
> 出错"的两个典型样本 —— 前者说明"按源图分组"这种纪律会在 id 分配这种细节上悄悄失效，
> 后者说明一个被误诊的容差可以把两种实现的差异藏住很久。它们现在是教学材料的一部分
> （见第 7、8 章与 `docs/RESULTS.md` 第 1 节）。

### 提交署名的一次清理（2026-09-14）

用 AI 编程助手协作时，它会在提交信息里自动追加
`Co-Authored-By: Claude Code <noreply@anthropic.com>` 这类尾注。GitHub 会把它当成
**共同作者**显示在提交流里 —— 与"贡献者列表"不同（本项目贡献者 API 一直只有仓库
所有者），但同样显眼。



```bash
git bundle create .git/backup.bundle --all        # 先全量备份
python scripts/strip_ai_trailers.py < msg > new   # 或直接用下面的 msg-filter
git filter-branch -f --msg-filter \
  'python scripts/strip_ai_trailers.py' -- <起点>^..HEAD
git push --force-with-lease origin main
```

以后不会再发生：`.githooks/commit-msg` 会在提交时当场剔除这类尾注
（启用一次：`make hooks`，即 `git config core.hooksPath .githooks`），CI 的
`attribution` job 则对整段历史兜底检查。

---

## 安装与运行

### 方式 0：Windows 安装版（推荐普通用户）

到 [Releases](https://github.com/Yukinoshita-lin/nsf5-steganography/releases/latest)
下载 `nsf5stego-setup-<版本>.exe` 双击安装（简体中文向导，每用户免管理员），
装完后像普通桌面软件一样使用，**不需要 Python**：

- 开始菜单（可选桌面快捷方式）→ **nsF5 隐写工具**，双击即开图形界面；
- 向导里勾选"加入用户 PATH"后，`nsf5stego embed / extract / analyze` 命令行
  直接可用（重开终端生效），卸载时自动从 PATH 移除；
- 产物（含密图 / 效率图）写在 `%APPDATA%\nsf5stego\output`，不污染安装目录；
- 免安装选择：同一 Release 的 `nsf5stego-portable-<版本>-win64.zip` 解压即用；
- 首次运行若遇 SmartScreen 提示，点"仍要运行"（项目未做代码签名）。

### 方式 A：从源码运行（Windows / Linux / macOS 通用）

```bash
git clone https://github.com/Yukinoshita-lin/nsf5-steganography.git
cd nsf5-steganography
python3 -m venv .venv
source .venv/bin/activate          # Linux / macOS
# Windows: .venv\Scripts\activate
python -m pip install -e .         # 或只装 numpy pillow
nsf5stego --help                   # 命令行界面（embed / extract / analyze / gui）
python src/gui.py                  # 图形界面（需要 tkinter）
```

> **跨平台说明**：无需任何预编译二进制——`fsfeatures`、`cppembed`、
> `featurize_v2` 与 `ml_predict` 在检测不到 `cpp/` 下的库时自动使用纯 Python
> 实现；想要加速就 `make cpp`（需要 g++/clang++）。仅 GUI 需要系统自带
> tkinter（Ubuntu/Debian：`sudo apt install python3-tk`）。
> 也可用仓库根目录的 `Makefile`：`make test`、`make cpp`、`make e2e`、`make notebooks`。

### 方式 B：安装打包的模块（wheel）

每个版本会以源码包发布，可构建并安装：

```bash
# 构建 wheel + sdist（需已安装 build）
python -m build

# 安装 wheel（核心模块：ns5_core / steganalysis / gui 等；文件名带版本号）
pip install dist/nsf5stego-<版本>-py3-none-any.whl
```

> 注意：wheel 仅含纯 Python 核心；C++ 加速库需自行 `make cpp` 编译，缺失时
> 自动回退纯 Python。两个部署模型（`models/*.joblib`）**已打进 wheel**
（安装为 `nsf5_models/`，含模型卡），`ml_predict` 会按"仓库布局 → 安装布局"
> 的顺序查找，所以 `pip install` 之后 ML 判定直接可用：
>
> ```python
> from ml_predict import MLPredictor
> p = MLPredictor()
> assert p.available, p.load_error      # 缺 lightgbm/pickle 版本不符时这里会说明原因
> print(p.predict(img))                  # {probability, verdict, threshold}
> ```
>
> （2026-09-15 修正：此前 wheel 里没有模型文件，安装后 `available` 恒为 False。）

### 命令行界面（1.8.0 起）

安装后除 GUI 外还有一条与 GUI 参数一一对应的命令行（服务器 / 脚本 / 批量
场景不再需要自己拼 `ns5_core` 调用）：

```bash
nsf5stego embed cover.png -m "秘密文本" -p 口令 -o stego.png
cat msg.txt | nsf5stego embed cover.png            # 文本也可从 stdin 传入
nsf5stego extract stego.png -p 口令                # 方法/p/口令 须与嵌入一致
nsf5stego analyze stego.png --sensitivity 宽松 --json   # 盲分析, --json 供脚本解析
nsf5stego analyze *.png                            # 批量: 逐图一行汇总
nsf5stego gui                                      # 图形界面（与 python src/gui.py 相同）
```

- **退出码**：成功 `0`；运行失败（容量超限 / 口令不匹配 / 图像无法读取）`1`；
  参数错误 `2` —— 脚本可直接判断成败。
- **解码失败不输出乱码**：口令或 方法/p 不匹配时解出的会是无效文本，CLI 统一
  以非零码退出并提示"请确认 方法/p/口令 与嵌入时一致"，而不是把替换字符打印出来。
- **analyze 批量语义**：单图 `--json` 输出对象，多图输出对象数组（每项带
  `image` 键）；批量里某张图读不出来会记为 `error` 条目继续跑完，整体以
  非零码退出。通配符（如 `*.png`）由 CLI 自己展开 —— Windows 的 shell 不
  展开，这条示例在三大平台都能直接用。单图行为与 1.8.0 初版完全兼容。
- 默认输出 `<原名>_stego.png`；目标文件已存在时会在 stderr 明示覆盖。
- 源码运行的等价形式：`python src/cli.py <子命令> ...`。
- **GUI 产物目录**：源码运行写 `仓库/output/`；`pip install` 安装后 GUI/效率图
  自动改写**当前工作目录**的 `output/`（不会写进 site-packages 或解释器目录）。

### 运行测试

```bash
python src/test_core.py    # 核心算法自测（嵌入/解码 + 汉明矩阵 + 湿纸 + 口令）
python src/test_steg.py    # 盲隐写分析自测（区分 干净/含密 图）
python src/run_e2e.py      # 端到端验证（嵌入→保存→解码→分析→绘图）
python src/test_gui.py     # GUI 冒烟测试（构建窗口/载入/预览）
# 全套（模型卡契约 / OOD 与实验链冒烟 / README 目录 / 权威表同步；条数见 CI 日志）
python -m pytest -q
make coverage              # 同上 + 覆盖率报告（门槛 65%，当前约 69%）
```

---

## GUI 使用流程

1. 点击 **载入原始图 / 含密图** 选择 8bit 图像（或点 **演示图** 一键生成测试封面）。
2. 在文本框输入待嵌入的 **文本**（UTF-8，支持中英文）。
3. 在 **参数** 区选择 **算法**（`nsF5` 或 `matrix`）、**参数 p**（块比特数，越大效率越高）、可选 **口令**。
4. 点击 **嵌入并保存**（Ctrl+E）→ 生成 `output/stego_*.png`，右侧预览含密图。
5. 点击 **解码提取**（Ctrl+D）→ 从含密图还原字符串（须与嵌入使用相同 算法/p/口令）。
6. 点击 **分析**（Ctrl+A）→ 「分析结果」页签显示 SHA256、卡方统计、RS 缺口、估计嵌入率、隐写概率与 ML 判定；过程细节在「运行日志」页签。
7. 辅助工具：**生成效率图**（理论 vs 实测效率对比）、**编码演示**（伴随式校验动画）、**载荷扫描**（检测能力随载荷变化曲线）。

> 解码与嵌入参数（方法/p/口令）必须一致；口令或图像内容不匹配将无法正确解码。

---

## 目录结构

```
nsf5-steganography/
├── README.md / CHANGELOG.md / LICENSE / NOTICE
├── Makefile                 # install/test/e2e/notebooks/docker/web 快捷命令
├── docker-compose.yml
├── cpp/                     # 可选 C++ 加速库源码（make cpp 编译，缺失时纯 Python 回退）
├── data/                    # 数据集 CSV 与 campus_jpg/BOSSbase 目录
├── docker/Dockerfile        # Linux JupyterLab 教学镜像
├── gpu/                     # PyTorch 批量特征 / GPU 训练
├── img/                     # 示例封面图
├── models/                  # 训练出的分类器 joblib
├── notebooks/               # 10 个按章 Colab/Jupyter Notebook
├── scripts/                 # 数据集下载、网页资源生成等脚本
├── src/
│   ├── ns5_core.py          # nsF5 核心：汉明码、湿纸、哈希键控、嵌入/解码
│   ├── cppembed.py          # C++ 嵌入封装（含纯 Python 回退）
│   ├── py_features.py       # 纯 Python 11 维特征（跨平台）
│   ├── featurize_v2.py      # v2 143 维特征
│   ├── srm_filter.py        # SRM 高通滤波（numpy/torch）
│   ├── fsfeatures.py        # C++ 特征绑定（DLL 缺失回退）
│   ├── steganalysis.py      # 卡方 + RS 盲隐写分析
│   ├── ml_predict.py        # 143d/53d 双版本模型推理
│   ├── make_dataset.py / train_model.py
│   ├── gui.py / efficiency.py / image_io.py
│   └── test_*.py            # 回归测试
├── teaching/                # 教学文档、Notebook 生成器、Jupyter Book 站点
├── webapp/                  # 交互式双语教学网站（Pages 首页）
└── output/                  # 运行产物（gitignore）
```

---

## 技术细节

### 伴随式矩阵编码（nsF5）

二元汉明码 `[n,k,d]`，`n = 2^p - 1`，校验矩阵 **H** 的列向量取 GF(2)^p 全部非零向量。
载体系数 LSB 奇偶向量 `x` 的伴随式 `s = H·x (mod 2)`。

- 嵌入 `p` 比特消息 `m`：若 `s == m` 不改动；否则 `d = s ⊕ m`，
  找到唯一列 `j`（`H_j == d`）翻转该系数 → **每块至多改 1 个系数**。
- 需要改动的概率 `(2^p-1)/2^p`，嵌入效率 `α(p) = p·2^p / (2^p-1)`，随 p 增大而提高。

### F5 → nsF5

- **F5**：直流/减幅归零时"收缩"，该块整块重嵌、载荷下降。
- **nsF5**：用**湿纸编码**预标记"减幅会归零 = 湿"的位置，湿位不动，
  在**干位**解 GF(2) 线性方程完成嵌入 → **无收缩**，效率与安全性更高。

### 图像哈希键控

载入原图计算 SHA-256（`cover_hash`）：

- 头部区：用仅口令派生的种子预埋 `cover_hash`（认证头）；
- 正文区：用 `cover_hash + 口令` 派生的种子键控置乱路径。

解码端先用口令种子读回头部，重算正文种子解码 → 隐藏路径由图像内容唯一决定，
改动任意像素会破坏解码结构，可经头部校验感知篡改。

### 盲隐写分析

- **卡方检验（Westfeld）**：相邻灰度对 `(2i,2i+1)` 频率在嵌密后趋近均衡，
  p 值高表示该区已随机化/嵌入。
- **RS 分析（Fridrich-Goljan-Du）**：统计正/负掩码的常规-奇异缺口
  `Gr`、`Gn`；干净图 LSB 平面有结构（Gn 明显为正），隐写使其随机化而下降。
- 综合多个统计量给出 **0–1 隐写倾向概率** 与判读（不太可能 / 可能 / 高度可能）。

> 盲隐写分析本质为启发式：没有原始封面时无法给出精确绝对概率，
> 此处概率供评估与教学参考。

---

## 有监督 ML 隐写分析（C++ 特征提取 + 校园照片训练）

将**特征提取**从 Python 移植到 **C++**（`cpp/fsfeatures.cpp`），
可显著降低逐图统计开销；嵌入热路径同样提供 **C++ 版**（`cpp/nsf5embed.cpp`，
`cppembed.py` 封装）。
并用真实校园照片做**有监督训练**，得到一个可部署的分类器。

> **关于"两条路径是否一致"**：特征提取逐位一致（`fsfeatures` 与 `py_features`
> 在 RS/卡方/熵上误差 <1e-6，卡方 p 值因 MinGW 的 `lgamma` 半整数精度有约 0.02
> 的容忍度）。嵌入路径则**不是逐像素一致**：`matrix` 逐像素相同；`nsF5` 在
> p≥3 时会有几十个像素不同（65536 中约 8–32 个），因为湿纸编码在多个等价解中
> 挑选哪一个 —— C++ 按固定下标顺序扫，Python 按种子派生顺序扫。两者都是合法解，
> 各自回环正确，且能互相解码。契约由
> `src/test_pipeline.py::test_cpp_python_embed_contract` 固定。

### 训练管线

```bash
# 1) 用 data/campus_jpg 下照片批量生成 干净/含密 特征数据集
#    (每张降采样 512x512 灰度, 1 干净 + 6 含密变体, C++ 提取 11 维特征 + C++ 嵌入)
python src/make_dataset.py

# 2) 训练/评估 (预留 held-out 测试集 + 5 折 GroupKFold 交叉验证选模)
python src/train_model.py
```

- **特征 (全由 C++ 计算)**：`RS_Gn, RS_Gr, Rm, Sm, Rn, Sn`、
  `chi2_pvalue, chi2_stat`、`diff_entropy、lsb_diff_entropy`、`median_prefix_p`。
- **6 档含密变体**：覆盖弱→强，`nsF5 p3`（弱密度）至 `matrix p2`（强）。
  414 张照片 → 2898 样本（414 干净 + 2484 含密）。
- **5 折 GroupKFold**：按照片分组交叉验证选模（杜绝同源泄漏），
  再在留出测试集上报综合指标与每密度层检出率。

### 在 GUI 中启用

GUI 新增 **判定灵敏度** 下拉框（严格 / 均衡 / 宽松），作用于 **3 分析**：

- **严格 (低误报)**：提高含密判定阈值 → 干净图更少被误判；
- **均衡**：默认。
- **宽松 (高检出)**：下调阈值 → 更易检出弱密度嵌入（代价是误报略升）。

它与下方 **ML 分类含密概率** 联动（同一张净图在三种灵敏度下阈值
0.95→0.77→0.57，ML 判决会由"干净"切换到"含密"），启发式概率也会围绕
0.5 上下牵引。GUI **3 分析**会在原有启发式结果下方追加一行 **ML 分类含密概率**，
输入图像会自动按训练一致的方式（转灰度→512 缩放→特征提取）送入模型。

**模型从哪来（重要）**：两个部署模型 `models/stego_classifier.joblib`（143d 默认）
与 `models/stego_classifier_v2_jpeg_lgb_51d.joblib`（53d 可解释）**已随仓库分发**，
clone 之后 ML 判定即可用（需要 `lightgbm`，已列为依赖）。

**模型可复现**：`experiments/train_deploy_models.py` 就是这两个文件的**生产者**，
口径已冻结：

- 语料 `data/dataset_campus_v2_jpeg.csv`（414 张校园照片 × 14 = 5796 样本）
- 按**源图**划分 `GroupShuffleSplit(test_size=0.25, random_state=seed)`
- LightGBM `learning_rate=0.03, num_leaves=31, n_estimators=800,
  min_child_samples=10, subsample=0.9, colsample_bytree=0.8`

复现证据（2026-09-14 审计后重跑）：143d 在 seed=0..7 上的 AUC 为
0.8939 / 0.8998 / 0.8987 / 0.8972 / 0.8892 / 0.9009 / 0.9145 / 0.8901
（均值 **0.8980**），53d 为 0.8391 / 0.8326 / 0.8531 / 0.8589 / 0.8408 / 0.8500 /
0.8615 / 0.8330（均值 **0.8461**）；随仓库分发的两个 `.joblib` 就是该脚本的产物。

生产者会在训练前检验**分组不变量**（每个 photo_id 的 variant 集合必须一致），
语料一旦出现"孤儿 id 块"就直接拒绝运行 —— 这正是 1.6.2 修掉的那类源图泄漏。
语料本身的生成链见 `python experiments/add_jpeg_clean.py`（JPEG 干净行的 photo_id
**继承源图**，且是真实 JPEG 往返而非 clean 行的副本）。

> **可复现性的边界（如实说明）**：校园语料的 414 张照片是作者本人的
> `data/campus_jpg/`（不入库，也无法分发）。因此**两个部署模型的训练语料
> 不能从零重建**——脚本链（`make_dataset.py` → `add_jpeg_clean.py` →
> `train_deploy_models.py`）是完整的，但需要自备同规模的 JPG 照片目录。
> 想完全从零复现，请走 BOSSbase 路线（`scripts/download_datasets.py` +
> `experiments/featurize_bossbase_npz.py` + `experiments/sota_compare.py`），
> 那条链只用公开基准，`docs/RESULTS.md` 的主表就是它。

```bash
python experiments/train_deploy_models.py             # 重训并覆盖 models/*.joblib（约 3 分钟）
python experiments/train_deploy_models.py --no-splits # 只跑 seed 0（约 10 秒）
python experiments/train_deploy_models.py --no-save   # 只评估，不落盘
```

```bash
# 单独用 ML 判定单张图
python -c "import sys; sys.path.insert(0,'src'); from ml_predict import get_predictor; \
import numpy as np,os; from PIL import Image; from ns5_core import embed_string; \
a=np.asarray(Image.open(r'img/cover.png').convert('L').resize((512,512)).convert('L')); \
print(get_predictor().predict(a))"
```

> **局限**：真实 JPEG 照片的 LSB 位平面天然近乎随机，弱密度 nsF5 嵌入的
> 统计足印很弱；ML 概率应与启发式判读**交叉印证**，不宜单独作为铁证。

### SRM 高通滤波预处理层（检测性能提升）

`src/srm_filter.py` 提供 **SRM (Spatial Rich Model) 高通滤波预处理层**，在提取
11 维统计特征**之前**对图像做高通滤波，突出嵌入噪声、抑制图像内容，从而增强
弱密度隐写的统计足印。

- **30 个标准 SRM 核**（Fridrich 体系，与 Ye-Net 所用一致）：一阶差分 8 + 二阶 4 +
  三阶 8 + 边缘 3×3 4 + 边缘 5×5 4 + 方形 3×3/5×5 各 1；按标准因子归一化并补齐到 5×5。
- **合成增强图**：逐像素取 30 个残差的最大绝对响应，clip 到 ±T（默认 4）后平移缩放到
  uint8 [0,255]，输出单通道增强图，直接喂给现有特征器，其余管线不变。
- **CPU (numpy) / GPU (torch conv)** 双实现，接口一致
  （`srm_residuals_np` / `srm_residuals_torch` / `preprocess_batch_torch`）。

接入方式（两条管线均已内置开关）：

```bash
# CPU数据集: --preprocess srm (默认 none=原图基线)
py src/make_dataset.py data/campus_jpg --out campus_srm --preprocess srm -j 16

# GPU特征: extract_features_gpu(x, use_srm=True) (默认开启)
#   自检: py gpu/featurize_gpu.py   (含 SRM 路径冒烟)
```

**实测效果（同一 414 张校园照片，LR，同协议 5 折 GroupKFold + 留出测试）**：

| 指标 | 原图基线 (dataset.csv) | SRM 增强 (dataset_campus_srm.csv) |
|------|:--:|:--:|
| 5 折 CV-AUC | 0.7594 | **0.7922** |
| held-out 测试 AUC | 0.7811 | **0.8085** |
| 低误报点含密检出率 | 41.2% | **50.3%** |
| 弱密度 `nsF5 p3 d0.25` 检出 | 51.9% | **62.5%** |
| `nsF5 p2 d0.35` 检出 | 76.0% | **90.4%** |
| 干净误报率（低误报阈值） | 9.6% | 9.6% |

SRM 在保持低误报不变的同时，将 CV-AUC 提升约 **3.3 个百分点**、测试 AUC 提升约
**2.7 个百分点**，弱密度档检测增益尤其显著，且 SRM 增强图约 0.35s/张、
16 核并行下生成全量数据集不受影响。

> **重要（跨源合并时结论相反）**：SRM 的收益只出现在**同源**内。接入
> BOSSbase 全量做**跨源合并训练**时，SRM 反而显著拉低性能，CPU 与 GPU 两管线
> 相互印证：
>
> | 跨源合并 | CPU (校园+全量BOSSbase, 测试AUC) | GPU (校园+BOSSbase2000源, 验证AUC) |
> |---|---|---|
> | 非 SRM（基线） | **0.741** | **0.651** |
> | SRM | 0.704（−3.7pp） | 0.572（−7.9pp） |
>
> 机理：SRM 高通滤波在抑制图像内容的同时，也把不同相机/JPEG 压缩源之间的可区分
> 信号一并压平——同源时被压掉的是嵌入噪声（收益），跨异构源时被压掉的是跨源可分
> 性（损失）。因此**默认模型采用未 SRM 的合并全量**（`stego_classifier.joblib`，
> 测试 AUC≈0.741），SRM 单源校园模型另存为
> `stego_classifier_campus_srm.joblib`（AUC≈0.8085，仅供校园同源场景）。

### v2 扩展特征（143 维）与多档变体 A/B（2026-09）

在原 11 维之上扩展为 **143 维 v2 特征集**：30 个 SRM 残差的均值 / 绝对均值 / 标准差
（90 维）+ 20 段前缀卡方 p（20 维）+ texture_noise + est_rate（2 维）+ 20 段 LSB 前缀
卡方 p（20 维）。同时把训练变体从 6 档扩展到 **12 档**（追加 `nsF5 p3 d=0.40`、
`matrix p3 d=0.40/0.60`、`lsb d=0.30/0.50/0.70`）。

```bash
# 重新生成 v2 数据集 (单进程 GPU, 414 张 ~7 min)
py src/make_dataset.py --out campus_v2 --feature-set v2 --variants all
py src/make_dataset.py --out campus_v2_min --feature-set v2 --variants minimal  # 6 档对照

# 训练 v2 143d + 4 模型 stacking (5 折 GroupKFold, 留出 25% 测)
DS_FILES="dataset_campus_v2.csv" python src/train_model.py
```

**严格 A/B 对比**（同一测试集照片 ID 分组，5 折 GroupKFold OOF）：

| 数据集 | 特征 | OOF-AUC | 弱档 `nsF5 p3 d=0.25` 检出 | 模型 |
|---|---|---|---|---|
| `dataset.csv` (6 档) | v1 11 维 | 0.7678 | 49% | XGB |
| `dataset_campus_v2.csv` (12 档) | v1 11 维 | 0.7868 | — | LR |
| `dataset_campus_v2.csv` (12 档) | v2 143 维 | **0.8143** | 25% | LR |
| `dataset_campus_v2.csv` (12 档) | v2 143 维 (held-out) | **0.8278** | 25% | LR |

- v2 数据集 12 档 vs 6 档：+0.019（同 11 维）→ 真实信号（多档位学到档位差）。
- v2 143 维 vs v1 11 维：+0.027 → SRM 残差 / 20 段前缀 p 确实贡献判别力。
- 累计 +0.05 AUC（0.78 → 0.83）。

> **部署警告**：v2 模型在 5 折 OOF 与 held-out 上 AUC 显著提升，但**实际部署到真实
> JPEG 干净图时倾向过激**（SRM 残差对 JPEG 高频噪声过于敏感，干净 JPEG 几乎全被
> 判 1.0）。**默认 `stego_classifier.joblib` 仍为 v1 11 维 XGB 模型**（held-out
> AUC=0.7435，干净 JPEG 误判率低）；v2 模型另存为
> `stego_classifier_v2_campus_stack.joblib`（AUC=0.8278，**仅供训练分布内的
> PGM/BMP 灰度图使用**）。

#### 分布外（OOD）分析与缓解尝试

为修复 v2 在真实 JPEG 干净图上的过激（logit 263 vs 训练集 clean mean 1.68），
探索了两种部署抗偏移方案（均不替换默认模型，仅做参考）：

| 方案 | 思路 | 真实 JPEG 干净 prob | 局限 |
|---|---|---|---|
| 原始 v2 | 无防护 | 1.000 | — |
| ① clip-to-±5σ | 把每维特征裁到训练集 μ±5σ | 0.996 | JPEG 干净 logit=5.6 已超训练集 clean p99=3.73 |
| ② OOD-cap | logit 超 clean p99 时封顶概率到 clean p99 prob (0.977) | 0.977 | 仍 ≥ thr 0.912；stego p5=1.30 已与 clean p99=3.73 重叠，单阈值无解 |

**根本原因**：训练集 clean 与 stego 的 logits 严重重叠（clean p99=3.73 vs
stego p5=1.30）。v2 模型在训练分布内已"过激"，JPEG 干净图即使 clip 也回不到训练分布内
位置。

#### ✅ 根本修复：v2 训练集追加真实 JPEG 干净样本（2026-09）

把 `data/campus_jpg/` 414 张真实 JPEG 干净图（campus PGM-derived 来源之外）作为
额外 clean 样本加入 v2 训练集，photo_id 独立区间 `max_id+1000 ~ max_id+1413`，
**与原训练集完全 disjoint**（防 group 泄漏）。重新训练 v2 4 模型 OOF stacking：

```bash
# 自动生成 v2 + JPEG 数据集 (campus_v2.csv + 414 JPEG clean -> campus_v2_jpeg.csv)
python experiments/add_jpeg_clean.py

# 训练 (与 v2 同样的 5 折 GroupKFold + 4 模型 stacking)
DS_FILES="dataset_campus_v2_jpeg.csv" python src/train_model.py
```

**v2 特征可解释性**(2026-09-14 审计后重算):

- 单特征 AUC（校园 v2_jpeg 语料，取 max(AUC, 1-AUC) 的中位数）:
  BASE 11 → 0.610, PREFIX 20 → 0.646, LSB PREFIX 20 → 0.607,
  SRM absmean → 0.512, SRM std → 0.550
- 特征消融（5 折 GroupKFold OOF, 8 seed 均值）:
  B11 0.8708 → 53d 子集 0.8513 → 完整 143d **0.9010**，即 **SRM 90 维贡献 +0.050 AUC**;
  按源图 GroupKFold(8) 上 143d 对 53d 是 **8/8 全胜**
- 结论（**与此前相反**）: SRM 90 维在**尺度正确**时不是噪声。2026-09-06 那版结论
  （"SRM 近随机、去掉反而更好"，gain 占比 SRM 26.6% / LSB PREFIX 42.2% 等）是在
  GPU 端把像素先 `/255` 的错误尺度上算出来的，已作废。
  现已按当前口径重算（`experiments/gain_importance.py`）：**SRM 90 维占 gain 的
  52.6%**，BASE 11 占 20.7%、LSB PREFIX 20 占 20.6%、PREFIX 20 占 6.2%；
  Top-5 特征为 `Rm`(7.6%) / `lsb_prefix_p4`(7.0%) / `srm_absmean_c29`(6.1%) /
  `RS_Gr`(5.5%) / `srm_std_c29`(4.6%)。旧表（SRM 26.6%）已作废。

### 双版本部署策略(2026-09-14 审计后更新)

经过可解释性对照实验,项目保留 **143d 默认版** 与 **53d 可解释版** 两套模型,各自适用场景不同:

> **语料提醒**：本节（以及下方"模型对比""最终决策"）的 AUC 全部来自**校园照片**
> 语料（自建，含 414 张真实 JPEG 干净图）。同一族模型在 BOSSbase 1.01 上是
> 0.8062（143d）/ 0.7172（53d），两者**不可并列**。权威表见
> [`docs/RESULTS.md`](docs/RESULTS.md)，头条口径见上文"指标口径"一节。

| 维度 | **143d 默认版**(`stego_classifier.joblib`) | **53d 可解释版**(`stego_classifier_v2_jpeg_lgb_51d.joblib`) |
|---|---|---|
| 特征构成 | BASE 11 + SRM 90 + PREFIX 20 + LSB PREFIX 20 + TEX/EST 2 | BASE 11 + PREFIX 20 + LSB PREFIX 20 + TEX/EST 2(去 SRM) |
| 8-split 平均 AUC（校园语料） | **0.8980** | 0.8461 |
| Held-out AUC（校园语料, seed=0） | **0.8939** | 0.8391 |
| 弱档检出 nsF5 p3 d=0.25 | **50.0%** | 41.4% |
| 特征消融（5 折 OOF, 8 seed 均值） | **0.9010** | 0.8513 |
| 模型文件大小 | 2.8 MB | 2.8 MB |
| 可解释性 | 一般(143 维,LIME/SHAP 可对单图解释) | **强**(51 维有明确统计定义,可直接列 Top 贡献) |
| 推荐场景 | 通用部署 / 异构数据 / 真实图像 | 论文 / 答辩 / 教学 / 单图分析 |

> **模型卡（2026-09-15）**：两个模型各有一份**入库的 JSON 模型卡** ——
> 语料与协议、held-out / 8-split 指标、逐档检出率、OOD 误报率、特征列表与顺序、
> 超参、训练环境、适用边界与已知局限，以及该 `.joblib` 的 `sha256`。
> 入口：[`models/README.md`](models/README.md)（说明）与
> `models/*.card.json`（机器可读）。
> 校验：`python experiments/model_card.py --check` —— 它检查卡片与二进制是否脱钩，
> CI 里随 `pytest` 一起跑；重训后由 `experiments/train_deploy_models.py` 自动刷新。

**核心结论（审计后）**:
- **143d 在 AUC 与弱档检出上更好**：去掉 SRM 90 维后消融 OOF AUC 从 0.9010 掉到 0.8513
- 53d 的价值在**可解释性**：53 维里 51 维有明确统计含义，可逐维列出贡献；代价是 AUC 低约 0.05
- 此前"53d 全面胜出、143d 靠 OOD 稳"的结论建立在两个缺陷之上（SRM 尺度错误 + 语料源图泄漏），见 1.6.2
- **工程上保留双版本,默认加载 143d**(更准),教学/答辩场景切 53d(可解释)

#### 53d 精简版定位（审计后更正）

- 按源图 GroupKFold(8) 的 OOF:143d **0.8966** vs 53d 0.8470 —— **143d 8/8 全胜**
  (此前记录的是"53d 7/8 胜", 那是错误尺度 + 泄漏语料的产物)
- **可解释优势**:53 维中 51 维有明确统计含义
  - `Rm/Sm/Rn/Sn/RS_Gr/RS_Gn`:RS 分析 6 个规则翻转率
  - `chi2_pvalue/chi2_stat`:LSB 卡方 p 值与统计量
  - `diff_entropy/lsb_diff_entropy`:全局/LSB 位平面熵差
  - `median_prefix_p`:20 段前缀卡方 p 中位数
  - `prefix_p1~p20`:全图像 20 段卡方 p
  - `lsb_prefix_p1~p20`:LSB 通道 20 段卡方 p
  - `texture_noise`:图像纹理方差归一化
  - `est_rate`:从 χ²p + 前缀 p 反推估计嵌入率

#### 切换 53d 模式(代码示例)

```python
import joblib
from ml_predict import MLPredictor

# 默认 143d
pred_143 = MLPredictor()  # model_path=stego_classifier.joblib

# 切换 53d
pred_53 = MLPredictor(model_path='models/stego_classifier_v2_jpeg_lgb_51d.joblib',
                      clip_outliers=False)  # 53d 不需要 clip(单特征 AUC 高,训练分布更稳)

# 同一张图
result_143 = pred_143.predict(img)  # AUC 更高 + 弱档检出更强
result_53 = pred_53.predict(img)    # AUC 略低, 但可对每维特征解释
```

#### GUI 集成(规划)

`src/gui.py` 在 ML 模型加载处增加单选框:`[●] 143d 默认(稳健)` / `[ ] 53d 可解释(AUC+)`。
切换后:
- 143d 模式:与现状完全一致,部署推荐
- 53d 模式:增加"贡献特征"面板,显示 Top 5 特征 + 方向 + 强度,供研究/教学场景



下表是**校园照片**语料上的历史记录。2026-09-14 已把其中"只打印不落盘"的配置
全部重跑并落盘（`experiments/data/train_model_metrics.csv`），所以下面的数字现在
**都有产物可查**；重跑值与原值并列，差异来自修正后的特征口径与语料。
与 BOSSbase 的数字不可并列。

| 模型 | 训练集 | Held-out AUC | 真实 JPEG 干净 prob | nsF5 p3 d=0.25 检出 |
|---|---|---|---|---|
| v1 XGB (旧默认) | dataset.csv (11维, 6档) | 0.7435 → **重跑 0.7371** | 0.30 (正确) | 49% |
| v2 LR (未修复) | campus_v2 (143维, 12档) | 0.8278 | **1.00 (误判)** | 25% |
| v2 XGB (旧默认) | campus_v2_jpeg (143维, 12档 + 414 JPEG clean) | 0.8723 | 0.14 (正确) | 15.6% |
| v2 XGB tuned | campus_v2_jpeg + 网格调优 (depth=5, n_est=500) | 0.8889 → **重跑 0.8889** | 0.14 (正确) | 67.9% |
| v2 XGB + WEAK_WEIGHT=3 | campus_v2_jpeg + 弱档加权×3 | 0.8534 → 重跑 XGB 0.8832 / LR 0.9055 | 0.14 (正确) | 20.2% |
| v2 XGB + WEAK_WEIGHT=5 | campus_v2_jpeg + 弱档加权×5 | 0.8377 | — | 25.7% |
| v2 STACK (WEAK_WEIGHT=3) | campus_v2_jpeg + 4 模型 LR meta | 0.8321 → **重跑 0.8672** | **0.10 (正确)** | **32.1%** |
| **v2 LGB tuned (新默认)** | **campus_v2_jpeg + LGB 网格调优 (nl=31, ne=800, lr=0.03)** | **0.8939** | 见下 | **50.0%** |

> 重跑还暴露一件事：在当前（修正后的）语料上，**LR（标准化 + 校准）常常是最强的
> 单模型**（0.9055），高于调优后的 XGB（0.8889）与 STACK（0.8672）。这与旧口径
> "XGB/LGB 更强"的印象相反，值得在下一轮实验里单独查清。

- **新默认** `stego_classifier.joblib` = **v2 LGB tuned**
  (`num_leaves=31, n_estimators=800, learning_rate=0.03, min_child_samples=10`)。
- 现口径（2026-09-14 重跑，可追到 `experiments/data/deploy_model_metrics.csv`）：
  held-out AUC **0.8939**、8-split 平均 **0.8980 ± 0.0074**；弱档 nsF5 p3 d=0.25 检出 **50.0%**、
  nsF5 p2 d=0.35 **71.2%**、matrix p3 d=0.40 **74.0%**；验证集干净误报 27.9%。
- 表内"测试集 AUC 0.8946 / 弱档 85.3%"等为**审计前记录**，含源图泄漏与 SRM 尺度错误，不再引用。
- XGB tuned 已备份为 `stego_classifier_v2_jpeg_xgb_tuned.bak.joblib`。
- **STACK 版** (`_weak3_stack.joblib`) 仍保留供 OOD 严重场景切换（人工噪声 prob 0.24 vs LGB 0.99）。
- v1 11 维 XGB 保留为参考。

> **最终决策**：项目保留**双版本模型**供不同场景使用：
>
> | 版本 | 文件 | 适用 |
> |---|---|---|
> | **143d 默认版(更准)** | `stego_classifier.joblib` | 通用部署 / 异构数据 / 真实图像(默认加载) |
> | **53d 可解释版** | `stego_classifier_v2_jpeg_lgb_51d.joblib` | 论文 / 答辩 / 教学 / 单图分析 |
>
> 143d 默认:LGB tuned(`num_leaves=31, n_estimators=800, learning_rate=0.03, min_child_samples=10`),
> held-out AUC **0.8939**（校园语料）、8-split 平均 **0.8980**、弱档检出 **50.0%**。
> 53d 可解释:同样超参,53 维特征(去 SRM),held-out AUC **0.8391**、8-split 平均 **0.8461**、
> 弱档检出 41.4% —— 用约 0.05 AUC 换"每一维都能解释"。
> 两者在 BOSSbase 上的同族数字是 0.8062 / 0.7172（头条口径，见上文"指标口径"）。
>
> ⚠ 上述 AUC 均为**校园语料**（自建，更容易）。同一族模型在 BOSSbase 1.01 上是
> **0.7529 / 0.7172** —— 对外引用、论文对比请用后者，见 [`docs/RESULTS.md`](docs/RESULTS.md)。
> 两个模型现已可由 `experiments/train_deploy_models.py` 现场复现（逐位一致）。
> STACK 与 XGB tuned 仍保留供场景切换;v1 11 维 XGB 保留为参考。

---

## GPU 版 (v1.2)：PyTorch 批量向量化的统计特征分析

`gpu/` 子目录提供一套 **GPU(CUDA) 加速**的隐写检测管线，复刻已验证的
**11 维统计特征**（RS、卡方、差分熵、LSB 熵、前缀中位 p，与 `src/fsfeatures.py`
参考实现 **bit 级一致**），把原来逐图 Python 循环（RS 逐组、20 段前缀卡方）
改写为 PyTorch 张量化算子，在 CUDA 上一批并行算完。

> **为什么是"特征法"而不是裸像素 CNN？** 实测表明：在 414 张校园照片上，
> 从头训练的整图深度卷积网络（多架构/输入/正则组合）均无法跨照片泛化
> （验证 AUC≤0.50）——有效独立样本只有照片数，弱 LSB 信号需数千张源图才能学稳。
> 统计特征法在相同数据上验证 AUC≈**0.79**，且特征提取可被 GPU 并行化，
> 因此 GPU 版选择**加速这条真正管用的路径**，而非裸 CNN。

### 管线

```bash
# 1) 生成数据集 (每张照片 1 干净 + 4 档含密变体, 完整 512x512, 不裁剪以保留统计)
python gpu/make_imageset.py  [照片目录] [张数]

# 2) GPU 批量提取特征 + 按照片分组训练 + 评估
python gpu/train_ml_gpu.py            # 输出 models/steg_classifier_gpu.joblib

# 3) 单张图像 GPU 检测
python gpu/predict_gpu.py <图像> [<图像>...]
```

### 实测 (RTX 4060 Laptop, 纯校园照片)

数据源已从 `data/campus_jpg` 中**去除 DIP4E 教材灰度 tif**，改用纯校园照片目录
`data/campus_jpg`（仅 414 张 jpg）作为唯一数据源，CPU 与 GPU 两条管线均已全量重跑。

- **GPU 统计特征管线**：特征 2070 张 512² 灰度约 **5s（≈397 img/s）**，GPU 利用率
  峰值 **99% / 平均 74%**；验证 **AUC≈0.790**，acc≈0.802（Youden 阈值 0.713）。
  > 2026-09-14 复核：这一行**已可复现**（`experiments/data/gpu_pipeline_metrics.csv`，
  > `--datas imageset --srm off` 得到 AUC **0.7903**、阈值 0.7130、acc 0.8024）。
  > 注意它对应 `--srm off`；默认 `--srm auto`（= on）在 `imageset` 上只有
  > AUC 0.6544 —— 两条口径不同，数字不可混用。
- **CPU 特征管线**：2898 样本（414 干净 + 2484 含密，6 档），CV 最佳为
  **LogisticRegression CV-AUC≈0.759**；held-out **AUC≈0.781**、acc≈0.780；
  `matrix p2 d0.80`/`p3 d0.50` 检出≈100%/99%、`nsF5 p2 d0.85`≈95%、弱 `nsF5 p3 d0.25`≈52%。
- **一致性**：`python gpu/featurize_gpu.py` 自检，GPU 与 CPU 参考特征逐项一致
  （RS 到 bit 级、浮点 ~1e-7）。注意该自检走 `use_srm=False`；而
  `gpu/train_ml_gpu.py` 默认 `--srm on`，训练时先做 SRM 高通预处理再过 11 维 ——
  与 CPU 侧"原始图直接提特征"不是同一口径，两边数字不可直接并列。

### 参考基准数据集 (BOSSbase 1.01) 重跑

用隐写分析领域事实标准基准 **BOSSbase 1.01**（官方 `dde.binghamton.edu`，1.67GB zip，
10,000 张 **512×512 灰度 PGM**，与管线工作尺寸完全一致）重新生成数据并重训
（`make_imageset.py` 已增补 `*.pgm` 支持）。

```bash
# 1) 下载解压至 data/BOSSbase_1.01\*.pgm (官方 zip 1.67GB)
# 2) 生成图像集 (本实验取前 2000 张源图 -> 10000 样本, 512x512)
py gpu/make_imageset.py nsf5-steganography\data/BOSSbase_1.01 2000
# 3) 训练 (默认读 imageset.npz, 覆盖 steg_classifier_gpu.joblib)
py gpu/train_ml_gpu.py
```

- **数据**：2000 源图 → **10000 样本**（2000 干净 + 8000 含密，1干净+4变体），npz ≈1.57GB。
- **实测 (验证 2000 样本, Youden 阈值 0.798)**：**AUC≈0.644**，acc≈0.655；逐档——
  `matrix p3 d0.50`≈80%、`nsF5 p2 d0.95`≈70%、`nsF5 p2 d0.50`≈68%、弱 `nsF5 p3 d0.30`≈57%、
  > 2026-09-14 复核：这一行**已可复现**（`--datas imageset_bossbase --srm off` →
  > AUC **0.6438**、阈值 0.7983、acc 0.6550）。
  干净误报≈48%。
- **对比**：相比校园照片基线（AUC≈0.767/0.79）下降，验证了文献公认结论——BOSSbase
  经去马赛克加工、统计结构更强，弱密度 LSB 嵌入足印更弱，是**更难的隐写分析基准**；
  远优于 CIFAR 32×32 补充实验（AUC≈0.555，已回退清理）。
- 原基于校园照片的 `imageset.npz` / `steg_classifier_gpu.joblib` 已备份为
  `imageset_photobase.npz` / `steg_classifier_gpu_photobase.joblib`；BOSSbase 数据与
  去 DIP4E 前的旧数据/模型同时备份于 `backup_20260905/`。默认模型为纯校园照片版
  `steg_classifier_gpu.joblib` / `stego_classifier.joblib`。

### BOSSbase 全量合并训练（校园 + BOSSbase 10,000 源图）

为最大化训练作用，将 **BOSSbase 全量 10,000 张源图**与校园照片**合并训练**，让单一
模型同时看到"自然校园 + 去马赛克基准"两类域。

- **数据生成**：`make_imageset.py` 现以 **memmap 逐张落盘**（`<base>_x.npy`，避免大数组
  一次性堆叠造成 OOM），支持 `--out / --id-offset / -n` 多源分工。
  ```bash
  py gpu/make_imageset.py data/campus_jpg   --out campus            # 校园 414 源 -> 2070 样本
  py gpu/make_imageset.py data/BOSSbase_1.01 --out bossbase          # BOSSbase 10000 源 -> 50000 样本
  py gpu/train_ml_gpu.py                    # 合并两源训练(默认读取两源)
  ```
- **数据**：校园 2070 + BOSSbase 50000 = **52070 样本**（414+10000 源图，各 1干净+4变体）。
- **实测 (验证 10430 样本, Youden 阈值 0.795)**：**AUC≈0.712**，acc≈0.690；逐档——
  `matrix p3`≈87%、`nsF5 p2 d0.95`≈79%、`nsF5 p2 d0.50`≈70%、弱 `nsF5 p3`≈50%、干净误报≈41%。
  > 2026-09-14 复核：本仓库现有的 BOSSbase 图像集只有 **10000 样本**（旧数是
  > 50000 样本的全量集），因此无法逐位复现 0.712。用现有数据重跑合并配置
  > （3410 + 10000 = 13410 样本, `--srm off`）得到 **AUC 0.6672**、阈值 0.7921、
  > acc 0.6776，见 `experiments/data/gpu_pipeline_metrics.csv`。
- **对比**：合并 AUC(0.712) 介于纯校园(0.790) 与 BOSSbase 单跑(0.644) 之间，符合数据
  难度梯度——模型在更难基准与自然场景间取得平衡；GPU 特征提取 52k 张约 146s，GPU 满载。

### CPU 全量合并训练（校园 + BOSSbase 全量）

CPU 版同样接入 BOSSbase 全量，与校园照片合并训练，覆盖 10,000 张基准源图。

```bash
py src/make_dataset.py data/campus_jpg --out campus       # 校园 414 源 -> 2898 样本(1干净+6变体)
py src/make_dataset.py data/BOSSbase_1.01 --out bossbase  # BOSSbase 10000 源 -> 70000 样本
py src/train_model.py                                     # 合并两源训练(默认读取两源)
```

- **数据**：校园 2898 + BOSSbase 70000 = **72898 样本**（10414 clean + 62484 stego，11 维特征）。
- **5 折 GroupKFold CV**（照片分组防泄漏）：RandomForest **CV-AUC≈0.745**（最佳）、
  XGBoost 0.744、LogisticRegression 0.736、GradientBoosting 0.732。
- **held-out 测试（18228 样本）**：**AUC≈0.741**，acc≈0.685，bacc≈0.668，thr=0.834；
  逐档检出——`matrix p2 d0.80`≈99%、`matrix p3 d0.50`≈85%、`nsF5 p2 d0.85`≈73%、`nsF5 p2 d0.35`≈59%、
  弱 `nsF5 p3`≈44–55%；低误报点(thr=0.910) 干净误报≈11.6%、含密检出≈38.9%。
- **代价**：CPU 全量特征提取 70000 行约 **79 分钟**（单进程串行，见下文并行改造说明）。
- **对比**：CPU AUC(0.741) 略高于 GPU(0.712)，符合 CPU 逐张 6 档、更多 stego 变体覆盖更强的预期。

> **多进程并行（已实现）**：`make_dataset.py` 内置多进程并行（`-j / --workers N`，默认
> 用满 CPU 核数）。每张图内 6 档 C++ 嵌入/特征有强数据依赖无法图内并行，但**图与图
> 相互独立**，按图分片交多个子进程并行处理、主进程流式合并。实测 100 张（700 样本）：
> 单进程 53.5s → 16 核并行 9.5s，**加速约 5.6 倍**，多进程与单进程输出逐行一致。
> 注意每进程各自加载 `fsfeatures.dll`，内存按核数倍增（单进程约 54MB）。
> 命令：`python src/make_dataset.py <目录> --out x -j 15`

> 依赖：`torch`(CUDA)、`numpy`、`Pillow`、`scipy`、`scikit-learn`、`joblib`、
> `nvidia-ml-py`(可选，用于上报 GPU 利用率)。样本数据 `gpu/data/*` 较大(含 `_x.npy`
> 大数组)，不入库，可随时重新生成。

> 同 CPU 版一样，GPU 检测器输出的是**统计含密概率**，弱密度嵌入应结合启发式判读
> 交叉印证。GPU 特征提取亦可作为大批量图片的批量分析入口复用。

---

## 持续集成 & 发版

- **CI**（`.github/workflows/ci.yml`）：任何对 `main` 的推送 / PR 都会自动运行
  `test_core.py` 与 `test_steg.py`（Python 3.9 / 3.11），并构建 `wheel + sdist`。
  `pytest` 作业另外守着**模型卡与二进制不脱钩**（`src/test_model_cards.py`：
  sha256 + payload + 指标交叉核对）、**手册事实与代码片段**、**notebook 可执行**。
  浏览器回归（含 axe 无障碍审计）在 `webapp-tests.yml` 里单独跑。
- **自动发布**：推送形如 `v1.1.0` 的 tag 时，CI 会构建包并自动创建 **GitHub Release**，
  附带 `wheel` 与 `sdist` 作为资产，同时自动生成发布说明。
- **发布流程**：

```bash
# 提交改动并打 tag（本地）
git add -A && git commit -m "feat: v1.1"
git tag v1.1 && git push origin main --tags
```

## 版本历史

- **v1.8.4 (当前) — GUI 操作台升级与品牌图标**
  - 操作台按工作流重排(输入/参数/执行三段 + 通栏分隔线), 分析结果与运行日志
    收进右栏页签, 进度条仅忙时显示; 主流程三键加粗并带快捷键 tooltip;
    跨平台字型(Win=YaHei UI / macOS=PingFang SC / Linux=Noto CJK);
    空态改为行动引导文案, 待嵌入标签更正为 UTF-8。
  - 品牌图标(照片卡+比特流+放大镜)接入 exe/安装器/窗口标题栏; 色板卡
    (palette.png)沉淀为设计 token; 关于对话框附联系邮箱。
- **v1.8.3 — 红灯的 tag 不该往 PyPI 送包**
  - v1.8.2 的 tag 上 `pytest` 是红的, 而 `发布到 PyPI` / `发布 GitHub Release`
    照样成功 —— 失败的全量测试作业拦不住发布。原因: `build` 只 `needs: [test]`。
    现在 `build` 需要**全部验证作业**通过（test / pytest / gui /
    feature-consistency / notebooks / handbook / attribution）, 任一红则
    build / release / pypi 都不跑（并行执行, 不增加墙钟时间）。
  - 那次 pytest 红的是我自己写的"并行超时必须报错"用例: 1 秒上限在快 runner 上
    来不及触发（2 张 256² 的小图 <1s 跑完）。现在压到 1 毫秒, "来不及"成为必然,
    本机连跑三次全过。测试一旦依赖时序就一定会 flake —— 这是本项目第二次栽在
    同一个坑上。

- **v1.8.2 — 一个 Release 里出现了两个不同的 wheel**
  - 审计 v1.8.1 产物时发现 PyPI 的 wheel 与 Release 里的同名 wheel **哈希不同**：
    `ci.yml`（Linux）与 `release.yml`（Windows）各构建了一次，后者以同名文件
    覆盖了前者。两份的成员文件内容一致，只有 `METADATA`/`RECORD` 与时间戳不同，
    但"一个版本一份字节"的溯源因此断了。
  - 现在 Windows 作业不再上传 wheel，Release 的 wheel/sdist 一律来自 `ci.yml`
    （与 PyPI 同源同字节），它只提供 setup exe 与便携 zip。

- **v1.8.1 — 审计 1.8.0：发布链路的可验证性**
  - 审计发现 v1.8.0 的 **tag CI 是红的**（PyPI 作业走 trusted publishing，而
    pypi.org 端从没配 pending publisher），修复只落在 tag 之后的提交上；不过
    PyPI 上的 wheel 与 Release 里的那个**字节相同**（sha256 `02d2177a…`），
    产物确实来自 tag 那次构建。
  - **打包链此前只在 tag 上跑**，所以它前两次真实运行（v1.8.0）才暴露问题。
    现在 `release.yml` 支持 `workflow_dispatch` 预演，且手动运行**不会发布**；
    新增**安装器静默安装/卸载冒烟**（装到临时目录、不选任务 → 断言用户 PATH
    没被动过 → 跑装好的 CLI 往返 → 静默卸载 → 断言目录清空）。
  - `ci.yml` 改最小权限（workflow 级 `contents: read`，只有 release 作业 `write`；
    去掉已不需要的 `id-token: write`），并在 build 作业里**真的调用安装后的
    `nsf5stego`**（`--version` / `embed`→`extract` / `analyze --json`）——
    此前 CI 只验"模型随包可用"，没验过发布入口本身。
  - `docs/PACKAGING.md` 的体积与验收按 CI 产物更正（85.9 / 113.4 MiB），
    PATH 还原的措辞按安装器真实行为收紧；`make help` 加了完整性护栏
    （这条漂移犯过两次）；`.zcodeignore` 收进 `.gitignore`。

- **v1.8.0 — 命令行界面**
  - 此前 `pip install` 之后唯一的入口是弹 tkinter 窗口（`nsf5stego = "gui:main"`），
    服务器、脚本与批量场景只能自己 `import ns5_core` 拼代码。1.8.0 起入口改为
    `src/cli.py`：`embed`（文本可 `-m` 或 stdin）/ `extract` / `analyze`（卡方 + RS
    + ML，`--json` 机器可读）/ `gui` 四个子命令，参数与方法/p/口令和 GUI 一一对应；
    退出码区分 成功(0)/运行失败(1)/参数错误(2)，解码失败与容量超限都以明确提示
    收场而不是栈回溯或乱码。`nsf5stego gui` 与 `python src/gui.py` 行为不变，
    且在 tkinter 缺失 / 无显示环境时给出可行动提示。
  - 另有三处小体验: `embed` 的 stdin 文本按 UTF-8 优先解码（cp936 控制台管道
    传中文不再乱码嵌入）; GUI 标题栏显示版本号; `make webapp` 一条命令本地起
    交互实验室。
  - **安装布局修复**: `pip install` 后 GUI 的产物目录原来是解释器根下的
    `output/`（系统 Python 直接 PermissionError），现在自动改写当前工作目录;
    `analyze` 支持多图批量（逐行汇总, `--json` 数组, 坏图容错）; GUI 演示图
    缺失时按 seed=42 确定性配方一键生成。
  - **GUI 蓝白"国企风"改版**: 深蓝横幅 + 白色卡片 + 主操作深蓝按钮,
    统一浅蓝边框与微软雅黑字体; 演示面板同风格、教学语义配色不变;
    顺手修复灵敏度提示叠字与窗口高度不足两处布局问题。
  - 测试：`src/test_cli.py` 十五个用例走真实子进程 + 进程内 stdin 解码单测，
    另新增 `src/test_pathutil.py`（输出目录两种布局），CLI 控制台输出同时受
    GBK 静态护栏约束。
- **v1.7.2 — 同类隐患的全仓排查**
  - 1.7.1 修掉 OOD 评估的 fork 死锁之后，把这类隐患全仓扫了一遍：进程池只有三处
    （`ood_eval` 已修、`make_dataset` 本次修、`video_engine_v2` 本来就是 spawn），
    DataLoader 默认 `num_workers=0`。`src/make_dataset.py` 改为显式 spawn +
    `--pool-timeout` 断路器，并补两条慢测试：`workers=2` 与 `workers=1` **逐行一致**、
    超时必须以非零码报错 —— 这条多进程分支此前从未被执行过（CLI 冒烟只看 `--help`）。
  - 给**部署模型的生产者**补上第一条端到端冒烟测试（自造 143 维小语料跑完整条链），
    它第一次跑就抓到：`youden_threshold` 会返回 `inf`（`roc_curve` 的首个阈值就是
    inf，弱可分数据上 `argmax` 常落在那里）——阈值成了 inf 之后，模型对任何图都不判
    含密且**不报错**。两处实现都已加 `np.isfinite` 过滤；随仓库分发的两个模型没踩到
    （0.9493 / 0.9595 都是有限值），重跑真实语料与入库指标**逐项一致**。
  - **手册里的"置换加速 263 倍"被更正**：它与项目自己的 `bench_permute.csv` 对不上，
    而且自己给的耗时（12.3 s / 0.223 s）算出来也只有约 55 倍。现在重跑基准并统一为
    "4096²：15.6 s → 0.24 s（约 65×），小 N 处最高约 240×，数据见
    `experiments/data/bench_permute.csv`"，网页 / DOCX / PDF / 视频脚本七处一致，
    并让 `handbook_facts.py` 守住（`263` 列入禁词、产物名列入必填）。

- **v1.7.1 — 教学材料的悬空引用**
  - 手册（中英文 ch03 / ch11 / 附录 F）把 `yccstego` 写成"项目 `yccstego` 扩展"，
    但它是**独立仓库与 PyPI 包**（`pip install yccstego`），本仓库里没有它的代码 ——
    读者按手册去找会一无所获。现已改为"姊妹项目 `yccstego`"并给出仓库地址，
    README 新增"姊妹项目"一行；`handbook_facts.py` 把该地址列进 `REQUIRED`，
    DOCX / 网页 / 入库 PDF 三份材料缺它即 CI 红。入库 PDF 重新导出（英文 74 → 75 页）。
  - **OOD 评估进 CI**：`experiments/ood_eval.py` 产出 README 的头条数字，此前从未
    在 CI 里跑过（要 1514 张外部照片），1.6.9 新增的 `--workers` 并行路径更是零覆盖。
    新增 `src/test_ood_smoke.py`：用合成照片跑通这条链，钉住"判据=payload 阈值""并行
    与单进程逐位一致""fp_rate/Wilson CI/ALL 行自洽"。
  - **备用 PDF 路径补字形**：xelatex 那条路径此前有 22 个符号（`① ᵖ ₄` 等）在字体里
    没有字形，日志里只有一行 `Missing character`、退出码仍是 0，两份 PDF 各有 80 余处
    会变空白；现已逐个映射并让脚本缺字形时返回非零。
  - **一次 CI 挂死事故的修复**：新加的 OOD 冒烟测试在 pytest 进程里 fork 出进程池
    （Linux 默认），与已初始化的 LightGBM/OpenMP 线程池撞成死锁，让 `pytest` 作业从
    1 分 44 秒变成**挂满 6 小时**。现在 `ood_eval.py` 显式用 spawn、每个分片带超时
    （卡住即报错）、冒烟测试改跑 CLI 子进程，CI 各作业也设了 `timeout-minutes` 兜底。

- **v1.7.0 — 模型治理：模型卡**
  - 两个随仓库分发的 `.joblib` 此前是**裸二进制**：语义只存在于 README 的散文
    与 pickle 的 payload 里，读 payload 得先装齐依赖再反序列化，而且
    "模型换了、文档没换"没有任何护栏。现在每个模型配一份**入库的 JSON 模型卡**
    （`models/*.card.json`）：语料与协议、指标（held-out / 8-split / 逐档检出 /
    真实照片误报率 / 跨语料参考）、**特征列表与顺序**、超参、训练环境与 git 版本、
    适用边界、不适用场景、已知局限，以及 `sha256`。
  - 新增契约测试 `src/test_model_cards.py`（sha256 硬绑定 + payload 逐字段核对 +
    有数据时与 `experiments/data/*.csv` 交叉核对），随 `pytest` 进 CI；
    生产者 `experiments/train_deploy_models.py` 在落盘后自动刷新模型卡，
    `make model-cards` / `make model-cards-check` 是本地入口。
  - 卡里如实写下边界：143d 在 1514 张公开真实干净照片上误报 **9.58%**、53d
    **28.86%**（DIV2K 那 100 张是 51% / 47%），因此两者都不适合单独定案；
    对外引用请用 BOSSbase 口径（0.8062 / 0.7172），而不是校园语料的
    0.8939 / 0.8391。

- **v1.6.1–v1.6.9 — 审计修复、发布链路与性能**
  - 逐版记录见 [`CHANGELOG.md`](CHANGELOG.md)。要点：源图泄漏与 SRM 特征尺度
    两处缺陷的修复（1.6.2）、DOI/PyPI 发布链路与仓库治理（1.6.6）、
    覆盖率测量口径的三次修正（1.6.7/1.6.8）、SRM 残差向量化与 OOD 并行
    （20 分钟 → 1.7 分钟）以及彩色输入的训练/推理偏差修复（1.6.9）。

- **v1.6.0 — 可验证性加固**
  - **CNN 对比实验补上真实实现与真实数据**：`gpu/train_cnn.py` 与
    `gpu/models/{xunet,yenet}.py` 此前并不存在（论文引用的路径是悬空的），
    而 `sota_compare.py` 会把 53 维请求静默降级成 11 维、仍标成 `LGB-53d`。
    现在特征列缺失即报错，`feat_set` 与 `dataset` 分列，所有基线共用同一份
    按源图划分与同一套按源图 bootstrap 的置信区间。实测（BOSSbase，按源图
    holdout）：Ye-Net 0.9541、LGB-143d 0.7529、LGB-53d 0.7172、LGB-11d 0.7128、
    Xu-Net 0.5007（**未收敛**，训练集 AUC 也是 0.50，故不构成"CNN 不如手工
    特征"的证据）。CNN 的实现与训练入口在 `gpu/train_cnn.py` 与
    `gpu/models/`。
  - **两个部署模型入库**：`models/stego_classifier.joblib`(143d) 与
    `models/stego_classifier_v2_jpeg_lgb_51d.joblib`(53d)，并补上此前缺失的
    `lightgbm` 依赖 —— 否则 clone 后 ML 判定仍是 `available=False`。
  - **C++ 改为源码跨平台编译**：不再入库任何二进制，`make cpp` 三平台各自
    产出 `.dll`/`.so`/`.dylib`；CI 现在真的编译并执行 C++ 一致性自检，
    随后再删掉产物验证纯 Python 回退路径。
  - **测试全量进 CI**：新增 pytest job 与无头 GUI job（xvfb）；`test_gui.py`
    此前不在任何 workflow 里；`test_false_positive.py` 里一处 `ok = ok` 恒真
    赋值让整个假阳性测试变成空断言。
  - **浏览器回归测试真正跑起来**：`webapp-tests.yml` 此前未被提交，且即使提交
    也会因 `python` 命令与 npmmirror 镜像源而失败。
  - **教学视频质检修复**：两张质检图不可用（一张 33 字节空图、一张缺失），
    根因是 `qa_sheet()` 在无抽帧时间时静默写出零高度 PNG 并中断后续章节。
  - 已知缺口（如实记录，**已于 v1.6.1 关闭**）：两个部署模型当时没有仓库内的
    生产者，`src/train_model.py` 训不出它们 —— 现由
    `experiments/train_deploy_models.py` 补齐（见本文件"模型从哪来"一节）。
  - 注：实验数据（`experiments/data/`、`data/dataset_*.csv`）按项目约定**不入库**
    （可重新生成）。本版新增的 CNN 实现放在 `gpu/`。

- **v1.5.0 — 学习手册发布 + Zenodo DOI**
  - 发布中英文学习手册(PDF)至 `docs/`:
    - `docs/学习手册-从零读懂nsF5隐写项目.pdf` (中文, 12 周快速入门路线, 深入版见附录 F, 67 页)
    - `docs/Learning-Handbook-From-Zero-to-nsF5-Steganography.pdf` (英文, 12-week quick-start roadmap, deep 6–12 month track in Appendix F, 74 页)
  - 涵盖 v1.4.0 全部新特性: 143d/53d 双版本 ML 模型、SRM 高通滤波、特征可解释性分析
  - 零基础: 从"像素与二进制"到"LGB 分类器超参调优"的完整学习路径
  - 新增 Zenodo 存档 DOI 徽章

- **v1.4.0 — 双版本 ML 模型** ⚠ **本节数字已于 2026-09-14 审计作废**
  （源图泄漏 + SRM 特征尺度错误；现口径见 CHANGELOG 1.6.2：
  143d held-out 0.8939 / 8-split 0.8980，53d 0.8391 / 0.8461，且 **143d 优于 53d**）
  - **143d 默认版**(`stego_classifier.joblib`) — LGB tuned
    (`num_leaves=31, n_estimators=800, learning_rate=0.03, min_child_samples=10`)
    - Held-out AUC **0.8946**,8 split 平均 **0.9085**
    - OOD 鲁棒:**1/8 fp**(median prob 0.011),真实校园 JPEG 干净
    - 训练数据:`data/dataset_campus_v2_jpeg.csv`(143d,12 档变体 + 414 张 JPEG 干净)
  - **53d 可解释版**(`stego_classifier_v2_jpeg_lgb_51d.joblib`) — 去 SRM 90 维
    - Held-out AUC **0.9100**(+0.015),8 split 平均 **0.9227**(+0.014)
    - 弱档检出全面优于 143d:nsF5 p3 d=0.25 87.2%、matrix p3 d=0.40 95.4%
    - OOD 鲁棒:3/8 fp(牺牲少量鲁棒性换 AUC 与可解释性)
    - 51 维有明确统计定义,可对单图输出 Top 贡献特征(论文/教学推荐)
  - 关键发现:v2 143d 中 ~73% 增益来自 31 个可解释特征,SRM 90 维平均单特征 AUC 仅 0.50~0.52
    (近随机),是 LGB 中的"噪声特征";但 SRM 残差对 JPEG 高频噪声有过滤作用,故 143d 在 OOD 上更稳
- **v1.3.1 (待发布)**
  - 补齐运行时依赖 `matplotlib`/`joblib`/`scikit-learn`/`pandas`(此前缺失导致 ML/绘图模块导入崩溃)
- **v1.3.0**
  - 新增**矩阵编码演示**面板(GUI):随机/可点击块 LSB,实时计算伴随式 `s`
    与目标 `m` 的差值 `d`,在汉明校验矩阵 `H` 中定位命中的列并**高亮被改系数**,
    执行修改后校验 `H·x==m`。核心逻辑独立于 `src/matrix_demo.py`。
  - 新增**隐写分析随载荷扫描**面板:`payload` 滑条 0→0.4,逐档重新嵌入并实时刷新
    卡方 p 值 / RS 估计嵌入率 / ML 含密概率三曲线
    （单图无真 AUC，以 ML 概率作区分趋势示意）。逻辑位于 `src/scan_panel.py`。

- **SRM 高通滤波预处理层**
  - 新增 `src/srm_filter.py`（30 个标准 SRM 核，numpy/torch 双实现，合成单通道增强图）；
    `make_dataset.py` 增 `--preprocess srm`、`featurize_gpu.extract_features_gpu` 增
    `use_srm` 开关（`train_ml_gpu` 增 `--srm on/off`）。
  - **同源校园**：CV-AUC 0.7594→0.7922、测试 AUC 0.7811→0.8085，弱密度检出明显提升。
  - **跨源合并（校园+BOSSbase 全量）**：CPU 测试 AUC 0.741→0.704、GPU 校园+BOSSbase2000源
    验证 AUC 0.651→0.572，SRM 均**下降**，两管线相互印证（详见上文 SRM 小节"重要"段）。
  - 结论与默认：默认模型=未 SRM 合并全量（≈0.741），SRM 单源校园（≈0.8085）另存为
    `stego_classifier_campus_srm.joblib` 备用。
- **数据重跑（移除 DIP4E）**
  - 从数据源**去除 DIP4E 教材灰度 tif**，建立纯校园照片目录 `data/campus_jpg`（仅 414 jpg），
    CPU 与 GPU 管线全量重跑：GPU 验证 AUC≈0.790、CPU held-out AUC≈0.781；
    旧数据/模型备份至 `backup_20260905/`。
- **v1.2.2**
  - GPU 数据集支持 **jpg/tif 等多格式混合**（`gpu/make_imageset.py`），去掉默认 150 张上限、默认全量；
    曾用加入 DIP4E tif 后的 **682 张 / 3410 样本** 重训（DIP4E 已于后续数据重跑中移除）。
  - README 检测指标**按数据集区分**（自然照片 A：AUC≈0.79 / 含 tif 混合 B：AUC≈0.767）；
    模型为二进制、不入库，与 PyPI 上 ver1.2.1 明确区分。
- **v1.2.1**
  - 修复仅 1 个有效灰度对的强二值图（`letterA/B/T.tif`）隐写分析 `lgamma(0)` 崩溃，返回中性 p 值。
  - 新增 C++ `nsf5_permute` 确定性置乱加速：4096² 置乱 524ms→210ms、整体嵌入约 540→281ms；
    DLL 缺失自动回退同算法 Python，编码/解码两端序列恒定可逆。
  - GUI 绘图预览崩溃修复，并按屏幕尺寸 1:1 高质量展示。
- **v1.2.0**
  - 新增 **GPU 版**（`gpu/`）：PyTorch 批量向量化复刻 11 维统计特征（RS/卡方/熵/前缀 p，
    与 CPU 参考实现 bit 级一致），GPU 提取 2070 张特征 ≈5s、利用率峰值 99%。
  - 新增 GPU 训练/推理管线 `train_ml_gpu.py`、`predict_gpu.py`；验证 **AUC≈0.79**。
  - 实测与文档说明了"裸像素深度 CNN 需海量独立源图、局部特征法更适合小样本隐写检测"。
- **v1.1.0**
  - 新增 **C++ 嵌入加速** `cpp/nsf5embed.dll`（修复汉明缓存越界；与 Python 像素级一致并经回环校验）。
  - 监督学习升级：数据集扩展为 **6 档密度变体**、构建提速约 8→**90 倍**，
    `train_model.py` 改为 **5 折 GroupKFold** 交叉验证选模。
  - 修复低误报阈值选择 bug（`threshold_for_fp` 取最低阈值而非最高，保障真实低误报检出率）。
  - GUI 新增 **判定灵敏度**（严格 / 均衡 / 宽松），联动启发式与 ML 判决阈值。
  - 新增 CI、Apache-2.0 License、构建与发版说明。
- **v1.0.0**
  - nsF5 伴随式矩阵编码 + 湿纸编码；图像哈希键控；盲隐写分析；GUI；码族与效率绘图。
  - C++ 特征提取 `cpp/fsfeatures.dll`；一版有监督分类器（LR，AUC≈0.78）。

## 许可

本项目基于 **Apache License 2.0** 发布，详见 [LICENSE](LICENSE) 与 [NOTICE](NOTICE)。
