☰
YOLO+PyQt跌倒检测系统实战:从模型选型到误报抑制的工程落地
2026/9/30 6:26:03 网站建设 项目流程

简介:这是一套面向老年人护理与安全监控场景的跌倒检测完整方案,基于YOLO26与PyQt构建,适合计算机视觉学习者、毕业设计开发者及智慧养老项目实践者使用。资源包内含1428张已标注图像,同时提供YOLO格式txt标签与VOC格式xml标签,并划分好train、val、test三个子集,附data.yaml文件,可直接用于yolov5至yolo26等主流算法的训练与迁移。压缩包共约2000个文件,以txt标注与说明、md文档、py脚本为主,另含cpp、js、html、yaml等工程文件,整体约77.41MB,兼顾模型训练、界面开发与部署参考。资源还包含使用教程和模型评价指标曲线图,便于读者快速理解训练效果与调参方向。目前已有36人学习关注,适合希望从数据集准备、模型训练到PyQt可视化界面落地一站式掌握跌倒检测项目的读者参考借鉴。

1. 从「yolo26-pyqt跌倒检测」说起:一套能跑在护理站电脑上的实时告警系统

养老院夜班最怕的不是忙,是安静。走廊没人走动、房间没有呼叫,值班人员盯着四路监控画面,注意力在二十分钟内就会明显下滑,而老人跌倒后的黄金处置窗口往往只有几分钟。标题里的「yolo26-pyqt跌倒检测-老年人护理和安全监控系统+数据集+训练好的模型.zip」,本质就是把目标检测模型和桌面端界面打包成一套能落地的东西:YOLO 负责从摄像头画面里框出「人」和「跌倒」两类目标,PyQt 负责把检测结果、告警声音、录像片段和日志呈现在一台普通护理站电脑上。它适合两类人:一类是想给养老机构、社区日间照料中心做一套低成本视觉告警的开发者,另一类是手里已经有数据集和权重、但卡在「模型跑得动、界面接不上」这一步的工程同学。数据集和训练好的模型意味着你不需要从零标注几千张跌倒样本,但也不代表拿过来就能直接上线——分辨率、光照、遮挡、误报抑制这些坑,一个都不会少。这一篇就按「先跑通、再调参、最后压误报」的顺序,把整套系统拆开讲清楚。

2. 跌倒检测为什么不能只靠一个 YOLO 模型:原理、选型与数据准备

2.1 跌倒检测的本质是「姿态突变 + 时序持续」,不是单帧分类

很多人第一次做跌倒检测,会把它当成一个二分类问题:给模型一张图,输出「跌倒 / 未跌倒」。真跑起来就会发现,单帧模型在几个场景下几乎必然翻车。老人弯腰捡东西、蹲下系鞋带、坐在床边前倾,这些姿态在单帧里和跌倒的视觉特征高度重合;反过来,快速跌倒的中间帧可能因为运动模糊,反而被模型判成「正常站立」。所以工业上常见的做法是「检测 + 判定」两层:YOLO 只负责稳定地框出人体,输出边界框的位置、宽高比和置信度;真正的跌倒判定交给一个轻量的时序逻辑,比如连续 N 帧内边界框中心点快速下移、宽高比从竖直转为水平、且框内人体置信度保持较高。这样做的另一个好处是,模型只需要学「人」这一个类,标注成本大幅下降,泛化能力反而更强。标题里带「数据集 + 训练好的模型」,通常意味着作者已经标好了跌倒类别,你可以直接用,但我一般会建议先确认标注是「跌倒姿态」还是「跌倒事件」——前者是单帧框,后者需要视频片段,两者训练出来的模型行为完全不同。

2.2 数据集怎么用:先做一次「场景对齐」再谈训练

拿到数据集别急着train.py。先做三件事:统计分辨率分布、统计光照条件、统计正负样本比例。跌倒数据集最常见的毛病是正样本太少、负样本里全是站立行走,导致模型对「坐」「蹲」「躺」这些中间姿态极度敏感。我一般会按下面的脚本先做一次体检,把每张图的尺寸、亮度均值、以及标注框的宽高比打出来,人工扫一遍异常值。

import os import cv2 import numpy as np # 遍历数据集图片,输出尺寸、亮度、标注框宽高比统计 img_dir = "dataset/images/train" label_dir = "dataset/labels/train" ratios = [] brightness = [] for name in os.listdir(img_dir): if not name.lower().endswith((".jpg", ".png")): continue img = cv2.imread(os.path.join(img_dir, name)) if img is None: continue h, w = img.shape[:2] gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) brightness.append(gray.mean()) # 读取同名 label 文件,YOLO 格式:cls cx cy bw bh label_path = os.path.join(label_dir, name.rsplit(".", 1)[0] + ".txt") if os.path.exists(label_path): with open(label_path) as f: for line in f: parts = line.strip().split() if len(parts) == 5: bw, bh = float(parts[3]), float(parts[4]) if bh > 0: ratios.append(bw / bh) print("图片数量:", len(brightness)) print("亮度均值:", np.mean(brightness), "最低:", np.min(brightness)) print("宽高比均值:", np.mean(ratios) if ratios else "无标注") print("宽高比 > 1.2 的比例:", np.mean([r > 1.2 for r in ratios]) if ratios else 0)

这段脚本的逻辑很直白:亮度均值低于 60 的图基本是夜间或逆光,需要单独做增强;宽高比大于 1.2 的框通常是横向的跌倒姿态,如果这个比例低于 5%,说明正样本严重不足,训练时要么加过采样,要么调高跌倒类的损失权重。参数上,bw/bh的阈值 1.2 不是固定的,站立人体一般在 0.3 到 0.6 之间,跌倒后可能到 1.5 以上,你可以按自己数据集的分布微调。做完这一步再决定要不要补充数据,比盲目训练省下大量时间。

2.3 模型选型:为什么是 YOLO,以及版本差异对落地的影响

标题写的是 yolo26,这个命名在不同仓库里指向的版本并不统一,可能是某个社区对 YOLOv8/v11 的二次封装,也可能是作者自己的迭代编号。不管具体是哪个,选型逻辑是一样的:跌倒检测要的是「小目标 + 实时 + 低误报」,所以优先看三个指标——输入分辨率、推理延迟、以及小目标召回。YOLO 系列里,n/s 级别的模型在 640 分辨率下,普通 CPU 大概 80 到 150 毫秒一帧,GPU 上可以到 10 毫秒以内;如果护理站电脑没有独显,建议用 n 级别并配合 416 或 480 分辨率,牺牲一点精度换流畅度。训练时常见的做法是先用 COCO 预训练权重做迁移,冻结主干训练 20 到 30 轮,再解冻全量微调 50 轮左右。学习率用余弦退火,初始 0.01,warmup 3 轮。这里有个血泪经验:跌倒数据集里如果「人」和「跌倒」是两个类,而「跌倒」样本又少,模型很容易把所有躺姿都判成跌倒,包括床上正常躺着的老人。解决办法是把「躺床上」单独标一个类,或者干脆只训「人」一个类,把跌倒判定完全交给时序逻辑。

3. PyQt 界面怎么接:从模型推理到告警弹窗的最小闭环

3.1 线程模型:为什么推理必须离开 UI 主线程

PyQt 的界面刷新在主线程里,如果你把model.predict()直接写在按钮回调里,画面会卡死,告警弹窗也弹不出来。标准做法是开一个QThread子类,在run()里循环读帧、推理、发信号;主线程只负责接收信号并更新QLabel和日志。下面是一个最小可跑的骨架,把摄像头读取、YOLO 推理、信号发送串起来。

from PyQt5.QtCore import QThread, pyqtSignal import cv2 from ultralytics import YOLO class DetectThread(QThread): frame_ready = pyqtSignal(object) # 发送带框的画面 alarm_triggered = pyqtSignal(str) # 发送告警文本 def __init__(self, source=0, model_path="best.pt"): super().__init__() self.source = source self.model = YOLO(model_path) self.running = True self.fall_counter = 0 # 连续跌倒帧计数 def run(self): cap = cv2.VideoCapture(self.source) while self.running and cap.isOpened(): ret, frame = cap.read() if not ret: break results = self.model(frame, imgsz=640, conf=0.4, verbose=False) annotated = results[0].plot() # 简单时序判定:连续 8 帧检测到跌倒类才告警 names = results[0].names fall_detected = any( names[int(c)] == "fall" for c in results[0].boxes.cls ) if fall_detected: self.fall_counter += 1 else: self.fall_counter = max(0, self.fall_counter - 1) if self.fall_counter >= 8: self.alarm_triggered.emit("检测到跌倒,请立即查看!") self.fall_counter = 0 self.frame_ready.emit(annotated) cap.release() def stop(self): self.running = False self.wait()

逻辑说明:frame_ready和alarm_triggered是两个自定义信号,跨线程安全。fall_counter是防抖用的,单帧命中不告警,连续 8 帧才触发,能过滤掉大部分弯腰、蹲下的瞬时误判。参数上,conf=0.4是置信度阈值,跌倒场景建议不要低于 0.35,否则误报会明显上升;imgsz=640是推理分辨率,如果 CPU 吃力可以降到 480,但小目标召回会下降。fall_counter的阈值 8 对应大约 0.3 到 0.5 秒,具体按帧率调整,25 帧每秒的话 8 帧差不多三分之一秒,既能防抖又不会太迟钝。

3.2 界面布局:告警、录像、日志三块怎么摆

护理站电脑的界面不需要花哨,核心是三块:实时画面、告警状态、历史记录。我一般用QHBoxLayout左边放QLabel显示画面,右边用QVBoxLayout竖排告警灯、最近告警列表和录像按钮。告警触发时,除了弹窗,最好同时做两件事:一是把当前帧前后各 5 秒的画面存成 mp4,二是往 SQLite 里写一条带时间戳的记录。这样值班人员事后能回看,也能统计误报率。下面是对应的槽函数写法。

from PyQt5.QtWidgets import QLabel, QListWidget, QListWidgetItem from PyQt5.QtCore import Qt import cv2, time, sqlite3 class MainWindow: def on_frame(self, frame): # 把 OpenCV 的 BGR 转成 Qt 能显示的 RGB rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch = rgb.shape from PyQt5.QtGui import QImage, QPixmap qimg = QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) self.video_label.setPixmap(QPixmap.fromImage(qimg)) def on_alarm(self, text): self.alarm_label.setText(text) self.alarm_label.setStyleSheet("background-color: red; color: white;") item = QListWidgetItem(f"{time.strftime('%H:%M:%S')} {text}") self.log_list.addItem(item) # 写入 SQLite,方便后续统计误报 conn = sqlite3.connect("alarm.db") conn.execute( "INSERT INTO alarms (ts, msg) VALUES (?, ?)", (time.time(), text) ) conn.commit() conn.close()

这段代码的关键点是QImage的构造参数:ch * w是每行字节数,写错会导致画面花屏或倾斜,这是 PyQt 接 OpenCV 最常见的翻车点之一。告警写库用 SQLite 就够了,单机场景不需要上 MySQL。录像部分可以用cv2.VideoWriter在检测线程里维护一个环形缓冲区,命中告警时把缓冲区落盘,避免一直写盘占空间。

3.3 参数怎么调:置信度、IOU、帧间隔的取舍

落地时最花时间的不是写代码,是调参。下面这张表是我在几个养老院场景里总结的起点值,你可以按现场情况微调。

参数建议起点调高后果调低后果
conf 置信度0.40漏检增多,跌倒不报误报增多,频繁弹窗
iou NMS 阈值0.50重叠框被合并,可能漏人同一人被多个框重复检测
imgsz 推理尺寸640CPU 占用高,延迟大小目标召回下降
告警连续帧数8响应变慢,可能错过快速跌倒弯腰蹲下容易误报
检测间隔每帧资源占用高快速跌倒可能被跳过

调参顺序建议是:先固定imgsz=640、iou=0.5,只调conf,用一段包含跌倒和日常活动的测试视频跑,统计漏报和误报;然后调连续帧数,把误报压到可接受范围;最后如果 CPU 扛不住,再降imgsz或改成隔帧检测。注意隔帧检测会拉长响应时间,跌倒这种场景不建议超过隔一帧。

4. 避坑与排查:跌倒检测系统上线前最容易翻车的五件事

4.1 夜间红外画面下模型几乎全瞎

现象:白天测试正常,晚上八点后告警要么不触发,要么疯狂误报。原因:训练集里夜间样本太少,或者摄像头切到红外后画面变成灰度,模型没见过这种分布。解决:先确认摄像头是否支持红外,如果支持,补拍夜间样本重新微调;如果不想重训,可以在推理前做一次直方图均衡化,把灰度画面拉亮,同时把conf降到 0.35 并适当提高连续帧数。更稳妥的做法是夜间单独用一个模型,或者干脆夜间只做「人体存在检测」,跌倒判定交给值班人员。

4.2 床上躺卧被反复判成跌倒

现象:老人正常午休,系统每隔几分钟弹一次告警。原因:跌倒和躺卧在单帧里几乎无法区分,模型只学了「横向人体 = 跌倒」。解决:在标注阶段把「床上躺卧」单独标一类,训练时让模型学会区分场景;或者在业务逻辑里加一个「床区域」的 ROI,落在床区域内的横向人体不触发告警。ROI 可以用cv2.rectangle画框后手动记录坐标,简单但有效。

4.3 多路摄像头时 PyQt 界面卡成幻灯片

现象:接一路摄像头流畅,接四路后界面几乎不刷新。原因:每路都开了一个推理线程,CPU 被占满,主线程拿不到时间片。解决:一是降低每路的推理频率,比如四路轮流推理,每路每秒 5 帧;二是把推理放到 GPU 上,用model.to('cuda'),但要注意多线程同时调用同一个模型可能出问题,常见做法是每路一个模型实例,或者用一个推理队列串行处理。三是把画面显示分辨率降下来,QLabel不需要显示 1080p,缩到 640 宽足够看。

4.4 告警弹窗把操作员逼到关掉声音

现象:上线第一天误报几十次,值班人员直接把音箱拔了。原因:没有做告警分级和冷却时间。解决:加一个冷却窗口,比如同一个人在同一区域 30 秒内只告警一次;同时把告警分成「疑似」和「确认」两级,疑似只在日志里记,确认才弹窗加声音。冷却逻辑用一个字典记录每个人体框的中心点最近告警时间即可,不需要复杂的状态机。

4.5 模型换了权重后界面直接崩

现象:替换best.pt后程序启动报错,或者检测结果类别对不上。原因:新权重的类别数和名称与代码里写死的names不一致。解决:不要硬编码类别名,从results[0].names动态读取;替换权重后先用一张测试图跑一遍model.predict,确认输出类别和置信度正常,再接入界面。另外,权重文件路径建议用相对路径加配置文件管理,避免换电脑后路径失效。

5. 把误报压下去:滑动窗口平滑与告警分级的实战技巧

误报是跌倒检测系统能不能活过第一周的关键。前面提到的连续帧计数只是最粗的防抖,真正要压到可用水平,我一般会叠两层:一层是滑动窗口平滑,一层是告警分级。滑动窗口的做法是维护一个长度为 15 的队列,记录最近 15 帧的跌倒命中情况,计算命中率,超过 0.6 才认为是一次有效跌倒。这样比单纯的连续计数更抗抖动,因为中间偶尔漏一帧不会把计数清零。下面是一个可以直接嵌进检测线程的实现。

from collections import deque class FallSmoother: def __init__(self, window=15, threshold=0.6, cooldown=30): self.window = deque(maxlen=window) self.threshold = threshold self.cooldown = cooldown self.last_alarm = 0 def update(self, hit: bool, now: float) -> bool: # hit 表示当前帧是否检测到跌倒类 self.window.append(1 if hit else 0) if len(self.window) < self.window.maxlen: return False ratio = sum(self.window) / len(self.window) if ratio >= self.threshold and now - self.last_alarm > self.cooldown: self.last_alarm = now self.window.clear() # 告警后清空,避免连续触发 return True return False

参数说明:window=15对应大约 0.6 秒(25 帧每秒),threshold=0.6表示 15 帧里至少 9 帧命中才告警,cooldown=30是 30 秒冷却。这三个值需要按现场误报情况调:误报多就提高threshold或加长window,漏报多就反过来。注意告警后clear()是必要的,否则窗口里残留的命中会立刻触发第二次告警。

告警分级则是把「疑似跌倒」和「确认跌倒」分开。疑似跌倒只写日志、在界面上把对应画面边框变黄;确认跌倒才弹窗、响铃、录像。分级的依据可以是滑动窗口命中率:0.4 到 0.6 算疑似,0.6 以上算确认。这样值班人员不会被频繁弹窗打扰,同时也不会漏掉真正的跌倒。我自己的习惯是,上线第一周把阈值调保守一点,宁可多报几次让值班人员建立信任,等误报样本收集够了再逐步收紧。这套系统值不值得做,取决于你能不能接受「它永远会有误报」这个前提——把它当成辅助提醒而不是全自动报警,落地阻力会小很多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询