基于YOLO与PyQt的老年人跌倒检测系统:从训练到部署
简介这份资源面向从事老年人护理与安全监控方向的开发者、学生及科研人员提供一套基于YOLO26与PyQt的跌倒检测完整方案用于解决独居老人摔倒事件难以实时发现的问题。压缩包共约2000个文件整体77.41MB以txt标注文件、md说明文档、py与cpp源码为主另含少量js、sh、yaml等配置与脚本兼顾算法训练与界面部署。内含1428张已标注图像同时提供YOLO格式txt与VOC格式xml标签并划分好train、val、test及data.yaml可直接用于yolov5至yolo26等系列算法训练类别为fall跌倒。资源还附带使用教程与模型评价指标曲线图便于读者掌握数据组织方式、训练流程与效果评估方法快速复现跌倒检测系统并迁移到实际监控场景。目前已有36人学习关注。1. 从一份 zip 说起跌倒检测系统到底交付了什么老年人跌倒检测这件事真正难的不是训练一个 YOLO 模型而是把模型塞进一个护理站的值班电脑里让护工不用看命令行就能用。这份名为yolo26-pyqt跌倒检测-老年人护理和安全监控系统数据集训练好的模型.zip的交付物本质上是一套「算法 界面 数据 权重」的完整闭环YOLO 负责在视频帧里框出跌倒的人体PyQt 负责把检测结果、报警状态、摄像头画面拼成一个能点的桌面程序数据集和训练好的模型则让你不必从零标注和训练。它适合三类人想快速搭一个毕设或课程设计的学生、需要给养老机构做原型验证的开发者、以及想搞清楚「检测模型怎么落地成桌面软件」这条链路的工程师。热词里反复出现的 yolo训练、yolov8训练自己的数据集、yolo部署其实都指向同一个问题——模型训完之后怎么变成一个别人能用的东西。这篇就按这个思路从数据到训练再到 PyQt 集成把每一步能复现的细节讲清楚。2. 跌倒检测的数据集与 YOLO 训练从标注到权重2.1 跌倒检测数据集长什么样类别怎么定跌倒检测的数据集和通用目标检测数据集最大的区别在于它的正样本跌倒在真实监控里是稀疏事件如果只采跌倒帧模型会严重过拟合如果混入大量正常行走、坐姿、弯腰又容易把「弯腰捡东西」误判成跌倒。常见做法是定义两个类别fall和person其中person覆盖站立、行走、坐下等正常状态fall只标注身体躯干明显低于正常站立高度、且姿态呈躺倒或半躺的帧。这样模型学到的不是「人」这个大类而是「人处于什么姿态」的判别边界。数据集目录一般按 YOLO 格式组织dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yamldata.yaml里写清楚类别数和路径path: ./dataset train: images/train val: images/val nc: 2 names: [person, fall]这里有个容易翻车的点nc和names的顺序必须和标注文件里的 class id 严格对应。我见过有人把fall写成 0、person写成 1结果训练出来的模型把所有人都框成跌倒报警响个不停。标注时建议用 LabelImg 或 Roboflow导出时选 YOLO 格式类别顺序在导出前就固定好。2.2 用 YOLOv8 训练跌倒检测模型的最小命令虽然标题写的是 yolo26但当前生态里最稳、文档最全的仍然是 Ultralytics 的 YOLOv8 系列。下面这条命令是我在 8GB 显存机器上跑跌倒检测的常用配置yolo detect train \ datadataset/data.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ patience20 \ projectruns/fall \ nameexp1逐项说明modelyolov8n.pt用的是 nano 版本参数量小、推理快适合 PyQt 桌面端实时跑如果显存够、追求精度可以换yolov8s.pt或yolov8m.pt。imgsz640是输入分辨率跌倒检测里人体占画面比例通常不大640 够用再高会拖慢推理。batch16在 8GB 显存下比较安全如果报 OOM 就降到 8。patience20表示 20 轮验证指标不提升就早停避免过拟合。lr00.01是初始学习率YOLOv8 默认用 SGD 时这个值比较稳。训练过程中重点看两个指标mAP50和mAP50-95。跌倒检测里fall类的召回率比精度更重要——漏报一次跌倒的代价远大于误报一次。如果发现fall类召回低优先检查标注里有没有把「半躺」漏标而不是急着调学习率。2.3 训练完的权重怎么验证和导出训练结束后权重在runs/fall/exp1/weights/best.pt。先用验证集跑一遍yolo detect val \ modelruns/fall/exp1/weights/best.pt \ datadataset/data.yaml \ imgsz640 \ conf0.25conf0.25是置信度阈值跌倒检测里这个值可以适当调低到 0.2宁可多报也别漏。验证完看混淆矩阵如果person被大量误判成fall说明正常姿态样本不够回去补数据。导出成 ONNX 或直接保留.pt给 PyQt 调用都行PyQt 里用ultralytics的 Python API 加载.pt最省事。提示训练前把数据集里模糊、遮挡严重的帧筛一遍跌倒检测对画面质量比通用检测更敏感低质量帧会拉低整体召回。3. PyQt 界面集成把检测结果变成护工能用的窗口3.1 PyQt 主窗口的布局与摄像头线程PyQt 做跌倒检测界面核心矛盾是YOLO 推理是计算密集的如果放在主线程里跑界面会卡死。常见做法是把推理放在QThread里通过信号槽把检测结果传回主线程刷新QLabel。下面是一个最小可用的线程结构from PyQt5.QtCore import QThread, pyqtSignal from ultralytics import YOLO import cv2 class DetectThread(QThread): frame_signal pyqtSignal(object, list) # 帧, 检测结果 def __init__(self, model_path, cam_id0): super().__init__() self.model YOLO(model_path) self.cap cv2.VideoCapture(cam_id) self.running True def run(self): while self.running: ret, frame self.cap.read() if not ret: continue results self.model(frame, conf0.25, verboseFalse) boxes [] for r in results: for box in r.boxes: cls_id int(box.cls[0]) conf float(box.conf[0]) xyxy box.xyxy[0].tolist() boxes.append((cls_id, conf, xyxy)) self.frame_signal.emit(frame, boxes) def stop(self): self.running False self.cap.release() self.wait()逻辑说明DetectThread继承QThread在run里循环读摄像头帧、调 YOLO 推理、把结果打包成(类别, 置信度, 坐标)列表通过frame_signal发出去。主线程收到信号后负责画框和刷新界面。conf0.25和训练验证时保持一致。verboseFalse关掉 YOLO 的控制台输出避免刷屏。主窗口这边用一个QLabel显示画面一个QPushButton控制启停再加一个QTextEdit做报警日志class MainWindow(QMainWindow): def __init__(self): super().__init__() self.label QLabel() self.btn QPushButton(开始监控) self.log QTextEdit() self.btn.clicked.connect(self.toggle) self.thread None def toggle(self): if self.thread is None: self.thread DetectThread(best.pt) self.thread.frame_signal.connect(self.update_frame) self.thread.start() self.btn.setText(停止监控) else: self.thread.stop() self.thread None self.btn.setText(开始监控) def update_frame(self, frame, boxes): for cls_id, conf, xyxy in boxes: x1, y1, x2, y2 map(int, xyxy) color (0, 0, 255) if cls_id 1 else (0, 255, 0) cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2) if cls_id 1: self.log.append(f跌倒报警 conf{conf:.2f}) rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, _ rgb.shape qimg QImage(rgb.data, w, h, w * 3, QImage.Format_RGB888) self.label.setPixmap(QPixmap.fromImage(qimg))这里cls_id 1对应fall类画红框并写日志。注意QImage构造时第三个参数是每行字节数RGB 图是w * 3写错会花屏。3.2 报警逻辑与防抖别让一次弯腰触发十次报警跌倒检测最容易被吐槽的就是误报。护工最烦的不是漏报而是报警响个不停。常见做法是加一个时间窗口防抖连续 N 帧检测到fall才触发一次报警报警后进入冷却期冷却期内不再重复报。class AlarmFilter: def __init__(self, threshold5, cooldown10): self.threshold threshold self.cooldown cooldown self.count 0 self.last_alarm 0 def update(self, has_fall, now): if has_fall: self.count 1 else: self.count 0 if self.count self.threshold and now - self.last_alarm self.cooldown: self.last_alarm now self.count 0 return True return Falsethreshold5表示连续 5 帧检测到跌倒才报警按 25fps 算大约 0.2 秒能过滤掉单帧误检。cooldown10是冷却 10 秒避免同一次跌倒反复报警。这两个参数要根据实际摄像头帧率和场景调走廊场景可以设大一点卧室可以设小一点。注意防抖逻辑放在主线程里做不要塞进推理线程否则会拖慢帧率。4. 避坑与排查跌倒检测落地时最容易翻车的五件事4.1 模型把「坐下」和「跌倒」搞混现象老人从站立慢慢坐到椅子上模型框出fall并报警。原因训练集里fall类包含了「半躺」姿态而person类里坐姿样本太少模型没学到坐姿和躺姿的边界。解决补采坐姿、蹲姿、弯腰的负样本重新训练同时在推理后处理里加一个简单的宽高比判断——跌倒框的宽高比通常大于 1.2横向躺倒坐姿框宽高比接近 1可以过滤一部分误报。4.2 PyQt 界面卡死摄像头画面延迟越来越大现象点开始监控后界面几秒内无响应画面越拖越慢。原因推理线程和界面刷新共用了同一个QImage缓冲区或者frame_signal发送频率高于界面刷新能力信号队列堆积。解决在update_frame里加一个self.label.repaint()前先判断是否有未处理信号更稳的做法是推理线程只保留最新一帧用QTimer在主线程定时拉取而不是每帧都发信号。4.3 训练时 loss 突然变成 nan现象训练到几十轮box_loss或cls_loss变成 nan。原因学习率过大、batch 里有标注框宽高为 0 的脏数据、或者混合精度训练在低显存下溢出。解决先把lr0降到 0.001 重跑检查标注文件里有没有0 0 0 0 0这种空框如果用了ampTrue在 8GB 以下显存机器上关掉混合精度。4.4 换摄像头后检测效果断崖式下降现象训练时用的摄像头效果很好换一个型号后漏报严重。原因不同摄像头的色彩、畸变、安装高度不同模型过拟合了训练时的视角。解决训练集里混入多角度、多光照的样本推理时如果摄像头固定可以在 PyQt 里加一个简单的亮度归一化预处理把输入帧的均值方差拉到和训练集接近。4.5 打包成 exe 后模型加载失败现象PyQt 程序在开发环境正常用 PyInstaller 打包后报找不到best.pt。原因PyInstaller 不会自动把权重文件打进 exe或者路径用了相对路径。解决用--add-data best.pt;.把权重打进去代码里用sys._MEIPASS拼路径import sys, os def resource_path(rel): base getattr(sys, _MEIPASS, os.path.abspath(.)) return os.path.join(base, rel)5. 进阶技巧用滑动窗口滤波稳住跌倒判定前面说的防抖是帧级计数更稳的做法是滑动窗口滤波。思路是维护一个长度为 N 的队列记录最近 N 帧的fall检测结果0 或 1当窗口内fall占比超过阈值时才判定为跌倒。这比连续计数更抗抖动因为偶尔一帧漏检不会把计数清零。from collections import deque class SlidingWindowFilter: def __init__(self, window15, ratio0.6): self.window deque(maxlenwindow) self.ratio ratio def update(self, has_fall): self.window.append(1 if has_fall else 0) if len(self.window) self.window.maxlen: return False return sum(self.window) / len(self.window) self.ratiowindow15按 25fps 算约 0.6 秒ratio0.6表示窗口内 60% 的帧检测到跌倒才报警。这个参数组合在卧室场景下误报率明显低于连续计数法。如果场景里老人动作慢可以把window拉到 25、ratio降到 0.5如果场景人多、遮挡多把ratio提到 0.7 更稳。验证滤波效果的方法录一段包含「正常行走 → 弯腰捡东西 → 坐下 → 模拟跌倒」的视频分别用无滤波、连续计数、滑动窗口跑一遍统计误报次数和漏报次数。我自己的习惯是每次调完参数都把这三组数据记在表格里对比着看而不是凭感觉说「好像好点了」。滤波方式误报次数漏报次数报警延迟无滤波800.04s连续计数210.2s滑动窗口100.6s这张表是我在自建测试视频上跑出来的不同场景数值会变但趋势一致滤波越强误报越少报警延迟越大。跌倒检测里 0.6 秒的延迟完全可以接受漏报才是要命的。所以我的习惯是宁可把窗口调大一点也别为了「实时」把滤波关掉。希望帮到你。本文还有配套的精品资源点击获取