
简介面向老年人看护等安全监控场景的MATLAB跌倒检测项目源码基于VIBE算法实现涵盖三轴加速度数据预处理、低通滤波去噪、峰值均值斜率特征提取以及身体运动轨迹图分析与模型判别等关键环节。代码简洁适合学习人体行为识别与算法开发的学生及初级开发者作为入门参考。压缩包共4个文件均为m脚本大小约3KB覆盖数据读取、标注、识别和人体关键点处理等基础功能便于直接阅读和调试。目前已有466人学习说明该小工具在跌倒检测方向具备一定的参考价值。通过这套源码读者可以快速理解如何将加速度信号转化为可视化轨迹图并用于跌倒判别也可在此基础上扩展特征规则或尝试接入支持向量机等分类模型用于课程设计、课题预研或最小可行原型的起步。1. 手表加速度信号里跌倒检测最难的不是“检测”而是“误报”佩戴在手腕上的三轴加速度计数据噪声远大于人体躯干。一次挥手、一次拍桌、一次弯腰系鞋带在加速度曲线上的形态和真实跌倒非常接近。VIBEVisual Interpretation of Body Accelerations for Event Detection算法的思路不是直接找突变峰值而是把连续的加速度信号画成“身体运动轨迹图”再做分类这样能把时间维度上的形变保留下来。这个工程包里的biaozhu.m、readTV.m、biaoshi.m、rentiGJ.m四个脚本正好覆盖从原始数据标注到最终判决的完整链路。做老年看护、康复监测或者只是想在MATLAB里验证可穿戴算法的工程师都能从这套代码里找到能直接改的起点。2. VIBE算法的数学基础三轴加速度预处理与Body Trajectory Maps构建2.1 原始三轴信号为什么不能直接进分类器三轴加速度计固定在手表内部但手表会随穿戴者手腕旋转。重力在X、Y、Z三个轴上的投影分量一直在变直接拿原始值做阈值判断跌倒时的下坠信号会被手臂姿态变化淹没。比如手背朝上时Z轴的重力分量可能接近0而手腕翻转后同一个动作在Z轴上的读数完全不同。VIBE的第一步是让窗口内的数据在同一个参考系下比较对窗口减去均值能去掉重力常量保留窗口均值作为特征则能补回姿态信息。更完整的做法是用加速度计估算倾角把每个轴旋转到“躯干坐标系”但旋转矩阵计算量大手表端不划算。滑动窗口是这类任务的标配。窗口长度取1到1.5秒重叠率50%到75%既能覆盖跌倒的失重、撞击、静止三个阶段又不会让事件被平均掉。如果窗口太短比如0.3秒看到的只有撞击峰值前倾和躺平信息全部丢失窗口太长跌倒前后的正常活动混进来模型很难聚焦。2.2 低通滤波与滑动窗口分帧的MATLAB实现原始加速度序列里有大量高频抖动来自手部震颤和传感器本身。跌倒动作的能量集中在0.5到8Hz所以低通截止频率设在12Hz左右就够了。我一般用二阶巴特沃斯滤波器阶数再高会引入明显相位延迟阶数太低又压不掉高频毛刺。下面这段代码完成滤波和分帧% 跌倒检测预处理滤波 分帧 fs 50; % 采样率常见手表加速度计是50Hz [b, a] butter(2, 12/(fs/2), low); % 12Hz低通fs/2是Nyquist频率 accX filter(b, a, rawX); accY filter(b, a, rawY); accZ filter(b, a, rawZ); winLen round(fs * 1.2); % 1.2秒窗口 stepLen round(winLen * 0.5); % 50%重叠 startIdx 1; frames []; while startIdx winLen - 1 length(rawX) idx startIdx : startIdx winLen - 1; frames(:, :, end1) [accX(idx), accY(idx), accZ(idx)]; startIdx startIdx stepLen; end滤波这里用的butter返回滤波器系数filter做因果滤波会引入相位延迟离线分析时建议换成filtfilt零相位效果更好。分帧后frames的第一个维度是窗口内采样点第二个维度是三轴第三个维度是帧数。每帧代表一次候选事件之后的特征提取全部在帧上进行。如果采集的数据不是50HzwinLen要除以真实采样率换算成点数否则窗口覆盖的物理时间会不对。低通和分帧的参数不是固定的下面是我在手腕场景下的推荐值参数推荐值说明采样率50Hz手表加速度计常用过低会丢峰值低通截止频率12Hz保留0.5-8Hz人体运动主频带窗口长度1.2s覆盖跌倒完整事件窗口重叠率50%防止事件落在窗口边界拒绝阈值1.8g低于该值的窗口不进入识别流程拒绝阈值会在第4章详细讲这里的意义是让后续SVM只处理可能跌倒的窗口把日常生活动作挡在门外。如果用的是老版本MATLABreadtable不可用可以把CSV读取改成importdata但要注意处理表头和缺失值的方式不同。2.3 把一维时序帧转换成Body Trajectory Maps直接从窗口里算均值、峰值、标准差虽然计算简单但丢掉了波形的先后顺序。同样是2.5g的峰值一个是先下坠后撞击一个是先挥手后拍桌统计量几乎一致波形却完全不同。Body Trajectory Maps把每个窗口变成一个可视化的灰度图保留时间顺序。function img acc2map(frame, fs) % frame: 一个窗口的三轴加速度 [N x 3] N size(frame, 1); map zeros(N, 3); % 三列对应X/Y/Z for ch 1:3 seg frame(:, ch); seg seg - mean(seg); % 去直流去掉重力常量 maxVal max(abs(seg)); if maxVal 0 seg seg / maxVal; % 样本内归一化 end map(:, ch) (seg 1) / 2; % 映射到[0,1] end img imresize(map, [64, 3], bilinear); img uint8(img * 255); % 0-255灰度 end这段代码把三轴各放在一列高度是采样点数缩放成固定的64行。为什么用样本内最大幅值归一化而不是固定阈值因为跌倒瞬间的峰值远大于日常活动固定阈值会把轻微跌倒的信号压成一条直线样本内归一化能保留相对幅值关系。在很多真实数据里3像素宽的轨迹图对CNN来说太薄实际使用时会把三个轴当作R、G、B三个通道再把时间轴重采样到64得到64×64的伪彩图。带宽不够的嵌入式设备可以只保留3列灰度图喂给线性分类器效果比直接展开原始波形要好。3. 从readTV到biaoshi四个MATLAB脚本的数据标定与姿态解算3.1 readTV.m与biaozhu.m先解决“数据在哪标签是谁”拿到工程包时我第一反应是先看readTV.m。这个脚本的名字暗示了它负责读取某种“时间序列”数据通常是手表导出的CSV文本里面至少有毫秒级时间戳和X/Y/Z三轴加速度。readTV.m的职责是把这些字段整理成统一结构体后续脚本不用再关心文件格式。下面是它常见的简化版function data readTV(filename, fs) % 从CSV读取三轴加速度和时间戳 raw readtable(filename); data.t raw.time_ms; % 毫秒时间戳 data.x raw.acc_x; % 单位g data.y raw.acc_y; data.z raw.acc_z; data.fs fs; valid ~(isnan(data.x) | isnan(data.y) | isnan(data.z)); data.x data.x(valid); % 剔除蓝牙丢包产生的NaN data.y data.y(valid); data.z data.z(valid); data.t data.t(valid); endreadTV返回的data结构体会被biaozhu.m消费。biaozhu.m是标注脚本它会画出合成加速度幅值曲线用ginput在图形窗口上点两个点标记一次跌倒的开始和结束时间。标注的原则是宁长勿短跌倒的前倾、失重、撞击、躺平四个阶段需要完整覆盖如果只标撞击峰值的几百毫秒模型学到的只是“一个尖峰”而不是“一次事件”。标注结果一般存成label.mat包含样本矩阵和标签向量。标注时还要注意样本不平衡问题正常动作的数量通常是跌倒的5到10倍训练前需要对正常类别做欠采样或者给SVM的不同类别设置不同的误分类代价。3.2 rentiGJ.m从加速度估算姿态角与关节位移rentiGJ.m从命名看是“人体骨架”的缩写这个工程里它主要负责从加速度数据里估计姿态角。手表能直接测到的是线加速度不是角度但静止或慢速运动时重力分量可以当作参考方向。用atan2函数可以算出佩戴部位的横滚角roll和俯仰角pitchfunction [roll, pitch] rentiGJ(accX, accY, accZ) % 用重力分量估算佩戴部位的横滚和俯仰角 roll atan2(accY, sqrt(accX.^2 accZ.^2)); pitch atan2(-accX, sqrt(accY.^2 accZ.^2)); roll roll * 180 / pi; % 弧度转角度 pitch pitch * 180 / pi; end这里的关键是atan2而不是atan因为atan2能正确处理四个象限角度范围是-180到180度不会出现方向模糊。摔倒时手腕角度会从接近垂直的约90度变成接近水平的约0度这种姿态变化和加速度峰值是互补的特征。但要注意撞击瞬间动态加速度会污染重力参考方向所以姿态角通常取撞击前100毫秒到撞击后100毫秒的均值而不是逐点取最大值。如果要估位移需要对加速度做双重积分漂移非常快我一般只在跌倒瞬间前后做短时积分然后强制归零否则误差会在1秒内累积到几十米。3.3 biaoshi.m两级判决粗筛加模型识别biaoshi.m是主识别脚本把所有模块串起来。它先计算窗口内合成加速度幅值低于1.8g的窗口直接跳过超过的才提取特征、调用分类器。这样做的原因是跌倒窗口在完整数据流中占比极低先粗筛能降低计算量和误报率。下面给出它的核心流程示意load(label.mat); % 标注结果 load(svmModel.mat); % 训练好的SVM data readTV(accel.csv, 50); for each窗口 if max(合成幅值) 1.8 feat extractFeatures(window); % 角度轨迹图特征 [label, score] predict(svmModel, feat); if label 1 score(:,2) 0.6 triggerAlarm(); % 触发报警或上传事件 end end end这段代码里extractFeatures实际要调用第2章的acc2map和3.2节的rentiGJ把窗口转成特征向量。粗筛阈值1.8g不是固定常数戴手腕时快速挥手也能到1.8g建议提到2.0g戴胸口时躯干动作更平稳可以降到1.6g。score阈值0.6是在灵敏度与误报之间取的折中这个值可以通过第4章的混淆矩阵继续调。要注意这里用的for each窗口是伪代码真实MATLAB里需要按窗口索引循环并预先分配好输出数组避免每次循环动态扩容拖慢速度。工程包里四个脚本的数据流可以概括成下表实际改动时按这个顺序替换数据源即可脚本输入输出主要作用readTV.mCSV/文本data结构体统一时间戳、剔除无效值biaozhu.mdata结构体label.mat人工标注跌倒区间rentiGJ.m三轴加速度roll/pitch姿态角估算biaoshi.m实时数据流报警信号粗筛模型判决4. 从轨迹图到分类结果特征提取与SVM模型训练4.1 特征向量峰值、姿态角变化和轨迹纹理手工设计的特征要能区分“跌倒”和“日常大动作”只靠一个加速度峰值远远不够。跌倒时的合成加速度会出现至少一个超过2g的冲击峰但拍桌子、甩手也会产生类似峰值。真正有区分度的是峰值前后的形态跌倒前有短暂的失重即合成加速度降到0.5g以下跌倒后有一个长时间静止平躺段方差迅速下降。我把这些特征整理成下面这张表特征计算方式区分度来源合成加速度峰值max(sqrt(x^2y^2z^2))撞击强度失重深度窗口内最小值低于1g前倾/下坠阶段峰值斜率峰前100ms的幅值变化率突发性姿态角变化量撞击前后roll/pitch差值从直立到水平平躺段方差峰值后300ms加速度方差倒地后静止轨迹图像素均值对acc2map输出求平均时间形态综合这些特征放进SVM之前要先做标准化因为峰值量级在3g左右角度在0到90度统一后能避免大数值特征压制小数值特征。粗筛阈值也可以借助这里的峰值特征来定正常行走的合成加速度通常在1g到1.5g之间把拒绝阈值放在1.8g到2.0g既能覆盖绝大多数日常活动又不会把真实的跌倒前失重段滤掉。4.2 用fitcsvm训练分类器并做保持集验证MATLAB自带的fitcsvm支持RBF核正好适合跌倒检测这种样本量不大、边界非线性的场景。训练之前我习惯把80%的数据随机分成训练集剩下20%做保持集验证不要用同一份数据既训练又评估。下面是训练代码load(label.mat); % 包含featureMatrix和isFall X label.featureMatrix; Y label.isFall; % 1表示跌倒0表示正常 rng(42); % 固定随机种子便于复现 cvp cvpartition(size(X,1), Holdout, 0.2); mdl fitcsvm(X(cvp.training,:), Y(cvp.training), ... KernelFunction, rbf, ... BoxConstraint, 1, ... Standardize, true); testPred predict(mdl, X(cvp.test,:)); C confusionmat(Y(cvp.test), testPred); disp(C);fitcsvm里的Standardize参数会在训练前自动对每个特征做零均值单位方差变换省去手工求mu和sigma。BoxConstraint控制误分类惩罚值越大模型越容易过拟合跌倒检测样本通常只有几千条取1到10比较合适。RBF核的核尺度由KernelScale自动选择如果训练时间太长再手动指定。保持集上的混淆矩阵C第一行是正常动作误报成跌倒的数量第二行是跌倒漏报的数量。我更关心第二行因为漏报一个真实的跌倒比误报一次严重得多。如果只有几百条样本建议用5折交叉验证替代单次Holdout能得到更稳定的性能估计。4.3 调整判决阈值用score而不是默认labelfitcsvm默认按0.5概率阈值给出label但跌倒检测的代价不对称。如果希望减少漏报可以把判决阈值从0.5降到0.3代价是误报增加如果希望减少打扰提到0.7。predict函数可以输出每个类的分数下面是利用概率分数调整阈值的方式[~, score] predict(mdl, X(cvp.test,:)); fallProb score(:,2); % 第二列是跌倒类概率 threshold 0.6; pred fallProb threshold; TP sum(Y(cvp.test) 1 pred 1); FN sum(Y(cvp.test) 1 pred 0); FP sum(Y(cvp.test) 0 pred 1); sensitivity TP / (TP FN); % 召回率 specificity (sum(Y(cvp.test)0) - FP) / sum(Y(cvp.test)0); fprintf(sensitivity%.2f specificity%.2f\n, sensitivity, specificity);阈值越高模型越保守漏报增加误报减少。下面是一组示意数据实际数据集不同但能看到趋势判决阈值灵敏度特异度适合场景0.30.970.88独居老人漏报代价高0.50.920.95一般监护0.70.810.98误报会引起频繁干预的医院这里的score不是真正的概率而是SVM的决策距离经过sigmoid拟合后的值但对调阈值足够了。若想更精确可以用fitPosterior对模型做概率校准。提示阈值调整后要用保持集重新评估不能用在训练集上得到的分数调阈值否则会把噪声当成规律。5. 手表彰片上的部署优化定点化判决与离线验证5.1 用线性SVM替代RBF核减少片上计算RBF核在PC上训练效果不错但预测时要计算大量指数函数对手表MCU不友好。实际部署时我会把RBF核换成线性核重新训练线性SVM的预测退化为一次点乘加偏置效果只要损失3%到5%。MATLAB里训练线性SVM后直接把Beta和Bias导成C数组mdlL fitcsvm(X, Y, KernelFunction, linear, Standardize, true); W mdlL.Beta; % 1xD B mdlL.Bias; % 保存为C数组需要的格式 cHeader sprintf(const float w[%d] {, length(W));线性SVM输出的score范围很大不适合直接整型化。再对score做一次查表映射映射到0到255的字节型置信度。手表的浮点运算单元如果有直接用浮点也够如果是低端MCU就把特征和权重都转成16位整数判决阈值也对应缩放。这个过程需要先在MATLAB里用Fixed-Point Designer验证误差不超过一个bit。5.2 离线回放与误报率检查部署前我用真实的连续数据离线回放不停表把整段数据按流式方式喂给biaoshi.m统计每小时的误报次数。验证时要特别关注三类样本快速爬起、跌倒后翻身、被人扶起。这三类在加速度形态上接近跌倒但实际不需要报警。把这三类数据单独挑出来看SVM输出的score曲线通常会发现在跌倒峰附近score先冲高再回落可以利用“score持续超过阈值至少200ms”和“后续加速度方差较低”两个条件进一步过滤。最终落地的判决条件可以写成% 判决条件峰值 2.0g 且 姿态角变化 40度 且 score 0.55 isFall peakMag 2.0 abs(rollDiff) 40 fallProb 0.55;peakMag来自窗口内合成加速度最大值rollDiff来自3.2节姿态角差fallProb来自4.3节SVM分数。这三个条件分别对应撞击、姿态改变、模型置信度缺一个都不报警。这样即便SVM被快速爬起骗过姿态角条件也能拦下来。本文还有配套的精品资源点击获取