
简介这份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), borderModecv2.BORDER_REPLICATE) # 2. 亮度微调模拟光源波动范围控制在 ±15 beta random.uniform(-15, 15) aug cv2.convertScaleAbs(aug, alpha1.0, betabeta) # 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_classes2): super().__init__() self.features nn.Sequential( # 128 - 64 nn.Conv2d(1, 16, 3, stride2, padding1), nn.BatchNorm2d(16), nn.ReLU(inplaceTrue), # 64 - 32 nn.Conv2d(16, 32, 3, stride2, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), # 32 - 16 nn.Conv2d(32, 64, 3, stride2, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), # 16 - 8 nn.Conv2d(64, 64, 3, stride2, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), ) 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(weightclass_weights) optimizer optim.Adam(model.parameters(), lr1e-3) scheduler optim.lr_scheduler.StepLR(optimizer, step_size20, gamma0.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, interpolationcv2.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(fdecode: {(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, threshold0.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), dim1) # 取 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% 以下才允许上线。上线后第一周每天抽检记录误检和漏检的样本第二周开始每周抽检。喷码检测没有一劳永逸的模型只有持续监控和迭代。希望帮到你。本文还有配套的精品资源点击获取