ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

YOLOv8+PyQt煤矿传送带异物检测:从数据集到部署全流程

YOLOv8+PyQt煤矿传送带异物检测:从数据集到部署全流程 简介本资源面向煤矿智能化巡检与工业视觉检测方向的开发者、研究生及工程技术人员提供一套基于YOLOv8的传送带矸石与锚杆异物检测完整方案可直接用于推理部署也可基于数据集重新训练。压缩包共约2000个文件以1991个XML标注文件为主另含少量md说明、pdf环境配置教程与py脚本整体约323.4MB标签为txt格式数据集已按train、val、test划分并附data.yamlnc为2类别涵盖bolt_object与bulk_objectyolov5、yolov7、yolov8、yolov9等算法均可直接训练。资源还包含PyQt可视化界面方便演示检测效果。已有233人学习适合希望快速复现煤矿异物检测项目、搭建可视化推理界面或进行二次开发的读者参考使用。1. 煤矿传送带异物检测YOLOv8 加 PyQt 这套组合到底能不能落地煤矿传送带上的矸石和锚杆是两类性质完全不同的异物。矸石是灰黑色、形状不规则的岩块和煤块在灰度上接近肉眼在粉尘环境下都容易看走眼锚杆是细长金属杆经常斜插在煤流里露出一截剩下的被煤埋住。这两类目标混在同一条皮带上传统方案要么靠人工盯着监控屏要么用简单的阈值分割加形状判断前者费人后者一遇到光照变化和粉尘就翻车。YOLOv8 目标检测模型加 3000 多张现场数据集再配一个 PyQt 可视化界面这套组合解决的就是「让模型在煤矿现场环境下稳定框出矸石和锚杆并且让现场人员能直接操作」的问题。适合谁看做工业视觉检测的算法工程师、想用 YOLOv8 落地自己数据集的目标检测从业者以及需要给现场做一套能演示、能部署的检测系统的开发者。这篇文章不讲空泛的算法原理只讲从数据集处理到模型训练再到 PyQt 界面集成的完整路径以及我在这个过程中踩过的坑。2. 数据集怎么处理才能让 YOLOv8 真正学到矸石和锚杆2.1 3000 张现场图的标注策略与类别定义拿到 3000 多张煤矿传送带现场图第一件事不是直接丢进 YOLOv8 训练而是先做类别定义和标注规范。常见做法是把类别定为两类gangshi矸石和maogan锚杆。但这里有个容易忽略的点——矸石和煤块在图像上高度相似如果标注时把边界模糊的岩块也标成矸石模型会学到大量噪声。我一般会定三条标注规则第一矸石只标那些与周围煤块有明显纹理或颜色差异的块状物边界模糊的不标第二锚杆只标可见部分被煤完全覆盖的不标第三锚杆与矸石重叠时两个类别都标允许框重叠。标注工具用 Labelme 或 LabelImg 都行导出 YOLO 格式的 txt 文件。标注完成后检查一下每类样本数量。3000 张图里如果锚杆只有两三百个实例而矸石有五六千个直接训练会导致锚杆漏检严重。这时候要么补标锚杆要么在训练时用类别权重做平衡。2.2 从原始图到 YOLOv8 训练格式的转换脚本YOLOv8 要求的数据格式是每张图对应一个同名 txt每行是class_id x_center y_center width height全部归一化到 0 到 1。如果你用 Labelme 标的是 JSON需要先转成 YOLO 格式。下面这个脚本处理 Labelme JSON 到 YOLO txt 的转换同时按 8:1:1 划分训练集、验证集和测试集。import json import os import random import shutil from pathlib import Path # 类别映射根据你的实际标注调整 CLASS_MAP {gangshi: 0, maogan: 1} def labelme_to_yolo(json_path, img_w, img_h): 将 Labelme JSON 转为 YOLO 格式的行列表 with open(json_path, r, encodingutf-8) as f: data json.load(f) lines [] for shape in data[shapes]: label shape[label] if label not in CLASS_MAP: continue cls_id CLASS_MAP[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) # 归一化中心点和宽高 x_center (x_min x_max) / 2.0 / img_w y_center (y_min y_max) / 2.0 / img_h w (x_max - x_min) / img_w h (y_max - y_min) / img_h # 过滤掉宽高为0的无效框 if w 0 or h 0: continue lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) return lines def split_dataset(img_dir, json_dir, out_dir, ratios(0.8, 0.1, 0.1)): 按比例划分数据集并生成 YOLO 格式目录 img_dir Path(img_dir) json_dir Path(json_dir) out_dir Path(out_dir) # 收集所有有对应 JSON 的图片 pairs [] for img_path in img_dir.glob(*.jpg): json_path json_dir / (img_path.stem .json) if json_path.exists(): pairs.append((img_path, json_path)) random.shuffle(pairs) n len(pairs) n_train int(n * ratios[0]) n_val int(n * ratios[1]) splits { train: pairs[:n_train], val: pairs[n_train:n_train n_val], test: pairs[n_train n_val:] } for split_name, split_pairs in splits.items(): img_out out_dir / images / split_name lbl_out out_dir / labels / split_name img_out.mkdir(parentsTrue, exist_okTrue) lbl_out.mkdir(parentsTrue, exist_okTrue) for img_path, json_path in split_pairs: # 复制图片 shutil.copy(img_path, img_out / img_path.name) # 读取图片尺寸这里假设已知或从文件名推断 # 实际使用时建议用 PIL 读取 from PIL import Image with Image.open(img_path) as im: w, h im.size lines labelme_to_yolo(json_path, w, h) with open(lbl_out / (img_path.stem .txt), w) as f: f.write(\n.join(lines)) if __name__ __main__: split_dataset( img_dirraw_images, json_dirraw_jsons, out_diryolo_dataset )这段代码的关键参数在ratios和CLASS_MAP。ratios控制训练验证测试比例煤矿数据量不大时建议 8:1:1如果锚杆样本少可以把验证集比例调小到 0.05把更多数据留给训练。CLASS_MAP必须和你的标注标签完全一致大小写都不能错否则转换出来全是空文件。转换完成后检查yolo_dataset/labels/train下的 txt 文件随便打开几个确认每行有 5 个值且后四个都在 0 到 1 之间。如果出现大于 1 的值说明归一化时图片尺寸读错了。2.3 数据增强煤矿场景下哪些增强有用哪些是负优化YOLOv8 自带的数据增强包括 HSV 调整、旋转、平移、缩放、马赛克增强等。在煤矿传送带场景下HSV 调整是有用的因为现场光照变化大粉尘浓度不同会导致图像整体偏灰或偏黄。旋转和平移也有用锚杆的插入角度多变。但马赛克增强要慎用。马赛克增强会把四张图拼成一张对于矸石这种需要看整体形状的目标拼接后模型可能学到错误的上下文关系。我一般会把mosaic概率从默认的 1.0 降到 0.5训练后期再关闭。另外上下翻转在传送带场景下是负优化因为煤流方向固定翻转后的图像不符合物理规律。在 YOLOv8 的配置文件中可以这样设置# data.yaml path: ./yolo_dataset train: images/train val: images/val test: images/test nc: 2 names: [gangshi, maogan]训练时的增强参数通过命令行传入后面训练章节会具体说。3. YOLOv8 训练矸石和锚杆检测模型的参数怎么设3.1 环境搭建与预训练权重选择环境搭建这一步Ubuntu 20.04 和 Windows 都可以但煤矿现场部署机器一般是 Windows 或者工控机所以建议在 Windows 上装一套 CUDA 环境做训练或者用 Ubuntu 训练完再移植。CPU 版本也能跑但 3000 张图训练 100 轮CPU 大概要跑一两天GPU 几小时就完事。显卡方面GTX 1660 Ti 这个级别跑 YOLOv8n 或 YOLOv8s 足够显存 6G 能跑 batch size 8 左右。安装命令很简单pip install ultralytics预训练权重选择上YOLOv8n 最快但精度最低YOLOv8m 精度好但速度慢。煤矿传送带检测对实时性有要求一般 15 到 30 FPS 就能满足现场需求所以 YOLOv8s 是比较平衡的选择。如果现场工控机性能差用 YOLOv8n 加更多训练轮次也能凑合。3.2 训练命令与关键参数逐项说明训练命令用 ultralytics 的 CLI 或者 Python 脚本都行。下面用 Python 脚本方式方便后面集成到 PyQt 里。from ultralytics import YOLO # 加载预训练权重 model YOLO(yolov8s.pt) # 开始训练 results model.train( datadata.yaml, # 数据集配置文件 epochs150, # 训练轮次 imgsz640, # 输入图像尺寸 batch8, # 批次大小根据显存调整 device0, # GPU 编号CPU 填 cpu workers4, # 数据加载线程数 optimizerSGD, # 优化器 lr00.01, # 初始学习率 lrf0.01, # 最终学习率因子 momentum0.937, # 动量 weight_decay0.0005, # 权重衰减 warmup_epochs3.0, # 预热轮次 mosaic0.5, # 马赛克增强概率从默认1.0降低 fliplr0.5, # 水平翻转概率 flipud0.0, # 关闭上下翻转 hsv_h0.015, # 色调增强 hsv_s0.7, # 饱和度增强 hsv_v0.4, # 亮度增强 patience30, # 早停耐心值 saveTrue, # 保存检查点 projectruns/train, # 输出目录 namecoal_detection # 实验名称 )逐项说几个关键参数。imgsz640是 YOLOv8 的默认值如果锚杆在图像中占比很小可以提到 1280但显存占用会翻倍。batch8在 6G 显存下跑 YOLOv8s 比较稳如果报显存不足就降到 4。lr00.01是 SGD 优化器的常用初始学习率如果 loss 震荡厉害降到 0.001 试试。patience30表示 30 轮验证集指标不提升就早停煤矿数据集小这个值别设太大否则过拟合。mosaic0.5是我特意从默认的 1.0 降下来的原因前面说了矸石检测不需要那么强的拼接增强。flipud0.0关闭上下翻转因为传送带煤流方向固定。3.3 训练过程监控与损失函数曲线怎么看训练开始后runs/train/coal_detection目录下会生成results.csv和一系列曲线图。重点看三个指标train/box_loss、val/box_loss和metrics/mAP50。如果train/box_loss持续下降但val/box_loss先降后升说明过拟合了需要减少训练轮次或者加数据增强。如果两个 loss 都下降很慢可能是学习率太小或者数据标注有问题。mAP50在矸石和锚杆两类上的表现要分开看YOLOv8 的验证输出里会给出每类的 AP如果锚杆的 AP 明显低于矸石说明锚杆样本太少或者标注质量差。想看损失函数曲线图可以直接用训练生成的results.png也可以用 pandas 读results.csv自己画import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/train/coal_detection/results.csv) df.columns df.columns.str.strip() # 去除列名空格 fig, axes plt.subplots(1, 2, figsize(12, 4)) axes[0].plot(df[epoch], df[train/box_loss], labeltrain_box_loss) axes[0].plot(df[epoch], df[val/box_loss], labelval_box_loss) axes[0].set_xlabel(epoch) axes[0].set_ylabel(box_loss) axes[0].legend() axes[1].plot(df[epoch], df[metrics/mAP50(B)], labelmAP50) axes[1].set_xlabel(epoch) axes[1].set_ylabel(mAP50) axes[1].legend() plt.tight_layout() plt.savefig(training_curves.png)这段代码读取训练日志画出损失和 mAP 曲线。注意results.csv的列名可能带空格用strip()处理一下。如果 mAP50 在 0.85 以上基本可以拿去现场试了如果低于 0.7回去检查标注和增强参数。4. PyQt 可视化界面怎么和 YOLOv8 推理对接4.1 界面布局设计与检测线程分离PyQt 界面做目标检测可视化核心问题是不能把推理放在主线程里否则界面会卡死。常见做法是用QThread开一个检测线程主线程只负责显示。界面布局上左边放视频显示区域右边放检测结果列表和控制按钮。一个最小可用的布局方案顶部一个QLabel显示视频帧下面一个QTextEdit显示检测到的目标类别和数量底部一排按钮控制开始、暂停、切换视频源。视频源可以是本地视频文件也可以是摄像头现场一般接工业相机用 OpenCV 的VideoCapture读取。4.2 推理线程的完整实现代码下面这个DetectionThread类继承QThread在run方法里循环读帧、推理、发信号。import cv2 from PyQt5.QtCore import QThread, pyqtSignal from ultralytics import YOLO class DetectionThread(QThread): # 信号发送带框的图像和检测结果文本 frame_ready pyqtSignal(object) result_ready pyqtSignal(str) def __init__(self, model_path, source0, conf0.5): super().__init__() self.model YOLO(model_path) self.source source self.conf conf self.running False def run(self): self.running True cap cv2.VideoCapture(self.source) if not cap.isOpened(): self.result_ready.emit(无法打开视频源) return while self.running: ret, frame cap.read() if not ret: break # 推理 results self.model(frame, confself.conf, verboseFalse) annotated results[0].plot() # 绘制检测框 # 统计检测结果 boxes results[0].boxes if boxes is not None: cls_ids boxes.cls.cpu().numpy().astype(int) names results[0].names counts {} for cid in cls_ids: name names[cid] counts[name] counts.get(name, 0) 1 text | .join([f{k}: {v} for k, v in counts.items()]) else: text 未检测到目标 self.frame_ready.emit(annotated) self.result_ready.emit(text) cap.release() def stop(self): self.running False self.wait()关键参数说明model_path指向训练好的best.ptsource填 0 表示默认摄像头填视频路径表示读文件conf0.5是置信度阈值现场如果漏检多就降到 0.3误检多就提到 0.7。results[0].plot()返回带框的 numpy 数组直接转成 QImage 就能显示。4.3 主窗口集成与信号槽连接主窗口里创建DetectionThread实例把frame_ready信号连到更新 QLabel 的槽函数把result_ready连到更新文本框的槽函数。from PyQt5.QtWidgets import QMainWindow, QLabel, QTextEdit, QPushButton, QVBoxLayout, QHBoxLayout, QWidget from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtCore import Qt class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(煤矿传送带异物检测系统) self.resize(1200, 700) self.video_label QLabel(视频显示区域) self.video_label.setAlignment(Qt.AlignCenter) self.video_label.setMinimumSize(800, 600) self.result_text QTextEdit() self.result_text.setReadOnly(True) self.result_text.setMaximumWidth(300) self.btn_start QPushButton(开始检测) self.btn_stop QPushButton(停止检测) self.btn_start.clicked.connect(self.start_detection) self.btn_stop.clicked.connect(self.stop_detection) right_layout QVBoxLayout() right_layout.addWidget(self.result_text) right_layout.addWidget(self.btn_start) right_layout.addWidget(self.btn_stop) main_layout QHBoxLayout() main_layout.addWidget(self.video_label, stretch3) main_layout.addLayout(right_layout, stretch1) container QWidget() container.setLayout(main_layout) self.setCentralWidget(container) self.thread None def start_detection(self): if self.thread is not None and self.thread.isRunning(): return self.thread DetectionThread( model_pathruns/train/coal_detection/weights/best.pt, source0, conf0.5 ) self.thread.frame_ready.connect(self.update_frame) self.thread.result_ready.connect(self.update_result) self.thread.start() def stop_detection(self): if self.thread is not None: self.thread.stop() self.thread None def update_frame(self, frame): # OpenCV 是 BGRQt 是 RGB需要转换 rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape bytes_per_line ch * w qimg QImage(rgb.data, w, h, bytes_per_line, QImage.Format_RGB888) self.video_label.setPixmap(QPixmap.fromImage(qimg).scaled( self.video_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) def update_result(self, text): self.result_text.append(text)这段代码里update_frame做了 BGR 到 RGB 的转换这是 OpenCV 和 Qt 颜色通道顺序不同导致的忘了转的话画面颜色会偏蓝。scaled保持宽高比避免画面拉伸变形。result_text.append会不断追加文本实际使用时可以改成setPlainText只显示当前帧结果。5. 从训练到部署这些坑我替你踩过了5.1 标注文件类别名大小写不一致导致训练全空现象训练开始后 loss 一直是 0 或者 nan验证集 mAP 为 0。原因Labelme 标注时类别名写的是Gangshi但CLASS_MAP里写的是gangshi转换脚本里if label not in CLASS_MAP直接跳过了所有标注生成的 txt 全是空文件。解决转换前先统计一遍所有 JSON 里的 label 值和CLASS_MAP的 key 做一次集合对比不一致的打印出来。养成习惯标注规范文档里明确写死类别名所有人统一用。5.2 锚杆漏检严重调低置信度后误检爆炸现象模型对矸石检测还行但锚杆十根漏八根。把conf从 0.5 降到 0.2锚杆能检出来一些但煤块边缘全被框成锚杆。原因锚杆样本太少模型没学充分。降置信度只是让更多低质量预测通过并没有解决模型本身对锚杆特征学习不足的问题。解决两条路一起走。一是补标锚杆数据至少补到和矸石实例数一个量级二是在训练时对锚杆类别加权YOLOv8 本身不直接支持类别权重但可以通过复制锚杆样本图片来间接实现。另外锚杆的细长形状对 anchor 匹配不友好可以试试把imgsz提到 1280让小目标特征更清晰。5.3 PyQt 界面跑几帧就卡死现象点击开始检测后视频显示几帧就不动了界面无响应。原因推理代码写在了主线程里或者DetectionThread里用了self.model(frame)但没设verboseFalse每帧都往控制台打印大量日志拖慢速度。解决确认推理在QThread的run方法里执行。model调用时加verboseFalse。另外frame_ready信号发送的是 numpy 数组如果数组很大且发送频率高Qt 信号队列会堆积可以在update_frame里加一个简单的丢帧逻辑比如判断当前时间距上一帧超过 50ms 才更新显示。5.4 训练好的模型换台机器推理结果全乱现象在训练机器上best.pt推理正常拷到现场工控机上检测框位置全偏了。原因现场工控机的 OpenCV 版本和训练机器不一致图像解码后的尺寸或颜色通道有差异。更常见的是现场摄像头分辨率和训练图像分辨率不同YOLOv8 虽然会自动 resize但如果宽高比差异太大resize 后目标变形严重。解决推理前统一做一次 letterbox 缩放保持宽高比填充灰边。YOLOv8 的model.predict内部其实做了 letterbox但如果你自己用 OpenCV 读帧后直接传最好确认一下输入尺寸。另外把训练时的imgsz和推理时的imgsz设成一样别训练用 640 推理用 1280。5.5 现场粉尘导致画面发灰模型置信度整体下降现象实验室测试 mAP 0.9到了现场同一套模型 mAP 掉到 0.6大量目标置信度在 0.3 到 0.5 之间。原因训练数据虽然来自现场但采集时粉尘浓度和实际生产时不同。粉尘会让图像对比度下降边缘模糊YOLOv8 的卷积特征提取对这种退化敏感。解决在训练数据里加入粉尘模拟增强用高斯模糊加对比度降低来模拟不同粉尘浓度。推理前做一次 CLAHE 直方图均衡化提升局部对比度。如果现场光照稳定也可以考虑加一个偏振镜减少粉尘反光。6. 把 mAP 再提一截锚杆小目标的针对性调优技巧锚杆检测是这套系统里最难的部分因为它在图像里通常只占几十个像素宽属于典型的小目标。YOLOv8 默认的 P3 检测头对小于 8x8 像素的目标召回率很低。我试过几种调优方式下面这个组合效果最明显。第一把输入尺寸从 640 提到 1024 或 1280。锚杆在 640 输入下可能只有 5 个像素宽提到 1280 后变成 10 个像素特征明显得多。代价是推理速度下降GTX 1660 Ti 上 1280 输入大概 10 FPS如果现场能接受就上。第二修改模型结构增加一个 P2 检测头。YOLOv8 的官方结构里最小是 P3对应 stride 8。加 P2 对应 stride 4能捕捉更小的目标。具体做法是改yolov8.yaml在 head 部分加一层head: - [-1, 1, nn.Upsample, [None, 2, nearest]] - [[-1, 2], 1, Concat, [1]] # 和 backbone 的 P2 层拼接 - [-1, 3, C2f, [128]] # 新增的 P2 检测分支 - [[-1, 4, 5], 1, Detect, [nc]] # 检测头包含 P2/P3/P4改完结构后预训练权重不能直接加载了需要从头训或者只加载 backbone 部分。训练轮次也要增加因为小目标检测头学得慢。第三用切片推理。把一张大图切成若干 640x640 的小块分别推理后再合并结果。这个方法对锚杆特别有效因为切片后锚杆在每块里的相对尺寸变大了。缺点是推理耗时成倍增加而且切片边界处的目标可能被切断。我一般用 20% 重叠切片合并时用 NMS 去重。第四后处理加一个形状过滤。锚杆的宽高比通常大于 3:1矸石的宽高比接近 1:1。在推理结果里对类别为锚杆的框如果宽高比小于 2直接丢弃。这个简单规则能过滤掉大量把煤块边缘误检成锚杆的情况。def filter_maogan(boxes, aspect_thresh2.0): 过滤宽高比不合理的锚杆检测框 keep [] for box in boxes: cls_id int(box.cls) if cls_id 1: # 锚杆类别 w box.xywh[0][2] h box.xywh[0][3] aspect max(w, h) / min(w, h) if aspect aspect_thresh: continue keep.append(box) return keep这个过滤逻辑放在results[0].boxes之后、plot()之前。aspect_thresh2.0是保守值实际可以调到 2.5 或 3.0看误检情况。最后说一个验证方法别只看 mAP去现场录几段不同时段的视频用训练好的模型跑一遍人工数一下漏检和误检。我自己的习惯是每改一次参数或结构就在固定的三段测试视频上跑一遍记录漏检数和误检数做成表格对比。这样能避免被验证集上的虚高指标骗了。这套方案从数据集处理到 PyQt 界面集成完整跑通大概需要两到三周其中标注和调参占大头。值不值得做如果你手头有现场数据和部署需求YOLOv8 加 PyQt 是目前工业检测里性价比很高的组合社区资源多遇到问题好查。但别指望一次训练就完美锚杆小目标检测需要反复调耐心点。希望帮到你。本文还有配套的精品资源点击获取
返回列表