简介:这份Python源码包面向计算机、自动化等专业的学生与开发者,提供一套基于机器学习的喷码缺陷检测完整实现,可直接用于高分毕业设计、课程设计或期末大作业,也适合作为工业视觉质检方向的自学案例。压缩包共208个文件,约156.86MB,包含11个py源码文件、12个pdparams与11个pdopt模型权重、11个yml配置、41个csv训练日志与指标记录、55张jpg与26张png样本及可视化图片,以及json、txt、md等说明文档,覆盖数据、训练、评估与部署各环节。项目已通过导师指导验收,下载即用无需修改,确保可运行。目前已有135人学习关注。读者可从中获得完整的缺陷检测流程方案、可复现的训练配置与权重、逐轮指标曲线与可视化结果,以及清晰的目录组织,便于快速理解模型训练与推理逻辑,并在此基础上完成二次开发或论文撰写。
1. 喷码缺陷检测为什么总在产线末端翻车
喷码缺陷检测这件事,真正做过产线的人都知道,难点从来不在"能不能识别出缺陷",而在"能不能在产线速度下稳定识别出缺陷,并且不把好品判成坏品"。喷码本身是高速喷墨或激光打标,字符可能只有几毫米高,背景是金属、塑料、纸盒、玻璃瓶,反光、曲面、油墨扩散、喷头堵塞导致的断线缺笔画,全都混在一起。传统做法是用模板匹配加形态学,字符位置稍微偏一点、光照变一点就误报,调参调到怀疑人生。
基于机器学习的喷码缺陷检测,核心思路是把"字符区域定位"和"缺陷判别"拆成两段:先用轻量检测或分割把喷码区域抠出来,再用分类或异常检测模型判断这个区域是否合格。Python 源码在这个方向上的价值,是把数据标注、训练、推理、产线对接这条链路用一套可读的代码串起来,而不是丢一个模型文件让你自己猜怎么用。这篇面向的是要拿它做毕业设计、或者要在自己产线上试一版原型的工程师,重点讲清楚数据怎么造、模型怎么选、参数怎么调、坑在哪。
2. 喷码缺陷检测的数据从哪来:采集、标注与增强的完整链路
2.1 先想清楚缺陷分类体系,再谈采集
很多人一上来就拍几千张图,标到一半发现类别定义是乱的。喷码缺陷常见就那么几类:缺笔画(断线)、字符粘连、字符缺失、位置偏移、重影、浓度不均(过淡或过深)、背景脏污干扰。你要先定一个判定标准,比如"缺笔画超过单字符笔画宽度 30% 判不合格",这个阈值必须和质检标准对齐,否则模型学出来的边界和产线判的边界不一致,上线必翻车。
分类体系建议做成两级:一级是 OK/NG,二级是 NG 的具体类型。一级用于产线快速判别,二级用于后续追溯和工艺改进。源码里如果只有二分类,你至少要保证 NG 样本覆盖了所有已知缺陷类型,不然模型见到没见过的缺陷会当 OK 放过去,这是最危险的情况。
采集时要注意三件事:第一,相机触发要和喷码动作同步,用光电传感器或编码器触发,不要用固定延时,产线速度一变就错位;第二,光源要固定,喷码检测最怕环境光变化,能用环形光或同轴光就上,别省这个钱;第三,每个批次、每个班次都要采,因为油墨粘度、喷头状态会随时间漂移,只采一个时间段的图,模型泛化能力会很差。
2.2 标注的粒度决定模型上限
喷码缺陷检测的标注有两种粒度:整图分类标注和字符级/像素级标注。整图分类标注成本低,但模型只能告诉你"这张图有问题",没法告诉你哪里有问题,调试时很难定位。字符级标注(每个字符一个框,标 OK/NG)成本高,但能训练检测模型,输出缺陷位置,产线调试和工艺分析都用得上。
我的建议是:如果做毕业设计,时间有限,先用整图分类跑通链路,再补字符级标注做检测。如果直接上产线,字符级标注是必须的,因为你要能告诉操作工"第几个字符有问题"。
标注工具用 LabelImg 或 CVAT 都行,关键是标注规范要统一。比如字符框要贴紧字符边缘,不要留太多背景;NG 框要框住缺陷字符,不要把整个喷码区域框进去。标注不一致是模型效果差的第一大原因,比模型选型影响还大。
2.3 数据增强要针对喷码场景,不要照搬通用增强
通用增强里的随机裁剪、大角度旋转、颜色抖动,在喷码检测里很多是有害的。喷码字符方向基本固定,大角度旋转会造出产线上不存在的样本;颜色抖动会改变字符浓度,而浓度本身是判别特征,抖过头会把 OK 样本变成 NG 样本。
针对喷码场景,有效的增强是:小角度旋转(±3 度以内)、轻微平移(模拟触发偏差)、亮度微调(模拟光源波动)、高斯噪声(模拟传感器噪声)、局部遮挡(模拟脏污)。如果要模拟缺笔画,可以用形态学腐蚀随机去掉部分笔画,但要注意别把 OK 样本腐蚀成 NG 还标 OK,那就把标签搞脏了。
import cv2 import numpy as np import random def augment_code_image(img, label): """ 针对喷码图像的增强,label 为 0(OK)/1(NG) 只做产线上真实存在的变换,避免标签污染 """ h, w = img.shape[:2] aug = img.copy() # 1. 小角度旋转,模拟触发偏差 angle = random.uniform(-3, 3) M = cv2.getRotationMatrix2D((w / 2, h / 2), angle, 1.0) aug = cv2.warpAffine(aug, M, (w, h), borderMode=cv2.BORDER_REPLICATE) # 2. 亮度微调,模拟光源波动,范围控制在 ±15 beta = random.uniform(-15, 15) aug = cv2.convertScaleAbs(aug, alpha=1.0, beta=beta) # 3. 高斯噪声,模拟传感器噪声 noise = np.random.normal(0, 5, aug.shape).astype(np.uint8) aug = cv2.add(aug, noise) # 4. 仅对 OK 样本做局部遮挡,模拟脏污但不改变标签 if label == 0 and random.random() < 0.3: x = random.randint(0, w - 20) y = random.randint(0, h - 20) aug[y:y + 20, x:x + 20] = random.randint(0, 255) return aug这段代码的关键点是:旋转角度限制在 ±3 度,亮度偏移限制在 ±15,噪声标准差控制在 5 左右。这些参数不是拍脑袋,是根据产线实际波动范围定的。如果你的产线光源更稳,可以把亮度范围再收窄;如果触发偏差更大,旋转角度可以放宽,但不要超过 5 度。局部遮挡只对 OK 样本做,是因为 NG 样本本身已经有缺陷,再加遮挡会让模型分不清主次。
2.4 数据集划分的坑:别用随机划分
随机划分训练集和验证集在喷码检测里是典型的错误做法。因为同一批次的图高度相似,随机划分会导致训练集和验证集里有几乎一样的图,验证准确率虚高,上线就崩。正确做法是按批次或按时间段划分:用前几个批次的图训练,用后一个批次的图验证。这样才能真实反映模型在新批次上的泛化能力。
如果数据量实在少,至少按采集时间段划分,比如上午的图训练,下午的图验证。划分比例不用死守 8:2,喷码检测里 7:3 甚至 6:4 都合理,因为验证集要足够大才能覆盖批次差异。
3. 模型选型与训练:从轻量 CNN 到异常检测的取舍
3.1 为什么喷码检测优先选轻量模型
产线末端通常没有 GPU 服务器,推理跑在工控机或边缘盒子上,算力有限。喷码检测的输入图像不大(字符区域可能就 100x100 到 300x300),缺陷特征相对明显,不需要 ResNet 这种级别的模型。MobileNetV3、ShuffleNetV2、或者自己搭一个 4 层卷积加全局池化的小网络,参数量控制在 1M 以内,推理时间能压到 10ms 以内,完全够用。
选模型时看三个指标:推理延迟、模型大小、在验证集上的召回率。喷码检测里召回率比准确率重要,因为漏检(把 NG 判成 OK)的代价远大于误检(把 OK 判成 NG)。误检大不了人工复检,漏检流到客户手里就是投诉。所以阈值要往召回率方向偏,宁可多报一点。
import torch import torch.nn as nn class CodeDefectNet(nn.Module): """ 轻量喷码缺陷分类网络 输入: 128x128 灰度图 输出: 2 类 (OK / NG) """ def __init__(self, num_classes=2): super().__init__() self.features = nn.Sequential( # 128 -> 64 nn.Conv2d(1, 16, 3, stride=2, padding=1), nn.BatchNorm2d(16), nn.ReLU(inplace=True), # 64 -> 32 nn.Conv2d(16, 32, 3, stride=2, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), # 32 -> 16 nn.Conv2d(32, 64, 3, stride=2, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), # 16 -> 8 nn.Conv2d(64, 64, 3, stride=2, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), ) self.gap = nn.AdaptiveAvgPool2d(1) self.fc = nn.Linear(64, num_classes) def forward(self, x): x = self.features(x) x = self.gap(x).flatten(1) return self.fc(x)这个网络的设计逻辑:输入用灰度图而不是 RGB,因为喷码检测主要看字符形状和浓度,颜色信息冗余,灰度图能减少计算量。四层卷积逐步下采样,最后用全局平均池化代替全连接,参数量小且不容易过拟合。BatchNorm 在每一层后加,是因为喷码图像亮度波动大,BN 能稳定训练。如果你的字符区域更小,可以把输入改成 64x64,再减一层卷积。
3.2 训练参数怎么设:学习率、批大小与损失函数
学习率用 1e-3 起步,配合余弦退火或 StepLR,每 20 个 epoch 降一半。批大小根据显存定,8 到 32 都行,但要注意如果用了 BatchNorm,批大小不要小于 8,否则 BN 统计量不准。优化器用 Adam 或 SGD 都可以,Adam 收敛快但最终精度可能略低,SGD 调好了泛化更好。喷码检测数据量通常不大,Adam 更省心。
损失函数方面,如果 OK/NG 样本不均衡(产线上 OK 远多于 NG),用带权重的交叉熵,权重按类别频率的倒数设。更激进一点可以用 Focal Loss,让模型更关注难分样本。但 Focal Loss 的 alpha 和 gamma 要调,gamma 一般 1.5 到 2 之间,alpha 按正负样本比例设。
import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss = 0 for imgs, labels in loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(imgs) loss = criterion(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() return total_loss / len(loader) # 类别权重: OK 样本多,权重低; NG 样本少,权重高 class_weights = torch.tensor([1.0, 3.0]).to(device) criterion = nn.CrossEntropyLoss(weight=class_weights) optimizer = optim.Adam(model.parameters(), lr=1e-3) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=20, gamma=0.5)类别权重设成 [1.0, 3.0] 是一个经验起点,意思是 NG 样本的损失权重是 OK 的 3 倍。如果你的 NG 样本占比不到 5%,可以调到 5.0 甚至更高。但权重不是越高越好,太高会导致模型把所有样本都判成 NG,误检率飙升。调权重的依据是看验证集上的召回率和误检率,找到一个平衡点。
3.3 异常检测路线:当 NG 样本太少时怎么办
实际产线上,NG 样本往往很少,可能几百张 OK 才有一张 NG。这时候分类模型训不好,可以考虑异常检测路线:只用 OK 样本训练一个自编码器或 One-Class SVM,推理时看重构误差或距离,超过阈值判 NG。
自编码器路线的实现是:编码器把 OK 图像压到低维隐空间,解码器重构回来,训练目标是最小化重构误差。推理时,如果输入是 OK,重构误差小;如果是 NG,重构误差大。阈值用 OK 验证集的重构误差分布定,比如取 99 分位数。
这条路线的好处是不需要 NG 样本,坏处是阈值难定,而且对"轻微缺陷"不敏感。如果缺陷很细微,重构误差可能和正常波动混在一起,区分不开。所以异常检测适合缺陷明显的场景,或者作为分类模型的补充,两者结合用。
4. 推理部署与产线对接:从模型文件到稳定运行的排查清单
4.1 推理速度优化:别让预处理成为瓶颈
模型推理本身可能只要 5ms,但图像预处理(解码、缩放、归一化)可能花 20ms,成为真正的瓶颈。优化顺序是:第一,用 OpenCV 的cv2.imread换成cv2.imdecode直接从内存解码,省掉磁盘 IO;第二,缩放用cv2.resize的INTER_LINEAR,别用INTER_CUBIC,后者慢很多;第三,归一化用 numpy 向量化操作,别用循环。
如果还嫌慢,可以把预处理也放到 GPU 上,用 CUDA 加速。但喷码检测的图像小,CPU 预处理通常够用,先测一下各阶段耗时再决定优化哪里。用time.perf_counter()打点,别凭感觉猜。
import cv2 import numpy as np import time def preprocess(img_bytes, target_size=(128, 128)): """从字节流到模型输入的完整预处理""" t0 = time.perf_counter() # 从内存解码,避免磁盘 IO img = cv2.imdecode(np.frombuffer(img_bytes, np.uint8), cv2.IMREAD_GRAYSCALE) t1 = time.perf_counter() # 缩放用线性插值,速度快 img = cv2.resize(img, target_size, interpolation=cv2.INTER_LINEAR) t2 = time.perf_counter() # 归一化到 [0,1] 并增加 batch 和 channel 维度 img = img.astype(np.float32) / 255.0 img = np.expand_dims(img, axis=(0, 1)) t3 = time.perf_counter() print(f"decode: {(t1-t0)*1000:.2f}ms, resize: {(t2-t1)*1000:.2f}ms, norm: {(t3-t2)*1000:.2f}ms") return img这段代码把预处理拆成三步并打点,方便你定位瓶颈。实际跑下来,decode 通常是大头,如果图像是 JPEG 压缩的,解码可能占 10ms 以上。如果产线速度要求高,可以考虑用硬件解码或者换更快的相机接口。
4.2 阈值设定与误检漏检的平衡
模型输出的是概率,你需要定一个阈值把概率转成 OK/NG。阈值不是 0.5 就完事,要根据误检和漏检的代价来定。如果漏检代价高,阈值往低设,比如 0.3,意思是只要 NG 概率超过 0.3 就判 NG,宁可误检。如果误检代价高(比如误检会导致频繁停机),阈值往高设。
更精细的做法是用验证集画 P-R 曲线,找到满足召回率要求的最大阈值。比如你要求召回率不低于 99%,那就找 P-R 曲线上召回率 99% 对应的阈值。这个阈值可能很低,导致误检多,但这是代价权衡的结果,不是模型不好。
产线运行一段时间后,要定期用新数据重新评估阈值。因为油墨、喷头、环境会漂移,半年前定的阈值可能不再适用。建议每周抽一批新数据跑一遍,看误检率和漏检率有没有明显变化。
4.3 与 PLC 和 MES 的对接要点
喷码检测系统最终要输出一个信号给产线:OK 放行,NG 剔除。这个信号通常通过 IO 口或 PLC 通信发出。对接时要注意:第一,信号时序要和产线速度匹配,检测必须在产品到达剔除工位之前完成,否则剔不掉;第二,要有防抖逻辑,连续几帧判 NG 才触发剔除,避免单帧误判导致误剔;第三,要记录每张图的检测结果和图像,方便追溯。
如果产线有 MES 系统,检测结果要上传,字段至少包括:时间戳、产品序列号、检测结果、NG 类型、置信度、图像路径。上传失败要有本地缓存和重传机制,别因为网络问题丢数据。
5. 喷码缺陷检测避坑:5 个血泪教训
5.1 现象:验证集准确率 99%,上线误检率 30%
原因:数据集划分用了随机划分,训练集和验证集里有同一批次的相似图,验证准确率虚高。上线后遇到新批次的图,模型没见过,误检率飙升。
解决:按批次或时间段划分数据集,验证集必须来自训练集没见过的批次。如果数据量不够,至少按采集时间划分,并且定期用新数据重新评估。
5.2 现象:模型对某种缺陷完全检测不出来
原因:训练集里这种缺陷样本太少,或者根本没有。模型没见过这种模式,自然学不会。更隐蔽的情况是,标注时把这种缺陷标成了 OK,把标签搞脏了。
解决:先统计训练集里各类缺陷的样本数,少于 50 张的类别要专门补数据。补不了就用异常检测路线兜底,或者用数据增强模拟这种缺陷。标注规范要反复检查,别让标注错误污染训练集。
5.3 现象:推理速度忽快忽慢,偶尔超过产线节拍
原因:预处理阶段有磁盘 IO 或内存分配抖动,或者模型推理时 GPU 被其他进程占用。产线环境里工控机可能还跑着其他软件,资源竞争很常见。
解决:预处理全部放内存,避免磁盘 IO;模型推理前预热几次,让 CUDA 初始化完成;工控机上关掉不必要的后台程序,给检测进程留足资源。如果还不行,考虑用 TensorRT 或 ONNX Runtime 加速推理。
5.4 现象:换了新批次的油墨后,误检率突然升高
原因:油墨浓度变化导致字符对比度变化,模型训练时的数据分布和新数据不匹配。这是典型的域漂移问题,喷码检测里非常常见。
解决:短期方案是调整预处理里的对比度归一化参数,或者用直方图均衡化把不同批次的图拉到同一分布。长期方案是定期用新数据微调模型,或者用域适应方法让模型对浓度变化不敏感。最省事的做法是在产线换油墨时,采一批新图快速微调最后一层。
5.5 现象:NG 剔除后,偶尔有 NG 产品流到下游
原因:剔除信号发出太晚,产品已经过了剔除工位;或者剔除机构动作不到位,产品没被真正剔掉。这是电气和机械问题,不是模型问题,但很容易被误认为是模型漏检。
解决:先确认检测完成时间是否早于剔除工位到达时间,用示波器或逻辑分析仪测信号时序。如果时序没问题,检查剔除机构的气压、动作行程、响应时间。喷码检测系统要和产线机械配合调试,不能只调模型。
6. 用混淆矩阵和置信度分布做上线前的最后验证
模型训完、阈值定完,别急着上线。先做两件事:画混淆矩阵,看误检和漏检具体分布在哪些样本上;画置信度分布,看 OK 和 NG 的置信度有没有重叠区。重叠区越大,说明模型对这两类的区分能力越弱,阈值怎么调都难受。
混淆矩阵要用独立测试集画,不能再用验证集。测试集最好来自不同批次,模拟真实上线场景。如果测试集上召回率低于 99%,先别上线,回去补数据或调模型。
置信度分布图能告诉你阈值该定在哪。如果 OK 样本的置信度集中在 0.9 以上,NG 样本集中在 0.1 以下,那阈值定 0.5 很安全。如果两者在 0.4 到 0.6 之间大量重叠,说明模型区分能力不足,需要换模型或补难样本。
import numpy as np from sklearn.metrics import confusion_matrix, roc_curve def evaluate_model(model, test_loader, device, threshold=0.5): """在测试集上评估,输出混淆矩阵和最佳阈值""" model.eval() all_probs = [] all_labels = [] with torch.no_grad(): for imgs, labels in test_loader: imgs = imgs.to(device) outputs = torch.softmax(model(imgs), dim=1) # 取 NG 类的概率 probs = outputs[:, 1].cpu().numpy() all_probs.extend(probs) all_labels.extend(labels.numpy()) all_probs = np.array(all_probs) all_labels = np.array(all_labels) # 按当前阈值算混淆矩阵 preds = (all_probs >= threshold).astype(int) cm = confusion_matrix(all_labels, preds) print(f"阈值 {threshold} 下的混淆矩阵:\n{cm}") # 画 P-R 曲线找满足召回率的最大阈值 fpr, tpr, thresholds = roc_curve(all_labels, all_probs) # 找召回率 >= 0.99 的最小阈值 valid_idx = np.where(tpr >= 0.99)[0] if len(valid_idx) > 0: best_threshold = thresholds[valid_idx[0]] print(f"召回率 >= 99% 对应阈值: {best_threshold:.4f}") return cm, best_threshold这段代码做了两件事:先用当前阈值算混淆矩阵,让你看清误检和漏检的数量;再用 ROC 曲线找满足召回率要求的最小阈值。注意roc_curve返回的 thresholds 是降序的,valid_idx[0]取的是第一个满足条件的,对应最大的阈值。实际用时,如果这个阈值太低(比如 0.05),说明模型区分能力差,靠调阈值救不回来,得回去改模型。
我自己的习惯是:上线前必须看到测试集召回率 99% 以上,且误检率在可接受范围内(比如 5% 以下),才允许上线。上线后第一周每天抽检,记录误检和漏检的样本,第二周开始每周抽检。喷码检测没有一劳永逸的模型,只有持续监控和迭代。希望帮到你。
本文还有配套的精品资源,点击获取