diff --git a/app/src/main/java/com/example/medilightv2android/managers/PiezoEchoAnalyzer.kt b/app/src/main/java/com/example/medilightv2android/managers/PiezoEchoAnalyzer.kt index 1b3d020..8892412 100644 --- a/app/src/main/java/com/example/medilightv2android/managers/PiezoEchoAnalyzer.kt +++ b/app/src/main/java/com/example/medilightv2android/managers/PiezoEchoAnalyzer.kt @@ -167,11 +167,18 @@ class PiezoEchoAnalyzer private constructor() { // TGC 전체 채널 통합 (Python preprocess_multichannel) val preprocessed = applyTgcMultichannel(denoised) + // 채널별 각도 보정 (Python _make_angle_kwargs_b) + val beamAngles = PiezoHW.degreeAll + fun channelOtsuRatio(chIdx: Int): Double { + if (chIdx >= beamAngles.size) return otsuRatio + return otsuRatio * kotlin.math.cos(beamAngles[chIdx] * Math.PI / 180.0) + } + // 1) Initial detect + BR candidate val initialResults = mutableListOf() val brCandidates = mutableListOf() for (i in channelData.indices) { - val r = detectLowEcho(raw = raws[i], denoised = preprocessed[i]) + val r = detectLowEcho(raw = raws[i], denoised = preprocessed[i], otsuRatioOverride = channelOtsuRatio(channelData[i].channel)) initialResults.add(r) brCandidates.add(if (r != null) BackReflection.buildCandidate(preprocessed[i], r.post, postMaxIdx) else null) } @@ -201,7 +208,7 @@ class PiezoEchoAnalyzer private constructor() { } val cleaned = BackReflection.suppress(preprocessed[i], suppressIdx) - val r2 = detectLowEcho(raw = raws[i], denoised = cleaned, otsuSignal = preprocessed[i], dynamicPostMax = suppressIdx) + val r2 = detectLowEcho(raw = raws[i], denoised = cleaned, otsuSignal = preprocessed[i], dynamicPostMax = suppressIdx, otsuRatioOverride = channelOtsuRatio(ch.channel)) val chosen = BackReflection.chooseResult(r, r2, brIdx = BackReflection.selectIdx(preprocessed[i], r?.post ?: 0, postMaxIdx, antIdx = r?.ant, sharedIdx = sharedBrIdx, channelCandidate = brCandidates[i]), suppressIdx = suppressIdx, minGap = 8) ChannelAnalysisResult(ch.channel, chosen, raws[i], cleaned) } catch (_: Exception) { @@ -402,7 +409,7 @@ class PiezoEchoAnalyzer private constructor() { val minScore: Double = 3000.0 /** 단일 1D 채널 → urine region 탐지 (Otsu-only, Python 동기화) */ - fun detectLowEcho(raw: DoubleArray, denoised: DoubleArray, otsuSignal: DoubleArray? = null, dynamicPostMax: Int? = null, lowEchoAmpOverride: Double? = null): LowEchoResult? { + fun detectLowEcho(raw: DoubleArray, denoised: DoubleArray, otsuSignal: DoubleArray? = null, dynamicPostMax: Int? = null, lowEchoAmpOverride: Double? = null, otsuRatioOverride: Double? = null): LowEchoResult? { val sg = denoised if (sg.size < 10) return null val effectivePostMax = dynamicPostMax ?: postMaxIdx @@ -417,10 +424,11 @@ class PiezoEchoAnalyzer private constructor() { return detectLowEchoCore(sg = sg, threshold = lowEchoAmpDefault, effectivePostMax = effectivePostMax) } - // Otsu-only: 단일 시도, fallback 없음 (Python detect_low_echo_method_b 동일) + // Otsu-only: 단일 시도 (Python detect_low_echo_method_b 동일) + val effectiveRatio = otsuRatioOverride ?: otsuRatio val otsuInput = otsuSignal ?: sg val (thr, sep) = otsu1dWithSeparability(otsuInput, otsuInput.size) - lastOtsuThreshold = thr * otsuRatio + lastOtsuThreshold = thr * effectiveRatio lastOtsuSeparability = sep return detectLowEchoCore(sg = sg, threshold = lastOtsuThreshold, effectivePostMax = effectivePostMax) } 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 d2209fd..323aaff 100644 --- a/app/src/main/java/com/example/medilightv2android/managers/PiezoEchoAnalyzerA.kt +++ b/app/src/main/java/com/example/medilightv2android/managers/PiezoEchoAnalyzerA.kt @@ -51,14 +51,18 @@ class PiezoEchoAnalyzerA private constructor() { } return try { val sg = PiezoEchoAnalyzer.shared.denoise(raw) - val result = detectMethodA(raw, sg) + // 각도 보정: min_wall_contrast_ratio × cos(θ) + val beamAngles = PiezoHW.degreeAll + val angleFactor = if (channel < beamAngles.size) kotlin.math.cos(beamAngles[channel] * Math.PI / 180.0) else 1.0 + val result = detectMethodA(raw, sg, contrastRatioOverride = minWallContrastRatio * angleFactor) ChannelAnalysisResult(channel, result, raw, sg) } catch (_: Exception) { ChannelAnalysisResult(channel, null, raw, raw.copyOf()) } } - fun detectMethodA(raw: DoubleArray, sg: DoubleArray): LowEchoResult? { + fun detectMethodA(raw: DoubleArray, sg: DoubleArray, contrastRatioOverride: Double? = null): LowEchoResult? { + val effectiveContrastRatio = contrastRatioOverride ?: minWallContrastRatio if (sg.size < 10) return null // 1) Composite signal y = sg + α|d1| + β|d2| @@ -107,7 +111,7 @@ class PiezoEchoAnalyzerA private constructor() { for (i in leftLo..min(spanStart, sg.size - 1)) if (sg[i] > leftMax) leftMax = sg[i] var rightMax = 0.0 for (i in spanEnd until rightHi) if (sg[i] > rightMax) rightMax = sg[i] - if (leftMax / platMean < minWallContrastRatio || rightMax / platMean < minWallContrastRatio) continue + if (leftMax / platMean < effectiveContrastRatio || rightMax / platMean < effectiveContrastRatio) continue // Find peaks val leftPks = findPeaksInRange(sg, leftLo, spanStart).filter { it < spanStart } 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 f51bbbd..dab89c0 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 @@ -77,6 +77,7 @@ fun PiezoMonitoringView(appState: AppState) { var measureRetryCount by remember { mutableIntStateOf(0) } val maxMeasureRetries = 2 var lastVolumeMl by remember { mutableStateOf(0.0) } + var wasDetached by remember { mutableStateOf(false) } var maxVolumeMl by remember { mutableStateOf(0.0) } var measureDoneSignal = remember { kotlinx.coroutines.CompletableDeferred() } val lastMultiChannelResult = remember { mutableStateListOf() } @@ -129,12 +130,17 @@ fun PiezoMonitoringView(appState: AppState) { val sortedBuffers = channels.sortedBy { it.channel }.map { it.buffer } val detachResult = com.example.medilightv2android.managers.DetachmentDetection.detect(sortedBuffers) if (detachResult.isDetached) { - Log.w("PiezoMonitor", "DETACHED mean=${"%.1f".format(detachResult.meanAbs)} std=${"%.1f".format(detachResult.stdVal)}") - bleManager.debugLogger.warn("DETACHED mean=${"%.1f".format(detachResult.meanAbs)} std=${"%.1f".format(detachResult.stdVal)}") - handler.post { bleManager.sendLedMode(4) } + if (!wasDetached) { + Log.w("PiezoMonitor", "DETACHED mean=${"%.1f".format(detachResult.meanAbs)} std=${"%.1f".format(detachResult.stdVal)}") + bleManager.debugLogger.warn("DETACHED mean=${"%.1f".format(detachResult.meanAbs)} std=${"%.1f".format(detachResult.stdVal)}") + handler.post { bleManager.sendLedMode(4) } + wasDetached = true + } } else { - // 정상 복귀 → LED OFF - handler.post { bleManager.sendLedMode(0) } + if (wasDetached) { + handler.post { bleManager.sendLedMode(0) } + wasDetached = false + } } }