基于YOLOv8与PyQt的煤矿传送带矸石锚杆异物检测实战
简介面向煤矿传送带矸石与锚杆异物检测场景这份资源提供基于YOLOv8的完整检测方案包含训练好的模型权重、三千多张标注数据集及Python图形界面库PyQt可视化界面适合工业视觉检测方向的算法工程师、科研人员及深度学习入门者使用。资源共两千个文件以XML标注文件为主另附环境配置PDF教程、PyQt运行步骤说明和Python辅助脚本压缩包约三百二十三兆字节。数据集已按训练集、验证集、测试集划分配有数据配置文件可直接用于YOLOv5、YOLOv7、YOLOv8、YOLOv9等算法训练也可以加载权重直接推理。图形界面支持交互式检测展示配套文档涵盖环境配置与运行流程便于快速复现。目前已有二百三十四人学习下载是煤矿异物检测项目落地与算法对比实验的高性价比参考。1. 煤矿传送带异物检测为什么YOLOv8PyQt这套组合值得你动手做矿山现场的传送带上矸石和锚杆是最让人头疼的两类异物。大块矸石会卡住转载点锚杆这类长条金属一旦卷入皮带轻则划伤胶面重则直接撕带停机检修一次少则几小时、多则一两天。过去靠人工盯监控一个值班员要看好几路画面夜班疲劳后漏看几乎是必然。用YOLOv8算法训练一个煤矿传送带矸石锚杆异物检测模型配合3000多张数据集和PyQt可视化界面就能把这套流程做成一个实时检测、自动报警的桌面工具。这篇笔记写给刚拿到数据集、想跑通“训练到界面”全流程的人也写给准备把模型部署到现场工控机的算法工程师。我会从数据集处理、YOLOv8训练、PyQt界面到现场避坑把能直接抄的步骤和参数写清楚。2. 先看清这张数据集的底细3000多张图怎么组织、怎么标2.1 数据集构成与标注格式拿到3000多张图第一件事不是急着开训而是先确认目录结构。常见的数据集组织方式是images和labels两个平级目录图片与同名.txt标注文件一一对应。标注文件每行格式为class_id x_center y_center width height其中坐标是相对图片宽高的比例值取值在 0 到 1 之间。比如一行0 0.5 0.3 0.2 0.15表示类别 0 的框中心在图片横向 50%、纵向 30% 处宽高分别为图片宽高的 20% 和 15%。我第一次拿到这类数据集时习惯先做一次“体检”看看类别分布是否均衡、有没有缺标注的图、有没有坐标越界的框。锚杆在传送带场景里属于稀疏目标一整张图经常只有一两根矸石则是密集目标一张图可能出现几十上百个。如果类别严重不平衡后面训练时模型会倾向于把矸石学得很好锚杆却经常漏掉。检查脚本很简单先用 Python 遍历标注文件统计每个类别数量。重点关注两点一是类别 ID 是否从 0 开始连续有没有跳号二是是否有空的标注文件。空标注文件本身不是错误但如果你在训练时开了mosaic增强空标注的图会被随机切碎反而可能制造出“无目标的背景块”影响模型收敛。我一般会把空标注图单独拎出来做背景样本而不是直接删掉。2.2 拆分为训练集、验证集与测试集一个可复用的 Python 脚本划分数据集是训练前最需要谨慎的一步。常见比例是 80% 训练、10% 验证、10% 测试但煤矿现场有个特殊问题如果同一条皮带的视频连续抽帧相邻帧背景高度相似随机划分很容易让验证集“泄漏”到训练集里。更稳妥的做法是按视频片段或按皮带编号划分再在片段内部抽帧。下面这个脚本按“先打乱图片列表再按比例切分”的方式工作适合你拿到的是已经整理好的图片集如果你手里是视频帧建议先按视频源分组再执行划分。import os import random from shutil import copy2 random.seed(42) # 固定随机种子保证多次运行结果一致 image_dir D:/dataset/images label_dir D:/dataset/labels train_img_dir D:/dataset/train/images train_lbl_dir D:/dataset/train/labels val_img_dir D:/dataset/val/images val_lbl_dir D:/dataset/val/labels test_img_dir D:/dataset/test/images test_lbl_dir D:/dataset/test/labels for d in [train_img_dir, train_lbl_dir, val_img_dir, val_lbl_dir, test_img_dir, test_lbl_dir]: os.makedirs(d, exist_okTrue) all_images [f for f in os.listdir(image_dir) if f.lower().endswith((.jpg, .jpeg, .png))] valid_images [] for img_name in all_images: stem os.path.splitext(img_name)[0] txt_path os.path.join(label_dir, stem .txt) if os.path.exists(txt_path): valid_images.append(img_name) else: print(缺少标注跳过:, img_name) random.shuffle(valid_images) total len(valid_images) train_n int(total * 0.8) val_n int(total * 0.1) train_list valid_images[:train_n] val_list valid_images[train_n:train_n val_n] test_list valid_images[train_n val_n:] def copy_image_and_label(img_name, img_out, lbl_out): stem os.path.splitext(img_name)[0] copy2(os.path.join(image_dir, img_name), os.path.join(img_out, img_name)) copy2(os.path.join(label_dir, stem .txt), os.path.join(lbl_out, stem .txt)) for img in train_list: copy_image_and_label(img, train_img_dir, train_lbl_dir) for img in val_list: copy_image_and_label(img, val_img_dir, val_lbl_dir) for img in test_list: copy_image_and_label(img, test_img_dir, test_lbl_dir) print(训练集:, len(train_list), 验证集:, len(val_list), 测试集:, len(test_list))这段脚本的核心思路是先过滤掉没有标注文件的图片再统一随机打乱最后按 80/10/10 切分。random.seed(42)很关键没有它每次运行划分结果都不同模型对比实验就失去意义。三个输出目录用于后续data.yaml里的train、val、test路径。如果你希望验证集更贴近真实工况可以修改划分逻辑改成按图片文件名中的皮带编号分组而不是全量随机。2.3 锚杆和矸石的类别特征与标注易错点这两类目标的物理特征差别很大。矸石是块状颜色和煤块接近在传送带光照不均时非常难区分锚杆是长条形有金属光泽但可能横着、斜着、被煤粉覆盖甚至只露出一端。标注时最常犯的三个错误第一个错误是矸石的框画得太紧。很多标注工具画完矩形后没有留边导致框只覆盖了目标中心边缘被切掉。YOLO 训练时背景上下文的损失很大框太紧反而让模型学不到“周围是传送带”这个关键信息。我一般建议在目标边缘留 2% 到 5% 的像素余量尤其是矸石。第二个错误是锚杆用水平框包一个斜着放置的长条。YOLOv8 默认是水平矩形框斜向锚杆会引入大量背景区域被误识别为“一根很宽的杠”。这种情况短期内不用急着换旋转框简单的做法是在标注时把长条锚杆拆成两段每段单独一个框尽量让每个框的长宽比接近 3:1 到 5:1。实测发现拆段后模型对斜向锚杆的召回率比硬画一个大框高不少。第三个错误是类别命名用中文。标注软件导出的类别名可能带中文但 YOLOv8 的data.yaml里类别名如果是中文在部分 Windows 环境下会因编码问题导致训练中断或界面显示乱码。我习惯把类别名写成拼音或英文比如gangshi和maogan界面显示时再映射成“矸石”“锚杆”。3. 训练YOLOv8检测模型环境、配置文件与损失曲线3.1 YOLOv8 环境配置与硬件检查训练之前先把环境装干净。常见的做法是用 Anaconda 创建独立环境避免把系统 Python 弄乱。YOLOv8 官方库ultralytics会拉取 PyTorch、OpenCV 等依赖建议先装 PyTorch 再装 ultralytics顺序反了可能装到 CPU 版 PyTorch。conda create -n yolov8 python3.9 -y conda activate yolov8 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics装好后先验证 GPU 是否可用很多训练“跑不起来”的翻车现场都卡在这一步。从 NVIDIA 官网查显卡算力GTX 1660 Ti 这类 6GB 显存的中端卡训练 YOLOv8s 没有问题如果只有 4GB建议直接选yolov8n或者把imgsz降到 480。python -c import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))如果输出True NVIDIA GeForce GTX 1660 Ti说明 GPU 正常如果输出False多半是 PyTorch 版本装成了 CPU 版。不要急着重装先pip list | grep torch看看是哪个包导致的很多时候是 CUDA 版本和 PyTorch 不匹配。3.2 写 data.yaml路径、类别名与类别数YOLOv8 训练时需要一个data.yaml它是整个训练的“地图”。路径建议用绝对路径避免换目录后找不到文件。文件内容如下path: D:/dataset train: train/images val: val/images test: test/images names: 0: gangshi 1: maogan这里path是数据集根目录train和val是相对路径。names的索引必须从 0 开始连续这一点和标注文件的 class_id 严格对应。如果你有标注文件中出现class_id2但names只定义了 0 和 1训练时会直接报错。遇到这种情况要么改标注文件要么在data.yaml里补上第三个名字不能跳号。还有一个容易忽略的参数是nc即类别数。如果你的names写了两个名字YOLOv8 会自动识别nc2如果你在names之外又显式写了nc: 1会覆盖掉names的推导结果导致模型输出的类别数和标注对不上。新手最容易在这里踩坑建议data.yaml里不要手动写nc让程序从names推导。3.3 启动训练命令、关键参数与 loss 曲线怎么看我用yolov8s作为起点在 3000 多张图上训练 100 轮显存占用大约 5GB 上下。如果你用的是yolov8m6GB 显存可能不够需要把batch降到 8。训练命令如下yolo detect train \ dataD:/dataset/data.yaml \ modelyolov8s.pt \ epochs100 \ imgsz640 \ batch16 \ device0 \ projectruns/yolo_mining \ nameexp1 \ patience20参数含义data指向刚才写的 yamlmodel用预训练权重做迁移学习epochs是训练轮数100 轮对于 3000 张图足够看到收敛趋势imgsz是训练分辨率640 是速度和精度的平衡点batch受显存限制显存不足优先降 batchpatience是早停参数验证损失连续 20 轮不下降就自动停止。训练开始后终端会打印Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size这样的日志。我最关心的三个曲线是train/box_loss、train/cls_loss和val/cls_loss。正常情况下训练和验证的损失都会下降并趋于平稳如果train/cls_loss一直降但val/cls_loss在中间开始反弹说明过拟合需要提前停止或增大验证集比例。YOLOv8 的训练日志会保存在runs/yolo_mining/exp1/目录下可以用tensorboard查看也可以打开results.png看曲线那个图里已经画好了损失和 mAP 的曲线。参数调整上我的血泪经验是不要一上来就把mosaic关掉。YOLOv8 默认开启mosaic1.0它对矸石这类密集目标很有效能让模型学会在目标重叠时仍然正确区分。但如果你的数据集里锚杆样本很少mosaic会随机裁剪图片导致锚杆被切成半截反而妨害学习。常见做法是把mosaic降到 0.5同时把mixup关掉给锚杆一个“干净”的学习环境。4. 把模型搬进 PyQt 可视化界面加载、推理与显示4.1 PyQt 界面架构推理线程与主线程别混在一起模型训练好之后下一步是做一个能实时显示检测结果的 PyQt 可视化界面。很多人在这一步翻车直接把YOLO模型放在主线程里跑结果界面不一会儿就“未响应”了。原因是model(frame)推理是耗时操作会阻塞 Qt 的事件循环导致界面无法刷新、按钮点不动。正确的架构是把推理放进一个QThread子线程界面主线程只负责显示子线程传上来的画面。模型对象在子线程里创建不要在主线程创建后再塞进子线程很多“黑匣子”崩溃就是这么来的。我用一个DetectWorker类来封装视频读取和模型推理通过信号把结果发回主界面。from PyQt5.QtCore import QThread, pyqtSignal import cv2 from ultralytics import YOLO class DetectWorker(QThread): frame_signal pyqtSignal(object) stats_signal pyqtSignal(dict) def __init__(self, model_path, source, conf0.25): super().__init__() self.model YOLO(model_path) self.cap cv2.VideoCapture(source) self.conf conf self._running True def run(self): while self._running: ret, frame self.cap.read() if not ret: break results self.model(frame, confself.conf, verboseFalse) annotated results[0].plot() self.frame_signal.emit(annotated) cls results[0].boxes.cls self.stats_signal.emit({ gangshi: int((cls 0).sum()), maogan: int((cls 1).sum()), }) self.cap.release() def stop(self): self._running False self.wait()代码里的frame_signal把画好检测框的画面传给主线程更新界面stats_signal把两类目标的实时数量传给主线程显示。YOLO(model_path)会在子线程内加载模型避免跨线程推理。这里有个细节results[0].plot()返回的是 BGR 格式的 numpy 数组传给主线程后需要转成QPixmap才能显示。stop()方法在界面关闭时调用防止子线程残留。参数说明conf0.25是置信度阈值现场场景如果怕漏检可以把阈值降到 0.15如果误检多则把阈值调到 0.4。这个参数应该做成界面里的可调控件而不是写死在代码里。4.2 实时显示检测框与统计信息的代码骨架主窗口部分只需要连接信号槽把画面和统计更新到对应控件。我习惯用QLabel放画面用一个QTableWidget放统计信息。界面代码骨架如下from PyQt5.QtWidgets import QMainWindow, QLabel, QVBoxLayout, QWidget from PyQt5.QtGui import QImage, QPixmap import numpy as np class MainWindow(QMainWindow): def __init__(self): super().__init__() self.label QLabel() self.setCentralWidget(self.label) self.worker None def start_detection(self, model_path, source): self.worker DetectWorker(model_path, source) self.worker.frame_signal.connect(self.update_frame) self.worker.stats_signal.connect(self.update_stats) self.worker.start() def update_frame(self, frame_bgr): rgb cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape qimage QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) self.label.setPixmap(QPixmap.fromImage(qimage).scaled( self.label.width(), self.label.height(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) def update_stats(self, stats): self.setWindowTitle(f矸石:{stats[gangshi]} 锚杆:{stats[maogan]})这段代码的核心在于update_frame里把 numpy 数组转成QImage再缩放显示。要特别注意的是scaled这一步如果检测分辨率是 640x640而界面只有 800x600不做缩放会占用大量内存。这里用KeepAspectRatio保持长宽比避免画面拉伸变形。SmoothTransformation会让画面稍微平滑一些但会增加 CPU 开销现场工控机配置不高的话可以换成快速模式避免界面卡顿。stats_signal的统计信息只包含两类目标的数量实际上还可以加上 FPS、当前帧号、模型推理耗时。这些信息对现场调试很重要我会在后面的验证章节展开。4.3 界面显示优化缩放、绘制边框与统计信息results[0].plot()虽然方便但它会在原图上叠加检测框、类别名和置信度绘制过程比较耗时。如果现场相机分辨率高比如 1920x1080每帧都plot()会明显拖慢推理线程。常见做法是从results[0].boxes里取出坐标和类别自己用cv2.rectangle和cv2.putText绘制这样可以把绘制分辨率和推理分辨率分开。推理用 640x640绘制只用坐标界面显示时再缩放到控件大小。还有一类坑是视频流断掉后没有提示。现场工控机可能连着网络摄像头网络抖动会导致cap.read()返回空帧如果没有重连逻辑界面就会定格在最后一帧值班员很难发现。我一般会在run()方法里加一个连续空帧计数超过 30 帧就重新打开视频源并向上抛一个“视频流中断正在重连”的信号。界面端此时显示提示文字而不是假装正常运行。统计信息不要只放在窗口标题上最好用表格控件列出全天累计数量、当前批次数量、平均置信度。尤其是累计数量现场人员用来判断“今天揪出多少根锚杆”很直观。这些统计可以在update_stats里用类成员变量累加每隔一分钟更新一次表格。5. 现场部署避坑漏检误检、界面卡顿与长期运行排查5.1 矸石漏检和煤块误检的排查方向现场跑起来后最先暴露的问题往往是“矸石没检出来”和“煤块被当成矸石”。这两个问题的现象、原因和解决思路完全不同我分开说。矸石漏检的最常见原因是目标太小。传送带宽度大矸石在画面里可能只有二三十个像素。YOLOv8 在 640x640 下对小目标的召回率并不理想。出现这个现象时不要急着换模型先把训练的imgsz从 640 提到 768 或 896重新训练一轮看验证集小目标召回率是否提升。如果显存不够可以降低 batch 来换取高分辨率。还有一种可能是验证集里全是近距离大矸石模型压根没见过远距离小矸石这就要回到数据集层面补充样本而不是改模型。煤块误检为矸石的原因是类别间特征太接近。煤块和矸石在灰度图上几乎无法区分只能靠纹理、反光等细节。解决误检的第一道闸门是置信度阈值现场可以把conf调到 0.35 以上如果误报依然多就需要采集“煤块正常通过”的负样本在训练集里加一个background类别或者在推理时对“矸石”类别的检出框做一个条件过滤框内平均灰度低于某阈值就忽略。后者在光照稳定的固定视角下效果不错但需要现场标定属于应急手段。5.2 PyQt 界面卡顿、内存持续上涨的处理现象界面开着两小时后点击按钮明显迟钝进程内存从 200MB 涨到 2GB。原因通常是三处第一是results[0].plot()返回的 BGR 数组每次都是新的主线程setPixmap后又没释放旧对象第二是子线程读视频帧的速度比主线程显示速度快信号槽内部的事件队列积压第三是 Qt 的QPixmap在频繁缩放时会临时创建大量中间对象。解决卡顿第一步是控制帧率。现场视频 25 帧/秒足够但检测线程不必每帧都发信号给 UI。我通常会在DetectWorker里加一个跳帧机制只发推理结果UI 显示交给一个定时器每 200ms 取最新帧。更彻底的方案是使用queue.Queue限制队长度主线程每取出一帧就丢掉旧帧保证界面永远显示最新的检测结果。内存上涨的排查思路是先定位到哪一步。给update_frame里加print日志观察frame_signal发射频率。如果发射频率稳定但内存照涨问题多半在QImage构造时的数据拷贝上。QImage(rgb.data, ...)这里rgb.data是 numpy 数组底层指针Qt 默认会拷贝数据如果你用QImage(rgb.data, w, h, bytesPerLine, QImage.Format_RGB888)并且不复制rgb在槽函数结束后被释放指针变为悬空界面会花屏。所以这个拷贝不能省但你可以每次在更新QPixmap前调用self.label.setPixmap(QPixmap())清空旧图强制回收上一帧资源。5.3 模型导出与推理速度的取舍如果现场工控机是 CPU 计算直接用.pt文件跑推理会很慢。有一个参数先调起来model(frame, devicecpu)时可以设置halfTrue半精度在 CPU 上不一定有加速但在部分支持 AVX512 的 CPU 上有明显提升。更常见的加速做法是导出成 ONNX再在 PyQt 里用 ONNX Runtime 加载。导出命令yolo export modelruns/yolo_mining/exp1/weights/best.pt formatonnx opset12 simplifyTrue导出后得到一个best.onnx大小比.pt小很多推理速度通常提升两三倍。需要用onnxruntime加载并注意输入张量的维度顺序是[1, 3, 640, 640]输出是[1, 6, 8400]其中 6 是cx, cy, w, h, score, class。后处理需要自己写 NMS这部分比直接用 ultralytics 麻烦但性能提升值得。如果你打算把 YOLOv8 部署到 RK3588 这类边缘设备需要先把 ONNX 转成 RKNN 格式转换时留意算子支持情况simplify过的模型更容易转换成功。从部署稳定性来看宁可先跑.pt版本确认功能正确再逐步换成 ONNX。不要一上来就图快最后界面闪烁、漏检增加反而很难定位到底是谁的问题。6. 用一段固定视频做回归测试给现场验收留个底线模型调参最忌讳“没有基线”。我吃过一次大亏直接在现场连相机调置信度阈值调了半天觉得“好像好了一点”但换一个光照时段的视频效果又变差了。后来我养成一个习惯落地前先录制一段包含典型工况的固定测试视频5 分钟包含大矸石、小矸石、横放锚杆、斜放锚杆、皮带空载煤流段。所有参数修改都以这段视频的结果为准不再凭现场感觉。这段视频的验证脚本不需要很复杂把检测结果逐帧写入 CSV统计总帧数、检测到目标的帧数、推理时间就够了。下面是一个最小实现import cv2 from ultralytics import YOLO model YOLO(best.pt) cap cv2.VideoCapture(test.mp4) fps 0 frame_count 0 target_frames 0 while cap.isOpened(): ret, frame cap.read() if not ret: break frame_count 1 results model(frame, conf0.25, verboseFalse) boxes results[0].boxes if len(boxes) 0: target_frames 1 fps 1 / results[0].speed[inference] # 注意单位毫秒需换算 cap.release() print(f总帧数: {frame_count}, 检出帧: {target_frames}, 平均推理FPS: {fps:.1f})这里results[0].speed返回的是preprocess、inference、postprocess三段时间单位是毫秒计算 FPS 时用1000 / inference才是每秒帧数。实际验证中我会额外把固定测试视频里“目标出现的帧号”人工标出来再对比检测结果算出召回率和误检率。这一步工作量不小但只有这样才能给现场验收一个明确的数据。我在部署 PyQt 界面时还会加一个“运行时长”显示记录程序启动时间和当前时间。如果界面卡死日志文件里最后一条时间戳就是崩溃点。这个笨办法帮过我两次一次是发现内存泄漏出现在连续运行 26 小时后另一次是抓到视频流重连逻辑触发太频繁导致的死循环。希望这篇笔记能帮你少走这些弯路。如果你也在做煤矿传送带异物检测不妨先录制那段固定视频再上手调参——它比任何经验帖都能让你少加班。本文还有配套的精品资源点击获取