diff --git a/app/src/main/java/com/example/medilightv2android/managers/PiezoBVEstimator.kt b/app/src/main/java/com/example/medilightv2android/managers/PiezoBVEstimator.kt index 5ef88e5..a4c48fc 100644 --- a/app/src/main/java/com/example/medilightv2android/managers/PiezoBVEstimator.kt +++ b/app/src/main/java/com/example/medilightv2android/managers/PiezoBVEstimator.kt @@ -117,7 +117,7 @@ object PiezoHW { fun computeLrRatio( centerWalls: List?>, lateralWalls: List?>, - maxRatio: Double = 3.0 + maxRatio: Double = 1.0 ): Double { val hw = PiezoHW val neighbors = hw.lateralNeighbors diff --git a/app/src/main/java/com/example/medilightv2android/managers/PiezoEchoAnalyzerA.kt b/app/src/main/java/com/example/medilightv2android/managers/PiezoEchoAnalyzerA.kt index c748e20..3a99eaa 100644 --- a/app/src/main/java/com/example/medilightv2android/managers/PiezoEchoAnalyzerA.kt +++ b/app/src/main/java/com/example/medilightv2android/managers/PiezoEchoAnalyzerA.kt @@ -68,13 +68,16 @@ class PiezoEchoAnalyzerA private constructor() { if (L < 10) return null val y = DoubleArray(L) { sg[it] + alpha * abs(d1[it]) + beta * abs(d2[it]) } - // 2) Plateau score + // 2) Plateau score — threshold from full array (Python 동일) val platScore = slidingScores1d(sg, scoreWin) + val fullSorted = platScore.sorted().toDoubleArray() + val qIdx = (fullSorted.size * plateauQ).toInt().coerceIn(0, fullSorted.size - 1) + val scoreThr = if (qIdx > 0 && qIdx < fullSorted.size - 1) { + (fullSorted[qIdx] + fullSorted[qIdx + 1]) / 2.0 + } else fullSorted[qIdx] val limit = min(platScore.size, postMaxIdx + 1) - val scoreSorted = platScore.sliceArray(0 until limit).sorted().toDoubleArray() - val scoreThr = scoreSorted[(scoreSorted.size * plateauQ).toInt().coerceIn(0, scoreSorted.size - 1)] - // 3) Plateau spans + merge + // 3) Plateau spans + merge (postMaxIdx 이내만) var plateauSpans = findPlateauSpans(platScore, limit, scoreThr, plateauMinLen) if (plateauSpans.size > 1) { val merged = mutableListOf(plateauSpans[0]) @@ -138,18 +141,17 @@ class PiezoEchoAnalyzerA private constructor() { } if (bestLeft < 0 || bestRight < 0 || bestRight <= bestLeft) continue - val urineLen = bestRight - bestLeft + val urineLen = bestRight - bestLeft - 1 if (urineLen < minUrineLen) continue val lowSlice = sg.sliceArray(spanStart..spanEnd) val lowMean = lowSlice.average() - val wallScore = (sg[bestLeft] + sg[bestRight]) / 2.0 - lowMean return LowEchoResult( ant = bestLeft, post = bestRight, lowStart = spanStart, lowEnd = spanEnd, lowMean = lowMean, urineLen = urineLen, - score = wallScore * urineLen, + score = (sg[bestLeft] + sg[bestRight]).toDouble(), innerPeaks = PiezoEchoAnalyzer.shared.findInnerPeaks(sg, bestLeft, bestRight) ) } @@ -166,42 +168,49 @@ class PiezoEchoAnalyzerA private constructor() { val slope = DoubleArray(T) val low = DoubleArray(T) + // Edge padding (mode="edge") — Python np.pad 동일 + val xp = DoubleArray(T + 2 * half) + for (i in 0 until half) xp[i] = x[0] + for (i in 0 until T) xp[i + half] = x[i] + for (i in 0 until half) xp[T + half + i] = x[T - 1] + val tt = DoubleArray(w) { (it - half).toDouble() } var ttSqSum = 0.0 for (t in tt) ttSqSum += t * t ttSqSum += 1e-12 for (i in 0 until T) { - val wStart = max(0, i - half) - val wEnd = min(T - 1, i + half) - val wLen = wEnd - wStart + 1 - + val ww = DoubleArray(w) { xp[i + it] } var sum = 0.0; var sqSum = 0.0 - val vals = mutableListOf() - for (j in wStart..wEnd) { sum += x[j]; sqSum += x[j] * x[j]; vals.add(x[j]) } - val mean = sum / wLen - flat[i] = sqrt(max(0.0, sqSum / wLen - mean * mean)) + for (v in ww) { sum += v; sqSum += v * v } + val mean = sum / w + flat[i] = sqrt(max(0.0, sqSum / w - mean * mean)) - vals.sort() - low[i] = vals[(wLen * 0.2).toInt().coerceIn(0, wLen - 1)] + val sorted = ww.sorted().toDoubleArray() + low[i] = sorted[(w * 0.2).toInt().coerceIn(0, w - 1)] var slopeNum = 0.0 - for (j in wStart..wEnd) { - slopeNum += (j - i).toDouble() * (x[j] - mean) + for (j in 0 until w) { + slopeNum += tt[j] * (ww[j] - mean) } slope[i] = abs(slopeNum / ttSqSum) } - return DoubleArray(T) { robustZ(low)[it] + robustZ(flat)[it] + robustZ(slope)[it] } + val rzLow = robustZ(low) + val rzFlat = robustZ(flat) + val rzSlope = robustZ(slope) + return DoubleArray(T) { rzLow[it] + rzFlat[it] + rzSlope[it] } } private fun robustZ(a: DoubleArray): DoubleArray { val sorted = a.sorted().toDoubleArray() - val med = sorted[sorted.size / 2] - val deviations = DoubleArray(a.size) { abs(a[it] - med) } + val n = sorted.size + val med = if (n % 2 == 0) (sorted[n / 2 - 1] + sorted[n / 2]) / 2.0 else sorted[n / 2] + val deviations = DoubleArray(n) { abs(a[it] - med) } val devSorted = deviations.sorted().toDoubleArray() - val mad = devSorted[devSorted.size / 2] + 1e-12 - return DoubleArray(a.size) { (a[it] - med) / (1.4826 * mad) } + val madMed = if (n % 2 == 0) (devSorted[n / 2 - 1] + devSorted[n / 2]) / 2.0 else devSorted[n / 2] + val mad = madMed + 1e-12 + return DoubleArray(n) { (a[it] - med) / (1.4826 * mad) } } // ── Plateau span detection ── @@ -256,7 +265,7 @@ class PiezoEchoAnalyzerA private constructor() { if (validCenter.size < 3) return corrected for (field in listOf("ant", "post")) { - val vals = validCenter.map { if (field == "ant") corrected[it]!!.first else corrected[it]!!.second }.toMutableList() + val vals = validCenter.map { if (field == "ant") corrected[it]!!.first else corrected[it]!!.second } for (j in vals.indices) { val neighbors = mutableListOf() if (j > 0) neighbors.add(vals[j - 1]) @@ -264,11 +273,10 @@ class PiezoEchoAnalyzerA private constructor() { if (neighbors.isEmpty()) continue val neighborMean = neighbors.average() if (abs(vals[j] - neighborMean) > crossValMaxGradient) { - val newVal = neighborMean.toInt() + val newVal = Math.round(neighborMean).toInt() val chIdx = validCenter[j] val w = corrected[chIdx]!! corrected[chIdx] = if (field == "ant") Pair(newVal, w.second) else Pair(w.first, newVal) - vals[j] = newVal } } }