fix: cross-channel outlier 제거 + placement 강화 + UI 수정
- Cross-channel urine length outlier: median의 1.35x 초과 또는 0.5x 미만 → 제거 (CH3 바닥반사 urine=50 vs 정상 ~35 → 480ml↔223ml 편차 해결) - Placement CV: V0=0.07, V1/V2=0.10 (기기별 분기) - Placement 기준 강화: LR dev 0.20, gradient 0.3, lenVariation 0.35 - "방향OK→무조건GREEN" 제거, CV 미달시 "Fine-tune position" - GREEN CV가이드/Hold steady 제거, ch별 len 분석 방향 안내 - 가이드 텍스트 상자 항상 표시 - 센서 이미지 topview 교체 (240×150dp) - 앱 버전 VBTAND0101 - Plot/Log allWalls 통일 (선택 Method 기준) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,17 @@
|
|||||||
"Bash(git push -u origin master)",
|
"Bash(git push -u origin master)",
|
||||||
"Bash(git pull:*)",
|
"Bash(git pull:*)",
|
||||||
"Bash(./gradlew assembleDebug)",
|
"Bash(./gradlew assembleDebug)",
|
||||||
"Bash(xargs grep:*)"
|
"Bash(xargs grep:*)",
|
||||||
|
"Bash(./gradlew compileDebugKotlin)",
|
||||||
|
"Bash(git add:*)",
|
||||||
|
"Bash(git reset:*)",
|
||||||
|
"Bash(git commit -m ':*)",
|
||||||
|
"Bash(git push:*)",
|
||||||
|
"Bash(pip install:*)",
|
||||||
|
"Read(//c/Users/장동우/Downloads/**)",
|
||||||
|
"Bash(python3 -c ':*)",
|
||||||
|
"Bash(python -c ':*)",
|
||||||
|
"Bash(./gradlew assembleDebug --rerun-tasks)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ android {
|
|||||||
applicationId = "com.example.medilightv2android"
|
applicationId = "com.example.medilightv2android"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 1
|
versionCode = 2
|
||||||
versionName = "1.0"
|
versionName = "VBTAND0101"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,11 @@ fun HomeView(appState: AppState) {
|
|||||||
|
|
||||||
Text("Smart Bladder Monitoring", fontSize = 16.sp, color = MlSecondaryText)
|
Text("Smart Bladder Monitoring", fontSize = 16.sp, color = MlSecondaryText)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
|
val versionName = context.packageManager.getPackageInfo(context.packageName, 0).versionName
|
||||||
|
Text(versionName ?: "", fontSize = 12.sp, color = MlSecondaryText.copy(alpha = 0.5f))
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
|
|||||||
+41
-14
@@ -85,6 +85,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
val spotVolumes = remember { mutableListOf<Double>() }
|
val spotVolumes = remember { mutableListOf<Double>() }
|
||||||
var measureDoneSignal = remember { kotlinx.coroutines.CompletableDeferred<Unit>() }
|
var measureDoneSignal = remember { kotlinx.coroutines.CompletableDeferred<Unit>() }
|
||||||
val lastMultiChannelResult = remember { mutableStateListOf<PiezoChannelData>() }
|
val lastMultiChannelResult = remember { mutableStateListOf<PiezoChannelData>() }
|
||||||
|
var lastAllWalls by remember { mutableStateOf<List<Pair<Int, Int>?>>(List(6) { null }) }
|
||||||
val analyzer = remember { PiezoEchoAnalyzer.shared }
|
val analyzer = remember { PiezoEchoAnalyzer.shared }
|
||||||
|
|
||||||
val screenWidth = LocalConfiguration.current.screenWidthDp
|
val screenWidth = LocalConfiguration.current.screenWidthDp
|
||||||
@@ -156,6 +157,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
var rawVolumeMl: Double? = null
|
var rawVolumeMl: Double? = null
|
||||||
var rawLrRatio = 1.0
|
var rawLrRatio = 1.0
|
||||||
var totalWallCount = 0
|
var totalWallCount = 0
|
||||||
|
val allWalls: MutableList<Pair<Int, Int>?> = MutableList(6) { null }
|
||||||
|
|
||||||
val validChannels = channels.filter { it.isValid }
|
val validChannels = channels.filter { it.isValid }
|
||||||
val allChannelsReceived = channels.filter { it.buffer.size >= 20 }.size >= 6
|
val allChannelsReceived = channels.filter { it.buffer.size >= 20 }.size >= 6
|
||||||
@@ -163,7 +165,6 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
val method = com.example.medilightv2android.managers.GreenZoneConstants.detectionMethod
|
val method = com.example.medilightv2android.managers.GreenZoneConstants.detectionMethod
|
||||||
val analyzerA = com.example.medilightv2android.managers.PiezoEchoAnalyzerA.shared
|
val analyzerA = com.example.medilightv2android.managers.PiezoEchoAnalyzerA.shared
|
||||||
|
|
||||||
val allWalls: MutableList<Pair<Int, Int>?> = MutableList(6) { null }
|
|
||||||
var methodCBvMl: Double? = null
|
var methodCBvMl: Double? = null
|
||||||
val methodLabel: String
|
val methodLabel: String
|
||||||
|
|
||||||
@@ -244,6 +245,21 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
}
|
}
|
||||||
val rawADC = channels.sortedBy { it.channel }.map { it.buffer }
|
val rawADC = channels.sortedBy { it.channel }.map { it.buffer }
|
||||||
|
|
||||||
|
// Cross-channel urine length outlier 제거
|
||||||
|
// median의 1.4배 초과 또는 0.6배 미만 채널은 outlier → null 처리
|
||||||
|
val urineLensAll = (0..5).map { allWalls[it]?.let { w -> w.second - w.first } ?: 0 }
|
||||||
|
val validLensAll = urineLensAll.filter { it > 0 }
|
||||||
|
if (validLensAll.size >= 3) {
|
||||||
|
val medianLen = validLensAll.sorted()[validLensAll.size / 2]
|
||||||
|
for (ch in 0..5) {
|
||||||
|
val len = urineLensAll[ch]
|
||||||
|
if (len > 0 && (len > medianLen * 1.35 || len < medianLen * 0.5)) {
|
||||||
|
Log.w("PiezoMonitor", "CH$ch urine=$len OUTLIER (median=$medianLen) → removed")
|
||||||
|
allWalls[ch] = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val bvMethodSetting = com.example.medilightv2android.managers.GreenZoneConstants.bvMethod
|
val bvMethodSetting = com.example.medilightv2android.managers.GreenZoneConstants.bvMethod
|
||||||
val useV41Bv = method == com.example.medilightv2android.managers.DetectionMethod.METHOD_C
|
val useV41Bv = method == com.example.medilightv2android.managers.DetectionMethod.METHOD_C
|
||||||
&& bvMethodSetting == com.example.medilightv2android.managers.BvMethod.V41
|
&& bvMethodSetting == com.example.medilightv2android.managers.BvMethod.V41
|
||||||
@@ -281,6 +297,8 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
com.example.medilightv2android.services.AdcCsvLogger.log(failADC, null)
|
com.example.medilightv2android.services.AdcCsvLogger.log(failADC, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// allWalls를 UI state에 저장 (ChannelPanel에서 사용)
|
||||||
|
handler.post { lastAllWalls = allWalls.toList() }
|
||||||
val finalRawVol = rawVolumeMl
|
val finalRawVol = rawVolumeMl
|
||||||
val finalTotalWalls = totalWallCount
|
val finalTotalWalls = totalWallCount
|
||||||
val analysisMs = System.currentTimeMillis() - analysisStart
|
val analysisMs = System.currentTimeMillis() - analysisStart
|
||||||
@@ -788,11 +806,11 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
size = donutSize
|
size = donutSize
|
||||||
)
|
)
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
// Bladdy 크기: 일반 모드에서 더 크게
|
// Bladdy 크기: 일반 모드에서 크게
|
||||||
val bladdySize = if (appState.isDevMode) {
|
val bladdySize = if (appState.isDevMode) {
|
||||||
if (isCompact) 80.dp else 110.dp
|
if (isCompact) 80.dp else 110.dp
|
||||||
} else {
|
} else {
|
||||||
if (isCompact) 120.dp else 150.dp // 원 지름의 ~1/2
|
if (isCompact) 180.dp else 220.dp
|
||||||
}
|
}
|
||||||
BladdyRiveView(urgency = urgency, size = bladdySize)
|
BladdyRiveView(urgency = urgency, size = bladdySize)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
@@ -927,6 +945,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
ChannelPanel(
|
ChannelPanel(
|
||||||
lastMultiChannelResult = lastMultiChannelResult,
|
lastMultiChannelResult = lastMultiChannelResult,
|
||||||
analyzer = analyzer,
|
analyzer = analyzer,
|
||||||
|
allWalls = lastAllWalls,
|
||||||
isPlacementTesting = isPlacementTesting,
|
isPlacementTesting = isPlacementTesting,
|
||||||
onMeasure = {
|
onMeasure = {
|
||||||
if (!isPlacementTesting && !isMeasuring) {
|
if (!isPlacementTesting && !isMeasuring) {
|
||||||
@@ -1173,6 +1192,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun ChannelPanel(
|
private fun ChannelPanel(
|
||||||
lastMultiChannelResult: List<PiezoChannelData>,
|
lastMultiChannelResult: List<PiezoChannelData>,
|
||||||
|
allWalls: List<Pair<Int, Int>?> = List(6) { null },
|
||||||
analyzer: PiezoEchoAnalyzer,
|
analyzer: PiezoEchoAnalyzer,
|
||||||
isPlacementTesting: Boolean,
|
isPlacementTesting: Boolean,
|
||||||
onMeasure: () -> Unit
|
onMeasure: () -> Unit
|
||||||
@@ -1272,7 +1292,9 @@ private fun ChannelPanel(
|
|||||||
.background(if (isValid) Color.Green else Color.Red.copy(alpha = 0.4f)),
|
.background(if (isValid) Color.Green else Color.Red.copy(alpha = 0.4f)),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
val label = chAnalysis?.result?.let { "u=${it.urineLen}" } ?: "—"
|
// allWalls의 실제 BV 계산값 표시 (post-ant)
|
||||||
|
val wallLen = allWalls.getOrNull(ch)?.let { it.second - it.first }
|
||||||
|
val label = wallLen?.let { "u=$it" } ?: "—"
|
||||||
Text(label, fontSize = 7.sp, fontWeight = FontWeight.Bold, color = Color.White)
|
Text(label, fontSize = 7.sp, fontWeight = FontWeight.Bold, color = Color.White)
|
||||||
}
|
}
|
||||||
Text("CH$ch", fontSize = 8.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText)
|
Text("CH$ch", fontSize = 8.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText)
|
||||||
@@ -1287,33 +1309,39 @@ private fun ChannelPanel(
|
|||||||
else -> analyzer.analyzeChannel(chData.buffer, chData.channel)
|
else -> analyzer.analyzeChannel(chData.buffer, chData.channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 실제 BV 계산에 사용된 allWalls 값으로 표시
|
||||||
|
val wall = allWalls.getOrNull(chData.channel)
|
||||||
|
val antIdx = wall?.first ?: chAnalysis.result?.ant
|
||||||
|
val postIdx = wall?.second ?: chAnalysis.result?.post
|
||||||
|
val wallLen = if (wall != null) wall.second - wall.first else null
|
||||||
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(3.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(3.dp)) {
|
||||||
// Channel header
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(8.dp)
|
.size(8.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(if (chAnalysis.isValid) Color.Green else Color.Gray.copy(alpha = 0.4f))
|
.background(if (wall != null) Color.Green else Color.Gray.copy(alpha = 0.4f))
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text("CH${chData.channel}", fontSize = 12.sp, fontWeight = FontWeight.Bold)
|
Text("CH${chData.channel}", fontSize = 12.sp, fontWeight = FontWeight.Bold)
|
||||||
|
|
||||||
if (chAnalysis.result != null) {
|
if (antIdx != null && postIdx != null) {
|
||||||
val r = chAnalysis.result
|
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
Text(
|
Text(
|
||||||
"urine idx ${r.ant}–${r.post}",
|
"urine idx ${antIdx}–${postIdx}",
|
||||||
fontSize = 10.sp, fontWeight = FontWeight.SemiBold, color = Color.Cyan
|
fontSize = 10.sp, fontWeight = FontWeight.SemiBold, color = Color.Cyan
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
val dps = com.example.medilightv2android.managers.PiezoHW.distancePerSample
|
||||||
|
val offset = com.example.medilightv2android.managers.PiezoHW.delayOffsetMm
|
||||||
Text(
|
Text(
|
||||||
"(${"%.0f".format(r.antDepthMm)}–${"%.0f".format(r.postDepthMm)}mm)",
|
"(${"%.0f".format(offset + antIdx * dps)}–${"%.0f".format(offset + postIdx * dps)}mm)",
|
||||||
fontSize = 9.sp, color = MlSecondaryText
|
fontSize = 9.sp, color = MlSecondaryText
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
Text(
|
Text(
|
||||||
"len=${r.urineLen}",
|
"len=${wallLen ?: (postIdx - antIdx)}",
|
||||||
fontSize = 9.sp, fontWeight = FontWeight.SemiBold, color = Color.Cyan
|
fontSize = 9.sp, fontWeight = FontWeight.SemiBold, color = Color.Cyan
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -1322,13 +1350,12 @@ private fun ChannelPanel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Waveform chart
|
|
||||||
WaveformChart(
|
WaveformChart(
|
||||||
buffer = chData.buffer,
|
buffer = chData.buffer,
|
||||||
peakIndex = chData.peakIndex.toInt(),
|
peakIndex = chData.peakIndex.toInt(),
|
||||||
analyzer = analyzer,
|
analyzer = analyzer,
|
||||||
antIndex = chAnalysis.result?.ant,
|
antIndex = antIdx,
|
||||||
postIndex = chAnalysis.result?.post,
|
postIndex = postIdx,
|
||||||
lowStart = chAnalysis.result?.lowStart,
|
lowStart = chAnalysis.result?.lowStart,
|
||||||
lowEnd = chAnalysis.result?.lowEnd,
|
lowEnd = chAnalysis.result?.lowEnd,
|
||||||
modifier = Modifier.fillMaxWidth().height(120.dp)
|
modifier = Modifier.fillMaxWidth().height(120.dp)
|
||||||
|
|||||||
+57
-39
@@ -167,6 +167,7 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
.then(if (appState.isDevMode) Modifier.verticalScroll(rememberScrollState()) else Modifier)
|
||||||
.padding(horizontal = 24.dp),
|
.padding(horizontal = 24.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
@@ -201,7 +202,7 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f),
|
.then(if (appState.isDevMode) Modifier.height(400.dp) else Modifier.weight(1f)),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
// 하복부 실루엣 (Canvas — CKLaw body silhouette)
|
// 하복부 실루엣 (Canvas — CKLaw body silhouette)
|
||||||
@@ -354,9 +355,9 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
painter = androidx.compose.ui.res.painterResource(com.example.medilightv2android.R.drawable.sensor_device),
|
painter = androidx.compose.ui.res.painterResource(com.example.medilightv2android.R.drawable.sensor_device),
|
||||||
contentDescription = "Sensor",
|
contentDescription = "Sensor",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(width = 200.dp, height = 125.dp)
|
.size(width = 240.dp, height = 150.dp)
|
||||||
.align(Alignment.Center)
|
.align(Alignment.Center)
|
||||||
.offset(x = 5.dp, y = 115.dp),
|
.offset(x = 0.dp, y = 80.dp),
|
||||||
contentScale = androidx.compose.ui.layout.ContentScale.Fit,
|
contentScale = androidx.compose.ui.layout.ContentScale.Fit,
|
||||||
alpha = if (isGreenZone) 0.7f else 0.95f
|
alpha = if (isGreenZone) 0.7f else 0.95f
|
||||||
)
|
)
|
||||||
@@ -402,23 +403,21 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 가이드 텍스트 (일러스트 아래) ──
|
// ── 가이드 텍스트 (일러스트 아래, 항상 표시) ──
|
||||||
if (directionHint.isNotEmpty()) {
|
val hintGreen = isGreenZone
|
||||||
val hintGreen = isGreenZone
|
val hintBg = if (hintGreen) Color(0xFF4CAF50).copy(alpha = 0.12f) else Color(0xFFFFF3E0)
|
||||||
val hintBg = if (hintGreen) Color(0xFF4CAF50).copy(alpha = 0.12f) else Color(0xFFFFF3E0)
|
val hintFg = if (hintGreen) Color(0xFF4CAF50) else Color(0xFFFF9800)
|
||||||
val hintFg = if (hintGreen) Color(0xFF4CAF50) else Color(0xFFFF9800)
|
Text(
|
||||||
Text(
|
text = directionHint.ifEmpty { " " },
|
||||||
text = directionHint,
|
fontSize = 15.sp,
|
||||||
fontSize = 15.sp,
|
fontWeight = FontWeight.SemiBold,
|
||||||
fontWeight = FontWeight.SemiBold,
|
color = hintFg,
|
||||||
color = hintFg,
|
textAlign = TextAlign.Center,
|
||||||
textAlign = TextAlign.Center,
|
modifier = Modifier
|
||||||
modifier = Modifier
|
.fillMaxWidth()
|
||||||
.fillMaxWidth()
|
.background(hintBg, RoundedCornerShape(12.dp))
|
||||||
.background(hintBg, RoundedCornerShape(12.dp))
|
.padding(12.dp)
|
||||||
.padding(12.dp)
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
@@ -1027,8 +1026,15 @@ data class PlacementGuideResult(
|
|||||||
val nextPhase: PlacementPhase = PlacementPhase.VERTICAL
|
val nextPhase: PlacementPhase = PlacementPhase.VERTICAL
|
||||||
)
|
)
|
||||||
|
|
||||||
private const val DEFAULT_CV_THR = 0.12
|
// CV threshold는 기기 preset에 따라 다름
|
||||||
private const val DEFAULT_LR_DEV_THR = 0.35
|
private val DEFAULT_CV_THR: Double
|
||||||
|
get() = when (com.example.medilightv2android.managers.PiezoHW.activePreset) {
|
||||||
|
com.example.medilightv2android.managers.PiezoHW.DevicePreset.V0 -> 0.07
|
||||||
|
com.example.medilightv2android.managers.PiezoHW.DevicePreset.V1,
|
||||||
|
com.example.medilightv2android.managers.PiezoHW.DevicePreset.V2 -> 0.10
|
||||||
|
else -> 0.08
|
||||||
|
}
|
||||||
|
private const val DEFAULT_LR_DEV_THR = 0.20
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gradient 기반 Placement Guide
|
* Gradient 기반 Placement Guide
|
||||||
@@ -1130,7 +1136,7 @@ private fun computePlacementGuide(
|
|||||||
val ch4ok = detected.getOrElse(4) { false }
|
val ch4ok = detected.getOrElse(4) { false }
|
||||||
val ch5ok = detected.getOrElse(5) { false }
|
val ch5ok = detected.getOrElse(5) { false }
|
||||||
// 좌우도 이미 균등하면 바로 GREEN
|
// 좌우도 이미 균등하면 바로 GREEN
|
||||||
if (ch4ok && ch5ok && kotlin.math.abs(l4 - l5) <= 10) {
|
if (ch4ok && ch5ok && kotlin.math.abs(l4 - l5) <= 5) {
|
||||||
PlacementGuideResult("Position OK! Final check...",
|
PlacementGuideResult("Position OK! Final check...",
|
||||||
"checkmark.circle", 90,
|
"checkmark.circle", 90,
|
||||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.GREEN)
|
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.GREEN)
|
||||||
@@ -1205,7 +1211,7 @@ private fun computePlacementGuide(
|
|||||||
val gradient = weightedCenter - 1.5 // 양수 = 아래쪽 무거움, 음수 = 위쪽 무거움
|
val gradient = weightedCenter - 1.5 // 양수 = 아래쪽 무거움, 음수 = 위쪽 무거움
|
||||||
|
|
||||||
// gradient threshold: 균등하다고 볼 수 있는 범위
|
// gradient threshold: 균등하다고 볼 수 있는 범위
|
||||||
val gradientThr = 0.4 // |gradient| < 0.4 → 상하 균등
|
val gradientThr = 0.3 // |gradient| < 0.3 → 상하 균등
|
||||||
|
|
||||||
// 추가 조건: variation (max-min)/max 도 체크
|
// 추가 조건: variation (max-min)/max 도 체크
|
||||||
val validLens = detectedLens.map { it.second }
|
val validLens = detectedLens.map { it.second }
|
||||||
@@ -1217,7 +1223,7 @@ private fun computePlacementGuide(
|
|||||||
"VERTICAL gradient=%.2f (center=%.2f) variation=%.2f detect=$centerDetected lens=$centerLens"
|
"VERTICAL gradient=%.2f (center=%.2f) variation=%.2f detect=$centerDetected lens=$centerLens"
|
||||||
.format(gradient, weightedCenter, lenVariation))
|
.format(gradient, weightedCenter, lenVariation))
|
||||||
|
|
||||||
if (kotlin.math.abs(gradient) < gradientThr && lenVariation < 0.5) {
|
if (kotlin.math.abs(gradient) < gradientThr && lenVariation < 0.35) {
|
||||||
// 상하 OK → 좌우 확인
|
// 상하 OK → 좌우 확인
|
||||||
val l4 = urineLens.getOrElse(4) { 0 }
|
val l4 = urineLens.getOrElse(4) { 0 }
|
||||||
val l5 = urineLens.getOrElse(5) { 0 }
|
val l5 = urineLens.getOrElse(5) { 0 }
|
||||||
@@ -1225,7 +1231,7 @@ private fun computePlacementGuide(
|
|||||||
val ch5ok = detected.getOrElse(5) { false }
|
val ch5ok = detected.getOrElse(5) { false }
|
||||||
|
|
||||||
// 좌우도 이미 균등하면 LATERAL 건너뛰고 바로 GREEN
|
// 좌우도 이미 균등하면 LATERAL 건너뛰고 바로 GREEN
|
||||||
if (ch4ok && ch5ok && kotlin.math.abs(l4 - l5) <= 10) {
|
if (ch4ok && ch5ok && kotlin.math.abs(l4 - l5) <= 5) {
|
||||||
return PlacementGuideResult("Position OK! Final check...",
|
return PlacementGuideResult("Position OK! Final check...",
|
||||||
"checkmark.circle", 65,
|
"checkmark.circle", 65,
|
||||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.GREEN)
|
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.GREEN)
|
||||||
@@ -1395,7 +1401,7 @@ private fun computePlacementGuide(
|
|||||||
if (isPass) {
|
if (isPass) {
|
||||||
val score = ((1.0 - cv / cvThr).coerceIn(0.0, 1.0) * 100).toInt().coerceAtLeast(70)
|
val score = ((1.0 - cv / cvThr).coerceIn(0.0, 1.0) * 100).toInt().coerceAtLeast(70)
|
||||||
return PlacementGuideResult(
|
return PlacementGuideResult(
|
||||||
"Green! Optimal position (CV=${"%.2f".format(cv)})",
|
"Optimal position!",
|
||||||
"checkmark.circle", score, cv, true, lrDev, lrPass,
|
"checkmark.circle", score, cv, true, lrDev, lrPass,
|
||||||
phase = PlacementPhase.GREEN, nextPhase = PlacementPhase.GREEN)
|
phase = PlacementPhase.GREEN, nextPhase = PlacementPhase.GREEN)
|
||||||
} else {
|
} else {
|
||||||
@@ -1406,16 +1412,7 @@ private fun computePlacementGuide(
|
|||||||
val weightedCenter = detectedLens.sumOf { (ch, len) -> ch.toDouble() * len } / totalLen
|
val weightedCenter = detectedLens.sumOf { (ch, len) -> ch.toDouble() * len } / totalLen
|
||||||
val gradient = weightedCenter - 1.5
|
val gradient = weightedCenter - 1.5
|
||||||
|
|
||||||
// 방향이 다 맞는데 CV만 미달 → 그냥 GREEN 통과 (위치는 맞음)
|
// 방향이 틀리면 해당 방향으로 안내, 방향 OK면 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
|
val adjustHint: String; val adjustIcon: String
|
||||||
when {
|
when {
|
||||||
!lrPass && lrDev > DEFAULT_LR_DEV_THR -> {
|
!lrPass && lrDev > DEFAULT_LR_DEV_THR -> {
|
||||||
@@ -1429,12 +1426,33 @@ private fun computePlacementGuide(
|
|||||||
gradient > 0.3 -> {
|
gradient > 0.3 -> {
|
||||||
adjustHint = "Lower ↓"; adjustIcon = "arrow.down"
|
adjustHint = "Lower ↓"; adjustIcon = "arrow.down"
|
||||||
}
|
}
|
||||||
else -> {
|
gradient < -0.3 -> {
|
||||||
adjustHint = "Raise ↑"; adjustIcon = "arrow.up"
|
adjustHint = "Raise ↑"; adjustIcon = "arrow.up"
|
||||||
}
|
}
|
||||||
|
else -> {
|
||||||
|
// 방향 애매 + CV 미달 → ch별 len 분석으로 방향 제시
|
||||||
|
val lens = (0..3).map { urineLens.getOrElse(it) { 0 } }
|
||||||
|
val maxCh = lens.withIndex().maxByOrNull { it.value }?.index ?: 0
|
||||||
|
val minCh = lens.withIndex().filter { it.value > 0 }.minByOrNull { it.value }?.index
|
||||||
|
if (minCh != null && lens[maxCh] > 0 && lens[minCh].toDouble() < lens[maxCh] * 0.6) {
|
||||||
|
// 최소 채널이 최대의 60% 미만 → 약한 쪽으로 안내
|
||||||
|
if (minCh <= 1) {
|
||||||
|
// ch0/ch1 약함 → 위로
|
||||||
|
adjustHint = "Raise slightly ↑"; adjustIcon = "arrow.up"
|
||||||
|
} else {
|
||||||
|
// ch2/ch3 약함 → 아래로
|
||||||
|
adjustHint = "Lower slightly ↓"; adjustIcon = "arrow.down"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
adjustHint = "Fine-tune position"; adjustIcon = "checkmark.circle"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// 구체적 방향(↑↓←→)이면 VERTICAL 복귀, 아니면 GREEN 유지
|
||||||
|
val stayGreen = adjustIcon == "checkmark.circle"
|
||||||
|
val nextPh = if (stayGreen) PlacementPhase.GREEN else PlacementPhase.VERTICAL
|
||||||
return PlacementGuideResult(adjustHint, adjustIcon, 50, cv, false, lrDev, lrPass,
|
return PlacementGuideResult(adjustHint, adjustIcon, 50, cv, false, lrDev, lrPass,
|
||||||
phase = PlacementPhase.GREEN, nextPhase = PlacementPhase.VERTICAL)
|
phase = PlacementPhase.GREEN, nextPhase = nextPh)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 205 KiB After Width: | Height: | Size: 2.1 MiB |
Reference in New Issue
Block a user