ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Android跌倒检测Demo:三轴加速度与状态机报警链路

Android跌倒检测Demo:三轴加速度与状态机报警链路 简介面向 Android 开发与计算机视觉入门者的跌倒检测识别演示程序聚焦移动端实时摔倒识别这一常见场景可用于课程设计、毕业项目原型验证或算法落地体验兼顾初学者与有一定移动端开发经验的读者。zip 压缩包仅含 2 个文件以 1 个 APK 安装包和 1 个 JSON 辅助文件为主前者可直接安装体验跌倒检测流程后者用于记录构建输出等信息整体约 50.44MB体积轻量获取后即可上手。目前已有 1946 人学习下载说明该方向具有稳定的实践需求。对希望快速了解 Android 端跌倒检测效果的读者而言可通过实际运行演示观察识别流程与交互表现并以此为参照评估自身方案在移动端的可行性适合作为入门体验与原型参考。1. 跌倒检测 Demo 的第一印象它不是一个“摔倒就报警”的按钮给家里老人手机装一个 Android 跌倒检测 Demo最直观的验证场景是手机放在口袋里人快速下蹲再起身App 不能乱响模拟一次自由落体加撞击它要在几秒内弹窗或响铃。这个 Demo 解决的就是把三轴加速度、姿态角和状态机串成一条可运行的跌倒识别链路适合想快速验证传感器方案、准备做老人看护或户外作业监护的 Android 开发者。它不是一上来就堆摄像头模型而是先把摔倒识别最核心的采集、判定、报警跑通后面再扩展视频跌倒检测和识别也更容易。2. 跌倒识别工程骨架SensorManager 加前台服务的最小闭环2.1 目录结构一个能跑的最小闭环这个 Demo 的工程目标很克制把 Android 跌倒检测 从传感器注册到通知弹窗跑成一条闭环不追求一上来就做深度学习分类。常见目录会按职责拆成 sensor、detector、service、alarm、ui 五块先把数据采集和判定分开后面换算法不用动界面。路径职责改动频率sensor/AccelCollector.kt注册加速度计、统一采样率、把SensorEvent转成数组低detector/FallDetector.kt滤波、阈值、状态机、输出跌倒事件高service/FallMonitorService.kt前台服务保活、传感器生命周期、报警分发中alarm/AlarmBus.kt本地通知、震动、页面回调的解耦入口中ui/MainActivity.kt权限申请、开始/停止监测、倒计时取消中res/layout/activity_main.xml一个开始按钮、一个状态文本、一个日志区低真正影响跌倒识别效果的只有detector和service两个目录。很多初版 Demo 把传感器注册写在 Activity 里屏幕一关就收不到数据后面排查半天以为是算法问题其实是生命周期没管住。我一般会把采集放进前台服务Activity 只负责权限和开关这样锁屏、切后台都不容易翻车。2.2 技术栈拆解为什么用三轴加速度而不是一上来就上摄像头选型上三轴加速度计是跌倒识别最省电的入口。手机放在口袋或腰间自由落体、撞击、静止三个物理阶段都能在加速度曲线上看到。摄像头方案能看姿态但功耗、隐私、角度遮挡都是成本端侧姿态关键点模型适合做二次校验不适合作为唯一触发。这个 Demo 用SensorManager拿TYPE_ACCELEROMETER配合前台服务和本地通知先把“摔倒识别”最刚需的链路跑通。依赖配置不用堆太多库常见做法是只保留核心和界面基础库// app/build.gradle.kts dependencies { implementation(androidx.core:core-ktx) implementation(androidx.appcompat:appcompat) implementation(androidx.lifecycle:lifecycle-service) // 需要曲线调试时再加图表库不要在首版塞进来 }这段配置不写具体版本号是为了让工程跟着本机 Android Studio 的模板走。lifecycle-service主要给前台服务用方便在服务里拿生命周期如果 Demo 里没有用到删掉也不影响传感器采集。依赖同步失败时先看仓库网络和镜像配置不要先怀疑代码。清单文件里要提前声明前台服务和通知权限!-- AndroidManifest.xml -- uses-permission android:nameandroid.permission.FOREGROUND_SERVICE / uses-permission android:nameandroid.permission.FOREGROUND_SERVICE_HEALTH / uses-permission android:nameandroid.permission.POST_NOTIFICATIONS / uses-permission android:nameandroid.permission.WAKE_LOCK / service android:name.service.FallMonitorService android:foregroundServiceTypehealth android:exportedfalse /foregroundServiceTypehealth是新系统对健康类前台服务的类型声明不写会在启动时抛异常。POST_NOTIFICATIONS在较新系统上必须运行时申请否则报警通知只会在通知栏外面“路过”一下。WAKE_LOCK不是让你一直持有而是在冲击确认后的短窗口里保证 CPU 不睡死报警发完就释放。提示监控入口最好从可见界面触发不要靠开机广播直接拉起前台服务新系统对后台启动限制很严。2.3 检测链路采集、滤波、阈值、状态机、报警一条完整的跌倒检测和识别链路通常按 50Hz 采样先做低通滤波分离重力再算合加速度和姿态角。状态机比单阈值靠谱自由落体、冲击、静止确认、报警中间任何一步不满足就回空闲。常见参数是自由落体阈值 0.5 到 0.6g冲击阈值 2.5 到 3.0g静止窗口 2 到 3 秒姿态角变化 50 到 60 度。具体值跟手机佩戴位置有关别照抄。链路可以拆成五步采集SensorManager.registerListener注册加速度计采样率用SENSOR_DELAY_GAME大约 50Hz够看清冲击尖峰。滤波用一阶低通分离重力分量原始值减重力得到线性加速度合加速度仍然用原始三轴计算。判定先看合加速度是否掉到自由落体区间再看是否出现短时高冲击最后看冲击后是否持续静止且姿态角明显改变。确认状态机进入静止确认后等 2 到 3 秒期间线性加速度不能太大否则回空闲。报警本地通知加震动页面弹 10 秒倒计时用户点“我没事”就取消不点再走紧急联系人。功耗上50Hz 对加速度计压力不大真正耗电的是屏幕常亮和网络请求。Demo 阶段建议只在报警时发网络请求平时只写本地日志。误报抑制也别只靠调阈值加入“冲击后静止”和“姿态角改变”两个条件坐下、起身、把手机扔桌上这类动作会少很多。后面第 4 章会讲几个真机调试时最容易踩的坑。3. 跌倒检测和识别核心三轴加速度采集与状态机判定3.1 传感器采集与滑动窗口滤波先看判定类。它不直接持有SensorManager只接收三轴数组和时间戳这样单元测试和离线回放都能复用。下面这段是 Demo 里最核心的FallDetector把滤波、阈值、状态机放在一个类里方便新手顺着读。// detector/FallDetector.kt class FallDetector( private val onFall: (FallEvent) - Unit ) { enum class State { IDLE, FREEFALL, IMPACT, STILL, ALARM } private var state State.IDLE private val gravity FloatArray(3) private var baseline floatArrayOf(0f, 0f, 9.81f) private var freeFallAt 0L private var impactAt 0L private var stillAt 0L private var lastAt 0L fun reset() { state State.IDLE lastAt 0L } fun process(values: FloatArray, now: Long) { if (lastAt 0L) { lastAt now // 第一次采样把当前重力方向当作佩戴基线 for (i in 0..2) gravity[i] values[i] baseline gravity.copyOf() return } lastAt now // 一阶低通分离重力0.8 越大越稳越小越灵敏 for (i in 0..2) { gravity[i] 0.8f * gravity[i] 0.2f * values[i] } val rawG sqrt( values[0] * values[0] values[1] * values[1] values[2] * values[2] ) / 9.81f val linearMax sqrt( (values[0] - gravity[0]) * (values[0] - gravity[0]) (values[1] - gravity[1]) * (values[1] - gravity[1]) (values[2] - gravity[2]) * (values[2] - gravity[2]) ) / 9.81f val angle angleBetween(gravity, baseline) when (state) { State.IDLE - { if (rawG 0.55f) { state State.FREEFALL freeFallAt now } } State.FREEFALL - { if (rawG 2.6f now - freeFallAt 1200) { state State.IMPACT impactAt now } else if (now - freeFallAt 1500) { state State.IDLE } } State.IMPACT - { if (now - impactAt 1200) { state State.IDLE } else if (rawG in 0.85f..1.15f) { state State.STILL stillAt now } } State.STILL - { val still rawG in 0.8f..1.2f linearMax 0.25f if (!still) { state State.IDLE } else if (now - stillAt 2500 angle 55f) { state State.ALARM onFall( FallEvent( time now, peakG rawG, angle angle, reason 冲击后静止且姿态改变 ) ) } } State.ALARM - { // 等页面取消或上层调用 reset不在这里重复上报 } } } private fun angleBetween(a: FloatArray, b: FloatArray): Float { val dot a[0] * b[0] a[1] * b[1] a[2] * b[2] val ma sqrt(a[0] * a[0] a[1] * a[1] a[2] * a[2]) val mb sqrt(b[0] * b[0] b[1] * b[1] b[2] * b[2]) if (ma 0f || mb 0f) return 0f val cos (dot / (ma * mb)).coerceIn(-1f, 1f) return Math.toDegrees(acos(cos).toDouble()).toFloat() } } data class FallEvent( val time: Long, val peakG: Float, val angle: Float, val reason: String )这段代码的关键在三个量rawG是原始合加速度用来抓自由落体和冲击linearMax是去掉重力后的线性加速度用来判断冲击后是否真的静下来angle是当前重力方向与佩戴基线的夹角用来区分“手机还在手里晃”和“人已经躺下”。gravity的 0.8 系数是低通滤波的平滑度调大后重力估计更稳但姿态变化反应慢调小后反应快静止时容易抖。状态机不能省。单看一个 3g 的冲击就报警拍桌子、跑步落地都会中招单看自由落体也不够手机从手里滑到沙发上也会触发自由落体。FREEFALL到IMPACT限时 1.2 秒是防止下落信号和冲击信号隔太久IMPACT到STILL限时 1.2 秒是防止人已经站起来才进入静止判断STILL持续 2.5 秒且角度超过 55 度才上报是给误报加一道闸。参数可以按下面这张表调参数默认值作用调大后的影响调小后的影响自由落体阈值0.55g判断失重更难触发漏报增加更容易触发误报增加冲击阈值2.6g判断撞击漏掉轻摔日常动作容易中招静止窗口2500ms确认人不再动报警更慢可能人还没停就报警姿态角阈值55 度判断手机姿态改变漏掉平躺角度小的场景坐下也可能触发线性加速度上限0.25g判断是否静止更容易进入静止更难进入静止3.2 前台服务与传感器生命周期判定类只负责算真正让它在锁屏后继续跑的是前台服务。下面这段把传感器注册、线程切换、通知和事件分发串起来。HandlerThread用来避开主线程传感器回调本来就在独立线程但统一放到一个 worker 里更好排查。// service/FallMonitorService.kt class FallMonitorService : Service(), SensorEventListener { private lateinit var sensorManager: SensorManager private var accelerometer: Sensor? null private lateinit var detector: FallDetector private lateinit var worker: HandlerThread private lateinit var handler: Handler override fun onCreate() { super.onCreate() startForeground(1001, buildMonitorNotification()) worker HandlerThread(fall-sensor).also { it.start() } handler Handler(worker.looper) sensorManager getSystemService(SENSOR_SERVICE) as SensorManager accelerometer sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) detector FallDetector { event - publishAlarm(event) } } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val sensor accelerometer if (sensor null) { // 设备没有加速度计直接停掉别让用户以为在监控 stopSelf() return START_NOT_STICKY } sensorManager.registerListener( this, sensor, SensorManager.SENSOR_DELAY_GAME, handler ) return START_STICKY } override fun onSensorChanged(event: SensorEvent) { // elapsedRealtime 不受用户改时间影响状态机时间戳用它 detector.process(event.values, SystemClock.elapsedRealtime()) } override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) Unit private fun publishAlarm(event: FallEvent) { val manager getSystemService(NOTIFICATION_SERVICE) as NotificationManager manager.notify(1002, buildAlarmNotification(event)) AlarmBus.emit(event) } override fun onDestroy() { sensorManager.unregisterListener(this) worker.quitSafely() super.onDestroy() } }SENSOR_DELAY_GAME是采样率参数大约 50Hz比SENSOR_DELAY_UI快又不像SENSOR_DELAY_FASTEST那样耗电。SystemClock.elapsedRealtime()比System.currentTimeMillis()稳用户改系统时间不影响状态机计时。START_STICKY让服务被系统回收后有机会重建但别把它当保活神器前台通知和电池优化白名单才是正路。publishAlarm里先发本地通知再走AlarmBus给页面。通知和页面回调分开是为了后面接短信、电话、网络上报时不用改判定类。通知渠道要在buildMonitorNotification和buildAlarmNotification里提前建好渠道被用户关掉后代码里再notify也不会显示。3.3 报警确认与取消倒计时跌倒检测最怕“报了但用户没法取消”。Demo 里给一个 10 秒倒计时用户点“我没事”就取消点不到再走紧急逻辑。这个交互比单纯弹通知更接近真实产品。// ui/MainActivity.kt 中的报警确认 private fun showFallCountdown(event: FallEvent) { val dialog AlertDialog.Builder(this) .setTitle(检测到跌倒) .setMessage(${event.reason}10 秒后通知紧急联系人) .setNegativeButton(我没事) { d, _ - d.dismiss() AlarmBus.cancel(event) } .create() dialog.show() handler.postDelayed({ if (dialog.isShowing) { dialog.dismiss() sendEmergency(event) // 这里再接短信、电话或自己的上报接口 } }, 10_000) }倒计时用Handler.postDelayed就够了不需要额外引调度库。AlarmBus.cancel(event)里把当前事件标记为已取消服务端和页面都不要再重复弹窗。sendEmergency是二次开发接出去的口子Demo 里可以先写日志后面换成自己的接口。注意倒计时期间如果 Activity 被销毁handler要跟着清理否则容易泄漏。4. 真机跑通与避坑摔倒识别 Demo 的 5 个高频翻车点4.1 运行步骤从导入工程到模拟一次跌倒拿到压缩包后先用 Android Studio 打开工程根目录等依赖同步完。不要直接在模拟器上点开始加速度计在模拟器里通常是固定值状态机永远进不了自由落体。真机开启开发者模式和 USB 调试连上后跑app模块。首次启动按这个顺序走授予通知权限没有通知权限时先别点开始监测。点“开始监测”确认通知栏出现常驻通知。把手机放口袋里走动 30 秒看日志里rawG是否在 0.8 到 1.2 之间波动。模拟跌倒手持手机快速下坠到软垫别真摔手机观察状态是否从IDLE走到IMPACT。锁屏 1 分钟再重复一次确认前台服务还在采数据。模拟跌倒时自由落体阶段要短暂冲击要明显。用手把手机从 30 厘米高度松到沙发垫上通常能打出 2g 以上冲击如果日志里rawG峰值只有 1.5g先把冲击阈值降到 2.2g 试一次再决定是否改硬件佩戴方式。4.2 常见问题排查现象、原因、解决现象一模拟器上日志一直不动状态机永远空闲。原因模拟器没有真实加速度计SensorEvent返回固定数组。 解决换真机必须在模拟器上演示时用传感器注入工具手动喂三轴数据但别拿注入结果判断算法好坏。现象二屏幕一锁跌倒不报警。原因传感器注册在 Activity界面不可见后回调被停或者服务被系统限制。 解决把注册放进前台服务通知类型声明正确测试机把 App 加入电池优化白名单。锁屏后看onSensorChanged是否还有日志没有日志就是生命周期问题不是阈值问题。现象三坐着起身、把手机放桌上也报警。原因只看了冲击阈值没有静止确认和姿态角或者冲击阈值太低。 解决把冲击阈值从 2.2g 提到 2.6g 以上静止窗口加到 2500ms姿态角阈值加到 55 度。先录一段正常动作的 CSV回放看误报点落在哪个状态别盲调。现象四报警时通知栏没有弹窗。原因没有申请POST_NOTIFICATIONS或者通知渠道被用户关掉或者渠道重要性太低。 解决启动时运行时申请权限在设置页引导用户检查通知渠道报警通知用高重要性渠道并加震动。代码里notify不报错不代表用户能看到。现象五registerListener返回 false或者服务启动就崩。原因设备没有加速度计采样率不支持或者前台服务类型没声明。 解决先判断getDefaultSensor是否为 null为空就提示设备不支持前台服务类型按健康类声明把registerListener的返回值打日志失败时降一档采样率再试。4.3 用日志和曲线验证阈值调阈值最忌“凭感觉”。我一般会在onSensorChanged里把时间戳、三轴、rawG、状态写进 CSV跑完导出来看曲线。正常走路是 0.8 到 1.2g 的小波动自由落体有一段明显低于 0.6g冲击是一个尖峰静止后回到 1g 附近。把这三个阶段在曲线上标出来阈值就有依据。# 真机调试时抓日志过滤自己的 tag adb logcat -s FallDetector:V FallService:V-s后面跟的是日志 tag代码里用Log.d(FallDetector, ...)打状态迁移用Log.d(FallService, ...)打传感器注册结果。日志里重点看状态顺序是不是IDLE - FREEFALL - IMPACT - STILL - ALARM如果经常FREEFALL直接回IDLE说明自由落体窗口太长或冲击没达到阈值。CSV 回放比真机反复摔手机靠谱也更容易对比两套参数。5. 二次开发把误报压下去并把报警接出去第一版跑通后最值得改的不是界面而是误报和报警出口。单阈值状态机在实验室里够用放到真实场景里会遇到跑步、下楼梯、坐电梯。我一般会先加特征不急着换模型滑动窗口里算均值、方差、峰值、过零率、姿态角变化再用一个轻量分类器做二次确认。特征和状态机不冲突状态机负责触发候选分类器负责压误报。改造点做法验证指标多特征分类2 秒窗口提取均值、方差、峰值、过零率、姿态角召回率、每天误报次数场景判别用屏幕状态、位置传感器区分口袋、手持、桌面误报下降比例报警外发定义FallAlarmSink接口本地通知和远端上报解耦断网重试、重复上报是否幂等离线回放CSV 录制与回放跑同一套判定类混淆矩阵、状态迁移次数接报警出口时别把网络请求写进FallDetector。判定类只吐FallEvent服务层决定发通知还是调接口。下面这个接口就够用interface FallAlarmSink { fun onFall(event: FallEvent) fun onCancel(event: FallEvent) }onFall里做本地通知、震动、上报onCancel里把已经排队的紧急动作撤掉。网络上报要加幂等键避免重试导致重复通知。验证时先录 20 组正常动作和 10 组模拟跌倒离线回放跑参数再上真机。阈值不是玄学记录曲线后就是调参问题。从那以后我每次改阈值都先走一遍 CSV 回放再拿真机试不直接拿手机反复摔。希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表