煤矿传送带异物检测实战:YOLOv8模型训练与PyQt界面开发
简介一套面向煤矿传送带矸石与锚杆异物检测的YOLOv8完整方案包含三千余张已标注数据集、可直接推理的训练权重以及PyQt可视化界面脚本适合安全生产监测、算法实战练习或毕业设计参考。压缩包共两千个文件以XML标注文件为主配套环境配置与运行步骤教程、界面脚本等整体大小三百余兆。数据集已划分训练、验证、测试目录并附data.yaml定义两个目标类别可直接用于YOLOv5、YOLOv7、YOLOv8、YOLOv9等算法重新训练省去数据预处理环节。训练好的权重开箱即用可快速完成推理验证PyQt脚本让非专业用户也能通过图形界面直观查看检测结果。已有235人学习下载对希望快速上手工业异物检测的开发者有较强参考价值。1. 煤矿传送带异物检测为什么偏偏是 YOLOv8 加 PyQt做过矿山智能化项目的人都有体会传送带上的矸石和锚杆异物检测看着是个目标检测问题实际落地却比实验室里跑 COCO 数据集难受得多。井下的粉尘、水雾、光照不均加上传送带一直在运动锚杆这类长条形目标经常被拉成斜线甚至断成两截矸石和煤块在灰度上又高度相似。这个项目标题把 YOLOv8、3000 多张数据集、PyQt 可视化界面三件事放在一起其实就是一条完整的落地路径模型选型、数据准备、交互界面缺一环都上不了矿。我接触过不少做矿山传送带异物识别的团队早期用传统图像处理的被光照变化折腾得够呛后来转深度学习又卡在数据和界面交付上。YOLOv8 能在精度和帧率之间找到平衡点PyQt 负责把检测结果变成现场工人看得懂的界面3000 多张数据则是让模型在井下环境里真正站住脚的基础。这篇文章会按照数据准备、模型训练、界面开发、部署排错的顺序把这个项目从零到一拆开重点写参数怎么调、坑在哪里。2. 检测目标与数据准备3000 多张图怎么组织才够用2.1 矸石和锚杆的视觉特征决定了标注策略矸石和锚杆在图像里的表现完全不同这意味着标注策略不能一刀切。矸石是块状的和煤炭伴生灰度接近但纹理和边缘形态有差异矸石表面更粗糙边缘轮廓不规则煤块相对圆润锚杆则是细长的圆柱体在传送带上经常横躺、斜放有时还被煤粉覆盖一部分标注框要是画大了模型就会学到一堆背景信息。我在处理这类数据时对矸石采用稍紧的边界框紧贴目标边缘宁可框小一点也不要包进煤块对锚杆则允许目标框包含少量背景因为锚杆是长条形旋转角度多样严格的矩形框必然带来背景噪音与其追求框的紧致不如让标注一致性好。锚杆标注不一致比框大一点更伤模型同一个锚杆有人标水平框、有人标斜框模型会学混乱。标注格式建议统一成 YOLO 的 txt 格式类别 id 从 0 开始0 代表矸石1 代表锚杆。坐标值全部归一化到 0 到 1 之间。3000 多张图如果按训练集 8验证集 2 划分大约能拿到 2400 多张训练图对于两个类别来说数据量不算富余尤其在锚杆样本占比低的情况下需要在预处理阶段就做针对性调整。2.2 用脚本完成数据划分与类别统计拿到标注数据后第一件事不是急着训练而是做一次数据体检。检查有没有标注框越界、类别是否平衡、各图的目标数量分布是否合理。下面这个脚本可以完成基本的划分和统计工作import os import random import shutil from collections import Counter random.seed(42) img_dir raw_images # 原始图像目录 label_dir raw_labels # 标注txt目录 train_img_dir dataset/images/train val_img_dir dataset/images/val train_label_dir dataset/labels/train val_label_dir dataset/labels/val os.makedirs(train_img_dir, exist_okTrue) os.makedirs(val_img_dir, exist_okTrue) os.makedirs(train_label_dir, exist_okTrue) os.makedirs(val_label_dir, exist_okTrue) all_imgs [f for f in os.listdir(img_dir) if f.endswith((.jpg, .png, .bmp))] all_imgs.sort() random.shuffle(all_imgs) split_idx int(len(all_imgs) * 0.8) train_imgs all_imgs[:split_idx] val_imgs all_imgs[split_idx:] def copy_data(img_list, target_img_dir, target_label_dir): for img in img_list: label os.path.splitext(img)[0] .txt src_label os.path.join(label_dir, label) if not os.path.exists(src_label): print(f警告: {label} 标注文件缺失) continue shutil.copy(os.path.join(img_dir, img), os.path.join(target_img_dir, img)) shutil.copy(src_label, os.path.join(target_label_dir, label)) copy_data(train_imgs, train_img_dir, train_label_dir) copy_data(val_imgs, val_img_dir, val_label_dir) class_stats Counter() total_labels 0 for subset in [train, val]: label_path fdataset/labels/{subset} for name in os.listdir(label_path): with open(os.path.join(label_path, name)) as f: lines f.readlines() total_labels len(lines) for line in lines: cls int(line.split()[0]) class_stats[f{subset}_cls_{cls}] 1 print(f总标注目标数: {total_labels}) print(class_stats)这段代码有几个值得注意的地方。random.seed(42) 固定随机种子保证每次运行结果一致方便复现。划分前先做一次 shuffle避免图像序列在时间上相关造成的数据泄漏。标注文件缺失直接跳过对应的图而不是中断报错因为实际整理数据时经常发现漏标情况。我在真实项目里遇到过图片序列的相邻帧高度相似的问题如果巡检视频抽帧得到的图片连续帧可能几乎一样直接 random 划分会让验证集和训练集近亲繁殖评估指标虚高。解决办法是先按图像来源的视频段分组再从组维度划分但那样代码会复杂很多。标题里说 3000 多张数据集如果本来就是分散采集的现场照片这个问题就不严重。2.3 数据增强煤矿场景要重点加强的三种变换训练集只有 3000 多张的情况下数据增强不是可选项是必需品。我用 YOLOv8 训练这种场景时会在默认增强基础上重点加强三类亮度对比度扰动、随机旋转和透视变换、以及 Mosaic 增强。井下光照不稳定的情况很常见有的区域强光灯直射有的区域处于阴影中。把 HSV 的亮度扰动范围从默认的 0.015 加大到 0.05 左右能显著提升模型在光照变化下的鲁棒性。旋转角度从默认 0 提高到 15 度锚杆在传送带上本来就可能以任意角度出现模型需要见过足够多的角度才能稳定识别。Mosaic 增强把四张图拼成一张变相扩大了单次训练能看到的目标数量和上下文种类让模型在锚杆样本不足的情况下仍然能学到有效的特征。这里要提醒一句增强幅度不是越大越好。旋转角度超过 30 度后矸石的形状特征会被破坏模型可能学到扭曲的形态反而误判如果训练集里没有那么多倾斜场景过强的旋转增强反而偏离真实分布。建议做法是先用一个较小的旋转角度训练一轮观察验证集表现再逐步加大找一个准确率不再上升的临界值。3. 用 YOLOv8 完成模型训练配置、命令与参数调优3.1 环境安装与数据集配置YOLOv8 的训练依赖并不复杂安装 ultralytics 包就能覆盖大部分需求。对于煤矿现场没有 GPU 的情况CPU 也能训练但速度很慢3000 多张图用 CPU 训练 200 轮可能需要十几个小时条件允许的话建议使用显存 8G 以上的显卡训练效率和显存容量都够用。安装完成之后需要准备数据集配置文件。这个文件是训练的人口写错了后面全是白做# coal_anomaly.yaml path: ./dataset train: images/train val: images/val names: 0: gangue # 矸石 1: anchor # 锚杆这里有一个细节path 字段要写数据集根目录train 和 val 使用相对路径。YOLOv8 会基于 path 拼接实际的图像目录写成绝对路径的话一旦项目目录迁移整个配置文件就失效了相对路径挪到哪都能用。names 的顺序要和标注文件里的类别 id 严格对应0 必须是 gangue1 必须是 anchor否则类别就错乱了。3.2 开始训练一条命令与前六个必调参数数据集配置好后训练命令本身不长yolo detect train \ datacoal_anomaly.yaml \ modelyolov8s.yaml \ epochs200 \ batch16 \ imgsz640 \ lr00.005 \ workers4 \ device0modelyolov8s.yaml 和 modelyolov8s.pt 是两条不同的路径。yolov8s.yaml 是从零开始训练的结构文件不使用预训练权重适合数据集分布和 COCO 差异极大的场景比如井下图像这种特殊域yolov8s.pt 则是加载 COCO 预训练权重然后微调收敛更快、精度更好。我的习惯是先用 .pt 做一次快速验证确认数据没有大问题再考虑从头训练。真正需要反复调的是那六个参数。imgsz640 是这个项目的默认分辨率传送带画面一般比较宽640 在检测小目标矸石时已经够用升到 1024 会提高小目标召回率但显存占用直线上升处理速度也会下降需要根据自己的卡来权衡。lr00.005 相对于默认值 0.01 减半了因为数据量不大过大的学习率会导致训练震荡损失曲线上下跳动验证集指标不稳定。batch16 在 8G 显存下略微偏大如果显存不够会直接 OOM改成 8 更保险batch 太小的话 BN 层的统计量不稳定训练过程会忽高忽低。epochs200 对于 3000 多张数据是够的实际训练时模型很可能在 150 轮左右就收敛了后面几十轮提升很小。建议打开早停机制patience 设为 30也就是说如果 30 轮内验证集指标没有提升训练会自动停止节省时间。workers4 负责数据加载的进程数Windows 上如果遇到 DataLoader 报错可以先改成 0 排查问题。3.3 训练结果怎么看哪个指标才是这个项目的命门训练结束后YOLOv8 会在 runs/detect/train 目录下生成一系列结果文件。很多人只看 mAP但在传送带场景里我建议把重点放在两个指标的对比上召回率和精确率。矸石漏检的后果是矸石进入下游工序影响煤质锚杆漏检的后果是异物进入破碎机可能造成设备损坏。锚杆的漏检代价远高于矸石所以调参时优先保证锚杆的召回率宁可让模型把一些黑色背景误判为锚杆也不能漏掉真正的锚杆。后面可以通过提高置信度阈值来过滤误检但漏检一旦发生改阈值是找不回来的。训练脚本里加一个参数就很关键yolo detect train \ datacoal_anomaly.yaml \ modelyolov8s.pt \ epochs200 \ batch16 \ imgsz640 \ lr00.005 \ seed42 \ saveTrueseed42 固定随机种子结果可复现。saveTrue 让训练结束保存权重。训练完成后取 runs/detect/train/weights/best.pt这是验证集表现最好的权重而不是用 last.pt 进行推理。last.pt 是最后一轮的状态往往已经过拟合或处于震荡区。训练过程中如果发现验证集的 loss 在下降但 mAP 一直不涨先去检查验证集图像里是否有标注错误再检查类别是否严重不平衡。我遇到过锚杆只有 300 个标注框、矸石有 5000 个框的情况模型到后期几乎只学矸石锚杆的召回率只有 0.2。解决方法是先把模型调到尽量拟合锚杆再松开参数去平衡两类目标。4. 把模型跑起来推理脚本与性能优化4.1 加载模型完成单帧推理训练完毕之后模型要能稳定地加载和推理。下面是一个最小推理脚本from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) results model.predict( sourcetest.jpg, conf0.35, iou0.45, imgsz640, devicecpu, saveTrue ) print(results[0].boxes.xyxy) print(results[0].boxes.cls) print(results[0].boxes.conf)conf0.35 是置信度阈值低于这个值的预测框全部丢弃。这个值不是随便设的我用一组没参与训练的现场图片跑一遍观察不同阈值下误检和漏检的变化曲线取误检开始明显上升之前的点。iou0.45 是 NMS 的 IoU 阈值两个框重叠超过 45% 就认为指向同一个目标留分数高的。这个值调低了会导致同一个目标出多个框调高了又把相邻目标合并成一个框。devicecpu 在矿井现场工控机上很常见模型推理速度会慢一些但 YOLOv8s 在 CPU 上处理 640 分辨率大约需要 100 到 200 毫秒传送带速度如果不太快这个速度勉强能跟上。要是现场有 NVIDIA 显卡devicecuda 直接跑帧率提升明显。4.2 视频流接入循环读帧和线程模型实际传送带场景是视频流不是单张图片。PyQt 界面里接入视频流的核心框架可以这样组织import cv2 from PyQt5.QtCore import QThread, pyqtSignal from PyQt5.QtGui import QImage, QPixmap class VideoThread(QThread): change_pixmap_signal pyqtSignal(QImage) def __init__(self, model, stream_url, conf0.35, iou0.45): super().__init__() self.model model self.stream_url stream_url self.conf conf self.iou iou self.running True def run(self): cap cv2.VideoCapture(self.stream_url) while self.running: ret, frame cap.read() if not ret: continue results self.model.predict( sourceframe, confself.conf, iouself.iou, imgsz640, verboseFalse ) annotated results[0].plot() rgb_image cv2.cvtColor(annotated, cv2.COLOR_BGR2RGB) h, w, ch rgb_image.shape bytes_per_line ch * w qt_img QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888) self.change_pixmap_signal.emit(qt_img) def stop(self): self.running False这个线程类的设计有几个关键点。预测过程放在 QThread 的 run 方法里避免阻塞 UI 线程否则界面会卡死拖动窗口都无响应。每次循环调用 predict 方法注意 verboseFalse不然控制台会被检测日志刷满。results[0].plot() 把检测框直接画在帧上省去手工绘制矩形框的代码。界面主窗口里创建线程实例绑定额定帧率self.video_thread VideoThread(model, stream_url) self.video_thread.change_pixmap_signal.connect(self.update_image) self.video_thread.start() def update_image(self, qt_img): self.video_label.setPixmap(QPixmap.fromImage(qt_img))这里要注意的就是线程生命周期管理。关闭窗口时要先调用 stop 方法让循环退出再调用 wait 等待线程结束否则程序退出时会崩溃。在实际项目中界面卡死 90% 是因为把推理写在了主线程里剩下的 10% 是视频读取和推理在同一线程里互相拖累画面一卡一卡的。4.3 检测结果的告警逻辑检测到异物之后需要在界面上给出提醒。我一般会设一个目标数阈值连续检测到锚杆超过 3 帧才触发告警避免个别帧的误检造成误报。这个逻辑单独写在信号处理函数里做法很直接self.anchor_frame_count 0 def update_image(self, qt_img): self.video_label.setPixmap(QPixmap.fromImage(qt_img)) results self.latest_results anchor_count sum(1 for cls in results[0].boxes.cls if int(cls) 1) if anchor_count 0: self.anchor_frame_count 1 else: self.anchor_frame_count 0 if self.anchor_frame_count 3: self.alert_label.setText(检测到锚杆请关注传送带)连续帧确认机制在运动场景下非常实用因为单帧的误检很难完全消除但误检往往是偶发的连续多帧都出现误检的概率低得多。这个逻辑还可以加一个延时复位锚杆离开画面后 2 秒取消告警不然警报一直响反而让工人麻痹了。5. 避坑指南训练和部署中最容易翻车的 5 个问题5.1 标注框坐标越界导致训练 loss 异常现象训练到一半 loss 突然变成 nan或者训练能进行但验证集的检测框全部偏移。原因标注软件导出的坐标里存在超出图像边界的情况比如框的右下角坐标超过图像宽高。YOLOv8 加载数据时做了限制但越界标注会让 anchor 匹配计算出现异常值梯度更新时炸掉。解决在训练脚本里加一段检查读每一个标注框的坐标凡是超出图像尺寸的按图像边界截断面积小于图像面积 0.1% 的框直接过滤掉因为那种目标太小了640 分辨率下根本学不到什么。5.2 锚杆类别召回率远低于矸石现象验证集上矸石的 mAP 到了 0.85锚杆只有 0.4模型把锚杆漏掉或者只检出一半。原因数据不平衡。矸石经常出现在画面里一张图上可能有七八个锚杆偶尔出现总标注框数可能只有矸石的十分之一。训练时模型对锚杆的特征学习不充分。解决优先做法是去现场多采集锚杆样本短期内采集不到的可以对锚杆样本做过采样把含锚杆的图像在训练时重复使用几次。也可以在损失函数层面提高锚杆类别权重YOLOv8 里可以通过 cls 参数调分类损失的权重。还有个比较实用的方法是把 anchor 的标注框在预处理阶段做小角度旋转增强让模型看到更多姿态的锚杆变相增加有效样本。5.3 PyQt 界面显示画面卡顿现象界面运行时画面一卡一顿帧率只有个位数拖动窗口也不流畅。原因推理和界面刷新在同一个线程里互相阻塞。模型推理一个 640 分辨率图像需要 100 毫秒以上这段时间界面无法处理事件表现为卡顿。解决把推理放到 QThread界面主线程只负责接收信号更新图像。如果还卡就把推理结果缓存起来用一个定时器每 100 毫秒刷新一次画面不要每帧都重新推理和重绘现场的流畅度比帧率更重要。另外注意推理结果的 plot 操作本身也有开销可以只保存带框图像的数据指针减少拷贝。5.4 部署到井下工控机后检测速度下降明显现象训练机上检测速度很正常换到现场工控机后变成 800 毫秒一帧完全跟不上传送带速度。原因现场工控机 CPU 性能偏低没有 GPU模型推理本身就需要几百毫秒加上图像采集和界面绘制总耗时就被放大了。解决先压缩输入分辨率从 640 降到 416检测精度会损失一些但速度能提上去再用 openvino 或 onnxruntime 把模型转成加速推理格式Int8 量化能再快一倍以上。精度和速度之间取平衡传送带速度不快的情况下 416 分辨率够用。还有一个容易被忽略的点QImage 的字节复制要避免不必要的深拷贝直接引用帧数据能省下不少时间。5.5 锚杆被煤粉覆盖导致漏检现象锚杆表面覆盖了一层煤粉视觉上和黑色的传送带背景融为一体模型完全没有检出。原因井下环境里锚杆往往不是干净的金属色煤粉覆盖让边缘特征极度弱化训练集里如果锚杆样本都是干净状态模型就学不到这种特征。解决采集数据时专门收集带煤粉的锚杆样本如果没有可以在训练增强阶段给锚杆区域叠加随机灰度噪声模拟煤粉覆盖的效果。推理时观察漏检帧的灰度分布如果漏检位置和背景灰度差很小就需要从数据层面解决单纯调阈值是找不回来的。6. 终极验证技巧用现场视频片段衡量模型能否上线模型训练完、界面开发好之后最后一步是验证。我习惯保留一段现场视频片段大约 3 到 5 分钟这段视频不参与任何训练和验证环节专门用来做最终评估。读取视频逐帧推理记录每一帧的检测结果然后手工核对哪些目标被检出、哪些被遗漏、哪些是误报统计出真实的召回率和误报率。有了这个指标你才真正知道模型能不能上线。训练集的 mAP 再高也不能替代真实场景的验证。我一般把目标定为锚杆检出率不低于 90%误报率控制在每 100 帧不超过 2 次矸石检出率不低于 85%达到这个标准再谈部署。优化流程是这样的先用上一步的视频片段做参数敏感性测试把 conf 阈值从 0.3 到 0.6 按步长 0.05 扫一遍画出漏检率和误报率的曲线挑一个两个指标都可接受的阈值。然后看漏检样本主要集中在哪种类别、哪个位置如果是传送带边缘的小目标矸石时常漏掉可以考虑把 imgsz 提升到 768 或者叠加一个滑窗推理方案在保持速度的前提下提高边缘区域的检测密度。最后一个小习惯把所有推理结果保存下来包括时间戳、目标类别、置信度和边界框坐标存成 CSV 文件。这些历史数据是后续优化的宝贵素材可以分析误报的时间分布、位置分布发现规律后针对性地补数据、调阈值、加规则。做过几个项目之后你会发现模型上线只是一个开始真正磨人的是持续调优靠现场数据在真实环境里不断迭代这个方案。希望我的这些经验能帮到你少走一些我已经踩过的弯路。本文还有配套的精品资源点击获取