
做机器视觉这行的朋友应该没少被“缺陷检测”折腾过。工件表面的划痕、麻点、脏污靠人工肉眼盯着产线看累不说漏检率还下不来。我去年在做一个金属零件出厂质检项目时就遇到了同样的困境客户要求检测节拍要快缺陷种类又杂现场环境光照还不稳定。后来我直接把方案换成了YOLOv8配合PyQt5做了个可视化检测界面整套流程从数据标注到训练、再到界面部署跑通之后效果立竿见影。这篇文章就把我完整的实操过程分享出来包括环境配置、数据集制作、训练调参、界面开发以及一路踩过的坑希望能给正在做类似项目的朋友省点时间。1. 项目整体设计与思路拆解1.1 核心需求解析工业零件表面缺陷检测场景上有几个鲜明的特点。第一缺陷种类多但每类样本稀少比如划痕可能成百上千个样本而凹坑、裂纹可能只有几十个第二缺陷尺寸通常很小甚至只有几十个像素属于典型的小目标检测第三产线对推理速度有硬性要求单张图片的处理时间往往要控制在几十毫秒以内。这些特性决定了YOLOv8这类单阶段目标检测器非常合适。那为什么是YOLOv8而不是Faster R-CNN或者SSD主要原因是它在速度和精度的平衡上做得最稳。Faster R-CNN精度高但速度慢SSD速度快但小目标召回率一般而YOLOv8在COCO数据集上的mAP和推理速度综合表现都很好尤其是对小目标的检测能力比前代YOLOv5提升了一个档次。再加上ultralytics官方把训练、验证、导出代码封装得非常干净基本开箱即用不用自己去拼一堆脚本。这个系统的完整链路是先准备并标注缺陷数据集然后训练YOLOv8目标检测模型最后把训练好的权重接进PyQt5界面实现图片、视频、摄像头实时检测。界面部分负责交互模型部分负责核心推理两者通过一个轻量级的推理线程衔接互不干扰。1.2 技术选型为什么是YOLOv8 PyQt5我先说说选型时的思考过程。模型这块YOLOv8在2023年初发布以后社区活跃度非常高模型结构上引入了C2f模块和Anchor-Free检测头收敛速度和精度都优于YOLOv5。而且ultralytics提供了丰富的预训练权重从n、s、m、l到x五个尺寸可以根据显卡性能灵活选择。对于工业部署场景我一般用yolov8n或yolov8s在GTX 1660 Ti这种中端卡上也能跑出不错的帧率。界面这块PyQt5的优势在于跨平台、控件丰富、开发效率高而且和OpenCV、NumPy的配合非常顺滑。比如用cv2读到的图像是BGR格式的numpy数组转成QImage再显示到QLabel上代码就几行。你要是换了其他的GUI框架比如Tkinter做图像显示和缩放就会很别扭用C Qt的话开发周期又太长不适合快速迭代。1.3 系统的功能模块划分整个系统从功能上可以拆成四个模块数据模块、训练模块、推理模块和界面模块。数据模块负责标注格式转换、数据划分和增强训练模块封装了YOLOv8的训练流程支持自定义参数和权重导出推理模块加载模型权重处理输入图像并输出检测结果界面模块负责用户交互包括加载模型、选择检测源、显示结果、保存日志等。这四个模块的依赖关系是单向的数据模块不依赖其他模块训练模块依赖数据模块的输出推理模块加载训练模块产出的权重界面模块调用推理模块的接口。这样分层的好处是每个模块可以独立测试和替换比如你以后想换ResNet或者ViT做特征提取只需要改推理模块内部实现界面完全不用动。2. 环境配置先把坑趟平2.1 一套能直接用的大环境清单环境配置是这个项目最烦人的环节没有之一。我前后折腾过三台机器总结出一套相对稳妥的配置组合操作系统Windows 10/11 或 Ubuntu 20.04/22.04我实测两者都能跑通Python版本3.8~3.10注意不要用3.11以上的版本有些依赖包还没完全适配CUDA11.8对应cuDNN 8.6PyTorch2.0.1或2.1.0ultralytics8.0.x以上版本推荐8.1.0PyQt55.15.9或5.15.10OpenCV-Python4.8.1.78其他常用库numpy1.23pandasmatplotlibtqdm我建议先把CUDA和PyTorch的匹配关系理清楚。具体操作是先去NVIDIA官网下载CUDA 11.8安装包装完后在命令行输入nvidia-smi确认显卡驱动识别正常然后用pip安装PyTorchpip install torch2.0.1cu118 torchvision0.15.2cu118 --extra-index-url https://download.pytorch.org/whl/cu118装完之后跑一句Python验证CUDA是否可用import torch print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))如果输出True和你的显卡型号就说明PyTorch的GPU版本装对了。这里有个常见的坑如果你直接用pip install torch默认装的是CPU版本训练速度会慢得让人怀疑人生。别问我怎么知道的。2.2 PyQt5安装的几道坎PyQt5的pip安装本身很顺利pip install PyQt5一行搞定但真正折磨人的是装完以后界面显示不出来。我遇到过两种情况第一种是程序启动后界面闪一下就不见了第二种是直接报OpenGL相关的错误。这里必须先解释一个底层原因PyQt5的高版本默认依赖OpenGL如果你的电脑显卡驱动装得不对或者使用的是远程桌面、虚拟机环境OpenGL上下文创建就会失败界面自然就黑屏或者崩掉了。我当时的排查思路是先升级显卡驱动再安装OpenGL相关的依赖库pip install PyOpenGL PyOpenGL_accelerate如果还是不行有一个从社区学来的workaround在创建QApplication之前设置环境变量import os os.environ[QT_OPENGL] software这样会强制Qt使用软件渲染模式虽然渲染速度慢一些但能解决90%的界面无显示问题。我当时在远程桌面环境里就是靠这一行代码把界面救回来的。2.3 版本兼容性的血泪教训版本兼容性是环境配置里最容易翻车的环节。我这里列一张表是我踩过坑以后整理出来的组件推荐版本避坑提醒Python3.103.11以上某些依赖没有wheel包PyTorch2.0.1cu118不要装2.2以上ultralytics有些钩子API会变ultralytics8.1.08.2之后部分参数名有调整PyQt55.15.105.15.7之前opengl问题更严重opencv-python4.8.1.78不要装contrib版体积大且容易冲突numpy1.24或1.26不要用2.xopencv和ultralytics都会报错特别是numpy这个坑我有一次装新环境时顺手装了numpy 2.0结果ultralytics直接报错说找不到np.float之类的属性回退到1.26才恢复正常。所以如果你也是从零开始配环境最好严格按照上表来别用pip的时候图省事给关键库加个版本限制。3. 数据集制作训练效果的分水岭3.1 工业缺陷数据从哪来很多朋友问过我工业零件表面缺陷的数据集到底去哪找说实话工业场景的数据集公开的非常少因为每家的零件形态、缺陷类型都不一样。如果你做的是轴承、齿轮、电机这类标准零件可以去搜一下CWRU轴承数据集、德国KITTI相关的工业视觉数据集但更常见的情况是你得自己攒数据。我当时的做法是从客户现场拿一批真实拍摄的零件表面图数量大概在500张左右然后每张图做多种数据增强旋转、平移、翻转、亮度扰动一层一层扩充到3000多张再手工标注。这里面有一个原则必须遵守增强操作不能引入虚假缺陷。比如你不能做随机的椒盐噪声增强否则模型会把噪声当成真实缺陷学进去导致误检率高得离谱。3.2 用LabelImg标注并转为YOLO格式标注工具我推荐LabelImg轻量、稳定、启动快。安装方式很简单pip install labelImg打开后设置YOLO格式的标注模式用矩形框把缺陷区域框起来每张图标注完会生成一个同名.txt文件。YOLO格式的标注内容是类别id、中心点x坐标、中心点y坐标、框宽、框高五个数字都用归一化来表示取值范围在0到1之间。这里有一个很关键的细节YOLO的归一化坐标是用像素坐标除以图片宽高得到的如果是小数也不碍事训练时模型自己会处理。但如果你用的是Pascal VOC格式的XML或者COCO格式的JSON需要先做一个格式转换别直接把LabelImg的VOC标注喂给ultralytics会直接报错。转换脚本我贴在下面可以参考import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_file, out_file, class_names): tree ET.parse(xml_file) root tree.getroot() img_w int(root.find(size).find(width).text) img_h int(root.find(size).find(height).text) with open(out_file, w) as f: for obj in root.iter(object): cls_name obj.find(name).text if cls_name not in class_names: continue cls_id class_names.index(cls_name) bbox obj.find(bndbox) x1 float(bbox.find(xmin).text) y1 float(bbox.find(ymin).text) x2 float(bbox.find(xmax).text) y2 float(bbox.find(ymax).text) # 转YOLO格式顺便做边界裁剪 x_center ((x1 x2) / 2) / img_w y_center ((y1 y2) / 2) / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h f.write(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}\n)3.3 数据划分与类别平衡数据做好以后需要按比例划分成训练集、验证集和测试集。我的习惯是7:2:1训练集用来拟合参数验证集用来监控过拟合和调参测试集只做最终评估。划分时注意两点一是按文件夹把图片和标签一起划分别拆散二是尽量保证每个类别在每个集合里都有一定数量的样本避免验证集里某个类别一个样本都没有指标失真。类别不平衡是工业缺陷检测里非常头疼的问题。我当时的项目里有三个缺陷类别划痕、麻点、脏污。划痕样本占了60%麻点只有15%。如果不做任何处理模型最终会对麻点几乎无感漏检率很高。解决办法有两个一是用image augmentation做在线增强给少数类加大增强概率二是在损失函数里调整类别权重。ultralytics里没有直接暴露类别权重参数但可以直接修改数据集yaml文件把少数类样本复制几份再做训练算是一种简单的过采样策略。4. YOLOv8模型训练与调优4.1 数据配置与训练启动训练前需要准备两个关键文件一个是数据集配置文件data.yaml一个是模型配置文件。data.yaml的内容长这样path: D:/projects/defect_yolov8 train: images/train val: images/val test: images/test nc: 3 names: [scratch, pit, stain]这里path是数据集根目录train/val/test填的是相对路径ultralytics会自动拼接。nc是类别数names是类别名字列表。重点提醒names的顺序必须和标注时LabelImg里设置的类别顺序一致否则类别对应关系会全乱。训练命令很简单yolo detect train datadata.yaml modelyolov8n.pt epochs200 imgsz640 batch8 lr00.01 patience30我解释一下这几个参数的含义。modelyolov8n.pt是加载预训练权重这比从头训练快得多因为模型在COCO上已经学会了通用的特征表达。imgsz640是输入图片尺寸虽然可以用512或768但工业缺陷往往是小目标我实测640比512的mAP高不少。batch8是受限于显卡显存的如果你是8GB显存就老老实实用8硬调大会爆显存。4.2 训练过程中的监控与判断训练跑起来以后ultralytics会实时输出每个epoch的metrics包括box_loss、cls_loss、dfl_loss、precision、recall和mAP50等。这里很多人只看mAP其实我更习惯看loss曲线的下降趋势。正常情况下train loss和val loss应该是同步下降的如果train loss持续下降但val loss开始回升就说明过拟合了就该考虑加正则化或者缩小模型。找最好的权重文件是一个很多人都忽略的细节。ultralytics训练结束后会在runs/detect/train目录下生成多个pt文件其中best.pt是验证集上指标最好的权重last.pt是最后一个epoch的权重。我自己倾向于用best.pt而不是last.pt因为last往往已经过拟合或者训练过头了。训练结束以后可以用下面这段代码做一次验证测试from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) metrics model.val(datadata.yaml, splittest) print(metrics.box.map50) print(metrics.box.map) # mAP50-95如果mAP50能达到85%以上对于工业质检场景一般够用了。注意工业场景对漏检的容忍度非常低你宁可将精度降一些也要把recall提上去尽量让所有的缺陷都被找出来。4.3 损失函数曲线图的绘制看训练趋势更直观的方法是画loss曲线。ultralytics训练完会在results.png里自动生成各类曲线图但我还是习惯自己读训练日志再画一次因为可以统一坐标轴方便多轮实验对比。实现思路是训练日志会记录每个epoch的loss值把文本日志解析成DataFrame再用matplotlib画曲线。核心代码如下import matplotlib.pyplot as plt import pandas as pd # 从runs/detect/train目录下的results.csv读取 df pd.read_csv(runs/detect/train/results.csv) epochs df[epoch] train_box_loss df[train/box_loss] val_box_loss df[val/box_loss] plt.figure(figsize(10, 6)) plt.plot(epochs, train_box_loss, labeltrain box_loss) plt.plot(epochs, val_box_loss, labelval box_loss) plt.xlabel(epoch) plt.ylabel(loss) plt.legend() plt.grid(True) plt.savefig(loss_curve.png, dpi300)从loss曲线里能看出很多问题。比如val loss在epoch 80左右出现明显回升说明该early stopping了比如train和val loss的差距一直很大说明模型过拟合如果两条曲线都在高位震荡不下降那大概率是学习率设置不对或者数据本身有问题。4.4 模型评估与导出训练完以后我习惯先跑一遍测试集看一下每张图的实际检测效果。可以用下面这个命令可视化预测结果yolo detect predict modelbest.pt sourcetest_images saveTrue conf0.25这里conf0.25是置信度阈值低于这个分数的不显示。工业场景下我会把阈值适当调高到0.4宁可漏掉一些轻微缺陷也要保证检出来的都是真缺陷否则现场误触发报警比漏检还烦人。如果测试效果可以接着就是模型导出导出为ONNX以便后续做TensorRT加速部署yolo export modelbest.pt formatonnx opset12导出后可以用onnxruntime做推理验证确认输出格式正确再继续做界面集成。我之前在这个环节踩过一个坑导出ONNX时默认opset12在TensorRT8.6里有些算子不支持建议改成opset11或13比较稳。5. PyQt5界面开发与推理集成5.1 界面功能规划PyQt5界面是整个系统面向用户的部分所以交互逻辑要尽量简单清晰。我设计的界面分成四个区域左上角是模型加载和参数设置区右上角是检测结果显示区下方是操作按钮和日志信息区侧边是检测历史列表。功能上至少需要这些加载模型权重文件.pt或.onnx选择检测源单张图片、视频文件、摄像头实时画面显示检测结果在原图上绘制边界框和类别标签一键保存检测结果图片或导出检测报告实时显示帧率和检测耗时我建议用QThread把推理放到后台线程避免界面卡死。实际开发中很多新手直接在主线程里跑YOLO推理导致界面拖动都卡成了幻灯片记得一定要用线程。5.2 推理线程与信号槽传递QThread的标准用法是写一个继承自QThread的类在run方法里做推理循环用pyqtSignal把结果传回主线程。我这里贴一个最核心的推理线程模板from PyQt5.QtCore import QThread, pyqtSignal import cv2 import numpy as np class InferThread(QThread): result_ready pyqtSignal(dict) def __init__(self, model, sourcecamera, parentNone): super().__init__(parent) self.model model self.source source self.running True def run(self): if self.source camera: cap cv2.VideoCapture(0) while self.running: ret, frame cap.read() if not ret: break results self.model(frame) # 把检测结果打包成dict发回主线程 boxes results[0].boxes.xyxy.cpu().numpy() confs results[0].boxes.conf.cpu().numpy() cls_ids results[0].boxes.cls.cpu().numpy().astype(int) self.result_ready.emit({ frame: frame, boxes: boxes, confs: confs, cls_ids: cls_ids }) cap.release() else: # 读取视频帧或单张图片逻辑类似 pass主线程里只需要负责显示结果用cv2和QImage做一个颜色空间转换再setPixmap上就行。色彩转换这里有个常见的坑OpenCV读图是BGR顺序如果不转换成RGB显示出来的图片颜色会偏蓝偏暗。正确做法是用cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)转一下再塞给QImage。5.3 界面与检测结果的联动展示检测结果有一个很重要的细节图像缩放。工业零件相机拍出来的图往往很大一个像素尺寸可能超过2000x2000但界面显示区域有限。如果你直接把原图缩小显示检测框的坐标和字体大小也会跟着缩最后字小到根本看不清。我的做法是固定显示区域尺寸比如640x480然后在绘制结果时按显示区域的缩放比例同步缩放框的坐标和文字大小。绘制代码大致是def draw_boxes_on_frame(frame, boxes, confs, cls_ids, names, show_size(640, 480)): h, w frame.shape[:2] scale_x show_size[0] / w scale_y show_size[1] / h dst cv2.resize(frame, show_size) for box, conf, cls_id in zip(boxes, confs, cls_ids): x1, y1, x2, y2 box # 坐标按显示区域比例缩放 x1s, y1s int(x1 * scale_x), int(y1 * scale_y) x2s, y2s int(x2 * scale_x), int(y2 * scale_y) color (0, 0, 255) cv2.rectangle(dst, (x1s, y1s), (x2s, y2s), color, 2) label f{names[cls_id]}: {conf:.2f} cv2.putText(dst, label, (x1s, max(0, y1s - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1) return dst如果想把标注过的图片保存下来记得按原图尺寸重新绘制一遍不要在缩放的图上保存否则导出的图片质量太差没法做成检测报告。5.4 PyQt5界面适配分辨率的问题界面做出来以后在1920x1080的屏幕上看着挺好换到1366x768的笔记本上按钮都挤到一起了。这是因为我没有考虑DPI缩放的问题。解决方法是设置Qt的全局缩放策略在创建QApplication之前加上from PyQt5.QtCore import Qt, QCoreApplication QCoreApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) QCoreApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)然后在QVBoxLayout和QHBoxLayout里尽量使用strech参数让控件能根据窗口大小自动拉伸而不是写死坐标。尤其是结果展示的那块用setMinimumSize和setScaledContents配合可以让QLabel里的图片适应窗口缩放了但要注意锁住宽高比否则图片会被拉伸变形。6. 部署实践与性能优化6.1 模型加速从PyTorch到TensorRT训练出的best.pt是PyTorch格式在实际质检产线上推理速度可能不够快。以GTX 1660 Ti为例yolov8s处理一张640x640的图大概需要25毫秒左右在部分高节拍产线上已经有点吃紧了。这时就需要导出TensorRT模型利用FP16量化把推理速度提升一倍以上。导出流程是先用前文提到的方式转ONNX然后用TensorRT的trtexec工具转enginetrtexec --onnxbest.onnx --saveEnginebest.engine --fp16 --workspace4096转换完成后在界面推理线程里加载engine文件替换PyTorch模型即可。有一个地方值得注意TensorRT推理时输入输出张量需要自己处理不像ultralytics那样一行代码搞定。如果不想造轮子可以用ultralytics自带的trt支持在YOLO类里直接model.predict(frame)但需要确保装对TensorRT版本。6.2 边缘设备部署RK3588上的落地经验除了工控机很多客户会要求在边缘计算设备上部署比如正点原子的RK3588开发板。RK3588的NPU对常见的检测模型支持还不错但需要把模型转成RKNN格式过程比TensorRT要曲折一些。先要在PC端安装rknn-toolkit2然后用Caffe、ONNX或PyTorch格式转成RKNN。这里一个关键的注意点是RKNN转换不支持所有的YOLOv8算子需要在转换时开启量化感知训练或者在模型结构里避免一些不太好转换的动态算子。如果你完全跟着官方文档走大概率会卡在某个op的报错上我的建议是先用yolov8n转一次确认链路通了再去换大模型别一上来就转最佳的精度配置排查时间会翻倍。6.3 界面推理视频流时的性能瓶颈做视频流或摄像头实时检测时推理线程和显示线程是两个环节瓶颈不一定在模型本身。我当时排查过一个很典型的问题模型帧率能到40 FPS但界面上显示的只有10 FPS。后来发现原因是主线程用了QTimer定时刷新整个QLabel产生了不必要的重复绘制。解决办法是只在新结果到达时触发刷新不要固定频率刷新。另外视频流解码和推理之间可以做一个带队列的生产者-消费者模型设置队列最大长度满了就丢弃最旧的帧这样能保证推理永远处理的是最新的画面而不是堆积的旧帧。这个思路对实时产线检测特别重要因为检测结果一旦有延迟现场抓拍和报警就会整体滞后。7. 常见问题与排查技巧实录7.1 PyQt5界面无显示或黑屏这个我在前面环境配置里提到过再做一个完整的排查流程。第一步先看是不是OpenGL上下文创建失败运行程序时如果命令行输出QOpenGLContext相关的错误就说明和显卡驱动有关把显卡驱动升级到最新版。第二步设置QT_OPENGLsoftware强制用软件渲染。第三步如果还是黑屏检查是不是装了多个PyQt5版本在pip list里看一下PyQt5和PyQt5-Qt5的版本号是否匹配不一致的话卸载重装。7.2 训练时loss不下降或为nanloss为nan是训练最恶心的问题之一。常见原因有三个一是学习率设得太大初始lr超过0.01容易导致梯度爆炸降到0.001试一下二是数据标签里出现了越界的检测框坐标比如x_center大于1ultralytics会尝试归一化但也会产生无效梯度用脚本检查一下标注文件里的坐标范围三是batch内出现了空标签也就是某张图没有缺陷标注而yaml里又配置了必须检测这时候把数据检查一遍确认每张训练图的txt文件都至少有一个标注框。7.3 检测精度差误检漏检多如果训练完的mAP50不高或者现场误检多第一步不是改模型而是去看数据。我遇到最多的情况是不同批次拍摄的图像光照不一致导致同一缺陷在不同图片里看起来差别很大。解决办法是在数据增强里加亮度、对比度扰动模拟不同光照环境另一个方向是做图像预处理统一色温比如在推理前做CLAHE直方图均衡化可以显著减少光照差异带来的误检。7.4 推理速度慢推理慢要分三段排查输入图像的缩放、模型推理本身、结果后处理。第一段检查是不是直接把原始大图送进模型了YOLOv8会自动缩放但如果原图长宽比极端会浪费大量算力在填充区域可以自己先裁剪一下。第二段把模型转成TensorRT参照6.1节。第三段如果用的是PyTorch模型后处理里有很多numpy操作可以尽量用yolov8自带的predict封装它内部做了很多优化比自己手写处理要快。7.5 标注报错或格式错乱标注文件里最常见的问题是类别ID越界和坐标越界。比如data.yaml里nc3标注时却写了一个类别ID 3训练会直接报错。我的建议是训练前先写一个脚本扫描所有标注文件检查每个txt里的类别ID是否在合法范围内以及坐标值是否都在0~1之间。还有一个容易忽视的细节如果图片是黑白图LabelImg在保存时可能输出不完全一样的标签格式需要统一处理成RGB三通道否则训练到一半会报通道数不匹配。我在这次工业零件缺陷检测系统的开发中最深的一个体会是整个项目里最重要的不是模型选型也不是界面美化而是数据本身。YOLOv8这个模型足够强大数据质量差的话换什么模型都救不回来。做工业视觉项目前期花在数据清洗、标注规范、光照统一上的时间往往比模型训练多好几倍但这也恰恰是项目能不能真正落地到产线的关键。最后再分享一个我实践中的小技巧在PyQt5界面上可以加一个实时置信度滑条和类别过滤的复选框现场调试设备时经常需要临时调一下阈值或者只看某一类缺陷有这个功能就不用一边改代码一边重启程序了。这种做法在客户现场演示和验收时非常加分整个系统的实用性和灵活性一下就体现出来了。