☰
基于YOLOv8的焊缝缺陷检测系统:从数据标注到部署全流程解析
2026/9/27 23:06:41 网站建设 项目流程

简介:这是一套基于YOLOv8的化工管道焊缝缺陷检测系统,面向计算机视觉、人工智能等专业学生完成毕业设计或课程设计,也可作为初学者的深度学习实战参考。项目代码经完整测试,包含可视化界面、完整数据集、部署说明,可一站式完成模型训练、检测与结果分析。压缩包内共97个文件,主体为70个Python脚本和12个编译缓存文件,涵盖模型训练、检测服务、工具函数等模块;4个pt权重文件用于模型加载,5个xml配置用于环境管理,另有效果演示视频与说明文档,包体约24.21MB。使用该系统可获得核心指标曲线、混淆矩阵、F1分数曲线、精确率-召回率曲线、验证集预测结果及标签分布图,便于展示模型性能并支撑答辩评审。目前已有56人学习下载,适合需要快速落地深度学习检测项目、获取完整可复现流程的在校学生与工程师。

1. 一套能直接跑的焊缝缺陷检测系统,解决了毕设的三个难题

每年到毕设季,做深度学习视觉方向的人都会卡在同一个节奏里:模型跑通了但界面做不出来,界面做出来了但数据集不是自己的,数据集凑齐了训练又翻车。这套基于 YOLOv8 的化工管道焊缝缺陷检测系统,把数据、训练、部署三个环节全串起来了。它自带完整数据集和可视化界面,不是那种只有 ipynb 演示的玩具项目,而是能真正跑出检测结果、能截图放进论文里的完整系统。适合两类人:一类是拿它直接做毕设或课设的本科生,另一类是刚学完 YOLO 原理、想看看工程化落地的研究生。你不必从零搭界面,也不必满网找数据集,拆开就能用,但前提是你得知道每个模块的边界在哪。

2. 选型逻辑与系统构成:为什么焊缝缺陷检测适合 YOLOv8

2.1 从两阶段检测到单阶段:焊缝场景要求实时与精度兼顾

化工管道焊缝缺陷检测和通用目标检测最大的区别在于:缺陷尺寸小、对比度低、背景是金属纹理噪声。如果按老思路用 Faster R-CNN,两阶段检测器在精度上确实有优势,但推理速度很难压到实时。产线上的场景往往是相机固定、管道匀速通过,一个缺陷框要在几十毫秒内出来才有意义。YOLOv8 作为单阶段检测器,把分类和回归放在同一个解耦头里一次完成,在 GPU 上跑小模型能做到百帧以上,这是它的核心价值。

再说为什么选 YOLOv8 而不是 YOLOv5。YOLOv8 改了 C2f 结构替代原来的 C3,梯度流更丰富,对小目标的特征提取更充分;检测头从 Anchor-Based 换成了 Anchor-Free,少了一步 anchor 聚类,也少了一个最容易翻车的超参数。焊缝缺陷里的气孔、咬边这类小目标,Anchor-Free 的 center-based 回归方式比固定 anchor 更稳。我在拆这个项目时特意去翻了它的模型结构图,backbone 用 SPPF 做多尺度池化,neck 是 PAN-FPN 结构,浅层特征和深层语义反复融合,这几点都是为小目标检测服务的。

2.2 系统整体架构:数据、训练、界面三条线怎么串

这套系统的完整链路是:原始焊缝图像 → LabelImg 或 LabelMe 标注 → 转成 YOLO 格式 txt → 按 8:2 划分训练集和验证集 → ultralytics 框架训练 → 导出 best.pt 权重 → PyQt5 可视化界面加载权重做实时推理。每一步对应一个目录或一个脚本,结构清楚。

模块作用对应文件/目录
数据集标注好的焊缝缺陷样本datasets/ 下含 images 和 labels
标注转换把标注格式统一成 YOLO 格式scripts/convert.py
训练配置数据集路径、类别数、类别名data.yaml
训练入口ultralytics YOLOv8 训练命令命令行执行
可视化界面PyQt5 + OpenCV 推理展示main_window.py
部署导出转 ONNX,脱离 PyTorch 运行export_onnx.py

需要注意,这套系统的数据集已经标注完成,但你在实际使用中大概率要加入自己拍的现场图片来扩充。原因很简单:化工管道焊缝的缺陷形态和相机角度高度相关,公开数据集里的图和你现场拍的图光照差异很大,直接拿原权重去测,漏检率可能高到怀疑人生。数据增强能有一定的缓解,但最有效的还是往训练集里加图、重新训练。

3. 数据准备:把标注格式转成 YOLOv8 能吃的 txt 并正确划分数据集

3.1 数据集目录结构长什么样

YOLOv8 训练时要求的数据目录,不是你随便扔一堆图片就能跑。标准结构是 images 和 labels 分开放,训练集和验证集各一份。这套系统里已经按这个规则整理好了:train/images、train/labels、val/images、val/labels。图片和标签的文件名必须一一对应,比如pipe_001.jpg对应pipe_001.txt,连后缀都不能错,否则训练时这张图会被静默跳过。

焊缝缺陷的类别定义需要和标注文件里的类名严格一致。我拆过的这类项目里,最常见的是按六类缺陷来标:咬边(undercut)、气孔(alveolus)、夹渣(slag_inclusion)、未熔合(lack_of_fusion)、未焊透(lack_of_penetration)、裂纹(crack)。如果你自己重新标注,务必把类名统一成英文字母,中文类名在 YOLO 训练时虽然不至于报错,但后续画混淆矩阵、做 UI 显示都会多一层编码转换的麻烦。

3.2 LabelMe 的 JSON 转 YOLO 格式:完整转换脚本

LabelMe 标注出来的是多边形轮廓,存成 JSON 文件,里面是绝对像素坐标的 points 数组。YOLO 格式要求的是归一化后的中心点坐标和宽高,而且只接受矩形框。这就必须把多边形转换成外接矩形,再除以图像宽高做归一化。下面是我在这类项目里常用的转换脚本:

import json import os import glob from pathlib import Path def labelme_json_to_yolo(json_path, class_names, output_dir): """把单个 LabelMe JSON 文件转成 YOLO 格式 txt""" with open(json_path, encoding='utf-8') as f: data = json.load(f) img_w = data['imageWidth'] # 原图宽度,归一化要用 img_h = data['imageHeight'] # 原图高度 txt_name = Path(json_path).stem + '.txt' lines = [] for shape in data['shapes']: label = shape['label'] if label not in class_names: continue # 跳过没有定义类别的标注 class_id = class_names.index(label) points = shape['points'] # 取所有点的最小外接矩形的四个边界 xs = [p[0] for p in points] ys = [p[1] for p in points] x_min, x_max = min(xs), max(xs) y_min, y_max = min(ys), max(ys) # YOLO 格式:类别 中心x 中心y 宽度 高度,全部归一化到 0~1 x_center = ((x_min + x_max) / 2) / img_w y_center = ((y_min + y_max) / 2) / img_h w = (x_max - x_min) / img_w h = (y_max - y_min) / img_h # 防越界:个别标注会超出图像边缘,裁剪到 [0,1] x_center = min(max(x_center, 0.0), 1.0) y_center = min(max(y_center, 0.0), 1.0) w = min(w, 1.0) h = min(h, 1.0) lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") with open(os.path.join(output_dir, txt_name), 'w', encoding='utf-8') as f: f.write('\n'.join(lines)) class_names = ['undercut', 'alveolus', 'slag_inclusion', 'lack_of_fusion', 'lack_of_penetration', 'crack'] input_dir = 'labelme_json' # 放 LabelMe 导出的 JSON output_dir = 'labels' # 转换后的 txt 输出目录 os.makedirs(output_dir, exist_ok=True) for json_file in glob.glob(os.path.join(input_dir, '*.json')): labelme_json_to_yolo(json_file, class_names, output_dir) print(f'converted: {json_file}')

这段脚本的关键在于:归一化时用的是 JSON 里记录的imageWidth和imageHeight,而不是cv2.imread读出来的宽高。原因很简单,LabelMe 在标注时可能对图片做过缩放显示,但 JSON 里存的是原始尺寸,用原始尺寸算出来的归一化坐标才是对的。如果标注时图片被编辑器压缩过,你没有实时拿到压缩后的尺寸,用不同的宽高去归一化,坐标会整体偏移,训练出来的模型检测框全部偏在左上角或右下角,属于最容易翻车的地方。

3.3 数据划分:别用 random.shuffle 裸跑

把图片和标签复制到训练、验证目录时,最忌讳的做法是random.shuffle之后直接按比例切,然后不固定随机种子。你每次跑出来的划分结果都不一样,模型性能的好坏就分不清是数据分布变了还是模型参数变了。我在这类项目里的固定做法是用sklearn的train_test_split固定random_state:

import os import random import shutil from pathlib import Path from sklearn.model_selection import train_test_split random.seed(42) image_dir = Path('images') # 所有原始图片 label_dir = Path('labels') # 所有 txt 标签 train_img = Path('train/images') val_img = Path('val/images') train_lbl = Path('train/labels') val_lbl = Path('val/labels') for d in [train_img, val_img, train_lbl, val_lbl]: d.mkdir(parents=True, exist_ok=True) images = sorted(list(image_dir.glob('*.jpg'))) + sorted(list(image_dir.glob('*.png'))) pairs = [] for img in images: lbl = label_dir / (img.stem + '.txt') if lbl.exists(): pairs.append((img, lbl)) # 只保留有标签的图片 train_pairs, val_pairs = train_test_split( pairs, test_size=0.2, random_state=42) for img, lbl in train_pairs: shutil.copy(img, train_img / img.name) shutil.copy(lbl, train_lbl / lbl.name) for img, lbl in val_pairs: shutil.copy(img, val_img / img.name) shutil.copy(lbl, val_lbl / lbl.name) print(f'train: {len(train_pairs)}, val: {len(val_pairs)}')

这里有个实际经验:过滤掉没有标签的图片是必须的一步。YOLO 训练时遇到没有 txt 的图片会跳过并打印 warning,但如果这种图占比超过 5%,你会发现训练集的有效样本比预期少很多,模型学不到足够的正样本,mAP 上不去还找不到原因。另外,验证集比例 0.2 是通用做法,如果样本量本身不足一百张,我建议降到 0.15,并且打开--augment加强数据增强。

4. 训练实战:data.yaml 怎么写、参数怎么调、损失曲线怎么看

4.1 data.yaml 的字段含义与路径陷阱

YOLOv8 的训练配置入口是 data.yaml,它不是 Python 文件,是纯文本,但缩进必须严格按 YAML 语法来。常见的写法如下:

path: D:/welding_defect_dataset # 数据集根目录,绝对路径或相对路径 train: images/train # 训练图片相对 path 的目录 val: images/val # 验证图片相对 path 的目录 nc: 6 # 类别数,必须和标注里的 class_id 对应 names: ['undercut', 'alveolus', 'slag_inclusion', 'lack_of_fusion', 'lack_of_penetration', 'crack']

这三个坑是我实际踩过的。第一,path尽量不要写成包含中文的路径。Windows 下中文路径经常导致 DataLoader 在读取文件时报 UnicodeDecodeError,报错信息还很隐蔽,只显示某个 jpg 文件无法读取,不会提示是路径问题。第二,train和val的目录要写相对path的路径,不要写成绝对路径里的完整目录。写反了训练也能跑,因为它内部会做路径拼接,但如果你换机器迁移,绝对路径直接失效。第三,nc必须和 names 列表长度一致,类别顺序不能乱。nc: 6而 names 只写了 5 个名字,训练不报错,但输出的类别索引和可视化标签对不上,推理时界面显示的缺陷名称永远是错位的。

4.2 训练命令一行一行拆解

训练用的命令是 ultralytics 的 CLI 入口,直接用yolo命令调用。这是我在 GTX 1660Ti 这种 6GB 显存显卡上调过的一套参数:

yolo detect train \ data=data.yaml \ model=yolov8n.pt \ epochs=100 \ batch=16 \ imgsz=640 \ device=0 \ patience=20 \ workers=4 \ cache=True

每个参数的含义和调节方向分开说。model=yolov8n.pt表示用 YOLOv8n 的预训练权重作为起点。这里有个微妙的地方:model参数给的如果不是一个已训练好的权重,而是类似yolov8n.yaml的结构文件,模型会从零开始训练,没有 COCO 预训练权重做初始化,收敛速度和最终精度都会差一截。除非你的数据集和 COCO 分布完全无关,否则建议跑微调,也就是给yolov8n.pt。

epochs=100对焊缝缺陷这种小数据集来说是够的。核心原因:这类任务不是大规模识别,样本量通常在几千张级别,100 轮足够让模型在验证集上收敛。更大的轮数不会显著提升 mAP,反而增加过拟合风险。patience=20是早停机制,连续 20 轮验证集 mAP 没有提升就自动停止训练,这是省时间的核心参数。

batch=16在 6GB 显存上是安全值。如果显存不够,训练会直接 OOM 报错,这时优先降batch,而不是降imgsz。因为imgsz=640直接影响检测精度,降到 480 后,气孔这种小尺寸缺陷的像素占比进一步缩小,召回率会明显下降。workers=4是数据加载线程数,Windows 上如果设为 8 偶尔会因为线程冲突卡死,4 是最稳的值。cache=True把数据集预加载到内存,第二次训练同一份数据时速度提升非常明显。

4.3 训练结果目录里的 results.png 怎么看

训练完成后,runs/detect/train/目录会生成结果文件,results.png里包含 loss 曲线和指标曲线,这个必须会读。第一行是 train/box_loss 和 train/cls_loss,第二行是 val/box_loss 和 val/cls_loss。判断模型是否收敛的标准是:训练 loss 持续下降、验证 loss 在最后 20 轮基本平稳。如果验证 loss 在前 30 轮就掉头向上,说明过拟合开始了,这时再去增加数据或调大patience意义不大,更该做的是换小模型,比如从yolov8s换回yolov8n。

metrics/mAP50(B)是 mAP@0.5 指标,也就是 IoU 阈值设 0.5 时的平均精度。焊缝缺陷检测里这个值能到 0.85 以上就算合格。metrics/mAP50-95(B)是更严苛的指标,从 0.5 到 0.95 每隔 0.05 算一次再取平均,这个值比 mAP50 更反映框的定位精度。如果你的 mAP50 高但 mAP50-95 低,说明框的位置不够准、或者不够紧凑,优先检查标注框是否把背景包进去了太多。

训练完成后,weights/best.pt和weights/last.pt最有用。best.pt是验证集表现最好的权重,推理和部署都用它;last.pt是最后一轮的权重,训练被中断时用来恢复。千万别在部署时用last.pt,它的表现通常比best.pt差好几个点。

5. 避坑指南:部署这套系统时踩过的四个真实问题

5.1 中文路径导致 DataLoader 崩溃

现象:训练命令看起来没问题,数据量也检查了,但一启动就在读取图片时报错,错误信息里有一长串 UnicodeDecodeError,后面跟着某个 jpg 的完整路径。

原因:数据集目录里含有中文或空格。ultralytics 框架在 Windows 下用多线程加载图片时,对非 ASCII 路径的处理并不友好,底层 PIL 读取文件时编码不一致直接抛异常。

解决:把整个数据集目录改成英文,比如从D:\毕设\焊缝缺陷数据集改成D:\welding_defect_dataset,并且把data.yaml里的path同步修改。项目里所有脚本路径涉及中文的都检查一遍,包括界面读取图片的目录。这一步是最省钱、最有效的后悔药,强烈建议在解压项目后第一时间做。

5.2 标注框越界导致 loss 变成 NaN

现象:训练正常跑了十几个 epoch,某一次开始 box_loss 突然变成 nan,之后所有 loss 都是 nan,训练直接废掉。有时候不报错,但 mAP 一直是 0。

原因:标注文件里存在超出图像边界的框,或者宽高为 0 的框。YOLOv8 在计算 IoU 时遇到越界坐标,梯度计算中出现除零,就变成 NaN。几何上,原始图像标注的时候框边缘正好压在图片边界上,归一化时四舍五入让 w 或 h 四舍五入成了 0,这是个很隐蔽的问题。

解决:训练前跑一遍批量检查脚本,找出所有坐标不在 [0,1] 内的行,以及 w 或 h 为 0 的行,把异常样本从数据集中移除,而不是直接改坐标值。改坐标值等于人为修改标注,不如删掉重标。我在这类项目里都会加一道校验,每个 txt 文件读出来逐行判断,发现异常就打印文件名并标记,这是一劳永逸的做法。

5.3 工业相机拍的图太大导致显存直接不够

现象:数据集里的图片大多是 1920×1080 甚至更大,训练时batch=16直接 OOM,改成batch=8仍然 OOM,最后只能batch=2,训练速度慢到无法接受,而且小 batch 的梯度噪声大,模型不容易收敛。

原因:imgsz=640意味着图片会被 resize 到 640×640 再进网络。显存消耗来自中间特征图,理论上和原始图片分辨率无关。但你的数据集如果是全高清图,数据加载器在 resize 前会先把原图完整读入内存,这个临时内存开销在某些版本下会叠加进显存统计。另外,批量加载时大图解码耗时也长,GPU 一直在等数据。

解决:先把全部图片统一缩放到 1280×1280 以内的尺寸再入训练集,批量脚本预处理一遍。或者更简单:直接在训练命令加imgsz=640的同时,把rect=True加进去,这个参数让 YOLO 按每批图片的长宽比做填充而不是暴力压成正方形,能在不损失信息的前提下省显存。配合batch=16重新跑,通常能解决。

5.4 可视化界面打开后加载模型像假死

现象:双击 exe 或运行python main_window.py后,窗口弹出来了,但一卡就是十几秒,鼠标转圈,标题栏显示“未响应”,用户第一反应是程序崩了直接关掉。

原因:PyQt5 的界面线程只有一个,而YOLO('weights/best.pt')加载模型时要初始化 CUDA 上下文、读取权重、预热模型,这个操作耗时 5~15 秒,期间主线程被阻塞,消息循环无法响应,系统就会标记为“未响应”。

解决:把模型加载移出主线程,放到启动线程里,加载完成后用信号通知主线程更新界面状态。或者最简单的方式:在主窗口显示之前先触发一次无画面推理预热,把加载耗时放在启动动画之后。更讲究的版本是做一个单独的闪屏窗口,模型加载完再切换到主窗口。这种细节,答辩演示时特别加分,用户不会觉得系统是卡死的。

6. 部署进阶:PyQt5 界面集成与 ONNX 导出技巧

6.1 PyQt5 界面加载 YOLOv8 模型的推理骨架

可视化界面这套系统自带 PyQt5 版本。核心结构就是 QLabel 显示画面、QTimer 定时抓帧、模型推理、绘制检测框。下面是一个最小可用的界面推理骨架,你可以在它的基础上做阈值滑块等功能:

import sys import cv2 from PyQt5.QtCore import QTimer, Qt from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtWidgets import QMainWindow, QLabel, QApplication from ultralytics import YOLO class Viewer(QMainWindow): def __init__(self): super().__init__() self.label = QLabel('画面显示区') self.setCentralWidget(self.label) self.model = YOLO('weights/best.pt') self.cap = cv2.VideoCapture('test_video.mp4') self.timer = QTimer(self) self.timer.timeout.connect(self.update_frame) self.timer.start(30) # 约 33ms 一帧 def update_frame(self): ret, frame = self.cap.read() if not ret: self.timer.stop() return results = self.model.predict( frame, conf=0.25, iou=0.45, verbose=False) annotated = results[0].plot() # YOLOv8 自带画框方法 # BGR 转 RGB 再转 QImage 显示 rgb = cv2.cvtColor(annotated, cv2.COLOR_BGR2RGB) h, w, ch = rgb.shape qimg = QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) self.label.setPixmap(QPixmap.fromImage(qimg))

这段代码里最值得说明的是results[0].plot(),它会在检测结果图上把类别名、置信度和边框全部画好,省去了手工写cv2.rectangle和cv2.putText的繁琐步骤,中文类别名也在这里正确显示。conf=0.25是置信度阈值,低于这个值的检测框会被过滤掉;iou=0.45是 NMS 的 IoU 阈值,值越小抑制越强,同一区域的重叠框越少。这两个参数就是你在界面上做成滑块调节的那两个,建议暴露出来,答辩时现场调阈值做对比展示非常有效。

6.2 训练好的模型怎么导出 ONNX 并脱离 PyTorch 跑

毕设验收时电脑上不一定装好了完整的 PyTorch 环境,所以把模型转成 ONNX 再用 onnxruntime 推理是更稳的一条路。导出的命令是:

yolo export model=weights/best.pt format=onnx opset=12 simplify=True imgsz=640

导出后验证一下模型是否正常:

yolo predict model=weights/best.onnx source=test_images/device=cpu

这里需要注意两点:opset=12是兼容性最广的算子集版本,装 onnxruntime 老版本的机器也能跑;simplify=True会调用 onnx-simplifier 做图优化,能砍掉不少多余算子。我用这个命令导出后,模型体积从 PyTorch 权重的大小压缩了大概三成,CPU 推理速度也能压到单张图百毫秒以内,对答辩演示来说足够。用 onnxruntime 推理的代码骨架如下:

import onnxruntime as ort import numpy as np import cv2 session = ort.InferenceSession( 'weights/best.onnx', providers=['CPUExecutionProvider']) frame = cv2.imread('pipe_test.jpg') input_tensor = cv2.dnn.blobFromImage( frame, 1/255.0, (640, 640), swapRB=True, crop=False) outputs = session.run(None, {session.get_inputs()[0].name: input_tensor}) # outputs[0] 的 shape 是 [1, 6, 8400],解析方式参考 YOLOv8 官方仓库

这里blobFromImage做了 resize、归一化和 BGR 转 RGB 的整套预处理,和训练时的预处理对齐。输出张量是 6×8400 的矩阵:6 是 4 个框坐标加 2 个类别概率(你的项目按类别数变化),8400 是三个尺度特征图的候选框总数。解析这个矩阵需要写一个非极大值抑制的后处理函数,虽然 YOLOv8 的 PyTorch 推理封装了这一切,但 ONNX 版本暴露了原始输出。如果你只是答辩演示,用 PyTorch 版本即可;如果要做成现场可演示的 exe,ONNX 方案更推荐。

6.3 我最后悔没早点做的一件事

第一次部署这套系统时,我把界面里的置信度阈值写死成了 0.25,因为代码里就是默认值,嫌加滑块麻烦。结果现场演示时,有一张焊缝图像的阴影被模型误报成了裂纹,置信度 0.31,界面直接框了个大红框,评审老师问“这个误检怎么回事”,我只能硬着头皮解释。从那以后,我每次给这类项目做界面,都强制把 conf、iou、输入尺寸这三个参数全部暴露成界面上的调节控件,让使用者能随时调整,而不是在代码里改完重启。看起来多写了一个 QSlider,实际上等于给系统留了一颗后悔药。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询