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 139abad..4ad3445 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 @@ -1496,6 +1496,9 @@ private val DEFAULT_CV_THR: Double else -> 0.08 } private const val DEFAULT_LR_DEV_THR = 0.20 +/** Edge 대칭 — |d_ch0 - d_ch3| / mean(d_ch0, d_ch3). ch3/ch0 가장자리 비대칭(센서 위치 오프셋) + * 잡는 게이트. CV(전체 균일성)는 outlier 1개를 희석시키므로 별도 검사 필요. */ +private const val DEFAULT_EDGE_ASYM_THR = 0.08 /** * Gradient 기반 Placement Guide @@ -1867,14 +1870,33 @@ private fun computePlacementGuide( } } + // Edge 대칭 (ch0 vs ch3) — 센서 비대칭 위치 잡기. + // 정상 정렬이면 ch0/ch3 가장자리 깊이가 비슷. 한쪽이 짧으면 센서 위치가 + // 그쪽 방광 가장자리로 치우침 (BV 저평가의 주 원인). + var edgeAsym = 0.0 + var edgePass = true + val w0 = if (0 < walls.size) walls[0] else null + val w3 = if (3 < walls.size) walls[3] else null + if (w0 != null && w3 != null) { + val d0 = ((offset + w0.second * dps) - (offset + w0.first * dps)) * + kotlin.math.cos(degAll[0] * Math.PI / 180.0) + val d3 = ((offset + w3.second * dps) - (offset + w3.first * dps)) * + kotlin.math.cos(degAll[3] * Math.PI / 180.0) + val edgeMean = (d0 + d3) / 2.0 + if (edgeMean > 0) { + edgeAsym = (d0 - d3) / edgeMean + edgePass = kotlin.math.abs(edgeAsym) <= DEFAULT_EDGE_ASYM_THR + } + } + // R-peak gate (SWEEP 모드에서만 적용) val isSweep = com.example.medilightv2android.managers.GreenZoneConstants.placementGuideMode == com.example.medilightv2android.managers.PlacementGuideMode.SWEEP val rPeakOk = !isSweep || currentRMm == null || bestRMm <= 0.0 || (bestRMm - currentRMm) <= 1.5 - val isPass = cv <= cvThr && lrPass && rPeakOk + val isPass = cv <= cvThr && lrPass && rPeakOk && edgePass android.util.Log.d("PlacementGuide", - "GREEN check: cv=${"%.3f".format(cv)} cvThr=${"%.3f".format(cvThr)} cvOk=${cv <= cvThr} lrPass=$lrPass lrDev=${"%.3f".format(lrDev)} rPeakOk=$rPeakOk → isPass=$isPass") + "GREEN check: cv=${"%.3f".format(cv)} cvThr=${"%.3f".format(cvThr)} cvOk=${cv <= cvThr} lrPass=$lrPass lrDev=${"%.3f".format(lrDev)} edgeAsym=${"%.3f".format(edgeAsym)} edgePass=$edgePass rPeakOk=$rPeakOk → isPass=$isPass") if (isPass) { val score = ((1.0 - cv / cvThr).coerceIn(0.0, 1.0) * 100).toInt().coerceAtLeast(70) @@ -1911,6 +1933,14 @@ private fun computePlacementGuide( !lrPass && lrDev < -DEFAULT_LR_DEV_THR -> { adjustHint = "Slide right →"; adjustIcon = "arrow.right" } + // Edge 비대칭 — ch3 짧음 (센서 너무 아래) → 위로 미세 조정 + !edgePass && edgeAsym > 0 -> { + adjustHint = "Slide up slightly ↑ (ch3 short)"; adjustIcon = "arrow.up" + } + // Edge 비대칭 — ch0 짧음 (센서 너무 위) → 아래로 미세 조정 + !edgePass && edgeAsym < 0 -> { + adjustHint = "Slide down slightly ↓ (ch0 short)"; adjustIcon = "arrow.down" + } gradient > 0.3 -> { adjustHint = "Slide down ↓"; adjustIcon = "arrow.down" }