fix: LR 방향 수정 + Almost there 제거 + postMaxIdx 슬라이더
- 좌우 방향 전면 수정: 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) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+18
@@ -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()
|
||||
|
||||
+40
-46
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user