在工业质检场景里摸爬滚打这几年,我做过最多的就是用目标检测模型去解决那些肉眼看得累、又不得不看的检测需求。今天要聊的这个项目——基于YOLOv8配合PyQt5搭建的焊缝质量检测系统,就是其中相当典型的一类。核心目标非常直白:把工业金属品外观瑕疵检测这件事,从依赖老师傅目检逐步变成机器自动识别,同时把检测结果可视化地呈现给产线操作人员。项目涉及 Python 源码、PyQt5 界面、标注数据集和整套训练代码,适合正在做工业视觉方向毕业设计、或者刚进入制造企业想用深度学习解决质检问题的朋友参考。
这个系统能做什么,我用一句话概括:给摄像头画面或者一张金属焊缝图片,模型自动标出气孔、裂纹、咬边、夹渣这些缺陷的位置和类别,界面实时显示坐标和置信度,检测结果还可以一键导出。整体来说,这类项目最核心的难点不在模型训练本身,而在于数据标注是否规范、界面交互是否顺手、模型误检漏检是否可控。下面我把整个项目的设计思路和实操过程完整拆开讲。
1. 项目背景与整体设计思路
1.1 这个系统到底要解决什么难题
在金属结构件制造环节,焊接几乎是绕不开的工艺。问题在于,焊接过程受电流、电压、焊丝送进速度、母材清洁度、操作者手法的影响很大,产生缺陷几乎是必然事件,只是概率高低不同。常见的焊缝表面缺陷包括裂纹、气孔、咬边、夹渣、烧穿、未熔合、飞溅等,这些缺陷直接影响结构强度和产品寿命。
传统质检主要靠人工目检或者抽检。人工目检的问题很明显:第一,效率低,一块焊缝密集的构件,老师傅逐条看下来需要不少时间;第二,标准不稳定,同一个缺陷不同人判定的结果可能有差异,同一个人疲劳状态下也会漏检;第三,岗位流动性大,经验积累难以复制。产线如果追求全检,视觉检测系统的价值就会立刻体现出来。
我当时接这个项目时,现场的需求很明确:一套离线检测工具,操作工把焊缝照片拖进界面,软件马上圈出缺陷位置和类型,同时给出置信度,方便质检员二次确认。不需要接入 PLC,不需要联动机械臂,先把“自动识别+可视化”这一环跑通。于是技术路线就清晰了:深度学习目标检测负责找缺陷,PyQt5 负责做桌面界面。
1.2 选型逻辑:为什么是 YOLOv8 加 PyQt5 的组合
目标检测框架的选择,我当时其实把 YOLOv5、YOLOv8、YOLOv9、YOLOv10 都对比过。最终选择 YOLOv8 不是因为它一定比其他版本精度高,而是综合考虑了三个现实因素。
第一是生态成熟度。Ultralytics 官方对 YOLOv8 的维护更新很稳定,文档齐全,pretrained 权重丰富,网上能查到的踩坑案例也最多。做工业项目最怕卡在某个 bug 上好几个小时还找不到解决方案,选生态成熟的版本,本质上是给自己买了一份“隐形保险”。
第二是部署和迁移的便利性。YOLOv8 的 Python 接口高度统一,训练的模型可以直接导出 ONNX 格式,后续就算要换到其他推理框架,或者用 OpenCV DNN 做边缘部署,路径都很短。工业项目的生命周期往往很长,今天做的是桌面工具,明天可能要上嵌入式的盒子,模型格式的兼容性很重要。
第三是精度的均衡性。焊缝表面缺陷本身尺寸跨度大,大裂纹几十厘米,小气孔只有几个像素。YOLOv8 从 n/s/m/l 多个尺寸都有,可以针对硬件算力灵活切换。我在实际项目中用的 YOLOv8s,在 NVIDIA 消费级显卡上推理速度能到 60 FPS 以上,精度又能满足产线要求,属于一个很舒服的平衡点。
至于 YOLOv9 和 YOLOv10,它们在某些公开数据集上表现确实更好,但工业缺陷检测这种小样本、强背景干扰的场景,模型的泛化能力更依赖数据质量和训练策略,版本带来的提升反而不是决定性的。所以如果有朋友问“用 v8 是不是过时了”,我的回答是:技术选型不是追新,而是看项目落地周期和维护成本。新框架如果社区资料少、兼容性风险高,反而会拖累进度。
界面端选 PyQt5,原因更简单:Python 生态里做桌面界面,PyQt5 是文档最全、控件最丰富、跨平台最省心的方案。相比 Tkinter 那种简陋控件,PyQt5 可以做出接近正规软件的交互体验;相比 Web 前后端方案,桌面程序不需要部署服务器,对一线操作工来说学习成本更低,双击打开就能用,这对工厂环境非常友好。
1.3 系统整体架构拆开看
整个系统的结构可以分成三块:数据层、模型层、应用层。
数据层负责图像采集和标注,包括从生产现场拍回的焊缝照片、按照统一规范标注好的缺陷框、划分好的训练集和验证集。模型层使用 Ultralytics YOLOv8 进行训练,产出的最佳权重一方面直接用于推理,另一方面可以导出 ONNX 格式备用。应用层就是 PyQt5 写的桌面程序,提供图片检测、批量检测、结果可视化、检测报告导出等功能。
三个层之间的关系很简单:数据层决定模型的天花板,模型层决定检测的准度,应用层决定工具好不好用。实际开发过程中,最容易返工的地方反而不是模型训练,而是界面交互逻辑和模型输出之间的衔接。比如操作工上传一批图片时希望批量处理,质检员希望每张图能放大看细节,这些需求必须在界面设计阶段就考虑进去,否则后期加功能会非常痛苦。
2. 数据集构建:模型效果真正的天花板
2.1 焊缝瑕疵样本怎么采集才够用
很多初学者容易陷入一个误区:一上来就问“用什么预训练模型”,却忽略了数据。实际上,对一个定制化缺陷检测项目来说,数据集的质量直接决定精度上限。预训练模型只是在通用物体上学会的特征,焊缝缺陷这种纹理复杂、尺度多变的工业场景,必须用真实样本微调,否则效果一定不理想。
采集样本我建议遵循“覆盖工艺波动”的原则。同一个焊工、同一台设备、同一批材料,在不同时段焊出来的外观都会有差异。更不用说不同焊工、不同焊接方法(手工电弧焊、气体保护焊、埋弧焊)、不同母材材质,缺陷的表现形式五花八门。我在项目中特意要求现场多拍一段时间,把白天、夜间的光线变化、不同拍照角度、不同表面清洁度的情况都纳入样本库,这样模型学到的特征才不会被某一种特定拍摄环境“绑架”。
数据量方面,焊缝缺陷检测的起步量级我建议每类缺陷至少 300 到 500 个实例。如果某些缺陷本身稀有,比如严重的烧穿,现场很难采集,可以先用手头少量样本训练,再配合后续数据增强和难例挖掘逐步补充。不要一味追求总数,而是要保证每类的实例数量相对均衡,否则模型容易对样本量大的类别过拟合,对稀有类别几乎不识别。
采集回来的原始图像,很多时候不能直接用。我需要做一次清洗:把严重模糊、过度曝光、完全脱焦的图删掉;把多张图内容重复的只留代表性的一张;把缺陷只占画面极小区域、且被其他物体遮挡的图打标为困难样本并单独管理,投入训练时单独评估。
2.2 标注规范和类别设定
标注工具我用的是 LabelImg,操作简单,能直接输出 YOLO 格式的 txt 文件,和 Ultralytics 的训练格式完全兼容。如果追求更快的标注效率,也可以用 X-AnyLabeling 这类半自动标注工具,先让模型预打标,人工再修正,速度能提升不少。
类别设定方面,我最终把焊缝表面状态分成了七类:
| 类别ID | 类别名称 | 定义 |
|---|---|---|
| 0 | weld_normal | 合格焊缝,无表面缺陷 |
| 1 | weld_crack | 表面裂纹,线状或网状 |
| 2 | weld_porosity | 气孔,单个或密集分布 |
| 3 | weld_undercut | 咬边,焊缝边缘的沟槽 |
| 4 | weld_slag | 夹渣,残留在表面的熔渣 |
| 5 | weld_spatter | 飞溅,溅落在母材上的焊滴 |
| 6 | weld_burn_through | 烧穿,焊缝上的孔洞或塌陷 |
这里有一个非常关键的操作细节:不要只标注缺陷,还必须标注“正常焊缝”这一类。原因很简单,在产线实际使用中,模型需要容忍大量“没有缺陷”的图像,如果训练集里全是缺陷,模型的先验就会被扭曲,导致正常焊缝被误判成缺陷。把正常样本单独成类,等于给模型划了一条不准越界的红线,能明显压低误检率。
标注框的大小把握也很讲究。一条长裂纹往往横跨几十厘米,如果仍然用整图一个框包住,那框里的大部分区域是背景,模型要学的是“裂纹的特征”还是“大框里什么都有的特征”?很难说。我的做法是:长裂纹拆成若干段,分段标注,每段框的长度控制在几百像素以内,让框内缺陷占比尽量大;密集的小气孔可以每个单独标框,如果实在太多,也可以按簇合并成一个大框,但要保证这个框内没有严重噪声。
2.3 数据增强怎么做不容易翻车
数据增强是提升模型泛化能力的重要手段,但是工业场景的数据增强有一个特殊禁忌:不能改变缺陷的物理纹理特征。比如典型的色调增强,如果把正常焊缝的颜色改成了偏绿偏蓝,模型就可能学到错误的颜色相关性;再比如把缺陷区域做大幅度平移旋转,这类几何增强在焊缝这种方向性明显的纹理上也要慎重。
我采用的增强策略分为两个层次。第一层是必选的基础增强:水平翻转、随机亮度对比度调整、轻微尺度缩放;第二层是针对性的增强:模拟不同光照条件的亮暗变化、模拟摄像头噪声的高斯模糊、随机裁剪拼接。这些增强操作叠加时,我会在训练配置里将增强强度控制在 Ultralytics 默认自动增强的中低档,避免增强过度导致模型学到失真的特征。
对于类别不平衡的问题,我会在标注文件层面做样本权重统计。比如气孔和飞溅占大多数,裂纹和烧穿偏少,那么稀有类别在 loss 计算中的权重可以适当调高,或者对稀有类别单独过采。具体做法是在训练脚本里读取所有标注文件,统计每类实例数,生成一个权重字典传给模型,并不复杂但非常有效。
注意:数据增强的验证标准永远是“增强后的图,人眼还能不能认出焊缝的纹理结构”。如果增强之后连人都辨别不清这是一条焊缝,模型就更没戏了。
3. 模型训练全程实操与调优记录
3.1 环境配置与常见安装坑
先说结论,我用的环境组合是 Python 3.10 + PyTorch 2.1.2 + CUDA 11.8 + Ultralytics 8.1.x。这个组合在当前生态里相当稳定,网上能搜到的兼容性问题最少。
安装过程有几个坑值得提前说。
第一个坑是 PyTorch 和 CUDA 版本不匹配。很多人直接 pip install torch 装的是 CPU 版本,训练速度慢到怀疑人生。正确的做法是去 PyTorch 官网选择对应 CUDA 版本的安装命令,比如pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118,装完用torch.cuda.is_available()验证一下,返回 True 再往下走。
第二个坑是 Ultralytics 版本升级导致的 API 变化。不同小版本的默认超参和输出格式有差异,如果项目做到一半升级版本,之前训练的权重可能无法正常加载。我的建议是:项目启动后固定 Ultralytics 版本,等到整个项目验收后再考虑升级。
第三个坑是依赖包冲突。OpenCV、matplotlib、numpy 这些库,PyQt5 和 Ultralytics 对它们都有依赖要求,如果环境里已经有老版本,可能会出现 pyqt5 图标加载异常或者 matplot 绘图中文字体变方块的问题。推荐用虚拟环境隔离,比如conda create -n weld_env python=3.10,把训练和界面开发放在同一个干净环境里。
3.2 核心训练参数的选择依据
训练脚本用 Ultralytics 封装的接口,核心代码就几行,但参数的抉择背后是一套实际考量。
from ultralytics import YOLO model = YOLO("yolov8s.pt") # 加载 COCO 预训练权重作为起点 results = model.train( data="weld_dataset.yaml", epochs=200, imgsz=640, batch=16, lr0=0.01, optimizer="AdamW", cos_lr=True, patience=20, workers=8, device=0, project="runs/weld", name="yolov8s_weld" )参数逐条说:
epochs=200:工业缺陷检测通常 150 到 300 轮之间收敛,太少欠拟合,太多浪费时间。配合 patience 早停机制可以自动结束训练。imgsz=640:这是速度和精度的平衡点。如果主观上觉得小气孔检测吃力,可以尝试 960,但推理耗时也相应上升;如果产线对速度敏感,640 是稳妥选择。batch=16:取决于显存。YOLOv8s 在 640 分辨率下、16 批量,大约需要 10G 显存。如果是 8G 显卡就降到 8,别硬扛。optimizer="AdamW":相比 SGD,AdamW 对学习率的敏感度更低,收敛更平稳,适合工业项目这种样本量不算特别大的场景。cos_lr=True:余弦退火可以平滑学习率衰减,在训练后期帮助模型收敛到更优的局部解。patience=20:验证集指标连续 20 轮不改善就提前终止,避免无效训练。
数据集配置文件也很重要,必须按 YAML 格式写清楚类别名和图片路径:
path: ./weld_dataset train: images/train val: images/val names: 0: weld_normal 1: weld_crack 2: weld_porosity 3: weld_undercut 4: weld_slag 5: weld_spatter 6: weld_burn_through3.3 训练过程监控与评估指标
训练启动后,我会盯三个东西:loss 曲线、验证集 mAP、每个类别的 PR 曲线。
loss 曲线看的是收敛情况。正常来说,box_loss 和 cls_loss 应该平稳下降,最终在一个小范围内波动,如果 training loss 持续下降但 validation loss 反而上升,基本可以判定过拟合,需要回退到前面保存的 checkpoint,或增加数据增强强度。
mAP 指标是直观的“好坏标尺”。我特别建议看重 mAP50 和 mAP50-95 两个值。mAP50 更贴近实际检测场景的“框有没有对得上”,mAP50-95 则更严格,严格要求框位置精确。焊缝缺陷检测中,大裂纹的框稍微偏一点问题不大,但小气孔的框偏一点就完全错位,所以最终决定上线时,我要求 mAP50 达到 0.9 以上,mAP50-95 尽量往 0.7 上靠。
类别 PR 曲线能直接暴露瓶颈。训练完看每个类别的 AP 值,如果某一类的 AP 明显低于平均线,说明该类的样本量不足,或者类间特征混淆严重。比如咬边和夹渣在视觉上确实很像,都是边缘附近的深色区域,这时候需要回看标注,确认这两类的边界划分是否清晰统一。
训练结束后的产物有best.pt和last.pt,我习惯用 best 做推理验证,last 则留着作为恢复训练的起点。如果对推理速度有更高要求,会把 best.pt 导出成 ONNX 甚至 TensorRT 格式:
yolo export model=runs/weld/yolov8s_weld/weights/best.pt format=onnx imgsz=640 opset=12导出的 onnx 文件可以直接给 PyQt5 后端用 OpenCV DNN 加载,这也是部署阶段常走的路线。
4. PyQt5 可视化界面开发的几个关键点
4.1 界面功能规划与布局
界面这部分,很多人觉得“无非就是放几个按钮”,但实际做下来,真正影响用户体验的是功能流程是否符合现场操作习惯。我在规划时把界面分成了几个功能区,排列顺序完全模拟操作工的使用路径。
顶部是图片导入区:支持拖拽单张图片、选择文件夹批量导入,图片缩略图列表展示。中间是主显示区:左边原始图,右边检测结果图,切换显示时保留原始的缩放比例,方便对比。右侧是检测信息区:显示每类缺陷的数量统计、每个检测框的类别、坐标和置信度,支持点击某一条记录时在主图中高亮对应的框。底部是操作区:一键检测、一键导出报告、阈值滑块。
这个布局的好处是操作路径最短。操作工打开软件、拖入图片、点一下检测、看结果、点导出,五步解决,不需要学习任何菜单逻辑。阈值滑块特别实用,现场发现误检多就往上调,漏检多就往下调,比如默认置信度 0.25,现场调到 0.3 或 0.2 都能实时生效。
4.2 推理线程设计:UI 卡死的解药
这是整个 PyQt5 界面开发中最容易出现严重体验问题的地方,没有之一。
如果直接在 PyQt5 主线程里调用模型推理,一张大图推理一次可能耗时 50 到 100 毫秒,听起来不长,但在这个过程中界面会处于无响应状态。如果操作工连续点击检测,界面会卡住几秒钟不动,体验非常糟糕。更严重的是,批量检测几十张图片时,界面会直接变成“未响应”状态,操作工大概率会认为软件崩溃了。
解决办法是把推理丢到独立线程。我用的是标准做法:一个继承QThread的工作线程,内部循环从任务队列中取图片,调用model.predict(),发出信号返回结果,界面主线程只负责接收信号并更新画面。
import cv2 import numpy as np from PyQt5.QtCore import QThread, pyqtSignal from ultralytics import YOLO class DetectThread(QThread): result_ready = pyqtSignal(dict) def __init__(self, model_path, conf_thres=0.25, parent=None): super().__init__(parent) self.model = YOLO(model_path) self.conf_thres = conf_thres self._image = None self._running = True def process_image(self, image): self._image = image def run(self): while self._running: if self._image is None: continue img = self._image self._image = None results = self.model.predict( source=img, conf=self.conf_thres, verbose=False ) boxes = results[0].boxes labels = results[0].names detections = [] for box in boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() conf = float(box.conf[0]) cls_id = int(box.cls[0]) detections.append({ "box": [x1, y1, x2, y2], "conf": conf, "cls_id": cls_id, "cls_name": labels[cls_id] }) self.result_ready.emit({"detections": detections})信号槽机制把耗时任务和界面刷新解耦,推理线程跑得再慢,界面也能保持流畅响应。批量检测时,这个设计更是救命:模型一张张处理,界面每张刷新一次,用户能即时看到进度,心里踏实很多。
4.3 检测结果绘制的像素级细节
检测框绘制不是一个随便画个矩形就完事的问题。工业质检场景,结果图要作为质量追溯的依据,必须足够清晰直观。
我做的绘制方案是:在原始图像的副本上,用不同颜色区分缺陷类型,裂纹用红色,气孔用橙色,咬边用黄色,正常焊缝用绿色。每个框在绘制前先计算位置坐标,再调用 OpenCV 画矩形和标签。画标签时,为了保证文字不被背景遮挡,我会在文字区域先画一个半透明底框,再叠白色文字。
def draw_detections(image, detections, class_colors): overlay = image.copy() for det in detections: x1, y1, x2, y2 = det["box"] cls_name = det["cls_name"] conf = det["conf"] color = class_colors.get(cls_name, (0, 255, 0)) cv2.rectangle(overlay, (int(x1), int(y1)), (int(x2), int(y2)), color, 2) label = f"{cls_name} {conf:.2f}" (tw, th), baseline = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2) cv2.rectangle(overlay, (int(x1), int(y1)-th-baseline-4), (int(x1)+tw+8, int(y1)), color, -1) cv2.putText(overlay, label, (int(x1)+4, int(y1)-baseline-2), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2) return overlay显示时,OpenCV 的图像是 BGR 通道,必须转成 RGB 再转成QImage,否则颜色会偏蓝偏红。这一步很多人忽略,做完才发现检测框是歪的、颜色不对,其实只是通道顺序没转对:
def cv2_to_qimage(cv_img): rgb_image = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB) h, w, ch = rgb_image.shape bytes_per_line = ch * w return QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888)我把导出报告也做成了一个小功能:检测完成后,把每张图的文件名、缺陷类型、缺陷数量、置信度列表写入 CSV 或者 Excel,后续质检员可以直接归档,不需要再手工誊写。
5. 实测表现与常见问题排查实录
5.1 实际检测性能记录
整个系统搭建完成后,我用一批现场采集的验证集做了完整测试。验证集不参与训练,只用来检验模型泛化能力。测试环境是一台消费级显卡,推理分辨率 640,批量推理开启半精度。
实测下来,单张图片平均推理耗时为 42 到 58 毫秒,也就是说每秒能处理超过 15 张图片。对于离线抽检工具来说,这个速度完全够用,操作工导入一文件夹几十张图,也就几秒钟的事。检测精度方面,作为对比验证,准确率、召回率与离线评估结果基本一致,没有出现明显掉点。
分类别的表现上,气孔和飞溅这种形态特征明显的缺陷 AP 值最高,裂纹由于某些细线型裂纹在低分辨率下容易断线,AP 略低。这也验证了前面说的:输入分辨率对细长目标的检测影响非常大,如果细裂纹项目现场报告漏检多,我会建议先把推理分辨率提到 960 再试一轮。
另外我专门关注了误检率。因为焊缝周边经常有金属反光、焊渣碎片之类的东西,模型偶尔会把反光区域误判为咬边。通过把置信度阈值从 0.25 调到 0.35,误检明显减少,而真正有问题的缺陷置信度通常都在 0.5 以上,影响不大。阈值可调这个设计,在现场真的是核心竞争力。
5.2 常见问题速查表
做这类系统,必然会遇到一些重复出现的问题。我整理成一张速查表,方便大家对着排查:
| 问题现象 | 可能原因 | 解决思路 |
|---|---|---|
| 训练时显存不足(CUDA out of memory) | batch 过大或分辨率过高 | 降低 batch 到 8/4 或调小 imgsz 至 480/512 |
| 界面点检测后卡死无响应 | 推理放在了主线程 | 改用 QThread 子线程加载模型和推理 |
| 小气孔漏检严重 | 输入分辨率太低 | 推理和训练分辨率都提到 960,或在标注时对小目标多细分 |
| 检测框颜色偏色 | OpenCV BGR 未转 RGB | 用 cvtColor 转 RGB 后再构造 QImage |
| 正常焊缝被误检 | 训练集中正常类别样本少 | 补充正常样本并单独成一类参与训练 |
| 加载权重报错 | Ultralytics 版本不匹配 | 固定项目使用的 Ultralytics 版本号保持一致 |
| 批量检测时程序响应慢 | 单线程逐张推理 | 批量检测同样走子线程,且每张处理完立即刷新 UI |
| ONNX 导出后推理结果不一致 | 动态轴与 opset 版本问题 | 固定 export opset=12,推理时保持同样分辨率 |
关于 “识别不稳定、同一张图每次框位置有波动” 的问题:这通常不是随机性问题,而是输入图片在预处理阶段的缩放存在差异。检查推理时是否做了同样的 letterbox 填充,保证图片送入模型的宽高比一致,结果就会稳定。
5.3 后续可以扩展的方向
项目到这一步,算是一个能交付的标准形态。但如果现场需求再往前走一步,扩展空间其实非常大。
第一个方向是接入实时视频流。现有检测核心已经支持单帧推理,只需在界面里增加摄像头采集线程,循环读取帧送入检测线程,就能实现实时检测,比如焊接过程在线监视。代价是显卡负载增加,帧率会下降,但对很多产线来说,能提前发现问题远比帧率重要。
第二个方向是导出更正式的质检报告。现有 CSV 导出可以继续升级成 PDF 报告模板,加入缺陷统计柱状图、按日期归档的功能,甚至对接企业的质量管理系统。
第三个方向是模型轻量化。把 YOLOv8s 蒸馏到更小的模型,再量化成 INT8,部署到边缘设备上。这样就能脱离 PC 运行,直接装在一个 USB 接口的智能相机盒子里,改造成本低、部署速度快,对中小产线特别有吸引力。
第四个方向是难例挖掘的闭环。把现场新发现的误检、漏检图定期收集回来,人工修正标注后加入训练集,定期增量微调。这能形成数据飞轮,让系统在特定产线上越用越准。
我个人在实际操作中最深的体会,是这类项目的成败其实更多取决于工程化细节,模型选型只是最基础的起点。数据怎么标、阈值怎么调、UI 怎么不卡、现场怎么用,每一个环节都需要认真对待。尤其是 PyQt5 界面这块,看起来无关紧要,但现场操作人员每天都在用,卡顿一次他们的耐心就少一分,这个项目在工厂里能不能真正用起来,拼的往往就是这些体验层面的事情。如果你们正在做类似的检测项目,我建议在动手之前先把现场需求摸透,再把数据洗干净,后面基本就是水到渠成的事。