YOLOv5焊缝质量检测实战:数据集构建、模型训练与PyQt部署 简介一份面向钢材焊接质量检测的YOLOv5深度学习资源适合有一定目标检测基础的开发者、质检算法工程师或相关专业学生。资源已包含训练完成的焊缝质量检测模型可直接用于识别焊缝好坏同时附带有PR曲线、loss曲线等训练过程评估结果便于理解模型性能。数据集部分提供约1134张JPG格式标注图片并同时包含对应的XML与TXT两种标签格式方便在不同检测框架中切换使用。压缩包内共2000个文件以JPG图像、XML与TXT标注文件为主要组成整体大小约222.96MB结构明确便于下载后快速进入实验。此外资源还搭载了基于PyQt的图形界面支持对图片、视频以及摄像头实时画面进行检测操作直观能够满足从模型验证到演示落地的常见需求。目前已有985人学习适合用来快速搭建焊接质量检测原型系统。1. 焊缝质量检测为什么选择 YOLOv5 而非传统视觉在钢结构和管道预制车间焊缝质量检测长期依赖人工目检或超声波抽检效率低且容易漏检。用深度学习目标检测来做焊缝质量好坏判别不是把图片丢进网络那么简单难点在于工业现场的光线变化、焊缝形状差异以及缺陷样本本身稀疏。YOLOv5 在这类任务里是投入产出比最高的选择单阶段检测速度快能部署到普通显卡社区资料多训练自己的数据集也很成熟。这里拆解的这份「YOLOv5焊缝质量好坏检测」资源包含训练好的权重、已用 LabelImg 标注好的钢材缺陷检测数据集图片为 JPGxml 与 txt 双格式标注分别保存以及 PR 曲线、loss 曲线等训练评估产物还有一个能检测图片、视频和调用摄像头的 PyQt 界面。对正在做检测类毕设或工业视觉课题的开发者来说这套东西可以直接拿来跑通流程再按自己的数据微调。2. 数据集构成与 xml/txt 双标注格式解析2.1 目录规划与文件对应关系拿到资源后先不要急着训练把数据集结构理清楚。资源里的图片全部是 JPG标注文件分两套一套是 LabelImg 导出的 Pascal VOC 格式 xml另一套是 YOLO 训练需要的 txt。资源里给出的 dataset_00294.txt、dataset_0079.txt、dataset_0017.txt 这类文件就是 YOLO 格式的标签文件每个 txt 与同名 JPG 一一对应。常见做法是把它们按训练集和验证集分开目录结构如下dataset/ ├── images/ │ ├── train/ │ │ └── dataset_0017.jpg │ └── val/ ├── xml_annotations/ # LabelImg 导出的 Pascal VOC 格式 │ ├── dataset_0017.xml │ └── ... ├── yolo_labels/ # YOLO 格式 txt与 images 一一对应 │ ├── dataset_0017.txt │ └── ... └── weld.yaml # YOLOv5 训练所需的数据配置这套目录并不是强制要求但 YOLOv5 的 train.py 默认就是按 images 和 labels 两个根目录去找数据的labels 的路径会把 images 替换为 labels后缀 .jpg 替换为 .txt。如果你把 xml 直接放在 labels 目录里训练脚本会读不到数据。所以 xml 单独存一个文件夹做存档yolo_labels 作为训练用的 labels这是最不容易出错的方案。检查对应关系时可以用下面这个命令快速找出没有标签的图片。for img in dataset/images/train/*.jpg; do base$(basename $img .jpg) [ -f dataset/yolo_labels/$base.txt ] || echo missing label: $img done这个循环遍历训练图片basename 去掉扩展名后去 yolo_labels 里找同名 txt找不到就把图片路径打印出来。训练前跑一遍这个命令可以避免因为漏标注导致训练时图片被跳过或者更隐蔽的“图片有但标签为空”导致 loss 异常。2.2 XML 与 TXT 标签的坐标换算LabelImg 默认可以输出两种格式VOC 格式把目标位置存成绝对像素坐标YOLO 格式存成相对图片宽高的归一化坐标。一份典型的 xml 长这样annotation folderimages/folder filenamedataset_0017.jpg/filename size width1920/width height1080/height depth3/depth /size object namedefect/name bndbox xmin312/xmin ymin245/ymin xmax867/xmax ymax342/ymax /bndbox /object /annotation这里name是类别名按你标注时的实际类别替换缺陷名称不要带空格和中文YOLO 对名称里的字符比较敏感。xml 里记录的是左上角和右下角的像素坐标而 YOLO 的 txt 记录的是归一化后的中心点坐标和宽高直接这样写0 0.3073 0.2718 0.2891 0.0898第一个数字是类别 id从 0 开始后面四个数是 x_center、y_center、width、height都除以了图片的宽高。如果类别不止一个每个目标单独一行。训练时 YOLOv5 会按行解析发现某一行不足五个数或者坐标超出 [0,1] 就会报警。如果需要自己把 xml 转成 txt我一般会在整理数据集时用一段小脚本避免手工换算出错。import xml.etree.ElementTree as ET def xml_to_yolo(xml_path, class_names, out_path): tree ET.parse(xml_path) root tree.getroot() w float(root.find(size/width).text) h float(root.find(size/height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_names: continue # 跳过不在类别表中的目标 cid class_names.index(name) box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) x_center (xmin xmax) / 2.0 / w y_center (ymin ymax) / 2.0 / h bw (xmax - xmin) / w bh (ymax - ymin) / h lines.append(f{cid} {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f}) with open(out_path, w, encodingutf-8) as f: f.write(\n.join(lines))函数里 class_names 是类别列表顺序必须与后面训练用的 weld.yaml 中 names 的顺序完全一致。xml_path 是输入文件out_path 是输出 txt 的路径。注意读取宽高时我用了 float因为有的标注工具会写出带小数点的尺寸。如果某些 xml 坐标解析出来是负数或者中心坐标大于 1说明标注越界了这种目标直接跳过比强行归一化更安全。2.3 数据集质量检查与训练/验证划分训练目标检测模型之前数据质量检查比调参更重要。最常见的坑是类别严重不均衡比如“合格焊缝”样本占 90%“缺陷焊缝”只占 10%模型很容易把缺陷漏掉。我一般会先用一段脚本统计每个类别的框数量分布。import glob counts {} for txt_path in glob.glob(dataset/yolo_labels/*.txt): with open(txt_path) as f: for line in f: cid line.split()[0] counts[cid] counts.get(cid, 0) 1 print(counts)这段代码遍历所有 YOLO txt把每一行的第一个字段即类别 id 统计出来。如果发现某个类别数量特别少优先考虑做数据增广或者从视频帧里抽更多负样本而不是一上来就调学习率。焊缝检测里很多缺陷在灰度上差别很小增广时不要用太强的颜色扰动容易把真实缺陷的颜色特征洗掉。数据准备好后做划分训练集和验证集一般按 8:2 或 9:1。注意划分时要保证同一张图不会同时出现在训练和验证里尤其是连续拍摄的焊缝图片有很强的相似性最好按文件夹或视频段划分而不是纯随机打散。简单的划分可以这样mkdir -p dataset/images/train dataset/images/val dataset/labels/train dataset/labels/val python - PY import glob, random, shutil, os imgs glob.glob(dataset/images/*.jpg) # 实际按你的存放路径调整 random.seed(42) random.shuffle(imgs) for i, img in enumerate(imgs): txt img.replace(.jpg, .txt).replace(images, yolo_labels) if i int(len(imgs) * 0.8): shutil.move(img, dataset/images/train/) shutil.move(txt, dataset/labels/train/) else: shutil.move(img, dataset/images/val/) shutil.move(txt, dataset/labels/val/) PY这段脚本先用 random.shuffle 打乱图片列表前 80% 放进 train后 20% 放进 val。replace 操作把图片路径里的 images 换成 yolo_labels从而找到同名 txt。注意如果你的目录名不完全对应需要按实际情况修改替换规则。这里 seed 固定为 42 保证可复现方便后面和训练日志对照。3. 训练自己的焊缝数据集参数配置与曲线判读3.1 数据配置 yaml 与超参数选择YOLOv5 训练的第一步是准备一个数据配置文件 weld.yaml。这个文件告诉训练脚本图片路径、验证集路径、类别数和类别名内容非常简单。train: dataset/images/train val: dataset/images/val nc: 2 names: [weld_ok, weld_defect]上面例子里的 names 用 weld_ok 和 weld_defect 占位实际使用时要替换成你标注时的类别名且索引顺序必须和 txt 标注里的类别 id 一一对应。只要这里写错一个顺序模型就会把类别标签学反而且损失曲线看起来完全正常这就是很多人在验证集上 mAP 很高但实际部署却发现错判的原因。训练命令一般从 yolov5 根目录执行最基本的调用是python train.py --weights yolov5s.pt --data weld.yaml --hyp hyp.scratch-low.yaml --epochs 100 --batch-size 16 --img 640这里把常用参数整理成一张表。参数作用建议取值--weights预训练权重路径填空字符串表示从头训练yolov5s.pt 或最接近工况的权重--data数据集 yaml 文件weld.yaml--hyp超参数文件hyp.scratch-low.yaml 或 hyp.scratch.yaml--epochs训练轮数100300样本少时可加大--batch-size每次迭代样本数受显存限制8、16、32--img输入图片尺寸训练时会 resize640--patience验证指标不提升多少轮就早停50--deviceGPU id 或 CPU0 或 cpu焊缝检测这类目标尺寸差异不大的任务img 从 640 开始就足够。如果焊缝在画面里非常小比如整幅图里焊缝宽度只有几十像素考虑把 img 提到 1280 并配合更大感受野但显存消耗会成倍上涨。超参数文件里主要关注 lr0、lrf、momentum 和 weight_decay正常场景不要动 momentum只用 hyp.scratch-low 就能稳定收敛。需要说明的是如果你的 GPU 只有 6GB 显存batch-size 16 可能爆显存换成 8 同时把 img 降到 480 通常也能跑只是精度会略降。3.2 训练产物权重、PR 曲线、loss 曲线怎么读训练结束后所有产出都在 runs/train/exp 下如果多次训练会有 exp2、exp3 等。关键产物如下runs/train/exp/ ├── weights/ │ ├── best.pt │ └── last.pt ├── PR_curve.png ├── P_curve.png ├── R_curve.png ├── F1_curve.png ├── confusion_matrix.png └── results.pngbest.pt 是验证集上综合指标最好的权重部署时优先用它last.pt 是训练结束时那一轮的权重如果后期过拟合严重last.pt 往往比 best.pt 更差。PR_curve.png 是 Precision-Recall 曲线它不需要指定置信度阈值而是把所有阈值下的精度和召回率画成一条曲线。曲线越靠近右上角说明模型在保持高精度的同时还能把所有目标找出来曲线下的面积就是 AP。多类别时会把每个类别的曲线画出来所有类别 AP 的平均值就是 mAP。results.png 是 YOLOv5 自动生成的训练过程曲线里面包含 box_loss、obj_loss、cls_loss 和 mAP 的变化趋势。读这条曲线时不要只盯着训练集上的 loss训练 loss 降得再漂亮验证集上不降就是白训。看曲线的顺序应该是先看 val/obj_loss 是否下降再看 mAP_0.5 是否上升最后才看训练 loss 的收敛情况。obj_loss 是目标置信度损失它代表模型对“这里有没有目标”的判断误差box_loss 是边界框回归损失管框得准不准cls_loss 是分类损失管类别分得对不对。3.3 从 loss 曲线判断是否过拟合过拟合在数据量不大的焊缝检测里非常常见典型表现是训练 loss 持续下降验证 loss 在某个 epoch 掉头向上同时 mAP 开始波动。下表给出几个直接可用的判据。现象诊断处理train loss 下降val loss 也下降正常收敛继续训练train loss 下降val loss 不再降容量足够已接近上界提高数据多样性或减少模型容量train loss 继续降val loss 上升过拟合增加增广、增大 weight_decay、提前停止cls_loss 高但 obj_loss 低类别混淆检查 names 顺序查看混淆矩阵mAP 曲线震荡剧烈学习率偏高等间隔降低 lr0 或启用余弦退火我一般会在 epoch 60 左右看一眼 loss 曲线如果 val/box_loss 已经连续 20 轮没有更新说明模型可学的东西已经学完继续硬跑纯属浪费电。YOLOv5 自带早停机制patience 参数设成 50 即可它会监控验证集 mAP连续 50 轮不提升就自动结束。注意早停触发后 best.pt 是历史上最好的权重不受最后一轮影响。4. PyQt5 界面集成图片、视频、摄像头三条检测链路4.1 加载自定义权重与推理函数训练好的 best.pt 要集成进界面第一步是把模型的加载和推理封装成独立类这样三个检测入口都能复用同一套逻辑。推荐直接用 ultralytics/yolov5 的 hub 接口加载本地权重代码量最少。import cv2 import torch class WeldDetector: def __init__(self, weights_path, conf0.25, iou0.45): self.model torch.hub.load(ultralytics/yolov5, custom, pathweights_path, force_reloadFalse) self.model.conf conf self.model.iou iou def detect(self, bgr_img): results self.model(bgr_img) rendered results.render()[0] return rendered, resultstorch.hub.load 的第一个参数是 repo 名custom 表示加载本地训练好的权重path 指向 best.pt。如果你的机器没有外网第一次加载会把仓库下载到缓存离线时可以在本地 clone 一份 yolov5 源码再把第一个参数换成本地路径例如 torch.hub.load(./yolov5, custom, pathbest.pt, sourcelocal)。model.conf 和 model.iou 分别控制置信度阈值和 NMS 阈值后面界面里的滑块就改这两个属性。detect 方法接收 OpenCV 读进来的 BGR 图像返回两个结果rendered 是画完框的 BGR 图像results 是原始 YOLO 结果对象可以从中取类别名、置信度或目标框坐标。注意 results.render() 返回的是一个列表取 [0] 是因为输入只有一张图批量推理时会返回多张。这里不要直接对显示图像做缩放后再喂给模型YOLOv5 内部会做 letterbox 填充你只需要保证输入是 numpy 数组即可。4.2 三种输入源的处理方式图片检测最简单QFileDialog 选到路径后cv2.imread 读进来直接喂给 detector.detect然后借助 QImage 把 numpy 数组转成 QPixmap 显示。视频和摄像头本质上都是视频流只是打开方式不同一个是文件路径一个是设备索引。from PyQt5.QtCore import QThread, pyqtSignal import cv2 class DetectThread(QThread): frame_ready pyqtSignal(object) def __init__(self, detector, source): super().__init__() self.detector detector self.cap cv2.VideoCapture(source) def run(self): while self.cap.isOpened(): ret, frame self.cap.read() if not ret: break rendered, _ self.detector.detect(frame) self.frame_ready.emit(rendered)这里的 source 可以是视频文件路径也可以是摄像头索引。cv2.VideoCapture(0) 表示使用第一个摄像头如果电脑插了外置 USB 摄像头索引可能变成 1 或 2实测发现部分摄像头对读取延迟很敏感最好在打开前加两帧预热否则前几帧是黑的。把推理放在 QThread 里是因为 GPU 推理和 OpenCV 读取都会阻塞主线程如果直接在界面槽函数里做窗口会失去响应。在主界面里线程通过信号 frame_ready 把画完框的图像送回来。为了控制视频播放速度我一般在主线程用 QTimer 定时触发读取而不是在 run 里死循环摄像头场景则必须用线程持续读取否则帧缓冲会越积越多画面延迟越来越大。摄像头用线程还可以在 closeEvent 里调用 thread.quit 和 thread.wait 保证退出时不崩溃。对于标签显示可以在界面上放两个 QLabel一个显示原图一个显示检测结果方便对比。4.3 界面设计中的常见坑把模型、线程和界面接起来后实际运行最先遇到的坑往往不是模型精度而是环境与资源问题。下表整理了几个高频问题。现象原因解决办法打开摄像头后界面卡死推理占用了 GUI 线程改为 QThread 信号槽检测结果框颜色不对OpenCV BGR 与 Qt RGB 混用显示前用 cv2.cvtColor 转为 RGB第一次 detect 特别慢模型加载和 warmup 耗时应用启动时先加载模型并跑一张空图视频检测帧率低每帧都做 letterbox 和 GPU 推理降低 img 尺寸或用 ONNX Runtime摄像头显示有延迟读帧与推理同步阻塞读帧线程和推理线程解耦BGR 转 RGB 是最容易漏的一步cv2.imread 读出来是 BGRQImage 构造时 format 用 Format_RGB888 会显示颜色发蓝。正确的转换是在传信号之前或之后执行 cv2.cvtColor(rendered, cv2.COLOR_BGR2RGB)。另外界面上的置信度滑块最好直接绑定 detector.model.conf槽函数里只做 setText 显示不要重新加载模型。5. 验证与进阶用 val.py 复现指标并调优置信度5.1 在验证集上复现精度指标拿到权重以后第一件事不是在界面上点两张图看效果而是在自己的验证集上跑一遍官方验证脚本复现 mAP。这样可以排除随机干扰判断这份权重是否真的可靠。python val.py --weights runs/train/exp/weights/best.pt --data weld.yaml --img 640 --conf-thres 0.25 --iou-thres 0.45val.py 会输出每个类别的 Precision、Recall 和 mAP50同时生成一个检测结果的 txt 文件。关注 mAP50 和 mAP50-95 这两个指标mAP50 是 IoU 阈值 0.5 下的平均精度适合判断目标有没有被框到mAP50-95 对框的精度更严格适合衡量边框贴合度。用这份输出和训练时 results.png 里的曲线对照能快速发现数据目录是否搞错。5.2 根据 PR 曲线调整界面置信度阈值PR 曲线调阈值时不要只看默认的 conf0.25。曲线上的每个点对应一组置信度阈值左上区域表示高阈值右下区域表示低阈值。焊缝质量检测里漏掉一个缺陷比多画一个框的风险大得多所以我会把 conf 降到 0.1并同步把 iou 从 0.45 提到 0.5。这样同一道焊缝即使出现两个重叠框也会被 NMS 合并不会在界面上看到重复报警。如果你更在意误检率比如自动判定流水线要求不合格品必须复核conf 则调到 0.4 以上。调完后一定要在含光照变化的视频序列上实测静止图片上 PR 曲线表现好不代表动态场景下判断稳定。对焊缝这种亮度集中的图像还可以对输入做一次直方图均衡化再送进模型部分场景能额外提升召回率。本文还有配套的精品资源点击获取