简介基于YOLOv5的煤矿传送带异物检测系统面向矿业安全生产与工业视觉检测开发者用于对传送带上的锚杆、石块等异物进行实时识别与定位。包内共80个文件压缩包约9.22MB包含完整Python源码、可直接运行的yolov5s.onnx模型、评估指标曲线图以及基于PyQt5开发的精美GUI界面其中大量jpg测试图片和xml标注可用于效果验证txt配置与模型说明方便快速了解环境依赖整体目录结构清晰便于部署和二次开发。目前已有705人学习适合希望掌握YOLOv5与ONNX Runtime集成、GUI封装流程的Python开发者作为参考项目。用户可通过界面加载图像或视频实时查看检测结果并对检测过程进行适当控制从而更直观地理解目标检测系统的完整落地流程。1. 煤矿传送带异物检测为什么绕不开YOLOv5从井下监控画面上的一次漏检说起井下的传送带跑得飞快煤流里混着锚杆、托盘、金属网片、大块矸石这些东西一旦卡进破碎机或者溜煤眼一次检修就是几个小时的停产严重的还会伤到设备。传统机器视觉用阈值分割和模板匹配做异物检测煤块和矸石在灰度上几乎分不开误报率高到值班工人直接把报警器静音。YOLOv5之所以成为这类项目的默认选择是因为它在实时目标检测里的工程成熟度最高训练代码、部署工具链、社区资料都齐全一张工业显卡或者 Jetson 板卡上跑到三五十帧不费劲而且从 PyTorch 训练到转 ONNX 再落地的路径极其顺滑。这篇笔记就以“基于YOLOv5的煤矿传送带异物检测系统”为主线把数据准备、训练、ONNX 导出、指标评估到 GUI 界面串成一条能照着复现的完整链路。2. 把YOLOv5训练跑通数据、环境与超参数的三方配合2.1 环境配置的版本矩阵torch、CUDA与onnxruntime的兼容关系YOLOv5 对 PyTorch 版本的要求不算苛刻常见的 Python 3.8/3.9、PyTorch 1.10 到 2.0 之间都能跑通。最容易翻车的其实是 CUDA 和 cuDNN 的对应关系驱动版本太老新装的 torch 会直接报 “no kernel image is available”驱动太新而 torch 编译版本旧又会提示 CUDA 初始化失败。我踩过几次之后养成了一个习惯先用nvidia-smi看驱动支持的最高 CUDA 版本再反推该装哪个版本的 PyTorch绝不在环境问题上硬刚。# 用conda创建干净环境避免把系统python搞乱 conda create -n yolo5 python3.9 -y conda activate yolo5 # 安装PyTorch注意cu118表示CUDA 11.8 pip install torch2.0.1 torchvision0.15.2 --index-url https://download.pytorch.org/whl/cu118这段命令里--index-url指定了 PyTorch 官方预编译轮子的源cu118 表示针对 CUDA 11.8 编译的版本。CUDA 版本不一定非要等于驱动显示的最高版本只要小于等于驱动支持的最高 CUDA 版本就能用。我选 CUDA 11.8 主要是考虑到 ONNX Runtime 和 TensorRT 在这个版本上的兼容性最稳定后面转 ONNX、上 TensorRT 都不用担心差半版导致算子注册失败。装完之后用两行代码验证 GPU 能否真正参与计算import torch print(torch.cuda.is_available()) # 必须输出 True print(torch.cuda.get_device_name(0)) # 输出显卡型号确认torch看到了卡如果is_available()返回 False大概率是驱动版本太旧或者 pip 装成了 CPU 版 torch。我总结的排查顺序是先重装 NVIDIA 驱动到较新版本再重跑一遍 pip 安装命令九成问题都能解决。这里没必要追最新版本稳定才是井下现场最看重的你在实验室用 RTX 4090 跑通的环境到了工控机上完全可能因为驱动版本对不上而重新折腾半天。2.2 煤矸石与锚杆数据集的标注规格txt标签与目录结构YOLOv5 要求的数据集格式和 VOC、COCO 都不一样它要的是每张图片对应一个同名 txt 文件里面每行一个目标五个数字分别是类别 id、归一化中心 x、归一化中心 y、归一化宽、归一化高。这种格式看起来简单但坑也藏在简单里。# labels/0001.txt 文件内容示例 1 0.5234 0.4211 0.3121 0.1534 0 0.2187 0.7634 0.0876 0.3412上面第一行表示类别 1比如锚杆归一化中心坐标是 (0.5234, 0.4211)宽高是 (0.3121, 0.1534)。所有数字必须在 0 到 1 之间因为 YOLOv5 在训练时会把坐标乘以输入尺寸映射回原图。如果某个标注框的坐标不小心写成了像素值而不是归一化值训练时 loss 会直接飘起来而且不容易发现。目录结构必须严格按 YOLOv5 约定的方式组织这是训练能否正常读数据的前提dataset/ ├── images/ │ ├── train/ # 训练集图片jpg/png均可 │ └── val/ # 验证集图片 ├── labels/ │ ├── train/ # 与images/train同名的txt标注 │ └── val/ ├── data.yaml # 数据集配置文件 └── 数据说明.txtdata.yaml 是训练入口的配置文件内容如下# data.yaml - 数据集配置 path: dataset # 数据集根目录 train: images/train # 训练集图片目录相对于path val: images/val # 验证集图片目录 nc: 3 # 类别数量 names: [coal_gangue, anchor_rod, metal_trash] # 类别名称顺序必须与标注id一致这里三个类别对应井下的三类典型异物煤矸石、锚杆、金属杂物。nc和names的顺序必须和标注 txt 里的类别 id 一一对应否则训练出来的模型会把“锚杆”认成“金属杂物”评估曲线再好看也没用。我在一次项目里就吃过这个亏标注时把煤矸石设成 0后面整理数据集时忘了改 names 顺序训练了两天才发现类别全错了白白浪费一宿电费。2.3 训练参数决定模型下限batch size、学习率与anchor背后的逻辑YOLOv5 的训练入口是train.py我的习惯是把参数固化到一个 shell 脚本里而不是每次交互式改参数这样能保证复现性也方便存档对比。python train.py \ --data data.yaml \ --weights yolov5s.pt \ --batch-size 16 \ --epochs 100 \ --imgsz 640 \ --device 0 \ --patience 20 \ --project runs/train \ --name gangue_detector这批参数里--batch-size 16是训练速度和显存占用之间的平衡点在 12GB 显存上跑 yolov5s 模型很稳。--patience 20表示连续 20 轮验证集指标不提升就提前停止训练避免后期过拟合浪费算力。--imgsz 640是送入网络的输入尺寸640 是 YOLOv5 的默认尺寸太小会损失小目标的检测能力太大训练时间成倍增加。--weights yolov5s.pt用的是官方预训练权重YOLOv5 的默认超参数学习率 0.01、余弦退火策略就是基于 COCO 数据调试出来的在新数据集上做迁移学习时这些默认值通常是个不错的起点。但有一点要注意自建数据集的类别和 COCO 差异越大初始学习率越应该调低。比如只检测锚杆和煤矸石这类和 COCO 里“椅子”“瓶子”完全不同的物体我一般会把学习率从 0.01 降到 0.005让模型在新数据上平稳适配别一开始就把预训练权重冲乱。训练过程中最值得盯的图表是runs/train/gangue_detector/results.png它把 loss、精度、召回率、mAP 六条曲线画在一张图里。如果 loss 曲线在几百轮后依然震荡不收敛先别怀疑网络结构大概率是学习率或者 batch size 出了问题。attention: batch size 翻倍时学习率也要跟着翻倍这是 YOLOv5 超参数调节里最朴素也最实用的规律。3. 从PyTorch到ONNX导出、推理与量化落地的完整链路3.1 用torch.onnx.export导出静态ONNX完整脚本与opset参数说明训练完成后runs/train/gangue_detector/weights/目录下会有best.pt和last.pt两个权重。要在工控机上脱离 PyTorch 环境部署第一步就是转成 ONNX。# export_onnx.py - 将best.pt导出为静态ONNX模型 import torch import yaml # 读取训练时的类别数量保证模型输出维度一致 with open(data.yaml, r) as f: data_cfg yaml.safe_load(f) nc data_cfg[nc] # 从本地权重加载YOLOv5模型nc覆盖原模型输出维度 model torch.hub.load(ultralytics/yolov5, custom, pathruns/train/gangue_detector/weights/best.pt, force_reloadFalse) model.eval() # 构造与训练输入维度一致的虚拟输入 dummy_input torch.randn(1, 3, 640, 640) # 导出ONNXopset_version是部署兼容性的关键 torch.onnx.export( model, dummy_input, gangue_detector.onnx, input_names[images], output_names[output], opset_version11, # 兼容性最好老版本ONNX Runtime也能跑 dynamic_axesNone # 静态形状固定batch size和分辨率 ) print(ONNX导出完成)这段代码里最需要注意的是opset_version。YOLOv5 官方推荐的 opset 是 11 或 12因为 ONNX Runtime 对这两个版本的支持最成熟。如果设成 13 甚至更高部分算子比如 SiLU 激活函数在老旧版本 ONNX Runtime 上会报 “Op not implemented” 错误。dynamic_axesNone意味着模型输入输出是静态形状固定 batch 1、分辨率 640×640这在部署阶段是最可靠的做法。动态轴虽然灵活但会拖慢推理速度而且转 TensorRT 时极不友好动不动就报维度不匹配。3.2 onnxruntime推理链路预处理、输出解析与NMS解码导出 ONNX 只是第一步部署时真正要写的是推理代码。onnxruntime 是微软的推理引擎CPU 和 GPU 都支持和 PyTorch 的关系要理清楚PyTorch 是训练框架onnxruntime 是推理引擎模型跑在 onnxruntime 上不需要安装 PyTorch这对工业现场的软件依赖是个很大的简化。# infer_onnx.py - 用onnxruntime加载ONNX模型并做单帧推理 import cv2 import numpy as np import onnxruntime as ort # 创建推理会话providers指定执行后端 sess ort.InferenceSession(gangue_detector.onnx, providers[CPUExecutionProvider]) input_name sess.get_inputs()[0].name output_name sess.get_outputs()[0].name def letterbox(img, new_size(640, 640), fill_color114): 保持宽高比的缩放不足部分用灰色填充与训练时预处理一致 h, w img.shape[:2] scale min(new_size[0] / h, new_size[1] / w) new_h, new_w int(h * scale), int(w * scale) resized cv2.resize(img, (new_w, new_h)) canvas np.full((new_size[0], new_size[1], 3), fill_color, dtypenp.uint8) canvas[:new_h, :new_w] resized return canvas, scale img cv2.imread(test_frame.jpg) img_letterbox, scale letterbox(img) # YOLOv5训练时用的是BGR通道顺序这里不要转RGB input_tensor img_letterbox.astype(np.float32) / 255.0 input_tensor np.transpose(input_tensor, (2, 0, 1))[None, :, :, :] # 推理 outputs sess.run([output_name], {input_name: input_tensor}) # 输出shape是(1, 25200, 85)25200 3个特征图尺度的anchor总数 predictions outputs[0][0] # 解码过滤低置信度目标再做NMS conf_thres, iou_thres 0.25, 0.45 boxes, scores, class_ids [], [], [] for pred in predictions: class_scores pred[5:] # 前4位是xywh第5位是objectness后面是类别分数 score float(pred[4]) * float(class_scores.max()) if score conf_thres: continue class_id int(np.argmax(class_scores)) x, y, w, h pred[0:4] # 还原到原始图片坐标 boxes.append([(x - w / 2) / scale, (y - h / 2) / scale, (x w / 2) / scale, (y h / 2) / scale]) scores.append(score) class_ids.append(class_id) # 用OpenCV自带的NMS实现极大简化代码 indices cv2.dnn.NMSBoxes(boxes, scores, conf_thres, iou_thres) for i in indices: x1, y1, x2, y2 [int(v) for v in boxes[i]] cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.imwrite(result_with_boxes.jpg, img)这段代码是整个部署链路的核心三个细节最值得讲。第一是 letterbox 缩放YOLOv5 训练时用的是保持宽高比加灰边填充的方式直接 resize 到 640×640 会把图像压变形部署时预处理必须和训练保持一致否则检测框的位置和置信度都会偏掉。第二是置信度得分计算源码里用的是 objectness 乘以最大类别分的组合方式比单独看类别分数要稳因为 objectness 表达的是“这个位置有没有目标”类别分表达的是“是什么目标”两者相乘才能抑制背景误检。第三是坐标还原NMS 得到的预测框坐标是 letterbox 坐标系下的除以 scale 映射回原图才能画对位置。3.3 ONNX int8量化尝试井下工控机的性能账井下工控机通常没有独立显卡最常见的是 Intel 赛扬或者酷睿的集成显卡。这种机器跑 FP32 的 ONNX 模型640×640 输入大概 10 到 20 帧每秒勉强够看但余量不足。如果要接两个摄像头的画面就需要考虑 int8 量化。ONNX 量化的常见做法是走 onnxruntime 的量化工具# quantize.py - 将FP32 ONNX模型转为int8动态量化版本 from onnxruntime.quantization import quantize_dynamic, QuantType quantize_dynamic( gangue_detector.onnx, gangue_detector_int8.onnx, weight_typeQuantType.QInt8 ) print(int8量化完成模型体积缩小约4倍)这段代码用的是动态量化不需要准备校准数据集直接对权重做 int8 压缩。好处是省事坏处是精度损失比静态量化多一些在细长目标比如铁丝、锚杆端头上可能出现漏检。如果需要保留更高精度就得用quantize_static静态量化先准备两百张带标注的验证图片做校准让量化器统计每层激活值的动态范围再决定缩放比例。我写过一段笔记量化这个事是个典型的收益递减选择如果 CPU 推理已经跑到 20 帧以上没必要上 int8如果帧率在 10 帧附近徘徊优先考虑换更轻的模型比如 yolov5s 换 yolov5n这比量化省事得多。量化是最后的降级手段因为一旦量化后精度掉得厉害排查是哪个算子导致的损失非常痛苦。我在一个项目里量化后锚杆漏检率从 2% 涨到 8%查了两天也没定位到具体层最后回到 FP32 才安心交付。4. 评估指标曲线mAP与PR曲线的计算和判读4.1 用val.py跑出指标文件COCO与自定义数据集的参数差异训练出模型后不能只盯着 loss 曲线看目标检测的行业通用评估体系是 mAP。YOLOv5 自带的val.py会计算 precision、recall、mAP.5 和 mAP.5:.95 四个核心指标并自动生成 PR 曲线和 F1 曲线图。python val.py \ --data data.yaml \ --weights runs/train/gangue_detector/weights/best.pt \ --batch-size 16 \ --imgsz 640 \ --project runs/val \ --name gangue_eval跑完后在runs/val/gangue_eval/目录下会生成一批文件核心的是PR_curve.png、F1_curve.png和results.csv。其中 results.csv 是数值源头PR_curve.png 是从它画出来的。评估输出文件清单文件名内容部署时的参考价值PR_curve.png所有类别的精确率-召回率曲线判断每个类别的漏检和误检倾向F1_curve.png不同置信度阈值下的F1值选择部署时的conf_thres参数labels.jpg标注框可视化预览检查标注是否有异常框results.csv每类的precision、recall、mAP数值与验收指标做精确对比这里需要说明 mAP.5 和 mAP.5:.95 的区别。mAP.5 是 IoU 阈值取 0.5 时的平均精度是工业质检项目验收时最常看的指标mAP.5:.95 是 IoU 从 0.5 到 0.95 以 0.05 步进共 10 个阈值下精度的平均值学术论文里更常用对定位精度更敏感。做煤矿异物检测mAP.5 是硬指标因为现场只需要框出异物位置让工人看到不需要像素级精确分割。4.2 PR曲线存档与逐类分析从指标反推数据短板val.py 已经自动把 PR 曲线画好了但如果你想统计现场真实录像的指标或者按类别做细粒度分析就需要自己写评估脚本。我的做法是复用 val.py 生成的 results.csv做逐类核对# check_metrics.py - 读取results.csv检查每个类别的表现 import pandas as pd df pd.read_csv(runs/val/gangue_eval/results.csv) # YOLOv5的results.csv第一行是表头第二行才是真实指标 metrics df.iloc[1] class_names [coal_gangue, anchor_rod, metal_trash] for cls_name in class_names: ap_col fmetrics/mAP_0.5({cls_name} if ap_col in df.columns: ap50 df[ap_col].iloc[1] print(f{cls_name}: mAP.5 {ap50:.3f}) else: print(f警告results.csv里找不到{cls_name}的mAP列检查类别名是否匹配)这个小脚本做的事情很朴素价值在于暴露“整体指标不错但个别类别拖后腿”的情况。很多时候整体 mAP 达到 90%但锚杆这个类别的 AP 只有 60%原因通常是锚杆数量少、形态细长、在图像里占比小。这种问题从全局指标上看不出来从类别明细里一眼就能发现。判断模型是否可用我参考的经验线三类异物 mAP.5 分别达到 90% 以上PR 曲线右上角区域饱满。如果某一类的 PR 曲线明显向右下角塌陷说明该类别漏检率高如果曲线向左平移说明误报多。这两种情况的对策完全不同漏检多就去补该类别的高质量标注数据误报多则优先调高置信度阈值或者检查类别定义是否模糊。4.3 从曲线反推部署问题漏检与误报的取舍方案有经验的工程师拿到 PR 曲线能直接判断模型部署到现场该调哪些参数。核心矛盾是置信度阈值 conf_thres 的选择调低了误报多工人嫌烦调高了漏检多安全没保障。看 F1_curve.png 找最优置信度是个高效办法。F1 曲线横轴是置信度阈值纵轴是 F1 值曲线最高点对应的位置就是理论上最优的置信度阈值设置点。如果最高点对应的 F1 只有 0.7 左右说明精确率和召回率之间存在明显矛盾别急着调阈值先回头检查数据问题。常见的情况是拍摄角度、光照条件和训练集差异过大。井下摄像头通常固定在距传送带两三米的位置俯拍如果训练数据是用手机在实验室拍的或者从网上下载的公共图片模型的泛化能力必然不够。这时候唯一有效的路径是去现场实地采集两三个班次的真实画面覆盖不同光照、不同煤流密度、不同皮带速度重新标注迭代。模型再先进数据分布不对也是白搭。5. 现场排查与避坑四类高频异常的定位和处置5.1 训练到一半loss变成NaN先查标注坐标再看学习率现象训练到第 10 轮左右loss 突然变成 NaN之后的训练结果全部作废浪费十几个小时。原因最常见的是标注数据里有异常值。某个 txt 文件里的坐标写成了像素值而不是归一化值或者宽高是负数训练时梯度过大直接爆炸。其次是手动改了--lr0学习率导致优化过程不稳定。解决先写一段脚本遍历所有标注文件检查坐标是否在 0 到 1 之间、宽高是否为正数把异常文件找出来修正。再恢复默认学习率重跑。注意图片损坏也会导致 NaNcv2.imread 读不出的图片返回 None数据加载器有时会跳过但偶尔会把异常的 tensor 送进计算图。这个坑排查起来最费时间我后来养成了训练前先跑数据检查脚本的习惯。5.2 ONNX推理结果与PyTorch不一致通道顺序、letterbox与输出解析现象同一张测试图片PyTorch 模型检测框位置正常转成 ONNX 后用 onnxruntime 推理框的位置偏了置信度也掉了。原因这是部署阶段最常见的“玄学”问题本质是预处理和解码环节没对齐。第一是 BGR 与 RGB 通道顺序YOLOv5 源码加载图片用的是 cv2.imread得到的是 BGR而你推理代码里如果画蛇添足转了 RGB颜色通道就反了检测框的位置和类别都会乱。第二是 letterbox 逻辑不一致训练和推理都用了 letterbox但 padding 位置计算方式不同坐标整体偏移。第三是输出解析YOLOv5 的 ONNX 输出格式在不同版本之间有调整6.0 之后的版本输出是 (1, 25200, 85) 的展平结构解析代码必须和导出时的输出定义对齐。# 用onnx官方工具检查模型输入输出定义确认名称和维度 python -c import onnx; monnx.load(gangue_detector.onnx); print([i.name for i in m.graph.input]); print([o.name for o in m.graph.output])如果输入输出名和推理代码里的字符串对不上会话创建时会直接报错这是最好排查的错误。真正难的是名字对上了但结果不对这时候就要逐段比对预处理代码和训练时的数据加载逻辑。5.3 GUI实时检测界面卡顿掉帧线程模型才是真凶现象GUI 界面一打开还能看运行几秒后画面开始掉帧拖动窗口时直接假死点击按钮半天没反应。原因几乎都是因为在 GUI 主线程里直接做了模型推理。PyQt5 的界面刷新、按钮响应和 onnxruntime 推理都在主线程执行推理一次耗时 0.5 秒界面就卡 0.5 秒用户体验极差。解决把推理放进独立线程。用 QThread 专门从视频队列取帧推理后通过信号把结果发回主线程主线程只负责刷新画面和更新状态栏。线程之间传递图像用队列不要共用同一个 numpy 数组因为推理线程可能正在修改数据主线程同时读取绘制的画面会是花的。这个线程模型的问题在实验室测试时很难暴露因为测试视频和处理逻辑简单到了现场接了真实工业相机才现出原形。5.4 井下低照度图像检测效果断崖gamma校正与数据回补现象训练时用工业相机白天拍的画面mAP 能到 95%部署到井下夜间开启红外补光后检测效果暴跌到 60% 左右。原因井下环境的亮度分布和训练集差异巨大。红外补光下物体边缘的高光、煤尘造成的散斑、传送带本身的强反射都会让网络输出分布发生漂移。这是典型的训练集与部署环境分布不一致问题。解决两个手段配合使用。一是在部署推理前做图像增强gamma 校正和 CLAHE 均衡是两种不改变语义的预处理先把现场图像统一到训练集的亮度区间再送进网络。我在现场常用 gamma 值 0.7 到 1.2 之间做快速试错有效果后再去现场补拍数据做正式训练。二是回到数据源头采集部署点位真实光照条件下的图像和原有训练集混合后再训一版。数据回补永远比算法技巧可靠这一点在井下环境尤其成立。6. GUI判读界面让值班工人看得懂比画得漂亮更重要GUI 界面的核心用户是在调度室值班的工人他们不是算法工程师不需要看到置信度数字一秒跳多少次。他们需要的就四样东西传送带实时视频、异物框标的颜色区分、声光报警的触发状态、以及最近一帧异物截图存档。我通常选 PyQt5 做 GUI 框架它在工业软件里的占有率最高坐席电脑上装个 Python 环境就能跑。推理放在 QThread 线程里视频流来自工业相机 SDK界面主线程只做三件事渲染图像、更新报警灯状态、写报警日志。线程逻辑是相机线程不断把最新一帧放入队列推理线程取帧做 ONNX 推理把带框的帧和检测结果通过信号传给主线程刷新画面。必须用信号槽机制不能用线程间共享变量否则 PyQt 的线程安全问题会让你频繁踩坑。画框时按类别区分颜色我习惯用三套固定搭配煤矸石黄色、锚杆红色、金属杂物橙色。报警阈值单独设置比如锚杆置信度 0.7 以上就触发声音报警煤矸石只显示不报警。因为煤矸石混入传送带是常态如果也做声音报警报警器天天响工人很快就会把报警当成耳旁风真正出事时反而没人反应。界面做完之后别急着交付先做压力测试同时打开两路视频、日志滚动、报警闪烁同时发生时界面能不能保持 30 帧刷新。我在一次现场交付时吃过亏一路视频正常两路视频画面开始撕裂后来把所有图像深拷贝改成浅拷贝加锁才把帧率稳住。GUI 这种工作看着简单真正考验的是对现场使用习惯的理解而不是控件排得有多好看。回过头看这个系统整体做下来数据收集标注大约占四成时间训练调参占两成ONNX 部署优化占两成GUI 与现场联调占两成。如果你准备照着做建议从一个小数据集开始跑通全链路从训练到 ONNX 到 GUI 完整走一遍再逐步扩大数据量。全链路每多跑通一次你踩过的坑就少一次。希望帮到你。本文还有配套的精品资源点击获取