简介:面向Android开发者的跌倒检测识别Demo压缩包,解决移动端实时跌倒和摔倒检测与告警问题,适用于老人看护、独居监护、安防巡检等场景,适合具备Android或深度学习基础的开发者快速接入相关能力。压缩包共两个文件,主体是release版APK安装包,可直接安装到设备体验实时识别效果,另附一个JSON元数据文件记录构建信息,整体约五十MB。目前已有1945人学习,下载即可获得一个无需编译的开箱即用Demo,能快速验证跌倒检测功能。该Demo基于YOLOv5目标检测方案实现,可在手机端识别跌倒姿态,配套系列博客还提供跌倒检测数据集与训练代码下载指引,帮助读者完整走通数据训练、模型优化到端侧部署的链路,通过该Demo可以快速评估模型效果,并为自建跌倒识别App提供参考架构。
1. 老人独自在家摔倒没人知道,这个 Android Demo 就是干这个的
家里老人独自在客厅活动,从摔倒到被发现往往超过半小时;独居年轻人洗澡滑倒,手机就在一米外却拿不到。这个「跌倒检测识别 Android Demo」解决的问题很具体:利用手机内置的加速度计,以约 50Hz 频率采集三轴加速度,通过滑动窗口实时计算合加速度变化,捕捉摔倒瞬间的冲击、失重与后续静止特征,触发弹窗报警、震动提示并写入本地日志。它作为一套 android 跌倒检测入门工程,可以直接导入 Android Studio 编译运行,同时把跌倒识别算法独立封装成检测器模块,适合做健康监测、老人看护类应用的开发者拿去做二次开发。对新手来说,它是一条完整的传感器采集、特征计算、阈值判定链路;对熟手来说,它是可以替换算法模型和报警策略的现成骨架。
2. 工程结构与数据链路:一个检测项目从界面到传感器的完整骨架
2.1 项目目录:检测器、数据采集器和界面各自待在哪
这个 Demo 的工程结构不算复杂,但分层很清晰。拿到解压后的项目,首先会看到 app 模块下的标准 Android 工程布局,核心代码都集中在 java 目录里。我一般会把这样的项目按职责划成三层:界面层负责启动检测和展示报警结果,采集层负责向传感器要数据,算法层负责从原始数据里判断是否发生跌倒。
app/src/main/ ├── java/com/example/fall_detection/ │ ├── MainActivity.java // 入口界面:启动/停止检测、显示状态 │ ├── sensor/ │ │ ├── SensorDataCollector.java // 传感器采集器:注册监听、回调原始数据 │ │ └── FallDetector.java // 跌倒检测器:窗口缓冲、特征计算、判定逻辑 │ └── ui/ │ ├── AlertActivity.java // 报警界面:全屏提醒、震动、日志写入 │ └── AlertDialogHelper.java // 弹窗辅助类,封装报警展示逻辑 ├── res/ │ ├── layout/ │ │ ├── activity_main.xml // 主界面布局:按钮、状态文字 │ │ └── activity_alert.xml // 报警界面布局:大字提醒、停止按钮 │ └── values/ │ ├── strings.xml │ └── colors.xml └── AndroidManifest.xml // 权限声明、Activity 注册这个包结构有一个值得借鉴的点:FallDetector 不持有任何 Android 上下文,它只接收 float 数组、输出布尔结果。这样设计的好处是算法可以脱离手机单独测试——想验证判定逻辑,直接写一个 Java main 方法喂数据就行,不用启动模拟器。采集器和检测器之间的调用关系也很简单:SensorDataCollector 在 onSensorChanged 回调里拿到三轴加速度,转成 float 数组交给 FallDetector 的 addSample 方法,检测器内部维护窗口,返回结果后由 MainActivity 决定是否拉起报警界面。
2.2 传感器采集链路:SensorManager 里那趟 50Hz 的数据管线
整个 Demo 的数据源头是 SensorManager 注册的加速度计监听。这段代码决定了后续所有算法的输入质量,频率选低了漏检,选高了耗电,所以通常取 SENSOR_DELAY_GAME 这个档位,大约每 20 毫秒回调一次,也就是 50Hz。两秒的窗口正好需要 100 个采样点,这个数字在后面对应缓冲区的长度。
public class SensorDataCollector implements SensorEventListener { private static final int SAMPLING_RATE = SensorManager.SENSOR_DELAY_GAME; private SensorManager sensorManager; private Sensor accelerometer; private OnSensorDataCallback callback; public interface OnSensorDataCallback { void onAccelerationData(float x, float y, float z, long timestamp); } @Override public void onSensorChanged(SensorEvent event) { if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) { float x = event.values[0]; // 手机坐标系 X 轴加速度,单位 m/s² float y = event.values[1]; // 手机坐标系 Y 轴加速度 float z = event.values[2]; // 手机坐标系 Z 轴加速度 long timestamp = event.timestamp; callback.onAccelerationData(x, y, z, timestamp); } } public void start() { sensorManager.registerListener(this, accelerometer, SAMPLING_RATE); } public void stop() { sensorManager.unregisterListener(this); } }这段代码里有几个参数值得留意。SENSOR_DELAY_GAME 在大多数机型上对应 20ms 左右的回调间隔,但部分 ROM 在息屏或省电模式下会把这个间隔拉长到 50ms 以上,后面避坑章节会专门说这个问题。timestamp 用的是事件自带的时间戳,而不是 System.currentTimeMillis(),因为纳秒级时间戳在做插值和窗口对齐时更可靠。回调接口单独定义,而不是直接把 FallDetector 传进来,是为了后续接 TensorFlow Lite 替换算法时不动采集层代码。
2.3 构建配置与权限:跑起来前要确认的三个点
Demo 项目使用的构建配置很克制,没有引入重型依赖。build.gradle 里通常只需要一个 appcompat 支持库和一个 constraintlayout,传感器相关 API 属于 Android 框架自带,不需要额外依赖。这在导入工程时是个优点——同步速度快,不容易因为网络问题卡在依赖下载上。需要重点检查的是 SDK 版本配置,我一般会确认 compileSdk 和 targetSdk 保持在本地已安装的 SDK 版本附近,否则会提示下载缺失平台。
android { namespace 'com.example.fall_detection' compileSdk 33 defaultConfig { applicationId "com.example.fall_detection" minSdk 21 targetSdk 33 versionCode 1 versionName "1.0" } } dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' }AndroidManifest 里的权限声明是整个 Demo 跑通的关键。加速度计属于普通权限,不需要运行时申请,但两个隐性的权限要注意:一是报警时震动需要声明 VIBRATE,二是 SDK 33 及以上弹通知需要在运行时申请 POST_NOTIFICATIONS 权限,否则报警声音能响但通知栏看不到消息。常见做法是直接在 MainActivity 的 onCreate 里统一检查这两个权限,避免报警触发了用户却毫无感知。
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <application> <activity android:name=".MainActivity" /> <activity android:name=".ui.AlertActivity" android:exported="false" android:showOnLockScreen="true" /> </application>showOnLockScreen 这个属性值得专门说一句。跌倒场景下用户很可能已经倒地,手机可能已经锁屏,报警界面设置成可以在锁屏上显示,能最大限度让周围人看到提醒。这个属性在部分国产 ROM 上会被忽略,但在原生系统上实测有效。如果需要语音播报,还要额外处理音频焦点,Demo 阶段可以先不做。
3. 核心代码拆解:跌倒识别里的冲击、失重与静止三阶段判定
3.1 特征计算:先把三轴数据折算成合加速度
跌倒检测的第一件事不是直接判断,而是把三维的加速度向量折算成一个标量,这样才能用简单的阈值去刻画「剧烈冲击」这个特征。合加速度 SVM 的计算公式是取三轴的平方和再开方,静止时它约等于 9.8 m/s²,也就是 1g;摔倒时人体撞到地面,这个值会瞬间冲到 2.5g 到 4g 以上。这个折算过程把三个维度的判断变成了一个维度的比较,是后面所有阈值逻辑的基础。
public class FallDetector { private static final float GRAVITY = 9.81f; private static final float IMPACT_THRESHOLD = 3.0f * GRAVITY; // 冲击阈值:约 3g private static final float WEIGHTLESS_THRESHOLD = 0.5f * GRAVITY; // 失重阈值:约 0.5g private static final float REST_THRESHOLD = 1.8f * GRAVITY; // 静止判定阈值 private static final float POSE_CHANGE_THRESHOLD = 60f; // 姿态变化角度阈值 private float computeSvm(float x, float y, float z) { return (float) Math.sqrt(x * x + y * y + z * z); } }四个阈值是这套算法的关键参数。IMPACT_THRESHOLD 取 3g 是个常见的经验值,大部分日常活动如快步走、弯腰、坐下都很难超过 2g,真正的跌倒冲击通常远超 3g,所以这个值能在误报和漏检之间取一个平衡。WEIGHTLESS_THRESHOLD 检测的是跌倒初期的失重阶段,人失去支撑到落地之间那几百毫秒,合加速度会跌破 0.5g,这是区分跌倒和「被重物砸到」之类纯冲击事件的重要中间态。REST_THRESHOLD 用于确认落地后人体短暂静止——正常摔倒后不太可能马上站起来跑动。
3.2 窗口缓冲与状态机:2 秒时间窗里走完三阶段判定
单次采样值没有意义,跌倒是一个过程,所以需要用一个环形缓冲区保存最近 2 秒的采样数据。把数据结构和状态判定分开写是这套代码比较清爽的地方:环形缓冲只负责存储和滑动,状态机负责消费缓冲里的数据。检测器内部维护一个枚举状态,依次经过 IDLE、IMPACT、WEIGHTLESS、POSTURE_CHECK、ALERT 这几个阶段,任一阶段条件不满足就回到 IDLE 重新等待。
public class FallDetector { private enum State { IDLE, IMPACT_DETECTED, WEIGHTLESS_DETECTED, REST_CONFIRMED, ALERT } private State state = State.IDLE; private float[] buffer = new float[100]; // 2秒窗口:50Hz采样 x 2秒 private int bufferIndex = 0; private long lastImpactTime = 0; public void addSample(float x, float y, float z) { buffer[bufferIndex] = computeSvm(x, y, z); bufferIndex = (bufferIndex + 1) % buffer.length; switch (state) { case IDLE: if (isLocalMaxAboveThreshold()) { state = State.IMPACT_DETECTED; lastImpactTime = System.currentTimeMillis(); } break; case IMPACT_DETECTED: if (hasWeightlessPhase()) { state = State.WEIGHTLESS_DETECTED; } else { state = State.IDLE; } break; case WEIGHTLESS_DETECTED: if (isStableAfterFall()) { state = State.ALERT; } else { state = State.IDLE; } break; } } public boolean isFallDetected() { return state == State.ALERT; } }状态机的核心思想是:不依赖单一次采样,而是验证一条完整的事件序列。isLocalMaxAboveThreshold 检查窗口内的局部最大值是否超过 3g,也就是寻找冲击峰;hasWeightlessPhase 在冲击峰附近往回找是否有低于 0.5g 的采样点;isStableAfterFall 检查冲击后 1 秒内窗口的均值是否回落到 1.8g 以下,模拟「摔完躺平不动」。只有这三个条件按顺序都成立,才拉响警报,这也是这套算法误报率比「只看冲击阈值」低一个数量级的原因。
3.3 姿态角辅助判断:解决手机摔了但人没事的尴尬
只靠加速度幅值有一个经典误报场景:手机从沙发滑落到地板,合加速度冲击也轻松超过 3g。为了过滤这类「手机摔了但人没事」的情况,Demo 引入了姿态角变化判断。手机的姿态可以近似用重力方向在手机坐标系里的分量计算,跌倒前人体竖直时,重力主要在 Z 轴;倒地后手机横躺,重力方向会转到 X 轴或 Y 轴。这个角度变化用反正切函数计算即可。
private float computeTiltAngle(float x, float y, float z) { // 重力方向与手机Z轴的夹角,单位:度 double verticalComponent = z; double horizontalComponent = Math.sqrt(x * x + y * y); double angle = Math.toDegrees(Math.atan2(horizontalComponent, verticalComponent)); return (float) angle; }逻辑说明:当手机竖直放置时,horizontalComponent 接近 0,角度接近 0 度;手机平躺时,z 接近 0,角度接近 90 度或 180 度。在实际判断中,我把跌倒前后的姿态角差作为一个附加条件——超过 60 度才认为人体姿势发生了大幅变化。这个参数不能设太小,否则低头捡东西这种动作也会误判。需要理解的是,姿态角判断依赖的是重力方向而不是手机屏幕方向,所以即使手机插在口袋里也能工作,这是很多新手容易搞反的点。
3.4 报警触发与日志:检测到之后怎么让周围的人注意到
检测结果最终要落到用户可感知的动作上。Demo 的做法是跳转到一个全屏的 AlertActivity,同时震动并写入日志。这里有一个设计取舍:为什么不直接在 MainActivity 里弹 Dialog?因为 Dialog 在锁屏状态下很可能弹不出来,而且如果 MainActivity 已经被系统回收,跳转 Activity 更可靠。日志写入则选用了 getExternalFilesDir 路径,这在 Android 10 以后不需要存储权限,是分区存储时代比较省事的做法。
public class AlertActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_alert); Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); long[] pattern = {0, 500, 200, 500}; // 立即开始、震动500ms、停200ms、再震500ms vibrator.vibrate(pattern, 0); writeLogToFile("Fall detected at " + System.currentTimeMillis()); // 这里预留接口:可以继续扩展为发送短信、上传云端或拨打紧急电话 } private void writeLogToFile(String message) { File logFile = new File(getExternalFilesDir(null), "fall_log.txt"); try (FileWriter writer = new FileWriter(logFile, true)) { writer.append(message).append("\n"); } catch (IOException e) { e.printStackTrace(); } } }震动模式采用长震动加间隔,比单次震动更容易引起注意。日志文件追加写入而非覆盖写,保证多次检测记录不丢失。FileWriter 以 true 打开即追加模式,这个细节对后续做数据分析很重要——你可以拿着 log 文件回放当时的传感器趋势,判断阈值设得合不合理。这段代码还留了一个扩展注释点:想接短信报警,只需要在同一个位置调用 SmsManager;想接云端推送,把消息发到自己的服务器即可。
4. 运行与避坑排查:从导入工程到真机调试的关键步骤
4.1 从 Android Studio 导入到真机运行的完整流程
这个 Demo 导入 Android Studio 的过程很标准,但有几处容易翻车的点值得提前说。第一步是在欢迎页选择 Open,直接选中解压后的项目根目录,等 Gradle 同步完成。Gradle 同步阶段最常见的失败原因是 SDK 版本不匹配,报错信息里会明确提示缺少哪个版本的 Build Tools 或 Platform,点 SDK Manager 安装对应版本即可,不需要修改项目代码。
第二步是真机准备。跌倒检测必须依赖真实的加速度计,模拟器无法真实模拟摔倒的动态过程,所以不要想着在 AVD 上验证效果。开启开发者选项里的 USB 调试后连接电脑,Android Studio 会自动识别设备。注意部分机型首次连接需要在手机上确认 RSA 指纹授权,否则会一直显示 offline。
第三步是在 MainActivity 里点开始检测,然后把手机握在手里,做一个「快速下蹲 + 顺势倒地」的动作。如果动作幅度不够大,合加速度峰值达不到 3g 阈值,就不会触发报警。测试时需要拉开架势,尽力模拟真实的摔倒冲击。下面这段是 MainActivity 里启动检测的核心调用:
public class MainActivity extends AppCompatActivity { private SensorDataCollector dataCollector; private FallDetector fallDetector; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); checkRequiredPermissions(); fallDetector = new FallDetector(); dataCollector = new SensorDataCollector(); dataCollector.setCallback((x, y, z, timestamp) -> { fallDetector.addSample(x, y, z); if (fallDetector.isFallDetected()) { runOnUiThread(() -> { Intent intent = new Intent(this, AlertActivity.class); startActivity(intent); }); } }); } private void checkRequiredPermissions() { if (Build.VERSION.SDK_INT >= 33) { requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 1); } } }这段代码有一个值得注意的细节:回调里直接使用 lambda 把传感器数据喂给检测器,检测到跌倒后通过 runOnUiThread 切换到主线程再启动 Activity。传感器回调本身运行在 Looper 所在的线程,如果直接在这里 startActivity 会抛异常,因为跨线程 UI 操作必须经过主线程。这也是新手最容易踩的坑之一。
4.2 高频踩坑记录:真机调试时反复遇到的四个问题
现象一:模拟跌倒动作时永远不报警,但看日志发现合加速度明明很高。原因是采样频率不够,部分省电模式下 SENSOR_DELAY_GAME 会被拉长到 40ms 以上,冲击峰值恰好落在两次采样之间就漏掉了。解决办法是不要只依赖 onSensorChanged 的调用频率,在检测器里做一次时间戳校验,如果相邻两帧间隔超过 30ms,就把上一步动作记录保留到下一帧,不让状态机直接丢弃冲击窗口。
现象二:手机从沙发上滑落却触发了报警。原因是只有冲击阈值而没有考虑人体失重和姿态变化。对照前面的状态机代码,检查是否完整实现了三阶段判定;如果只实现了 IMPACT_THRESHOLD 单条件,再高的姿态角过滤逻辑也救不回来。这个问题的本质是「加速度冲击」和「人体跌倒」不是等价事件,需要把状态机补全。
现象三:日志文件写不进去,FileWriter 一直报 FileNotFoundException。原因是 targetSdk 升到 29 以上后,路径直接写成 Environment.getExternalStorageDirectory() 会被分区存储拦截。解决方法是改用 getExternalFilesDir(null),这个路径属于应用专属目录,不需要任何存储权限,卸载应用时数据自动清除,对 Demo 来说反而更干净。
现象四:锁屏后检测服务被系统杀掉,解锁发现报警 Activity 还在但不是自己触发的。原因是普通 Activity 无法在锁屏状态下稳定运行,很多 ROM 会在息屏几分钟后回收后台 Activity。解决方法是尝试给 MainActivity 所在的包关闭电池优化,或者在应用内申请忽略电池优化白名单;如果要在后台持续检测,需要把采集器放进前台服务并伴随常驻通知。Demo 阶段用 Activity 方案足够,真正产品化必须迁移到 Service。
5. 二次开发:把阈值 Demo 改造成真正可用的跌倒报警器
这个 Demo 的价值不在于开箱即用,而在于它是一个能跑的真骨架。拿到手之后最值得做的第一件事,不是改 UI 或换图标,而是把传感器原始数据落盘。具体做法是在 SensorDataCollector 里加一个可开关的数据录制功能,把每次回调的 x、y、z 和时间戳追加写入 CSV。有了真实数据,你后续调阈值、训练模型、验证算法才有依据,不然每次都是让同事一遍遍摔手机,效率低还不安全。
接下来是三个具体改造方向。第一个是动态阈值:现在 IMPACT_THRESHOLD 是固定 3g,但老人和年轻人的跌倒冲击差异很大。常见做法是用一个短窗口(比如 5 秒)估算用户的日常活动峰值,把这个值乘以 1.5 作为自适应冲击阈值,这样误报会明显减少。第二个是替换检测模型:把 FallDetector 的接口保持不动,内部实现从状态机换成 TensorFlow Lite 模型,输入是 100 个合加速度点加姿态角序列,输出是跌倒概率。我一般会先在 PC 上把 CSV 数据导入 Python 训练好模型,再导出 tflite 文件放进 assets 目录。第三个是报警方式扩展:在 AlertActivity 的预留位置接上 SmsManager 发送紧急短信,或者用 OkHttp 把位置信息 POST 到云端接口,这取决于你的应用场景。
验证方法也要讲究。不要盲目让测试者反复摔倒在硬地板上,一下午下来人受不了,数据质量也差。我的习惯是分两步:先用「手机固定在测试者腰部,从站立位置快速蹲坐到垫子上」来模拟,这个动作能产生接近真实跌倒的冲击和姿态变化;再用手持手机快速下挥到低处来验证误报率。每轮测试后立即查看 CSV 里的合加速度波形,确认冲击峰、失重谷、静止段三段都完整,再决定要不要调整阈值。从那以后我每次做传感器类项目,拿到手的第一个版本代码一律先补上数据录制,再谈调参和模型替换,这个习惯帮我少走了很多弯路。希望帮到你。
本文还有配套的精品资源,点击获取