简介:这是一份基于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 "-std=c++11" } } } externalNativeBuild { cmake { path "src/main/cpp/CMakeLists.txt" } } }如果你本机的NDK版本太高,CMake可能报“不兼容”,但常见做法是直接在SDK Manager里安装24.0.0以上版本,同时在local.properties指定NDK路径。如果你的电脑没有装NDK,Android 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真正挂进业务系统。
我做这类项目养成了一个习惯:任何检测类算法都要同时保留“原始数据导出”和“事件日志”两个开关。否则当你在现场遇到误报或漏报时,手里没有任何可分析的素材。从那以后,我每次拿到新工程,都先找有没有数据落盘能力,没有就先补上。这能帮你省掉大量靠猜的调试时间。希望这篇拆解对你跑通这个工程有帮助,改出你想要的那版跌倒监测模块。
本文还有配套的精品资源,点击获取