摔倒检测系统,尤其是基于YOLOv8/YOLOv5加PySide6做桌面端工具的项目,这两年问的人很多。这类系统核心解决的问题很明确:从视频或摄像头画面中识别出人是否摔倒,并通过界面报警或记录。适合谁看?如果你正在做安全监控、老人看护、社区养老、医院走廊这类场景的演示或落地项目,这篇内容可以帮你把数据集、训练、模型导出、界面集成和常见报错串起来。最值得关注的不是YOLO本身的精度,而是从检测模型到桌面应用之间那一段工程链路:模型怎么导出,界面怎么调用,报警逻辑怎么做,误报怎么压下去。
下面按实际落地顺序拆一遍,不会先堆功能列表。项目本身不算复杂,但很多人会卡在“模型能跑”和“桌面应用能稳定用”之间的衔接上。我会把环境、训练、导出、PySide6 集成、测试、排查这些环节逐个说清楚。
1. 先搞清楚摔倒检测系统到底要做什么
1.1 摔倒检测是行为判断,不是单纯目标检测
很多人一上来就想着训练一个 YOLO 模型,检测到“fall”这个类别就报警。这个思路能快速出 Demo,但和真正可用的摔倒检测系统还有距离。YOLO 是目标检测模型,它擅长在单张图里找出物体位置和类别。摔倒本身却是一个行为过程,包含时间维度:一个人从站立变成倒地,倒地后可能静止,也可能有挣扎动作。
如果只靠单帧分类,会出现很多误报。比如一个人蹲下来系鞋带、躺在沙发上休息、侧卧睡觉,在单帧画面上都很像摔倒。再加上监控摄像头视角不同,同一个动作从侧面看和从顶上看,画面上的人体框比例完全不同。
所以做这个项目之前,要先想清楚采用哪种方案:
- 方案一:检测“人”类别,再用人体框宽高比、关键点角度、连续帧变化来判断是否摔倒。
- 方案二:在数据集里单独标一个“fall”类别,检测到该类别后,再加连续帧确认和冷却时间去报警。
- 方案三:用 YOLOv8-pose 做人脸/人体关键点检测,通过头部、髋部、脚踝的位置和角度计算摔倒概率。
从工程复杂度看,方案一最简单,但逻辑要写不少;方案二训练起来直观,但数据标注质量很影响结果;方案三更接近人体姿态分析,效果上限更高,但后处理也更复杂。我建议先用方案二把 Demo 跑通,再逐步加上关键点判断去降误报。
1.2 功能边界和报警规则要先定义清楚
一个完整的 PySide6 桌面摔倒检测系统,通常不只是“画面里画个框”。实际要做的还包括:
- 选择视频文件或摄像头索引。
- 实时显示检测画面。
- 检测到摔倒后报警,比如弹出提示、播放声音、保存当前截图。
- 记录报警时间、置信度、截图路径。
- 支持手动设置置信度阈值和报警冷却时间。
这些功能都要在写代码前定义清楚,否则会在开发过程中反复改界面和逻辑。比如“摔倒后要不要自动保存录像片段”这个问题,如果一开始没想好,后面加录像功能会比加截图麻烦很多。
还要明确场景。如果是一个房间里的单人看护场景,逻辑可以简单一些;如果是走廊、大厅这种多人场景,就需要跟踪同一目标,避免同一事件反复报警。第一次做这个系统,不要想着把多人跟踪、跨摄像头、长时间录像全部塞进去,先把单路视频、单人摔倒识别跑稳定。
2. 从数据集到标注:决定准确率的前置环节
2.1 摔倒行为的数据来源和类别设计
很多模型精度不够,问题不在 YOLO,而在数据。摔倒检测比较特殊,公开数据集不是特别多,常见的有 UR Fall Detection、Le2i Fall Detection Dataset、UP-Fall Detection 这类。使用公开数据集时要注意两点:一是确认数据集的使用许可,二是确认拍摄视角和你的实际场景是否接近。
公开数据集里的相机高度、角度、背景和室内环境可能很干净,但在真实监控画面里,摄像头通常装在墙角或屋顶,视角差异很大。模型在公开数据集上训练完,放到自己的测试视频里,性能会明显下降。
所以我建议在公开数据集基础上,自己补一批数据。不需要一开始就采几千张,可以先用手机或普通摄像头在不同角度拍几段视频,然后抽帧标注。重点覆盖这几类画面:
- 正常的走、坐、蹲、躺。
- 摔倒的瞬间、倒地后静止、倒地后挣扎。
- 不同光照:白天、晚上、开灯、关灯。
- 不同距离:人离摄像头近、中、远。
- 不同身体方向:正面、侧面、背面。
类别设计上,最简单的做法是只设两个类别:person 和 fall。person 是所有正常站立、行走、坐着的人;fall 是已经被判定为摔倒状态的人。这里有一个容易踩的坑:如果“摔倒”和“坐在地上”“蹲下”分不清,标注的时候很难受。我的建议是,如果你最终要报警,就把“倒地后需要帮助”的场景标成 fall,把临时蹲下、坐下正常行为的画面放到负样本里,不要全都标成 fall。
2.2 标注格式、目录结构和质量检查
YOLO 系列常用的标注格式是:一张图片对应一个同名的 txt 文件,txt 里每一行表示一个目标。
每行格式为:
class_id x_center y_center width height其中 x_center、y_center、width、height 都经过归一化,取值范围是 0 到 1。比如一张 1280x720 的图片里,某个目标中心点像素坐标是 (640, 360),宽是 320,高是 640,那么对应的值就是:
0 0.5 0.5 0.25 0.8889目录结构一般这样组织:
fall_dataset/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/标注工具可以用 labelImg,也可以用 X-AnyLabeling、Labelme 之类的工具。关键是检查三点:
- 图片和 txt 文件名是否完全一致。
- 坐标是否出现负数或大于 1 的值。
- 类别 id 是否和数据配置文件里的顺序一致。
如果数据集是自己从视频抽帧,建议不要只抽摔倒后的帧,要把摔倒前、摔倒中、摔倒后都保留一部分。一个典型误区是为了凑样本量,从连续视频里每秒抽 30 帧,结果大量图片内容几乎一样,模型容易过拟合,验证集 mAP 看着高,换一段新视频就不行。
一般抽样节奏是一段视频每秒抽 1 到 2 帧,宁可样本量小一点,也要保证多样性。训练集和验证集最好按照 8:2 或 7:2:1 划分,并且确保同一个视频的帧不要同时出现在训练集和验证集里,不然验证结果很虚。
3. 训练YOLOv8或YOLOv5之前,把环境先理顺
3.1 选版本和前置环境
YOLOv8 和 YOLOv5 都可以做这个项目。新项目我建议直接用 YOLOv8,接口更统一,导出 ONNX 也方便。如果公司或课程项目里已经有 YOLOv5 的旧代码,继续用 YOLOv5 也没问题,功能上足够。
环境上最容易出问题的是 Python 版本、PyTorch 版本和 PySide6 版本之间的兼容性。建议用虚拟环境隔离,不要直接装到系统 Python 里。
conda create -n fall python=3.10 conda activate fall pip install ultralytics opencv-python PySide6如果你有 NVIDIA 显卡,先确认显存和驱动,再安装对应版本的 PyTorch。如果没有 GPU,用 CPU 也能训练,但速度会慢很多。第一次测试可以用很小的网络和很少的轮数跑通流程,不要一上来就用自己的全量数据加 100 轮。
要注意:YOLOv8 的 pip 包名是 ultralytics,YOLOv5 的代码通常来自 GitHub 仓库,两者训练命令不同。不要混用,否则容易遇到模型结构加载失败的问题。
3.2 训练参数、data.yaml 和指标判断
在 YOLOv8 中,数据配置文件一般叫 data.yaml,内容类似:
path: D:/fall_dataset train: images/train val: images/val nc: 2 names: ['person', 'fall']训练命令:
yolo detect train data=data.yaml model=yolov8s.pt epochs=100 imgsz=640 batch=16 device=0如果你用的是 YOLOv5:
python train.py --data data.yaml --weights yolov5s.pt --img 640 --batch 16 --epochs 100这里几个关键参数:
| 参数 | 作用 | 建议 |
|---|---|---|
| imgsz | 训练图片输入尺寸 | 默认 640,不要一开始就用 1280,显存和速度压力大 |
| batch | 每批图片数量 | 根据显存调整,8 或 16 起步 |
| epochs | 训练轮数 | 100 轮左右,开启早停后看曲线 |
| patience | 指标不再上升时停止的轮数 | 20 左右 |
| device | 设备编号 | 0 表示第一张显卡,CPU 可写 cpu |
训练完成后,重点看几个指标:
- mAP50:IoU 阈值 0.5 时的平均精度,先看这个能不能到 0.8 以上。
- mAP50-95:更严格,但摔倒检测不需要追求极高。
- Precision:报警里有多少是对的。
- Recall:真正摔倒的人里有多少被检测出来了。
摔倒检测场景里,漏报比误报更危险。如果 recall 太低,说明很多摔倒画面没检测出来,这时要降低置信度阈值、增加数据、检查标注。但如果 precision 太低,报警会很吵,用户没多久就烦了。实际落地时需要在 precision 和 recall 之间找一个平衡点。
还有一个很多人遇到过的问题:训练一开始统计的参数量和最后训练完得到的参数量不一致。这个通常是正常的。训练前打印的是初始模型结构里的参数数量,训练过程中可能启用了 EMA、混合精度或更改了部分权重状态;有些版本的代码还会把 BatchNorm 统计量算进去。只要模型能正常加载推理,这个差异一般不影响部署。不要因为这个就去反复改动网络结构。
4. 模型导出:让PySide6能正常调用
4.1 导出 ONNX,而不是直接拷 .pt
训练完成后,PySide6 这边调用模型有两种常见方式:
- 直接用 ultralytics 的 YOLO 类加载
.pt文件。 - 导出成 ONNX,用 onnxruntime 推理。
直接加载.pt最简单,但会把整个 PyTorch 和 ultralytics 环境带到桌面应用里,打包体积大,启动也慢。对于桌面工具,我更建议导出 ONNX,然后用 onnxruntime 推理,依赖更少,部署更可控。
YOLOv8 导出命令:
yolo export model=best.pt format=onnx imgsz=640 opset=12YOLOv5 导出命令:
python export.py --weights best.pt --include onnx --img 640如果不是特别需要动态尺寸,建议固定 640x640 输入。动态尺寸虽然灵活,但后处理坐标映射会更复杂,对桌面应用来说没必要。
如果你打算部署到边缘设备,比如带 NPU 的板子,导出就不是 ONNX 这么简单了。很多边缘平台要转成自己的模型格式,比如瑞芯微平台可能要转 RKNN,Jetson 上可能转 TensorRT。转换前先确认模型里的算子是不是都被目标平台支持。普通 PC 上先不用考虑这个问题。
4.2 推理脚本骨架:预处理、后处理、NMS
用 ONNX 做推理,核心是预处理、模型推理、后处理三步。下面是一个很朴素的伪代码骨架:
import cv2 import numpy as np import onnxruntime as ort session = ort.InferenceSession("best.onnx", providers=["CPUExecutionProvider"]) def letterbox(img, size=640): # 保持宽高比缩放,不足部分用灰边填充 # 返回处理后的图、缩放比例、填充偏移量 pass def detect(frame): img, ratio, (dw, dh) = letterbox(frame, 640) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) blob = img.astype(np.float32) / 255.0 blob = np.transpose(blob, (2, 0, 1))[None] input_name = session.get_inputs()[0].name output_name = session.get_outputs()[0].name outputs = session.run([output_name], {input_name: blob})[0] # 解析 outputs:根据模型输出格式,取出边界框、置信度、类别 # 再加 NMS 去除重叠框 boxes, scores, class_ids = post_process(outputs) # 坐标还原到原图 return boxes, scores, class_ids这里最容易被忽略的是 BGR 和 RGB 的顺序。OpenCV 读出来的是 BGR,YOLO 训练时通常用 RGB。如果预处理写反,模型结果会明显变差,甚至什么都检测不到。
另一个点是 letterbox 之后的坐标还原。模型输出的框坐标是基于 640x640 缩放图的,要还原到原始画面,需要把框的 x、y、w、h 减去填充偏移量,再除以缩放比例。这一步很多人会漏,导致画框位置偏移。
如果你不想自己写后处理和 NMS,用 ultralytics 的 YOLO 类也能跑:
from ultralytics import YOLO model = YOLO("best.pt") results = model(frame)这种方式最省事,但在 PySide6 里要注意:不要把results = model(frame)写在主线程里,否则画面会卡。推理应该放到工作线程,主线程只负责刷新界面和接收报警信号。
5. PySide6界面和业务逻辑怎么结合起来
5.1 界面模块划分
一个典型的 PySide6 摔倒检测界面可以分成三块:
- 左侧:视频显示区,用 QLabel 显示实时画面。
- 右侧:控制区,包含启动/停止按钮、视频源选择下拉框、置信度输入框、报警状态提示。
- 下方或右侧:报警记录表格,显示时间、置信度、截图路径。
置信度输入框可以用 QLineEdit。这里有一个很常见的问题:很多人在 QLineEdit 里输入阈值后,没有把字符串转换成 float,或者输入为空时直接报错。建议在读取时做默认值处理,比如:
text = self.conf_lineEdit.text().strip() conf_threshold = float(text) if text else 0.5界面和检测逻辑要分开。不要把视频采集、模型推理、报警判断全部写进 MainWindow 类里。最简单的做法是建一个 DetectWorker,继承 QThread,在 run 方法里循环读取视频帧并推理,再用信号把画面和报警信息发回主线程。
class DetectWorker(QThread): frame_ready = pyqtSignal(QImage) fall_detected = pyqtSignal(dict) def run(self): while self.running: ok, frame = cap.read() if not ok: break boxes, scores, class_ids = detect(frame) draw_boxes(frame, boxes, scores, class_ids) self.frame_ready.emit(to_qimage(frame)) if fall_found: self.fall_detected.emit({ "time": current_time(), "confidence": score, "screenshot": save_path })这样做的原因是,PySide6 的主线程负责事件循环,如果在里面做耗时操作,窗口会无响应。检测这种可能上百毫秒一帧的任务,必须放到子线程里。QTimer 定时器可以用于刷新显示,但不要在里面跑推理。
5.2 报警逻辑和记录输出
报警逻辑不要做成“检测到一次 fall 就报警”。YOLO 单帧偶尔会有抖动,连续几帧出现 fall 类别再报警会更可靠。常见做法是设置一个连续帧计数器:
if class_id == fall_id and conf >= conf_threshold: fall_counter += 1 if fall_counter >= 3: trigger_alarm() fall_counter = 0 else: fall_counter = 0连续帧数这个参数可以根据视频帧率设置。如果摄像头是 25 帧每秒,3 帧确认大约 120 毫秒,反应比较快;如果希望更稳,可以设 5 到 10 帧。
触发报警后,要做好三件事:
- 保存当前帧截图,路径按时间命名。
- 在界面上显示报警状态。
- 把报警信息写进日志文件。
报警不能反复触发,否则一个人摔倒后会弹出几十次提示。加一个冷却时间,比如 10 秒内不重复报警。冷却时间可以作为界面参数暴露出来,方便演示时调整。
如果做多人场景,还要考虑同一时间多个人摔倒的情况。简单系统可以直接对所有检测框判断,出现任意一个 fall 就报警;更严谨的做法是给每个人分配跟踪 ID,同一个 ID 的连续报警只触发一次。第一次实现时不需要引入太重的跟踪器,先用冷却时间控制。
6. 单路测试、批量测试和误报处理
6.1 最小验证路径
系统写好后,不要直接接真实摄像头。我的习惯是先跑三段数据:
- 一张包含正常站立和摔倒的图片。
- 一段包含完整过程的视频。
- 摄像头实时画面。
先用图片能很快发现问题,比如模型没加载、BGR/RGB 反了、坐标还原不对。再跑视频,看连续帧里能不能稳定检测。最后接摄像头,看实时性和画面卡顿情况。
如果小样本跑不稳,不要急着调参数。先检查是不是输入格式问题。YOLO 模型对图像质量有一定要求,模糊、过暗、逆光都可能影响检测。把测试视频里的帧单独存出来,用脚本逐张推理,看哪一帧开始漏检,再对比原始画面和预处理后的画面。
6.2 参数调整和验证标准
下面这些参数是摔倒检测系统里最常见的调节项:
| 参数 | 作用 | 调节方向 |
|---|---|---|
| confidence threshold | 置信度阈值 | 调高减少误报,调低减少漏报 |
| IoU threshold | NMS 去重阈值 | 通常 0.45 到 0.7 |
| fall frame count | 连续几帧确认报警 | 调大更稳,但报警延迟增加 |
| cooldown time | 报警冷却时间 | 调大避免重复报警 |
| frame interval | 每隔多少帧做一次检测 | 调大降低 CPU/GPU 占用 |
验证时要有明确的通过标准。比如:
- 摔倒发生后 3 秒内必须报警。
- 正常行走、坐下、弯腰 1 分钟内误报不超过 1 次。
- 画面里出现多人时,至少能把摔倒的那个人标出来。
不要只看“能画框”就说系统完成,还要看误报和漏报。我自己会准备一个 5 到 10 分钟的测试视频,里面混合正常活动和摔倒,跑完看报警记录和实际事件是否对得上。
6.3 误报高时怎么排查
误报是摔倒检测系统最常见的问题。如果频繁把正常姿势识别成摔倒,排查顺序一般是:
- 先看数据里正常姿势的样本够不够,尤其是坐、蹲、躺。
- 再看置信度阈值是不是太低,比如 0.25 会让很多模糊画面被识别成 fall。
- 如果单帧检测很难区分,考虑引入姿态关键点。
YOLOv8 也有关键点检测能力,可以输出人的头、肩、髋、膝、脚踝等关键点。摔倒时,人的身体趋于水平,关键点角度会发生变化。如果只靠目标框判断,一个蹲着的人宽高比可能和躺着的人接近,但关键点角度会有差异。做了姿态辅助判断后,误报会明显降低。
还有一个有效手段是检测人的中心点变化速度。摔倒动作往往伴随着短时间内的快速位移,摔倒后又趋于静止。可以根据连续帧里的人体中心点位移,计算一个“运动速度”,只有摔倒瞬间速度快、随后静止的情况才报警。这个逻辑不复杂,但对降低误报很有帮助。
7. 部署到老旧电脑或边缘设备时的取舍
7.1 CPU 设备怎么调
如果目标电脑没有独立显卡,只有 CPU,那就要把模型尽量做小。YOLOv8n、YOLOv5s 这类轻量模型是首选,导出 ONNX 后用 onnxruntime 的 CPUExecutionProvider 推理。
CPU 推理最怕的是每帧都跑全分辨率。建议在摄像头读取后,先把画面缩放到较小尺寸,再送进模型。比如摄像头输出 1920x1080,可以先缩放成 960x540,再 letterbox 到 640。这样既减少预处理耗时,也不会明显影响检测效果。
另一个技巧是隔帧检测。比如每 5 帧检测一次,中间 4 帧直接显示上一帧的检测结果。这样画面看起来还是连续的,但推理压力变成原来的五分之一。
如果还要进一步优化,可以考虑 ONNX 模型量化。常见做法是把 FP32 转成 FP16 或 INT8,但 INT8 可能会掉精度,需要拿自己的测试视频验证。
7.2 帧率、延迟和批量
摔倒检测对实时性要求不像自动驾驶那么极端,延迟 1 到 3 秒通常是可以接受的。所以不用非要追求 25 FPS 实时推理。只要报警能在摔倒后的 3 秒内出现,就符合大多数看护场景。
多路摄像头接入时,要注意不能每一路都独占一块 GPU 显存。可以做一个简单的任务队列,多个摄像头轮流取帧进入检测线程,检测完再把结果写回对应的界面。这样做的好处是控制总体资源占用,避免同时跑多个模型导致 OOM。
如果是边缘设备,比如 RV1126、Jetson 这类平台,需要重新评估模型算子支持和转换工具。RV1126 这类 NPU 平台通常不能直接跑 ONNX,要转成平台要求的格式。转换前先确认模型后处理部分能不能移到 CPU 上,很多边缘板卡跑 YOLO 时,后处理还是放在 CPU 上做的。
8. 常见报错与排查思路
8.1 界面能启动但没有检测框
这是最常见的问题。现象是 PySide6 窗口能正常打开,画面也在实时显示,但画面上没有检测框,也没有报警。
先按这个顺序排查:
- 模型是否加载成功。看启动日志里有没有报错,模型路径是否正确。
- 检查输入图像颜色顺序,BGR 和 RGB 有没有搞反。
- 检查置信度阈值,如果设成 0.9,而模型对当前画面只有 0.6,就会显示为没检测到。
- 检查后处理输出解析,YOLOv8 和 YOLOv5 的输出格式不一样,不能直接套同一个后处理。
- 检查坐标还原逻辑,有可能检测到了,但框画到了画面外。
如果是第一次用 ONNX,可以先写一个测试脚本,单独读一张图推理并打印结果,确认输出结果里有目标,再去接 PySide6。这样能定位问题是在推理层还是界面层。
8.2 训练、导出和调用中的常见问题
训练时最容易遇到的是显存不足。解决方法很简单:降低 batch,或者降低 imgsz。不要一上来就开大 batch,也不要同时运行多个测试程序。
数据配置问题也很常见。出现No labels found之类的提示,先检查 data.yaml 里的路径是不是绝对路径,或者相对路径是否基于当前命令运行的目录。图片和标签目录层级写错,训练一开始就会失败。
导出 ONNX 后,用 onnxruntime 推理时要注意输出名和输出形状。有的模型导出后输出名不是output0,用session.get_outputs()[0].name拿最稳。
PySide6 安装问题通常是环境没激活。明明pip install PySide6成功了,运行脚本还是报No module named PySide6,大概率是当前终端和安装包不在同一个 Python 环境。用pip list先确认一下。
8.3 QLineEdit 输入和摄像头问题
很多界面逻辑问题出在输入校验上。建议所有数值输入都做默认值处理,QLineEdit 为空时用默认阈值。否则用户清空输入框再点击启动,程序会直接 crash。
摄像头打不开的话,先确认索引。笔记本自带摄像头可能索引是 0,外接 USB 摄像头是 1 或 2。可以写一个简单的测试脚本:
import cv2 cap = cv2.VideoCapture(0) print(cap.isOpened())如果返回 False,先检查摄像头是否被其他程序占用,再换索引。把cv2.VideoCapture放在子线程里打开,打开成功后不断读取帧,这样界面不会因为初始化摄像头失败而卡死。
最后提一句:这个系统真正落地时,最该盯住的不是功能列表,而是输入格式、资源占用和失败重试。先把单路视频跑稳,再考虑批量和边缘部署。踩过几次之后会发现,很多问题不是模型能力不够,而是前置环境和输入材料没有处理干净。