PySide6 + YOLOv8 摔倒检测实战:从模型训练到桌面应用

发布时间:2026/9/3 18:13:53
PySide6 + YOLOv8 摔倒检测实战:从模型训练到桌面应用 摔倒检测在很多人眼里就是一个“目标检测”问题拿个 YOLO 模型检测到 person 就完事了。但这句话里其实藏着两个坑。第一个坑YOLO 的 COCO 预训练权重里根本没有“摔倒”这个类别。你拿yolov8n.pt直接去识别最多输出一个人框。人躺着、蹲着、摔倒它都只会告诉你“这里有个 person”。摔倒检测之所以需要针对性训练不是因为目标检测不好用而是因为“摔倒”这个语义通用模型没有专门学过。第二个坑就算你训练了一个能识别fall的模型离“能用”还差一个工程壳。实际交付给用户的是一个桌面系统能打开摄像头、能看画面、能弹告警、能保存截图。这一层做不好模型再准也落不了地。本文就把这条完整链路拆开讲清楚如何用 YOLOv8 / YOLOv5 训练摔倒检测模型如何用 PySide6 搭建桌面界面如何把两者安全地集成到一个不会卡死的线程模型里。文章以 YOLOv8 为主要示例因为 ultralytics 的 API 足够简洁训练、验证、导出一次到位同时我会在关键位置说明 YOLOv5 的对应做法方便维护旧项目的读者。读完你应该能照着搭出一套可演示、可扩展的摔倒检测系统而不是只在终端里看到一行坐标。1. 这篇文章真正要解决的问题很多人一开始会把摔倒检测理解成单纯的“目标检测任务”也就是找到图像里的人框出来。但真实场景下这个理解是不够的。摔倒检测真正的难点在于两层。第一层是语义层面。模型需要区分“正常站立的人”“弯腰的人”“蹲下的人”“已经跌倒躺地的人”。这些状态在视觉上经常非常接近单张图像上就连人眼都可能误判。如果只用二分类“人”和“摔倒”模型很容易把弯腰捡东西、蹲下系鞋带也识别成摔倒。所以数据标注时要尽量把容易混淆的状态都覆盖进去甚至单独建一个“其他非站立状态”的副类别来辅助模型收敛。第二层是工程层面。训练好的模型只是输出坐标和类别真正要做成一个“系统”还需要完成视频流读取、帧率控制、结果叠加显示、连续帧判断、告警输出、截图保存这些工作。这些功能如果全堆在 OpenCV 的imshow窗口里你会发现界面简陋、交互混乱、多线程一加就卡死。这也是为什么很多项目最终都会选择 Qt 系方案。所以这篇文章并不只讲“YOLOv8 怎么训练”而是讲“从数据集到桌面系统的完整落地路径”。对纯算法同学来说你缺的是 GUI 和线程集成经验对做软件的同学来说你缺的是训练数据和模型部署的流程。这篇文章把两边都覆盖到。读完你会得到三样东西一个可复用的摔倒检测训练流程一个用 PySide6 写的、可扩展的桌面界面框架一套在多线程场景下不卡界面、不乱告警的集成思路。2. YOLOv8 与 YOLOv5 选型核心概念与对比YOLOv5 和 YOLOv8 都是 Ultralytics 团队维护的目标检测框架但它们的发布阶段和技术底座不同。YOLOv5 是 2020 年推出的系列最大的优势是稳定和资料多。你可以找到大量老教程、部署案例和公司内部项目是基于 YOLOv5 的。它的网络主干使用 CSPDarknet其中核心模块是C3检测头是 anchor-based 的方式。如果你要维护一个已经跑通的 YOLOv5 工程或者你的硬件设备上已经固化了 YOLOv5 的部署 SDK那么继续用 YOLOv5 完全没有问题。YOLOv8 是 2023 年推出的新一代目标检测框架。它做了几个关键改动主干中的C3换成了C2f结构检测头改成了 Anchor-Free并且将分类和回归分支解耦。这些改动的直接收益是训练收敛更稳定、对小目标更友好、导出部署时更灵活。同时 Ultralytics 的统一 API 让train / val / predict / export一条命令完成对新手非常友好。下面这张表可以帮助你快速做选型判断维度YOLOv5YOLOv8网络主干CSPDarknet C3改良版 CSPDarknet C2f检测头Anchor-BasedAnchor-Free 解耦头训练 API仓库脚本 train.pyultralytics 统一 CLI/Python API部署生态ONNX / TensorRT 资料极多ONNX / TensorRT / NCNN 均支持适合场景老项目维护、公司既有SDK新项目、快速原型、弱化调参成本上手难度需要进仓库改配置一条命令训练配置简单还有一个现实问题如果你手里是 GTX 1660 Ti 这类 6GB 显存的显卡训练时建议从 YOLOv8n 或 YOLOv8s 开始。直接上 x 或 l 很容易出现显存不足训练速度也会让你失去耐心。我的判断很简单没有历史包袱的新项目优先选 YOLOv8如果你只是想在现有 YOLOv5 工程里加一个摔倒类别那继续用 YOLOv5本文后面的训练思路是完全通用的。3. 摔倒检测系统的整体架构设计在写代码之前先想清楚整个系统的模块边界。摔倒检测系统可以拆成 5 层。第一层是输入层负责获取视频流来源可能是 USB 摄像头、RTSP 网络摄像头或本地视频文件。这一层用 OpenCV 统一处理因为cv2.VideoCapture对这三种来源都支持。第二层是模型层使用训练好的 YOLOv8 或 YOLOv5 权重对每一帧图像做目标检测输出目标的类别、置信度、边界框坐标。第三层是逻辑层这一层最关键。YOLO 模型输出的fall只是一个“单帧预测”并不代表系统应该立刻告警。逻辑层需要拿到连续多帧的结果判断摔倒状态是否稳定成立避免弯腰、蹲下这类短暂动作造成误报。第四层是展示层使用 PySide6 显示实时画面并在画面中绘制检测框、类别标签和置信度。这一层负责与用户交互打开摄像头、选择视频文件、启动/停止检测。第五层是告警层当逻辑层判断摔倒事件成立后界面切换状态颜色、保存当前帧截图、写入日志文件并预留推送通知和声音告警的扩展口。数据流是这样的摄像头帧先被读取线程拿到通过信号槽交给推理线程推理线程调用 YOLO 模型得到检测框然后送入摔倒判定逻辑判定结果连同画框后的图像一起发回主线程主线程更新界面并决定是否告警。这里我特意把“读取线程”和“推理线程”分开描述。实际代码中两者可以在同一个QThread里顺序执行也就是“读一帧推理一帧”这样实现简单、帧率稳定。真正高并发场景才需要把读帧和推理拆成两个线程中间加队列。对于本文的摔倒检测系统推荐先用单线程方案避免过度设计后面再根据性能需求优化。4. 环境准备与数据集制作4.1 安装依赖建议使用 conda 创建独立虚拟环境避免污染其他项目。conda create -n fall python3.8 -y conda activate fall # PyTorch 请参考 PyTorch 官方安装命令选择与你的 CUDA 版本匹配的版本 pip install torch torchvision # 目标检测框架、GUI 和图像处理库 pip install ultralytics PySide6 opencv-python numpy如果你的机器只有 CPUtorch的 CPU 版本也能跑通全流程只是训练和推理速度慢一些。如果使用 GPU请先运行nvidia-smi确认显卡驱动和 CUDA 版本再安装对应版本的 PyTorch。PySide6 是 Qt for Python 的官方绑定跨平台、控件丰富和 OpenCV 配合显示视频流非常合适。这里不推荐 Tkinter因为做视频实时显示和复杂布局时Tkinter 的刷新效率和控件风格都会变成瓶颈。4.2 数据集结构YOLO 训练需要的数据集目录一般长这样fall_dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── fall.yamlimages里是图片文件labels里是对应的 YOLO 格式 txt 标注文件。每个 txt 标注文件与图片同名每一行含义是类别id 归一化中心x 归一化中心y 归一化宽 归一化高例如一张 640x640 的图片中一个摔倒目标的框位于中心 (320, 400)宽 200高 300那么标注是1 0.5 0.625 0.3125 0.468754.3 类别设计与标注建议很多新手只标注fall一个类别这是错误做法。模型如果只见过摔倒的人没见过站立、行走、弯腰的人它会把很多非摔倒目标都误检为摔倒。推荐至少标注两个类别0: person 1: fallperson类用来让模型认识“正常状态”fall类用来让模型认识“摔倒状态”。这样模型才会在两种状态之间形成区分能力。标注工具可以用 LabelImg 或 X-AnyLabeling。LabelImg 可以直接输出 YOLO 格式最省事。X-AnyLabeling 更适合多段视频抽帧后快速标注。标注时有几个经验可以复用从实际摄像头视角录一段视频再抽帧标注不要全用网上搜来的图片。因为摄像头安装高度和角度决定了摔倒目标的形态。连续帧尽量都标注不要只标注“最像摔倒”的中间帧。这样模型能学到摔倒过程的变化。同一个摔倒动作多标注几个角度正面摔倒、侧面摔倒、背对摄像头摔倒。如果你发现模型总把“弯腰捡东西”识别成摔倒就在数据里补充这类容易混淆的图片并确保它们被标成person或单独加一个bend类别。4.4 数据划分训练集和验证集按 9:1 或 8:2 划分。如果数据量很小不要随意从同一段连续视频里抽帧后随机划分否则训练集和验证集会高度相似验证指标虚高。建议按视频片段划分同一段视频的帧全部进训练集或全部进验证集这样更能反映真实场景。5. 模型训练与导出YOLOv8 / YOLOv5 实战5.1 配置文件在数据集根目录创建fall.yaml# 文件路径fall_dataset/fall.yaml path: /your/absolute/path/fall_dataset train: images/train val: images/val names: 0: person 1: fall注意path建议写绝对路径避免命令行在不同目录下运行时报找不到数据集的错误。5.2 YOLOv8 训练命令yolo detect train \ datafall.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ device0参数含义modelyolov8n.pt用 COCO 预训练权重作为起点迁移学习能让模型更快收敛。如果你的类别和 COCO 完全不同也可以从yolov8n.yaml随机初始化训练但需要更多数据。epochs100摔倒检测属于相对简单的类别区分任务一般 100 个 epoch 足够看到效果。batch16根据显存调整。6GB 显存建议 8 或 16跑不起来就降到 4。device0使用第一张 GPU。CPU 训练可以改成devicecpu。训练过程会输出每一轮的box_loss、cls_loss、dfl_loss以及验证集的 precision、recall、mAP50、mAP50-95。最终权重保存在runs/detect/train/weights/下best.pt是验证集效果最好的模型last.pt是最后一轮的模型。5.3 YOLOv5 的对应做法如果你使用的是 YOLOv5 仓库流程类似只是训练入口不同# 进入 YOLOv5 仓库目录后执行 python train.py \ --data fall.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100YOLOv5 的配置文件中类别名写法基本一致只是names字段索引从 0 开始。训练好的权重同样在runs/train/exp下。5.4 模型验证与导出训练完成后先用验证集评估模型yolo detect val modelruns/detect/train/weights/best.pt datafall.yaml关注两个指标mAP50IoU 阈值 0.5 下的平均精度反映模型定位是否准确。mAP50-95更严格的多阈值平均精度反映模型整体质量。当 mAP50 能稳定到 0.85 以上摔倒检测基本可用。如果 mAP50 很低优先检查数据标注是否错标、漏标而不是盲目加训练轮数。导出 ONNX 格式用于部署yolo export modelruns/detect/train/weights/best.pt formatonnx opset12 simplifyTrue导出成功后你可以在推理时改用 ONNX Runtime 加载速度更快而且不依赖 PyTorch 环境。5.5 用训练好的模型做单张图片预测yolo predict modelruns/detect/train/weights/best.pt sourcetest.jpg conf0.4这一步可以快速检查模型在真实图片上的表现确认确实能识别人和摔倒状态再进入 GUI 集成。6. PySide6 桌面界面与视频流实现6.1 为什么用 PySide6PySide6 是 Qt6 的官方 Python 绑定信号槽机制非常适合多线程 GUI。视频流实时显示如果用 OpenCV 的imshow它会在独立窗口弹出布局和交互都很难做而 PySide6 可以将每一帧QImage转成QPixmap显示在QLabel上天然适合做产品化的监控界面。6.2 主窗口代码下面是一个最小可运行的主窗口包含视频显示区域、模型路径输入框和控制按钮。# 文件路径main_window.py import sys from PySide6.QtWidgets import ( QApplication, QMainWindow, QWidget, QLabel, QPushButton, QVBoxLayout, QHBoxLayout, QLineEdit, QMessageBox ) from PySide6.QtCore import Qt from PySide6.QtGui import QImage, QPixmap class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(摔倒检测系统) self.setMinimumSize(960, 640) # 视频显示区 self.video_label QLabel(等待视频输入) self.video_label.setAlignment(Qt.AlignCenter) self.video_label.setStyleSheet( background-color: #1e1e1e; color: #cccccc; font-size: 20px; ) # 模型路径输入 self.model_path_edit QLineEdit() self.model_path_edit.setPlaceholderText(填写模型权重路径例如 weights/fall_best.pt) # 控制按钮 self.btn_open_camera QPushButton(打开摄像头) self.btn_open_file QPushButton(选择视频文件) self.btn_start QPushButton(启动检测) self.btn_stop QPushButton(停止) # 布局 control_layout QHBoxLayout() control_layout.addWidget(self.model_path_edit) control_layout.addWidget(self.btn_open_camera) control_layout.addWidget(self.btn_open_file) control_layout.addWidget(self.btn_start) control_layout.addWidget(self.btn_stop) main_layout QVBoxLayout() main_layout.addWidget(self.video_label) main_layout.addLayout(control_layout) container QWidget() container.setLayout(main_layout) self.setCentralWidget(container) # 按钮事件 self.btn_start.clicked.connect(self.start_detection) def start_detection(self): # 这里用到 QLineEdit 的判空检查 model_path self.model_path_edit.text().strip() if not model_path: QMessageBox.warning(self, 提示, 请先填写模型权重路径) return print(加载模型:, model_path) # 后续在这里启动推理线程 if __name__ __main__: app QApplication(sys.argv) window MainWindow() window.show() sys.exit(app.exec())这里特别处理了一个新手很容易踩的坑QLineEdit的text()拿到用户输入后必须用strip()去掉首尾空格再判断是否为空。很多人在界面上填了路径但程序说找不到文件就是因为路径前后带了空字符。6.3 视频读取线程视频读取不能放在主线程否则拖动窗口时画面会卡死。用一个QThread专门读帧并通过信号把QImage发回主线程。# 文件路径video_thread.py import cv2 from PySide6.QtCore import QThread, Signal from PySide6.QtGui import QImage class VideoThread(QThread): frame_ready Signal(QImage) def __init__(self, source0, parentNone): super().__init__(parent) self.source source # 0 表示默认摄像头也可以是视频文件路径 self._running True def stop(self): self._running False def run(self): cap cv2.VideoCapture(self.source) if not cap.isOpened(): print(无法打开视频源:, self.source) return while self._running: ret, frame cap.read() if not ret: break # OpenCV 默认 BGRQt 显示需要 RGB rgb_frame cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch rgb_frame.shape bytes_per_line ch * w # 必须调用 copy()否则 QImage 引用的临时数据被释放后会出现花屏 qimage QImage( rgb_frame.data, w, h, bytes_per_line, QImage.Format_RGB888 ).copy() self.frame_ready.emit(qimage) cap.release()QImage的copy()这一步非常关键。如果省略QImage只是浅引用rgb_frame.data当循环进入下一帧、数组内存被复用后界面上的图像会变成乱码。7. 系统集成推理线程、信号槽与告警逻辑7.1 线程模型一个较合理的桌面端摔倒检测系统可以只用一个后台线程线程内部完成“读帧 YOLO 推理 摔倒判定 画框”然后把结果通过信号发回主线程。这样界面不会卡逻辑也不复杂。如果你后续要做高帧率多路视频再把“读帧”和“推理”拆成两个线程中间加队列。下面给出的是单工作线程方案代码更容易理解。7.2 推理线程代码# 文件路径inference_thread.py import cv2 import datetime from PySide6.QtCore import QThread, Signal from PySide6.QtGui import QImage from ultralytics import YOLO class InferenceThread(QThread): image_ready Signal(QImage) # 画框后的图像 result_ready Signal(dict) # 检测结果 fall_alert Signal(str) # 摔倒告警消息 def __init__(self, model_path, video_source0, parentNone): super().__init__(parent) self.model YOLO(model_path) self.video_source video_source self._running True self.conf_threshold 0.4 self.last_alert_time None self.alert_cooldown 30 # 秒防止告警刷屏 def stop(self): self._running False def run(self): cap cv2.VideoCapture(self.video_source) if not cap.isOpened(): print(无法打开视频源:, self.video_source) return judger FallJudger(need_frames5, aspect_ratio_threshold0.8) while self._running: ret, frame cap.read() if not ret: break results self.model.predict( frame, confself.conf_threshold, imgsz640, verboseFalse ) result results[0] boxes result.boxes cls_ids boxes.cls.tolist() confs boxes.conf.tolist() xyxy boxes.xyxy.tolist() names result.names # 在原始帧上绘制检测框 for box, cls_id, conf in zip(xyxy, cls_ids, confs): x1, y1, x2, y2 map(int, box) label f{names[int(cls_id)]} {conf:.2f} # fall 类别用红色person 用白色 color (0, 0, 255) if int(cls_id) 1 else (255, 255, 255) cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2) cv2.putText( frame, label, (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2 ) # 摔倒判定 if judger.update(xyxy, cls_ids): now datetime.datetime.now() if self._can_alert(now): alert_file self._save_alert_frame(frame, now) self.fall_alert.emit(f检测到摔倒截图已保存{alert_file}) # 转换为 QImage 发回主线程 rgb_frame cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch rgb_frame.shape qimage QImage( rgb_frame.data, w, h, ch * w, QImage.Format_RGB888 ).copy() self.image_ready.emit(qimage) self.result_ready.emit({ boxes: xyxy, cls_ids: cls_ids, confs: confs }) cap.release() def _can_alert(self, now): if self.last_alert_time is None: self.last_alert_time now return True delta (now - self.last_alert_time).total_seconds() if delta self.alert_cooldown: self.last_alert_time now return True return False def _save_alert_frame(self, frame, now): import os os.makedirs(alerts, exist_okTrue) filename falerts/fall_{now:%Y%m%d_%H%M%S}.jpg cv2.imwrite(filename, frame) return filename这段代码有几个点值得说明。第一FallJudger负责连续帧判断。单帧检测到fall并不会触发告警必须连续多帧都满足条件才能避免弯腰、蹲下这类瞬时误报。第二告警有冷却时间alert_cooldown否则一个躺着的人会持续触发告警刷屏。30 秒只是一个示例值实际项目中你可以根据场景调整。第三模型推理和告警截图