1. 整体方案拆解:为什么把姿态解算和时序识别拆成两段
边缘智能跌倒检测这件事,我前后折腾了大半年,从最早"一个分类网络直接看图像判断有没有摔倒",一路改到现在的 YOLOv8n-Pose 加 LSTM 的两段式结构,中间推翻过好几次方案。写这篇东西的起因很简单:身边几个做适老看护设备的朋友都在问同一件事——一块算力和内存都紧巴巴的边缘板子,到底能不能独立把跌倒这件事判准。我的结论是能,但前提是你不能指望一个模型干完所有活。这里的关键词是边缘智能跌倒检测、YOLOv8n-Pose 关键点、LSTM 时序分类、K230D 部署,四个东西凑在一起才成立,缺一块都不好用。
这篇文章适合两类人看:一类是手上已经有边缘开发板、想做一个真正能跑的视觉应用的工程师,另一类是被"图像直接二分类判跌倒"的高误报率折磨过、想知道问题出在哪的算法同学。我会把特征怎么设计、模型怎么压、板子上双核怎么分工、量化之后哪些坑会冒出来,全部摊开讲。所有参数和流程都是我自己在板子上跑出来的,不是抄文档;如果某个地方是我的经验判断而非唯一答案,我会直接说明。
先说最核心的一次思路转变。最初我做的方案是:把摄像头画面缩到 224×224,丢进一个轻量分类网络,输出"跌倒/正常"两个类别。这个方案在自建数据集上准确率能到 96%,看起来很美,拿到真实场景一周就崩了——老人蹲下捡东西、坐在低矮沙发上、弯腰系鞋带,全都会触发告警。原因很朴素:单帧图像里,"蹲下"和"摔倒中途"这两件事的视觉形态几乎是同一个东西,你让网络只看一张图去区分"正在倒"和"已经蹲好",它在信息上就是不够的,这不是模型容量的问题,是输入维度的问题。跌倒本质上是一个随时间演化的事件,不是一种静态姿态。这句话想明白之后,后面的架构选择就顺理成章了。
第二段转变是把"图像"换成"骨骼点"。把人体 17 个关键点提出来之后,送进后续模型的数据从 224×224×3 的像素块变成了一串坐标数字,尺寸直接小了三个数量级,带来的好处是连锁的:时序模型可以做得非常小、可以放在 CPU 核上跑;原始画面不需要离开设备,隐私上更干净;而且一旦检测出错,你能清楚地看到是哪个关节的轨迹出了问题,而不是面对一个黑盒概率值发呆。代价也很明确——关键点检测本身在边缘端要吃掉绝大部分算力,YOLOv8n-Pose 在 K230D 上的帧率直接决定了整个系统的响应上限,这一点后面会详细算账。
所以最后定下来的流水线是三段:大核负责图像采集与关键点推理,小核负责时序分类与状态机,两者通过共享内存环形缓冲交接数据。这个切分不是拍脑袋定的,是被两块核的算力特性和实时性要求逼出来的,下面一节把账算清楚。
1.1 算力与内存的账必须先算,不能先写代码
很多人上手就在 PC 上训模型,训完发现板子跑不动,回头改架构,来回折腾。我的习惯是先算预算,再动手。这块板子的关键约束有三条:KPU 的 INT8 算力在 1 TOPS 量级,但那是理论峰值,实际能用到三到四成就算不错;DDR 容量很紧,几个张量同时驻留就要开始担心;CPU 是大小核异构,大核主频高但要做的事也多,小核慢但空闲。
YOLOv8n-Pose 的输入我最终定在 320×320。为什么不是 640?640 输入在 KPU 上的推理耗时大约是 320 的四倍左右,实测下来连 8 fps 都摸不到,端到端延迟直接破 400 ms,对跌倒这种要求"事后几秒内响应"的场景其实还能忍,但摄像头预览会明显卡顿,用户体验很差。为什么不是 256?256 在人体占画面比例较小时(老人坐在房间另一头),手腕和脚踝的关键点会频繁丢失,置信度掉到 0.3 以下,特征维度里一堆空洞,时序模型等于在猜。320 是一个我试过的平衡点:站着的人占画面高度 40% 以上时,主要关节的稳定检出率能到 90% 出头。
内存这块要说细一点。320×320×3 的输入张量,INT8 下是 300 KB;中间层激活值 nncase 会做内存复用,但峰值仍然要预留一两 MB;检测头输出的张量不大,可是输出布局的转换很容易被忽略——KPU 输出的特征图排布和 CPU 侧期望的连续数组往往不一致,直接整块 memcpy 再重排,一帧多花几毫秒很常见,正确做法是在读取时按块做 stride 跳转,只对有效区域做拷贝。这个细节我在第 3 章会给出具体写法。
时序模型这边的预算宽松得多。我用的是两层 LSTM,隐藏单元 48,输入 24 维,序列长度 24 帧。参数量算下来不到 5 万,float32 存储也就 200 KB 左右,放小核上跑一次前向是微秒级的。这里有个重要决定:LSTM 我没有丢给 KPU,而是手写在小核上跑。原因后面第 3 章讲,简单说就是量化工具链对循环结构的支持不理想,硬塞进去精度损失比收益大。
| 模块 | 运行位置 | 输入规格 | 实测单次耗时 | 内存占用 |
|---|---|---|---|---|
| 关键点检测 | KPU + 大核调度 | 320×320×3 INT8 | 45 到 60 ms | 峰值约 2.5 MB |
| 关键点后处理 | 大核 | 输出张量解码 + NMS | 6 到 9 ms | 几十 KB |
| 特征构造 | 小核 | 24 维 × 24 帧 | 小于 1 ms | 约 3 KB |
| LSTM 前向 | 小核 | 24×24 序列 | 小于 1 ms | 约 200 KB |
| 状态机与输出 | 小核 | 类别概率 | 可忽略 | 可忽略 |
这张表是我连续跑了两个小时、取稳定段平均值得到的,不是理论值。可以看到端到端的大头完全在关键点检测上,所以后续所有优化精力都应该往这里投,而不是去优化那个已经很轻的 LSTM。这也是我特别想强调的一点:边缘项目的优化顺序必须由 profile 数据决定,凭直觉优化十有八九优化错了地方。我见过有人在时序模型上折腾了一周,把 LSTM 换成 TCN 又换回去,最后发现整体帧率一点没变,因为瓶颈根本不在那儿。
1.2 模块划分与数据交接的设计取舍
三段式流水线听着简单,真正难的是中间的交接。关键点检测出帧的节奏是不均匀的——这一帧 48 ms,下一帧可能因为画面复杂度上升变成 70 ms,而 LSTM 是按固定窗口滑动的。如果直接用"每检测一次算一帧"的方式喂时序模型,那么序列里的时间间隔是抖动的,速度特征就会被严重污染:同样是 0.2 秒内下降 0.3 个归一化单位,在快帧率下是缓慢移动,在慢帧率下就变成了急速坠落,模型学到的其实是帧率噪声。
我的处理办法是双时间轴:内部维护一个单调递增的时间戳,所有运动类特征在计算时都除以真实 dt 做归一化,把"每帧位移"换算成"每秒位移";同时给序列补一个 dt 通道,让模型自己也能感知节奏变化。这个改动看起来很小,实际把误报率压下来了差不多三成。另一个做法是用固定采样率重采样,但那样会引入插值误差,而且在帧率剧烈波动时插值出来的轨迹是假的,我试过,不如直接喂真实 dt。
交接用的结构是一个固定长度的环形缓冲,长度 32(比序列长度 24 多留几个冗余槽位),写指针由大核推进,读指针由小核维护,两边不做锁,只用原子读写保证一致性。溢出策略是覆盖最旧的一帧,而不是阻塞等待——边缘设备上阻塞一次就可能连锁导致整个管线卡死,宁可丢数据也不能卡。这个选择牺牲了一点点完整性,换来了系统的确定性,我认为是划算的。
提示:环形缓冲里每一帧除了 24 维特征,还必须带上时间戳、序号和有效标志位。少了有效标志位,你会在关键点大面积丢失的时候得到一堆全零向量,模型会把它当成"人突然消失",行为非常诡异。
2. 从 17 个关键点到能判断的 24 维特征
这一段是整个项目里我最花时间、也最值钱的部分。模型结构本身没什么新意,LSTM 就是教科书上的 LSTM,真正决定成败的是你喂给它的东西。我前后设计过三版特征,第一版 44 维,训练损失降不下去;第二版 16 维,欠拟合;最后收敛到 24 维,才算稳定。下面把这个演进过程里的判断依据讲清楚,你照着改自己的场景会省很多时间。
先明确关键点的定义,这块用的是 COCO 那套 17 点顺序:鼻子、左右眼、左右耳、左右肩、左右肘、左右腕、左右髋、左右膝、左右踝。面部五个点我全部丢掉了——老人低头、侧脸、戴帽子时这几个点抖动最厉害,而且对判断跌倒几乎没贡献,留着纯属增加噪声维度。实际用到的只有 12 个躯干与四肢点。
2.1 归一化做不对,后面全白搭
原始坐标是像素值,直接喂进去,模型会学到跟人距离摄像头远近强相关的规律:站在远处的人跌倒,位移绝对值很小,模型可能判不出来。所以必须归一化,问题是怎么归一化。
我试过三种。第一种按图像宽高归一化到 [0,1],简单,但距离不变性很差。第二种用人体检测框的对角线长度做尺度归一,距离不变性好了,可是框本身在跌倒过程中会剧烈变形,导致归一化基准自己也在动,反而引入了伪运动。第三种是我最后采用的:以髋关节中心为原点,以"肩中心到髋中心的距离"为尺度单位。选肩髋距离做尺度的理由是它在站立、坐下、跌倒三种状态下变化最小,是个相对稳定的身体标尺;选髋中心做原点的理由是人的整体位移主要由髋部体现,把原点绑在髋上,剩下的坐标变化就纯粹是姿态变化,这正是我们想分离出来的信号。
除了位置,方向也必须处理。同样的"低头弯腰",人正对镜头和侧对镜头,投影到二维图像上的关键点分布完全不同。我用了一个近似办法:不修正方向,而是在特征里显式加入躯干倾角的 sin 和 cos 两个分量,让模型有据可依。严格的朝向归一化需要三维信息,二维关键点做不了,强行做会引入更大的误差,这是二维方案的固有局限,得认。
2.2 24 维特征的构成与每一维的用意
下面这张表是我最终定版的输入向量,每帧 24 维。写出来是为了让你直接抄,但更希望你看懂每一维在拦什么。
| 维度序号 | 特征含义 | 归一化方式 | 主要作用 |
|---|---|---|---|
| 1 到 2 | 髋中心相对画面中心的偏移 | 除以肩髋距离 | 捕捉整体位移 |
| 3 到 4 | 躯干与竖直方向的 sin、cos | 无需归一 | 判断倾斜与横卧 |
| 5 | 检测框宽高比 | 无需归一 | 站立高瘦、躺卧宽扁 |
| 6 | 头部到踝的竖直投影距离 | 除以肩髋距离 | 身高塌缩的强信号 |
| 7 到 10 | 左右肩、左右髋相对髋中心的 y | 除以肩髋距离 | 上半身折叠 |
| 11 到 14 | 左右膝、左右踝相对髋中心的 y | 除以肩髋距离 | 下肢支撑是否丢失 |
| 15 到 16 | 双腕相对髋中心的 x、y | 除以肩髋距离 | 支撑、抓握、护头动作 |
| 17 | 关键点平均置信度 | 无需归一 | 反映遮挡与检出质量 |
| 18 | 有效关键点比例 | 无需归一 | 给模型缺失程度的显式信号 |
| 19 到 20 | 髋中心每秒位移、每秒加速度 | 除以肩髋距离并除以 dt | 坠落速度,最关键的判据 |
| 21 | 躯干倾角每秒变化率 | 除以 dt | 快速翻转 |
| 22 | 检测框对角线长度的变化率 | 除以 dt 并对尺度归一 | 身体塌缩速度 |
| 23 | 帧间关键点位移均值 | 已归一 | 整体运动剧烈程度 |
| 24 | 时间间隔 dt | 秒 | 让模型感知节奏 |
第 19 到 22 这四维是整个系统的灵魂。我做过消融,把它们全去掉之后,模型的召回从 95% 掉到 81%,而且是那种"慢慢坐下"混进"跌倒"的典型错误——因为纯姿态特征确实分不清缓慢下滑和慢慢下蹲,只有速度信息能区分。反过来,如果只留速度类特征,去掉姿态类特征,误报率会飙升,因为剧烈运动的东西太多了:跳跃、快速转身、打喷嚏时身体猛的一晃,全都是高速度。姿态负责定性(现在是横的还是竖的),速度负责定量(变化有多剧烈),两者缺一不可。
第 24 维那个 dt 通道值得单独说。加它之前,我遇到过一个很隐蔽的问题:在光线好的时候系统工作正常,一到傍晚,摄像头自动降帧率,速度特征整体放大,误报率翻倍。当时的排查花了我整整两天,最后用打印时间戳的方式才定位到。加进 dt 通道、并且把所有速度特征除以真实 dt 之后,这个问题彻底消失了。这也是我建议所有做边缘时序项目的人必做的功课:时间戳一定用单调时钟,绝对不能用帧序号。
2.3 关键点丢失时怎么办,这是工程分水岭
真实场景里关键点一定会丢。老人盖着毯子坐在沙发上,下半身关节全部低置信度;走到画面边缘,半个身子被裁掉;夜里红外补光下腿部轮廓模糊。这时候如果你把丢失点当零处理,特征向量会突然出现一堆"0",模型看到的是一个荒谬的人体骨架,输出会变成随机值。
我的处理分三层。第一层,置信度低于 0.35 的点标记为无效,不参与任何角度和距离计算。第二层,无效点的位置用上一帧位置加一次速度外推补齐,但同时在有效比例通道里如实反映缺失程度。第三层,如果单帧有效点少于 6 个,这一帧直接不进序列,用上一帧特征原地重复填充,并维持 dt。三层的顺序不能颠倒,尤其是第一层——先判有效性再做几何计算,否则你会在无效点上算出除零或者离谱的角度值,后面全部污染。
这套机制上线之后,毯子场景下的误报基本归零,代价是明显的遮挡下响应会延迟一两帧,可以接受。
3. 模型量化与板端部署的实操全流程
训练在服务器上做完,导出 ONNX,接下来才是真正见真章的部分。这一章把从权重文件到板子上跑起来的过程按顺序写一遍,包括我踩过的坑。
3.1 导出与量化的关键参数
导出 ONNX 的时候有两个坑。第一个是输出节点的选择,YOLOv8-Pose 的检测头输出包含框坐标、类别置信度、关键点坐标与关键点置信度几部分,导出前要确认算子融合没有把关键点分支融掉,最保险的办法是导出后拿同一张图在 PyTorch 和 ONNX Runtime 上各跑一次,逐元素比对,误差在 1e-4 量级以内才算通过。这个比对步骤千万别省,我吃过一次亏:导出时因为动态维度设置没关干净,ONNX 图里留了一个动态轴,量化工具直接报错,白折腾半天。
第二个是输入尺寸固定。训练时我用的是 320×320,导出时把 dynamic 参数全部关掉,形状写死。量化工具对动态形状的支持普遍不好,写死之后校准流程顺畅得多。
量化用的是训练后量化,INT8,校准集我选了 400 张。这个数量不是随便定的:先试了 100 张,量化后的关键点位置抖动明显,尤其是手腕;加到 400 张,抖动基本消失。校准集的内容比数量更重要,必须覆盖你的部署场景:强光、弱光、逆光、室内、走廊、站姿、坐姿、躺姿、多人、单人、靠近、远离,我按这些条件分层各挑了二十来张。如果校准集全是白天正常站姿的图像,量化出来的模型在弱光下会表现得一塌糊涂,这个现象非常常见。
注意:量化后检测头的输出置信度分布会整体偏移,原来在 float 模型上能用的 0.5 阈值,在量化模型上可能等效于 0.4 或 0.6。阈值必须在板子上用真实图像重新标定,不能沿用 PC 上的值。我为此专门写了个小工具,把一批标注图跑一遍,输出置信度直方图,手动找到最佳分割点。
量化前后我做了一轮对比,用同一批 200 张测试图,比对关键点位置的均方误差(以肩髋距离归一化):
| 版本 | 关键点定位误差 | 单帧推理耗时 | 模型体积 |
|---|---|---|---|
| float32(PC 参考) | 基准 | 不适用 | 约 6.8 MB |
| 量化 INT8(板端) | 增加约 3% 到 5% | 45 到 60 ms | 约 1.9 MB |
3% 到 5% 的定位误差增量,对跌倒判断几乎没有影响,因为时序模型看的是趋势不是精确位置,但对那个"每秒加速度"特征有轻微影响,表现为偶尔出现尖锐脉冲。我的应对是在特征端加了一阶指数平滑,系数取 0.5,脉冲就被吃掉了。
3.2 板端推理管线的搭建
板端的代码结构我大致分成四层:采集层、推理层、后处理层、应用层。采集层负责取流和格式转换,注意 K230D 的摄像头输出格式和 KPU 期望的输入格式往往不一致,中间要做一次颜色通道重排和归一化,这一步如果用 CPU 逐个像素做,能耗和耗时都不低,能走硬件加速就走硬件加速。推理层调用 nncase 的运行时接口,把输入张量填进去、触发一次推理、等结果。后处理层做输出解码、置信度过滤和关键点重组。
这里有个具体的性能细节值得单说。检测头输出的关键点部分通常是一个形状为 [1, 17, H, W] 或者展平后的张量,从 KPU 内存里取出来的时候,如果按"[1,17,H,W] 逐元素遍历",缓存命中率极差。我改成先按维度 17 拆成 17 个块,每块内部按行连续读,耗时从 9 ms 降到 4 ms 左右,翻了一倍还多。这类优化在边缘端收益巨大,而且不改变任何算法逻辑。
后处理里的 NMS 也要留意,跌倒场景通常画面里就一两个人,可以设一个最大检测数上限(我设的是 3),超过的框直接丢掉,避免在多人画面里白白消耗时间。如果画面里检出了多个人,我取检测框面积最大的那个作为关注目标——优先照顾画面主视角的人物,这个策略在卧室、卫生间这类单人场景下完全够用。
3.3 双核分工为什么这么切
回到第 1 章留下的那个问题:为什么 LSTM 不放 KPU 上跑。原因有三条,都很实际。
第一,工具链对循环结构的量化支持确实不理想。LSTM 内部有逐时间步的递归、有门控的逐元素乘法,量化工具在遇到这类结构时会插入大量反量化再量化的节点,甚至直接把整个子图退回 float 执行,结果就是你以为它在 KPU 上跑,实际上速度没提升多少,精度还掉了。第二,LSTM 的计算量本身极小,45 ms 的关键点推理面前,几百微秒的 LSTM 前向完全是噪声,放哪跑都无所谓。第三,放 CPU 上可以用 float32 权重,精度更稳,调试也方便——你可以直接打印每一步的门控值,而在 KPU 上你只能看到一个黑盒。
所以最终的分工是:大核专心喂 KPU、做后处理、写环形缓冲;小核专心读缓冲、算特征、跑 LSTM、维护状态机。这样切之后,两块核的负载都比较均衡,实测大核占用在 60% 上下,小核 35% 上下,都留了余量。温度是关键约束,连续跑两小时,板子表面温度会上去,主频可能被压下来,帧率掉到原来八成左右。我的做法是在机壳上做了导热处理,另外在软件里加了个降级策略:如果连续检测到帧率低于某个阈值超过 30 秒,就自动把输入分辨率从 320 降到 288,保证响应延迟不崩。这是个典型的工程妥协,精度让位于可用性。
LSTM 前向我是用 C 手写的,两层、隐藏 48。权重在编译期直接烧进固件,避免运行时读文件。手写的时候最容易错的地方是遗忘门的顺序——不同框架的权重排布是 [i, f, g, o] 还是 [i, f, o, g],各家不一样,导权重的时候必须核对,我在这上面浪费了一个下午,输出全是 NaN,排查了很久才发现是权重顺序搞反导致门控饱和。
3.4 端到端联调与延迟测量
联调阶段我建议先测单模块延迟,再测端到端。端到端延迟的测法要严谨:在摄像头前放一个秒表,用一个专门的小脚本记录"事件发生时刻"和"告警输出时刻",两者相减。不要用代码里打的时间差来近似,因为从画面产生到真正被采集,中间有曝光和传输的延迟,这部分你代码里看不到。
实测数据:关键点推理平均 52 ms,后处理 7 ms,特征与 LSTM 合计 2 ms,状态机判定需要累积 3 帧,也就是大约 180 ms 的确认窗口,再加上采集与显示的固有延迟,从人体真正落地到系统输出告警,总共在 250 到 320 ms 之间。这个数字对实际应用完全够用——即使按最保守的 500 ms 算,也远在人工发现之前。
顺带说一个非常容易被忽略的点:告警要不要立刻发。我的做法是不要。检测到跌倒后,先进入"疑似跌倒"状态,然后做两个确认——一是持续检测到人体处于低位姿态超过 2 秒,二是检测不到起身动作。两个条件都满足才真正触发。这个两秒的等待把误报率又压掉了将近一半,因为大量"跌倒"其实是老人自己慢慢坐到地上,二十秒内就爬起来了。用一个状态机替代瞬时判断,本质上是在时间维度上做了一次低成本的后处理,比再加一个复杂模型划算得多。
4. 常见问题排查与误报压制实录
这一章全是实打实踩出来的,按现象列,你可以直接拿去对照。
4.1 典型故障速查表
| 现象 | 最可能的原因 | 快速验证方式 | 处理办法 |
|---|---|---|---|
| 关键点整体抖动,手腕脚踝乱跳 | 量化校准集不足或分布单一 | 用静止站立画面观察点轨迹 | 扩充校准集,特征端加指数平滑 |
| 帧率逐渐下降,重启后恢复 | 内存泄漏或缓冲未回收 | 长时间运行打印剩余堆空间 | 检查环形缓冲读写指针与张量释放 |
| 弱光下频繁漏检 | 图像增益导致噪声,量化敏感 | 对比白天与夜间检出率 | 增加弱光样本校准,或加简单图像预处理 |
| 坐下、蹲下被判跌倒 | 缺少低位保持时长判断 | 回放坐姿样本看速度特征 | 引入姿态持续时长与起身检测 |
| 剧烈挥手、打喷嚏误报 | 速度特征尖峰未被抑制 | 打印速度通道数值曲线 | 速度特征做滑动平均,提高确认帧数 |
| 多人画面帧率骤降 | NMS 计算量随框数量上升 | 打印检测框数量 | 限制最大检测数,只处理主目标 |
| 长时间运行后输出异常定值 | 权重读取越界或小核栈溢出 | 加断言检查指针范围 | 检查手写前向的索引与栈大小 |
| 告警时间明显滞后 | 状态机确认窗口过长 | 记录各阶段时间戳 | 缩短确认帧数,改用时间窗而非帧数窗 |
这张表里的每一条我都真遇到过。挑几条展开说。
关键点抖动那条是我最早遇到的问题,当时甚至怀疑是摄像头的问题,换了镜头还是一样。后来打印原始输出才发现,是量化后手腕这个点的置信度在阈值附近反复横跳,导致这一帧有值、下一帧无效、再下一帧又有值,位置就跳来跳去。解决办法是给每个关节的置信度做时间上的平滑,同时对位置做一阶低通。低通系数不能太大,我试过 0.8,结果快速跌倒时的速度特征被削掉了一半,反而漏检。0.5 是我实测下来"能压抖动又不损失快速运动"的那个点。
"坐下被判跌倒"那条是这个项目的核心难题之一。我最后是用组合拳解决的:一是把"低位保持时长"作为独立特征,不是看当前姿态,而是看"已经保持低位多少秒";二是在状态机里加入起身检测——真正的跌倒在几秒内不会有明显的躯干直立动作,而主动坐下的人往往会调整姿势。这套逻辑加上去之后,误报从每小时若干次降到了可以接受的水平。顺带一提,这就是为什么我坚持用状态机而不是纯端到端模型:状态机里的每一条规则都是可解释、可调、可针对场景定制的,而模型的黑盒输出你没法跟产品经理解释。
4.2 阈值调优的方法论
阈值不要拍脑袋。我的做法是准备一个小型验证集,按场景分好组,然后写个脚本扫描阈值网格,输出每一组下的召回和误报次数,画成表格。工程上我关注的是两个数:召回率和每小时误报次数。前者不能低于某个底线(这是安全相关的功能,宁可多报也不能漏报),后者决定了产品能不能用——一天报几十次假警,用户第三天就把设备拔了。
这个平衡点没有通用答案,得看使用场景。我的取舍是:把召回做到尽可能高,然后靠状态机的多级确认来压误报,而不是靠提高模型阈值来压。因为提高模型阈值会同时压低召回,而状态机的时间确认只过滤掉那些"短暂、不持续"的事件,对真实跌倒几乎没有损失。模型负责不要漏,状态机负责不要滥,这个分工原则我建议所有人都采用。
4.3 现场部署的几个非技术细节
摄像头安装位置对效果影响极大,这一点很多算法同学会忽略。我的经验是:装在房间角落,斜向下俯视,视场要能覆盖主要活动区域,但不要正对窗户(逆光会让关键点全丢)。安装高度两米出头比较合适,太高了人会显得很小,关键点精度掉得厉害。
另一个细节是画面里人体的目标比例。如果老人习惯坐在房间一角,而那个位置在画面里只占很小一块,那这个区域的检测质量会很差。解决办法要么是调整安装位置,要么是给这个区域做感兴趣区域裁剪后单独推理——后者会增加算力开销,我的选择是前者,因为不花钱。
提示:上线前一定要用真实的部署环境采一段连续视频做离线回放测试,至少覆盖一整天。实验室里跑通和现场稳定运行之间,隔着光照变化、家具遮挡、人员走动这些你在实验室里想不到的变量。
5. 参数记录与踩坑复盘
把关键参数集中列一下,方便你直接照抄起步,然后按自己的场景微调。
| 参数 | 取值 | 调整依据 |
|---|---|---|
| 关键点模型输入 | 320×320 | 精度与帧率的平衡点 |
| 关键点置信度阈值 | 0.35 | 低于此值视为缺失 |
| 序列长度 | 24 帧 | 覆盖约 1.5 到 2 秒 |
| 滑窗步长 | 4 帧 | 重叠 20 帧,保证不漏事件 |
| LSTM 层数与隐藏单元 | 2 层,48 | 再大收益有限,反而增加内存 |
| 位置低通系数 | 0.5 | 压抖动又不损失快速运动 |
| 疑似跌倒确认时长 | 2 秒 | 过滤主动坐下 |
| 最大检测目标数 | 3 | 单人场景够用,省算力 |
| 量化校准集 | 400 张,分层采样 | 少了抖动,多了收益递减 |
关于训练数据这块单独说两句。跌倒样本天然稀少,我一开始只收集到几十条真实跌倒序列,训练出来严重过拟合。后来用了两条路补:一是用正常动作合成——把站立序列的末尾若干帧按重力加速度曲线插值变换成倒地姿态,虽然是合成的,但速度曲线的物理合理性保证了它有用;二是负样本的精挑细选,特别是"缓慢坐下""弯腰捡东西""单膝跪地"这三类,我把它们作为硬负样本单独加权。最终正负样本比控制在 1:8 左右,再高就开始损害召回了。
类别不均衡的损失函数我用了带权交叉熵而不是 focal loss,原因很朴素:focal loss 的超参对结果影响太大,调参成本高,而在这个样本量下带权交叉熵表现差不多,还更好解释。工程上我倾向于选那个"出问题了你知道该往哪调"的方案,而不是理论上更先进的那个。
最后一个想说的小技巧:给系统加一个"随时可回放的日志"。每一帧的关键点坐标、特征向量、模型输出概率、状态机状态,全部落盘,用环形文件控制体积。这东西在排查误报时价值巨大——用户说"昨天下午三点误报了一次",你把日志拉出来回放,几秒钟就能定位到是哪个特征出了问题。我加上这个功能之后,排查效率提升了不止一倍,之前只能靠猜,现在能看。存储开销不大,一段十分钟的低频记录也就几百 KB。
这套东西目前在一个小范围的看护场景里持续跑着,日常误报已经压到很低的程度,真正有价值的告警没有漏过。要说还有什么想改进的,我最想动的是把二维关键点升级成带深度估计的三维骨架,因为现在最大的残余误差还是来自视角变化——侧躺和正面躺,在二维投影下的特征差异很微妙,模型偶尔会犹豫。不过这需要额外的深度信息源,算力预算要重新算一遍,是下一个阶段的事了。眼下这套方案,对想在边缘设备上做实时行为识别的朋友来说,我认为已经是一个足够扎实的起点——先把双时间轴、有效性判定、状态机确认这三件事做对,你会发现原本一堆莫名其妙的误报,其实都有明确的来源。