基于YOLOv8的轮胎缺陷检测系统:从训练到ONNX部署与GUI实战

发布时间:2026/10/5 4:53:23
基于YOLOv8的轮胎缺陷检测系统:从训练到ONNX部署与GUI实战
简介本资源是一套基于YOLOv8的轮胎缺陷检测系统完整工程包面向从事工业质检、智能制造方向的开发者与算法学习者可用于轮胎图像中debris、side_cut、ground、side等缺陷的自动识别与分类。包内共149个文件以jpg与png图像样本、xml标注、py源码、pt与onnx模型、yaml配置及csv评估记录为主压缩包约34.74MB覆盖数据、训练、推理与部署全流程。系统配套PyQt5精美GUI界面包含图像显示窗口与结果展示区域非专业人员也能直观操作同时提供评估指标曲线便于查看准确率、召回率与mAP等性能表现。运行环境为Windows10搭配anaconda3、python3.8、torch1.9.0与ultralytics8.2.70。目前已有494人学习下载适合希望快速复现轮胎缺陷检测方案、理解YOLOv8工程落地细节的读者参考。1. 轮胎缺陷检测为什么值得用 YOLOv8 重做一遍轮胎X光或表面缺陷检测这条产线我前后跟过三条。早期方案基本是传统视觉边缘检测加模板匹配遇到胎侧气泡、帘布层错位、异物嵌入这几类缺陷误报率高得离谱换一批胎型就得重新调阈值。后来上深度学习从 YOLOv5 一路换到 YOLOv8才算把召回率和误报率同时压下来。这套「基于 YOLOv8 的轮胎缺陷检测系统」讲的就是这么一件事用 YOLOv8 训练一个能识别轮胎缺陷的检测模型导出 ONNX 做推理加速配上评估指标曲线判断模型到底行不行最后套一个 GUI 界面让产线质检员能直接点着用。它解决的核心问题是把「训练—评估—部署—交互」这条链路一次性打通而不是只丢给你一个训练脚本。适合两类人一类是想拿轮胎缺陷当练手项目、顺便把 YOLOv8 全流程走一遍的算法新手另一类是产线上已经有相机和工控机、想把检测环节换成深度学习方案的工程师。下面我按自己落地的顺序把数据、训练、导出、评估、GUI 这几块拆开讲参数和坑都写清楚。2. 数据准备与 YOLOv8 训练从轮胎缺陷标注到第一个能用的权重2.1 轮胎缺陷的类别定义与标注规范轮胎缺陷检测翻车十次有八次栽在标注上。轮胎缺陷大致分几类胎侧气泡bulge、帘布层错位cord_misalign、异物foreign_object、裂纹crack、缺胶lack_rubber。类别不要贪多我一般先收敛到 35 类类别太细会导致类间样本极度不均衡模型学不动。标注用 LabelImg 或 X-AnyLabeling输出 YOLO 格式的 txt每行class_id cx cy w h坐标全部归一化到 01。这里有个血泪经验轮胎是圆的缺陷经常贴着胎圈边缘标注框很容易超出图像边界。归一化后如果出现负值或大于 1 的值训练时会被静默丢弃你还在纳闷为什么 loss 不降。标完一定要跑一遍校验脚本。import os from pathlib import Path def check_labels(label_dir, img_dir): 校验 YOLO 标注坐标越界、空标注、类别越界 bad [] for txt in Path(label_dir).glob(*.txt): img Path(img_dir) / (txt.stem .jpg) if not img.exists(): bad.append((txt.name, 缺对应图片)) continue lines txt.read_text().strip().splitlines() if not lines: bad.append((txt.name, 空标注)) continue for ln in lines: parts ln.split() if len(parts) ! 5: bad.append((txt.name, f字段数错误:{ln})) continue cid, cx, cy, w, h parts vals [float(cx), float(cy), float(w), float(h)] if any(v 0 or v 1 for v in vals): bad.append((txt.name, f坐标越界:{ln})) if int(cid) 0: bad.append((txt.name, f类别越界:{ln})) return bad if __name__ __main__: problems check_labels(labels/train, images/train) for name, reason in problems: print(name, reason) print(f共发现 {len(problems)} 处问题)这段脚本做三件事检查每张标注是否有对应图片、是否有空标注、坐标和类别是否越界。参数上label_dir和img_dir要指向同一批数据训练集和验证集分别跑一次。发现越界框要么重新标要么在脚本里做 clip 截断但截断会改变框的真实尺寸我一般选择重标。2.2 数据集目录结构与 data.yaml 配置YOLOv8 对目录结构有约定别自己乱放。标准结构是 images/train、images/val、labels/train、labels/val 四个目录图片和标注文件名一一对应。data.yaml 里写清路径和类别名path: /data/tire_defect train: images/train val: images/val nc: 4 names: 0: bulge 1: cord_misalign 2: foreign_object 3: cracknc是类别数必须和 names 的条目数一致写错了训练直接报维度不匹配。path用绝对路径最稳相对路径在不同工作目录下跑容易找不到数据。如果数据量小可以只留 train 和 valYOLOv8 会自动从 train 里切一部分做验证但我不推荐轮胎缺陷样本本来就少手动分层切分更能保证每类在验证集里都有。2.3 用命令行和 Python 两种方式启动训练YOLOv8 训练有两种入口命令行适合快速试Python 脚本适合写进流水线。先装环境pip install ultralytics onnx onnxruntime opencv-python pyqt5命令行训练yolo detect train \ modelyolov8s.pt \ data/data/tire_defect/data.yaml \ epochs200 \ imgsz640 \ batch16 \ lr00.01 \ patience30 \ device0 \ projectruns/tire \ nameexp1Python 方式更适合加回调、改损失from ultralytics import YOLO model YOLO(yolov8s.pt) # 从预训练权重起步小数据集必备 results model.train( data/data/tire_defect/data.yaml, epochs200, imgsz640, batch16, lr00.01, lrf0.01, # 最终学习率 lr0 * lrf patience30, # 30 轮无提升就早停 device0, projectruns/tire, nameexp1, exist_okTrue, )参数说明model选 yolov8s 是精度和速度的折中工控机上跑得动imgsz640是默认值轮胎缺陷如果很小比如细裂纹可以提到 960但显存和推理时间都会涨batch16在 8G 显存上比较稳爆显存就降到 8patience30是早停防止过拟合lr00.01是初始学习率配合预训练权重够用。训练完权重在runs/tire/exp1/weights/best.pt。2.4 训练过程怎么看损失曲线和 mAP 曲线训练时终端会打印每轮的 box_loss、cls_loss、dfl_loss 和 mAP50、mAP50-95。判断模型好不好别只盯最终 mAP要看曲线形状。box_loss 和 cls_loss 应该平滑下降如果震荡剧烈多半是学习率太大或 batch 太小mAP50 前期快速上升后趋平是正常的如果一直上不去先怀疑标注质量再怀疑类别不均衡。YOLOv8 训练完会在runs/tire/exp1/下生成 results.csv 和一堆曲线图。results.csv 里每行是一轮列名包括 train/box_loss、metrics/mAP50 等。想自己画图用 pandas 读出来import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/tire/exp1/results.csv) df.columns [c.strip() for c in df.columns] # 列名可能带空格 fig, ax1 plt.subplots(figsize(10, 5)) ax1.plot(df[epoch], df[train/box_loss], labelbox_loss) ax1.plot(df[epoch], df[val/box_loss], labelval_box_loss) ax1.set_xlabel(epoch) ax1.set_ylabel(loss) ax1.legend(locupper right) ax2 ax1.twinx() ax2.plot(df[epoch], df[metrics/mAP50(B)], colorred, labelmAP50) ax2.set_ylabel(mAP50) ax2.legend(loclower right) plt.title(Tire Defect Training Curve) plt.savefig(train_curve.png, dpi150)这里有个热搜里常提的坑——「python画图横坐标太密集」。epoch 多了以后 x 轴标签挤成一团解决办法是用ax1.xaxis.set_major_locator(plt.MaxNLocator(10))限制刻度数量或者把图拉宽。曲线图是给评估用的不是装饰横轴看不清等于白画。3. 导出 ONNX 与推理加速让模型离开 PyTorch 也能跑3.1 为什么轮胎检测要导出 ONNXPyTorch 权重部署到产线工控机得装一整套 torch包大、启动慢还挑 CUDA 版本。ONNX 是中间格式用 onnxruntime 推理依赖轻、跨平台CPU 上也能跑出可接受的帧率。轮胎检测这种单帧推理任务ONNX 基本是标配。热搜里「pytorch转onnx」「yolo导出onnx模型」说的就是这一步。3.2 导出命令与关键参数yolo export \ modelruns/tire/exp1/weights/best.pt \ formatonnx \ imgsz640 \ opset12 \ simplifyTrue \ dynamicFalse参数逐个说opset12兼容性最好别盲目追高版本有些 onnxruntime 版本对高 opset 支持不全simplifyTrue会调用 onnx-simplifier 做图优化能去掉冗余节点但偶尔会改坏结构导出后一定要验证dynamicFalse表示固定输入尺寸产线相机分辨率固定时选 False推理更快如果输入尺寸会变才设 True。导出后得到 best.onnx用 netron 打开看一眼输入输出节点名后面写推理代码要用。3.3 onnxruntime 推理代码与前后处理import cv2 import numpy as np import onnxruntime as ort class TireDetector: def __init__(self, onnx_path, conf_thres0.25, iou_thres0.45): self.session ort.InferenceSession( onnx_path, providers[CPUExecutionProvider] ) self.input_name self.session.get_inputs()[0].name self.conf_thres conf_thres self.iou_thres iou_thres def preprocess(self, img, size640): # letterbox 保持长宽比避免轮胎被拉伸变形 h, w img.shape[:2] scale min(size / h, size / w) nh, nw int(h * scale), int(w * scale) resized cv2.resize(img, (nw, nh)) canvas np.full((size, size, 3), 114, dtypenp.uint8) top (size - nh) // 2 left (size - nw) // 2 canvas[top:top nh, left:left nw] resized blob canvas[:, :, ::-1].transpose(2, 0, 1) # BGR-RGB, HWC-CHW blob np.ascontiguousarray(blob, dtypenp.float32) / 255.0 return blob[None, ...], scale, left, top def postprocess(self, output, scale, left, top, orig_shape): # YOLOv8 输出形状 [1, 4nc, 8400]转置后按行处理 preds np.squeeze(output).T boxes, scores, class_ids [], [], [] for pred in preds: cls_scores pred[4:] cid int(np.argmax(cls_scores)) conf float(cls_scores[cid]) if conf self.conf_thres: continue cx, cy, bw, bh pred[:4] x1 (cx - bw / 2 - left) / scale y1 (cy - bh / 2 - top) / scale x2 (cx bw / 2 - left) / scale y2 (cy bh / 2 - top) / scale boxes.append([x1, y1, x2, y2]) scores.append(conf) class_ids.append(cid) if not boxes: return [] idx cv2.dnn.NMSBoxes(boxes, scores, self.conf_thres, self.iou_thres) return [(boxes[i], scores[i], class_ids[i]) for i in idx] def detect(self, img): blob, scale, left, top self.preprocess(img) output self.session.run(None, {self.input_name: blob})[0] return self.postprocess(output, scale, left, top, img.shape)逻辑说明preprocess 用 letterbox 把图片缩放到 640×640 并居中填充填充值 114 是 YOLO 系列的惯例保持长宽比能避免轮胎圆形被压扁导致缺陷形变。postprocess 里 YOLOv8 的输出是[1, 4nc, 8400]要先转置成[8400, 4nc]再逐行解析前 4 个是 cx、cy、w、h后面是各类别分数。坐标还原时要把 letterbox 的偏移和缩放反算回去。NMSBoxes 做非极大值抑制conf_thres0.25、iou_thres0.45是通用起点轮胎缺陷重叠少iou 可以适当调低到 0.4。3.4 ONNX 量化到 INT8 的取舍热搜里「.onnx量化int8」问得多。INT8 量化能把模型体积压到约四分之一CPU 推理速度提升明显但精度会掉。轮胎缺陷里细裂纹这种小目标对量化最敏感我一般先跑 FP32 拿到基准 mAP再量化对比掉超过 2 个点就不划算。量化用 onnxruntime 的静态量化from onnxruntime.quantization import quantize_static, CalibrationDataReader, QuantType import numpy as np class TireCalibReader(CalibrationDataReader): def __init__(self, img_list, size640): self.data iter(img_list) self.size size def get_next(self): try: path next(self.data) except StopIteration: return None img cv2.imread(path) blob, _, _, _ TireDetector(dummy.onnx).preprocess(img, self.size) return {images: blob} quantize_static( model_inputbest.onnx, model_outputbest_int8.onnx, calibration_data_readerTireCalibReader(calib_imgs), quant_formatQuantType.QInt8, )校准集要覆盖各类缺陷和不同光照一般 100300 张就够。量化后必须重新跑一遍验证集对比 mAP别只看文件变小了就上线。4. 评估指标曲线怎么读别被单一 mAP 骗了4.1 混淆矩阵与 PR 曲线YOLOv8 验证时会生成 confusion_matrix.png 和 PR_curve.png。混淆矩阵看的是类别间误判轮胎检测里最常见的是 bulge 和 crack 互相混因为两者在低分辨率下纹理相似。如果混淆矩阵对角线不干净先查标注是否把两类标混了再考虑加数据。PR 曲线看的是不同置信度下的精确率和召回率权衡。曲线越靠右上越好曲线下的面积就是各类的 AP。如果某一类 AP 明显低说明这类样本太少或太难优先补这类数据。4.2 用验证脚本拿到每类指标from ultralytics import YOLO model YOLO(runs/tire/exp1/weights/best.pt) metrics model.val( data/data/tire_defect/data.yaml, imgsz640, batch16, conf0.001, # 验证时用低阈值让 PR 曲线完整 iou0.6, plotsTrue, ) print(mAP50:, metrics.box.map50) print(mAP50-95:, metrics.box.map) for i, name in enumerate(metrics.names.values()): print(name, AP50:, metrics.box.ap50[i])conf0.001是验证专用为了画出完整 PR 曲线实际部署时用 0.25 左右。iou0.6是 NMS 的 IoU 阈值验证阶段放宽一点能更真实反映模型能力。逐类打印 AP 是关键整体 mAP 高但某一类拉胯上线后那一类就是漏检重灾区。4.3 指标达标了但产线还是漏检问题出在哪这是最玄学的一环。验证集 mAP 0.9上线漏检一堆常见原因有三个一是验证集和产线数据分布不一致产线光照、相机角度变了二是验证集里缺陷都是清晰大目标产线有小目标三是预处理不一致训练用 letterbox推理用了直接 resize。排查方法是从产线抓一批真实图人工标一遍单独跑一次 val看指标掉多少。掉得多就是分布问题得补产线数据重训。5. GUI 界面与部署避坑从脚本到质检员能用的工具5.1 用 PyQt5 搭一个最小可用界面质检员不会用命令行GUI 是这套系统能不能落地的关键。用 PyQt5 搭一个左边显示图片右边显示检测结果和类别统计底部放「选择图片」「开始检测」「导出报告」三个按钮。import sys import cv2 from PyQt5.QtWidgets import ( QApplication, QMainWindow, QLabel, QPushButton, QVBoxLayout, QHBoxLayout, QWidget, QFileDialog ) from PyQt5.QtGui import QImage, QPixmap class TireGUI(QMainWindow): def __init__(self, detector): super().__init__() self.detector detector self.setWindowTitle(轮胎缺陷检测系统) self.img_label QLabel(请选择图片) self.result_label QLabel(等待检测) btn_open QPushButton(选择图片) btn_run QPushButton(开始检测) btn_open.clicked.connect(self.open_image) btn_run.clicked.connect(self.run_detect) right QVBoxLayout() right.addWidget(self.result_label) right.addWidget(btn_open) right.addWidget(btn_run) layout QHBoxLayout() layout.addWidget(self.img_label, 3) layout.addLayout(right, 1) container QWidget() container.setLayout(layout) self.setCentralWidget(container) self.current_path None def open_image(self): path, _ QFileDialog.getOpenFileName(self, 选择图片, , Images (*.jpg *.png)) if path: self.current_path path self.show_image(path) def show_image(self, path): img cv2.imread(path) rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) h, w, _ rgb.shape qimg QImage(rgb.data, w, h, 3 * w, QImage.Format_RGB888) self.img_label.setPixmap(QPixmap.fromImage(qimg).scaled(800, 600)) def run_detect(self): if not self.current_path: return img cv2.imread(self.current_path) results self.detector.detect(img) for box, score, cid in results: x1, y1, x2, y2 map(int, box) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, f{cid}:{score:.2f}, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite(result.jpg, img) self.show_image(result.jpg) self.result_label.setText(f检出 {len(results)} 个缺陷) if __name__ __main__: app QApplication(sys.argv) detector TireDetector(best.onnx) win TireGUI(detector) win.show() sys.exit(app.exec_())逻辑说明界面用 QHBoxLayout 左右分栏左边图片区、右边控制区。open_image 弹文件框选图run_detect 调 detector 推理并在图上画框。参数上图片显示做了 scaled 缩放避免大图撑爆窗口。这套界面是最小可用版产线上还要加批量检测、结果存数据库、报警联动但骨架就是这个。5.2 部署到 RK3588 这类边缘设备的注意点热搜里「yolov8 部署到rk3588」「rk3588部署yolov8」问得多。RK3588 有 NPU但 onnxruntime 用不上 NPU得转成 RKNN 格式。流程是 ONNX → RKNN Toolkit 转换 → 板端推理。转换时注意输入尺寸要和导出 ONNX 时一致量化要用板端真实数据做校准NPU 对某些算子支持不全转换报错就回退到 CPU 跑。别指望一次转成功我一般先在 PC 上用 rknn-toolkit2 的模拟器验证精度再上板。5.3 避坑与常见问题排查现象训练 loss 正常下降但 mAP 一直是 0。原因data.yaml 里 names 和标注的 class_id 对不上或者验证集路径写错导致验证集为空。解决打印验证集图片数量确认非零用校验脚本核对 class_id 范围。现象导出 ONNX 后推理结果和 PyTorch 不一致。原因simplify 改坏了图或者前后处理不一致训练 letterbox、推理直接 resize。解决先关掉 simplify 导出对比确认是简化的问题前后处理统一用 letterbox。现象GUI 里图片显示颜色发蓝。原因OpenCV 读的是 BGRQImage 要 RGB忘了 cvtColor。解决显示前统一cv2.cvtColor(img, cv2.COLOR_BGR2RGB)。现象INT8 量化后小目标全丢。原因校准集没覆盖小目标量化把细裂纹的特征压没了。解决校准集里加入足量小目标样本或者只对部分层量化保留检测头为 FP32。现象产线推理帧率只有个位数。原因用了 CPUExecutionProvider 且没开多线程或者输入尺寸设太大。解决设ort.SessionOptions()的 intra_op_num_threads输入尺寸降到 640 甚至 512必要时上 GPU 或 NPU。6. 把评估曲线变成调参依据一个我常用的迭代习惯模型训完不是终点评估曲线才是下一轮迭代的起点。我自己的习惯是每次训练完先把 results.csv 里的 mAP50 和逐类 AP 拉出来和上一版对比只关注两个数——掉得最多的那一类和涨得最少的那一类。掉得多的那类回去翻它的标注和样本量涨得少的那类说明当前数据已经喂饱了加数据收益不大得从增强或结构上想办法。具体做法是写一个对比脚本把两版 results.csv 对齐 epoch 画在同一张图上import pandas as pd import matplotlib.pyplot as plt def compare_runs(csv_a, csv_b, label_av1, label_bv2): a pd.read_csv(csv_a) b pd.read_csv(csv_b) a.columns [c.strip() for c in a.columns] b.columns [c.strip() for c in b.columns] plt.figure(figsize(10, 5)) plt.plot(a[epoch], a[metrics/mAP50(B)], labellabel_a) plt.plot(b[epoch], b[metrics/mAP50(B)], labellabel_b) plt.xlabel(epoch) plt.ylabel(mAP50) plt.legend() plt.savefig(compare_map50.png, dpi150) compare_runs(runs/tire/exp1/results.csv, runs/tire/exp2/results.csv)对比时有个细节两版训练的 epoch 数可能不同横轴对齐到较短的那个否则曲线尾部会误导判断。另外mAP50 涨了不代表能上线还要看 mAP50-95后者对小目标更敏感。轮胎细裂纹这类缺陷我一般要求 mAP50-95 不低于 0.45 才考虑上线。再补一个验证技巧拿产线真实图做一次「盲测」。从产线抓 200 张没参与训练的图人工标好跑 val看逐类 AP。这一步能暴露验证集和真实分布的差距比任何曲线都直接。我踩过的最大坑就是只看验证集 mAP 就上线结果产线光照一变召回率直接腰斩返工重标了两周数据。调参上如果逐类 AP 里某一类持续低优先动三个地方一是这类样本的复制粘贴增强mosaic 对小目标有效但轮胎缺陷用 mosaic 要小心拼接后缺陷可能被裁掉二是把 imgsz 提到 960 重训三是检查这类缺陷的标注框是不是普遍偏大或偏小。这三招按顺序试基本能定位问题。最后说个习惯每次训练完把 data.yaml、训练命令、results.csv、best.pt 和 ONNX 一起归档到一个带日期的目录里。轮胎检测项目迭代周期长三个月后你根本记不清哪版权重对应哪批数据没有后悔药。这套流程我跑了两年从 YOLOv5 换到 YOLOv8骨架没变过变的只是模型和参数。希望帮到你。本文还有配套的精品资源点击获取