简介这份资源是面向高校学生与机器学习入门者的喷码缺陷检测完整项目源码可直接用于毕业设计、课程设计或期末大作业。项目以Python实现围绕工业喷码字符的缺陷识别展开涵盖数据预处理、模型训练与评估等环节适合作为计算机视觉方向的实战参考。压缩包共208个文件约156.86MB包含55张jpg与26张png图像样本、41个csv训练日志与指标记录、12个json配置、12个pdparams与11个pdopt模型权重文件、11个yml参数配置以及11个py源码脚本另附ttf字体与md说明文档结构完整、层次清晰。目前已有135人学习下载。读者可从中获得一套可直接运行的检测方案包括模型权重、训练曲线、参数配置与源码实现便于快速复现结果、理解缺陷检测流程并在此基础上进行二次开发或论文撰写。1. 喷码缺陷检测为什么值得用机器学习重做一遍产线上喷码字符缺笔画、重影、偏移、糊成一团靠人眼在高速传送带旁边盯着十分钟就开始漏检。传统做法是用模板匹配或者形态学算子字符位置稍微一抖、光照一变误报率立刻飙升调参调到怀疑人生。喷码缺陷检测这个方向之所以适合用机器学习来做核心原因是缺陷形态太多样——断点、拉丝、墨量过重、字符粘连、局部缺失这些用固定规则很难穷举但用数据驱动的方式反而能学到判别边界。Python 源码在这个场景里的价值是把数据标注、模型训练、推理部署串成一条可复现的链路而不是停留在跑通一个 demo。这套东西适合谁适合做计算机视觉方向毕业设计的学生、适合产线视觉工程师想快速验证方案可行性、也适合刚入门机器学习想找一个完整项目练手的人。它解决的不是“能不能检测”而是“能不能稳定检测、能不能快速换型、能不能解释误判”。2. 从喷码图像到可训练数据集采集、标注与增强的完整链路2.1 喷码缺陷检测的数据从哪来、怎么标喷码缺陷检测的第一个翻车点往往不在模型而在数据。产线相机拍出来的原始图背景可能是金属反光、塑料哑光、纸箱粗糙面喷码字符本身对比度也不一样。我一般会先固定相机高度、光源角度和曝光时间拍至少 200 张正常样本和 100 张以上缺陷样本缺陷类型要覆盖断点、重影、偏移、墨量异常这几类。标注方式看任务粒度如果只判断“有缺陷/无缺陷”用文件夹分类即可如果要定位缺陷位置就用 LabelImg 或 X-AnyLabeling 画矩形框存成 YOLO 格式的 txt。标注时注意一个坑字符轻微拉丝但可读的算不算缺陷这个边界必须在标注规范里写死否则不同人标出来的数据互相打架模型学出来就是个玄学。# 目录结构建议分类任务用这个就够 dataset/ ├── train/ │ ├── normal/ # 正常喷码图 │ └── defect/ # 缺陷喷码图 ├── val/ │ ├── normal/ │ └── defect/ └── test/ ├── normal/ └── defect/上面这个结构适合二分类或细分类任务。如果要做目标检测把 train/val/test 下面换成 images 和 labels 两个子目录labels 里放同名的 txt 文件。划分比例我一般用 7:2:1缺陷样本少的时候用 6:2:2保证验证集和测试集里每类缺陷都有足够样本。注意不要随机划分后训练集和验证集出现同一张图的增强版本那会导致验证指标虚高实际部署就露馅。2.2 数据增强参数怎么设才不引入假缺陷喷码缺陷检测的数据增强和普通图像分类不一样有些增强会制造出根本不存在的缺陷。比如随机遮挡如果遮在字符上模型会学成“被遮住就是缺陷”但实际产线没有这种缺陷。我一般只做这几类增强亮度对比度微调±15%、小角度旋转±5°、轻微高斯噪声、水平平移±10 像素。翻转要慎用喷码字符有方向性上下翻转会变成另一个字符。用 Albumentations 写增强管道比较顺手参数也好调。import albumentations as A import cv2 # 喷码缺陷检测增强管道避免引入假缺陷 transform A.Compose([ A.RandomBrightnessContrast( brightness_limit0.15, # 亮度浮动不超过15% contrast_limit0.15, # 对比度浮动不超过15% p0.5 ), A.Rotate(limit5, p0.3), # 小角度旋转模拟相机轻微抖动 A.GaussNoise(var_limit(5.0, 20.0), p0.3), # 轻微噪声模拟传感器噪声 A.ShiftScaleRotate( shift_limit0.1, # 平移不超过10% scale_limit0.0, # 缩放关掉喷码大小由相机固定 rotate_limit0, # 旋转已在上面做过 p0.3 ), ]) # 读取图片并应用增强 image cv2.imread(sample.jpg) augmented transform(imageimage)[image] cv2.imwrite(aug_sample.jpg, augmented)这段代码里每个参数都有实际含义。brightness_limit 和 contrast_limit 控制在 0.15 以内是因为产线光源波动一般不会超过这个范围设大了模型会学到不存在的极端光照。Rotate 的 limit5 对应相机安装时的轻微角度偏差。GaussNoise 的 var_limit 上限 20 是经验值再大字符边缘就糊了模型会把噪声当成缺陷特征。ShiftScaleRotate 里 scale_limit 设 0因为喷码字符在图像中的大小由相机焦距和距离决定缩放会破坏这个先验。增强后的图要人工抽检几十张确认没有把正常字符变成看起来像缺陷的样本这一步不能省。2.3 数据集划分与标签格式转换的边界坑如果原始标注是 VOC 的 xml 格式要转成 YOLO 的 txt转换脚本里有个容易翻车的地方归一化坐标要用图像实际宽高而不是标注文件里写的 width/height因为有些标注工具写的是显示尺寸和实际像素尺寸不一致。转换完要随机抽 10 张图用脚本把框画回原图上看一眼确认框位置对得上。import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_path, img_w, img_h, class_map): tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.findall(object): cls_name obj.find(name).text if cls_name not in class_map: continue cls_id class_map[cls_name] bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # 归一化到 0-1YOLO 格式要求 x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) return lines # class_map 根据你的缺陷类别定义比如 {defect: 0, normal: 1} class_map {defect: 0} xml_file annotations/001.xml img_w, img_h 1280, 720 # 必须用实际图像尺寸 yolo_lines voc_to_yolo(xml_file, img_w, img_h, class_map) with open(labels/001.txt, w) as f: f.write(\n.join(yolo_lines))这段转换逻辑里img_w 和 img_h 必须从对应图像文件读出来不能硬编码也不能信 xml 里的 size 字段。class_map 的类别顺序要和训练时 data.yaml 里的 names 顺序完全一致否则模型学出来的类别是错位的。转换后建议写个校验脚本检查每行是否有 5 个值、坐标是否在 0 到 1 之间、宽高是否大于 0。喷码缺陷检测里小目标多如果框的宽高小于 0.01训练时容易被忽略这种样本要么重新标要么在损失函数里给小目标加权。3. 模型选型与训练从传统机器学习到轻量卷积网络3.1 传统机器学习模型在喷码缺陷检测里还有没有用很多人一上来就上深度学习但喷码缺陷检测如果缺陷特征比较明显、样本量不大传统机器学习模型反而更稳。我一般会先跑一个 HOG SVM 的基线用 OpenCV 提取字符区域的 HOG 特征送进 SVM 做二分类。这个基线训练快、可解释、对光照变化有一定鲁棒性而且能帮你判断数据本身有没有判别性。如果 HOG SVM 的验证集准确率能到 85% 以上说明缺陷在特征层面是可分的再上卷积网络提升空间才大。如果 HOG SVM 只有 60%那要么数据标注有问题要么缺陷定义太模糊先回去修数据。import cv2 import numpy as np from sklearn.svm import SVC from sklearn.metrics import classification_report from skimage.feature import hog def extract_hog(img_path): img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) img cv2.resize(img, (128, 64)) # 统一尺寸喷码区域裁剪后送进来 # HOG 参数orientations9, pixels_per_cell(8,8), cells_per_block(2,2) features hog( img, orientations9, pixels_per_cell(8, 8), cells_per_block(2, 2), block_normL2-Hys ) return features # 假设 train_paths 和 train_labels 已经准备好 X_train np.array([extract_hog(p) for p in train_paths]) y_train np.array(train_labels) X_val np.array([extract_hog(p) for p in val_paths]) y_val np.array(val_labels) clf SVC(kernelrbf, C10, gammascale, class_weightbalanced) clf.fit(X_train, y_train) y_pred clf.predict(X_val) print(classification_report(y_val, y_pred))HOG 的 orientations9 是经典设置对字符边缘方向敏感。pixels_per_cell(8,8) 意味着每个 8x8 像素块统计梯度方向喷码字符笔画宽度一般在 3 到 8 像素这个粒度能抓住笔画边缘。cells_per_block(2,2) 做块归一化抵抗光照变化。SVM 的 C10 是偏大的惩罚系数因为缺陷样本少希望模型更关注错分样本class_weightbalanced 自动按类别频率加权防止正常样本多导致模型偏向正常类。这个基线跑完如果召回率低于 80%先别急着换模型检查一下缺陷样本是不是标注得太粗或者裁剪区域有没有把字符裁掉。3.2 轻量卷积网络训练参数怎么设、什么时候停深度学习模型我一般选 MobileNetV3-Small 或 ShuffleNetV2 这种轻量结构参数量小、推理快适合产线边缘设备。输入尺寸用 224x224 或 128x128喷码字符区域裁剪后送进去。训练用 AdamW学习率 1e-3余弦退火到 1e-5batch size 根据显存来一般 32 或 64。损失函数用带类别权重的交叉熵缺陷样本少的时候权重设 3 到 5 倍。早停策略看验证集 F1连续 10 个 epoch 不提升就停同时保存 F1 最高的权重。import torch import torch.nn as nn from torchvision.models import mobilenet_v3_small from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR # 二分类正常 vs 缺陷 model mobilenet_v3_small(weightsIMAGENET1K_V1) model.classifier[3] nn.Linear(model.classifier[3].in_features, 2) # 类别权重缺陷样本少给更大权重 class_weights torch.tensor([1.0, 4.0]) # 正常:缺陷 1:4 criterion nn.CrossEntropyLoss(weightclass_weights) optimizer AdamW(model.parameters(), lr1e-3, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_max50, eta_min1e-5) # 训练循环核心逻辑 best_f1 0.0 patience 10 no_improve 0 for epoch in range(100): model.train() for imgs, labels in train_loader: optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() scheduler.step() # 验证阶段计算 F1 model.eval() # ... 推理并计算 f1_score ... if val_f1 best_f1: best_f1 val_f1 torch.save(model.state_dict(), best_model.pth) no_improve 0 else: no_improve 1 if no_improve patience: print(fEarly stop at epoch {epoch}) break这里有几个参数需要根据实际情况调。class_weights 的 [1.0, 4.0] 是经验值如果缺陷样本占比低于 10%可以调到 [1.0, 8.0] 甚至更高但别超过 10否则模型会把正常样本也判成缺陷误报率飙升。学习率 1e-3 对 AdamW 是常用起点如果 loss 震荡厉害就降到 5e-4。CosineAnnealingLR 的 T_max 设成总 epoch 数让学习率平滑降到接近 0。早停的 patience10 是防止过拟合喷码缺陷检测数据集通常不大过拟合来得快。保存权重时只存 state_dict不要存整个模型方便后续换结构加载。3.3 训练过程监控看什么指标、怎么判断模型是不是学歪了训练时不能只看 loss要看验证集的精确率、召回率和 F1。喷码缺陷检测里召回率比精确率更重要因为漏检一个缺陷流到客户手里代价比误报一个正常品高得多。如果召回率高但精确率低说明模型把很多正常品判成缺陷这时候可以适当降低缺陷类权重或者提高分类阈值。如果召回率低说明模型对缺陷不敏感要么增加缺陷样本要么用 focal loss 替代交叉熵。另外要画混淆矩阵看看缺陷被误判成正常的多还是正常被误判成缺陷的多这两个方向的修正策略完全不同。训练日志里还要记录每个 epoch 的学习率确认余弦退火有没有正常生效。4. 推理部署与产线集成从单张图片到实时视频流4.1 单张图片推理脚本与阈值调优训练完模型先写一个单张图片推理脚本确认模型在测试集上的表现和训练时一致。推理时要做和训练一样的预处理裁剪喷码区域、缩放到 224x224、归一化。输出用 softmax 转成概率然后设一个阈值判断是否缺陷。阈值不是固定 0.5要根据验证集上的精确率-召回率曲线来选。如果产线对漏检零容忍阈值可以降到 0.3牺牲一点精确率换召回率。import torch import torch.nn.functional as F from PIL import Image from torchvision import transforms # 推理预处理必须和训练时一致 infer_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) def predict(image_path, model, threshold0.5): img Image.open(image_path).convert(RGB) tensor infer_transform(img).unsqueeze(0) # 增加 batch 维度 model.eval() with torch.no_grad(): logits model(tensor) probs F.softmax(logits, dim1) defect_prob probs[0, 1].item() # 假设索引1是缺陷类 label defect if defect_prob threshold else normal return label, defect_prob # 加载模型 model mobilenet_v3_small() model.classifier[3] nn.Linear(model.classifier[3].in_features, 2) model.load_state_dict(torch.load(best_model.pth, map_locationcpu)) model.eval() label, prob predict(test_sample.jpg, model, threshold0.35) print(f预测: {label}, 缺陷概率: {prob:.4f})这段代码里 threshold0.35 是偏保守的设置适合漏检代价高的场景。Normalize 的 mean 和 std 用的是 ImageNet 的统计值因为模型是在 ImageNet 预训练权重上微调的训练时也用了同样的归一化推理必须保持一致否则输入分布偏移会导致概率失真。map_locationcpu 是为了在没有 GPU 的机器上也能加载。如果产线用 GPU 推理去掉这个参数并确保模型和权重都在同一设备上。4.2 视频流实时检测的帧率与延迟控制产线相机一般是 30fps 或 60fps如果每帧都跑一遍模型MobileNetV3-Small 在 CPU 上大概 20 到 30ms 一帧勉强能跟上 30fps但加上预处理和后处理延迟会累积。我一般用两种策略一是跳帧检测每 3 帧检一次中间帧用上一帧结果二是用 ONNX Runtime 或 TensorRT 加速把推理时间压到 10ms 以内。跳帧策略要注意如果传送带速度快跳帧可能导致漏检这时候要保证相机曝光时间足够短避免运动模糊。import cv2 import onnxruntime as ort import numpy as np # 加载 ONNX 模型比 PyTorch 原生推理快 session ort.InferenceSession(model.onnx) input_name session.get_inputs()[0].name cap cv2.VideoCapture(0) # 产线相机索引 frame_count 0 last_result normal while True: ret, frame cap.read() if not ret: break frame_count 1 # 每3帧检测一次 if frame_count % 3 0: # 裁剪喷码区域这里假设 ROI 已固定 roi frame[100:300, 400:800] roi cv2.resize(roi, (224, 224)) roi roi.astype(np.float32) / 255.0 roi (roi - np.array([0.485, 0.456, 0.406])) / np.array([0.229, 0.224, 0.225]) roi np.transpose(roi, (2, 0, 1)).unsqueeze(0) if hasattr(np, unsqueeze) else np.expand_dims(np.transpose(roi, (2, 0, 1)), 0) outputs session.run(None, {input_name: roi.astype(np.float32)}) probs np.exp(outputs[0]) / np.sum(np.exp(outputs[0])) last_result defect if probs[0, 1] 0.35 else normal # 在画面上显示结果 cv2.putText(frame, last_result, (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 0, 255), 2) cv2.imshow(Inspection, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()ONNX Runtime 的推理速度比 PyTorch 原生快 1.5 到 2 倍适合产线部署。跳帧策略里 frame_count % 3 表示每 3 帧检一次中间帧沿用上次结果。ROI 裁剪的坐标要根据实际相机视野来定喷码区域在画面中的位置固定后直接硬编码坐标比每帧做目标检测更快。归一化那一步的 mean 和 std 必须和训练一致ONNX 模型不会自动做这个。如果产线要求每帧都检就把跳帧去掉同时用 TensorRT 或 OpenVINO 进一步加速具体选哪个看硬件平台。4.3 误报和漏报的现场排查方法部署到产线后误报和漏报是必然出现的关键是怎么快速定位。我一般会在推理脚本里加一个日志功能把每张判为缺陷的图存下来同时记录缺陷概率。误报多的时候先看这些误报图的缺陷概率分布如果集中在 0.35 到 0.5 之间说明阈值设低了调高到 0.5 或 0.6 试试。如果误报图看起来和正常图没区别那可能是训练集里缺少这类正常样本需要补数据重新训练。漏报多的时候看漏报图的缺陷概率如果都在 0.2 以下说明模型根本没学到这类缺陷特征要么缺陷样本太少要么缺陷类型和训练集里的差异太大。现场排查时把误报和漏报的图各存 50 张按缺陷类型分类很快就能看出问题在哪。5. 喷码缺陷检测落地避坑5 个血泪教训5.1 现象验证集准确率 99%产线误报率 30%原因训练集和验证集是从同一批图片里随机划分的同一张图的增强版本同时出现在训练集和验证集里模型记住了增强后的特征但产线光照和角度和训练集不一样。解决按拍摄批次划分数据集同一批次拍的图要么全在训练集要么全在验证集。另外验证集要用产线实际环境拍的图不能用实验室补光拍的图。5.2 现象模型对某类缺陷完全检不出原因这类缺陷在训练集里只有个位数样本模型被正常样本主导根本没学到这类特征。解决要么补拍这类缺陷至少 50 张要么用 few-shot 方法把这类缺陷和已知缺陷做相似度匹配。如果补不了数据就在损失函数里给这类缺陷单独加大权重但效果有限补数据才是根本。5.3 现象推理结果和训练时对不上概率全是 0.5 左右原因推理时的预处理和训练时不一致最常见的是归一化参数不同、输入尺寸不同、或者通道顺序从 RGB 变成了 BGR。解决把训练时的预处理代码单独封装成一个函数推理时直接调用同一个函数不要重新写一遍。用 OpenCV 读图默认是 BGR送进模型前要转成 RGB这个坑几乎每个人都踩过。5.4 现象模型在 CPU 上推理太慢跟不上产线节拍原因用了 ResNet50 或 VGG 这类大模型参数量大CPU 推理一帧要 100ms 以上。解决换成 MobileNetV3、ShuffleNetV2 或 EfficientNet-Lite参数量降到 1M 到 5MCPU 推理能压到 20ms 以内。如果还不行用 ONNX Runtime 或 OpenVINO 量化成 INT8速度还能再快一倍精度损失一般在 1% 以内。5.5 现象换了一批喷码墨水模型全部判成缺陷原因墨水颜色或反光特性变了图像整体色调偏移模型没见过这种分布。解决在训练集里加入不同墨水、不同光照条件的样本做数据增强时加入色调抖动hue 微调 ±5。如果换墨水频繁考虑用灰度图训练减少颜色通道的影响同时用直方图均衡化做预处理让不同墨水的图像分布更接近。6. 把模型压到 1MB 以内量化与剪枝的实操技巧模型量化是喷码缺陷检测部署到边缘设备的关键一步。我一般用 PyTorch 的动态量化把 Linear 和 Conv 层权重从 FP32 转成 INT8模型大小直接降到原来的四分之一推理速度提升 1.5 到 2 倍精度损失通常不到 1%。动态量化不需要校准数据一行代码就能跑适合快速验证。如果动态量化精度掉得多再用静态量化需要准备 100 到 200 张校准图精度保持更好。import torch import torch.quantization # 加载训练好的模型 model mobilenet_v3_small() model.classifier[3] nn.Linear(model.classifier[3].in_features, 2) model.load_state_dict(torch.load(best_model.pth, map_locationcpu)) model.eval() # 动态量化权重转 INT8激活值推理时动态量化 quantized_model torch.quantization.quantize_dynamic( model, {nn.Linear, nn.Conv2d}, # 量化这些层 dtypetorch.qint8 ) # 保存量化模型 torch.save(quantized_model.state_dict(), quantized_model.pth) # 对比模型大小 import os print(f原始模型: {os.path.getsize(best_model.pth) / 1024:.1f} KB) print(f量化模型: {os.path.getsize(quantized_model.pth) / 1024:.1f} KB)这段代码里 quantize_dynamic 的第二个参数指定要量化的层类型Linear 和 Conv2d 是主要计算层量化收益最大。dtypetorch.qint8 表示权重用 8 位整数存储。量化后的模型在 CPU 上推理会自动用 INT8 指令集加速。注意量化后的模型不能再训练只能推理。如果量化后精度掉超过 2%检查一下是不是某些层对量化敏感可以把这些层排除在量化范围外只量化全连接层。剪枝是另一个压缩手段但喷码缺陷检测里我不太推荐激进剪枝因为缺陷特征本身就很细微剪掉太多通道容易把关键特征也剪没。如果非要做用 L1 范数剪枝剪掉权重最小的 20% 通道然后微调 10 个 epoch 恢复精度。剪枝加量化组合起来模型能压到 1MB 以内适合嵌入式设备部署。验证量化模型时不能只看准确率要看量化前后在同一测试集上的预测结果一致性如果某些图量化前判缺陷、量化后判正常这些图要单独拿出来分析确认是不是量化误差导致的边界翻转。我一般会保留量化前的模型作为备份产线部署先用量化模型跑一周对比两者的判定差异确认没有系统性偏差后再完全切换。这个习惯帮我避免过好几次因为量化导致的批量漏检希望帮到你。本文还有配套的精品资源点击获取
