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? {
|
fun buildCandidate(tgcSg: DoubleArray, postIdx: Int, postMaxIdx: Int, postMatchTol: Int = POST_MATCH_TOL): Int? {
|
||||||
val globalBr = findBackReflectionIdx(tgcSg, postMaxIdx)
|
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)
|
return findBackReflectionAfterPost(tgcSg, postIdx, postMaxIdx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── consensus_back_reflection_idx ──
|
// ── 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()
|
val vals = candidates.filterNotNull()
|
||||||
if (vals.isEmpty()) return null
|
if (vals.size < minVotes) return null
|
||||||
if (vals.size == 1) return vals[0]
|
|
||||||
val sorted = vals.sorted()
|
val sorted = vals.sorted()
|
||||||
val med = sorted[sorted.size / 2].toDouble()
|
val med = sorted[sorted.size / 2].toDouble()
|
||||||
val inliers = vals.filter { abs(it - med) <= outlierTol }
|
val inliers = vals.filter { abs(it - med) <= outlierTol }
|
||||||
@@ -119,11 +121,11 @@ object BackReflection {
|
|||||||
|
|
||||||
// ── is_suspicious_post_peak ──
|
// ── 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
|
val n = tgcSg.size
|
||||||
if (postIdx < 0 || postIdx >= n) return false
|
if (postIdx < 0 || postIdx >= n) return false
|
||||||
var suspicious = 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
|
if (sharedIdx != null && abs(postIdx - sharedIdx) <= sharedTol) suspicious = true
|
||||||
return suspicious
|
return suspicious
|
||||||
}
|
}
|
||||||
@@ -171,13 +173,15 @@ object BackReflection {
|
|||||||
wallPairs: List<Pair<Int?, Int?>>,
|
wallPairs: List<Pair<Int?, Int?>>,
|
||||||
sharedIdx: Int? = null,
|
sharedIdx: Int? = null,
|
||||||
sharedTol: Int = SHARED_TOL,
|
sharedTol: Int = SHARED_TOL,
|
||||||
minChannels: Int = MIN_CHANNELS
|
minChannels: Int = MIN_CHANNELS,
|
||||||
|
ampRatio: Double = AMP_RATIO
|
||||||
): Boolean {
|
): Boolean {
|
||||||
data class Valid(val i: Int, val ant: Int, val post: Int)
|
data class Valid(val i: Int, val ant: Int, val post: Int)
|
||||||
val valid = wallPairs.mapIndexedNotNull { i, (ant, post) ->
|
val valid = wallPairs.mapIndexedNotNull { i, (ant, post) ->
|
||||||
if (ant != null && post != null) Valid(i, ant, post) else null
|
if (ant != null && post != null) Valid(i, ant, post) else null
|
||||||
}
|
}
|
||||||
if (valid.size < minChannels) return false
|
if (valid.size < minChannels) return false
|
||||||
|
val ratio = ampRatio
|
||||||
|
|
||||||
// 조건 A: shared_idx 기반
|
// 조건 A: shared_idx 기반
|
||||||
var effectiveShared = sharedIdx
|
var effectiveShared = sharedIdx
|
||||||
@@ -188,7 +192,7 @@ object BackReflection {
|
|||||||
if (inliers.size >= minChannels) effectiveShared = med.roundToInt()
|
if (inliers.size >= minChannels) effectiveShared = med.roundToInt()
|
||||||
}
|
}
|
||||||
if (effectiveShared != null) {
|
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 클러스터
|
// 조건 B: post 클러스터
|
||||||
@@ -196,7 +200,7 @@ object BackReflection {
|
|||||||
val medPost = postVals[postVals.size / 2].toDouble()
|
val medPost = postVals[postVals.size / 2].toDouble()
|
||||||
val inlierTriples = valid.filter { abs(it.post - medPost) <= sharedTol }
|
val inlierTriples = valid.filter { abs(it.post - medPost) <= sharedTol }
|
||||||
if (inlierTriples.size >= minChannels) {
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -447,7 +447,31 @@ fun estimateBladderVolume(
|
|||||||
dPost.add(dFar)
|
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
|
val n = validChannels.size
|
||||||
|
|
||||||
// theta, sensor_z for valid channels
|
// 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 })
|
(0..5).map { allWalls[it]?.let { w -> w.second - w.first } ?: 0 })
|
||||||
logService.logMeasurement(methodCBvMl, channelLogs, rawADC)
|
logService.logMeasurement(methodCBvMl, channelLogs, rawADC)
|
||||||
com.example.medilightv2android.services.AdcCsvLogger.log(rawADC, methodCBvMl)
|
com.example.medilightv2android.services.AdcCsvLogger.log(rawADC, methodCBvMl)
|
||||||
} else if (centerWallCount >= 3) {
|
} else if (centerWallCount >= 1) {
|
||||||
val bvResult = estimateBladderVolume6ch(allWalls)
|
val bvResult = estimateBladderVolume6ch(allWalls)
|
||||||
if (bvResult != null) {
|
if (bvResult != null) {
|
||||||
rawVolumeMl = bvResult.volumeMl
|
rawVolumeMl = bvResult.volumeMl
|
||||||
rawLrRatio = bvResult.lrRatio
|
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,
|
bleManager.debugLogger.measurementResult(bvResult.volumeMl, bvResult.lrRatio, totalWallCount,
|
||||||
(0..5).map { allWalls[it]?.let { w -> w.second - w.first } ?: 0 },
|
(0..5).map { allWalls[it]?.let { w -> w.second - w.first } ?: 0 },
|
||||||
otsuThreshold = analyzer.lastOtsuThreshold)
|
otsuThreshold = analyzer.lastOtsuThreshold)
|
||||||
logService.logMeasurement(bvResult.volumeMl, channelLogs, rawADC)
|
logService.logMeasurement(bvResult.volumeMl, channelLogs, rawADC)
|
||||||
com.example.medilightv2android.services.AdcCsvLogger.log(rawADC, bvResult.volumeMl, bvResult.lrRatio)
|
com.example.medilightv2android.services.AdcCsvLogger.log(rawADC, bvResult.volumeMl, bvResult.lrRatio)
|
||||||
}
|
}
|
||||||
} else if (centerWallCount > 0) {
|
} else if (totalWallCount > 0) {
|
||||||
Log.w("PiezoMonitor", "Insufficient center channels: $centerWallCount/4 (need 3+)")
|
Log.w("PiezoMonitor", "No center channels: center=0 total=$totalWallCount/6")
|
||||||
bleManager.debugLogger.warn("BV_SKIP scan=${com.example.medilightv2android.services.AdcCsvLogger.lastScanId} center=$centerWallCount/4 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)
|
com.example.medilightv2android.services.AdcCsvLogger.log(rawADC, null)
|
||||||
if (!isAutoMeasuring) {
|
if (!isAutoMeasuring) {
|
||||||
handler.post { android.widget.Toast.makeText(context,
|
handler.post { android.widget.Toast.makeText(context,
|
||||||
|
|||||||
Reference in New Issue
Block a user