diff --git a/app/src/main/java/com/medithings/vesiscan/managers/ImuPostureClassifier.kt b/app/src/main/java/com/medithings/vesiscan/managers/ImuPostureClassifier.kt index 8f6b3af..54d65b6 100644 --- a/app/src/main/java/com/medithings/vesiscan/managers/ImuPostureClassifier.kt +++ b/app/src/main/java/com/medithings/vesiscan/managers/ImuPostureClassifier.kt @@ -6,18 +6,27 @@ * * 단계: * 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 모드 설정에서 변경 가능. + * 2. Tilt 계산 (Z축과 중력방향 사이 각도): + * horizontal = √(ax² + ay²) + * tilt = atan(horizontal / |az|) × 180/π + * → 방향 독립적. X축이든 Y축이든 어느쪽이 세로여도 동일 결과. + * 3. 분류 (tilt 기준, threshold = GreenZoneConstants.pitchLyingThreshold): + * ≤ threshold → LYING (Z축이 수직 = 기기 평평) + * > threshold → NON_LYING (Z축이 기울어짐 = 기기 세워짐) + * threshold default = 25° = 기기 평평기준 ±25° 여유. + * + * 이력: + * 2026-07-03: pitch(atan(ax/...)) → tilt(atan(hxy/|az|)) 로 교체. + * 기존은 X축 세로만 감지 → 손 테스트 시 Y축이 세로면 항상 LYING. + * Z축 tilt 방식은 orientation-independent 라 정상 착용 (배 위 부착) + + * hand-test 시나리오 모두 커버. */ package com.medithings.vesiscan.managers import com.medithings.vesiscan.ble.ImuSample import kotlin.math.abs import kotlin.math.atan +import kotlin.math.hypot import kotlin.math.sqrt enum class Posture { UNKNOWN, LYING, NON_LYING, WALKING } @@ -57,8 +66,18 @@ object ImuPostureClassifier { 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() + // Tilt = Z축과 중력방향 사이 각도 (방향 독립적). + // horizontal = √(ax² + ay²) = XY 평면 내 중력 성분 + // tilt = atan(horizontal / |az|) — Z축이 중력과 얼마나 어긋났는지 (0°=평평, 90°=세로) + // 정상 착용 (배 위) + hand-test 어느 시나리오에서도 동일 동작. + // 기기 평평 (AZ up) → horizontal=0, tilt=0° → LYING + // 기기 X축 세로 (AX up, 정상 서있음) → horizontal=1, |az|=0, tilt=90° → NON_LYING + // 기기 Y축 세로 (손으로 세로 잡음) → horizontal=1, |az|=0, tilt=90° → NON_LYING ★ 이전엔 실패 + val absAz = abs(az) + val horizontalMag = hypot(ax, ay) + val pitchPre = if (absAz > 1e-4f) + (atan(horizontalMag / absAz) * 180.0 / Math.PI).toFloat() + else 90f // Z축 성분 거의 없음 = 완전히 옆으로 세워짐 if (accMag < GreenZoneConstants.postureAccMagMin || accMag > GreenZoneConstants.postureAccMagMax) { return Result(Posture.UNKNOWN, pitchPre, accMag, gyroMag, @@ -66,13 +85,11 @@ object ImuPostureClassifier { } // 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 + // tilt 의미: 기기 평평 (AZ up) 이면 0°, 세워지면 90° 로 향함. + // → 작은 tilt → LYING (Z축이 수직 유지), 큰 tilt → NON_LYING (기기 세워짐) + val posture = if (pitchPre <= thr) Posture.LYING else Posture.NON_LYING return Result(posture, pitchPre, accMag, gyroMag, "ok") } }