fix: 후면반사 오판방지 수정 + 1채널 구 BV 추가
BackReflection.kt (Python 동기화): - consensusIdx: min_votes=3 (후보 3개 미만이면 None) - buildCandidate: post≈global_br이면 None 반환 (post를 BR로 판단 안함) - isSuspiciousPostPeak: post>ant → post>ant×1.5 (amp_ratio=1.5) - isReflectionOnlyMultichannel: post>ant → post>ant×1.5 BV Estimation: - 1채널 구(sphere) 계산 추가 (Python _single_channel_bv) - V = (4/3)πR³, R = D/2 (cos 보정) - centerWallCount >= 1이면 BV 계산 (기존 3 → 1) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<Int?>, outlierTol: Int = OUTLIER_TOL): Int? {
|
||||
private const val MIN_VOTES = 3
|
||||
private const val AMP_RATIO = 1.5
|
||||
|
||||
fun consensusIdx(candidates: List<Int?>, 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<Pair<Int?, Int?>>,
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+6
-5
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user