feat: low echo detection + Placement 알고리즘 업데이트 (원은지님 공유)
Low Echo Detection 업데이트: - VALLEY_STOP_RISE: 10→50 (valley 스캔 early stop 임계) - EDGE_DIST_DECAY: 0.12 (신규, edge 거리 penalty) - Edge boundary 확장: left [leftLo:edge+1], right [edge-1:rightHi+1] - Peak scoring: prominence → prominence / (1 + 0.12 × dist) (low echo에 가까운 peak이 더 높은 점수) Placement 알고리즘 전면 교체 (CV threshold 방식): - 기존 urineLen score → cos-corrected depth의 CV(변동계수) - CV ≤ 0.15 → PASS (Best Placement) - CV > 0.15 → FAIL → weakest channel 기반 방향 안내 - CH0/CH1(top) weakest → MOVE DOWN - CH2/CH3(bottom) weakest → MOVE UP - severity: slight(<10%) / moderate(10~25%) / severe(≥25%) - LR deviation: CH4/CH5 cos-corrected depth 비교 - |lr_dev| ≤ 0.15 → PASS - lr_dev > 0 → MOVE RIGHT, < 0 → MOVE LEFT - 반복 시 CV threshold 완화 (+0.02/회) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -137,6 +137,8 @@ class PiezoEchoAnalyzer private constructor() {
|
||||
val minUrineLen: Int = 3
|
||||
val postRefineTolRatio: Double = 0.05
|
||||
val maxRealisticVolumeMl: Double = 1500.0
|
||||
val valleyStopRise: Double = 50.0
|
||||
val edgeDistDecay: Double = 0.12
|
||||
|
||||
// ── Public API ──
|
||||
|
||||
@@ -399,20 +401,19 @@ class PiezoEchoAnalyzer private constructor() {
|
||||
for (i in (peakIdx + 1) until min(n, peakIdx + maxDist)) {
|
||||
if (sg[i] < v) {
|
||||
v = sg[i]
|
||||
} else if (sg[i] > v + 10) {
|
||||
} else if (sg[i] > v + valleyStopRise) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
/** peak 왼쪽에서 가장 가까운 valley의 sg 값 */
|
||||
private fun findLeftValley(sg: DoubleArray, peakIdx: Int, maxDist: Int = 20): Double {
|
||||
var v = sg[peakIdx]
|
||||
for (i in (peakIdx - 1) downTo max(0, peakIdx - maxDist)) {
|
||||
if (sg[i] < v) {
|
||||
v = sg[i]
|
||||
} else if (sg[i] > v + 10) {
|
||||
} else if (sg[i] > v + valleyStopRise) {
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -446,24 +447,28 @@ class PiezoEchoAnalyzer private constructor() {
|
||||
rightHi = min(n - 1, edge + searchWin) // 바깥 (자유)
|
||||
}
|
||||
|
||||
// edge 왼쪽 peak 후보 (가까운 순)
|
||||
// edge 왼쪽 peak 후보 — edge+1 포함 (inclusive boundary)
|
||||
var leftCandidates = listOf<Int>()
|
||||
if (edge > leftLo) {
|
||||
val seg = safeSlice(sg, from = leftLo, to = edge - 1)
|
||||
val leftEnd = min(edge + 1, n)
|
||||
if (leftEnd > leftLo) {
|
||||
val seg = safeSlice(sg, from = leftLo, to = leftEnd - 1)
|
||||
if (seg != null) {
|
||||
val pks = findPeaks1D(seg)
|
||||
val global = pks.map { it + leftLo }.filter { sg[it] >= peakMin }
|
||||
val global = pks.map { it + leftLo }
|
||||
.filter { it < edge && sg[it] >= peakMin }
|
||||
leftCandidates = global.sortedBy { abs(it - edge) }.take(maxPeakCandidates)
|
||||
}
|
||||
}
|
||||
|
||||
// edge 오른쪽 peak 후보 (가까운 순)
|
||||
// edge 오른쪽 peak 후보 — edge-1부터 시작 (inclusive boundary)
|
||||
var rightCandidates = listOf<Int>()
|
||||
if (rightHi >= edge) {
|
||||
val seg = safeSlice(sg, from = edge, to = rightHi)
|
||||
val rightStart = max(edge - 1, 0)
|
||||
if (rightHi >= rightStart) {
|
||||
val seg = safeSlice(sg, from = rightStart, to = rightHi)
|
||||
if (seg != null) {
|
||||
val pks = findPeaks1D(seg)
|
||||
val global = pks.map { it + edge }.filter { sg[it] >= peakMin }
|
||||
val global = pks.map { it + rightStart }
|
||||
.filter { it >= edge && sg[it] >= peakMin }
|
||||
rightCandidates = global.sortedBy { abs(it - edge) }.take(maxPeakCandidates)
|
||||
}
|
||||
}
|
||||
@@ -471,19 +476,21 @@ class PiezoEchoAnalyzer private constructor() {
|
||||
val candidates = leftCandidates + rightCandidates
|
||||
if (candidates.isEmpty()) return null
|
||||
|
||||
// prominence 계산 (urine 방향 valley 기준)
|
||||
// prominence + edge distance penalty (EDGE_DIST_DECAY=0.12)
|
||||
var bestPeak: Int? = null
|
||||
var bestProm = -1.0
|
||||
var bestScore = -1.0
|
||||
|
||||
for (p in candidates) {
|
||||
val valley: Double = if (side == WallSide.ANT) {
|
||||
findRightValley(sg, peakIdx = p) // urine 방향 = 오른쪽
|
||||
findRightValley(sg, peakIdx = p)
|
||||
} else {
|
||||
findLeftValley(sg, peakIdx = p) // urine 방향 = 왼쪽
|
||||
findLeftValley(sg, peakIdx = p)
|
||||
}
|
||||
val prom = sg[p] - valley
|
||||
if (prom > bestProm) {
|
||||
bestProm = prom
|
||||
val dist = abs(p - edge)
|
||||
val score = prom / (1.0 + edgeDistDecay * dist)
|
||||
if (score > bestScore) {
|
||||
bestScore = score
|
||||
bestPeak = p
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user