機報警鏈路)
簡介面向 Android 開發(fā)與計算機視覺入門者的跌倒檢測識別演示程序聚焦移動端實時摔倒識別這一常見場景可用于課程設計、畢業(yè)項目原型驗證或算法落地體驗兼顧初學者與有一定移動端開發(fā)經(jīng)驗的讀者。zip 壓縮包僅含 2 個文件以 1 個 APK 安裝包和 1 個 JSON 輔助文件為主前者可直接安裝體驗跌倒檢測流程后者用于記錄構建輸出等信息整體約 50.44MB體積輕量獲取后即可上手。目前已有 1946 人學習下載說明該方向具有穩(wěn)定的實踐需求。對希望快速了解 Android 端跌倒檢測效果的讀者而言可通過實際運行演示觀察識別流程與交互表現(xiàn)并以此為參照評估自身方案在移動端的可行性適合作為入門體驗與原型參考。1. 跌倒檢測 Demo 的第一印象它不是一個“摔倒就報警”的按鈕給家里老人手機裝一個 Android 跌倒檢測 Demo最直觀的驗證場景是手機放在口袋里人快速下蹲再起身App 不能亂響模擬一次自由落體加撞擊它要在幾秒內彈窗或響鈴。這個 Demo 解決的就是把三軸加速度、姿態(tài)角和狀態(tài)機串成一條可運行的跌倒識別鏈路適合想快速驗證傳感器方案、準備做老人看護或戶外作業(yè)監(jiān)護的 Android 開發(fā)者。它不是一上來就堆攝像頭模型而是先把摔倒識別最核心的采集、判定、報警跑通后面再擴展視頻跌倒檢測和識別也更容易。2. 跌倒識別工程骨架SensorManager 加前臺服務的最小閉環(huán)2.1 目錄結構一個能跑的最小閉環(huán)這個 Demo 的工程目標很克制把 Android 跌倒檢測 從傳感器注冊到通知彈窗跑成一條閉環(huán)不追求一上來就做深度學習分類。常見目錄會按職責拆成 sensor、detector、service、alarm、ui 五塊先把數(shù)據(jù)采集和判定分開后面換算法不用動界面。路徑職責改動頻率sensor/AccelCollector.kt注冊加速度計、統(tǒng)一采樣率、把SensorEvent轉成數(shù)組低detector/FallDetector.kt濾波、閾值、狀態(tài)機、輸出跌倒事件高service/FallMonitorService.kt前臺服務保活、傳感器生命周期、報警分發(fā)中alarm/AlarmBus.kt本地通知、震動、頁面回調的解耦入口中ui/MainActivity.kt權限申請、開始/停止監(jiān)測、倒計時取消中res/layout/activity_main.xml一個開始按鈕、一個狀態(tài)文本、一個日志區(qū)低真正影響跌倒識別效果的只有detector和service兩個目錄。很多初版 Demo 把傳感器注冊寫在 Activity 里屏幕一關就收不到數(shù)據(jù)后面排查半天以為是算法問題其實是生命周期沒管住。我一般會把采集放進前臺服務Activity 只負責權限和開關這樣鎖屏、切后臺都不容易翻車。2.2 技術棧拆解為什么用三軸加速度而不是一上來就上攝像頭選型上三軸加速度計是跌倒識別最省電的入口。手機放在口袋或腰間自由落體、撞擊、靜止三個物理階段都能在加速度曲線上看到。攝像頭方案能看姿態(tài)但功耗、隱私、角度遮擋都是成本端側姿態(tài)關鍵點模型適合做二次校驗不適合作為唯一觸發(fā)。這個 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 里沒有用到刪掉也不影響傳感器采集。依賴同步失敗時先看倉庫網(wǎng)絡和鏡像配置不要先懷疑代碼。清單文件里要提前聲明前臺服務和通知權限!-- 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是新系統(tǒng)對健康類前臺服務的類型聲明不寫會在啟動時拋異常。POST_NOTIFICATIONS在較新系統(tǒng)上必須運行時申請否則報警通知只會在通知欄外面“路過”一下。WAKE_LOCK不是讓你一直持有而是在沖擊確認后的短窗口里保證 CPU 不睡死報警發(fā)完就釋放。提示監(jiān)控入口最好從可見界面觸發(fā)不要靠開機廣播直接拉起前臺服務新系統(tǒng)對后臺啟動限制很嚴。2.3 檢測鏈路采集、濾波、閾值、狀態(tài)機、報警一條完整的跌倒檢測和識別鏈路通常按 50Hz 采樣先做低通濾波分離重力再算合加速度和姿態(tài)角。狀態(tài)機比單閾值靠譜自由落體、沖擊、靜止確認、報警中間任何一步不滿足就回空閑。常見參數(shù)是自由落體閾值 0.5 到 0.6g沖擊閾值 2.5 到 3.0g靜止窗口 2 到 3 秒姿態(tài)角變化 50 到 60 度。具體值跟手機佩戴位置有關別照抄。鏈路可以拆成五步采集SensorManager.registerListener注冊加速度計采樣率用SENSOR_DELAY_GAME大約 50Hz夠看清沖擊尖峰。濾波用一階低通分離重力分量原始值減重力得到線性加速度合加速度仍然用原始三軸計算。判定先看合加速度是否掉到自由落體區(qū)間再看是否出現(xiàn)短時高沖擊最后看沖擊后是否持續(xù)靜止且姿態(tài)角明顯改變。確認狀態(tài)機進入靜止確認后等 2 到 3 秒期間線性加速度不能太大否則回空閑。報警本地通知加震動頁面彈 10 秒倒計時用戶點“我沒事”就取消不點再走緊急聯(lián)系人。功耗上50Hz 對加速度計壓力不大真正耗電的是屏幕常亮和網(wǎng)絡請求。Demo 階段建議只在報警時發(fā)網(wǎng)絡請求平時只寫本地日志。誤報抑制也別只靠調閾值加入“沖擊后靜止”和“姿態(tài)角改變”兩個條件坐下、起身、把手機扔桌上這類動作會少很多。后面第 4 章會講幾個真機調試時最容易踩的坑。3. 跌倒檢測和識別核心三軸加速度采集與狀態(tài)機判定3.1 傳感器采集與滑動窗口濾波先看判定類。它不直接持有SensorManager只接收三軸數(shù)組和時間戳這樣單元測試和離線回放都能復用。下面這段是 Demo 里最核心的FallDetector把濾波、閾值、狀態(tài)機放在一個類里方便新手順著讀。// 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 越大越穩(wěn)越小越靈敏 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 沖擊后靜止且姿態(tài)改變 ) ) } } 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是當前重力方向與佩戴基線的夾角用來區(qū)分“手機還在手里晃”和“人已經(jīng)躺下”。gravity的 0.8 系數(shù)是低通濾波的平滑度調大后重力估計更穩(wěn)但姿態(tài)變化反應慢調小后反應快靜止時容易抖。狀態(tài)機不能省。單看一個 3g 的沖擊就報警拍桌子、跑步落地都會中招單看自由落體也不夠手機從手里滑到沙發(fā)上也會觸發(fā)自由落體。FREEFALL到IMPACT限時 1.2 秒是防止下落信號和沖擊信號隔太久IMPACT到STILL限時 1.2 秒是防止人已經(jīng)站起來才進入靜止判斷STILL持續(xù) 2.5 秒且角度超過 55 度才上報是給誤報加一道閘。參數(shù)可以按下面這張表調參數(shù)默認值作用調大后的影響調小后的影響自由落體閾值0.55g判斷失重更難觸發(fā)漏報增加更容易觸發(fā)誤報增加沖擊閾值2.6g判斷撞擊漏掉輕摔日常動作容易中招靜止窗口2500ms確認人不再動報警更慢可能人還沒停就報警姿態(tài)角閾值55 度判斷手機姿態(tài)改變漏掉平躺角度小的場景坐下也可能觸發(fā)線性加速度上限0.25g判斷是否靜止更容易進入靜止更難進入靜止3.2 前臺服務與傳感器生命周期判定類只負責算真正讓它在鎖屏后繼續(xù)跑的是前臺服務。下面這段把傳感器注冊、線程切換、通知和事件分發(fā)串起來。HandlerThread用來避開主線程傳感器回調本來就在獨立線程但統(tǒng)一放到一個 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) { // 設備沒有加速度計直接停掉別讓用戶以為在監(jiān)控 stopSelf() return START_NOT_STICKY } sensorManager.registerListener( this, sensor, SensorManager.SENSOR_DELAY_GAME, handler ) return START_STICKY } override fun onSensorChanged(event: SensorEvent) { // elapsedRealtime 不受用戶改時間影響狀態(tài)機時間戳用它 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是采樣率參數(shù)大約 50Hz比SENSOR_DELAY_UI快又不像SENSOR_DELAY_FASTEST那樣耗電。SystemClock.elapsedRealtime()比System.currentTimeMillis()穩(wěn)用戶改系統(tǒng)時間不影響狀態(tài)機計時。START_STICKY讓服務被系統(tǒng)回收后有機會重建但別把它當?;钌衿髑芭_通知和電池優(yōu)化白名單才是正路。publishAlarm里先發(fā)本地通知再走AlarmBus給頁面。通知和頁面回調分開是為了后面接短信、電話、網(wǎng)絡上報時不用改判定類。通知渠道要在buildMonitorNotification和buildAlarmNotification里提前建好渠道被用戶關掉后代碼里再notify也不會顯示。3.3 報警確認與取消倒計時跌倒檢測最怕“報了但用戶沒法取消”。Demo 里給一個 10 秒倒計時用戶點“我沒事”就取消點不到再走緊急邏輯。這個交互比單純彈通知更接近真實產(chǎn)品。// ui/MainActivity.kt 中的報警確認 private fun showFallCountdown(event: FallEvent) { val dialog AlertDialog.Builder(this) .setTitle(檢測到跌倒) .setMessage(${event.reason}10 秒后通知緊急聯(lián)系人) .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是二次開發(fā)接出去的口子Demo 里可以先寫日志后面換成自己的接口。注意倒計時期間如果 Activity 被銷毀handler要跟著清理否則容易泄漏。4. 真機跑通與避坑摔倒識別 Demo 的 5 個高頻翻車點4.1 運行步驟從導入工程到模擬一次跌倒拿到壓縮包后先用 Android Studio 打開工程根目錄等依賴同步完。不要直接在模擬器上點開始加速度計在模擬器里通常是固定值狀態(tài)機永遠進不了自由落體。真機開啟開發(fā)者模式和 USB 調試連上后跑app模塊。首次啟動按這個順序走授予通知權限沒有通知權限時先別點開始監(jiān)測。點“開始監(jiān)測”確認通知欄出現(xiàn)常駐通知。把手機放口袋里走動 30 秒看日志里rawG是否在 0.8 到 1.2 之間波動。模擬跌倒手持手機快速下墜到軟墊別真摔手機觀察狀態(tài)是否從IDLE走到IMPACT。鎖屏 1 分鐘再重復一次確認前臺服務還在采數(shù)據(jù)。模擬跌倒時自由落體階段要短暫沖擊要明顯。用手把手機從 30 厘米高度松到沙發(fā)墊上通常能打出 2g 以上沖擊如果日志里rawG峰值只有 1.5g先把沖擊閾值降到 2.2g 試一次再決定是否改硬件佩戴方式。4.2 常見問題排查現(xiàn)象、原因、解決現(xiàn)象一模擬器上日志一直不動狀態(tài)機永遠空閑。原因模擬器沒有真實加速度計SensorEvent返回固定數(shù)組。 解決換真機必須在模擬器上演示時用傳感器注入工具手動喂三軸數(shù)據(jù)但別拿注入結果判斷算法好壞?,F(xiàn)象二屏幕一鎖跌倒不報警。原因傳感器注冊在 Activity界面不可見后回調被?;蛘叻毡幌到y(tǒng)限制。 解決把注冊放進前臺服務通知類型聲明正確測試機把 App 加入電池優(yōu)化白名單。鎖屏后看onSensorChanged是否還有日志沒有日志就是生命周期問題不是閾值問題?,F(xiàn)象三坐著起身、把手機放桌上也報警。原因只看了沖擊閾值沒有靜止確認和姿態(tài)角或者沖擊閾值太低。 解決把沖擊閾值從 2.2g 提到 2.6g 以上靜止窗口加到 2500ms姿態(tài)角閾值加到 55 度。先錄一段正常動作的 CSV回放看誤報點落在哪個狀態(tài)別盲調?,F(xiàn)象四報警時通知欄沒有彈窗。原因沒有申請POST_NOTIFICATIONS或者通知渠道被用戶關掉或者渠道重要性太低。 解決啟動時運行時申請權限在設置頁引導用戶檢查通知渠道報警通知用高重要性渠道并加震動。代碼里notify不報錯不代表用戶能看到?,F(xiàn)象五registerListener返回 false或者服務啟動就崩。原因設備沒有加速度計采樣率不支持或者前臺服務類型沒聲明。 解決先判斷getDefaultSensor是否為 null為空就提示設備不支持前臺服務類型按健康類聲明把registerListener的返回值打日志失敗時降一檔采樣率再試。4.3 用日志和曲線驗證閾值調閾值最忌“憑感覺”。我一般會在onSensorChanged里把時間戳、三軸、rawG、狀態(tài)寫進 CSV跑完導出來看曲線。正常走路是 0.8 到 1.2g 的小波動自由落體有一段明顯低于 0.6g沖擊是一個尖峰靜止后回到 1g 附近。把這三個階段在曲線上標出來閾值就有依據(jù)。# 真機調試時抓日志過濾自己的 tag adb logcat -s FallDetector:V FallService:V-s后面跟的是日志 tag代碼里用Log.d(FallDetector, ...)打狀態(tài)遷移用Log.d(FallService, ...)打傳感器注冊結果。日志里重點看狀態(tài)順序是不是IDLE - FREEFALL - IMPACT - STILL - ALARM如果經(jīng)常FREEFALL直接回IDLE說明自由落體窗口太長或沖擊沒達到閾值。CSV 回放比真機反復摔手機靠譜也更容易對比兩套參數(shù)。5. 二次開發(fā)把誤報壓下去并把報警接出去第一版跑通后最值得改的不是界面而是誤報和報警出口。單閾值狀態(tài)機在實驗室里夠用放到真實場景里會遇到跑步、下樓梯、坐電梯。我一般會先加特征不急著換模型滑動窗口里算均值、方差、峰值、過零率、姿態(tài)角變化再用一個輕量分類器做二次確認。特征和狀態(tài)機不沖突狀態(tài)機負責觸發(fā)候選分類器負責壓誤報。改造點做法驗證指標多特征分類2 秒窗口提取均值、方差、峰值、過零率、姿態(tài)角召回率、每天誤報次數(shù)場景判別用屏幕狀態(tài)、位置傳感器區(qū)分口袋、手持、桌面誤報下降比例報警外發(fā)定義FallAlarmSink接口本地通知和遠端上報解耦斷網(wǎng)重試、重復上報是否冪等離線回放CSV 錄制與回放跑同一套判定類混淆矩陣、狀態(tài)遷移次數(shù)接報警出口時別把網(wǎng)絡請求寫進FallDetector。判定類只吐FallEvent服務層決定發(fā)通知還是調接口。下面這個接口就夠用interface FallAlarmSink { fun onFall(event: FallEvent) fun onCancel(event: FallEvent) }onFall里做本地通知、震動、上報onCancel里把已經(jīng)排隊的緊急動作撤掉。網(wǎng)絡上報要加冪等鍵避免重試導致重復通知。驗證時先錄 20 組正常動作和 10 組模擬跌倒離線回放跑參數(shù)再上真機。閾值不是玄學記錄曲線后就是調參問題。從那以后我每次改閾值都先走一遍 CSV 回放再拿真機試不直接拿手機反復摔。希望幫到你。本文還有配套的精品資源點擊獲取