ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

边缘智能跌倒检测:YOLOv8n-Pose与LSTM在K230D上的实战

边缘智能跌倒检测:YOLOv8n-Pose与LSTM在K230D上的实战 1. 为什么要在边缘侧做跌倒检测1.1 从真实痛点说起家里有老人的朋友应该都有这种体会老人独自在家最怕的就是摔一跤没人知道。根据公开的流行病学数据跌倒是65岁以上人群意外伤害致死的首位原因而跌倒后如果超过1小时没有得到救助致残率和死亡率会显著上升。传统的做法无非几种请护工、装摄像头靠人盯着、戴一个带SOS按钮的手环。护工成本高摄像头涉及隐私且不可能24小时有人看手环的问题更直接——老人摔倒了可能根本按不动按钮甚至摔的时候手环就飞出去了。我接触这个方向是从一个很朴素的念头开始的能不能让设备自己看懂人是不是摔倒了而且不用把视频传到云端这个不用传云端就是边缘智能的核心诉求。原因有三第一卧室、卫生间这些高跌倒风险区域装摄像头视频上云在隐私上很难让人接受第二家庭网络上行带宽普遍有限多路视频流实时上传不现实第三跌倒检测是典型的低延迟场景从跌倒到报警最好在几秒内完成走一趟云端往返会增加不确定性。所以这个项目的定位就很清楚了在本地算力盒子上用姿态估计加时序分类的两级流水线实时判断画面中的人是否发生跌倒并触发本地报警。硬件上我选的是正点原子的K230D Box算法上用的是YOLOv8n-Pose做人体关键点提取再用LSTM做时序行为分类。下面我把整套设计思路、踩过的坑和可复现的细节完整讲一遍。1.2 这套方案适合谁参考如果你正在做以下几类事情这篇内容应该能直接抄作业一是做居家养老、独居监护类产品的嵌入式或算法工程师二是想入门边缘AI手里正好有K230D这类带NPU的开发板想找一个有完整业务闭环的项目练手三是做计算机视觉课程设计或毕业设计的学生需要一个姿态估计时序模型的完整案例。前提是你对Python和PyTorch有基本了解C不熟也没关系K230D的推理部分可以用官方工具链把模型转过去。需要提前说明的是跌倒检测这个任务没有想象中那么简单。它不是检测到人躺下就报警——老人午睡、弯腰捡东西、蹲下系鞋带这些动作在单帧画面上和跌倒极其相似。这也是为什么我坚持要用两级架构第一级YOLOv8n-Pose负责看到人、拿到骨架第二级LSTM负责看骨架怎么动、判断是不是摔。单靠任何一级都做不好这件事。2. 整体架构设计与选型逻辑2.1 两级流水线的设计哲学整套系统的数据流是这样的摄像头采集视频帧 → YOLOv8n-Pose逐帧推理输出人体边界框和17个COCO关键点坐标 → 关键点经过归一化和坐标变换形成时序特征序列 → 滑动窗口送入LSTM输出跌倒/正常的分类结果 → 连续多帧判定为跌倒则触发报警。为什么拆成两级而不是端到端一个模型搞定这里有几个很实际的考量。第一可解释性和可调试性。端到端模型输出一个概率值你不知道它为什么判断跌倒出了问题很难定位。而两级架构里如果误报我可以直接看关键点序列一眼就能看出是姿态估计错了还是时序判断错了。第二算力分配。姿态估计是逐帧的密集计算适合放在NPU上跑LSTM是轻量级的时序推理参数量小放在CPU上跑完全够。第三复用性。姿态估计模型是通用的换一个时序分类头就能做别的行为识别比如久坐提醒、异常徘徊检测。提示两级架构的代价是引入了级联误差。如果第一级关键点抖动严重第二级再强也救不回来。所以姿态估计的稳定性是整个系统的地基后面我会专门讲怎么处理关键点抖动。2.2 为什么是YOLOv8n-Pose而不是其他姿态模型姿态估计模型的选择其实不少MediaPipe、OpenPose、HRNet、MoveNet都能做。我最终选YOLOv8n-Pose理由很具体单阶段、速度快。YOLOv8-Pose把检测和关键点回归放在一个网络里一次前向就出结果不像两阶段方法要先检测再裁剪再回归。n版本是nano参数量约3.2M在K230D的NPU上能跑到比较理想的帧率。部署友好。Ultralytics的生态成熟导出ONNX、转K230D的kmodel都有现成路径省去了大量适配工作。精度够用。跌倒检测不需要毫米级的关键点精度17个COCO关键点里我们真正依赖的是肩、髋、膝、踝这几个大关节YOLOv8n-Pose在这些大关节上的稳定性完全够。对比一下几个候选方案方便你根据自己情况取舍模型参数量关键点数边缘部署难度适用场景YOLOv8n-Pose~3.2M17低官方工具链本项目首选MediaPipe Pose~3M33中依赖特定运行时手机端、桌面端MoveNet Lightning~2.9M17中TF生态移动端HRNet-W32~28M17高算力要求大服务器高精度场景2.3 为什么用LSTM做时序判断跌倒的本质是一个过程不是一帧的状态。从站立到失衡到触地通常持续0.5到1.5秒。LSTM长短期记忆网络天生适合处理这种变长时序它通过门控机制记住之前几帧人是什么姿态从而区分快速下蹲后站起和快速下蹲后没起来。有人会问用1D卷积或者Transformer行不行当然行。1D卷积更快但感受野有限对长时序依赖建模弱一些Transformer表达能力强但参数量大、推理慢在边缘设备上不划算。LSTM是精度、速度、参数量三者之间比较平衡的选择而且PyTorch实现简单转ONNX也顺。这里说的LSTM就是Hochreiter和Schmidhuber在1997年提出的那个经典结构不是什么新东西但在这个任务上就是好用。2.4 硬件选型K230D Box的定位K230D是嘉楠科技的一款边缘AI芯片K230D Box是正点原子基于它做的开发板套件。它的核心卖点是在很低的功耗下提供了可观的NPU算力适合做视觉推理。选它的原因一是NPU对量化后的模型加速明显YOLOv8n-Pose量化后能实时跑二是接口齐全摄像头、屏幕、网络都有做原型验证不用额外画板子三是社区资料相对丰富遇到问题能查到。不过要提醒一句K230D的NPU对算子支持有偏好不是所有PyTorch算子都能顺利转换。后面部署章节我会讲怎么规避。3. 核心细节解析与实操要点3.1 关键点数据的预处理决定成败的一步从YOLOv8n-Pose拿到的是17个关键点的像素坐标直接喂给LSTM是不行的因为像素坐标受摄像头分辨率、人离镜头远近、人在画面中位置的影响极大。同一个人做同一个动作站在画面左边和右边坐标完全不同。所以预处理必须做归一化。我的做法是以人体边界框为参考系做归一化。具体来说把每个关键点的坐标减去边界框左上角坐标再除以边界框的宽和高得到0到1之间的相对坐标。这样无论人在画面哪个位置、离镜头多远骨架的相对形状是一致的。这一步做完还要做中心化把髋部中心左右髋的中点平移到原点消除整体平移。def normalize_keypoints(kpts, bbox): # kpts: (17, 2) 像素坐标 # bbox: (x1, y1, x2, y2) x1, y1, x2, y2 bbox w x2 - x1 h y2 - y1 # 相对坐标归一化 norm (kpts - np.array([x1, y1])) / np.array([w, h]) # 以髋部中心为中心化 hip_center (norm[11] norm[12]) / 2.0 norm norm - hip_center return norm.flatten() # 34维向量除了空间归一化还要处理缺失关键点。遮挡、出画都会导致某些关键点置信度很低。我的策略是置信度低于阈值的点用上一帧的值填充如果连续多帧都缺失就用该点的历史均值填充。千万别直接填0填0会让LSTM以为关节跑到了原点产生严重误判。3.2 时序窗口的设计多长才合适LSTM的输入是一个滑动窗口内的关键点序列。窗口长度是个关键参数。太短捕捉不到完整的跌倒过程太长延迟高且引入无关信息。我实测下来16到24帧比较合适。假设摄像头15fps24帧就是1.6秒刚好覆盖一次跌倒的完整过程。窗口的滑动步长也要考虑。如果步长等于窗口长度就是非重叠分窗延迟大如果步长是1每帧都推理一次算力浪费。我取的是步长4即每4帧做一次LSTM推理兼顾实时性和算力。特征维度上除了34维的归一化坐标我还额外加了几个手工特征效果提升明显一是躯干与水平面的夹角判断是否躺倒二是髋部中心的垂直速度判断下坠快慢三是左右肩髋的宽高比躺倒时这个比例会突变。这些特征给LSTM提供了很强的先验让它学起来更快。3.3 LSTM网络结构的具体设计网络本身不复杂两层LSTM加一个全连接分类头。第一层LSTM隐藏单元64第二层32最后接一个全连接层输出2类跌倒/正常。中间加了Dropout防止过拟合因为跌倒样本天然比正常样本少。import torch import torch.nn as nn class FallLSTM(nn.Module): def __init__(self, input_dim40, hidden164, hidden232, num_classes2): super().__init__() self.lstm1 nn.LSTM(input_dim, hidden1, batch_firstTrue) self.dropout nn.Dropout(0.3) self.lstm2 nn.LSTM(hidden1, hidden2, batch_firstTrue) self.fc nn.Linear(hidden2, num_classes) def forward(self, x): # x: (batch, seq_len, input_dim) out, _ self.lstm1(x) out self.dropout(out) out, (h, c) self.lstm2(out) # 取最后一个时间步 out self.fc(h[-1]) return out输入维度40 34归一化坐标 6手工特征。这个规模在CPU上单次推理不到1毫秒完全不是瓶颈。3.4 数据集的构建与标注策略这是整个项目最费精力也最容易被低估的部分。公开的跌倒数据集有但大多场景单一、视角固定直接拿来训练在实际家庭环境里泛化很差。我的做法是公开数据集打底 自采数据微调。自采数据时我找了几个志愿者模拟了这些动作正常行走、坐下、起立、弯腰捡东西、蹲下、躺下睡觉、快速下蹲、以及真实的跌倒在软垫上。关键是要把易混淆动作采足因为误报主要就来自这些。每个动作至少采20次不同角度、不同光照。标注上我不是逐帧标跌倒/正常而是标动作片段一段视频标一个动作类别然后在训练时用滑动窗口切分窗口内如果包含跌倒的触地时刻就标为正样本。这样标注效率高很多。注意正负样本比例要控制。跌倒样本占比太高模型会倾向于报跌倒太低又学不到。我最终控制在1:3左右配合类别权重损失函数。4. 实操过程与核心环节实现4.1 环境搭建与模型训练训练在带GPU的机器上做推理部署到K230D。环境用conda建一个干净的Python 3.9环境装PyTorch、Ultralytics、onnx、onnxruntime。conda create -n fall python3.9 conda activate fall pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics onnx onnxruntime numpy opencv-python姿态估计部分我直接用Ultralytics预训练的yolov8n-pose.pt没有重新训练因为COCO上预训练的模型在人体关键点上已经足够好。如果你有特定场景需求比如特殊工装、特殊视角可以拿自己的数据微调。LSTM的训练脚本核心逻辑先离线把所有视频用YOLOv8n-Pose跑一遍把关键点序列缓存成npy文件避免训练时反复推理。然后写一个Dataset类读取这些序列滑动窗口切分。class FallDataset(torch.utils.data.Dataset): def __init__(self, seq_list, labels, window24, stride4): self.samples [] for seq, label in zip(seq_list, labels): for i in range(0, len(seq) - window 1, stride): self.samples.append((seq[i:iwindow], label)) def __len__(self): return len(self.samples) def __getitem__(self, idx): seq, label self.samples[idx] return torch.tensor(seq, dtypetorch.float32), torch.tensor(label)训练参数Adam优化器学习率1e-3batch size 32训练50个epoch用早停防止过拟合。损失函数用带类别权重的CrossEntropyLoss。4.2 模型导出与K230D部署训练好的LSTM导出成ONNX再通过K230D的工具链转成kmodel。这里有个坑K230D的NPU对LSTM的支持不如对卷积友好如果直接转LSTM算子可能失败。我的解决方案是把LSTM在导出时展开成静态图或者干脆把LSTM放到CPU上跑只把YOLOv8n-Pose放到NPU上。实测下来LSTM在CPU上跑完全不影响整体帧率因为它的计算量相比姿态估计小两个数量级。YOLOv8n-Pose的部署流程先用Ultralytics导出ONNX注意输入尺寸固定为640x640然后用量化工具做INT8量化。量化时要用一批真实场景的图片做校准校准集要覆盖不同光照和场景否则量化后精度掉得厉害。from ultralytics import YOLO model YOLO(yolov8n-pose.pt) model.export(formatonnx, imgsz640, simplifyTrue, opset11)导出后检查一下ONNX模型的输入输出节点名K230D的转换工具需要指定。转换命令大致如下具体以官方工具版本为准# 伪代码实际以官方nncase工具为准 nncase compile yolov8n_pose.onnx yolov8n_pose.kmodel \ --target k230 --input-type uint8 --quantize4.3 主循环与报警逻辑K230D上的主程序是一个循环抓帧 → NPU推理姿态 → 预处理关键点 → 送入LSTM → 判定。报警逻辑不能只看单次LSTM输出要做时序平滑。我的做法是维护一个长度为5的判定队列5次里有3次以上判为跌倒才触发报警避免单帧抖动误报。from collections import deque judge_queue deque(maxlen5) FALL_THRESHOLD 0.7 def check_fall(lstm_prob): judge_queue.append(1 if lstm_prob FALL_THRESHOLD else 0) if sum(judge_queue) 3: return True return False报警触发后本地蜂鸣器响、屏幕弹提示同时可以通过网络推送一条消息给家属。这里只做本地报警不涉及任何外部服务符合隐私要求。4.4 实测帧率与资源占用在K230D Box上实测640x640输入下YOLOv8n-Pose量化后单帧推理约30到40毫秒加上前后处理整体能跑到15到20fps。LSTM单次推理不到1毫秒可以忽略。内存占用方面模型加载后常驻内存约几十MBK230D的配置完全够用。功耗上整板运行在几瓦级别长期通电没有压力。环节耗时ms说明图像采集与预处理5-8缩放、格式转换YOLOv8n-Pose推理30-40NPU加速INT8量化关键点后处理2-3归一化、特征计算LSTM推理1CPU报警判定与输出1-2队列平滑合计约40-55对应18-25fps5. 常见问题与排查技巧实录5.1 误报太多怎么办这是新手最常遇到的问题几乎每个人都会经历。误报的来源主要有三个一是关键点抖动二是易混淆动作三是判定逻辑太激进。针对关键点抖动加时序滤波。我用的是简单的一欧元滤波One Euro Filter它对低速运动平滑强、高速运动延迟小非常适合人体关键点。实测能明显减少静止时的抖动误报。针对易混淆动作回到数据层面解决。把误报的视频片段截出来标成负样本加入训练集重新训练。我前后迭代了三轮误报率从最初的每小时好几次降到一天偶尔一次。针对判定逻辑调整阈值和平滑窗口。阈值从0.5提到0.7平滑窗口从3提到5误报明显下降代价是响应稍微慢一点。这个权衡要根据实际场景调。5.2 漏报怎么排查漏报通常发生在快速跌倒或者部分遮挡的情况。快速跌倒时如果帧率不够可能整个跌倒过程只有几帧LSTM窗口里信息不足。解决办法是提高帧率或者用更短的窗口配合更高的推理频率。遮挡问题更棘手。如果人摔在沙发后面关键点大面积缺失姿态估计直接失效。这种情况单靠视觉很难解决实际产品里通常会融合毫米波雷达或者压力垫做多模态。纯视觉方案要接受这个局限在部署时尽量选无遮挡的视角。5.3 量化后精度下降严重INT8量化对姿态估计的影响比对分类任务大因为关键点回归对数值精度敏感。如果量化后关键点明显偏移检查两点一是校准集是否覆盖了实际场景二是是否对敏感层做了混合量化部分层保持FP16。K230D的工具链支持混合量化把姿态回归头那几层保留高精度能明显改善。5.4 常见问题速查表现象可能原因排查方向解决手段频繁误报关键点抖动看静止时关键点是否跳加一欧元滤波频繁误报易混淆动作回放误报片段补负样本重训漏报帧率不足统计跌倒过程帧数提高帧率或缩短窗口漏报遮挡看关键点置信度多模态融合量化掉点校准集偏差对比量化前后关键点混合量化、补校准集部署失败算子不支持看转换日志LSTM放CPU、展开静态图5.5 几个我踩过的坑第一个坑是坐标归一化的参考系选错。我一开始用整幅画面做归一化结果人一走动坐标就全变了LSTM完全学不到东西。换成边界框归一化后立刻好转。第二个坑是训练和推理的预处理不一致。训练时用的归一化参数和部署时的不一样导致模型在板子上表现和PC上差很多。后来我把预处理写成一个函数训练和推理共用同一份代码问题解决。第三个坑是忽视负样本的多样性。早期负样本只有正常行走结果模型把坐下也判成跌倒。补了各种日常动作后泛化能力大幅提升。第四个坑是报警逻辑太敏感。一开始单次LSTM输出超过阈值就报警结果老人弯腰系鞋带都能触发。加了时序平滑后体验好了很多。6. 后续可以怎么扩展这套框架的延展性其实很好。把LSTM的分类头从2类扩到多类就能同时做跌倒检测、久坐提醒、异常徘徊识别。把单目标跟踪加进来可以区分多个家庭成员避免张冠李戴。再进一步把姿态序列做成更丰富的动作表征甚至可以做简单的康复动作规范性评估。硬件上如果算力允许可以把姿态估计和时序模型合并成一个端到端网络减少级联误差。但就目前K230D的算力而言两级架构是更务实的选择。我个人在实际操作中的体会是跌倒检测这类项目算法只是一半另一半是对真实场景的理解。你得真的去看老人怎么生活、怎么动作才能设计出靠谱的判定逻辑。纯在实验室调模型永远调不出能用的产品。最后分享一个小技巧部署前一定要拿真实场景、真实老人或模拟老人动作的志愿者测至少一周把误报漏报都记录下来这比任何离线指标都有说服力。
返回列表