Files
VesiscanClinicalAndroid/app/src/main/java/com/example/medilightv2android/managers/ImuPostureClassifier.kt
T
dw.jang 6bf399c7b9 fix(walking): chained failure — acc gate fail 시 walking 잃던 버그
증상: 로그캣 분석 — 활발히 걷는 중 (accMag > 1.2g) 일수록 화면에 안 보임.
  11:24:03 NON_LYING → WALKING 성공
  11:24:04 accMag=1.80 → UNKNOWN, walking 도 grace 들어감
  11:24:05 grace 2초 초과 → walking 종료
  → 활발 보행 = 가장 위험한 순간에 알람 사라짐 (정반대)

원인 두 가지:
  1) Classifier 가 acc gate fail 시 pitch=0 반환
     → WalkingDetector 가 pitch=0 받아서 pitchOk=false → grace 진입
  2) Composite 로직: r.posture==UNKNOWN 이면 WALKING 무시하고 UNKNOWN 표시

수정:
  1) ImuPostureClassifier: pitch 계산을 acc gate 이전으로 옮김
     → UNKNOWN Result 에도 실제 pitch 값 포함
     → WalkingDetector 가 정확한 pitch 로 grace 판단 가능

  2) PiezoMonitoringView composite 순서 변경:
     when {
       isWalkingNow -> WALKING          // 우선 (자체 게이트 신뢰)
       r.posture==UNKNOWN -> UNKNOWN
       else -> r.posture
     }
     walking detector 가 walking 이라고 하면 acc gate 실패해도 표시.
     walking 자체에 시간/wmean/wstd 게이트 있어 false-positive 거의 없음.

테스트 시나리오:
  정지 lying → 흔들면서 걷기 (pitch>45°) → walking 검출 → 격하게 흔들기
  (accMag>1.2g) 도 → "Walking" 계속 표시 + 빨간 팝업 유지.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-24 11:27:58 +09:00

81 lines
3.4 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* Posture classifier — flowchart `Posture classification` 기반 (2026-06-22).
*
* 단순화: 3 state (SUPINE/SITTING/STANDING) → **2 state (LYING / NON_LYING)**.
* - 낙상 예방 목적상 sitting/standing 구분 불필요. "누웠나, 일어났나" 만 중요.
*
* 단계:
* 1. Acc magnitude 게이트: 0.8 ≤ |a| ≤ 1.2 → motion/bad sample 거부
* 2. Gyro magnitude 게이트: |g| ≤ 9.16 dps → 회전 중 sample 거부
* 3. Pitch 계산: atan(ax / sqrt(ay² + az²)) × 180/π
* 4. 분류 (|pitch| 기준, threshold = GreenZoneConstants.pitchLyingThreshold):
* > threshold → LYING (누움)
* ≤ threshold → NON_LYING (앉음/섬)
* threshold default = 25°, dev 모드 설정에서 변경 가능.
*/
package com.example.medilightv2android.managers
import com.example.medilightv2android.ble.ImuSample
import kotlin.math.abs
import kotlin.math.atan
import kotlin.math.sqrt
enum class Posture { UNKNOWN, LYING, NON_LYING, WALKING }
object ImuPostureClassifier {
// 게이트 임계값
private const val ACC_MAG_MIN = 0.8f
private const val ACC_MAG_MAX = 1.2f
// Gyro gate 비활성화 — 일부 디바이스의 gyro calibration bias (-25 dps 등) 가
// flowchart 임계(9.16) 보다 커서 정지 상태에서도 fail → 분류 영구 불가.
// accel magnitude gate (0.8-1.2g) 가 이미 큰 모션 거름. gyro 게이트 제거.
// 단 dev 모드 디버그 위해 magnitude 는 계속 계산해 로그/Result 에 노출.
/** 자세 + 진단 정보 (UI/log용). */
data class Result(
val posture: Posture,
val pitchDeg: Float,
val accMag: Float,
val gyroMagDps: Float,
val reason: String,
)
fun classify(samples: List<ImuSample>): Result {
if (samples.isEmpty()) {
return Result(Posture.UNKNOWN, 0f, 0f, 0f, "no samples")
}
var sumAx = 0f; var sumAy = 0f; var sumAz = 0f
var sumGx = 0f; var sumGy = 0f; var sumGz = 0f
for (s in samples) {
sumAx += s.ax; sumAy += s.ay; sumAz += s.az
sumGx += s.gx; sumGy += s.gy; sumGz += s.gz
}
val n = samples.size
val ax = sumAx / n; val ay = sumAy / n; val az = sumAz / n
val gx = sumGx / n; val gy = sumGy / n; val gz = sumGz / n
val accMag = sqrt(ax * ax + ay * ay + az * az)
val gyroMag = sqrt(gx * gx + gy * gy + gz * gz)
// Pitch 는 acc gate 통과 여부와 무관하게 항상 계산 (walking detector 가 사용).
val pitchPre = (atan(ax / sqrt(ay * ay + az * az)) * 180.0 / Math.PI).toFloat()
if (accMag < ACC_MAG_MIN || accMag > ACC_MAG_MAX) {
return Result(Posture.UNKNOWN, pitchPre, accMag, gyroMag,
"acc mag out of range (%.2f)".format(accMag))
}
// Gyro gate 제거 — bias 큰 디바이스에서 영구 fail 문제 (위 주석 참조)
val absPitch = abs(pitchPre)
val thr = GreenZoneConstants.pitchLyingThreshold
// Pitch 의미: AX 가 중력과 일치할수록 큰 값 (≈90°), AZ 가 중력과 일치하면 작은 값 (≈0°).
// 사용자 직관: 센서 평평하게 둠 (AZ up) = LYING, 세움 (AX up) = NON_LYING (upright).
// → 작은 pitch → LYING, 큰 pitch → NON_LYING
val posture = if (absPitch <= thr) Posture.LYING else Posture.NON_LYING
return Result(posture, pitchPre, accMag, gyroMag, "ok")
}
}