Rishichamp/hybrid-source-camera-identification
GitHub: Rishichamp/hybrid-source-camera-identification
一个混合深度学习框架,通过融合小波域 PRNU 指纹匹配与 CNN 特征学习,解决数字图像取证中的稳健源头相机识别问题。
Stars: 0 | Forks: 0
# 源头相机识别 -- 混合 PRNU + CNN
混合 PRNU + CNN 源头相机识别
一个混合深度学习框架,将基于 Wavelet 的 PRNU 提取与 CNN 特征学习相结合,用于数字图像取证中稳健的源头相机识别。








## 概述
本仓库实现了一个用于**源头相机识别**的 **Hybrid PRNU + CNN** 框架,将经典的**Photo Response Non-Uniformity (PRNU)** 指纹与深度卷积神经网络特征相结合。与单独使用任一方法相比,融合后的表示显著提高了分类准确率,使该系统适用于数字图像取证和相机归属研究。
## 架构
## 目录
- 概述
- 结果
- 架构
- 项目结构
- 安装说明
- 数据集
- 训练
- 交叉验证
- 预测
- 演示
- 开发历史
- 参考文献
- 许可证
一个混合源头相机识别系统,将经典的 Sensor
Pattern Noise (SPN/PRNU) 相关性与一个从零开始训练的小型 CNN 相结合,
并通过逻辑回归元分类器进行融合。它最初是一个 B.Tech 毕业设计项目,后来从零开始重建为一个可复现的、
经过测试且基准测试诚实的 pipeline。
## 结果
在一个包含 10 个相机型号的数据集(Kaggle 的 "IEEE's
Signal Processing Society -- Camera Model Identification" 数据集;在原始分辨率裁剪后每个相机 200 张图像 -- 参见 [数据集](#using-a-public-dataset-kaggles-camera-model-identification-set))上进行训练和评估。
| 指标 | 结果 |
|---|---|
| **5 折交叉验证准确率** | **92.17% ± 1.57%** (每次拆分: 93.75%, 92.50%, 89.17%, 92.92%, 92.50%) |
| 单次留出测试集运行 | 94.58% |
| -- 仅 CNN 分支 | 74.17% |
| -- 仅 PRNU 相关性分支 | 80.00% |
| -- **混合 (融合)** | **94.58%** |
**该融合方法大幅超越了两个单独的分支** (74.17% /
80.00% 单独运行对比 94.58% 融合运行) -- 这是本项目的核心实证发现:
元分类器确实是在融合来自每个分支的互补
信号,而不是仅仅选择较好的那一个。有关
这些数字来源的完整的、带时间戳的、未经编辑的输出,请参见本仓库中的
`reports/results.txt` 和 `reports/cross_validation.txt`。


上述每一个数字都可以从本仓库端到端地复现(参见
[分步指南](#step-by-step-guide))-- 这里没有任何事后的
手动调整或选择性报告;`cross_validate.py` 和
`hybrid_train.py` 会自动将每次运行的结果追加到 `reports/` 中,
无论成败。
## 开发历史
这个 pipeline 经历了多次真实的、有据可查的迭代 -- 一个天真的
高斯模糊 "SPN" 提取方法将整张照片缩放(破坏了它试图测量的
逐像素噪声),一个在噪声数据上训练不足的 ImageNet 预训练 CNN
分支,以及在真实硬件上将相机类别从 5 个扩展到 10 个时暴露出的三个独立的内存错误。这些
问题中的每一个都找到了根本原因并进行了修复,而不是采取临时方案 -- 要查看
完整的记录,包括出了什么问题、为什么出问题以及实际的修复方法是什么,请参见
[CHANGELOG.md](CHANGELOG.md)。这段历史被有意保留
下来,而不是被掩盖:它是关于这个结果实际上是如何达成的
更诚实的记录。
## 为什么是重新设计,而不仅仅是减少图像
在约 700 张训练图像上微调 DenseNet121 的最后 15 层 (v1) 已经
显示出训练和验证准确率之间存在过拟合差距。
如果用每个类别约 140 张训练图像 (v2 的原始数量) 做同样的事情,情况会
更糟。这个版本没有采取“相同的模型,更小的数据集”的做法,而是围绕三个原则重新设计了
pipeline:
1. **提取更好的信号**,以便模型需要更少的数据来学习。
2. **从相同的图像中获取更多有效样本**,而不是要求
提供更多图像。
3. **针对它天然适合的问题部分,使用一种几乎不需要数据的经典技术**,
而不是要求 CNN 从头开始学习
所有内容。
## 与 v1 的区别
| | v1 | v2 (本项目) |
|---|---|---|
| 噪声提取 | 高斯模糊 "高通" (粗糙) | 小波域自适应维纳降噪 (Lukás/Fridrich/Goljan 风格的 PRNU) |
| 残差提取区域 | 整个图像缩放至 128x128 | 原始分辨率中心裁剪 (默认 512x512) -- 缩放整张照片会模糊逐像素的 PRNU |
| 骨干网络 | DenseNet121,微调最后 15 层 (约 8M+ 可训练参数) | 小型自定义 CNN,完全从零开始训练 (约 0.3M 参数) -- ImageNet 特征不能很好地迁移到噪声残差数据 |
| 输入单元 | 整个 128x128 图像 | 每张图像多个 96x96 重叠的 patch (在 512x512 裁剪下约 49 个 patch/图像) |
| 经典 PRNU 匹配 | 未使用 | 每个相机平均的指纹 + 归一化互相关性 |
| 最终决策 | 仅使用 CNN softmax | 堆叠元分类器 (Logistic Regression) 融合 PRNU 相关性分数 + CNN 分数 |
| 准确率估计 | 单一训练/验证/测试集拆分 | `cross_validate.py` 通过多个随机种子重复整个 pipeline -> 平均值 +/- 标准差 |
这本质上是原始报告中对比
表里的 **"Hybrid SPN + CNN"** 行 -- 得到了正确的实现,而不是
实际构建的仅含 CNN 的 pipeline。
## 混合 pipeline 的工作原理
```
┌─────────────────────────┐
│ Wavelet-based PRNU │
Input image → │ residual extraction │
└──────────┬──────────────┘
│
┌─────────────┴─────────────┐
▼ ▼
┌────────────────────┐ ┌───────────────────────┐
│ Correlate against │ │ Split into patches -> │
│ 5 camera reference│ │ custom CNN (scratch) │
│ fingerprints │ │ classifier -> avg. │
│ -> 5 scores │ │ softmax -> 5 scores │
└──────────┬──────────┘ └───────────┬────────────┘
│ │
└───────────────┬──────────────┘
▼
Logistic Regression meta-classifier
│
▼
Final predicted camera
```
## 项目结构
```
source-camera-id-v2/
├── README.md
├── CHANGELOG.md
├── LICENSE
├── requirements.txt
├── requirements-dev.txt
├── .gitignore
├── .github/workflows/tests.yml <- CI: runs unit tests on every push
├── data/raw/
<- your dataset (any camera names -- auto-discovered)
├── src/
│ ├── prnu_extraction.py <- wavelet PRNU, native-res center crop, patches,
│ │ fingerprints, NCC, CNN input normalization
│ ├── dataset.py <- auto-discovers camera classes, fingerprint/classifier
│ │ split (returns whole-image residuals, not patches)
│ ├── patch_sequence.py <- lazily extracts+normalizes patches per training batch
│ │ (bounded memory regardless of dataset size)
│ ├── cnn_branch.py <- small custom CNN, trained from scratch, regularized
│ ├── hybrid_train.py <- trains CNN + meta-classifier, evaluates, saves class_names.json
│ ├── cross_validate.py <- repeated-split robustness check
│ ├── reporting.py <- saves training curves / confusion matrix / logs
│ └── predict.py <- single-image end-to-end inference
├── tests/ <- unit tests (no dataset needed), run via pytest
│ ├── test_prnu_extraction.py
│ ├── test_dataset.py
│ ├── test_patch_sequence.py
│ ├── test_hybrid_train.py
│ └── test_prepare_camera_dataset.py
├── scripts/prepare_camera_dataset.py <- shrink a public dataset (e.g. Kaggle's) into
│ a small, repo-friendly data/raw/ folder
├── saved_models/ <- cnn_branch.keras, meta_classifier.joblib,
│ fingerprints.npy, class_names.json
├── reports/ <- auto-generated figures + results logs
├── demo/app.py <- free Gradio demo
└── notebooks/
```
## 分步指南
### 1. 环境
```
git clone https://github.com//source-camera-id-v2.git
cd source-camera-id-v2
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
```
### 2. 添加你的数据集
你有两个选择。
**选项 A -- 你自己的数据集 (如前所述):** 在
`data/raw/` 下每个相机一个文件夹,例如 `data/raw/Phone_1_Samsung_S3Mini/`。无需在
代码中编辑任何内容 -- `dataset.py` 会自动从这些文件夹名中发现相机类别。
**选项 B -- 公共基准测试数据集 (如果要将准确率推得更高,推荐使用):** 参见下文的“使用公共数据集”。如果你想
有把握地达到 90% 以上,这是值得尝试的途径 --
你自己的小数据集是一个合理的概念验证,但在这个问题中,准确率在很大程度上随着你拥有的每台相机的干净图像数量而扩展,而公共基准测试
数据集拥有的数量要多得多。
无论哪种方式,这个 pipeline 都会自动保留每台相机 `--fingerprint-n` 张
图像 (默认为 40 张) 纯粹用于构建每台相机的 PRNU 指纹,
并使用其余的图像进行 CNN 分支的训练/验证/测试集拆分 -- 你
不需要自己分离这些。
**重要提示:** `--crop-size` (默认 512) 必须小于你的
源照片的短边 -- 这是原始分辨率的裁剪,而不是
缩放 (参见 prnu_extraction.py)。如果你的图像特别小,请相应地降低
`--crop-size`。
## 使用公共数据集 (Kaggle 的 Camera Model Identification 数据集)
**关于 Dresden 的说明:** Dresden Image Database 过去是这项特定任务的
标准推荐,但截至今日,多份独立报告称其原始下载
主机不可用/已失效。我不建议你去追逐一个可能已经失效的学术镜像。
**推荐替代方案:Kaggle 的 "IEEE's Signal Processing Society - Camera
Model Identification" 数据集** (在 kaggle.com 上搜索 `sp-society-camera-model
-identification`,或者一旦你接受了比赛规则,通过 Kaggle CLI 使用 `kaggle competitions download -c
sp-society-camera-model-identification`)。它非常适合这个 pipeline:
- **10 款不同的相机型号,每款一个物理设备,每台设备 275 张
全分辨率图像** (总共 2,750 张图像) -- 不同*型号*分类是这项任务中更简单、分离度更好的版本 (也是在干净数据下原始 PRNU 文献报告接近完美准确率的那种),而且它和你项目已经使用的任务结构是一样的。
- 它是一个真实的、可引用的、已发布的基准测试 (一次真正的 IEEE 赞助
比赛),对于报告来说,这是一个比不可验证/失效链接更强的方法论说明。
- 它的 `train/` 文件夹已经布局为每个相机
型号一个子文件夹 -- 这正是 `prepare_camera_dataset.py` 的文件夹模式所期望的,
无需重新组织。一旦你下载了它,请验证确切的类别文件夹名称 (我是从比赛的公开文档中回忆起的可能名称,而不是照着我面前的一页打字) -- 无论
哪种方式,`dataset.py` 都会自动发现 `data/raw/` 下实际存在的任何
文件夹名称,因此无论如何都不需要硬编码或编辑任何内容。
**不要将原始数据集提交到 git。** 即使是几百 MB 也就
超出了一个 git 仓库应该承载的容量,而且 GitHub 积极劝阻大型
仓库。`data/raw/` 已经被 gitignore 忽略了。相反:
- 将你在本地准备好的数据集仅保留在本地。
- 提交 `scripts/prepare_camera_dataset.py`、你的 `saved_models/`
制品以及 `reports/` (这些都很小) -- 任何人都可以通过针对免费下载的 Kaggle 源重新运行准备脚本来复现你的
确切数据集。这比一个包含大型二进制文件的仓库更强大、更专业 -- 而且它完全避免了
"1-1.5GB 仓库" 的担忧,因为数据集大小的任何内容都不会
触及 git。
**准备一个小型且适配 pipeline 的数据集:** 这个 pipeline 永远只需要每张图像的原始分辨率中心裁剪 (而不是整个
数百万像素的原始图像 -- 参见 `prnu_extraction.py`),因此
`scripts/prepare_camera_dataset.py` 预先将每张选定的图像裁剪一次,
并且只保存该裁剪部分 -- 通常会将多 GB 的源数据
缩小到几百 MB,并且**不会损失 PRNU 信号质量**,因为
裁剪部分是图像中 pipeline 唯一会查看的
部分。
```
cd scripts
# Kaggle 的 train/ 文件夹已经为每个相机型号包含了一个子文件夹:
python prepare_camera_dataset.py \
--input-dir /path/to/sp-society-camera-model-identification/train \
--output-dir ../data/raw \
--per-camera 200 --crop-size 768 --jpeg-quality 95 --max-total-mb 1200
```
该脚本在写入任何内容之前会估计总输出大小,如果
超过 `--max-total-mb` (默认 1200,即约 1.2GB -- 符合
典型的“保持适合作品集”的预算),则会中止 -- 如果是这样,请降低 `--per-camera`、
`--crop-size` 或 `--jpeg-quality`,或者传递 `--force` 以继续
执行。存储比你的训练 `--crop-size` 稍大一点的裁剪
(例如存储 768 对比 训练 512) 会留有余地,使得 pipeline 自己的
裁剪永远不会正好落在边缘。
由于 Kaggle 训练集中每台设备只有 275 张图像,`--per-camera
200` 使用了大部分可用的图像,同时留出一些供你在看起来不对劲时手动
查看。然后只需训练 -- **无需编辑代码**,
`dataset.py` 会自动从 `prepare_camera_dataset.py` 生成的任何文件夹中发现相机类名:
### 3. 训练混合 pipeline
```
cd src
python hybrid_train.py --epochs 40 --batch-size 32 --fingerprint-n 40 --crop-size 512
```
最后,它会打印:
- 在留出测试集上的融合混合准确率,
- 完整的分类报告 + 混淆矩阵,
- **诊断性的仅 CNN 和仅 PR 的准确率**,以便你可以确切地看到
融合效果比单独使用任一分支高出多少 -- 对于
你报告的“结果与分析”部分很有用。
它还会从这次确切的运行中**自动保存**:
- `reports/figures/training_curves.png` -- 训练/验证准确率与损失对比 epoch
- `reports/figures/confusion_matrix.png` -- 一张热力图,可直接放入
报告或幻灯片中
- `reports/results.txt` -- 追加记录的、带时间戳的日志,包含每次运行的
准确率数字和分类报告
这些都是直接根据 pipeline 在你的图像上实际测量出的结果生成的 --
没有什么需要手动填写的,而且每次运行都会在 `results.txt` 中保留自己的带时间戳的记录,因此你最终
报告中的数字可以追溯到特定的某次运行。
制品将保存到 `saved_models/`:`cnn_branch.keras`、
`meta_classifier.joblib`,`fingerprints.npy`。
### 4. 获得可靠的准确率估计 (鉴于数据集较小,推荐此项)
```
python cross_validate.py --n-splits 5 --epochs 25
```
在 5 个随机拆分中重复整个 pipeline (新的指纹集,新的 CNN 训练,新的
元分类器),并报告平均准确率 +/- 标准差 --
在答辩/报告中,这比单次运行的数字更有说服力,特别是在
这种数据集规模下。还会向 `reports/cross_validation.txt` 追加摘要。
### 5. 运行单元测试 (可选,但在你依赖结果之前推荐运行)
```
pip install -r requirements-dev.txt
cd src && python -m pytest ../tests -v
```
这些测试会检查核心的 PRNU 数学原理 (残差提取、分块、指纹
相关性),并专门防止在 v2.1 中修复的 CNN 输入归一化
bug 再次出现。它们在几秒钟内就能运行完,并且不需要你的
数据集。GitHub Actions 工作流 (`.github/workflows/tests.yml`) 会在
每次推送时自动运行它们。
### 6. 对新图像进行预测
```
python predict.py --image /path/to/photo.jpg
```
打印出融合预测以及两个分支的原始分数,以便你可以看到
*为什么*它做出了这样的决定 (例如,“PRNU 强烈倾向于 Phone 3,而 CNN
在 Phone 3 和 Phone 4 之间犹豫不决,融合决策:Phone 3”)。
### 7. 免费的交互式演示
```
cd ..
python demo/app.py
```
或者在 Hugging Face Spaces 上免费部署 (步骤与 v1 的 README 相同):
创建一个 Gradio Space,推送 `demo/app.py`、`requirements.txt`、`src/` 以及
你的 `saved_models/` 制品。
### 8. 发布到 GitHub
```
git init && git add . && git commit -m "Hybrid PRNU + CNN source camera identification (refined, low-data)"
git branch -M main
git remote add origin https://github.com//source-camera-id-v2.git
git push -u origin main
```
## 关于设计选择的说明
- **为什么对于 CNN 分支使用 patch 而不是整张图像**:PRNU 是一种
像素级的统计属性,而不是全局形状特征 -- 网络
不需要看到整张照片就能估计其噪声是否与某台
相机匹配。Patch 可以让你从一张图像中反复提取该
信号,这正是小型数据集所需要的乘数。
- **为什么指纹集要与 CNN 的数据分开**:通过平均来构建
PRNU 指纹并不是过拟合意义上的“训练”
(场景内容相互抵消,传感器图案不会),因此可以安全地
将这些图像视为免费的额外证据,而不是 CNN 分支
正在争夺的数据。
- **为什么元分类器使用 Logistic Regression 而不是另一个 CNN**:由于
只有 10 个输入特征 (5 个相关性分数 + 5 个 CNN 分数) 以及几
百个图像级别的训练样本,线性模型具有正确的
复杂度 -- 任何更复杂的方法都有可能导致融合步骤
本身发生过拟合。
## 局限性 (沿用以往,仍然适用)
- 封闭集:只能识别经过训练的 5 个特定的物理设备。
- PRNU 类的方法仍然对重度压缩/裁剪很敏感。
- 在此设计下,每台相机 200 张图像是可行的,但更多的数据 (特别是
更多用于构建指纹的图像) 仍然可能进一步改善结果
-- 如果你以后能收集到更多数据,首先提高 `--fingerprint-n`
是最高杠杆率的改变。
## 关于报告结果的说明
本 README 示例中的每一个准确率数字都是说明性的,而不是
承诺 -- 你的实际数字取决于你的数据集、硬件和
随机性。`hybrid_train.py` 和 `cross_validate.py` 将它们的输出
直接从实际运行写入到 `reports/` 中,并带有时间戳,因此无论最终
出现在你的项目报告或幻灯片中的是什么,都可以追溯到代码的
一次特定执行。如果审稿人或主考官问“你能给我看看
这次运行吗”,`reports/results.txt` (或 `cross_validation.txt`) 就是答案
-- 这种可追溯性对你的成绩和你的可信度来说,比稍微高一点
的数字更有价值,而且在答辩中进行辩护时压力也
更小。
## 参考文献
1. Lukás, J., Fridrich, J., & Goljan, M. (2006). Digital camera
identification from sensor pattern noise. *IEEE TIFS*, 1(2), 205-214.
2. Mihcak, M. K., Kozintsev, I., & Ramchandran, K. (1999). Spatially
adaptive statistical modeling of wavelet image coefficients and its
application to denoising. *ICASSP*.
3. Huang, G., et al. (2017). Densely connected convolutional networks
(说明为什么 v1 使用 DenseNet121 的背景信息). *CVPR*.
## 许可证
MIT -- 参见 [LICENSE](LICENSE)。标签:PRNU传感器噪声, TensorFlow, 卷积神经网络, 小波变换, 数字图像取证, 深度学习, 特征融合, 相机源识别, 逆向工具