滤光片外观质检 · 开箱即用推理包
暗场成像的 IR-CUT 滤光片裁片,判良品 / 不良,可选输出缺陷类型(划痕 / 麻点 / 崩边)。
推理端不需要 PyTorch、不需要 timm、不需要 GPU,只要 4 个 pip 包。
两个版本,v3 与 v4 并列
| v3(仓库根目录) | v4(v4/ 目录) |
|
|---|---|---|
| 换模型 | 要重新打包程序 | 拷一个目录 + 一条命令 |
| 预处理 | 归一化写死 gwz |
由 pack.json 驱动 |
| 换错模型 | 静默出错 | 8 步闸门,带编号退出 |
| 状态 | 稳定,已在用 | 新增,未在产线跑过 |
v3 的一切保持原样,下面的说明继续有效。v4 是并列新增的,不影响现有用法。
v4 解决的是什么
v3 的 predict.py 把归一化写死成 gwz,而 filter_binary.json 里的 norm 字段被忽略。
后果不是"换不了模型",是更糟的——换上去会静默出错:
| 实测场景 | 结果 |
|---|---|
imnet 模型被喂 gwz |
AUROC 0.9976→0.9940,漏检 7→23 张(3.3 倍),而误报两边都是 3 |
gwz 模型被喂 imnet |
AUROC 0.9914→0.3542,排序翻转,185 张良品误报 141 张 |
第一行是关键:误报数一模一样,只有漏检在偷偷涨,看结果表永远发现不了。
而本仓库 test AUROC 最高的模型(efficientnet_b0 / imnet = 0.9976)正好是 imnet 归一化的
——它在 v3 下根本部署不了。
v4 怎么用
# CPU 版(80 MB)—— 不确定就用这个
wget https://huggingface.co/daipath/filter-inspection/resolve/main/v4/binaries/filter-predict-v4-portable
chmod +x filter-predict-v4-portable
# GPU 版(1.08 GB,内置 CUDA 12 运行库,目标机只需 NVIDIA 驱动)
# wget https://huggingface.co/daipath/filter-inspection/resolve/main/v4/binaries/filter-predict-v4-gpu
# 把模型包目录也拷下来(exe 里只内置了一个判废兜底包)
hf download daipath/filter-inspection --include "v4/models/*" --local-dir .
mv v4/models .
./filter-predict-v4-portable --list # 有哪些包、什么预处理、阈值从哪来
./filter-predict-v4-portable -i 图片目录/ -o 结果.xlsx
换模型:
cp -r 新包目录 models/ # 1. 拷进去
./filter-predict-v4-portable --verify 新包名 # 2. 8 步校验
./filter-predict-v4-portable --activate reject=新包名 # 3. 切换(可一条命令回滚)
详见 v4/README.md、v4/模型包手册.md
与 v4/binaries/README.md(三个二进制怎么选、glibc 要求、GPU 注意事项)。
三个二进制(共用同一套模型包,换模型流程完全一样):
| 文件 | 体积 | 要求 | 实测速度 |
|---|---|---|---|
filter-predict-v4-portable |
80 MB | GLIBC ≥ 2.28 | 20 张/秒 |
filter-predict-v4-gpu |
1.08 GB | GLIBC ≥ 2.28 + CUDA 12.x 驱动 | 103 张/秒 |
filter-predict-v4 |
57 MB | GLIBC ≥ 2.38 | 20 张/秒 |
GPU 只快约 5 倍而非 200 倍——纯推理 4359 张/秒,但瓶颈已是预处理(纯 numpy 跑在 CPU)。 另:GPU 与 CPU 分数不逐位相同(409 张 test 上判定 0 处不一致,分数最大差 7.0e-03, 距阈值最近的图裕度 0.0185 = 2.6 倍)。产线固定用同一个二进制,别混用。
⚠ v4 与 v3 的分数不逐位相同。 v4 修正了一处训练/推理不一致(训练走
cv2.resize、 v3 走 PILBILINEAR,两者内核语义不同)。判定层面 0/409 翻转、FP/FN/AUROC 不变, 但分数最大差 0.0035、38/409 张 >1e-4。若已基于 v3 输出建了 SPC 基线,换 v4 需重新验收。
📚 完整文档
| 文件 | 内容 |
|---|---|
docs/INDEX.md |
项目总索引:四个仓库怎么选、三个核心结论、数据一览 |
docs/REPRODUCE.md |
完整复现指南:环境、数据准备、逐个实验的命令与预期数字 |
docs/FILES.md |
逐文件使用说明:41 个脚本、结果表字段、51 个训练产物目录 |
docs/RESULTS.md |
全部实验结果:42 个模型完整榜单、分割 vs 分类同口径对决 |
docs/AUDIT.md |
诚实性审计:四道审计的原理与结果,为什么这次的高分是真的 |
docs/CAVEATS.md |
⚠ 已知问题与边界,上线前必读(含我犯过的评估口径错误) |
docs/REPOS.md |
四个仓库逐一说明与下载方式 |
三种用法,任选
A. 零安装 —— 单个可执行文件(推荐)
目标机上不需要 Python、不需要装任何东西,下载一个文件就能跑。模型已打进二进制。
# CPU 版(145 MB,任何 x86_64 Linux 都能跑)
chmod +x binaries/filter-predict-cpu
./binaries/filter-predict-cpu -i 图片目录/ -o result.xlsx
# GPU 版(1.3 GB,自动用 CUDA;需要 NVIDIA 驱动 ≥ CUDA 12.x)
chmod +x binaries/filter-predict-gpu
./binaries/filter-predict-gpu -i 图片目录/ -o result.xlsx
实测速度:CPU 22 张/秒,GPU(RTX 4090)3608 张/秒。
B. Python 脚本(想改代码时用)
pip install -r requirements.txt # onnxruntime numpy pillow openpyxl
python predict.py -i sample_images/ -o result.xlsx
C. 只要 ONNX,自己接
filter_binary.onnx 输入 float32[N,3,320,320],输出 logits[N,2]。
预处理必须与 predict.py:preprocess() 一致(等比缩放补零 → 灰世界白平衡 → 逐图 z-score),否则结果不可信。
单张图
python predict.py -i /path/to/filter_0001.png
文件 : filter_0001.png
判定 : 良品
不良概率: 0.0060
整个目录 → xlsx(递归扫描子目录)
python predict.py -i /path/to/images/ -o result.xlsx
输出表格:图片名 | 判定 | 不良概率 | 相对路径
附带缺陷类型
python predict.py -i images/ -o result.xlsx --with-type
多两列:缺陷类型 | 类型置信度(只对判为不良的填)
不想装 openpyxl
输出写 .csv 即可(UTF-8 BOM,Excel 直接双击能开):
python predict.py -i images/ -o result.csv
调判废松紧
只有一个参数可调:--threshold(不良概率的判废线)。
调高 → 判不良的更少,不良桶更纯,但漏检多。调低 → 抓得全,但误杀更多良品。
标定表(阈值在 val 集上定,test 锁箱只评一次;「产线 10%」是按产线不良率 10% 换算的):
| 档位 | 阈值 | test 误报 | test 检出 | 不良桶混入良品 | 产线10%口径 | 每万片冤杀 | 每万片漏检 |
|---|---|---|---|---|---|---|---|
| 最严 | 0.787 |
4/185 | 95.5% | 1.83% | 16.9% | 195 | 45 |
| 保守 | 0.509 |
8/185 | 98.7% | 3.49% | 28.3% | 389 | 13 |
| 平衡(默认) | 0.459 |
8/185 | 99.1% | 3.48% | 28.2% | 389 | 9 |
| 宽松 | 0.411 |
9/185 | 99.1% | 3.90% | 30.6% | 438 | 9 |
| 高召回 | 0.242 |
11/185 | 99.6% | 4.70% | 35.0% | 535 | 4 |
python predict.py -i images/ -o r.xlsx --threshold 0.787 # 最严
注意:分数分布是双峰的,0.51~0.79 之间几乎是空的,所以中间档位区分度很低。 真正有意义的选择是「最严 0.787」和「平衡 0.459」两档。
模型与性能
| 判废模型 | 类型模型 | |
|---|---|---|
| 文件 | filter_binary.onnx (16.8 MB) |
filter_4class.onnx (85.1 MB) |
| 骨干 | mobilenetv3_large_100 (4.2M) | resnet34 (21.3M) |
| 输入 | 320×320 | 320×320 |
| 输出 | 良品 / 不良 | 良品 / 划痕 / 麻点 / 崩边 |
| test AUROC | 0.9913 ± 0.0049 | 0.9867 |
| test 平衡准确率 | 0.9746 | 0.9213 |
速度(实测,纯 CPU,无 GPU):约 20 张/秒。有 GPU 会自动用(装 onnxruntime-gpu 即可),约 1500 张/秒。
四分类逐类召回(test):划痕 88.0% / 崩边 94.1% / 良品 95.1% / 麻点 91.3%
这个模型可信吗
做过 4 道诚实性审计,全部通过:
| 审计 | 结果 |
|---|---|
| 批次指纹强度 | 92% —— 图像里确实带着可区分批次的信息(有作弊空间) |
| 未见类「碎」检出 | 100% —— 这一类整类从没进过训练,模型照样抓到 |
| 跨批次泛化 | 留整个批次当考卷,AUROC 0.9862 vs 同批次 0.9911,只掉 0.005 |
| 冻结特征对照 | 只见良品的 kNN (0.9634) 打败 用了标签的线性探针 (0.9461) —— 拿到标签没优势,说明没有可解锁的捷径 |
这套审计不是形式:本项目上一代的图级分类模型曾拿到 AUROC=1.0,一测就发现它认的是"这张图来自哪个压缩包",30 张目检干净的图全被打 0.999+。这一代是真的。
已知边界(务必阅读)
- 验证样本量有限:test 只有 185 张良品。0 误报的 95% 置信上界是 1.61%, 无法证明产线级的低误杀率。要证明误报率 <1%,需要约 300 张干净良品;<0.1% 需要约 3000 张。
- 「不良桶混入良品」随产线不良率剧烈变化。表里 test 口径 1.83%, 换算到产线不良率 10% 就是 16.9%。这个指标不是模型属性,必须绑定你的实际不良率才有意义。
- 色调漂移未做压力测试。原始数据存在采集配置的色调漂移(B 通道逐日 15→8→5)。 预处理里的逐图灰世界白平衡是为此设计的,但没有在真实漂移上验证过。 上线建议固定白平衡,并监控良品分数分布。
- **不可擦拭脏污检出仅约 53%**。这一类"擦不擦得掉"是物理属性, 单张图像原理上判不了,训练时已整类排除。
- 训练数据的良品标签本身约 3.3% 存疑(漏标的真缺陷)。误报数字受此影响。
- 输入图假定是已裁到单片边界的暗场图,原始尺寸 300~360px 量级。整版图或明场图不适用。
文件清单
binaries/
filter-predict-cpu 零安装可执行文件,145 MB,CPU,22 张/秒
filter-predict-gpu 零安装可执行文件,1.3 GB,CUDA,3608 张/秒
predict.py 推理脚本(需自备 4 个 pip 包)
filter_binary.onnx 判废模型 + filter_binary.json(含阈值标定表)
filter_4class.onnx 类型模型 + filter_4class.json
requirements.txt 4 个 pip 包
sample_images/ 22 张样例图(来自锁箱 test,从未参与训练)
weights/ PyTorch 原始权重(.pt,需 timm + torch 才能加载)
results/ 全部实验结果表、训练曲线、逐图打分
README.md 本文件
配套数据集:https://huggingface.co/datasets/daipath/filter-inspection-data