去年接了某零件厂的一个需求:人工质检员每天盯着焊缝做外观检查,时间一长就容易漏检,尤其是一些细微的裂纹和气孔。现场想上一套基于视觉的检测系统,但条件比较苛刻——不能改动现有产线太多,操作工要能快速上手,最好在一个界面里完成图片/实时检测和结果保存。于是我围绕YOLOv8、PyQt5这两个关键词搭了一套焊缝质量检测系统,顺带把YOLOv9、YOLOv10也拉进来做了对比调优。项目以python源码为核心,配合PyQt5界面、标注数据集和完整训练代码,整体从数据采集到部署落地都走了一遍。
这篇文章想分享的是整个系统的设计思路、关键代码细节和实际落地过程中的经验。如果你正在做工业金属外观检测、瑕疵检测,或者想用YOLO系模型配合PyQt5做一个小型上位机工具,这里面的选型逻辑、参数调整、踩坑记录应该能帮到你。
1. 为什么要把YOLO和PyQt5组合起来做焊缝检测
1.1 焊缝瑕疵检测到底难在哪
焊缝外观检测不是“看有没有缺陷”这么简单。常见的缺陷类型包括气孔、裂纹、未熔合、咬边、焊瘤、飞溅等,每一种在图像上的特征都不一样。气孔通常是圆形暗斑,裂纹是细长且方向不定的暗线,飞溅则是一些高亮或暗色的小颗粒。更麻烦的是,现场工况下还有油污、锈迹、反光、光照变化,这些干扰很容易让传统图像处理方法(比如阈值分割、边缘检测)出现大量误检。
人工检测的问题不只是慢,还有标准不统一。同一个缺陷,早班和中班的检验员判断可能就不一样。长期对着强光和焊缝眼睛也容易疲劳。所以客户想上深度学习检测,本质是把质量标准和检测经验固化到模型里,让系统输出一个可量化的结果,同时保留人工复核的入口。
这类项目的难点在于:缺陷尺度差异大,有的裂纹只有几个像素宽;正负样本极度不平衡,大多数焊缝是合格的;工业现场对误检率又非常敏感,漏一个可能造成批量报废。这些特点直接决定了后续的数据采集、标注和模型选择方案。
1.2 技术选型的取舍逻辑
目标检测框架我最终选了YOLO系列。原因很直接:它在工业落地中部署成熟,训练代码和推理接口都非常完善,且YOLOv8、YOLOv9、YOLOv10都有官方预训练模型和相对活跃的社区支持。对于“金属表面小目标缺陷”这种任务,单阶段检测器在速度和精度之间的平衡最适合实际产线。
PyQt5作为界面框架,是因为它稳定、跨平台,而且与OpenCV、NumPy等视觉生态结合得很自然。虽然也有更炫酷的Web方案,但工业现场往往要求离线运行、内网环境、操作工不希望在浏览器里点击,PyQt5打包出来的桌面程序更契合。
另外,我在项目里做了YOLOv8、v9、v10三个版本的对比,不是单纯追新。从实际结果看,v10的推理速度有优势,v9在部分缺陷上召回率更高,v8最稳,部署资料最多。具体数据我在第4章会详细给出。整个系统选择“YOLO系列模型+PyQt5界面”的组合,既满足了现场检测速度和精度的要求,也让后期维护和模型切换变得灵活。
2. 系统的整体架构和关键模块设计
2.1 从相机到界面的数据流
焊缝检测系统的数据流可以简化为:图像采集 → 预处理 → 模型推理 → 结果解析 → 界面展示 → 数据保存。这个链条里的每一环都可能影响最终检测质量。
图像采集部分,现场通常用工业相机或者USB摄像头。接口需要留好,因为有些客户车间里是GigE相机,有些只拿手机拍图片。所以我们做成了“图片检测”和“视频/实时检测”两种模式,基于OpenCV的VideoCapture和多线程队列来兼容不同数据源。预处理包括尺寸缩放、色彩空间转换、归一化,这些操作要尽量与训练时的设置保持一致,比如统一缩放到640x640。
模型推理部分,我用的是Ultralytics YOLO封装。在代码里实例化一个YOLO对象,调用predict方法就能得到检测框、类别和置信度。这个过程不能直接放到UI主线程里跑,否则画面会卡死。我在项目里用QThread把这个任务拆到了子线程,通过信号和槽把结果传回界面。
结果展示部分,除了画检测框,还要把类别名称、置信度、数量统计显示在界面上。比如“气孔 3个,裂纹 1条,置信度 0.92”。最后要把检测结果保存到CSV或Excel表格,并保存标注后的图片,方便后续追踪和报表生成。
2.2 PyQt5界面的模块划分与线程模型
一个合适的桌面界面应包含几个区域:左上角是实时画面/图片预览区,右上角是检测结果列表和统计信息,底部是操作按钮和参数控制区。界面代码如果全塞在MainWindow里会很乱,我建议按功能拆分模块。
我实际的目录组织是这样的:
- main.py:程序入口,创建QApplication和主窗口。
- ui_mainwindow.py:界面搭建代码。
- detector.py:模型加载和推理封装。
- worker.py:QThread子线程,负责处理图像和调用detector。
- utils/:图像读取、预处理、结果保存等工具函数。
线程模型是最容易出问题的地方。PyQt的QThread不能直接操作UI控件,必须通过信号与槽机制。我在worker里定义一个send_result信号,携带原图、标注图、检测结果字典,主窗口接收到信号后再更新控件。
这里重点提醒:工业相机连续采集时,如果每帧都去做模型推理,即使模型只跑20毫秒,也可能会因为摄像头帧率过高导致界面响应卡顿。正确做法是“摄像头采集线程持续出帧,推理线程只对最新帧工作”,配合队列丢掉积压的旧帧,保证推理结果尽量实时但不阻塞UI。这种设计我用了好几年,稳定度明显好于在UI线程里直接跑while循环。
3. 数据集采集、标注与增强的完整打法
3.1 现场数据采集的规范性
焊缝检测的数据集最大的问题是现场采集不规范,导致模型学到的“背景特征”而不是“缺陷特征”。比如采集时总把工件放在画面正中间、打光方向固定,这样模型很容易把工作台的纹理当成背景。训练集里背景单一,测试时换一个车间就立刻露馅。
我建议在采集阶段就要有意识地控制变量:
- 拍摄角度:分别从正面、侧面、俯视各采一部分。
- 光照条件:强光、暗光、自然光、补光灯都尽量覆盖。
- 工件批次:不同表面粗糙度、不同材质状态都要有。
- 缺陷形态:轻微、中度、严重的缺陷都要采,比例可以参考现场统计。
对焊缝检测来说,数据量的要求没有自动驾驶那么夸张,但至少也要做到“每一类缺陷有500张以上有效标注图”。如果现场缺陷数据实在不够,可以先用公开的钢材表面缺陷数据集做预训练,再用现场数据微调,效果会好很多。
3.2 标注类别定义与工具选择
标注类别建议不要太多太细。我开始时定义了“气孔、裂纹、未熔合、咬边、飞溅、合格”六类,后来发现“合格”类在目标检测里并不合适,因为背景就是无缺陷区域,没必要标注。最后去掉合格类,只保留五类缺陷。
类别之间还需要考虑合并类似外观。比如“咬边”和“未熔合”在二维图像上极难区分,如果不是硬性要求,合并成“成型不良”可能更有利于模型收敛。我最终采用了四类:气孔、裂纹、未熔合、飞溅。具体可以根据下游工序的处置方式来定义。
标注工具我用的是LabelImg,因为它的YOLO格式导出最简单。操作上要注意:标注框要紧紧贴合缺陷区域,不要留太多背景;对于细长裂纹,用倾斜框不准,YOLO的矩形框没法表达方向,所以只能通过多段框或旋转图像来补充样本,让模型学习到不同角度。一个小技巧是:对同一张图做90度、180度、270度旋转后再标注,相当于白捡四倍数据。
3.3 数据增强策略与踩坑边界
数据增强是解决工业样本少、分布单一的重要手段。Ultralytics框架自带Mosaic、MixUp、HSV、旋转、翻转、缩放等增强。但焊缝检测有特殊性,不能盲目开满所有增强。
比如Mosaic会把四张图拼到一起,缩小缺陷目标的像素占比,对小目标检测反而不利。又比如HSV色相变化,如果现场焊缝颜色本身是固定的,过度的色相偏移会让模型学到错误的颜色关联。我的经验是:基础翻转、轻微旋转(正负15度以内)、亮度对比度调整、随机噪声这几项对焊缝检测有帮助;Mosaic可以开但把概率调低到0.3左右,MixUp直接关掉。
数据增强还有一个更重要的用途——缓解类别不平衡。如果气孔样本多、裂纹样本少,可以在每个epoch里对裂纹样本重复采样,或者用复制粘贴的方式把裂纹贴到新背景上。这类增强要谨慎,必须保证贴图后边缘过渡自然,否则模型只会记住贴图的伪影。
数据划分也走标准流程:训练集70%,验证集20%,测试集10%。划分的时候要按“图像级别”而不是“缺陷框级别”去重,同一张图上不同缺陷不能拆到不同集合里,防止数据泄露。划分后检查每一类在训练/验证/测试里的比例差异,差异太大时用分层抽样修正。
4. YOLOv8/v9/v10训练对比与参数调优
4.1 环境配置与数据集组织
环境配置其实很简单,如果之前没用过Ultralytics,可以参考下面步骤。这里基于Python 3.10和CUDA 11.8(或用你的版本):
conda create -n weld python=3.10 -y conda activate weld pip install ultralytics pytorch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118如果只是CPU推理,直接pip install ultralytics即可,但训练还是建议用NVIDIA GPU。显存至少8GB,否则batch size会受限制。
数据集目录按YOLO格式组织:
dataset/ images/ train/ val/ test/ labels/ train/ val/ test/ data.yamldata.yaml内容要指定路径和类别名:
path: D:/weld_dataset train: images/train val: images/val test: images/test nc: 4 names: ['porosity', 'crack', 'lack_of_fusion', 'spatter']注意path最好用绝对路径,否则训练时会因为相对路径问题找不到图片。标签是txt格式,每行五个数字:类别id、归一化中心x、归一化中心y、归一化宽、归一化高。
4.2 训练参数的选择思路
训练命令很简单,但参数我会做针对性调整,不能照抄默认值:
yolo detect train data=D:/weld_dataset/data.yaml model=yolov8n.pt epochs=150 imgsz=640 batch=16 lr0=0.01 patience=20几个关键参数的说明:
- model:可以选择yolov8n/s/m/l/x。焊缝缺陷很多是小目标,n模型检测效果偏弱,我一般从v8m或v9c起步。
- epochs:工业场景数据集不复杂,150轮足够,超过300轮大概率过拟合。
- imgsz:虽然640是默认值,但如果硬件允许,用960或1280对小目标有明显提升,推理速度会下降。
- batch:根据显存调整,显存不足时调小batch,同时适当降低lr0。
- lr0:学习率太大会导致Loss震荡,建议用0.001到0.01之间,配合cosine LR调度。
- patience:早停策略,验证集指标连续20轮不涨就停止,省时间。
训练过程中要同时观察验证集mAP和PR曲线,不能只看训练loss降没降。我习惯每20轮输出一次验证结果:
yolo detect val data=D:/weld_dataset/data.yaml model=runs/.../weights/best.pt4.3 三个版本模型的实际表现
为了确定最终部署用哪个模型,我用同一份数据集分别训练了YOLOv8m、YOLOv9c、YOLOv10n/s这几个量级。需要说明的是,YOLOv9和YOLOv10的配置文件在Ultralytics中可以直接用yolov9c.pt、yolov10s.pt加载,训练命令几乎一样。
在某个模拟项目X的测试集上(约200张带标注焊缝图像),我得到了下面的结果:
| 模型 | 参数量 | mAP@0.5 | 推理时间(GPU) | 小目标召回率 |
|---|---|---|---|---|
| YOLOv8m | 25.9M | 0.894 | 8.2ms | 0.78 |
| YOLOv9c | 25.5M | 0.906 | 9.6ms | 0.81 |
| YOLOv10s | 7.2M | 0.871 | 5.4ms | 0.74 |
从这个对比能看出:YOLOv9c在mAP和小目标召回率上略有优势,但推理耗时多了1.4毫秒;YOLOv10s速度快,但准确率低一些;YOLOv8m处于中间,胜在资料多、问题好排查。最终现场部署我选了YOLOv8m,因为检测任务要求稳定可靠,而现场推理机的GPU性能比较弱,v9c在报错和兼容性上偶尔会出幺蛾子。如果你要追求极致速度且缺陷较大,YOLOv10会更合适。
训练时我还试过把输入分辨率从640提升到960,mAP提升了大概2.3个百分点,但推理耗时翻倍。考虑到产线节拍,最终保留640。实际部署时还可以用TensorRT或ONNX加速弥补分辨率提升的代价,这个问题我放到第5章讲。
5. PyQt5界面集成、推理提速与打包发布
5.1 模型推理封装与界面交互
在PyQt5里集成YOLO,核心是把模型推理封装成一个独立的类,和界面解耦。下面是我常用的Detector类模板:
from ultralytics import YOLO class Detector: def __init__(self, model_path, conf_thres=0.25, iou_thres=0.45): self.model = YOLO(model_path) self.conf_thres = conf_thres self.iou_thres = iou_thres def detect_image(self, image): results = self.model.predict( source=image, conf=self.conf_thres, iou=self.iou_thres, verbose=False ) return self._parse_results(results[0]) def _parse_results(self, result): boxes = result.boxes.xyxy.cpu().numpy() confs = result.boxes.conf.cpu().numpy() clss = result.boxes.cls.cpu().numpy().astype(int) names = result.names return [{'box': box, 'conf': conf, 'cls': cls, 'name': names[cls]} for box, conf, cls in zip(boxes, confs, clss)]这段代码用result.boxes直接取xyxy坐标、置信度和类别id,效率比遍历result.pandas().xyxy()高。界面侧的按钮点击后,调用一个QThread子线程执行上述detect_image,避免界面假死。
界面交互最简单的逻辑:点击“打开图片”按钮,用QFileDialog选图,把图像文件路径传给worker线程;worker检测完毕后,通过信号把原图和标注图返回给主线程,主线程用QPixmap显示。这个逻辑有几点要特别注意:图片路径不要直接传给worker后又被界面线程同时读取文件,最好在worker里复制一份图像数组,避免资源竞争。
5.2 总卡顿问题的线程处理
PyQt5界面卡顿90%是因为把耗时操作放到了主线程。比如在paintEvent里做图像缩放、在槽函数里直接调用model.predict、用循环反复读取摄像头帧。我的处理方案是:
- QThread只负责计算,不碰任何界面控件。
- 摄像头采集单独一个QThread,通过队列把最新帧交给推理QThread。
- 推理QThread处理完当前帧后,如果队列里还有新帧,就丢弃中间帧,只取最新帧。
- 界面端使用QLabel显示图像,每次重新设置QPixmap,不叠加绘制。
另外,显示大图时要注意内存占用。如果原图是4000x3000,每次都转成QPixmap再缩放,界面会明显卡顿。我一般记下原图的比例,用QImage.scaled缩放到与QLabel尺寸匹配后再显示。这个细节很多人容易忽略。
5.3 推理部署提速与可执行文件打包
模型训练完成后,通常要用更高效率的推理方式部署到目标机器。Ultralytics官方支持model.export(format='onnx')或format='engine',在项目里我优先用ONNX,再用OpenCV或者ONNXRuntime来跑。这样能减少使用PyTorch全框架的依赖体积,推理速度也有提升。
ONNX导出命令:
yolo export model=best.pt format=onnx imgsz=640 opset=12导出后在Detector里加载onnx:
import cv2 import onnxruntime as ort ort_session = ort.InferenceSession('best.onnx')不过要注意,ONNX模型输入的预处理要求(缩放、归一化)必须与训练时一致。比如输入是0-255范围还是0-1范围,格式是BHWC还是BCHW,导出后最好写一个快速测试脚本对比ONNX和PyTorch的推理输出,误差大于1%就要查原因。
PyQt5程序打包用的是PyInstaller。命令大致如下:
pyinstaller -F -w -i icon.ico main.py但直接打包会漏掉ultralytics的配置文件和字体文件,需要在main.py里显式指定路径,或者用--add-data参数:
pyinstaller -F -w --add-data "D:/venv/Lib/site-packages/ultralytics;ultralytics" main.py打包出来的exe体积通常在500MB以上,因为包含了CUDA库。如果不用GPU推理,可以安装CPU版本的torch后再打包,体积可以降到150MB左右。现场机器配置足够的话,我还是建议保留GPU,因为焊缝检测需要实时处理。
6. 常见问题排查:从训练到现场的全链路实录
6.1 模型训练阶段的典型翻车
训练阶段遇到最多的是loss不降或mAP为0。这类问题十有八九是标签文件格式错误。YOLO标签是归一化坐标,如果用了像素坐标,或者类别id超出nc范围,训练时就会识别不全。排查方法很简单:写一个脚本随机读取训练集的txt,把坐标乘回图像宽高画框,看是否与缺陷位置一致。我习惯用check_labels.py做这步检查。
另一个常见问题是过拟合。训练集loss降得很好,验证集mAP却很低,一般是因为数据量太小或者增强过强。解决办法是增加数据样本、减少随机增强强度,或者换用小模型。还有一点,训练时不要用随机初始权重,一定要用预训练权重,否则收敛极慢且效果不稳定。预训练权重在迁移学习中的优势不用多说,尤其对小数据集更是决定性的。
6.2 推理与界面阶段的坑
推理阶段发现检测框偏移,比如框在缺陷上方一点点,通常不是模型问题,而是推理时的输入尺寸和训练不一致。如果训练用的imgsz是640,推理时直接传原图,Ultralytics内部虽然会自动resize,但如果结果解析时没有正确映射回原图坐标,就会偏移。建议统一在Detector内部先做resize,然后解析出box后除以缩放比例,再返回坐标。
还有一个坑是置信度阈值设置不合理。界面里我做了动态调节功能,默认0.25,但工业场景建议调高到0.45以上,避免大量误检。客户如果反映漏检,可以把阈值调低,但误检会增多。实际部署时最好根据现场正样本/负样本比例做一个ROC曲线,找到误检和漏检的平衡点,而不是拍脑袋定数值。
PyQt5界面无法打开摄像头也是高频问题。常见原因是权限被占用、相机SDK不兼容OpenCV。我一般先用OpenCV写一个测试脚本,确认该相机能被正常读取,再集成到界面里。如果是工业相机,尽量用厂商自己的SDK采集,再通过帧回调推给界面。
6.3 现场部署的隐藏雷区
到现场部署时,算法问题反而不是最大的,环境问题才是。比如:现场主机没有安装显卡驱动,PyTorch识别不到GPU,代码仍然能跑但速度极慢;工控机系统为Windows 7,装不了新版Python和依赖;界面路径写死了D:/dataset,但设备上没有D盘等等。
规避做法:打包时把所有资源文件、模型文件、配置文件统一放在exe同目录下,用sys.path[0]和os.path.join动态获取路径,不要写绝对路径。数据集和模型分开存放,界面启动前做一次环境自检,检查GPU、模型文件、相机是否可用,并把状态显示在界面里,避免用户点半天没反应。
网络传输也不容忽视。现场可能有多台相机,如果使用网络相机,通信协议和缓冲区配置直接用默认参数很容易造成延迟或丢帧。最好在图像采集层做线程缓冲,并统计帧率,一旦发现采集帧率低于设定值,就在界面给出告警,而不是默默丢帧误导检测结果。
最后分享一个我个人的体会:做这类工业检测系统,真正能落地的是“稳定”而不是“精度”。模型mAP从0.88涨到0.90,对现场操作工来说感知可能不明显,但如果界面三天两头卡死、保存数据丢失、相机断线没有自动重连,这套系统就一定会被弃用。所以我建议在开发时多花时间在异常处理、状态监控和数据记录上,而不是反复调那零点几个点的准确率。这套血脉用下来,焊缝检测项目从调试到上线只花了两周,后续维护也一直很省心。如果你也在做类似的YOLO+桌面应用项目,希望这些经验能让你少踩几个坑。