先说个实际的场景。家里长辈年纪大了,一个人住最怕的就是在浴室或者卧室摔倒,如果没人及时发现,后果很严重。这也是我做跌倒识别检测这一类项目时,心里最真实的出发点。这个项目的标题一看就是一套完整的东西:深度学习模型负责“看”,UI界面负责“用”,登录注册负责“管入口”,Python源码和训练好的模型负责“能直接跑”。说白了,它适合三类人:做毕业设计或课程项目的学生、刚入门目标检测但想练手完整工程化流程的开发者、以及真的想把跌倒报警做成一个可用小系统的个人爱好者。后面我会从头到尾把整个系统的构建思路、数据准备、训练调优、界面设计、打包部署这些环节里能踩的坑,都认真过一遍。
1. 项目整体拆解:跌倒识别系统到底在解决什么问题
1.1 检测方案的选型逻辑
很多人看到“跌倒检测”第一反应是拿OpenCV做背景建模,再根据人体框的宽高比判断是不是摔倒。这种传统做法我以前也试过,结论是:在固定摄像头、场景简单、光照稳定的教室里,它能跑;一旦放到真实室内环境,比如有窗帘晃动、灯光变化、宠物乱窜、人蹲下来系鞋带,误报率会高到你根本不敢开报警功能。原因是背景建模对“前景”的定义太脆弱,而宽高比判断又过于简单,跌倒过程中人体轮廓会剧烈变形,宽高比阈值怎么调都不稳。
所以这个项目选YOLO系列是非常合理的路线。YOLO把目标检测当成回归任务,一次前向推理同时输出目标的类别和位置,速度快且工程生态成熟。至于为什么选YOLOv12而不是继续用v8、v11,核心原因是v12在结构上引入了注意力机制,主干网络不再只是堆叠卷积,而是用注意力模块来建模特征之间的关联。跌倒这个场景有个很明显的特征:人体从直立到倒地,姿态变化非常剧烈,局部特征会突然“消失”或“变形”,这时候如果网络能结合上下文信息判断“这是一个正在倒地的人”,会比单纯看局部卷积特征更可靠。v12这类带注意力机制的检测结构,在思路上更匹配这个问题。当然也要说句公道话,v8的社区权重和第三方教程明显更多,遇到问题更容易搜到答案;v12版本较新,如果只是做演示或课程设计,先用v8打底,再换成v12做对比实验,是更稳的做法。
1.2 跌倒检测的真正难点在哪儿
整个项目里,模型训练难度其实不是最高的,数据才是最让人头疼的。跌倒姿态的多样性太大了,向前摔倒、向后仰倒、侧身倒、扶墙慢慢滑倒,不同角度下人体外观完全不一样。再加上室内环境的遮挡,桌子、床、轮椅、沙发都可能挡住身体的一部分,模型学到的是“残缺的人体也在跌倒”,这对特征提取要求很高。
另一个容易被忽略的问题是类别不平衡。一段监控视频里,正常人走路、坐下、弯腰拿东西的帧可能占95%,真正跌倒的瞬间可能只有几帧。如果不做处理,模型很容易把所有的正常行为都预测成“person”,跌倒类别根本学不到有效的判别边界。所以我在处理这个项目时,会刻意把跌倒样本的比重往上拉,甚至在训练时给跌倒类别提高损失权重。这点在后面的数据准备和训练部分我会详细展开。
1.3 系统整体模块划分
整个系统按功能可以拆成下面几块:
| 模块 | 职责 | 技术要点 |
|---|---|---|
| 模型模块 | 加载训练好的权重,对输入帧做目标检测 | YOLOv12权重、ONNX或TorchScript导出 |
| 数据模块 | 整理训练集、验证集,标注格式转换 | YOLO格式txt、label文件与图片一一对应 |
| 视觉处理模块 | 调用摄像头或读取视频,逐帧推理并绘制结果 | OpenCV、多线程队列 |
| 行为判断模块 | 根据检测框宽高比、中心点位移、连续帧状态判断是否跌倒 | 非模型逻辑,降低误报的关键 |
| UI界面模块 | 视频显示、检测结果展示、操作按钮、状态栏 | PyQt5、QThread |
| 登录注册模块 | 用户注册、登录验证、用户信息存储 | SQLite、密码哈希 |
| 告警模块 | 检测到跌倒后弹出提示、保存告警截图 | 信号机制、本地文件存储 |
这样拆的好处是,每一块都能独立测试。很多新手容易犯的错误是一上来就写UI,把模型推理直接塞进界面代码里,结果摄像头卡顿、界面假死、打包还失败。正确的工程顺序应该是:先把模型推理在命令行里跑通,再把行为判断逻辑加上,最后才套UI外壳。
2. 数据集准备:公开数据集和自建数据的正确处理方式
2.1 常用公开数据集怎么选
这个项目如果完全靠自建数据,工作量非常可观。好在学术界和工业界有一些公开的跌倒检测数据集可以直接用,比如Le2i、UR Fall Detection、MUVIM这类。Le2i是室内固定摄像头采集的,包含多人的日常行为和跌倒动作,场景涵盖客厅、办公室、楼梯等,是跌倒检测里非常经典的benchmark;UR Fall Detection除了摄像头画面,还带加速度计数据,不过做纯视觉项目时一般只取它的视频部分。
使用公开数据集时不要直接全量灌进训练,先做一次目录整理。很多数据集原始格式是按“视频”组织的,每一段视频里既有正常行为又有跌倒动作,需要先把视频切帧,再按帧打标签;还有一部分数据集本身就提供标注框,格式可能是VOC或COCO,需要转成YOLO格式的txt。我自己习惯写一个Python脚本统一处理:读原始标注文件,把归一化的中心点坐标、宽高换算成YOLO要求的格式,同时过滤掉没有目标物体的空标签文件。
如果做的是“带UI的完整项目”,我会建议在公开数据集基础上,额外收集两三个自建场景:比如从手机摄像头俯拍的角度、侧面墙角的角度、以及模拟有人在镜头前缓慢滑倒的过程。这样模型在真实部署时不会因为视角变化而断崖式掉点。自建数据时注意脱敏,不要拍到不必要的个人信息,室内场景也尽量避免暴露门牌号等敏感信息。
2.2 标注格式与标注工具的坑
YOLO系列的标注格式很简单:每个txt文件与同名图片对应,一行代表一个目标,格式为category_id x_center y_center width height,其中x_center、y_center、width、height都是归一化到0到1之间的小数。这个格式非常容易写错,我有一次大规模生成标签时,把“中心点坐标”写成了“左上角坐标”,训练时损失一直不降,查了很久才发现是坐标系搞混了。
标注工具方面,LabelImg是老牌选择,操作简单,可以直接导出YOLO格式;Labelme也能用,但导出是JSON格式,需要二次转换。这里有一个非常重要的设计决策:跌倒检测的类别到底怎么定义?
我看到很多开源项目直接只标一个falling类,遇到跌倒姿态就框出来,其余人一律不标。这种做法在demo里看起来没问题,但实际上会让模型非常困惑,因为模型根本不知道该不该把人检测出来。更合理的做法有两种:
- 方案A:只检测
person这一类,跌倒判断完全交给后处理逻辑,根据人体框的宽高比、中心点移动速度、连续帧的变化率来判断。 - 方案B:标注
person和falling两类,其中falling表示正在跌倒过程或已经倒地的人。
我自己实战下来更推荐方案A。原因很直接:跌倒姿态也是人,与正常站立相比最大的区别就是姿态和位置的变化,让目标检测网络去强行学“摔倒的样子”很容易过拟合到某一个固定视角;反而把“人”检测稳了,再用工程逻辑判断姿态是否异常,系统的鲁棒性会高很多。如果项目要求必须输出“是否跌倒”的类别,那就用方案B,但标注时要严格统一标准:倒地瞬间算falling还是摔完躺平也算falling?多人场景里有人正常走有人倒地,要不要都标?这些规则不提前定好,标注质量和训练效果都会崩。
2.3 数据增强和目标平衡策略
模型的泛化能力很大程度上取决于训练数据的多样性。使用Ultralytics框架训练时,默认会启用mosaic增强、随机平移、翻转、HSV颜色变化等策略,这些对普通目标检测任务非常友好。但跌倒检测有个特殊点:跌倒目标在画面里的占比往往偏小,如果mosaic增强过于激进,四个小图拼在一起后目标会更小,模型可能学不到有效的细节特征。所以我会把mosaic的开关保留,但适当降低它的使用概率,同时增加随机旋转和透视变换。
类别平衡很关键。如果训练集的person类有两万张,falling类只有几百张,模型几乎不可能同时学好两个类别。处理方式有两种直接的手段:一是数据重采样,对样本量少的类别做重复采样,让每个epoch里两个类别的样本量接近;二是从损失函数层面给少数类提高权重,在Ultralytics中可以用cls参数调整类别损失的系数,但更直接的方法是在数据层面先尽量平衡。实际操作中,我会把跌倒帧用视频抽帧的方式多抽几遍,再加上水平翻转、小角度旋转,把跌倒样本扩到person样本的30%到50%左右,效果比单纯调参数明显得多。
3. 模型训练:从环境搭建到参数调优的完整流程
3.1 训练环境和模型结构选择
先搭环境。建议用Python 3.8以上版本,PyTorch 2.x,CUDA 11.8或12.x,显卡显存最少建议6G以上。这个项目如果只用CPU训练,跑YOLOv12的小模型也会非常痛苦,一个epoch在几百张图的规模下都要等很久,所以能上GPU就上GPU。Ultralytics框架对YOLO系列模型统一管理,训练、验证、导出一条龙,YOLOv12官方也提供对应的预训练权重。如果你用的是社区适配版本,记得先确认框架代码里确实有对应的模型配置文件,别一上来就model=yolov12n.pt结果报错找不到。
模型结构选择可以参考这张表:
| 模型版本 | 特点 | 适用场景 |
|---|---|---|
| YOLOv12n | 体积最小、速度最快,精度偏低 | 低算力设备、实时性要求高的场景 |
| YOLOv12s | 精度和速度均衡,显存占用约6-8G | 最常见的项目起步选择 |
| YOLOv12m | 精度更高,但推理速度下降 | 摄像头性能足够,想要更低漏检率 |
我建议至少从s起步。跌倒检测里目标往往不大,n模型为了速度牺牲了太多特征提取能力,在远距离摄像头下容易出现整段视频完全漏检的情况。如果显存允许,直接用m,后面导出ONNX时还可以再优化速度。
3.2 训练数据集配置与命令
训练前先写一个data.yaml,指明训练集和验证集路径,以及类别名称。如果采用方案A只检测person,配置非常简单;如果用方案B同时检测person和falling,则需要写两个类别。下面是方案B的配置示例:
path: dataset/fall train: images/train val: images/val names: 0: person 1: falling训练命令我用的是Ultralytics的标准CLI,类似这样:
yolo train data=fall.yaml model=yolov12s.pt epochs=200 imgsz=640 batch=16 device=0这里有几个关键参数值得细说。
epochs不要只看200这个数字,关键是观察验证集的损失曲线和mAP曲线是否收敛。我在小数据集上试过,150到200轮左右基本就平了,继续往上加只会浪费时间。batch由显存决定,如果显存不够而报OOM,把batch降到8或者4,同时把imgsz从640降到512,跌倒检测不需要像车牌识别那样追求超高分辨率。学习率方面,Ultralytics默认的AdamW配合lr0=0.01在大多数场景都能用,不需要特意改。真正需要调的是conf阈值,也就是置信度阈值,这个更多是在推理阶段调整,训练阶段用默认值即可。
还要注意一个细节:训练集和验证集的数据分布要尽量一致,别把训练集里同一个摄像头录的视频全部放进去,验证集里又全是另一个摄像头的视角,这样验证指标看似很高,换到真实场景就露馅。我习惯按“视频片段”而不是按“帧”拆分数据集,确保同一段视频的连续帧不会同时出现在训练和验证里。
3.3 指标评估与模型导出
训练结束后不要只盯着mAP50一个数字。跌倒检测这种场景里,我更关注的是recall和precision的平衡。recall低说明有很多真正的跌倒没有被检测出来,这往往是跌倒样本太少或标注不完整导致的;precision低说明正常行为被误判成了跌倒,这在报警系统里会造成非常烦人的误报。把conf阈值调高可以提升precision,但会牺牲recall,所以需要根据实际需求找到平衡点。我在项目中通常先看验证集的混淆矩阵,如果falling类别大量被预测成person,那就说明模型没有把跌倒姿态和正常姿态区分开,需要去检查标注的准确性,而不是盲目调阈值。
训练完之后,要把权重导出成部署友好的格式。如果UI端直接加载PyTorch权重,代码简单但推理速度和打包体积都不理想;如果导出成ONNX,再用ONNX Runtime加载,打包体积会小很多,推理速度也更快。Ultralytics提供了现成命令:
yolo export model=runs/detect/train/weights/best.pt format=onnx导出后写几行代码验证一下推理结果和原来的PyTorch权重是否一致,因为有时候输入图像预处理细节不一致会导致结果出现偏差。我最开始没做这一步验证,导出完直接在UI里调用,结果置信度明显偏低,后来发现是导出时输入尺寸和测试尺寸不一致导致的。
4. UI界面与登录系统的设计与实现
4.1 登录注册模块的设计要点
这套系统的登录注册功能,很多人把它当成一个“凑数的”模块,但实际上它涉及到一个非常基础的安全问题:用户密码到底该怎么存。
如果直接把密码明文存进数据库,那就是给自己挖坑。哪怕这只是一个毕设项目或者个人工具,也应该做一层哈希处理。Python的hashlib模块就够用,对密码加盐后做SHA-256或SHA-512,存储盐值和哈希值;登录时重新计算比对,而不是存明文。用SQLite存储用户信息是最省事的方案,字段一般包括用户名、密码哈希、盐值、创建时间。注册时先检查用户名是否已存在,登录时验证哈希是否匹配,再用一个全局变量保存当前登录的用户名,供主界面显示。
页面切换用QStackedWidget实现,注册页和登录页之间做切换按钮,登录成功后跳到主界面。这套结构的好处是逻辑清晰,而且把每个页面的代码拆成独立的类,后面维护也方便。
4.2 检测主界面的功能布局
主界面是整个系统的门面,我用PyQt5搭的布局大致是这样的:左侧是视频显示区域,用QLabel承载QPixmap显示检测后的画面;右侧是一个信息面板,显示当前FPS、检测到的人数、当前行为状态(正常/跌倒风险/已跌倒)以及置信度;底部放一排操作按钮,包括打开图片、打开视频、打开摄像头、停止检测、保存截图、退出登录。
这个项目最核心的三个检测模式都要做:单张图片检测,适合快速验证模型效果;视频文件检测,适合离线分析一段监控录像;摄像头实时检测,是实际部署中最常用的模式。实现时,图片检测最简单,读取一张图、用模型推理、绘制检测框、显示结果即可;视频和摄像头则必须处理多线程,否则视频画面会一帧一帧地卡顿。
4.3 多线程处理保证界面流畅
这是整个UI开发里最关键的实现细节。OpenCV的cv2.VideoCapture.read()在摄像头无信号的极端情况下会阻塞,YOLO模型的单帧推理即使用了GPU也需要几十毫秒。如果这些操作直接放在PyQt5的主线程里,拖动窗口都会卡住,因为主线程要同时处理界面刷新和耗时的计算任务。
正确做法是把摄像头读取和模型推理放进QThread,推理完成后通过pyqtSignal把带检测结果的图像发回主线程。大致是这个结构:
class DetectionThread(QThread): frame_ready = pyqtSignal(object) status_ready = pyqtSignal(dict) def run(self): cap = cv2.VideoCapture(self.source) while self.running: ret, frame = cap.read() if not ret: break results = self.model(frame, conf=0.4) annotated_frame = results[0].plot() fps = self.cal_fps() self.frame_ready.emit(annotated_frame) self.status_ready.emit({"fps": fps, "speed": ...}) cap.release()主线程收到frame_ready信号后,只用label.setPixmap(...)做显示操作,不做任何耗时的计算。这样界面才能保持流畅。信号传递的是内存中的图像对象,传输成本极低,完全不用担心性能瓶颈。一个很容易踩的坑是线程退出时机,如果关闭窗口时没有正确停止线程,会出现进程无法退出的问题,需要在closeEvent里设置running=False并调用wait()。
跌倒报警的联动逻辑也放在这个线程里。不能单帧判断,否则人蹲下捡东西就会被报警。我在行为判断模块里维护了一个长度为8到10帧的列表,只有当连续多帧都满足“人体框宽高比大于阈值、且中心点Y坐标发生大幅下移、且置信度高于阈值”时,才触发报警。这个连续帧的缓冲机制非常有效,能把误报率降低一大截。
5. 部署打包与常见问题排查实录
5.1 PyInstaller打包桌面应用的注意事项
项目做完之后,大部分人都会想着打包成exe发给别人用。我用PyInstaller打包过很多次带PyTorch和PyQt5的项目,这里面的坑比想象中多。
首先,打包模式建议用--onedir(也就是-D)而不是--onefile(-F)。-F会把所有文件塞进一个exe里,启动时要先解压到临时目录,启动速度慢,而且容易被杀毒软件误报;-D生成一个文件夹,启动快,调试也方便。命令大致是:
pyinstaller -w -D main.py --add-data "best.pt;." --hidden-import=ultralytics-w是隐藏控制台窗口,如果调试阶段建议先不加,这样报错信息还能看到。--add-data把模型文件带进去,路径分隔符在Windows下是分号。最麻烦的是PyTorch和Ultralytics的隐藏导入,打包后在别人的电脑上运行时经常报ModuleNotFoundError,解决办法很简单:先不加--hidden-import试一次,看缺哪个模块就补哪个。
还有一个非常经典的路径问题。打包后程序运行目录可能是临时解压目录,用相对路径加载best.pt会失败。稳妥做法是用sys._MEIPASS判断当前是不是打包环境,再拼接出模型的绝对路径:
import sys import os base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__))) model_path = os.path.join(base_path, 'best.pt')另外,PyTorch打包出来的体积普遍在3到4G,这是正常的,不用太惊讶。如果嫌太大,可以放弃PyTorch,改用ONNX Runtime加载导出的模型,打包体积能压缩到几百兆,速度也不会差。
5.2 实时检测的优化技巧
实时检测的优化优先级是这样的:先保证模型推理速度,再处理摄像头读取,最后才考虑界面显示。
模型推理层面,如果显卡支持半精度浮点,推理时把模型转成half模式,速度提升明显;或者在Ultralytics推理时传half=True。又或者直接导出ONNX后用ONNX Runtime跑,在CPU上也能获得比PyTorch更稳定的推理速度。
摄像头读取层面,遇到的问题是read()是阻塞的,如果推理速度慢,下一帧就会被不断往后积压,导致画面越来越延迟。解决方法是把摄像头读取和推理放到同一个线程里,但每次推理前先从队列里清掉旧帧,只保留最新的一帧。具体实现时可以用一个deque,长度设为1,新帧来了替换旧帧。这样即使推理速度只有10FPS,画面也始终是接近实时的,而不是越积越多。
还有一个容易忽略的点:直接把1920x1080的原始帧送去推理是很浪费的。我在摄像头读取后先resize到640x640或768x768再推理,检测框坐标再映射回原图。这样模型推理时间能下降一个量级,精度损失却很小。
5.3 常见问题速查表
| 问题 | 表现 | 排查思路 |
|---|---|---|
| 训练损失不下降 | loss一直很大,mAP接近零 | 检查标注文件中的类别编号是否与names一致,检查中心点坐标是否写成了左上角坐标 |
| 漏检严重 | 跌倒瞬间完全没有检测框 | 降低conf阈值,补充更多跌倒姿态的训练样本,考虑把imgsz增大到768 |
| 误报多 | 蹲下、弯腰被判定为跌倒 | 提高conf阈值,增加连续帧判断逻辑,不要只看单帧 |
| 摄像头黑屏 | 摄像头指示灯亮但界面无画面 | 尝试更换设备索引0/1,检查摄像头是否被其他程序占用 |
| 打包后双击无反应 | exe打开后闪退 | 在命令行里运行exe查看报错信息,检查模型路径是否使用了sys._MEIPASS |
| 导出ONNX后精度下降 | 同一张图结果和PyTorch不一致 | 检查导出时的输入尺寸是否与推理尺寸一致,检查是否关闭了训练模式下的dropout |
我在实际使用这套系统时,最大的体会是:别在调参上纠结太久,先保证端到端能跑通。你永远想象不到真实录像里有多少“奇奇怪怪”的姿态——老人慢慢蹲下拿东西、小孩子突然扑倒、猫从镜头前蹿过去。模型在公开数据集上刷到0.95,换个摄像头可能直接掉到0.7。所以,把数据采集当成一个持续积累的过程,系统才有落地价值。如果你是想拿这套系统做毕业设计,建议把精力重点放在“跌倒事件的连续判断逻辑”和“UI交互体验”上,这两块是真正能体现工程能力的地方。另外一个小技巧:检测到跌倒后,除了界面弹窗,一定要把告警帧和前后几秒的视频片段保存下来,这样事后复盘误报原因时,你手里有证据,而不是只能对着代码猜。