From d6e981e1d2316a5507691e846d5b017aa1b7cc6f Mon Sep 17 00:00:00 2001 From: jjangddu Date: Mon, 4 May 2026 16:32:31 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=B1=84=EB=84=90=EB=B3=84=20=EB=B9=94?= =?UTF-8?q?=20=EA=B0=81=EB=8F=84=20=EB=B3=B4=EC=A0=95=20(Python=20PR#20=20?= =?UTF-8?q?=EB=8F=99=EA=B8=B0=ED=99=94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Method B: - 채널별 otsu_ratio = OTSU_RATIO × cos(θ) 보정 - 큰 각도 채널은 threshold 완화 → 약한 후위벽 감지 개선 - detectLowEcho에 otsuRatioOverride 파라미터 추가 - analyzeMultiChannelWithTgc: initial + rerun 모두 각도 보정 적용 Method A: - 채널별 min_wall_contrast_ratio = 1.2 × cos(θ) 보정 - detectMethodA에 contrastRatioOverride 파라미터 추가 - analyzeChannel에서 PiezoHW.degreeAll 기반 자동 계산 Monitoring: - Detachment LED: 상태 변경 시에만 전송 (wasDetached 추적) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../managers/PiezoEchoAnalyzer.kt | 18 +++++++++++++----- .../managers/PiezoEchoAnalyzerA.kt | 10 +++++++--- .../ui/views/monitoring/PiezoMonitoringView.kt | 16 +++++++++++----- 3 files changed, 31 insertions(+), 13 deletions(-) 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 + } } }