1. 康复场景里"数动作"这件事,为什么不能靠人盯
1.1 康复师不够用,是这类系统存在的第一理由
先说个我在一个社区康复服务站观察到的真实场景:一位康复师一个上午要带六到八位老人轮换做训练,每位老人的方案还不一样,有的是手臂前举,有的是坐姿起立,有的是踝泵运动。康复师要盯动作做得标不标准,要数做了几组、每组几个,还要记录疲劳程度,一个流程下来人基本处于高度紧绷状态。老人这边呢,动作做得快慢不一,有的偷懒幅度不够,有的发力代偿,偏差在一两分钟内就积累起来了。这种"人盯人"的模式,在人力富余的大医院都未必转得开,更不要说社区和居家场景。
所以我做"长者康复活动识别系统"这个项目的出发点,不是想用AI取代康复师,而是想把人力从"数数盯人"这类低附加值劳动里解放出来,让康复师把精力放到真正需要专业判断的地方。系统要干的事很简单,概括成三条:识别老人在做什么动作,统计动作做了多少次,评估动作做得规不规范。这三件事听起来不复杂,但真正落到摄像头画面里、不同体型的老人身上、不同亮度的房间里,坑比预想的多得多。
1.2 从"打卡式监督"到"动作质量分析",系统定位的转变
最开始我和身边朋友讨论的时候,不少人第一反应是:这不就是个人体动作识别吗?找个开源模型,识别"站""坐""举手"不就行了。但真去啃康复医学的需求时才发现,康复训练监督和日常行为识别有本质区别。日常行为识别关心的是"正在做什么",比如判断一个人是在走路还是在跑步;康复训练监督关心的是"这个动作做到了什么程度",比如手臂有没有抬到规定高度、膝盖有没有伸直、躯干有没有过度前倾。
这就把系统定位完全改写了。它不是一个事后打标签的分类器,而是一个实时的、逐帧的、带容错的动作质量分析器。老人做了一次不规范的前举,系统不能等动作结束才说"刚才那次不合格",那样老人根本不知道自己错在哪;系统必须在动作过程中,要么提示"再抬高一点",要么在动作结束时给出明确的修正建议。这个"实时性+可解释性"的需求,决定了后面的算法选型和工程架构都跟普通的动作识别不太一样。
1.3 先圈定最小可用的功能边界
项目启动时我特意控制了一件事:不在第一版里塞太多功能。做这类系统特别容易陷入功能膨胀,什么动作都要支持、什么指标都要输出、什么平台都要适配。我当时和一位康复师朋友反复对需求,最后圈定了第一版的功能边界:
- 固定摄像头单人多动作识别,不做跨摄像头追踪
- 支持三类基础康复动作:坐姿手臂前举、坐姿起立-坐下、仰卧位踝泵运动
- 输出三样东西:动作类型、动作计数、标准度评分(按关节角度区间给判断)
- 实时性要求:从动作发生到系统给出提示,延迟不超过1秒
这个边界看起来保守,但足够覆盖社区康复站一天里80%以上的训练场景。更重要的是,它为后面的技术验证划了一条清晰的基线,所有算法选型都有一个明确的目标去对齐。事实证明这个决定帮了大忙,后面好几轮迭代都是在这个边界内完成的,没有因为需求发散而返工。
2. 系统骨架:单目摄像头方案与模块切分
2.1 没选穿戴设备的原因
着手设计系统的时候,摆在面前的第一条岔路是:用摄像头做视觉识别,还是用穿戴设备做惯性测量。穿戴设备有它的优势,比如不受遮挡影响、数据直接是关节运动学信号,但我在评估后放弃了这条路。原因有三个。
第一是部署成本。一套带多个传感器的穿戴设备,单价不低,社区康复站动辄要备十几套,还要考虑消毒、佩戴、充电管理,运维压力全落在本来就人手不足的站点身上。摄像头方案一台设备就够,几乎零穿戴成本。第二是老人接受度。不少老人对身上绑东西有天然的抗拒,尤其是需要长时间佩戴的场景,动不动就要摘下来;而摄像头挂在墙上,老人几乎感知不到它的存在。第三也是我认为最关键的一点:穿戴设备解决不了"动作质量"的完整评估。它测的是肢体加速度和角速度,能判断幅度,但很难判断关节对位关系,比如躯干代偿这类问题,摄像头在侧面视角下反而看得更清楚。
综合下来我选了单目摄像头方案。单目摄像头的劣势也很明显,深度信息缺失、对遮挡敏感,但这些问题可以通过合理的摄像头架设位置和应用层逻辑来缓解,后面会专门讲。
2.2 四个模块的分工
整个系统在逻辑上拆成了四个模块:采集模块、姿态估计模块、动作分析模块、反馈交互模块。拆分的依据是让每个模块可以独立替换和测试,避免一改算法就要动整个链路。
采集模块负责读摄像头帧、做基础的画面预处理,比如缩放、帧率控制。姿态估计模块负责从单帧图像中提取人体关键点坐标,这是整个系统的视觉基础。动作分析模块是整个系统的核心,它接收关键点序列,通过角度计算、规则判断和序列模型,输出动作类型、次数和标准度。反馈交互模块负责把分析结果翻译成老人和康复师能理解的信息,比如语音提示、界面显示。
四个模块之间通过统一的数据结构解耦。采集模块输出的关照每一帧,姿态估计模块输出带时间戳的关键点数组,动作分析模块消费这个数组,反馈模块只消费最终的判定结果。这个设计在后面调试时非常有用——某一个阶段出问题,直接对着数据流查就可以,不用从摄像头一路追到界面。
2.3 数据流和部署位置
实际的数据流是这样的:每一帧走完采集到反馈的完整链路,生成一次判定结果并落库。我在设计里特意加了一个轻量级本地缓存,用于保存最近30秒的关键点序列,便于现场回看和复现问题——注意这里保存的是关键点坐标,不是原始图像,这个取舍后面讲隐私的部分会展开。
部署位置选了两类环境做验证。一类是社区康复站的活动室,摄像头固定在墙角,离地面约2.2米,能覆盖3米乘4米的活动区域;另一类是模拟居家环境,摄像头放在电视柜一侧,俯视大约15度角。两处都跑通了,但暴露出的问题完全不同,社区康复站的问题是光线复杂,居家环境的问题则是视角受限。这些实测情况放到专门的章节讲。
3. 姿态估计选型:MediaPipe为什么拿下了这个活儿
3.1 三款主流方案的横向对比
姿态估计是整个系统的视觉地基,这块我做了不少功课。候选方案有OpenPose、MoveNet和MediaPipe Pose。OpenPose是最经典的方案,关键点检测精度高,但模型体积大、推理速度慢,在普通CPU上跑不到实时帧率,而且部署依赖较重。MoveNet是轻量级的代表,在移动端优化很好,精确度和速度的平衡做得很不错,但生态和配套工具相对少。
最后定下来的是MediaPipe Pose,理由不是它每项指标都最强,而是它在"精度、速度、易用性、跨平台"这四个维度上最均衡。它对Python和移动端都有完善支持,模型文件只有几兆大小,CPU上也能跑到30帧以上,且自带关键点平滑等配套能力。这个选型放在服务端和边缘设备都做过的老手眼里可能不够"极致性能",但对一个要快速落地、后续要长期迭代的项目来说,均衡性就是最大的生产力。
3.2 关键点坐标系与骨架抽象
MediaPipe Pose输出的是人体33个关键点的三维坐标(x、y和相对深度z,基于图像坐标系归一化)。它背后用的是BlazePose架构,分两步走:先检测人体区域,再在区域内回归关键点位置。这个"检测-对齐"两段式的好处是计算量可控,且在人物部分离开画面时仍然能维持一段时间的姿态追踪。
实际使用时,我主要关注上半身的12个关键点:鼻、双肩、双肘、双手腕、双髋、双膝、双脚踝等。系统只取其中与目标动作直接相关的点做计算。比如坐姿手臂前举,只用双肩、双肘、双手腕这几个点;坐姿起立,额外用到髋和膝。其他关键点并非无效信息,而是可以作为过滤条件,帮助判断画面中的人的朝向和姿态可信度,比如某个关键点置信度太低时,直接丢弃当前帧,避免垃圾进垃圾出。
3.3 在老人场景里特别在意的性能指标
姿态估计在年轻人身上表现很好,但老人场景有几个特别情况。
第一是老人穿的衣物宽松,比如棉袄、大衣,这会显著降低关键点的定位精度。第二是老人体型差异大,弯腰驼背的骨架形态和训练数据集里的标准姿态偏差明显。第三是动作幅度小、速度慢,关键点在连续帧之间的位移很小,反而容易受噪声干扰。
针对这几个情况,我在实际代码里加了一套卡控逻辑:每个关键点输出带置信度,低于0.5的直接置为无效;连续无效超过15帧则终止当前动作组判定,防止系统在人物丢失的情况下继续计数。这个"宁可漏报,不可误报"的原则贯穿了后面所有分析逻辑,在康复场景里,一个错误计数对训练方案的误导比漏掉一次计数严重得多。
4. 把"动作"变成"数字":关节角度与运动学特征
4.1 用余弦定理算关节角度
姿态估计拿到了关键点坐标,但坐标本身不能直接用于判断动作标准度。比如判断手臂前举是否到位,不能只看手腕的y坐标,因为老人的身高、坐高、离摄像头远近都不一样,绝对坐标会带来很大偏差。所以我把坐标转换成关节角度再做判断——角度是尺度不变的,受体型和拍摄距离的影响远小于坐标。
计算方式很简单,用三个关键点坐标算夹角,本质是余弦定理。以右肩关节角度为例,取右肩、右肘、右腕三个点,向量肩到肘、肩到腕之间的夹角即可。在Python里实现也就十几行:
import math def joint_angle(a, b, c): # b 是关节顶点,a、c 是它两侧的关键点 v1 = (a["x"] - b["x"], a["y"] - b["y"]) v2 = (c["x"] - b["x"], c["y"] - b["y"]) dot = v1[0] * v2[0] + v1[1] * v2[1] len_v1 = math.hypot(v1[0], v1[1]) len_v2 = math.hypot(v2[0], v2[1]) cos_theta = max(-1.0, min(1.0, dot / (len_v1 * len_v2))) return math.degrees(math.acos(cos_theta))这里的"b是顶点"是关键。算肩关节角,顶点就是肩膀;算肘关节角,顶点是肘;算膝关节角,顶点是膝。每个动作要算哪几个角度,在特征设计阶段就要和康复师逐一确认,这一点特别重要。算法工程师容易凭直觉选特征,但康复师会在意这个角度到底反映的是哪个环节的活动度,两者结合起来,特征才有临床意义。
4.2 三类典型康复动作的特征曲线
以第一版支持的三个动作来说明特征怎么定。
坐姿手臂前举,核心判据是肩关节屈曲角度。老人坐正后,手臂自然下垂时肩角约0度,前举到与地面平行时约90度。系统取双肩、双肘、双腕算出左右肩角,配合肘关节伸展角度(应接近180度)一起判断。这个动作的特征曲线是一条从基线爬升到峰值、再回落的近似梯形波。
坐姿起立-坐下,核心判据是躯干前倾角度和髋膝关节角度的联动。躯干前倾角由肩-髋-膝三个点算出,正常起立时躯干会有一个前倾-直立的过程,同时髋、膝角度会发生相应变化。这个动作的陷阱在于,很多老人会用猛蹬地面的方式代偿,躯干前倾角度很大但大腿发力不足。所以单看某个角度不够,要看髋、膝、躯干三个角度的联动关系。
踝泵运动就更有代表性了,它是躺着做的,脚踝做背屈-跖屈往复。这里只用到髋、膝、踝、脚趾四个点,核心是踝关节角度。老人仰卧时,小腿基本不动,脚掌绕踝关节转动,角度曲线是典型的正弦波。这也是三个动作里对关键点精度要求最高的一个,因为脚掌区域的点小、位移少,稍微有点噪声就会让曲线变得毛糙。
4.3 滤波、滑窗与时间尺度归一化
关键点坐标带有不同程度的抖动,直接算出来的角度曲线噪声不小。我先后试过均值滤波、中值滤波和一阶低通滤波,最终效果最好的是轻量级的指数平滑,公式是每一帧角度与前一帧平滑值按权重融合:
smoothed = alpha * current_angle + (1 - alpha) * previous_smoothedalpha取值在0.2到0.4之间,太小会滞后严重,太大会滤掉真实动作细节。这个参数在前举动作上试过很多次,alpha等于0.3左右,既能让曲线平稳,又不至于把一次两秒的完整动作拍扁。
时间尺度归一化是被我一开始忽略、后来踩坑才补上的。老人动作速度差异很大,同样一次手臂前举,有人1秒完成,有人要3秒。如果直接用固定长度的时间窗口去截取特征序列,快动作和慢动作得到的序列长度完全对不上。我的做法是先把一个完整动作周期重采样到固定长度(比如40帧),再做后续判断,这样模型和规则都不会被速度差异干扰。
5. 识别与判分:规则引擎打底,LSTM补位
5.1 动作计数:峰值检测加滞回区间
动作计数是整个系统最容易被低估的模块。最初我天真地以为,检测到角度曲线的一次波峰就算一次动作,结果老人坐着晃一下身体都算了一次前举。问题的本质是,单帧角度落在"动作区间"内并不等于正在做这个动作,噪声和无关晃动也会形成类似的信号。
解决思路是引入滞回区间。判断一次动作完成,需要角度曲线先跨过"启动阈值",再越过"确认阈值",然后回落穿过启动阈值——这样完整走完一轮才算一次。比如手臂前举,启动阈值设为30度,确认阈值设为75度,只有当角度从30度以下持续上升到75度以上,再回到30度以下,才判定完成一次。这个"双阈值加方向"的机制,能滤掉大部分在半途晃一下又缩回去的伪动作。
滞回区间的宽度需要根据每个老人的活动度现场调。有的老人肩关节活动受限,举到60度就再也上不去了,这时候康复师可以在系统里把确认阈值降到55度。系统做了一个参数配置界面,让康复师直接改阈值,而不是改代码,这个设计后期非常受欢迎。
5.2 标准度评估:阈值区间与容错设计
标准度评估的核心逻辑是给每个关键角度定义"合格区间"和"代偿信号"。以前举动作为例,合格条件是双肩角均达到设定的目标角度,且躯干没有明显后仰;如果肩角达到但肘关节弯了(肘角低于150度),这说明是缩着手臂举上去的,代偿明显,评分就是"不合格,请伸直手臂"。
每个动作都定义一套这样的角度规则,但直接硬判断会有一个问题:老人的动作天然有波动,偶尔一次没达标就报错,会严重打击训练积极性。所以我在规则里加了容错窗口,以连续3次动作为一个窗口,3次里至少2次达标才算合格;同时标准度评分采用"本次合格次数/总次数"的百分比。这么设计是跟康复师确认过的,康复训练的本质是循序渐进,系统要打分,但不能把老人打怕。
5.3 复杂动作交给LSTM序列模型
规则引擎在前举、踝泵这类角度特征明显的动作上表现很好,但在坐姿起立这种多关节联动动作上会感到捉襟见肘。躯干前倾和髋膝伸展的时序关系不是简单的"某角度跨过某阈值",而是一个动态的模式。这时候我引入了LSTM序列模型来补位。
模型输入是一个完整动作周期重采样后的特征序列,特征包含髋、膝、躯干前倾角以及它们的帧间差分,输出是动作类别(起立/坐下/不规范/其他)。训练数据来自我和团队在模拟康复场景里录制的样本,共采集了约2小时、超过1200个动作周期。训练时用到了数据增强,对角度序列做小幅度的随机缩放和偏移,模拟不同体型和动作幅度。
model = tf.keras.Sequential([ tf.keras.layers.LSTM(64, input_shape=(40, 6), return_sequences=True), tf.keras.layers.LSTM(32), tf.keras.layers.Dense(16, activation='relu'), tf.keras.layers.Dense(4, activation='softmax') ])LSTM上线后的准确率大约在93%左右,比纯规则引擎的88%高了不少,尤其是对"半起立又坐回去"这类模糊动作的判别改善明显。但我也保留了规则引擎作为兜底——当LSTM输出的置信度低于阈值时,回退到规则判断。这里面有个工程上的务实考量:LSTM这类模型,在没见过的新数据上可能给出离谱的结果,而规则的至少出错方式可预期,回退逻辑能把不可预期的风险锁住。
5.4 前端交互:给老人看的提示,给康复师看的报告
判分结果要落到两个不同的接收方。给老人看的是实时语音和简洁图标提示,比如"第5次前举,请再抬高一点",用本地合成的语音播报,声音温和但不啰嗦。给康复师看的是一天训练结束后的汇总报告,包含每个动作的完成次数、合格率、平均关节角度、连续不合格的动作片段。
这个交互设计其实对系统架构有影响。反馈模块虽然是链路末端,但它在数据上有自己的输入要求:语音提示需要的是"动作中的实时状态",报告需要的是"动作完成后的汇总统计"。所以我在分析模块里做了两个输出接口,一个提供流式的实时判定,一个写入结构化的事件日志。两块逻辑没有耦合,后面扩展新的报告维度时不需要动实时链路的代码。
6. 实测翻车现场:光照、遮挡与"不规范动作"处理
6.1 摄像头角度与距离的调整
第一轮实测就翻车了。我把摄像头放在活动室正前方,俯视角度大约5度,基本平视。结果发现,坐姿手臂前举时,当前举到与视线平行的角度,手臂在图像里正好和身体重叠,关键点检测把手臂和躯干连成一片,肘关节和手腕位置频繁跳变。这个问题的本质是二维投影里的自遮挡,单目摄像头天生没法把"手臂水平前伸"和"手臂收在胸前"完全区分开。
调整办法是把摄像头移到活动区域侧前方45度左右的位置,俯视角度增大到15到20度。把摄像头从"正对"改成"斜侧方"之后,自遮挡问题大幅缓解。这里有个经验值可供参考:摄像头与被拍摄对象的水平距离在3米左右,离地高度2.2到2.5米,这样的架设能让全身关键点在画面里的分布相对均匀,边缘畸变也不会太夸张。
6.2 光照变化和反光的坑
社区康复站的活动室采光很好,但这反而成了问题。午后阳光从东侧窗户射进来,老人背对窗户坐时,身体轮廓边缘出现强烈的高光,关键点检测在肩部位置反复抖动;而老年活动室常有的瓷砖地面,会产生大面积镜面反光,画面下半部分的踝关节关键点频繁丢失。
我的处理方案是双管齐下。硬件端在摄像头附近加了一个补光灯,但不对着人脸直射,而是打向墙面做漫反射补光,把人物和背景的对比度拉到一个稳定水平;软件端在采集模块里加了一段自适应的曝光校准,开流前用前30帧统计亮度分布,动态调节帧的增益。效果最明显的是踝泵动作,补光前踝关节置信度在0.3左右跳动,补光后稳定到了0.7以上。
6.3 老人动作慢、幅度小的对策
慢动作是另一个反复调优的点。年轻测试者做一次起立大约0.8秒,老人可能要2到3秒,而且膝盖几乎不打直。这导致两个问题:一是动作周期的重采样后特征被压缩变形,二是滞回区间的"跨越速度"太慢,系统容易把一次动作分割成两次。
对策有两个,一个是把滞回区间的最小跨越时间要求放宽,比如规定角度必须保持在确认阈值以上至少0.6秒才算有效波峰,这个时间要求配合阈值一起在配置界面里开放;另一个是对LSTM的输入序列做时间拉伸增强,把训练数据中的动作周期人为拉长到2到4秒的区间,让模型见过更多慢动作的形态。做完这两件事,慢动作场景的计数准确率从82%提到了94%,效果立竿见影。
6.4 性能优化与长时间运行的稳定性
系统的运行时长需求是连续工作8小时以上,这对推理链路压力不小。最初我在普通迷你主机上跑完整链路,姿态估计加LSTM推理,帧率只有15帧左右,勉强能用但冗余不足。优化主要做了三件事:一是把采集帧率从30帧降到20帧,分析任务只在关键点发生变化时才触发,静止画面直接跳过;二是姿态估计模型做了线程级分离,用两个线程交替处理相邻帧,避免单线程排队造成的延迟抖动;三是把角度计算和LSTM推理全部向量化,用NumPy重写了原来循环实现的特征抽取。
优化之后,整条链路的单帧处理时间稳定在45毫秒上下,约22帧每秒,距离"动作发生到反馈出现"的1秒延迟预算还剩一半余量。这一轮优化让我意识到,这类系统瓶颈多数不在模型本身,而在帧处理管线里的无效计算和耦合逻辑上,先把管线理干净比盲目换更大的模型要紧得多。
7. 隐私保护与部署落地
7.1 本地推理、不留原始视频
长者场景对隐私的敏感度比一般应用高得多。老人和家属对"摄像头上传数据"这几个字极其警觉,哪怕只是在协议层面写着"加密传输",都会天然产生不信任感。所以系统从设计之初就定了一个原则:图像只在本地设备上处理,姿态估计完成后立即丢弃原始帧,整个系统只保留关键点序列和角度日志用于分析。
这个选择其实也是技术上的最优解。关键点序列和角度日志已经包含了动作识别所需的全部信息,原始视频帧对于后续分析几乎没有增量价值,反而带来存储和隐私的双重负担。系统里我加了一个明确的落库边界:数据库表里存的是"时间、动作类型、计数、角度值、合格标记",不存在任何图像字段。部署到社区康复站和模拟居家环境验收时,这一点也作为重点向家属说明,配合界面上的隐私说明页,基本打消了大家的顾虑。
7.2 硬件选型与部署形态
硬件选型上我走了一条中间路线:不用云服务器,也不用高端工作站,选择一台带CPU加速能力的迷你主机。原因在于,社区和居家环境不具备稳定的高带宽网络,云推理的延迟和成本都不可控;而专业的边缘推理硬件短期采购周期长,对于这个阶段的项目规模来说没有性价比。主流迷你主机加上CPU上的MediaPipe推理,实测稳定跑满20帧以上,足够满足实时反馈需求。
系统部署形态做成了一体化的嵌入式服务,开机自启,通过局域网内的浏览器访问管理界面。老人训练时只需要摄像头前站好,系统自动开始分析并语音播报。康复师通过网页查看实时状态和训练报告,不需要在手机或电脑上装任何客户端。这套形态在不同站点复用时,只要改一下摄像头角度参数和动作阈值配置,就能直接跑起来,部署时间从最初的两天压缩到了两小时。
7.3 参数可调:留给康复师的"旋钮"
系统里专门做了一组面向康复师的参数调整项,包括各项角度阈值、滞回区间宽度、最小跨越时间、合格窗口大小。这些参数在界面上以滑杆的形式呈现,旁边还显示当前参数对应的历史动作曲型样本,康复师可以对照调整。这个设计源于一个很实际的教训:第一版把参数写死在配置文件里,康复师想根据老人的恢复进度微调标准,得找开发改代码,一个简单的需求来回要一到两天。
把参数开放出去后,使用率出乎意料地高。最频繁的调整是肩角达标阈值——老人康复中期,康复师会有意提高目标角度,逼着老人把动作做得更到位;也有反向调整的案例——对高龄老人降低达标门槛,优先保证完成量和积极性。这套"旋钮"本质上把系统从一个固定的评判器,变成了一个康复师可以灵活掌握的辅助工具,这个定位转变,远比多做一个炫酷功能重要。
最后再分享一个小技巧。如果你也要做类似的长者服务系统,建议在第一个版本就把所有日志带上时间戳和环境标签,哪怕当时觉得用不上。后面做参数调优和问题排查时,没有这些标签几乎是寸步难行。我们这个项目能够在多轮迭代里快速定位问题,靠的就是最初那批看似多余、实际居功至伟的日志字段。