refactor: 加速度计改为低通滤波+状态机方案(v4)
之前的最小值+连续高值方案在原始值剧烈跳动(-2~18)下无法同时 满足灵敏度和防误触发,阈值怎么调都有问题。 v4方案: - 低通滤波(α=0.8)去除运动尖峰,只保留重力方向 - 三态状态机:UNKNOWN→DOWN(Z<4)→TRIGGERED(Z>7,亮屏)→DOWN - 小幅摆动:滤波值收敛到振荡均值~6,不超过7,不误触发 - 快速抬手:~0.8秒触发;缓慢抬手:~2秒触发 - 代码从复杂的窗口/最小值/连续计数简化为滤波+状态机 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,18 +16,16 @@ import javax.inject.Singleton
|
||||
*
|
||||
* 双模式策略:
|
||||
* - 方案D:TYPE_WRIST_TILT_GESTURE(type=26),系统级手势识别,最省电最准确
|
||||
* - 方案C:TYPE_ACCELEROMETER Z轴变化趋势检测,作为降级方案
|
||||
* - 方案C:TYPE_ACCELEROMETER + 低通滤波 + 状态机,作为降级方案
|
||||
*
|
||||
* 方案C核心逻辑(v3 最小值跳变检测):
|
||||
* 追踪近期5个采样(≈1秒)的最小Z值。当近期有低值(<5,手臂曾下垂)
|
||||
* 且当前Z值高(≥7,手臂已抬起)时判定为抬手。
|
||||
* 只需1个低值采样即可触发,比均值方案灵敏得多。
|
||||
* 触发后2秒冷却期防连续触发。
|
||||
* 方案C核心逻辑(v4 低通滤波):
|
||||
* 对原始Z轴数据做低通滤波,去除运动抖动/尖峰,只保留重力方向(手臂朝向)。
|
||||
* 用状态机跟踪手臂姿态:DOWN(下垂)→ 滤波值超过阈值 → 亮屏。
|
||||
*
|
||||
* 与旧版和v1/v2方案的改进:
|
||||
* - 旧版Z≥5:平放桌面常亮、走路闪烁
|
||||
* - v1/v2均值对比:需要连续3个低值,抬手动作太快时漏触发
|
||||
* - v3最小值:只需1个低值,更符合真实抬手动作的数据特征
|
||||
* 低通滤波优势:
|
||||
* - 原始值跳动 2→12→3→10 → 滤波后平滑 5.2→6.2→5.7→6.3
|
||||
* - 手臂稳定抬起 → filteredZ 缓慢上升到 ~8-9
|
||||
* - 小幅摆动 → filteredZ 收敛到振荡均值 ~5-6,不超过触发阈值
|
||||
*/
|
||||
@Singleton
|
||||
class FiseAccelerometerWake @Inject constructor(
|
||||
@@ -36,16 +34,12 @@ class FiseAccelerometerWake @Inject constructor(
|
||||
) : AccelerometerWakeController, SensorEventListener {
|
||||
|
||||
companion object {
|
||||
/** 方案C:近期最小值窗口大小(5个采样≈1秒,用于追踪"手臂曾经放下过") */
|
||||
private const val MIN_WINDOW_SIZE = 5
|
||||
/** 方案C:最小值阈值,近期有采样低于此值才认为"手臂曾明确下垂"(实测:小幅摆动Z≈5-6,下垂Z≈1-3) */
|
||||
private const val Z_MIN_THRESHOLD = 3f
|
||||
/** 方案C:当前值阈值,超过此值认为"手臂已明确抬起"(实测:抬手稳定Z≈7.5-9,偶尔波动到7.5) */
|
||||
private const val Z_CURRENT_THRESHOLD = 7.5f
|
||||
/** 方案C:连续高值计数要求(需连续N个采样≥阈值才触发,防摆动尖峰误触发) */
|
||||
private const val HIGH_COUNT_REQUIRED = 3
|
||||
/** 方案C:触发后冷却采样数(防连续触发,10个≈2秒) */
|
||||
private const val COOLDOWN_SAMPLES = 10
|
||||
/** 低通滤波系数(0~1,越大越平滑。0.8在SENSOR_DELAY_NORMAL下约1.5秒达到稳定值) */
|
||||
private const val ALPHA = 0.8f
|
||||
/** 下垂阈值:滤波后Z低于此值认为手臂已下垂,进入DOWN状态 */
|
||||
private const val Z_DOWN = 4f
|
||||
/** 抬起阈值:滤波后Z高于此值认为手臂已抬起,触发亮屏 */
|
||||
private const val Z_UP = 7f
|
||||
}
|
||||
|
||||
private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
||||
@@ -57,17 +51,21 @@ class FiseAccelerometerWake @Inject constructor(
|
||||
/** 是否已启动 */
|
||||
private var started = false
|
||||
|
||||
// === 方案C:最小值追踪 ===
|
||||
/** 环形缓冲区,存储最近N个Z轴采样值,用于找最小值 */
|
||||
private val zWindow = FloatArray(MIN_WINDOW_SIZE) { Float.MAX_VALUE }
|
||||
/** 当前写入位置 */
|
||||
private var windowIndex = 0
|
||||
/** 窗口是否已填满 */
|
||||
private var windowFilled = false
|
||||
/** 触发后冷却计数器 */
|
||||
private var cooldownCount = 0
|
||||
/** 连续高值计数器(Z >= 阈值的连续采样数) */
|
||||
private var highCount = 0
|
||||
// === 方案C:低通滤波 + 状态机 ===
|
||||
|
||||
/** 低通滤波后的Z值 */
|
||||
private var filteredZ = 0f
|
||||
/** 是否已初始化滤波值(第一个采样直接赋值,不做滤波) */
|
||||
private var filterInitialized = false
|
||||
|
||||
/**
|
||||
* 手臂状态机
|
||||
* - UNKNOWN: 初始状态,等待首次进入DOWN
|
||||
* - DOWN: 手臂下垂(filteredZ < Z_DOWN),等待抬手
|
||||
* - TRIGGERED: 已触发亮屏,等待手臂放下后重新进入DOWN
|
||||
*/
|
||||
private enum class ArmState { UNKNOWN, DOWN, TRIGGERED }
|
||||
private var armState = ArmState.UNKNOWN
|
||||
|
||||
/**
|
||||
* 开始监听传感器
|
||||
@@ -86,13 +84,13 @@ class FiseAccelerometerWake @Inject constructor(
|
||||
return
|
||||
}
|
||||
|
||||
// 方案C:降级使用加速度计Z轴变化趋势检测
|
||||
// 方案C:降级使用加速度计 + 低通滤波
|
||||
val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
|
||||
if (accelerometer != null) {
|
||||
sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_NORMAL)
|
||||
useWristTilt = false
|
||||
started = true
|
||||
Timber.i("抬手亮屏: 使用方案C(加速度计Z轴变化趋势),WRIST_TILT不可用")
|
||||
Timber.i("抬手亮屏: 使用方案C(加速度计+低通滤波),WRIST_TILT不可用")
|
||||
} else {
|
||||
Timber.w("抬手亮屏: 无可用传感器,功能不可用")
|
||||
}
|
||||
@@ -103,7 +101,7 @@ class FiseAccelerometerWake @Inject constructor(
|
||||
if (!started) return
|
||||
sensorManager.unregisterListener(this)
|
||||
started = false
|
||||
resetWindow()
|
||||
resetState()
|
||||
Timber.d("抬手亮屏: 已停止")
|
||||
}
|
||||
|
||||
@@ -116,7 +114,7 @@ class FiseAccelerometerWake @Inject constructor(
|
||||
/** 恢复检测 */
|
||||
override fun resume() {
|
||||
paused = false
|
||||
resetWindow() // 恢复时清空窗口,避免暂停期间积累的数据干扰判断
|
||||
resetState() // 恢复时重置,避免暂停期间积累的数据干扰
|
||||
Timber.d("抬手亮屏: 已恢复")
|
||||
}
|
||||
|
||||
@@ -131,7 +129,7 @@ class FiseAccelerometerWake @Inject constructor(
|
||||
return
|
||||
}
|
||||
|
||||
// 方案C:Z轴变化趋势检测
|
||||
// 方案C:低通滤波 + 状态机
|
||||
detectWristRaise(event.values[2])
|
||||
}
|
||||
|
||||
@@ -142,57 +140,51 @@ class FiseAccelerometerWake @Inject constructor(
|
||||
// === 方案C 核心逻辑 ===
|
||||
|
||||
/**
|
||||
* 最小值 + 连续高值检测
|
||||
* 低通滤波 + 状态机检测抬手
|
||||
*
|
||||
* 两个条件同时满足才触发:
|
||||
* 1. 近期5个采样中有值 < 3(手臂曾明确下垂)
|
||||
* 2. 当前连续 3 个采样都 ≥ 8(手臂已抬起并稳定,非瞬间尖峰)
|
||||
*
|
||||
* 连续高值要求可区分:
|
||||
* - 真正抬手:Z 稳定在 8-9 持续多个采样 → 连续 3 个 ≥ 8 ✅
|
||||
* - 小幅摆动:Z 在 0-10 间剧烈跳动,尖峰后立即回落 → 连续不到 3 个 ❌
|
||||
* 1. 对原始Z值做低通滤波,去除运动尖峰,保留重力方向
|
||||
* 2. 状态机:
|
||||
* UNKNOWN ──[filteredZ < Z_DOWN]──► DOWN ──[filteredZ > Z_UP]──► TRIGGERED(亮屏)
|
||||
* ▲ │
|
||||
* └──[filteredZ < Z_DOWN]─────────┘
|
||||
*/
|
||||
private fun detectWristRaise(z: Float) {
|
||||
// 冷却期:刚触发过,等待冷却结束
|
||||
if (cooldownCount > 0) {
|
||||
cooldownCount--
|
||||
return
|
||||
}
|
||||
|
||||
// 写入环形缓冲区(追踪近期最小值)
|
||||
val pos = windowIndex % MIN_WINDOW_SIZE
|
||||
zWindow[pos] = z
|
||||
windowIndex++
|
||||
|
||||
if (!windowFilled && windowIndex >= MIN_WINDOW_SIZE) {
|
||||
windowFilled = true
|
||||
}
|
||||
if (!windowFilled) return
|
||||
|
||||
// 更新连续高值计数
|
||||
if (z >= Z_CURRENT_THRESHOLD) {
|
||||
highCount++
|
||||
private fun detectWristRaise(rawZ: Float) {
|
||||
// 低通滤波:去除高频抖动,保留手臂朝向
|
||||
if (!filterInitialized) {
|
||||
filteredZ = rawZ
|
||||
filterInitialized = true
|
||||
} else {
|
||||
highCount = 0
|
||||
filteredZ = ALPHA * filteredZ + (1 - ALPHA) * rawZ
|
||||
}
|
||||
|
||||
// 找近期最小值
|
||||
var recentMin = Float.MAX_VALUE
|
||||
for (i in 0 until MIN_WINDOW_SIZE) {
|
||||
if (zWindow[i] < recentMin) recentMin = zWindow[i]
|
||||
}
|
||||
// DEBUG: 输出滤波值和状态(正式发布时删除)
|
||||
Timber.v("抬手亮屏: raw=%.1f filtered=%.1f state=%s (阈值: DOWN<%.0f UP>%.0f)",
|
||||
rawZ, filteredZ, armState.name, Z_DOWN, Z_UP)
|
||||
|
||||
// DEBUG: 输出每次计算结果,用于调参(正式发布时删除)
|
||||
Timber.v("抬手亮屏: Z=%.1f recentMin=%.1f highCount=%d (阈值: min<%.0f AND %d连续>=%.0f)",
|
||||
z, recentMin, highCount, Z_MIN_THRESHOLD, HIGH_COUNT_REQUIRED, Z_CURRENT_THRESHOLD)
|
||||
|
||||
// 判断:近期有低值(手臂曾下垂)且连续高值达标(手臂已稳定抬起)
|
||||
if (recentMin < Z_MIN_THRESHOLD && highCount >= HIGH_COUNT_REQUIRED) {
|
||||
wakeScreenIfOff()
|
||||
// 触发后进入冷却期 + 清空状态
|
||||
cooldownCount = COOLDOWN_SAMPLES
|
||||
highCount = 0
|
||||
resetWindow()
|
||||
// 状态机转换
|
||||
when (armState) {
|
||||
ArmState.UNKNOWN -> {
|
||||
// 初始状态:等待手臂明确下垂
|
||||
if (filteredZ < Z_DOWN) {
|
||||
armState = ArmState.DOWN
|
||||
Timber.d("抬手亮屏: 状态 UNKNOWN→DOWN (filteredZ=%.1f)", filteredZ)
|
||||
}
|
||||
}
|
||||
ArmState.DOWN -> {
|
||||
// 手臂下垂状态:检测是否抬起
|
||||
if (filteredZ > Z_UP) {
|
||||
wakeScreenIfOff()
|
||||
armState = ArmState.TRIGGERED
|
||||
Timber.d("抬手亮屏: 状态 DOWN→TRIGGERED (filteredZ=%.1f)", filteredZ)
|
||||
}
|
||||
}
|
||||
ArmState.TRIGGERED -> {
|
||||
// 已触发状态:等待手臂重新放下
|
||||
if (filteredZ < Z_DOWN) {
|
||||
armState = ArmState.DOWN
|
||||
Timber.d("抬手亮屏: 状态 TRIGGERED→DOWN (filteredZ=%.1f)", filteredZ)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,10 +196,10 @@ class FiseAccelerometerWake @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置窗口 */
|
||||
private fun resetWindow() {
|
||||
windowIndex = 0
|
||||
windowFilled = false
|
||||
zWindow.fill(Float.MAX_VALUE)
|
||||
/** 重置状态 */
|
||||
private fun resetState() {
|
||||
filteredZ = 0f
|
||||
filterInitialized = false
|
||||
armState = ArmState.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user