基于YOLOv8的焊缝缺陷检测系统:从数据标注到部署全流程解析
简介这是一套基于YOLOv8的化工管道焊缝缺陷检测系统面向计算机视觉、人工智能等专业学生完成毕业设计或课程设计也可作为初学者的深度学习实战参考。项目代码经完整测试包含可视化界面、完整数据集、部署说明可一站式完成模型训练、检测与结果分析。压缩包内共97个文件主体为70个Python脚本和12个编译缓存文件涵盖模型训练、检测服务、工具函数等模块4个pt权重文件用于模型加载5个xml配置用于环境管理另有效果演示视频与说明文档包体约24.21MB。使用该系统可获得核心指标曲线、混淆矩阵、F1分数曲线、精确率-召回率曲线、验证集预测结果及标签分布图便于展示模型性能并支撑答辩评审。目前已有56人学习下载适合需要快速落地深度学习检测项目、获取完整可复现流程的在校学生与工程师。1. 一套能直接跑的焊缝缺陷检测系统解决了毕设的三个难题每年到毕设季做深度学习视觉方向的人都会卡在同一个节奏里模型跑通了但界面做不出来界面做出来了但数据集不是自己的数据集凑齐了训练又翻车。这套基于 YOLOv8 的化工管道焊缝缺陷检测系统把数据、训练、部署三个环节全串起来了。它自带完整数据集和可视化界面不是那种只有 ipynb 演示的玩具项目而是能真正跑出检测结果、能截图放进论文里的完整系统。适合两类人一类是拿它直接做毕设或课设的本科生另一类是刚学完 YOLO 原理、想看看工程化落地的研究生。你不必从零搭界面也不必满网找数据集拆开就能用但前提是你得知道每个模块的边界在哪。2. 选型逻辑与系统构成为什么焊缝缺陷检测适合 YOLOv82.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, encodingutf-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, encodingutf-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_okTrue) for json_file in glob.glob(os.path.join(input_dir, *.json)): labelme_json_to_yolo(json_file, class_names, output_dir) print(fconverted: {json_file})这段脚本的关键在于归一化时用的是 JSON 里记录的imageWidth和imageHeight而不是cv2.imread读出来的宽高。原因很简单LabelMe 在标注时可能对图片做过缩放显示但 JSON 里存的是原始尺寸用原始尺寸算出来的归一化坐标才是对的。如果标注时图片被编辑器压缩过你没有实时拿到压缩后的尺寸用不同的宽高去归一化坐标会整体偏移训练出来的模型检测框全部偏在左上角或右下角属于最容易翻车的地方。3.3 数据划分别用 random.shuffle 裸跑把图片和标签复制到训练、验证目录时最忌讳的做法是random.shuffle之后直接按比例切然后不固定随机种子。你每次跑出来的划分结果都不一样模型性能的好坏就分不清是数据分布变了还是模型参数变了。我在这类项目里的固定做法是用sklearn的train_test_split固定random_stateimport 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(parentsTrue, exist_okTrue) 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_size0.2, random_state42) 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(ftrain: {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 \ datadata.yaml \ modelyolov8n.pt \ epochs100 \ batch16 \ imgsz640 \ device0 \ patience20 \ workers4 \ cacheTrue每个参数的含义和调节方向分开说。modelyolov8n.pt表示用 YOLOv8n 的预训练权重作为起点。这里有个微妙的地方model参数给的如果不是一个已训练好的权重而是类似yolov8n.yaml的结构文件模型会从零开始训练没有 COCO 预训练权重做初始化收敛速度和最终精度都会差一截。除非你的数据集和 COCO 分布完全无关否则建议跑微调也就是给yolov8n.pt。epochs100对焊缝缺陷这种小数据集来说是够的。核心原因这类任务不是大规模识别样本量通常在几千张级别100 轮足够让模型在验证集上收敛。更大的轮数不会显著提升 mAP反而增加过拟合风险。patience20是早停机制连续 20 轮验证集 mAP 没有提升就自动停止训练这是省时间的核心参数。batch16在 6GB 显存上是安全值。如果显存不够训练会直接 OOM 报错这时优先降batch而不是降imgsz。因为imgsz640直接影响检测精度降到 480 后气孔这种小尺寸缺陷的像素占比进一步缩小召回率会明显下降。workers4是数据加载线程数Windows 上如果设为 8 偶尔会因为线程冲突卡死4 是最稳的值。cacheTrue把数据集预加载到内存第二次训练同一份数据时速度提升非常明显。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)是 mAP0.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 甚至更大训练时batch16直接 OOM改成batch8仍然 OOM最后只能batch2训练速度慢到无法接受而且小 batch 的梯度噪声大模型不容易收敛。原因imgsz640意味着图片会被 resize 到 640×640 再进网络。显存消耗来自中间特征图理论上和原始图片分辨率无关。但你的数据集如果是全高清图数据加载器在 resize 前会先把原图完整读入内存这个临时内存开销在某些版本下会叠加进显存统计。另外批量加载时大图解码耗时也长GPU 一直在等数据。解决先把全部图片统一缩放到 1280×1280 以内的尺寸再入训练集批量脚本预处理一遍。或者更简单直接在训练命令加imgsz640的同时把rectTrue加进去这个参数让 YOLO 按每批图片的长宽比做填充而不是暴力压成正方形能在不损失信息的前提下省显存。配合batch16重新跑通常能解决。5.4 可视化界面打开后加载模型像假死现象双击 exe 或运行python main_window.py后窗口弹出来了但一卡就是十几秒鼠标转圈标题栏显示“未响应”用户第一反应是程序崩了直接关掉。原因PyQt5 的界面线程只有一个而YOLO(weights/best.pt)加载模型时要初始化 CUDA 上下文、读取权重、预热模型这个操作耗时 515 秒期间主线程被阻塞消息循环无法响应系统就会标记为“未响应”。解决把模型加载移出主线程放到启动线程里加载完成后用信号通知主线程更新界面状态。或者最简单的方式在主窗口显示之前先触发一次无画面推理预热把加载耗时放在启动动画之后。更讲究的版本是做一个单独的闪屏窗口模型加载完再切换到主窗口。这种细节答辩演示时特别加分用户不会觉得系统是卡死的。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, conf0.25, iou0.45, verboseFalse) 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的繁琐步骤中文类别名也在这里正确显示。conf0.25是置信度阈值低于这个值的检测框会被过滤掉iou0.45是 NMS 的 IoU 阈值值越小抑制越强同一区域的重叠框越少。这两个参数就是你在界面上做成滑块调节的那两个建议暴露出来答辩时现场调阈值做对比展示非常有效。6.2 训练好的模型怎么导出 ONNX 并脱离 PyTorch 跑毕设验收时电脑上不一定装好了完整的 PyTorch 环境所以把模型转成 ONNX 再用 onnxruntime 推理是更稳的一条路。导出的命令是yolo export modelweights/best.pt formatonnx opset12 simplifyTrue imgsz640导出后验证一下模型是否正常yolo predict modelweights/best.onnx sourcetest_images/devicecpu这里需要注意两点opset12是兼容性最广的算子集版本装 onnxruntime 老版本的机器也能跑simplifyTrue会调用 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), swapRBTrue, cropFalse) 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 版本即可如果要做成现场可演示的 exeONNX 方案更推荐。6.3 我最后悔没早点做的一件事第一次部署这套系统时我把界面里的置信度阈值写死成了 0.25因为代码里就是默认值嫌加滑块麻烦。结果现场演示时有一张焊缝图像的阴影被模型误报成了裂纹置信度 0.31界面直接框了个大红框评审老师问“这个误检怎么回事”我只能硬着头皮解释。从那以后我每次给这类项目做界面都强制把 conf、iou、输入尺寸这三个参数全部暴露成界面上的调节控件让使用者能随时调整而不是在代码里改完重启。看起来多写了一个 QSlider实际上等于给系统留了一颗后悔药。希望帮到你。本文还有配套的精品资源点击获取