Android跌倒检测Demo拆解:从传感器数据到实时报警的完整实现
简介这是一份基于YOLOv5的跌倒检测识别Android Demo资源面向需要在移动端部署实时跌倒检测功能的Android开发者、AI算法工程师及相关研究人员。资源采用YOLOv5深度学习方案可用于老人跌倒监护、安防监控等场景。压缩包共2个文件大小50.44MB主要包含一个可直接安装运行的app-release.apk以及一个记录版本信息的output-metadata.json文件APK已封装好检测模型便于快速验证效果。目前已有1945人学习下载。配套资源还覆盖跌倒检测数据集下载、YOLOv5训练代码、Android与C实现方案等系列内容读者可获得从模型训练到移动端部署的完整技术闭环。通过该Demo可以直观体验实时跌倒检测效果为后续二次开发或算法调优提供可参考的工程实现。1. 跌倒检测识别Android Demo一个能直接跑通的动作识别落地样例拿到这个Demo的第一个下午我就在一台老掉牙的测试机上把它跑了起来——没有外接传感器没有云服务纯粹靠手机内置的加速度计和陀螺仪识别“人摔倒”这个动作。它给我的第一印象是这不像网上那些只丢一堆论文源码却跑不起来的半成品而是一个能装进APK、能感应震动、能弹通知的完整工程。项目里没有复杂的神经网络也没有需要联网的模型而是用轻量规则加小模型的方式在Android手机上实时判断姿态突变。如果你正准备做老人防跌倒监测、工厂安全帽跌倒报警或者想学怎么在移动端做实时的动作识别这个Demo可以作为起步模板直接拿来改。它最适合两类人一类是想给学生或团队做一个“传感器识别动作”的演示项目另一类是把跌倒检测作为子功能嵌进已有APP的开发者。跟纯算法研究不同这个Demo把“检测”这件事完整落到了系统层——什么时候采数据、怎么算特征、怎么触发报警、怎么防止误报每一步都有对应代码。我从一个最初只看懂传感器API的开发者角度把它从工程结构、核心逻辑到调参技巧逐块拆开可能比你自己读源码省半天时间。2. 技术选型与工程结构传感器数据如何变成报警事件2.1 选型Java层调度 JNI推理为什么不用纯Java这个Demo没有把全部逻辑都堆在Java层。传感器监听、数据缓存和特征提取放在Android原生层也就是Java/Kotlin这边而滑动窗口内特征向量的打分则通过JNI调用C侧的一个轻量分类器。这样设计的原因很实际传感器回调频率通常是50Hz到200Hz如果在Java层频繁做浮点运算GC和主线程卡顿会直接影响采样时序。C侧做矩阵运算和阈值判断更可控也能复用同一套代码到其他嵌入式平台。常见做法是直接用TensorFlow Lite跑一个模型但这里用的是更轻的方案C里内置了一组经过预训练的分类权重输入是窗口内的统计特征输出是“跌倒”概率。好处是APK体积小冷启动快不依赖Google Play服务坏处是换模型时要连C代码一起改。如果你打算直接替换深度学习模型我会在第五章说明怎么接TFLite。2.2 目录结构从MainActivity到NativeLib的调用链看一个Android Demo先看它的包结构基本就知道作者的设计习惯。这个工程的主模块是标准的Android应用层加JNI层关键文件分布如下app/ ├── src/main/ │ ├── java/com/demo/falling/ │ │ ├── MainActivity.java # 主界面启动传感器与状态显示 │ │ ├── SensorCollector.java # 传感器数据采集负责注册监听与缓存 │ │ ├── FallDetector.java # 对外检测接口封装native方法 │ │ ├── AlarmManagerHelper.java # 通知与震动报警 │ │ └── utils/FeatureBuffer.java # 环形缓冲区与滑动窗口切片 │ ├── cpp/ │ │ ├── fall_detector.cpp # C核心读取特征并计算跌倒概率 │ │ ├── fall_detector.h │ │ └── CMakeLists.txt # JNI编译脚本 │ ├── res/ │ │ ├── layout/activity_main.xml # 实时数值显示布局 │ │ └── values/strings.xml │ └── AndroidManifest.xml # 声明传感器与通知权限 └── build.gradle # 配置NDK与ABI过滤调用链是单向的MainActivity启动时创建SensorCollector后者在onSensorChanged里把原始的三轴加速度和角速度写入FeatureBuffer。缓冲区的数据达到一个窗口大小后调用FallDetector.nativeDetect(features)由C返回当前窗口的跌倒概率。若概率大于阈值AlarmManagerHelper弹出通知并震动。值得注意的一点是C侧没有直接访问Java层的List而是通过JNI接收一个float数组。这样避免了大量对象复制也用简单数据结构强行限制了算法复杂度。初次读这个工程时我建议从FeatureBuffer看起它是承上启下的关键。2.3 数据管线采样、缓存、窗口切片的时序设计传感器采样的时序设计直接决定识别效果。这个Demo的采样率设定为100Hz也就是每10毫秒回调一次。它没有每来一个点就判断一次而是攒够一个时间窗口——默认2秒也就是200个采样点——才做一次检测。这样做的理由是跌倒作为一个连续动作通常持续1到2秒单点突变更可能是甩手或手机掉落不是人体姿态变化。特征窗口的滑动步长是50点0.5秒意味着每秒做2次检测既不会太密导致CPU占用过高也不会漏掉关键过程。你可以把它理解成看监控时每0.5秒扫一眼最近2秒的录像。窗口重叠保证了动作连续性每次判断都有足够的上下文。同时FeatureBuffer内部实现了一个循环数组新数据覆盖最旧的数据避免每次移动所有元素。采集线程与检测线程通过一个简单的volatile标志位同步没有使用复杂的锁——因为单个写线程加单个读线程足够安全。这个设计在我后续改造时几乎没有改动成本值得借鉴。3. 核心模块拆解跌倒检测从姿态突变到报警响应的完整链路3.1 传感器监听与高频采样实现SensorCollector是整个识别链路的入口。它注册了TYPE_ACCELEROMETER和TYPE_GYROSCOPE两种传感器采样率设为SENSOR_DELAY_GAME这是Android能提供的最高档之一对应大约50到100Hz。实际频率取决于设备性能所以代码里用时间戳自己计数而不是依赖系统切换频率。public class SensorCollector implements SensorEventListener { private static final int WINDOW_SIZE 200; // 2秒窗口 100Hz private FeatureBuffer buffer; private long lastSampleTimeNs 0L; private FallDetector fallDetector; Override public void onSensorChanged(SensorEvent event) { if (event.sensor.getType() Sensor.TYPE_ACCELEROMETER) { float[] acc event.values.clone(); // 用时间戳做采样率剔除防止系统“合并”两次回调 long now event.timestamp; if (lastSampleTimeNs ! 0L (now - lastSampleTimeNs) 10_000_000L) { return; // 时间间隔小于10ms的数据丢弃保证窗口内数据均匀 } lastSampleTimeNs now; buffer.pushAccelerometer(acc[0], acc[1], acc[2]); } else if (event.sensor.getType() Sensor.TYPE_GYROSCOPE) { buffer.pushGyroscope(event.values[0], event.values[1], event.values[2]); } } }这里的关键是10毫秒间隔过滤。很多手机标称200Hz但实际回调频率受系统负载影响可能一会儿密一会儿疏。如果直接按“每回调一次就算一个点”窗口内的时间跨度会忽长忽短特征值方差失真。这个防御性过滤在低端机上尤其重要。FeatureBuffer.pushAccelerometer内部会把三轴加速度连同合成加速度一起存入数组。我建议自己跑一遍时打印一下每个窗口的首尾时间戳能直观看到采样均匀性。如果发现大量丢点需要降级到SENSOR_DELAY_NORMAL同时把窗口大小从200改成100否则窗口不完整。3.2 特征工程合加速度、倾斜角与窗口统计量拿到原始数据还不够C侧需要能反映“人体姿态”的数值。这个Demo提取了五个特征合加速度的均值、合加速度的峰值、X轴倾斜角变化、Y轴倾斜角变化以及窗口内加速度方差。合加速度是三轴平方和开根号表示“整体受力大小”倾斜角则由重力分量算得。void computeWindowFeatures(const float* accX, const float* accY, const float* accZ, int len, float* out) { float sumMag 0.0f, sumAngleX 0.0f, sumAngleY 0.0f; float maxMag 0.0f; float meanX 0.0f; for (int i 0; i len; i) { float mag sqrtf(accX[i] * accX[i] accY[i] * accY[i] accZ[i] * accZ[i]); if (mag maxMag) maxMag mag; sumMag mag; // 近似垂直时z轴占主导angleAtan2可反映姿态角变化 sumAngleX atan2f(accY[i], accZ[i]); sumAngleY atan2f(accX[i], accZ[i]); meanX accX[i]; } meanX / len; float variance 0.0f; for (int i 0; i len; i) { float diff accX[i] - meanX; variance diff * diff; } out[0] sumMag / len; // 平均合加速度正常时约9.8 out[1] maxMag; // 峰值合加速度跌倒时往往超过2G out[2] sumAngleX / len; // X轴平均倾斜角 out[3] sumAngleY / len; // Y轴平均倾斜角 out[4] variance / len; // 加速度方差晃动时偏大 }参数说明len是窗口内实际采样点数out数组至少要有5个浮点空间。代码里用atan2f而不是asin或acos因为它的返回值范围是[-π, π]能区分手机朝上还是朝下适合判断“人躺平”的姿态。这里有一个容易误解的地方跌倒时合加速度会先猛增到12到20m/s²然后迅速回落到接近0因为人体砸到地面后短暂静止。只看峰值很容易把“猛跳一下”误判所以还要结合后续的方差变小和倾斜角变化。特征组合的核心是“先冲击后静止”这个时序模式。3.3 分类决策规则阈值与模型评分结合这个Demo没有用单一模型而是先跑一条快速规则链做粗筛再交给模型打分。规则链做两件事第一检查窗口内峰值合加速度是否超过预设的14m/s²即约1.4G第二检查窗口末尾的平均倾斜角相比窗口开头是否变化超过30度。这两个条件都不满足就直接返回“未跌倒”省去模型调用。int fall_detect(float features[5]) { if (features[1] 14.0f) return 0; // 没有明显冲击 if (fabsf(features[2]) 0.5f fabsf(features[3]) 0.5f) return 0; // 姿态未突变 // 粗筛通过使用朴素贝叶斯分类器的对数概率评分 float score logWeight[0] * features[4] logWeight[1] * features[1] logWeight[2] * features[0] bias; return score 0.5f ? 1 : 0; }粗筛的两个阈值可以在fall_detector.cpp顶部用宏定义方便快速调。logWeight是预训练模型的权重作者没有给出训练脚本但根据数值结构判断它是从一组标注好的跌倒/日常动作数据中统计出来的。如果你要换场景我会在第五章说怎么重训或替换。这里还有一个工程细节C返回0或1之后Java层并没有立刻报警而是再确认一次“这个状态持续了多久”。这是为了避开短促动作属于状态机的一部分。3.4 报警状态机防抖与复位逻辑报警状态机是这个工程里我最喜欢的一部分。它维护一个lastFallTime和fallCount变量只有当连续两个窗口都判定为“跌倒”并且两次判定时间间隔小于1秒时才真正触发报警。这相当于一个简单的时间滤波能滤掉弯腰系鞋带、蹲下起立这类短暂动作。public class FallDetector { private static final int CONFIRM_WINDOW_MS 1000; private long lastFallTime 0L; private int pendingFlags 0; public int onWindowFinished(float[] features, long currentTimeMs) { int nativeResult nativeDetect(features); if (nativeResult 1 (currentTimeMs - lastFallTime) CONFIRM_WINDOW_MS) { pendingFlags; if (pendingFlags 2) { pendingFlags 0; lastFallTime currentTimeMs; return 1; // 触发真正报警 } } else { pendingFlags 0; } if (nativeResult 1) lastFallTime currentTimeMs; return 0; } }参数说明CONFIRM_WINDOW_MS是两次判定允许的最大间隔pendingFlags是连续命中计数器。这个逻辑的巧妙之处在于它不要求一帧之内命中而是允许一次误判后紧接着再确认一次极大降低了单帧噪声的影响。报警触发后MainActivity会调用AlarmManagerHelper发送一个带震动和声音的通知。通知的channelId需要在Android 8以上的系统注册这点在第四章运行部分会踩到坑。4. 运行部署与避坑指南在真机上把这个Demo跑起来4.1 环境准备AS版本、SDK与Gradle配置工程默认用Android Studio打开建议用相对新的版本但Gradle版本并不激进。打开build.gradle时首先要确认三件事SDK版本、NDK版本、ABI过滤。工程里app/build.gradle中的关键配置大概长这样android { compileSdk 33 defaultConfig { applicationId com.demo.falling minSdk 21 targetSdk 33 ndk { abiFilters armeabi-v7a, arm64-v8a } externalNativeBuild { cmake { cppFlags -stdc11 } } } externalNativeBuild { cmake { path src/main/cpp/CMakeLists.txt } } }如果你本机的NDK版本太高CMake可能报“不兼容”但常见做法是直接在SDK Manager里安装24.0.0以上版本同时在local.properties指定NDK路径。如果你的电脑没有装NDKAndroid Studio会提示下载别忽略。ABI过滤只保留了两个主流平台模拟器通常是x86_64所以真机调试是必然选择。想跑模拟器的话需要把abiFilters改成x86_64但模拟器的传感器数据是模拟的没法测真实跌倒所以不如直接真机。4.2 权限配置与真机调试步骤Android 6以上运行时权限和Android 13通知权限都需要动态申请。这个Demo在MainActivity里用requestPermissions同时申请了ACTIVITY_RECOGNITION、POST_NOTIFICATIONS和VIBRATE。如果你跑在Android 14上还需要检查是否要声明FOREGROUND_SERVICE因为跌倒检测常被做成后台服务。真机调试的步骤比较简单# 开启手机开发者选项授权USB调试后连接电脑 adb devices # 安装Debug包 ./gradlew installDebug # 启动应用 adb shell am start -n com.demo.falling/.MainActivity启动后你会看到界面实时刷新三轴加速度和合加速度。这时候把手机放在口袋或绑在手臂上模拟“走路时手机滑落”的动作。注意不要把手机从高处猛砸到沙发上那样加速度峰值可能超过20m/s²反而触发报警。界面底部有一个“测试模式”按钮点击后会用合成的正弦波数据跑一遍检测流程方便你在室内没有剧烈动作时验证链路完整性。这个功能我建议所有做传感器应用的开发者都加上它省去了每次都要做危险动作的麻烦。4.3 三个典型坑采样率失效、崩溃、误报第一个坑是手机上报的采样率跟设定不一致。现象是合加速度曲线出现“阶梯状”每个点都持续几十毫秒不变。原因是某些系统在低电量时强制降频或者SensorManager的缓存队列满了。解决方法是像我3.1节那样用时间戳过滤同时把采样率从SENSOR_DELAY_GAME改成SENSOR_DELAY_FASTEST后者在部分设备上优先级更高。第二个坑是安装后打开直接崩溃Logcat显示UnsatisfiedLinkError。原因几乎总是ABI不匹配或CMake编译失败。查看app/build/intermediates/merged_native_libs目录确认lib下有没有libfall_detector.so。如果只有arm64目录而你的手机是32位CPU就会崩。解决方法是把两个ABI都编译进去然后检查手机CPU架构adb shell getprop ro.product.cpu.abi。第三个坑是误报极高走路带风都报警。这通常是阈值太低导致的。默认峰值阈值14m/s²对老人和儿童偏敏感因为他们的动作冲击更小。解决方法是把FALL_PEAK_THRESHOLD从14调到16或18同时拉长确认窗口到1500毫秒。我在测试中发现阈值调高后对真正摔倒是“灵敏度下降”但日常误报会少很多这需要根据佩戴位置调整。还有一个隐藏的坑如果你在Android 13及以上设备测试没有弹通知是因为没有动态申请通知权限。这是现象原因是targetSdk 33后才有的行为。解决是在MainActivity要主动请求POST_NOTIFICATIONS权限否则报警功能“静默失败”——代码不崩溃但用户看不到任何反馈。5. 二次开发指南把跌倒识别改造成你想要的监测模块5.1 替换分类模型如果你想把预置的朴素贝叶斯权重换成深度学习模型最顺滑的路径是引入TensorFlow Lite。具体做法是把fall_detector.cpp里的nativeDetect改成一个转调过程先把float[5]特征写入ByteBuffer再调用TFLite的interpreter-Invoke()拿输出。extern C JNIEXPORT jint JNICALL Java_com_demo_falling_FallDetector_nativeDetect(JNIEnv* env, jobject thiz, jfloatArray features) { jfloat* feat env-GetFloatArrayElements(features, nullptr); // 把feat传入你的模型输入 TfLiteTensor* input interpreter-input_tensor(0); memcpy(input-data.f, feat, 5 * sizeof(float)); if (interpreter-Invoke() ! kTfLiteOk) { return 0; } TfLiteTensor* output interpreter-output_tensor(0); float prob output-data.f[0]; env-ReleaseFloatArrayElements(features, feat, 0); return prob 0.6f ? 1 : 0; }这里的关键是TFLite的模型输入维度必须跟Interpreter实例匹配而且要在初始化时加载.tflite文件通常放到assets目录。我一般会用FileUtil.loadFile读取并映射到MemoryMappedBuffer避免一次性载入大对象。5.2 灵敏度调优调灵敏度不只是改阈值还要改滑动窗口长度和步长。比如你要检测的是“忽然坐下”冲击较小窗口应从200点缩短到100点步长从50缩短到25这样反应更快。反之如果检测滑倒保持默认更稳。建议做一个配置页面把窗口大小、峰值阈值、确认次数三个参数暴露给用户比每次改代码重新编译高效得多。5.3 扩展告警通道报警不能只弹通知。一个实用的改法是加一个BroadcastReceiver当检测到跌倒时发送自定义广播让主工程里的其他模块响应该事件——比如发短信给紧急联系人或者上传到云端。这个Demo的AlarmManagerHelper已经预留了triggerAlarm的入口你只需在其内部追加一个sendBroadcast即可。改动量很小却能把Demo真正挂进业务系统。我做这类项目养成了一个习惯任何检测类算法都要同时保留“原始数据导出”和“事件日志”两个开关。否则当你在现场遇到误报或漏报时手里没有任何可分析的素材。从那以后我每次拿到新工程都先找有没有数据落盘能力没有就先补上。这能帮你省掉大量靠猜的调试时间。希望这篇拆解对你跑通这个工程有帮助改出你想要的那版跌倒监测模块。本文还有配套的精品资源点击获取