fix(align V1): GREEN 게이트에 ch0/ch3 edge 대칭 검사 추가
증상: 270 mL 방광인데 220 mL 로 측정 (~50 mL 저평가).
원인 — 센서가 살짝 아래쪽에 부착돼 ch3 가 방광 하단 가장자리
만 보고 짧게 잡힘. ch0~ch3 CV 는 0.10 임계 안에 떨어져서
"In position!" 통과되지만 BV 결과는 저평가.
원리: CV 는 4채널 전체 변동성 — outlier 1개를 N=4 로 희석.
예: depths (75,78,75,65) → mean 73.25, std 4.79, cv 0.065
→ CV 0.10 통과지만 ch3 가 10mm 짧음.
수정: edge 대칭 게이트 (|d_ch0 - d_ch3| / mean(d0, d3) ≤ 0.08)
추가. CV 와 직교 — 균일 케이스는 영향 없고, edge outlier
케이스만 잡음.
- DEFAULT_EDGE_ASYM_THR = 0.08 도입
- computePlacementGuide GREEN 체크에 edgeAsym/edgePass 계산
- isPass = cv ≤ cvThr && lrPass && rPeakOk && **edgePass**
- hint 분기: edgeAsym > 0 → "Slide up slightly ↑ (ch3 short)"
edgeAsym < 0 → "Slide down slightly ↓ (ch0 short)"
- Log 에 edgeAsym/edgePass 추가
CV(0.10) 는 기존 유지. edge 만 보강.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+32
-2
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user