简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕商场扶梯场景下的逆行行为预警展开,基于YOLOv8目标检测模型实现从训练到推理的完整流程,适合作为毕设、课程设计或大作业的参考方案,也便于初学者进阶学习。压缩包共8个文件,包含3个Python脚本、3个模型权重文件与2个说明文本,整体约15.91MB,脚本分别对应可视化界面、模型训练与视频检测,权重文件可直接加载推理,说明文档则提供部署与使用指引。项目已完整跑通,可生成核心指标曲线、混淆矩阵、F1分数曲线、精确率-召回率曲线、验证集预测结果及标签分布图,便于答辩展示与结果分析。目前已有37人学习下载,读者可据此快速复现实验、理解检测流程,并在源码基础上修改扩展,用于其他行为识别任务。
1. 扶梯逆行预警这件事,为什么值得用 YOLOv8 认真做一遍
商场扶梯的逆行行为,说白了就是有人图省事,在向下运行的梯级上往上走,或者反过来。这事看起来只是个人习惯问题,但一旦摔倒,后面的人跟着叠上去,后果不是一句“注意安全”能兜住的。传统做法靠保安盯监控,一个商场几十路摄像头,人眼根本看不过来,漏报是常态。基于 YOLOv8 的扶梯逆行行为预警系统,核心就是用目标检测加轨迹跟踪,自动判断梯级上的人是不是在跟运行方向对着干,一旦确认就触发告警。这套方案适合谁?做毕设或课程设计的学生,需要一套能跑通、有界面、有数据集、部署不折腾的完整项目;也适合想入门视频行为分析的工程师,拿它当第一个端到端落地案例。源码、可视化界面、完整数据集、部署教程都齐了,简单部署即可运行,重点是你得知道每一步在干什么,不然换个场景就翻车。
2. 从检测到预警:YOLOv8 扶梯逆行系统的技术链路拆解
2.1 为什么选 YOLOv8 而不是其他检测器
扶梯场景对检测器的要求很具体:人得框准,尤其是侧身、弯腰、被遮挡的情况;速度得够,商场摄像头通常 25fps,检测加跟踪不能掉到 10fps 以下;模型得轻,最好能在边缘设备上跑,比如 RK3588 或者带 GPU 的工控机。YOLOv8 在这三点上比较均衡。相比 YOLOv5,它的 C2f 模块和解耦头在中小目标上更稳,扶梯上的人远近距离跨度大,这个改进直接受益。相比 RT-DETR 这类 Transformer 检测器,YOLOv8 的推理速度快一截,部署工具链也更成熟,ONNX、TensorRT、OpenVINO 都有现成路径。
我一般会选 yolov8s 或 yolov8m 作为起点。yolov8n 太快但精度掉得明显,扶梯上的人一多就漏;yolov8l 精度好但边缘设备跑不动。yolov8s 在 GTX1660Ti 上能跑到 60fps 以上,在 RK3588 上量化后也能到 15fps 左右,够用。训练参数方面,imgsz 设 640,batch 根据显存调,8G 显存用 16,12G 用 32。epochs 不用一上来就 300,先跑 100 看 mAP 曲线,如果 80 轮后还在涨再加。学习率用默认的 0.01 配合余弦退火,但扶梯数据集通常不大,几千张图,这时候把 lr0 降到 0.001 更稳,不然前期震荡厉害。
数据集这块,标题里说“完整数据集”,但你要清楚它大概率是标注好的扶梯场景图片,可能包含正常行走、逆行、摔倒等类别。拿到手先别急着训练,用 labelme 或者 labelImg 抽查几十张,看框有没有漏标、错标。扶梯逆行这个任务,关键类别其实是“人”和“梯级运行方向”,但梯级方向没法从单帧看出来,所以实际做法是:检测人,跟踪人,再结合扶梯运行方向做逻辑判断。数据集里如果有“逆行”这个行为类别,那多半是视频抽帧后人工标的,训练时要注意帧间泄漏——同一段视频的相邻帧不能同时出现在训练集和验证集,否则 mAP 虚高,上线就翻车。
2.2 逆行判断的逻辑层:从检测框到行为告警
YOLOv8 只负责框人,逆行判断得自己写。常见做法是:用 ByteTrack 或 BoT-SORT 做多目标跟踪,给每个人一个 ID,然后记录每个人在画面中的运动轨迹。扶梯的运行方向是固定的,比如画面里梯级从上往下走,那正常人的 y 坐标应该逐渐增大。如果某个 ID 的 y 坐标在连续 N 帧内持续减小,且位移超过阈值,就判定为逆行。
这里有几个参数必须调。第一是轨迹长度,太短容易误判,比如人原地转身;太长反应慢。我一般设 15 帧,25fps 下约 0.6 秒。第二是位移阈值,跟画面分辨率有关,1080p 下扶梯区域高度大概 600 像素,逆行 0.6 秒位移至少 30 像素才明显,所以阈值设 25 到 35 之间。第三是置信度过滤,YOLOv8 输出的框置信度低于 0.5 的直接丢掉,不参与跟踪,不然 ID 跳变会让你怀疑人生。
import cv2 import numpy as np from ultralytics import YOLO from collections import defaultdict, deque # 加载 YOLOv8 模型,这里用 yolov8s,平衡速度和精度 model = YOLO("yolov8s.pt") # 扶梯区域,根据实际摄像头画面标定,格式 [x1, y1, x2, y2] escalator_roi = [200, 300, 800, 900] # 存储每个跟踪 ID 的最近 y 坐标,用于判断运动方向 track_history = defaultdict(lambda: deque(maxlen=15)) # 逆行判定阈值:连续帧内 y 坐标变化超过该值且方向与扶梯相反 REVERSE_THRESHOLD = 30 cap = cv2.VideoCapture("escalator.mp4") while cap.isOpened(): ret, frame = cap.read() if not ret: break # YOLOv8 检测 + ByteTrack 跟踪,persist=True 保持帧间跟踪状态 results = model.track(frame, persist=True, classes=[0], conf=0.5) if results[0].boxes.id is not None: boxes = results[0].boxes.xyxy.cpu().numpy() ids = results[0].boxes.id.cpu().numpy().astype(int) for box, tid in zip(boxes, ids): x1, y1, x2, y2 = box cx, cy = (x1 + x2) / 2, (y1 + y2) / 2 # 只处理落在扶梯区域内的人 if escalator_roi[0] < cx < escalator_roi[2] and escalator_roi[1] < cy < escalator_roi[3]: track_history[tid].append(cy) # 轨迹长度足够才判断 if len(track_history[tid]) == 15: dy = track_history[tid][-1] - track_history[tid][0] # 假设扶梯向下运行,正常 y 增大,dy 为负且绝对值大则逆行 if dy < -REVERSE_THRESHOLD: cv2.putText(frame, "REVERSE!", (int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2) # 这里可以触发告警:写日志、推流、发消息 cv2.imshow("Escalator Monitor", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()这段代码的逻辑很直白:YOLOv8 负责检测人,ByteTrack 负责给每个人分配 ID,track_history 记录每个 ID 最近 15 帧的 y 坐标中心。如果 15 帧内 y 坐标减少了超过 30 像素,就认为这个人在往上走,而扶梯是向下的,判定逆行。参数方面,conf=0.5 是置信度阈值,调低会引入更多误检,调高会漏人;classes=[0] 表示只检测人这一类,COCO 数据集里人的类别 ID 是 0;persist=True 很关键,不加的话每帧跟踪状态重置,ID 会乱跳。REVERSE_THRESHOLD 需要根据你的摄像头角度和分辨率微调,建议先在测试视频上跑一遍,把每个人的 dy 值打印出来看分布,再定阈值。
2.3 可视化界面怎么搭才不鸡肋
标题里提到可视化界面,很多人用 PyQt 或者 Gradio 糊一个,能显示视频就行。但实际用起来,界面得解决三个问题:实时看、回看告警、调参数。我一般用 PyQt5 加 OpenCV 做,左边显示实时画面,右边列出最近告警记录,底部放几个滑块调置信度和逆行阈值。这样演示的时候不用改代码,直接拖滑块就能展示效果,毕设答辩很加分。
界面刷新用 QTimer,每 30ms 读一帧,跟视频帧率匹配。告警记录存到一个 deque 里,最多留 100 条,每条包含时间戳、跟踪 ID、画面截图路径。截图用 cv2.imwrite 存到本地,命名用时间戳加 ID,避免覆盖。参数调节滑块绑定回调函数,实时更新全局变量。注意 PyQt 和 OpenCV 的线程问题,视频读取和界面刷新最好放同一个线程,不然容易卡死。如果要用多线程,YOLO 推理放子线程,通过信号槽把结果传回主线程更新界面,但这样代码复杂度上去了,毕设级别单线程够用。
from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QSlider, QVBoxLayout, QWidget from PyQt5.QtCore import QTimer, Qt from PyQt5.QtGui import QImage, QPixmap import cv2 import sys class MonitorWindow(QMainWindow): def __init__(self): super().__init__() self.conf_threshold = 0.5 self.cap = cv2.VideoCapture("escalator.mp4") self.model = YOLO("yolov8s.pt") # 界面布局:视频显示标签 + 置信度滑块 self.video_label = QLabel() self.slider = QSlider(Qt.Horizontal) self.slider.setRange(10, 90) self.slider.setValue(50) self.slider.valueChanged.connect(self.update_conf) layout = QVBoxLayout() layout.addWidget(self.video_label) layout.addWidget(self.slider) container = QWidget() container.setLayout(layout) self.setCentralWidget(container) # 定时器每 30ms 刷新一帧 self.timer = QTimer() self.timer.timeout.connect(self.update_frame) self.timer.start(30) def update_conf(self, value): # 滑块值 10-90 映射到 0.1-0.9 self.conf_threshold = value / 100.0 def update_frame(self): ret, frame = self.cap.read() if not ret: return results = self.model(frame, conf=self.conf_threshold, classes=[0]) annotated = results[0].plot() rgb = cv2.cvtColor(annotated, cv2.COLOR_BGR2RGB) h, w, ch = rgb.shape qimg = QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) self.video_label.setPixmap(QPixmap.fromImage(qimg)) def closeEvent(self, event): self.cap.release() app = QApplication(sys.argv) window = MonitorWindow() window.show() sys.exit(app.exec_())这个界面代码只做了最核心的两件事:显示带检测框的视频,用滑块调置信度。滑块值除以 100 得到 0.1 到 0.9 的置信度,传给 YOLO 的 conf 参数。QTimer 每 30ms 触发一次 update_frame,读一帧、推理、画框、转 QImage、显示。注意 QImage 构造时 data 必须保持有效,这里 rgb 是局部变量,但 setPixmap 会拷贝数据,所以没问题。实际项目里还要加告警列表和截图保存,逻辑类似,不展开。
3. 训练自己的扶梯数据集:标注、转换与参数设置
3.1 用 labelme 标注扶梯场景的四个注意点
扶梯场景标注跟普通行人检测不太一样。第一,扶梯上的人经常被扶手带遮挡,只露出上半身,这时候框要标到可见部分,不要脑补下半身,不然模型学出来框会飘。第二,梯级本身有纹理,容易跟人的衣服混淆,标注时把梯级区域也框出来作为背景类,能降低误检。第三,逆行的人往往侧身或背对镜头,姿态跟正常行走差异大,标注时确保每个 ID 在连续帧里都标到,不要跳帧,否则跟踪训练会断。第四,如果要做行为分类,除了框人,还得给每个人打上“正常”或“逆行”标签,这个标签是帧级的,不是框级的,存成单独的 json 文件。
labelme 标注完会生成 json,每个 json 对应一张图。转 YOLO 格式需要提取 shapes 里的 label 和 points,归一化成 cx cy w h。注意 labelme 的 points 是左上角和右下角,YOLO 要的是中心点和宽高,转换公式:cx = (x1+x2)/2/w,cy = (y1+y2)/2/h,bw = (x2-x1)/w,bh = (y2-y1)/h。类别名映射成数字 ID,存成 classes.txt,顺序要和训练时的 data.yaml 一致。
import json import os from pathlib import Path # labelme json 转 YOLO txt def labelme_to_yolo(json_dir, output_dir, class_map): json_dir = Path(json_dir) output_dir = Path(output_dir) output_dir.mkdir(parents=True, exist_ok=True) for json_file in json_dir.glob("*.json"): with open(json_file, "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"] x1, y1 = points[0] x2, y2 = points[1] # 归一化并转成 YOLO 格式 cx = (x1 + x2) / 2 / img_w cy = (y1 + y2) / 2 / img_h bw = abs(x2 - x1) / img_w bh = abs(y2 - y1) / img_h lines.append(f"{class_map[label]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") txt_file = output_dir / (json_file.stem + ".txt") with open(txt_file, "w") as f: f.write("\n".join(lines)) # 类别映射:0 是人,1 是梯级背景 class_map = {"person": 0, "escalator_step": 1} labelme_to_yolo("labels_json", "labels_yolo", class_map)转换脚本的核心是归一化,cx cy bw bh 都是相对于图片宽高的比例,范围 0 到 1。class_map 里 person 映射到 0,escalator_step 映射到 1,顺序必须和 data.yaml 里的 names 一致。如果标注时用了中文标签,这里要改成对应的英文,不然训练时报错。转换完抽查几个 txt,用 cv2 画出来跟原图对比,确认框的位置和大小没错。
3.2 data.yaml 与训练命令的完整参数
data.yaml 是 YOLOv8 训练的数据配置文件,路径、类别数、类别名都在里面。路径最好用绝对路径,相对路径容易因为工作目录变化找不到文件。train 和 val 分别指向训练集和验证集的图片目录,YOLO 会自动去找同名的 txt 标签。nc 是类别数,names 是类别名列表,顺序和转换时的 class_map 一致。
# data.yaml path: /home/user/escalator_dataset train: images/train val: images/val nc: 2 names: 0: person 1: escalator_step训练命令用 yolo 命令行或者 Python 脚本都行。命令行更简洁,适合快速跑。参数里 imgsz=640 是输入尺寸,epochs=100 先跑一轮看效果,batch=16 根据显存调,workers=4 是数据加载线程数,Windows 下设 0 避免多进程问题。device=0 指定第一块 GPU,没 GPU 就设 cpu,但速度会慢很多。patience=20 表示 20 轮 mAP 不涨就早停,省时间。save_period=10 每 10 轮存一次权重,防止训练中断丢进度。
yolo detect train \ data=data.yaml \ model=yolov8s.pt \ imgsz=640 \ epochs=100 \ batch=16 \ workers=4 \ device=0 \ patience=20 \ save_period=10 \ project=escalator_runs \ name=exp1跑完训练后,runs/detect/escalator_runs/exp1 目录下有 weights/best.pt 和 last.pt,还有 results.csv 和混淆矩阵。results.csv 里记录了每轮的 box_loss、cls_loss、mAP50、mAP50-95,用 pandas 读出来画曲线,能直观看到有没有过拟合。如果 mAP50 在 80 轮后还在涨,把 patience 调大继续跑;如果训练 loss 降但验证 loss 涨,说明过拟合,加数据增强或者减模型复杂度。扶梯数据集通常几千张,yolov8s 跑 100 轮大概半小时到一小时,取决于 GPU。
4. 部署上线的避坑清单:从训练到推理的五个翻车点
4.1 坑一:验证集 mAP 很高,上线后误报不断
现象:训练完 mAP50 到 0.95,信心满满部署到商场,结果正常行走的人也被标成逆行,保安说这系统没法用。原因:验证集和训练集来自同一段视频的相邻帧,模型记住了背景和人的外观,换一个摄像头、换一批人就不行了。解决:划分数据集时按视频段分,同一段视频的帧只能出现在训练集或验证集之一,不能混。另外验证集里要包含不同时段、不同光照、不同摄像头角度的样本,最好留一个完全没见过的摄像头做测试集。
4.2 坑二:ByteTrack 的 ID 跳变导致轨迹断裂
现象:同一个人跟踪到一半 ID 变了,track_history 里两个 ID 各有一段轨迹,都不到 15 帧,逆行判断失效。原因:遮挡、检测框抖动、置信度波动都会导致跟踪器分配新 ID。解决:调 ByteTrack 的 track_high_thresh 和 track_low_thresh,默认 0.5 和 0.1,扶梯场景可以降到 0.4 和 0.05,让低置信度框也参与匹配。另外开启 track_buffer,默认 30 帧,可以加到 60,给遮挡后重新匹配留更多时间。如果还不行,换 BoT-SORT,它对相机运动补偿更好,但速度稍慢。
4.3 坑三:逆行阈值设死,换摄像头就失效
现象:在测试视频上调好的 REVERSE_THRESHOLD=30,换到另一个摄像头,要么不报警,要么乱报警。原因:不同摄像头的安装角度、焦距、分辨率不同,人在画面里的像素位移速度不一样。解决:不要用固定像素阈值,改成相对值。比如计算扶梯区域的高度 H,阈值设为 H 的 5%,或者用人在画面中的框高作为参考,位移超过框高的 1.5 倍才判定。更稳的做法是标定扶梯运行速度,用光流法估计梯级运动,人的运动方向跟梯级方向夹角超过 90 度才判逆行。
4.4 坑四:PyQt 界面卡死,视频延迟越跑越大
现象:界面跑几分钟后卡住,视频画面延迟好几秒,点滑块没反应。原因:YOLO 推理和界面刷新在同一个线程,推理耗时波动时 QTimer 事件堆积,消息队列堵死。解决:把推理放子线程,用 QThread 或 Python threading,通过信号槽把检测结果传回主线程更新界面。子线程里读帧、推理、画框,主线程只负责显示。注意 OpenCV 的 VideoCapture 不要在多个线程里同时读写,加锁或者只在子线程里操作。
4.5 坑五:RK3588 部署时模型转换后精度掉一半
现象:PyTorch 模型在 PC 上 mAP50 0.92,转成 ONNX 再转 RKNN 放到 RK3588 上,mAP50 掉到 0.6,漏检严重。原因:RKNN 量化时用了默认的 uint8 量化,扶梯场景光照变化大,量化误差累积。解决:量化时用混合量化,对第一层和最后一层保持 float16,中间层用 uint8。另外校准集要覆盖不同光照和场景,至少 200 张图,不要随便拿几十张凑数。转换脚本里 mean_values 和 std_values 要跟训练时一致,YOLOv8 默认是 0 到 1 归一化,mean 设 0,0,0,std 设 255,255,255。
5. 把预警系统跑稳的三个进阶习惯
第一个习惯:用视频抽帧做回归测试。每次改完代码,不要只跑一张图看效果,把测试视频抽成 100 帧,批量跑一遍,统计误报和漏报数量。我一般写个脚本,读视频、逐帧推理、记录告警次数,跟人工标注的 ground truth 对比,算准确率和召回率。这样改参数心里有数,不会凭感觉调。
第二个习惯:告警日志要带上下文。只记“某帧某 ID 逆行”没用,要把前后 30 帧的截图存下来,按时间戳和 ID 建文件夹。出问题回看的时候,能直接看到人是怎么走的,是误报还是漏报一目了然。日志格式用 JSON,字段包括 timestamp、track_id、bbox、dy、confidence,方便后续分析。
第三个习惯:模型更新用增量训练。商场场景会变,比如换了地砖、加了广告牌,旧模型可能误检。不要每次从头训,用旧权重做预训练,新数据跑 20 到 30 轮,学习率降到 0.0001,既能适应新场景,又不会遗忘旧知识。增量训练的数据要包含旧场景的样本,比例大概 1:1,不然会灾难性遗忘。
| 进阶技巧 | 关键参数 | 适用场景 |
|---|---|---|
| 视频抽帧回归测试 | 抽帧间隔 5,统计误报/漏报 | 每次代码或参数变更后 |
| 告警日志带上下文 | 前后 30 帧截图,JSON 格式 | 排查误报、复盘漏报 |
| 增量训练 | lr0=0.0001,epochs=30,新旧数据 1:1 | 场景变化、模型迭代 |
最后说个我自己的教训:别一上来就追求高 mAP,先把推理链路跑通,用默认的 yolov8s.pt 加 ByteTrack 跑一遍测试视频,看看告警逻辑对不对。很多时候问题不在模型,在轨迹判断和阈值设置。把逻辑调稳了,再换更大的模型或者调训练参数,事半功倍。希望帮到你。
本文还有配套的精品资源,点击获取