Mediapipe 3D骨架结合KNN:跌倒检测特征工程与实时推理 简介随着人口老龄化与居家看护需求上升跌倒检测成为智能健康领域的重要方向。本项目源码基于Mediapipe框架实时提取人体3D骨架关键点再通过KNN算法对姿态特征分类判断是否出现跌倒事件适合具备一定Python基础、想要实践姿态估计与机器学习流程的开发者。资源共9个文件打包约7.98MB包含可运行Python脚本、CSV关键点数据集、已训练好的joblib模型、MP4操作演示视频与GIF效果图脚本按关键点提取、数据预处理、模型训练和推理等环节组织目录清晰便于逐段阅读和二次开发。已有259人学习下载可用于课程设计、算法对比或小型安防原型验证。通过该项目可掌握Mediapipe姿态估计接口的调用、KNN最近邻分类的构建与调参以及从数据整理到结果可视化的一整套工程实现思路是一份质量较高的跌倒检测实战源码。1. 跌倒检测用 Mediapipe 3D骨架 KNN这套组合到底解决了什么想象一个独居老人或夜间病房的场景老人起身时头晕摔倒无法自己爬起来如果没人发现在地上躺半小时和躺两小时后果完全不同。这就是跌倒检测要解决的核心问题。这个项目给出的答案是用 Mediapipe 从摄像头画面里提取人体 3D 骨架33 个关键点的坐标再交给 KNN 算法去判断“这个人是不是摔倒了”。和端到端的深度学习方案比它不需要上万张标注跌倒图片和穿戴式手环/挂坠比它不增加老人身上的任何负担一个普通监控摄像头就能跑。这套方案适合正在做产品原型的开发者、做毕设或课题的学生、以及想在边缘设备上验证跌倒检测可行性的从业者。整条链路的难点不在 Mediapipe 也不在 KNN而在“怎么把一段骨架序列变成 KNN 能识别的特征”下面按这条主线展开。2. 用 Mediapipe 提取人体3D骨架安装、最小复现与三个关键参数2.1 Mediapipe Pose 输出的“3D”是什么以及为什么够用Mediapipe Pose 模型会输出 33 个人体关键点landmark每个点包含 x、y、z 三个坐标和 visibility 可见度。x、y 是画面内的归一化坐标z 是模型估计的深度——注意这个 z 是相对深度以臀部中心为参考点单位也不是米而是模型内部估计的相对值。它和激光雷达或深度相机输出的绝对三维坐标不是一回事但在跌倒检测场景里完全够用人站立时肩膀和髋部的 z 值非常接近倒地侧躺时两者 z 值会出现明显差异这个差异就是“身体从竖直变成水平”的有效信号。选 Mediapipe 而不是 OpenPose 或 MMPose理由很直接。OpenPose 精度确实高但依赖库重、模型文件大在 CPU 上跑实时很吃力MMPose 灵活但需要自己配置 config 和模型权重学习成本高。Mediapipe Pose 的优势是安装一条 pip 命令、CPU 上就能跑到 20-30fps、输出统一结构的 landmark 列表对“快速验证跌倒检测”这个目标来说性价比最高。它也有自己的黑匣子部分——关键点抖动、遮挡时定位漂移这些坑后面单独讲。2.2 安装与最小复现一段把视频变成骨架坐标序列的脚本先装依赖。mediapipe 安装时会自动带上一批依赖包建议在干净的 Python 虚拟环境里装避免和已有项目冲突。mediapipe 对 Python 版本有要求具体以 pip 当前解析到的版本为准安装时遇到 protobuf 或 grpc 版本冲突是常见问题优先用虚拟环境隔离解决。pip install mediapipe opencv-python numpy下面这段脚本把一段视频逐帧读入用 Mediapipe 提取每帧的 33 个关键点坐标按固定顺序存成一个序列文件。这是整个跌倒检测链路的第一步——先把原始视频变成干净的骨架数据。import cv2 import mediapipe as mp import numpy as np mp_pose mp.solutions.pose pose mp_pose.Pose( static_image_modeFalse, # 视频流模式逐帧跟踪 model_complexity1, # 模型复杂度0轻量 / 1平衡 / 2高精度 smooth_landmarksTrue, # 打开时序平滑减少抖动 min_detection_confidence0.5, # 初始检测置信度阈值 min_tracking_confidence0.5, # 跟踪置信度阈值 ) cap cv2.VideoCapture(input.mp4) landmarks_seq [] while cap.isOpened(): ret, frame cap.read() if not ret: break # mediapipe 要求 RGB 输入opencv 读出来是 BGR必须转换 frame_rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results pose.process(frame_rgb) if results.pose_landmarks is not None: # 33个关键点按固定顺序排列成 (33, 3) 数组 pts np.array([[lm.x, lm.y, lm.z] for lm in results.pose_landmarks.landmark]) landmarks_seq.append(pts) else: landmarks_seq.append(None) # 当前帧没检测到人占位 cap.release() np.save(landmarks.npy, np.array(landmarks_seq, dtypeobject), allow_pickleTrue)逻辑说明每帧读入后先转 RGB 再交给pose.process返回的 landmark 顺序是固定的——索引 0 是鼻子11、12 是左右肩23、24 是左右髋这个固定顺序是后面特征计算的基础千万不能打乱。z 是相对深度估计值范围大约在 -1 到 1 之间数值大小代表相对摄像头的远近但不同体型、不同距离下不能直接横向比较只能作为同一个人的时序变化参考。这里有两个容易踩的地方。第一np.save用了dtypeobject是因为序列里可能有 None 帧实际做训练数据时我会丢掉这些空帧或用前后帧插值填充不能让空帧参与特征计算。第二如果发现关键点抖动得厉害先检查是不是忘了转 RGB——这个错误很隐蔽因为 BGR 图也能跑出关键点只是精度明显下降。2.3 三个关键参数模型复杂度、检测置信度、平滑开关Mediapipe Pose 构造参数里对跌倒检测效果影响最大的是下面三个建议按这个顺序调。model_complexity0、1、2 三档。0 档在低端 CPU 上帧率最高但关键点精度最差对跌倒检测这种对关节角度敏感的场景不建议用2 档精度最高但帧率下降明显适合性能好的机器或离线处理。我的习惯是先跑 1 档看特征分布如果发现关键点抖动影响判断再升到 2 档对比。min_detection_confidence和min_tracking_confidence前者是初始检测的阈值后者是后续帧跟踪的阈值默认都是 0.5。摄像头离人远、画面里人多、光线不足时检测置信度会整体下降这时不是调低能解决一切——试过就知道太低的阈值比如 0.3 以下会把背景里的杂物也当成关键点。室内场景我一般用 0.4-0.5室外或遮挡多时提到 0.6。smooth_landmarksTrue默认开启对关键点做时序平滑滤波。跌倒检测里建议保持开启它能让骨架特征曲线更干净减少 KNN 输入的噪声。副作用是倒地这个快速动作会被稍微钝化但跌倒判定看的是“状态”不是“瞬间动作”影响不大。提示调参前先跑一段 30 秒的“站立-走动-弯腰”视频把关键点可视化出来确认稳定再进特征工程。参数没调稳就往下走后面所有环节都会被污染。3. 把3D骨架变成可判别的特征倾角、重心高度与速度才是核心3.1 跌倒与“坐下、弯腰”的本质差别状态切换的速度和幅度KNN 不能直接吃 33 个关键点的原始 xyz 坐标原因有两个。第一原始坐标受画面中人物大小和位置影响同一个人站在远处和近处x、y 数值差异极大KNN 计算距离时会被这种无关差异主导。第二99 维原始坐标在高维空间下距离度量非常不可靠这就是维数灾难——维度越高最近邻和最近邻之间的距离越接近分类边界越模糊。所以必须先做特征工程把高维骨架压缩成低维、有物理含义的特征。跌倒在物理上有几个共性身体从竖直姿态快速变成接近水平姿态、重心高度显著下降、整个过程发生在 0.5-1.5 秒内。而“坐下”“弯腰捡东西”虽然也有重心下降但要么运动速度慢得多要么最终姿态仍然竖直。“躺着”和“走路”的区别更明显。所以特征方向就是这三个身体倾角、重心高度、关键点速度。3.2 特征计算落地倾角、重心比、帧间速度的参考实现下面这段代码输入一段连续的骨架序列每帧 33×3输出一个 5 维特征向量。这是整个方案里最核心的一段代码值得反复调试。import numpy as np def extract_features(seq): seq: (T, 33, 3) 骨架序列T 取 30 帧约 1 秒 30fps 返回: 5 维特征 [平均倾角, 重心高度比, 平均速度, 最大速度, 深度扩散度] # 1. 躯干倾角两肩中点 - 两髋中点向量与竖直方向 y 轴的夹角度 shoulder_mid (seq[:, 11] seq[:, 12]) / 2 # 左肩11、右肩12 hip_mid (seq[:, 23] seq[:, 24]) / 2 # 左髋23、右髋24 trunk_vec shoulder_mid - hip_mid up np.array([0.0, 1.0, 0.0]) cos_angle np.sum(trunk_vec * up, axis1) / ( np.linalg.norm(trunk_vec, axis1) * np.linalg.norm(up) 1e-6) angle np.arccos(np.clip(cos_angle, -1, 1)) * 180 / np.pi angle_feat np.mean(angle) # 2. 重心高度比髋部中点 y 除以躯干长度消除镜头距离和身高影响 body_len np.linalg.norm(trunk_vec, axis1) height_ratio hip_mid[:, 1] / (body_len 1e-6) height_feat np.mean(height_ratio) # 3. 髋部中点相邻帧位移的平均速度和最大速度 hip_vel np.linalg.norm(np.diff(hip_mid, axis0), axis1) speed_feat np.mean(hip_vel) max_speed np.max(hip_vel) # 4. 深度方向扩散度所有关键点 z 值的标准差捕捉倒地躺平的形态 z_spread np.std(seq[:, :, 2], axis1).mean() return np.array([angle_feat, height_feat, speed_feat, max_speed, z_spread])逻辑说明第一个特征用躯干和竖直方向的夹角站立时通常 0-15 度倒地时能到 60-90 度这是区分“站着/走着”和“躺下/摔倒”最直接的量。第二个特征把髋部 y 坐标除以躯干长度做归一化这样不管人离镜头远近、个子高矮站立时的重心高度比都落在相近区间。第三、第四个特征捕捉动作速度这是区分“跌倒”和“慢慢坐下”的关键——跌倒的加速度远大于坐下。第五个特征用 z 方向的扩散度倒地侧躺时肩膀和髋部的深度差明显增大站立时则很小。特征向量的维度控制在 5 维左右是合理的。KNN 在高维下距离会被稀释维度越高效果越差但维度太低比如只留倾角又区分不了坐下和跌倒。窗口长度取 30 帧约 1 秒能覆盖“从直立到倒地”这个完整过程滑窗步长取 5-10 帧既保证相邻窗口有重叠又不会让特征曲线过于平滑而丢掉速度信息。3.3 样本哪里来公开数据集、自采数据与滑窗标注特征提取器的输出只是特征KNN 还需要标签——哪些特征向量是跌倒哪些不是。数据来源有两条路。公开数据集方面跌倒检测领域常用的是 UR Fall Detection含加速度计与视觉数据、Le2i Fall Detection Dataset室内监控视角等。这些数据集的好处是已经标注好了跌倒/非跌倒事件可以直接用来验证整个特征KNN 链路是否跑得通。要注意它们的摄像头高度和视角与你的应用场景未必一致跨场景迁移时指标通常会明显下降这只是起点。自采数据是项目真正落地前必须做的一步。我一般会准备两类素材日常动作走路、坐下、起立、弯腰捡物、蹲下、上下床和模拟跌倒前倒、侧倒、后倒、坐姿滑倒每个动作拍多次覆盖不同人物、不同距离。然后用滑窗切样本窗口长度 30 帧窗口内如果包含跌落的“起始帧到着地帧”区间标为 1完全不包含才标为 0。落在跌倒事件边界附近、只有一半重叠的窗口是噪声源我通常在事件前后各加 5 帧缓冲区落在缓冲区的窗口直接丢弃不让 KNN 学到不伦不类的中间态。这个方案几百到几千个滑窗样本就足够 KNN 跑出能用的结果这也是“MediapipeKNN 组合”相比端到端深度方案的最大优势——不需要上万级别的标注数据一个下午拍摄加标注就能支撑原型验证。4. KNN 识别跌倒特征归一化、k 值选择与实时推理4.1 KNN 算法为什么适合这个场景小样本、边界清晰、可解释KNNK-Nearest Neighbors的原理很直觉来一个新特征向量在训练集里找距离最近的 k 个样本按多数投票决定类别。它的特性和这个项目匹配度高但不是没有代价——下面这些特点决定了你不需要为了用 KNN 而用 KNN而是因为它确实合适。第一KNN 不需要训练过程新增标注样本后直接加入特征库即可原型迭代极快。跌倒检测的样本量只有几千级别这个优势很实际。第二跌倒样本和日常动作样本在特征空间里的分界其实相当清晰倾角大 速度快 重心低就是跌倒这个边界是局部非线性的KNN 能很好地“包裹”出来线性模型比如 Logistic Regression容易欠拟合。第三可解释性强——预测结果可以把最近的 k 个邻居打印出来误报时一眼能看出是哪个特征维度出了问题。局限也明显推理时要和全部训练样本算一遍距离样本上万后延迟会变高但对几千样本、5 维特征来说单次推理在毫秒级别不是瓶颈。数据量再大就该换 SVM 或随机森林了这里是 KNN 的舒适区。4.2 训练流程标准化、交叉验证选 k、保存模型KNN 对特征尺度极其敏感。倾角的数值范围是 0-90速度可能是 0-1如果不归一化KNN 算距离时倾角会完全主导速度的作用被稀释。所以标准化是必做的一步。注意一个关键细节必须用训练集的均值和标准差去变换测试集不能把全部数据混在一起 fit 再切分那是典型的数据泄漏会让交叉验证指标虚高。import joblib import numpy as np from sklearn.preprocessing import StandardScaler from sklearn.neighbors import KNeighborsClassifier from sklearn.model_selection import cross_val_score # X: (n_samples, 5) 特征矩阵y: (n_samples,) 标签1跌倒 0日常 X np.load(features.npy) y np.load(labels.npy) # 标准化用训练集统计量变换 scaler StandardScaler() X_scaled scaler.fit_transform(X) # 交叉验证选 k3 到 15奇数避免平票 for k in range(3, 16, 2): knn KNeighborsClassifier(n_neighborsk, metriceuclidean, weightsdistance) scores cross_val_score(knn, X_scaled, y, cv5, scoringaccuracy) print(fk{k}, acc{scores.mean():.3f} (/- {scores.std():.3f})) # 实际项目中我一般选交叉验证准确率最高且标准差最小的 k常见在 7-11 best_k 7 knn KNeighborsClassifier(n_neighborsbest_k, metriceuclidean, weightsdistance) knn.fit(X_scaled, y) # 保存模型和 scaler推理时两个都要加载缺一不可 joblib.dump(knn, knn_fall.pkl) joblib.dump(scaler, scaler.pkl)逻辑说明weightsdistance表示投票时按距离加权距离近的邻居话语权更大能缓解 k 取偏大时的误判。metriceuclidean对标准化后的特征适用。交叉验证选 k 的原则不要只看准确率均值还要看标准差——均值高但标准差大的 k 值说明结果不稳定换数据集容易翻车。提示跌倒检测场景里召回率真实的跌倒有多少被报出来了比准确率更重要。误报可以靠后处理降噪漏报是安全事故。交叉验证时同时打印 recall 和 specificity别只盯 accuracy。4.3 实时推理滑动窗口聚合 多数投票 事件触发推理端不能每帧单独判一次因为单帧的 KNN 预测噪声很大——弯腰、遮挡、转身都可能触发误判。我一般做两层聚合先用滑窗生成特征向量每 5 帧算一个然后对最近的一批特征向量分别预测按比例投票最后还要时序平滑——连续多次判定跌倒才真正触发报警。import joblib import numpy as np def predict_window(features_window): features_window: (num_features, 5) 按时间顺序排列的特征向量 返回: 0 或 11 表示该窗口判定为跌倒 knn joblib.load(knn_fall.pkl) scaler joblib.load(scaler.pkl) X scaler.transform(features_window) preds knn.predict(X) # 窗口内超过 60% 的切片判为跌倒才输出 1 vote np.mean(preds) 0.6 return int(vote)推理时具体做法是读到新的一帧骨架后取最近 30 帧算一个特征向量放进一个长度固定的 FIFO 队列每来 5 帧算一次队列积累到 N 个特征向量后做一次投票。真正触发报警还要再加一道闸——连续 3 个推断周期都判定为跌倒才报警因为真正的跌倒是“状态持续数秒”瞬时误报不会持续这么久。0.6 的投票比例和连续 3 次确认这两个阈值是经验值。误报多就提高投票比例或连续次数漏报多就降低这是整套方案里最值得反复试的“调参旋钮”。5. 避坑手册跌倒检测里 5 个典型的翻车现场5.1 现象人站着不动系统隔几秒就报一次跌倒原因第一特征没归一化就丢给 KNN倾角数值大、速度数值小距离被倾角主导一些“站姿但倾角略偏”的样本被误判为离跌倒样本很近。第二训练集里日常动作样本不足KNN 找不到足够的“非跌倒”邻居做参考边界自然跑偏。解决先做 StandardScaler 再进 KNN这是必做项然后扩充日常动作样本特别是静止站立、原地转身、伸懒腰这类幅度不大但和跌倒特征比较接近的“小动作”。我调过的最离谱的一次误报是“老人坐着打瞌睡点头”——头部的 z 值变化让深度扩散度增大没有速度特征做约束就误判了。5.2 现象人慢慢坐到椅子上被识别成跌倒原因特征设计里只用了倾角和重心高度没考虑速度。“坐下”和“跌倒”最核心的差别正是速度——坐下时重心下降是匀速且缓慢的跌倒是加速的。解决把帧间速度特征平均速度和最大速度加进来重新提取、重新训练。如果加了速度还是误报检查窗口长度——窗口覆盖 1.5 秒以上时“慢慢坐下”在窗口内的平均速度也会被抬高这时把窗口缩到 30 帧约 1 秒再试。这个“坐下误报”的问题我几乎每个项目都会遇到特征设计时先问自己一句“这个特征能不能区分跌倒和坐下”能区分再加不能区分就是白搭。5.3 现象Mediapipe 在低端 CPU 上只有 5fps实时检测根本卡原因model_complexity2加上 1920×1080 的原始分辨率输入Mediapipe 处理一帧要 200-300ms实时性直接归零。解决先把输入帧缩到 640×480 或 640×360 再喂给 Mediapipe检测精度在这个分辨率下损失很小但速度能提升好几倍再把model_complexity降到 1。这是我在弱设备上踩过最实的坑——一定不要拿原始分辨率直接跑先缩放再推理是标配流程。如果还是卡考虑跳帧处理每 2 帧取 1 帧做骨架提取跌倒检测对帧率的要求不高10-15fps 足够。5.4 现象人侧身或弯腰时关键点在肩、髋之间乱跳原因Mediapipe 对遮挡或大角度姿势的估计置信度低一旦低于阈值就输出不稳定的坐标。而跌倒检测又恰好对人“躺平/侧身”这种姿势最敏感两者叠加导致特征值剧烈跳动。解决把min_detection_confidence从 0.5 调到 0.4让模型在低置信度下也能勉强跟上目标同时在特征计算之前按 visibility 过滤——肩部或髋部关键点的 visibility 低于 0.3 就丢弃当前帧等关键点恢复稳定再继续而不是把不可靠的坐标强行塞给特征提取器。这个过滤逻辑要放在特征提取函数外面单独做一个“骨架有效性检查”的步骤。5.5 现象测试集上准确率 95%换到真实场景一测就崩原因训练用的公开数据集或自采视频摄像头视角、高度、光线都和真实场景不同特征分布完全对不上。这是跌倒检测项目里最常见的“自测过拟合”——自己在测试集上自嗨上线就翻车。解决训练集要按摄像头视角分组保证里面至少包含 3 种不同视角的样本平视、稍俯视、正上方俯视真实场景部署前一定用现场摄像头重新拍一段数据做微调。KNN 的好处是微调成本极低——把现场采集的几十条新样本加入特征库重新保存模型就行不用重训。这个“现场采集-微调-再验证”的闭环是 KNN 方案相比深度学习方案最大的落地优势。6. 收尾技巧时序平滑和事件判定是最后一道闸6.1 单帧 KNN 输出不能直接当事件要做时序投票单帧预测的噪声来自两方面一是 Mediapipe 关键点本身的估算误差二是相邻帧之间的正常姿态波动。直接把单帧结果当事件触发报警你会被误报淹没。正确的做法是在时间维度上做聚合——滑窗内投票 连续确认。滑窗投票滤掉单帧噪声连续确认滤掉短时误报两级下来误报率能降一个数量级。6.2 一个顺手的后处理冷却时间机制触发报警后进入 30-60 秒的冷却期冷却期内不再重复报警。这个细节能避免同一个跌倒事件上报五六次也避免老人在跌倒后自己站起来又被检测到“起身-再倒”的重复事件。报警消息里带上时间戳和当前窗口的投票比例方便事后回溯。6.3 验证方法按事件评估不要只看帧准确率最后说验证。很多人拿帧级准确率说事但跌倒检测真正关心的是“一次跌倒有没有被报出来”“一天有多少次误报”。正确做法是事件级评估定义一个事件命中窗口比如跌倒开始前后 3 秒内触发报警就算命中统计召回率真实跌倒命中比例和误报次数24 小时内误报警次数。这两个指标才是产品能用的关键指标。我自己的习惯是每次调试这类项目先把误报样本和它的 k 个邻居打印出来看一遍——KNN 的黑匣子程度最低特征空间里一眼就能看出是特征设计问题还是样本不足问题。这个习惯帮我少走了很多弯路希望帮到你。本文还有配套的精品资源点击获取