diff --git a/app/src/main/java/com/example/medilightv2android/managers/BackReflection.kt b/app/src/main/java/com/example/medilightv2android/managers/BackReflection.kt index 4fc0a00..f8431e5 100644 --- a/app/src/main/java/com/example/medilightv2android/managers/BackReflection.kt +++ b/app/src/main/java/com/example/medilightv2android/managers/BackReflection.kt @@ -99,16 +99,18 @@ object BackReflection { fun buildCandidate(tgcSg: DoubleArray, postIdx: Int, postMaxIdx: Int, postMatchTol: Int = POST_MATCH_TOL): Int? { val globalBr = findBackReflectionIdx(tgcSg, postMaxIdx) - if (abs(globalBr - postIdx) <= postMatchTol) return postIdx + if (abs(globalBr - postIdx) <= postMatchTol) return null // 후위벽≈전체BR → 별도 BR 없음 return findBackReflectionAfterPost(tgcSg, postIdx, postMaxIdx) } // ── consensus_back_reflection_idx ── - fun consensusIdx(candidates: List, outlierTol: Int = OUTLIER_TOL): Int? { + private const val MIN_VOTES = 3 + private const val AMP_RATIO = 1.5 + + fun consensusIdx(candidates: List, outlierTol: Int = OUTLIER_TOL, minVotes: Int = MIN_VOTES): Int? { val vals = candidates.filterNotNull() - if (vals.isEmpty()) return null - if (vals.size == 1) return vals[0] + if (vals.size < minVotes) return null val sorted = vals.sorted() val med = sorted[sorted.size / 2].toDouble() val inliers = vals.filter { abs(it - med) <= outlierTol } @@ -119,11 +121,11 @@ object BackReflection { // ── is_suspicious_post_peak ── - fun isSuspiciousPostPeak(tgcSg: DoubleArray, postIdx: Int, antIdx: Int? = null, sharedIdx: Int? = null, sharedTol: Int = 2): Boolean { + fun isSuspiciousPostPeak(tgcSg: DoubleArray, postIdx: Int, antIdx: Int? = null, sharedIdx: Int? = null, sharedTol: Int = 2, ampRatio: Double = AMP_RATIO): Boolean { val n = tgcSg.size if (postIdx < 0 || postIdx >= n) return false var suspicious = false - if (antIdx != null && antIdx in 0 until n && tgcSg[postIdx] > tgcSg[antIdx]) suspicious = true + if (antIdx != null && antIdx in 0 until n && tgcSg[postIdx] > tgcSg[antIdx] * ampRatio) suspicious = true if (sharedIdx != null && abs(postIdx - sharedIdx) <= sharedTol) suspicious = true return suspicious } @@ -171,13 +173,15 @@ object BackReflection { wallPairs: List>, sharedIdx: Int? = null, sharedTol: Int = SHARED_TOL, - minChannels: Int = MIN_CHANNELS + minChannels: Int = MIN_CHANNELS, + ampRatio: Double = AMP_RATIO ): Boolean { data class Valid(val i: Int, val ant: Int, val post: Int) val valid = wallPairs.mapIndexedNotNull { i, (ant, post) -> if (ant != null && post != null) Valid(i, ant, post) else null } if (valid.size < minChannels) return false + val ratio = ampRatio // 조건 A: shared_idx 기반 var effectiveShared = sharedIdx @@ -188,7 +192,7 @@ object BackReflection { if (inliers.size >= minChannels) effectiveShared = med.roundToInt() } if (effectiveShared != null) { - if (valid.all { abs(it.post - effectiveShared!!) <= sharedTol && tgcSignals[it.i][it.post] > tgcSignals[it.i][it.ant] }) return true + if (valid.all { abs(it.post - effectiveShared!!) <= sharedTol && tgcSignals[it.i][it.post] > tgcSignals[it.i][it.ant] * ratio }) return true } // 조건 B: post 클러스터 @@ -196,7 +200,7 @@ object BackReflection { val medPost = postVals[postVals.size / 2].toDouble() val inlierTriples = valid.filter { abs(it.post - medPost) <= sharedTol } if (inlierTriples.size >= minChannels) { - if (inlierTriples.all { tgcSignals[it.i][it.post] > tgcSignals[it.i][it.ant] }) return true + if (inlierTriples.all { tgcSignals[it.i][it.post] > tgcSignals[it.i][it.ant] * ratio }) return true } return false } 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 a4c48fc..2250146 100644 --- a/app/src/main/java/com/example/medilightv2android/managers/PiezoBVEstimator.kt +++ b/app/src/main/java/com/example/medilightv2android/managers/PiezoBVEstimator.kt @@ -447,7 +447,31 @@ fun estimateBladderVolume( dPost.add(dFar) } - if (validChannels.size < 2) return null + // 1채널 케이스: 구(sphere) 부피 (Python _single_channel_bv) + if (validChannels.size < 2) { + if (validChannels.size == 1) { + val ch = validChannels[0] + val theta = degreeDeg[ch] * Math.PI / 180.0 + val dRaw = dPost[0] - dAnt[0] + val D = if (applyAngleCorrection) dRaw * cos(theta) else dRaw + val R = D / 2.0 + val bvMm3 = (4.0 / 3.0) * Math.PI * R * R * R + val S = areaK * D * D * lrRatio + val yMid = sensorZMm[ch] + (dAnt[0] + dPost[0]) / 2.0 * sin(theta) + return BVResult( + volumeMl = bvMm3 / 1000.0, volumeMm3 = bvMm3, + validChannels = validChannels, dAntMm = dAnt.toDoubleArray(), dPostMm = dPost.toDoubleArray(), + dMm = doubleArrayOf(D), sMm2 = doubleArrayOf(S), + sortedChannels = listOf(ch), sortedYMm = doubleArrayOf(yMid), sortedSMm2 = doubleArrayOf(S), + vFrustumMm3 = doubleArrayOf(), vCoreMm3 = 0.0, + vBottomMm3 = bvMm3, vTopMm3 = 0.0, + bottomHMm = R, topHMm = 0.0, + bottomKind = "sphere", topKind = "sphere", + lrRatio = lrRatio, distancePerSample = distancePerSample, delayOffsetMm = delayOffsetMm + ) + } + return null + } val n = validChannels.size // theta, sensor_z for valid channels diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt index 1829534..eafe00a 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt @@ -229,21 +229,22 @@ fun PiezoMonitoringView(appState: AppState) { (0..5).map { allWalls[it]?.let { w -> w.second - w.first } ?: 0 }) logService.logMeasurement(methodCBvMl, channelLogs, rawADC) com.example.medilightv2android.services.AdcCsvLogger.log(rawADC, methodCBvMl) - } else if (centerWallCount >= 3) { + } else if (centerWallCount >= 1) { val bvResult = estimateBladderVolume6ch(allWalls) if (bvResult != null) { rawVolumeMl = bvResult.volumeMl rawLrRatio = bvResult.lrRatio - Log.d("PiezoMonitor", "BV[$methodLabel]: ${"%.1f".format(bvResult.volumeMl)}ml (6ch, lr=${"%.2f".format(bvResult.lrRatio)}, center=${centerWallCount}/4)") + val mode = if (centerWallCount == 1) "sphere" else "frustum" + Log.d("PiezoMonitor", "BV[$methodLabel]: ${"%.1f".format(bvResult.volumeMl)}ml ($mode, lr=${"%.2f".format(bvResult.lrRatio)}, center=${centerWallCount}/4)") bleManager.debugLogger.measurementResult(bvResult.volumeMl, bvResult.lrRatio, totalWallCount, (0..5).map { allWalls[it]?.let { w -> w.second - w.first } ?: 0 }, otsuThreshold = analyzer.lastOtsuThreshold) logService.logMeasurement(bvResult.volumeMl, channelLogs, rawADC) com.example.medilightv2android.services.AdcCsvLogger.log(rawADC, bvResult.volumeMl, bvResult.lrRatio) } - } else if (centerWallCount > 0) { - Log.w("PiezoMonitor", "Insufficient center channels: $centerWallCount/4 (need 3+)") - bleManager.debugLogger.warn("BV_SKIP scan=${com.example.medilightv2android.services.AdcCsvLogger.lastScanId} center=$centerWallCount/4 total=$totalWallCount/6") + } else if (totalWallCount > 0) { + Log.w("PiezoMonitor", "No center channels: center=0 total=$totalWallCount/6") + bleManager.debugLogger.warn("BV_SKIP scan=${com.example.medilightv2android.services.AdcCsvLogger.lastScanId} center=0 total=$totalWallCount/6") com.example.medilightv2android.services.AdcCsvLogger.log(rawADC, null) if (!isAutoMeasuring) { handler.post { android.widget.Toast.makeText(context,