From c819d615d0370205280fbd6c7179b7cd66db3051 Mon Sep 17 00:00:00 2001 From: jjangddu Date: Fri, 8 May 2026 09:41:44 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20LR=20=EB=B0=A9=ED=96=A5=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20+=20Almost=20there=20=EC=A0=9C=EA=B1=B0=20+=20postM?= =?UTF-8?q?axIdx=20=EC=8A=AC=EB=9D=BC=EC=9D=B4=EB=8D=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 좌우 방향 전면 수정: ch4(왼쪽)길다→←, ch5(오른쪽)길다→→ ch4만 감지→← (기기 오른쪽 치우침), ch5만→→ - Almost there 제거: 방향OK+CV미달 → 그냥 GREEN 통과 - postMaxIdx 런타임 슬라이더 (개발자 설정, 기본 80, 범위 50-95) Method A/B 공통 적용 — 바닥반사 peak 무시 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../managers/GreenZoneConstants.kt | 2 + .../managers/PiezoEchoAnalyzer.kt | 2 +- .../managers/PiezoEchoAnalyzerA.kt | 2 +- .../views/monitoring/PiezoMonitoringView.kt | 18 ++++ .../ui/views/monitoring/PlacementGuideView.kt | 86 +++++++++---------- 5 files changed, 62 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/com/example/medilightv2android/managers/GreenZoneConstants.kt b/app/src/main/java/com/example/medilightv2android/managers/GreenZoneConstants.kt index fd3819d..fb56521 100644 --- a/app/src/main/java/com/example/medilightv2android/managers/GreenZoneConstants.kt +++ b/app/src/main/java/com/example/medilightv2android/managers/GreenZoneConstants.kt @@ -18,6 +18,8 @@ enum class BvMethod { FRUSTUM, V41 } object GreenZoneConstants { + var postMaxIdx: Int = 80 // 후벽 탐색 최대 sample index (이 이상 peak 무시) + var detectionMethod: DetectionMethod = DetectionMethod.METHOD_C var bvMethod: BvMethod = BvMethod.V41 var placementGuideMode: PlacementGuideMode = PlacementGuideMode.SIMPLE diff --git a/app/src/main/java/com/example/medilightv2android/managers/PiezoEchoAnalyzer.kt b/app/src/main/java/com/example/medilightv2android/managers/PiezoEchoAnalyzer.kt index 8892412..182d24a 100644 --- a/app/src/main/java/com/example/medilightv2android/managers/PiezoEchoAnalyzer.kt +++ b/app/src/main/java/com/example/medilightv2android/managers/PiezoEchoAnalyzer.kt @@ -132,7 +132,7 @@ class PiezoEchoAnalyzer private constructor() { val lowMinLen: Int = 3 val mergeGapMax: Int = 3 val peakSearchWin: Int = 20 - val postMaxIdx: Int = 80 + val postMaxIdx: Int get() = GreenZoneConstants.postMaxIdx val minPeakMargin: Double = 30.0 val minUrineLen: Int = 3 val postRefineTolRatio: Double = 0.05 diff --git a/app/src/main/java/com/example/medilightv2android/managers/PiezoEchoAnalyzerA.kt b/app/src/main/java/com/example/medilightv2android/managers/PiezoEchoAnalyzerA.kt index 323aaff..a587b8e 100644 --- a/app/src/main/java/com/example/medilightv2android/managers/PiezoEchoAnalyzerA.kt +++ b/app/src/main/java/com/example/medilightv2android/managers/PiezoEchoAnalyzerA.kt @@ -27,7 +27,7 @@ class PiezoEchoAnalyzerA private constructor() { val tvLambda: Double = 3.0 val alpha: Double = 1.0 val beta: Double = 1.0 - val postMaxIdx: Int = 80 + val postMaxIdx: Int get() = GreenZoneConstants.postMaxIdx val plateauMinLen: Int = 3 val plateauMergeGap: Int = 5 val scoreWin: Int = 7 diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt index e067388..cc1eb0b 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt @@ -1141,6 +1141,24 @@ fun PiezoMonitoringView(appState: AppState) { Switch(checked = sgEnabled, onCheckedChange = { sgEnabled = it; analyzer.useSgFilter = it }, colors = SwitchDefaults.colors(checkedTrackColor = MlPrimary)) } + + HorizontalDivider() + + // Post Max Index (바닥반사 무시 경계) + var postMaxVal by remember { mutableStateOf(GreenZoneConstants.postMaxIdx.toFloat()) } + Row(verticalAlignment = Alignment.CenterVertically) { + Text("Post Max", fontSize = 14.sp, fontWeight = FontWeight.SemiBold) + Spacer(modifier = Modifier.width(8.dp)) + Text("${postMaxVal.toInt()} / 100", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = MlPrimary) + Spacer(modifier = Modifier.weight(1f)) + } + Slider( + value = postMaxVal, + onValueChange = { postMaxVal = it; GreenZoneConstants.postMaxIdx = it.toInt() }, + valueRange = 50f..95f, + steps = 8, + colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary) + ) } HorizontalDivider() diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt index b0908b1..15fdea5 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt @@ -96,7 +96,6 @@ fun PlacementGuideView(appState: AppState) { // SWEEP 추가 state var sweepLastDetected by remember { mutableIntStateOf(0) } var sweepEverSawAll by remember { mutableStateOf(false) } - var almostThereCount by remember { mutableIntStateOf(0) } // 화면 진입 시 상태 리셋 LaunchedEffect(Unit) { @@ -111,7 +110,7 @@ fun PlacementGuideView(appState: AppState) { rTracker.reset() currentRMm = null; bestRMm = 0.0 sagittalArgMaxCh = null; lateralOffsetMm = null - sweepLastDetected = 0; sweepEverSawAll = false; almostThereCount = 0 + sweepLastDetected = 0; sweepEverSawAll = false } // 화면 벗어날 때 LED OFF @@ -657,25 +656,10 @@ fun PlacementGuideView(appState: AppState) { lastHintChangeMs = now } - // "Almost there" 연속 3회 → 자동 GREEN 통과 - val isAlmostThere = guideResult.hint.contains("Almost there") - if (isAlmostThere) almostThereCount++ else almostThereCount = 0 - val autoPass = guideResult.isPass || almostThereCount >= 3 - - if (autoPass) { + if (guideResult.isPass) { isLocked = true - almostThereCount = 0 if (lastLedMode != 6) { bleManager.sendLedMode(6); lastLedMode = 6 } - if (guideResult.isPass) { - bleManager.debugLogger.info("PLACEMENT_GREEN cv=${"%.3f".format(guideResult.cv)} lr_dev=${"%.3f".format(guideResult.lrDev)} score=${guideResult.score} otsu=${"%.0f".format(analyzer.lastOtsuThreshold)}") - } else { - // Auto-pass: 위치 OK인데 CV 노이즈로 3회 연속 미달 - directionHint = "Green! Position locked" - directionIcon = "checkmark.circle" - placementScore = 70 - placementPhase = PlacementPhase.GREEN - bleManager.debugLogger.info("PLACEMENT_GREEN_AUTO cv=${"%.3f".format(guideResult.cv)} almostThereCount=3") - } + bleManager.debugLogger.info("PLACEMENT_GREEN cv=${"%.3f".format(guideResult.cv)} lr_dev=${"%.3f".format(guideResult.lrDev)} score=${guideResult.score} otsu=${"%.0f".format(analyzer.lastOtsuThreshold)}") } else { isLocked = false if (lastLedMode != 5) { bleManager.sendLedMode(5); lastLedMode = 5 } @@ -1153,13 +1137,13 @@ private fun computePlacementGuide( phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.GREEN) } else { val lrHint = when { - l4 > 0 && l5 > 0 && l4 > l5 -> "Move right →" - l4 > 0 && l5 > 0 && l5 > l4 -> "Move left ←" + l4 > 0 && l5 > 0 && l4 > l5 -> "Move left ←" + l4 > 0 && l5 > 0 && l5 > l4 -> "Move right →" else -> "Checking lateral..." } val lrIcon = when { - l4 > 0 && l5 > 0 && l4 > l5 -> "arrow.right" - l4 > 0 && l5 > 0 && l5 > l4 -> "arrow.left" + l4 > 0 && l5 > 0 && l4 > l5 -> "arrow.left" + l4 > 0 && l5 > 0 && l5 > l4 -> "arrow.right" else -> "checkmark.circle" } PlacementGuideResult(lrHint, lrIcon, 90, @@ -1249,13 +1233,13 @@ private fun computePlacementGuide( } val lrHint = when { - l4 > 0 && l5 > 0 && l4 > l5 -> "Move right →" - l4 > 0 && l5 > 0 && l5 > l4 -> "Move left ←" + l4 > 0 && l5 > 0 && l4 > l5 -> "Move left ←" + l4 > 0 && l5 > 0 && l5 > l4 -> "Move right →" else -> "Checking lateral..." } val lrIcon = when { - l4 > 0 && l5 > 0 && l4 > l5 -> "arrow.right" - l4 > 0 && l5 > 0 && l5 > l4 -> "arrow.left" + l4 > 0 && l5 > 0 && l4 > l5 -> "arrow.left" + l4 > 0 && l5 > 0 && l5 > l4 -> "arrow.right" else -> "checkmark.circle" } val score = (40 + ((1.0 - kotlin.math.abs(gradient) / gradientThr) * 20).toInt()) @@ -1318,17 +1302,19 @@ private fun computePlacementGuide( } } - // 한쪽만 detect → amplitude 기반 방향 안내 + // 한쪽만 detect → 반대쪽으로 이동 (잡히는 쪽의 반대로) + // ch4(왼쪽)만 잡힘 = 기기가 오른쪽 치우침 = 왼쪽으로 if (ch4det && !ch5det) { - return PlacementGuideResult("Move right →", "arrow.right", 40, - phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.LATERAL) - } - if (ch5det && !ch4det) { return PlacementGuideResult("Move left ←", "arrow.left", 40, phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.LATERAL) } + // ch5(오른쪽)만 잡힘 = 기기가 왼쪽 치우침 = 오른쪽으로 + if (ch5det && !ch4det) { + return PlacementGuideResult("Move right →", "arrow.right", 40, + phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.LATERAL) + } if (!ch4det && !ch5det) { - return PlacementGuideResult("Try moving left ←", "arrow.left", 35, + return PlacementGuideResult("Try moving right →", "arrow.right", 35, phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.LATERAL) } @@ -1349,8 +1335,9 @@ private fun computePlacementGuide( } // ch4 > ch5 → 방광이 오른쪽에 있음 → 오른쪽으로 이동 - val hint = if (lrDiff > 0) "Move sensor right →" else "Move sensor left ←" - val icon = if (lrDiff > 0) "arrow.right" else "arrow.left" + // lrDiff > 0 = ch4 길다 = 방광이 왼쪽 → 왼쪽으로 + val hint = if (lrDiff > 0) "Move sensor left ←" else "Move sensor right →" + val icon = if (lrDiff > 0) "arrow.left" else "arrow.right" return PlacementGuideResult(hint, icon, 45, phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.LATERAL) } @@ -1420,28 +1407,35 @@ private fun computePlacementGuide( val weightedCenter = detectedLens.sumOf { (ch, len) -> ch.toDouble() * len } / totalLen val gradient = weightedCenter - 1.5 - val adjustHint: String; val adjustIcon: String; val stayInGreen: Boolean + // 방향이 다 맞는데 CV만 미달 → 그냥 GREEN 통과 (위치는 맞음) + if (lrPass && kotlin.math.abs(gradient) <= 0.3) { + val score = ((1.0 - cv / cvThr).coerceIn(0.0, 1.0) * 100).toInt().coerceAtLeast(60) + return PlacementGuideResult( + "Green! Optimal position", + "checkmark.circle", score, cv, true, lrDev, lrPass, + phase = PlacementPhase.GREEN, nextPhase = PlacementPhase.GREEN) + } + + // 방향이 틀리면 VERTICAL로 복귀 + val adjustHint: String; val adjustIcon: String when { !lrPass && lrDev > DEFAULT_LR_DEV_THR -> { - adjustHint = "Move right →"; adjustIcon = "arrow.right"; stayInGreen = false + // d4 > d5 → 방광 왼쪽 → 왼쪽으로 + adjustHint = "Move left ←"; adjustIcon = "arrow.left" } !lrPass && lrDev < -DEFAULT_LR_DEV_THR -> { - adjustHint = "Move left ←"; adjustIcon = "arrow.left"; stayInGreen = false + // d5 > d4 → 방광 오른쪽 → 오른쪽으로 + adjustHint = "Move right →"; adjustIcon = "arrow.right" } gradient > 0.3 -> { - adjustHint = "Lower ↓"; adjustIcon = "arrow.down"; stayInGreen = false - } - gradient < -0.3 -> { - adjustHint = "Raise ↑"; adjustIcon = "arrow.up"; stayInGreen = false + adjustHint = "Lower ↓"; adjustIcon = "arrow.down" } else -> { - // CV만 간당간당 — GREEN에 머물며 재시도 (VERTICAL로 돌아가지 않음) - adjustHint = "Almost there..."; adjustIcon = "checkmark.circle"; stayInGreen = true + adjustHint = "Raise ↑"; adjustIcon = "arrow.up" } } - val nextPh = if (stayInGreen) PlacementPhase.GREEN else PlacementPhase.VERTICAL return PlacementGuideResult(adjustHint, adjustIcon, 50, cv, false, lrDev, lrPass, - phase = PlacementPhase.GREEN, nextPhase = nextPh) + phase = PlacementPhase.GREEN, nextPhase = PlacementPhase.VERTICAL) } }