feat(verify): Posture event CSV logger + ground-truth annotation 버튼
알고리즘 검증 인프라.
신규 PostureEventLogger.kt:
- PiezoMonitoring 진입 시 새 CSV 파일 자동 생성
경로: Downloads/VesiScan_Posture/yyyy-MM-dd_HHmmss.csv
- 매 cycle 자세/walking 결과 + 신호값 1 row
- 알람 발동 시 별도 row (walking_start, walking_end, wakeup_alarm_lying_Ns)
- 사용자 annotation row (ground truth marker)
- 컬럼: timestamp_ms, type, posture, isWalking, pitch, wmean, wstd,
accMag, gyroMag, alarm_event, annotation
PiezoMonitoring 연결:
- LaunchedEffect(Unit) 에서 startSession 호출
- imuCollector.onComplete: logCycle 매 cycle 호출
- WALKING 전이: logAlarm("walking_start" / "walking_end")
- Wakeup alarm: logAlarm("wakeup_alarm_lying_Ns")
Dev settings — Verification 섹션:
- 5개 annotation 버튼: LIE / STAND / WALK↑ / WALK↓ / SIT
- 누르면 timestamp + 라벨이 CSV 에 ANNOTATION row 로 기록
- Toast 로 "Marked: 걷기시작" 같은 피드백
- 파일 경로 안내 텍스트
사용 흐름:
1. PiezoMonitoring 진입 (파일 자동 생성)
2. 동작 직전 dev settings 의 해당 버튼 누름 (예: WALK↑)
3. 동작 수행 (걷기)
4. CSV 회수: adb pull /sdcard/Download/VesiScan_Posture/
5. Excel / pandas 에서 분석:
- ANNOTATION row 의 timestamp = ground truth
- CYCLE row 의 isWalking 변화 = detection
- 두 시간 차 = latency
- 미발동 = FN, 과발동 = FP → confusion matrix
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* PostureEventLogger — 자세/walking 알고리즘 검증용 CSV logger.
|
||||
*
|
||||
* 매 cycle 마다 분류 결과 + walking 상태 + 신호값 기록 +
|
||||
* 사용자가 dev settings 의 annotation 버튼 누를 때 ground-truth marker 기록.
|
||||
*
|
||||
* 파일: Downloads/VesiScan_Posture/yyyy-MM-dd_HHmmss.csv
|
||||
* 컬럼:
|
||||
* timestamp_ms, type, posture, isWalking, pitch, wmean, wstd, accMag, gyroMag,
|
||||
* alarm_event, annotation
|
||||
*
|
||||
* Row type:
|
||||
* CYCLE — 매 cycle 의 자세/walking detector 결과
|
||||
* ALARM — 알람 발동 (wakeup / walking)
|
||||
* ANNOTATION — 사용자가 "지금 OOO" 표시 버튼 누름 (ground truth)
|
||||
*
|
||||
* 사용:
|
||||
* 1. logSession(context) 한 번 호출 (PiezoMonitoring 진입 시) — 파일 생성
|
||||
* 2. logCycle(...) 매 cycle 마다 호출
|
||||
* 3. logAlarm(...) 알람 발동 시
|
||||
* 4. annotate("WALK_START") 사용자가 버튼 누를 때
|
||||
* 5. closeSession() 종료 시 (선택)
|
||||
*
|
||||
* 분석: CSV → Excel / python pandas → ANNOTATION 과 CYCLE 의 walking 전이 대조.
|
||||
*/
|
||||
package com.example.medilightv2android.services
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Environment
|
||||
import java.io.File
|
||||
import java.io.FileWriter
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
object PostureEventLogger {
|
||||
|
||||
private val sessionFmt = SimpleDateFormat("yyyy-MM-dd_HHmmss", Locale.US)
|
||||
@Volatile private var sessionFile: File? = null
|
||||
|
||||
val currentFile: File? get() = sessionFile
|
||||
|
||||
private fun getDir(): File {
|
||||
val dir = File(
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||
"VesiScan_Posture"
|
||||
)
|
||||
if (!dir.exists()) dir.mkdirs()
|
||||
return dir
|
||||
}
|
||||
|
||||
/** 새 세션 파일 생성 + 헤더 작성. */
|
||||
fun startSession(@Suppress("UNUSED_PARAMETER") context: Context): File {
|
||||
val f = File(getDir(), "${sessionFmt.format(Date())}.csv")
|
||||
try {
|
||||
FileWriter(f, false).buffered().use { w ->
|
||||
w.write("timestamp_ms,type,posture,isWalking,pitch,wmean,wstd,accMag,gyroMag,alarm_event,annotation")
|
||||
w.newLine()
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
sessionFile = f
|
||||
return f
|
||||
}
|
||||
|
||||
/** Cycle 의 자세 분류 결과 기록. */
|
||||
fun logCycle(
|
||||
nowMs: Long,
|
||||
posture: String,
|
||||
isWalking: Boolean,
|
||||
pitch: Float,
|
||||
wmean: Float,
|
||||
wstd: Float,
|
||||
accMag: Float,
|
||||
gyroMag: Float,
|
||||
) {
|
||||
appendRow(
|
||||
nowMs, "CYCLE", posture, isWalking.toString(),
|
||||
pitch, wmean, wstd, accMag, gyroMag, "", ""
|
||||
)
|
||||
}
|
||||
|
||||
/** 알람 발동 (wakeup / walking) 기록. */
|
||||
fun logAlarm(nowMs: Long, alarmEvent: String) {
|
||||
appendRow(nowMs, "ALARM", "", "", 0f, 0f, 0f, 0f, 0f, alarmEvent, "")
|
||||
}
|
||||
|
||||
/** 사용자 ground-truth annotation 기록 (예: "WALK_START"). */
|
||||
fun annotate(label: String, nowMs: Long = System.currentTimeMillis()) {
|
||||
appendRow(nowMs, "ANNOTATION", "", "", 0f, 0f, 0f, 0f, 0f, "", label)
|
||||
}
|
||||
|
||||
fun closeSession() {
|
||||
sessionFile = null
|
||||
}
|
||||
|
||||
private fun appendRow(
|
||||
ts: Long, type: String, posture: String, isWalking: String,
|
||||
pitch: Float, wmean: Float, wstd: Float, accMag: Float, gyroMag: Float,
|
||||
alarm: String, annotation: String
|
||||
) {
|
||||
val f = sessionFile ?: return
|
||||
try {
|
||||
FileWriter(f, true).buffered().use { w ->
|
||||
w.write("$ts,$type,$posture,$isWalking,${"%.2f".format(pitch)},${"%.2f".format(wmean)},${"%.2f".format(wstd)},${"%.3f".format(accMag)},${"%.2f".format(gyroMag)},$alarm,$annotation")
|
||||
w.newLine()
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user