简介:Android端实时跌倒检测与识别Demo,面向移动端AI开发者或安全监护类应用开发者,可用于养老看护、独居老人安全监控、公共场所异常行为预警等场景,解决将YOLOv5人体检测能力落地到手机端的工程参考问题。资源包总计2个文件,主要包括可直接安装到手机的APK调试包及其对应的构建信息JSON文件,APK用于真机运行与实时检测体验,JSON则记录构建环境便于技术核对,压缩包整体约50.44MB,体积精简便于下载使用。已有1945人学习下载,适合需要快速跑通跌倒检测流程的开发者。APK可直接安装到Android设备进行实时跌倒识别测试,感受摔倒检测的实际效果;配套作者博客系列还分别覆盖了跌倒检测数据集获取、YOLOv5训练实现、Android源码解读以及C++端实现,可作为从模型训练到多端部署的完整参考路径。
1. 跌倒检测识别 Android Demo:先弄清它能识别什么、不能识别什么
如果你手头的需求是“给家人手机做个跌倒提醒”,最稳的起步方式就是先拿一个 android 跌倒检测 Demo 跑通闭环——从传感器读取到摔倒识别再到通知报警。这个「跌倒检测识别 Android Demo」解决的正是这条链路:采集加速度计和陀螺仪数据,用状态机判断跌倒识别事件,触发通知与震动。它不是训练好的花哨模型,而是一个能让你在真机上感觉到“跌倒识别和摔倒识别到底怎么触发”的工程基线。适合想把行为识别快速落到手机端的 Android 开发者,也适合做看护类应用的人拿它当测试基准。要说清楚的是,它帮你判断“是不是摔了”,不负责分析“为什么摔”。
2. 传感器选型与识别原理:为什么加速度计比摄像头更适合跌倒检测
2.1 三条技术路线:摄像头、可穿戴与手机内置传感器
做跌倒识别,第一件事是选信号来源。摄像头方案依赖人形检测和姿态估计算法,室内光线不足、遮挡、隐私问题都很麻烦,而且摄像头一直开着,功耗和后台保活都是坎。可穿戴设备把传感器贴在身上,信号质量好,跌倒识别也准,但你要给用户发额外硬件,大多数人不会为尝鲜去额外买一只。手机内置传感器方案的优势,是它复用用户每天都带在身上的设备,加速度计和陀螺仪随系统采样,不需要额外硬件,也方便以后做远程报警。
拆这个 Demo 时我印象最深的一点是:它没有走“收集海量视频训练模型”的路子,而是先用手机传感器把事件检测出来。这样做的好处是传感器耗电小、响应快,数据是数值而不是画面,天然比摄像头方案更容易过隐私这一关。所以如果你要做的只是一个“提醒有人可能摔了”的预警层,手机传感器往往比摄像头更实用。等检测到跌倒事件,再联动摄像头确认现场,这样能兼顾功耗和隐私。
2.2 加速度计与陀螺仪的分工:合加速度、姿态角与冲击检测
加速度计负责测量手机三个轴上的加速度,静止时只感受到重力。这里的关键指标是合加速度 magnitude,计算公式是sqrt(ax*ax + ay*ay + az*az)。站立或端坐时,合加速度接近重力加速度(约 9.8 m/s²);如果手机突然自由落体,比如从裤袋掉出,合加速度会降到接近 0;身体撞击地面时,合加速度会在极短时间里冲到 20 m/s² 以上。这三个特征分别是“失重、撞击、恢复静止”的信号。
陀螺仪输出的是各轴角速度,用来补充旋转信息。比如人往侧面摔时手机会快速翻转,角速度出现明显突起。有些机型还有气压计,跌落时高度变化会导致气压跳变,但气压计在部分 Android 设备上被裁剪掉了,所以不适合做唯一依据。这份 Demo 的常见做法是:加速度计为主,陀螺仪为辅,必要时用重力方向计算手机姿态角,判断人是不是从直立变成平躺。
| 信号 | 跌倒特征 | 典型参考阈值 |
|---|---|---|
| 合加速度 | 失重瞬间接近 0 | < 2.0 m/s² |
| 合加速度 | 撞击瞬间突变 | > 20.0 m/s² |
| 姿态角 | 手机从直立接近水平 | 倾斜角 > 60° |
| 角速度 | 身体快速翻转 | 任一角速度 > 5 rad/s |
注意阈值不是死值,跑步、跳跃、上下楼都会带来类似的加速度突跳,所以必须有后面的多阶段状态机把关。
2.3 Demo 的状态机判定逻辑:失重、撞击、静止三阶段确认
如果把“合加速度突变”当成唯一判据,走路、拍口袋、蹦跳都容易误报。这个 Demo 把一次跌倒拆成三个阶段来确认,顺序是:先失重、再撞击、最后静止。
第一阶段是失重:加速度模值小于 2.0 m/s² 并持续约 200ms,此时手机处于近似自由落体状态。如果这个过程中任何一帧加速度回到 9.8 附近,就说明只是一次甩手或下蹲,状态复位。
第二阶段是撞击:失重结束后的几百毫秒内,加速度模值快速超过 20.0 m/s²。这里要注意,跑步落地也能达到 15~18,但不一定先有明显失重,所以状态机要求“必须先经过失重,才承认撞击”,这个顺序本身就是一道过滤器。
第三阶段是静止确认:撞击之后,如果合加速度在 1.5 秒左右恢复到 9.8 ± 2.0 之间,说明人摔到地面并且暂时不动了,这时才确认跌倒,触发通知和震动。加上静止确认,是为了过滤掉“摔下去马上爬起来继续走”的情况——那种场景大概率不是需要报警的跌倒。
完整数据流可以看成:传感器回调每次拿到一帧原始值,先做简单低通滤波,再计算合加速度与姿态角,然后喂给状态机。低通滤波可以用output = alpha * raw + (1 - alpha) * output,alpha 取 0.3~0.5 即可,太重会抹掉撞击尖峰,太轻则保留噪声。状态机里还埋了一个姿态变化条件:如果撞击后检测到手机接近水平,也就是重力方向在设备坐标系 Z 轴的分量变小到接近 0,置信度会更高。遇到扶着墙慢慢坐下来的情况,失重阶段根本不会触发,也就不会误报。如果你后续想替换成机器学习模型,也可以保留这个状态机做第一层粗筛,再让模型做细判,这样推理频率不用拉满,对功耗更友好。
| 常见动作 | 合加速度特征 | 是否能通过三阶段 |
|---|---|---|
| 正常走路 | 偶尔出现 1.5~2.5 波动 | 通常不能,失重段不够长 |
| 跑步落地 | 冲击可到 15~18,无明显失重 | 不能,顺序不对 |
| 弯腰捡东西 | 失重短暂且伴随晃动 | 不能,姿态基本直立 |
| 从站到侧摔 | 明显失重 + 撞击 + 静止 | 能,触发报警 |
3. 把工程跑起来:从 Android Studio 导入到真机权限配置
3.1 导入前准备:SDK 版本、构建工具与模拟器限制
拿到 Demo 后别直接双击 APK,先用 Android Studio 打开工程。常见做法是选择Open an Existing Project,定位到解压目录下的根build.gradle,等 Gradle Sync 跑完。这里第一个坑是版本:如果 Demo 用的 Gradle 插件比较老,新版 Android Studio 会提示升级,建议先点“暂不升级”,否则升级牵连到 AGP 和 Kotlin 版本,排查起来很花时间。
编译要求一般是minSdkVersion 21以上,targetSdk看你手里的 Demo 而定。minSdk 过低会在大屏或低版本机上有兼容问题,过高则要处理分区存储和通知权限。我一般会在app/build.gradle里把targetSdk对齐到当前要发布的应用市场要求,然后按这个版本去补运行时权限。
模拟器基本上跑不出真实效果,因为它没有加速度计传感器,getDefaultSensor通常会返回null。所以验证跌倒识别一定要用真机。如果手里的真机是 Android 13 及以上,记得在运行前先授权通知权限,否则后面报警不会弹。
注意:权限声明只写了
POST_NOTIFICATIONS还不够,它在 Android 13 上是运行时权限,必须在页面里动态申请,否则通知会被系统静默拦截。
权限配置在AndroidManifest.xml里,按需声明:
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.VIBRATE" />说明一下:WAKE_LOCK是防止屏幕熄灭后 CPU 休眠导致传感器停更;FOREGROUND_SERVICE是让检测服务常驻;POST_NOTIFICATIONS是 Android 13 引入的运行时通知权限;VIBRATE用于报警震动。如果 Demo 里没写FOREGROUND_SERVICE_TYPE,较新 SDK 可能要求补上,比如android:foregroundServiceType="specialUse"。
3.2 传感器数据采集代码:注册 SensorManager 与回调缓冲
传感器数据通过SensorManager注册监听。为了让状态机有稳定的采样节奏,我一般用SENSOR_DELAY_GAME,这个档位在多数设备上约 50Hz,每帧间隔 20ms 左右,既能满足跌倒特征捕捉,又不会像SENSOR_DELAY_FASTEST一样把 CPU 拉满。
private SensorManager sensorManager; private long lastSampleTime = 0L; private static final long SAMPLE_INTERVAL_MS = 20L; sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); Sensor accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); if (accelerometer == null) { // 没有加速度计就不支持跌倒检测,降级处理 return; } sensorManager.registerListener(sensorEventListener, accelerometer, SensorManager.SENSOR_DELAY_GAME);注意这里没有注册陀螺仪,因为陀螺仪全速采样很耗电,先用加速度计验证主流程,等要接入模型时再把陀螺仪加进去。SAMPLE_INTERVAL_MS是手动限频:部分设备即使声明了SENSOR_DELAY_GAME,回调频率也可能高于标称值,在onSensorChanged里做时间戳过滤能保证状态机看到的数据节奏稳定。
3.3 检测与报警代码:状态机实现、通知与震动
回调里先把数据交给检测器:
@Override public void onSensorChanged(SensorEvent event) { if (event.sensor.getType() != Sensor.TYPE_ACCELEROMETER) return; long now = System.currentTimeMillis(); if (now - lastSampleTime < SAMPLE_INTERVAL_MS) return; lastSampleTime = now; fallDetector.feed(event.values[0], event.values[1], event.values[2], now); }状态机核心用枚举切换。下面这段是简化版,把前面说到的三阶段落成代码:
public void feed(float ax, float ay, float az, long timestamp) { double magnitude = Math.sqrt(ax * ax + ay * ay + az * az); switch (state) { case NORMAL: if (magnitude < FREEFALL_THRESHOLD) { freefallStartTime = timestamp; state = State.FREEFALL; } break; case FREEFALL: if (timestamp - freefallStartTime > FREEFALL_DURATION_MS && magnitude < FREEFALL_THRESHOLD) { state = State.WAITING_IMPACT; } else if (magnitude >= FREEFALL_THRESHOLD) { state = State.NORMAL; } break; case WAITING_IMPACT: if (magnitude > IMPACT_THRESHOLD) { impactTime = timestamp; state = State.RECOVERING; } else if (timestamp - freefallStartTime > STAGE_TIMEOUT_MS) { state = State.NORMAL; } break; case RECOVERING: if (timestamp - impactTime > VERIFY_STILL_MS && magnitude > NEAR_GRAVITY_LOW && magnitude < NEAR_GRAVITY_HIGH) { state = State.CONFIRMED; onFallDetected(); } else if (timestamp - impactTime > STAGE_TIMEOUT_MS) { state = State.NORMAL; } break; case CONFIRMED: // 报警后进入死区,避免重复触发 if (timestamp - impactTime > ALARM_COOLDOWN_MS) { state = State.NORMAL; } break; } }几个参数建议放在单独的常量类里:FREEFALL_THRESHOLD用 2.0,FREEFALL_DURATION_MS用 200~300,IMPACT_THRESHOLD用 20.0,VERIFY_STILL_MS用 1500~2000,NEAR_GRAVITY_HIGH/LOW用 12.0 和 7.0,STAGE_TIMEOUT_MS用 3000。这里最需要注意的是WAITING_IMPACT阶段,如果在失重后迟迟等不到撞击,就说明也许只是把手机往上抛,必须超时复位,否则状态机卡死。
确认跌倒后,发通知加震动。这段是 Android 13 以下会用的写法,高版本要额外检查通知权限:
NotificationManager manager = getSystemService(NotificationManager.class); if (Build.VERSION.SDK_INT >= 26) { NotificationChannel channel = new NotificationChannel( "fall_alert", "跌倒报警", NotificationManager.IMPORTANCE_HIGH); manager.createNotificationChannel(channel); } NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "fall_alert") .setSmallIcon(R.drawable.ic_fall_alert) .setContentTitle("检测到跌倒") .setContentText("请确认是否需要帮助") .setPriority(NotificationCompat.PRIORITY_MAX); notificationManager.notify(FALL_NOTIFY_ID, builder.build()); Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); vibrator.vibrate(new long[]{0, 500, 300, 500}, -1);注意震动和通知只是最低限度的本地提醒。做正式产品时,还要把确认事件写入本地日志,或通过回调发送到服务器,方便家属和平台联动。
4. 避坑指南:跌倒检测 Demo 常见的 5 个翻车现场
4.1 传感器失效与系统吞权限:现象、原因与解决
现象一:模拟器上打开就闪退。原因是模拟器没有加速度计,getDefaultSensor返回null,代码没判空就去注册监听。解决:注册前先判断传感器是否存在,不存在就提示“当前设备不支持跌倒检测”,别硬启动。这类问题通常在第一次真机 Debug 时遇到,分不清是权限问题还是传感器问题,先用一行 logcat 把getSensorList的结果打出来,是最快的定位办法。
List<Sensor> accelSensors = sensorManager.getSensorList(Sensor.TYPE_ACCELEROMETER); if (accelSensors.isEmpty()) { // 不支持跌倒检测,禁用相关按钮 return; }现象二:Android 13 真机上通知不弹,但代码没报错。原因是POST_NOTIFICATIONS属于运行时权限,只在 Manifest 声明还不够,需要在启动时动态申请,否则notify()会被系统静默忽略。解决:在 MainActivity 的onCreate里调用requestPermissions,拿到授权后再进入检测页面;同时检查通知开关。
if (Build.VERSION.SDK_INT >= 33 && checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 2001); }现象三:屏幕熄灭几个小时后检测完全失效。原因是应用没有跑前台服务,也没有持 WakeLock,系统休眠后传感器回调被冻结。解决:把传感器监听放到前台服务里,设置android:foregroundServiceType,并在onCreate里获取PARTIAL_WAKE_LOCK;还要引导用户打开忽略电池优化,否则国产 ROM 的省电策略仍然会杀掉进程。
@Override public void onCreate() { PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "FallApp:WakeLock"); wakeLock.acquire(); } @Override public void onDestroy() { if (wakeLock != null && wakeLock.isHeld()) { wakeLock.release(); } }4.2 误报与漏报并存:现象、原因与解决
现象四:弯腰捡钥匙、快速下蹲,甚至拍桌上的手机都会误报。原因是阈值只认加速度模值,忽略整个时序和姿态。下蹲时也会有失重段和落地冲击,但手机没有从直立变成水平。解决:状态机增加两重约束,失重必须超过 200ms,撞击后手机姿态接近水平才算确认。姿态角可以用重力向量在 Z 轴的分量算:
double gz = az / magnitude; double tiltAngle = Math.acos(Math.abs(gz)); if (tiltAngle > Math.toRadians(60.0)) { // 手机接近水平,跌倒置信度更高 }现象五:报警延迟 4 秒以上,或者一条报警没处理完又触发第二条。原因是静止确认窗口设定太长,且没有报警死区与超时复位。解决:VERIFY_STILL_MS设成 1500~2000ms,确认后进入 30 秒冷却期,冷却期内不再触发新报警;在状态机每一阶段都加上超时复位,防止卡死。另一个技巧是不要在SensorEventListener里直接写文件或做网络请求,这些耗时操作会阻塞回调,也会拉高延迟,事件确认后把动作丢到单线程 Handler 里处理。
case CONFIRMED: if (timestamp - impactTime > ALARM_COOLDOWN_MS) { state = State.NORMAL; } break;5. 提升识别准确率:从阈值调参到替换成 TFLite 模型
5.1 阈值参数调优:灵敏度与误报的平衡
把 Demo 跑通后,第一步不是急着找模型,而是先把手里的参数调到能过你的场地测试。我一般在调参前定义好两组动作:一组是正常行为(走动、慢跑、弯腰、坐下、跳一下),另一组是模拟跌倒(向前倒、向后倒、侧倒、从沙发上滑落)。然后跑几轮,统计误报数和漏报数。
几个关键参数的调整逻辑:
| 参数 | 调高时的效果 | 调低时的效果 |
|---|---|---|
| FREEFALL_THRESHOLD | 更容易进入失重态,对甩动手臂更敏感 | 不易失重,漏报下降,误报也会减少 |
| FREEFALL_DURATION_MS | 需要有更长的失重时间,过滤瞬时抛掷 | 失重刚出现就进入撞击等待,容易把快速下蹲当跌倒 |
| IMPACT_THRESHOLD | 要求更强烈的撞击,漏报可能增加 | 容易把跳跃或跺脚识别为撞击 |
| VERIFY_STILL_MS | 要求静止更久,降低误报但延迟变高 | 忽略“摔后不动”这一特征,爬起来也会报警 |
实际操作中,我会先固定FREEFALL_DURATION_MS=250和VERIFY_STILL_MS=1800,主要调两个阈值,直到自己的场地测试通过。如果某类动作频繁误报,就单独记录它的传感器曲线,看看是失重段不明显还是撞击幅度不够,对症去调参数,而不是无脑把阈值往上推。
5.2 接入 TFLite 模型:训练后转格式与 Interpreter 调用
阈值方案毕竟手调上限有限。等你有了一批标注数据,可以把模型替换成 TensorFlow Lite。常见做法是先用 LSTM 或一维 CNN 处理 3 秒窗口的六轴序列,输出跌倒/非跌倒两类概率。
训练环境里把模型导出成 SavedModel,再转成 tflite。下面这段是在 Python 一侧做转换的常用写法:
import tensorflow as tf converter = tf.lite.TFLiteConverter.from_saved_model("fall_model_saved") converter.target_spec.supported_types = [tf.float16] converter.optimizations = [tf.lite.Optimize.DEFAULT] tflite_quant_model = converter.convert() with open("fall.tflite", "wb") as f: f.write(tflite_quant_model)说明:target_spec.supported_types指定 FP16 半精度,模型体积大约减半,普通手机 CPU 和 GPU 都能跑,精度损失一般可以接受。如果想把体积压得更小,可以改成 INT8 量化,但需要准备校准数据集,而且部分老旧设备对 INT8 算子的支持不如 FP16 好。
Android 端把fall.tflite放进assets目录,加载和推理用Interpreter:
try (Interpreter tflite = new Interpreter(loadModelFile(this, "fall.tflite"))) { float[][][] input = new float[1][WINDOW_FRAMES][6]; float[][] output = new float[1][2]; tflite.run(input, output); float score = output[0][1]; if (score > 0.8f && stateMachine.allowsAlert()) { notifyFall(); } } catch (IOException e) { Log.e("FallModel", "model load failed", e); }这里WINDOW_FRAMES对应 3 秒窗口乘以 50Hz,也就是 150 帧。输入矩阵的最后一维是[ax, ay, az, gx, gy, gz]。喂给模型之前最好按训练时用的均值和标准差做归一化,否则不同手机的传感器量程差异会直接拉低准确率。注意Interpreter是线程不安全的,如果检测服务里同时有多个入口调用它,最好加锁或复用单例。
5.3 自制跌落数据:录制 CSV 样本并快速验证
没有现成数据集时,自己录样本是最快的路径。我一般会让被测者把手机放在裤袋或随身包里,然后按动作顺序录制。录数据代码写得很简单,把每次回调追加到 CSV 文件:
File file = new File(getExternalFilesDir(null), "fall_samples.csv"); try (FileWriter writer = new FileWriter(file, true)) { String line = String.format("%d,%.4f,%.4f,%.4f,%.4f,%.4f,%.4f%n", System.currentTimeMillis(), ax, ay, az, gx, gy, gz); writer.write(line); } catch (IOException e) { Log.e("CsvRecorder", "write failed", e); }写 CSV 时不要把动作标记行插在传感器数据行中间,因为保存数据本身会带来几十毫秒延迟,跨线程写文件还会打乱时序。更好做法是先开一个独立事件回调,每个动作开始前插入一行带时间戳的标记,数据采集完再做对齐。录制时每个动作重复 5~8 次,跌倒动作要控制好落点,最好在垫子或床上做,别拿真马路去拼。
录完以后,用导出的 CSV 跑离线回放比反复真机测试快得多。你可以在电脑上写个小脚本,把同样一份 6 轴数据按 20ms 一帧喂给状态机,输出检测到的事件时间,再对照标注判断漏报和延迟。这个流程会比每次插真机、找姿势快很多。
6. 验证与交付:给看护场景做一个最小可用版本
6.1 交付前跑一遍「半小时烟雾测试」
这里我要推荐一个每次都用的验证方法:交付前固定跑半小时烟雾测试。测试用例要覆盖最容易翻车的几个动作,而不是只在手里颠两下。
| 测试动作 | 预期结果 |
|---|---|
| 正常走路 / 慢跑 | 不触发报警 |
| 弯腰捡东西 / 蹲下再站起 | 不触发报警 |
| 坐沙发时快速后仰 | 可能触发,需人工确认 |
| 向前跌倒 / 向后跌倒 | 触发报警,延迟 < 2s |
| 摔倒后立刻坐起来 | 不触发或延迟报警 |
测试时用adb logcat把检测服务日志导出来,方便回看:
adb logcat -s FallDetector -v time > fall_run.log跑完之后看两样东西:一是该触发的有没有触发,二是误报率是不是高到会让人关掉通知。如果误报太高,宁可把灵敏度降一点。
6.2 给误报留一个「后悔药」按钮
看护场景里,误报对老人的打扰比漏报对家属的惊吓更伤人。所以我会在确认跌倒后弹一个 10 秒倒计时通知,用户不需要时说“我没事”,通知和震动立刻撤掉。这样即使传感器把一次坐空当成了跌倒,也能在造成恐慌之前被取消。
new CountDownTimer(10000, 1000) { @Override public void onFinish() { // 保持报警并推送给家属端 } @Override public void onTick(long millisUntilFinished) { updateNotification(millisUntilFinished); } }.start();倒计时结束才推送,给现场用户留出反应时间,也避免通知刷屏。
我从第一次做模拟项目X的跌倒检测翻车之后,养成了一个习惯:任何阈值或模型改动,先跑同一组录好的样本做离线验证,再上真机。这能帮你把“感觉应该好了”和“真的好了”分开。希望这套验证流程也能帮到你。
本文还有配套的精品资源,点击获取