简介:一套基于YOLOv8的商场自动扶梯梳齿板异物卡滞预警系统完整项目,专门针对扶梯梳齿板异物卡滞场景的实时检测与告警,适用于计算机视觉、深度学习方向的毕业设计、课程设计或初期项目立项,并已跑通完整流程。压缩包共8个文件,包含3个Python脚本(可视化界面、视频检测、模型训练)、3个PyTorch模型权重(含预训练与最优权重)以及2个txt说明文档,整体仅15.91MB,轻量易部署,下载后按README即可快速复现。目前已有36人学习下载。项目包含源码、完整数据集、可视化页面和部署教程,训练脚本可输出核心指标曲线、混淆矩阵、F1分数曲线、精确率-召回率曲线、验证集预测结果及标签分布图,从模型训练到验证演示形成闭环。这套方案能充分支撑毕设答辩中的实验展示,也适合小白学习者逐步进阶。
1. 扶梯梳齿板异物卡滞:这个YOLOv8系统解决的是哪一类问题
商场自动扶梯的梳齿板在出入口处,齿槽间隙一旦卡入钢丝、竹签、硬币或者高跟鞋跟,轻则触发急停、重则造成梯级损坏甚至伤人。这类问题的难点在于异物尺寸小、与背景对比度低、出现位置固定在梳齿区域,传统帧差法和背景建模在反光的金属齿面上误报率极高。用YOLOv8做目标检测,本质上是把「人工盯守」变成「单阶段模型即时判定」,对钢丝、瓶盖、防滑条碎块这些常见异物做实时框选,触发报警后联动急停信号。这套方案的热搜词恰好集中在「yolov8训练自己的数据集」「可视化界面」「完整数据集」上,说明它面向的是两类人:准备毕设/课设的学生,以及想快速验证视觉方案可行性的现场工程师。
我要先说明一个容易混淆的点:这个标题不是让你从零写一个检测算法,而是站在YOLOv8的肩膀上做数据、调参、封装界面这三件事。梳齿板异物检测属于典型的小目标检测场景,YOLOv8自带anchor-free解耦头,配合合适的输入尺寸,在1080p监控画面上能稳定识别12像素以上的异物。后面我按环境搭建、数据准备、训练调参、界面联调、常见踩坑这个顺序展开,你能直接复现出一个能跑的完整系统。
2. 在商场扶梯场景里选YOLOv8:小目标与单阶段检测的适配逻辑
2.1 梳齿板异物为什么难检测
扶梯梳齿板的物理特性决定了检测难度。它是金属材质,表面有规律齿槽,环境光从顶棚射灯到侧窗自然光都在变化,齿面会产生周期性高光反射。异物卡进去之后,钢丝可能只有2-3像素宽,但长度能到十几像素;瓶盖这类扁平物体则是低对比度、与齿槽颜色接近。传统图像处理里用边缘检测+Canny再找轮廓,在齿槽自身边缘干扰下根本分不清异物和梳齿。背景建模也失效,因为扶梯本身在运动,梯级每秒钟都在变化,静态背景假设不成立。
YOLOv8在这种场景下的优势来自三个方面。一是C2f结构替换了原先的C3,梯度流更丰富,小目标的浅层特征保留更好;二是anchor-free检测头直接回归中心点到边界的距离,对长宽比极端的细长异物(比如钢丝、竹签)更友好,不用像anchor-based那样依赖先验框长宽比;三是模型本身有多种尺寸可选,n/s/m/l/x从4M到100M+参数,毕设场景跑n或s足够,现场部署可以压缩到onnx再交给rk3588这类边缘盒子,后面我会单独讲。
需要警惕的是,YOLOv8不是调完参就能直接用的。它的默认锚框设计偏向COCO数据集的物体尺度分布,而梳齿板异物的平均像素面积通常只有整张图的0.5%以下。如果直接拿默认配置训练,模型会倾向于把整条梳齿板当成一个大物体来学习,而不是聚焦齿槽内的异常。所以后面在数据增强和损失权重上要做针对性调整。
2.2 把数据集做成YOLO格式:标注规范与目录组织
做这个项目第一步不是写代码,是整理数据。完整数据集一般包含正常梳齿板图、卡钢丝图、卡瓶盖/纸屑图、卡高跟鞋跟图,每类至少200张,总计800-1000张才能保证mAP50不掉到0.6以下。采集途径有三种:商场实地拍摄、从扶梯维保监控视频中抽帧、网上找电梯部件检测公开数据。前两种最可靠,因为光照和角度跟实际部署一致。
标注工具用labelme或labelImg都行,但我更推荐labelme,因为它的多边形标注能贴合细长异物的形状,导出JSON后再转YOLO格式。这里有个关键点:YOLO格式的标签是归一化的中心点坐标加宽高,不是多边形点集。转的时候要处理两个边界情况——标注框超出图像边缘时要不要裁剪、多个异物重叠时保留哪个框。我给的转换脚本如下:
import json import os def labelme_to_yolo(json_path, out_dir, class_map): os.makedirs(out_dir, exist_ok=True) with open(json_path, 'r', encoding='utf-8') as f: data = json.load(f) img_w = data['imageWidth'] img_h = data['imageHeight'] lines = [] for shape in data['shapes']: label = shape['label'] if label not in class_map: continue 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_min = max(0, x_min) x_max = min(img_w, x_max) y_min = max(0, y_min) y_max = min(img_h, y_max) if x_max <= x_min or y_max <= y_min: continue box_w = x_max - x_min box_h = y_max - y_min cx = (x_min + x_max) / 2 / img_w cy = (y_min + y_max) / 2 / img_h w = box_w / img_w h = box_h / img_h class_id = class_map[label] lines.append(f"{class_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") base = os.path.basename(json_path).replace('.json', '') with open(os.path.join(out_dir, base + '.txt'), 'w') as f: f.write('\n'.join(lines)) class_map = {'wire': 0, 'bottlecap': 1, 'paper': 2, 'heel': 3} # 用法示例:遍历labels目录下所有json for jf in os.listdir('labels'): if jf.endswith('.json'): labelme_to_yolo(os.path.join('labels', jf), 'yolo_labels', class_map)这个脚本的边界裁剪逻辑很关键。标注时手一抖,框可能画出图像边界,YOLO训练时Box损失会计算出负数坐标直接loss变为NaN。裁剪后还能保住框内区域,不会丢样本。另外class_map的类别顺序一旦确定就不要再改,改了就相当于重新标注。
标注粒度也要注意:钢丝这类细长物,标注框不要包太多背景。YOLOv8的损失函数对背景敏感的,框内背景占比超过50%时,模型容易学会「看到梳齿纹理就框选」而不是「看到异物才框选」。正确的做法是让标注框紧贴异物轮廓,对弯曲的钢丝可以用两个小框拼接,虽然人工标注量上去了,但mAP能提5-8个点。
2.3 数据划分与增强策略:别让模型死记硬背
数据划分上,我一般按7:2:1分训练、验证、测试。有一个细节——同一个监控摄像头连续视频帧抽出来的图片,必须先做视频级去重,再划分数据集。不然训练集和验证集里可能同时出现同一秒的前后帧,模型相当于开了天眼,验证精度虚高,部署到现场就翻车。去重方法很简单:先按场景视频片段分组,整段视频的帧只进一个集合。
数据增强在Ultralytics YOLOv8里通过配置文件控制,我给的参数如下:
# aug.yaml 用于扶梯梳齿板场景的增强配置 flipud: 0.5 fliplr: 0.5 hsv_h: 0.015 hsv_s: 0.5 hsv_v: 0.4 translate: 0.1 scale: 0.3 mosaic: 0.8 mixup: 0.2 copy_paste: 0.3注意三个参数:hsv_h我压到了0.015,因为商场内部光源基本是暖白或冷白,色相漂移太大会让模型学到不真实颜色;translate设为0.1就够,扶梯梳齿板在画面中的位置是固定的,过度平移等于把异物移到梳齿区域外制造假样本;mosaic开0.8,这个默认值够用,mixup保留0.2,可以让模型看到异物叠加在半透明图层上的效果。
增强的底层逻辑是模拟扶梯运行中的真实变化——亮度变化、视角微动、阴影遮挡。但增强过强会让模型泛化到「随便一个暗色物体就是异物」,所以测试集的提纯很重要。测试集只放现场采集的、未增强的原始帧,所有评估指标以测试集为准,不以验证集为准。
3. 用部署包在本地跑通最小环境:从Python依赖到第一个检测框
3.1 部署包里的三个实用模块
常见的毕业设计部署包结构大概是:源码目录、可视化界面目录、数据集目录、训练好的权重文件、部署教程文档。不要指望它是开箱即用的商业软件,拿到手先要做三件事——确认Python版本兼容性、补装缺失依赖、把权重路径改成你本地绝对路径。
我建议用conda建独立环境,避免把系统Python搞乱。Python版本选3.8-3.10之间,太新版本有些CUDA轮子还没跟上,太老版本Ultralytics新版已经放弃支持。Ultralytics包版本选8.1.x到8.2.x之间即可,新版本改动频繁,接口不稳定。装依赖的命令:
conda create -n escalator_yolo python=3.9 conda activate escalator_yolo pip install ultralytics==8.2.0 opencv-python==4.9.0.80 pillow numpy pandas # 如果要用GPU加速再加torch,CPU版本跑推理也够毕设演示 pip install torch==2.1.0 torchvision==0.16.0 --index-url https://download.pytorch.org/whl/cu118如果手里机器是NVIDIA显卡,务必装CUDA版torch;纯CPU跑一个小视频流大概1-2帧每秒,只够验证流程,现场肯定不够用。装完可以用python -c "import torch; print(torch.cuda.is_available())"验证CUDA是否可用。
3.2 用已有权重跑通第一次推理
部署包里一般带了训练好的best.pt权重文件。先用它跑一张测试图,确认环境没问题:
from ultralytics import YOLO model = YOLO('weights/best.pt') # 路径改成你本地的实际路径 result = model.predict( source='test_samples/frame_001.jpg', conf=0.25, # 置信度阈值,梳齿板场景建议不低于0.3 iou=0.5, save=True, project='runs/detect', name='escalator_test' ) print(result[0].boxes.xyxy) # 打印每个检测框的坐标 print(result[0].boxes.cls) # 打印类别id这里的conf阈值决定灵敏度。梳齿板场景属于低漏检要求——漏掉一根钢丝比误报一次更严重,所以conf建议先降到0.15-0.2试跑,看误报情况再往上调。iou阈值控制重复框合并,0.5是通用值,如果你的数据集里异物紧密相邻(比如一根钢丝断成两截),降到0.4能避免第二个框被合并掉。
首次推理如果报models模块找不到,先检查weights文件是否损坏,用torch.load读一下;如果报NumPy版本冲突,一般是ultralytics版本和numpy不兼容,指定numpy==1.24.4重装即可。
3.3 可视化界面的启动方式和数据流设计
可视化界面通常用PyQt5或Tkinter实现,我见过做得比较合理的版本是:主界面显示摄像头实时画面,右上角有个检测状态灯(正常/预警/急停),底部记录当前帧画面、异物类别、置信度。它的核心逻辑不是画图,而是「读帧-推理-决策」循环。一个可参考的简化版本:
import sys import cv2 from PyQt5.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtCore import QTimer from ultralytics import YOLO class EscalatorMonitor(QWidget): def __init__(self, model_path, video_source): super().__init__() self.model = YOLO(model_path) self.cap = cv2.VideoCapture(video_source) self.label = QLabel(self) layout = QVBoxLayout(self) layout.addWidget(self.label) self.timer = QTimer(self) self.timer.timeout.connect(self.process_frame) self.timer.start(30) # 每30毫秒处理一帧 def process_frame(self): ret, frame = self.cap.read() if not ret: self.timer.stop() return result = self.model.predict(frame, conf=0.2, iou=0.5, verbose=False)[0] plot_frame = result.plot() # ultralytics自带画框功能 rgb = cv2.cvtColor(plot_frame, cv2.COLOR_BGR2RGB) h, w, c = rgb.shape qimg = QImage(rgb.data, w, h, c * w, QImage.Format_RGB888) self.label.setPixmap(QPixmap.fromImage(qimg)) if __name__ == '__main__': app = QApplication(sys.argv) win = EscalatorMonitor('weights/best.pt', 'test_videos/cam01.mp4') win.show() sys.exit(app.exec_())这个界面模板暴露了毕设项目的典型坑——QTimer频率設30毫秒看起来流畅,但如果推理一次要80毫秒,实际帧率只有12,界面会显得卡。常见做法是把推理丢到子线程,主线程只管显示最新结果,避免QTimer回调阻塞UI事件循环。另一个问题是result.plot()会拷贝整个图像,多一倍的RGB内存占用,嵌入式的低内存环境建议改用boxes.xyxy自己画框:
for box, cls in zip(result.boxes.xyxy, result.boxes.cls): x1, y1, x2, y2 = map(int, box.tolist()) cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 0, 255), 2)这个改动对内存占用影响很大。处理1080p视频帧时,result.plot()产生的中间数组约6MB,连续跑一小时会触发几十次内存拷贝,边缘设备扛不住。
4. 训练自己的数据集:从标注到mAP的完整链路
4.1 数据配置文件与训练启动命令
数据准备完毕以后,需要写一个yaml描述数据路径和类别,然后启动训练。数据集目录结构要严格匹配Ultralytics的约定:
dataset/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/对应的yaml文件:
# escalator.yaml path: dataset train: images/train val: images/val nc: 4 names: ['wire', 'bottlecap', 'paper', 'heel']训练启动命令:
yolo detect train \ model=yolov8s.pt \ data=escalator.yaml \ epochs=100 \ imgsz=640 \ batch=8 \ device=0 \ lr0=0.005 \ lrf=0.01 \ project=runs/train \ name=escalator_v1这里的参数含义逐一说清:model=yolov8s.pt表示加载s尺寸的预训练权重,相比从零训练能快3-5倍收敛,而且小目标特征提取更充分;imgsz=640是输入分辨率,不要盲目提到1280,那会让显存爆炸且训练时间翻倍,梳齿板异物在640输入下已经能覆盖到12像素以上;batch=8在8GB显存下刚好,如果你的显卡是16GB可以提到16,梯度更稳定;lr0=0.005是初学率,预训练权重加载后这个值足够,从零训练可以用0.01。
有一个参数容易被忽略——patience。Ultralytics默认早停机制,如果连续50个epoch验证集mAP不提升就自动终止。对于小目标检测,类别样本不均衡时偶尔会卡住5-10个epoch然后突然提升,我习惯把patience设成30,避免错过后期反弹。
4.2 训练过程监控与损失函数曲线解读
训练过程中要用tensorboard看曲线,而不是等训练完再一次性看结果。启动方式:
# 训练时自动生成runs/train/escalator_v1目录 # 新开终端执行tensorboard --logdir runs/train --port 6006重点看三个指标:train/box_loss是否稳定下降,不下降说明学习率过大或数据标注噪声太大;metrics/mAP50和metrics/mAP50-95之间的差距,差距超过0.3说明模型过拟合,需要提升数据增强强度或增加样本量;val/box_loss在训练后期如果开始回升,说明已经过拟合,此时不应等早停,手动Ctrl+C终止训练,取出之前的best.pt。
用Python脚本画损失曲线也是毕设中常见需求:
import pandas as pd import matplotlib.pyplot as plt data = pd.read_csv('runs/train/escalator_v1/results.csv') plt.figure(figsize=(12, 4)) plt.subplot(1, 2, 1) plt.plot(data['epoch'], data['train/box_loss'], label='train box loss') plt.plot(data['epoch'], data['val/box_loss'], label='val box loss') plt.legend() plt.subplot(1, 2, 2) plt.plot(data['epoch'], data['metrics/mAP50(B)'], label='mAP50') plt.legend() plt.savefig('loss_curve.png', dpi=150)一个经验值:mAP50在0.85以上且mAP50-95在0.6以上,这个模型在扶梯场景下是可用的。低于这个数,不要急着加数据,先检查标注框是不是太大、类别是否严重不平衡。我见过一次铁丝标注框把整个梳齿板包进去,模型学出来的检测框永远覆盖半屏,这是标注质量的问题,不是模型容量的问题。
4.3 训练完成后的模型转换与预警接入
训练结束后,best.pt用于Python推理没问题,但如果你想在边缘设备上跑,或者做C++/Java集成,就要导出成onnx或engine格式:
yolo export model=runs/train/escalator_v1/weights/best.pt format=onnx dynamic=False opset=12导出时注意dynamic=False,固定输入尺寸640x640,否则导出后的模型在onnxruntime里动态shape支持不好,容易报错。转换后用onnxruntime验证精度是否与PyTorch一致:
import onnxruntime as ort import numpy as np from ultralytics.utils.ops import non_max_suppression session = ort.InferenceSession('best.onnx') input_name = session.get_inputs()[0].name img = cv2.imread('test_samples/frame_002.jpg') img_resized = cv2.resize(img, (640, 640)) img_input = img_resized[:, :, ::-1].transpose(2, 0, 1)[None] / 255.0 pred = session.run(None, {input_name: img_input.astype(np.float32)})[0] boxes = non_max_suppression(torch.from_numpy(pred), conf_thres=0.2, iou_thres=0.5)这一步很重要,ONNX转换后输出层的形状和排序跟PyTorch原始输出有微妙差异,不经过NMS直接输出的话,框坐标错位是常见现象。导出后再画一次框对比原图,确认坐标没跑偏再接预警逻辑。
5. 避坑指南:从训练翻车到部署踩坑的五个常见问题
5.1 训练Loss一开始就出现NaN
现象:训练启动后前几步loss直接变成NaN,随后loss曲线一条直线。
原因:大概率是标签文件中有负数坐标或者框宽高为0。我在2.2节给了边界裁剪代码,但很多标注工具导出的JSON中points坐标本身就是反的,比如先写右下角再写左上角,导致算出来的宽高是负值。另一种情况是标注的类别ID超过nc数量,和ymaml不一致,这种错误在读取label时不会报错,但训练时损失函数会炸。
解决:写一个数据体检脚本,遍历所有txt标签,检查每行5个数值是否都在合理范围内:
import os for split in ['train', 'val']: label_dir = f'dataset/labels/{split}' for f in os.listdir(label_dir): with open(os.path.join(label_dir, f)) as fh: for line in fh: parts = line.strip().split() if len(parts) != 5: print(f'bad format: {f}: {line}') cls = int(parts[0]) vals = list(map(float, parts[1:])) if cls >= 4 or any(v < 0 or v > 1 for v in vals): print(f'bad value: {f}: {line}')这个脚本务必在训练前跑,能省掉大半天的调试时间。
5.2 扶梯反光把模型搞成「玻璃探测器」
现象:训练后模型把梳齿板旁边的不锈钢立柱、镜面装饰板都识别成了异物,误报率超过40%。
原因:训练样本里正常梳齿板的光照模式单一,模型学到的是「高亮金属区域」这个特征,而不是「异物与齿槽的差异」。这是数据多样性不足的表现,不是模型问题。
解决:从训练集中抽出所有高反光场景的图片,单独看它们的标注框。如果标注框都是贴在反光点旁边,说明标注本身就把反光当成异物了。正确的做法是「正常反光不标注、异物反光才标注」。另外,训练数据的采集尽量覆盖早中晚三个时段、顶灯和侧光两种角度,让模型学会区分反光形状的规律性——梳齿板的反射是规律的条状,异物反射是异常的点状或块状。
5.3 推理速度够快但预警频繁误触发
现象:部署到商场后每10分钟就报警一次,商场运维取消了这个功能。
原因:单帧检测的置信度波动是正常的,一根钢丝在某一帧被识别为wire,下一帧因为梯级震动画面模糊,模型置信度掉了0.1,又识别不出来。更深层的原因是预警逻辑只看单帧,不考虑时间维度的连续性。
解决:预警逻辑改成连续N帧确认机制,要求连续3帧以上且置信度都超过阈值才触发预警。这个方案我用在多个现场,误报率能降低80%:
class AlarmThrottle: def __init__(self, threshold=3, conf=0.25): self.threshold = threshold self.conf = conf self.last_hit = {} self.last_clear = {} def update(self, detections): current = len(detections) > 0 and any(d.conf > self.conf for d in detections) now = time.time() if current: self.last_hit[now] = now # 清理5秒前的记录 self.last_hit = {k: v for k, v in self.last_hit.items() if now - k < 5} return len(self.last_hit) >= self.threshold5.4 PyQt5界面在推理时卡死
现象:点击开始检测后,整个界面无响应,移动窗口变白。
原因:QTimer的回调里做了耗时的模型推理,阻塞了Qt事件循环。推理一次50-100ms,期间Qt无法处理重绘事件,界面自然卡死。
解决:把推理丢进QThread工作线程,主线程通过信号槽接收结果,只负责绘制:
class InferenceWorker(QThread): result_ready = pyqtSignal(object) def __init__(self, model, video_source): super().__init__() self.cap = cv2.VideoCapture(video_source) def run(self): while True: ret, frame = self.cap.read() if not ret: break result = self.model.predict(frame)[0] self.result_ready.emit(result)这是界面编程的经典教训,凡是做了界面的人都应该把耗时操作丢线程,不要嫌麻烦直接塞主线程。
5.5 OpenCV读取网络摄像头延迟严重
现象:使用RTSP协议读取商场监控流,画面延迟3-4秒,完全没法做实时预警。
原因:OpenCV的VideoCapture默认启用内部缓冲队列,连续读帧会不断累积旧帧,导致画面滞后。扶梯场景对延迟敏感,钢丝卡进去到急停的窗口通常只有几秒。
解决:关闭缓冲并手动清帧:
cv2.VideoCapture.set(cv2.CAP_PROP_BUFFERSIZE, 1)注意设置要在open之后,最好在循环里每读一帧就清一次缓冲。另外RTSP传输层改成UDP或TCP都会影响延迟,我一般在rtsp://后面加?tcp参数走TCP,延迟稳定在300-500ms内。
6. 把预警系统从「会跑」做成「能干活」:连续帧判定与后端联动
预警系统最后一个环节是后端联动——识别到异物以后不只是屏幕上亮个红框,需要让现场人员真正接到信号。比较实用的方案有三种:继电器输出控制急停、MQTT消息推送到监控室、HTTP POST写入Web管理系统。毕设阶段做到第二种最合适,既简单又能演示:
import paho.mqtt.client as mqtt client = mqtt.Client() client.connect('192.168.1.100', 1883) def on_detect(detections, frame_id): for det in detections: if det.cls == 0 and det.conf > 0.3: # wire类触发预警 payload = { 'frame_id': frame_id, 'class': 'wire', 'confidence': round(float(det.conf), 3), 'bbox': [int(v) for v in det.xyxy[0].tolist()], 'time': time.strftime('%Y-%m-%d %H:%M:%S') } client.publish('escalator/alarm', json.dumps(payload)) with open('alarm_log.txt', 'a') as f: f.write(json.dumps(payload) + '\n')MQTT的好处是现场多个摄像头可以共用一条报警通道,值班室大屏用一个订阅端就能接收所有扶梯的报警事件。日志落盘是排查故障的关键证据,报警后商场运维去现场确认时要对得上时间戳和画面帧号。
最后再给一个调试技巧:部署后跑一次「正向验证」——拿一根拉直的钢丝放到梳齿板上,观察模型框选是否稳定;再拿一片揉皱的铝箔纸贴在玻璃门上测误报。这两个测试通过以后,系统才真正具备交付条件。我做这类项目习惯先把「最不可能漏检」的类别调到最优再考虑误检,因为漏一次可能砸到人,误报一次只是烦人。希望帮到你。
本文还有配套的精品资源,点击获取