fix: UX feedback + BV ellipse outlier (#22) + README v3 update
- #1 배터리 최적화 페이지 이동 제거 (ForegroundService+WakeLock만) - #2 Auto 첫 측정 대기 시 "Measuring... (n/5)" loading 표시 - #3 Placement 멘트 정리 — 방향만 남기기 (CH 언급 제거) - #4 CH0~CH5 dot row 개발자 모드 전용 - #5 Spot 재시도 로직 (최대 6회, 유효 3개 목표, 300ms 간격) - LR 좌우 조건 완화: ±8mm, symmetry≥0.5, lrDev≤0.35 - |len4-len5|≤10 → LATERAL 건너뛰고 바로 GREEN - GREEN 미달 시 CV만 간당간당하면 GREEN 유지 ("Almost there...") - CV 기본 임계 0.08→0.12, 디바운스 3초→1.5초 - BV 타원 피팅 outlier 반복 제거 (appshare #22) - README v3 업데이트 (3모드 placement, BV 파이프라인, 상태표) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -504,31 +504,69 @@ fun estimateBladderVolume(
|
||||
val aCapS = DoubleArray(order.size) { aCap[order[it]] }
|
||||
val sortedCh = order.map { validChannels[it] }
|
||||
|
||||
// 7) y-z 평면 타원 피팅 → cap 높이 추정 (#21 merge)
|
||||
val yw = DoubleArray(2 * n) { i -> if (i < n) yWallAnt[i] else yWallPost[i - n] }
|
||||
val zw = DoubleArray(2 * n) { i -> if (i < n) zWallAnt[i] else zWallPost[i - n] }
|
||||
val nPts = yw.size
|
||||
// 7) y-z 평면 타원 피팅 + 반복 outlier 제거 (#22 merge)
|
||||
val allYw = DoubleArray(2 * n) { i -> if (i < n) yWallAnt[i] else yWallPost[i - n] }
|
||||
val allZw = DoubleArray(2 * n) { i -> if (i < n) zWallAnt[i] else zWallPost[i - n] }
|
||||
val nPts = allYw.size
|
||||
var capKind = "fallback"
|
||||
var z0Ellipse: Double? = null
|
||||
var hCapBot = aCapS[0]
|
||||
var hCapTop = aCapS[n - 1]
|
||||
val ellipseCostThr = 0.5 // 점당 평균 잔차 임계
|
||||
|
||||
if (nPts >= 4) {
|
||||
val ym = yw.average(); val ysStd = std(yw) + 1e-12
|
||||
val zm = zw.average(); val zsStd = std(zw) + 1e-12
|
||||
val yn = DoubleArray(nPts) { (yw[it] - ym) / ysStd }
|
||||
val zn = DoubleArray(nPts) { (zw[it] - zm) / zsStd }
|
||||
// 타원 피팅 helper: 성공 시 (y0, z0, bSi, aAp, residuals) 반환
|
||||
fun fitEllipsePts(ywF: DoubleArray, zwF: DoubleArray): Array<Any>? {
|
||||
val cnt = ywF.size
|
||||
val ym = ywF.average(); val ysS = std(ywF) + 1e-12
|
||||
val zm = zwF.average(); val zsS = std(zwF) + 1e-12
|
||||
val yn = DoubleArray(cnt) { (ywF[it] - ym) / ysS }
|
||||
val zn = DoubleArray(cnt) { (zwF[it] - zm) / zsS }
|
||||
val sol = solveEllipseLSQ(yn, zn, cnt) ?: return null
|
||||
if (sol[0] <= 1e-12 || sol[1] <= 1e-12) return null
|
||||
val rr = 1.0 + sol[2] * sol[2] / (4.0 * sol[0]) + sol[3] * sol[3] / (4.0 * sol[1])
|
||||
if (rr <= 0) return null
|
||||
val y0 = (-sol[2] / (2.0 * sol[0])) * ysS + ym
|
||||
val z0 = (-sol[3] / (2.0 * sol[1])) * zsS + zm
|
||||
val bSi = sqrt(rr / sol[0]) * ysS
|
||||
val aAp = sqrt(rr / sol[1]) * zsS
|
||||
val resid = DoubleArray(cnt) {
|
||||
abs(((ywF[it] - y0) / bSi) * ((ywF[it] - y0) / bSi) +
|
||||
((zwF[it] - z0) / aAp) * ((zwF[it] - z0) / aAp) - 1.0)
|
||||
}
|
||||
return arrayOf(y0, z0, bSi, aAp, resid)
|
||||
}
|
||||
|
||||
val sol = solveEllipseLSQ(yn, zn, nPts)
|
||||
if (sol != null && sol[0] > 1e-12 && sol[1] > 1e-12) {
|
||||
val y0n = -sol[2] / (2.0 * sol[0])
|
||||
val z0n = -sol[3] / (2.0 * sol[1])
|
||||
val rVal = 1.0 + sol[2] * sol[2] / (4.0 * sol[0]) + sol[3] * sol[3] / (4.0 * sol[1])
|
||||
if (rVal > 0) {
|
||||
val bSiN = sqrt(rVal / sol[0])
|
||||
val y0 = y0n * ysStd + ym
|
||||
z0Ellipse = z0n * zsStd + zm
|
||||
val bSi = bSiN * ysStd
|
||||
if (nPts >= 5) {
|
||||
val keep = BooleanArray(nPts) { true }
|
||||
var fit = fitEllipsePts(allYw, allZw)
|
||||
|
||||
if (fit != null) {
|
||||
var y0 = fit[0] as Double; var z0 = fit[1] as Double
|
||||
var bSi = fit[2] as Double; var aAp = fit[3] as Double
|
||||
var resid = fit[4] as DoubleArray
|
||||
var meanRes = resid.average()
|
||||
|
||||
// 반복 outlier 제거: worst 점 하나씩, 최소 5점 유지
|
||||
while (meanRes > ellipseCostThr && keep.count { it } > 5) {
|
||||
val worstLocal = resid.indices.maxByOrNull { resid[it] } ?: break
|
||||
val activeIndices = keep.indices.filter { keep[it] }
|
||||
keep[activeIndices[worstLocal]] = false
|
||||
val keptY = keep.indices.filter { keep[it] }.map { allYw[it] }.toDoubleArray()
|
||||
val keptZ = keep.indices.filter { keep[it] }.map { allZw[it] }.toDoubleArray()
|
||||
val fit2 = fitEllipsePts(keptY, keptZ) ?: break
|
||||
val meanRes2 = (fit2[4] as DoubleArray).average()
|
||||
if (meanRes2 < meanRes) {
|
||||
y0 = fit2[0] as Double; z0 = fit2[1] as Double
|
||||
bSi = fit2[2] as Double; aAp = fit2[3] as Double
|
||||
resid = fit2[4] as DoubleArray; meanRes = meanRes2
|
||||
} else break
|
||||
}
|
||||
|
||||
// 품질 판정: 평균 잔차 ≤ threshold
|
||||
if (meanRes <= ellipseCostThr) {
|
||||
z0Ellipse = z0
|
||||
// b_si 상한: a_ap × 1.3 (해부학적 SI/AP 비율 제한)
|
||||
if (bSi > aAp * 1.3) bSi = aAp * 1.3
|
||||
hCapBot = max(0.0, yS[0] - (y0 - bSi))
|
||||
hCapTop = max(0.0, (y0 + bSi) - yS[n - 1])
|
||||
hCapBot = min(hCapBot, aCapS[0])
|
||||
|
||||
Reference in New Issue
Block a user