From 6889136783c719a302bb16d53dfd3c48b50c7eb3 Mon Sep 17 00:00:00 2001 From: jjangddu Date: Fri, 24 Apr 2026 14:58:28 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20adaptive=20threshold=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(percentile=20+=20=EA=B3=A0=EC=A0=95=EA=B0=92=20ble?= =?UTF-8?q?nded)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit detectLowEcho: 고정 1150 → adaptive threshold (기본 활성화) 1. ringSkip(3) 이후 신호에서 q25/q50/q75 계산 2. percentile 기반: q25 + iqr*0.3 ~ q50 - iqr*0.3 3. 고정값(1150) 범위 제한: ×0.7 ~ ×1.5 (805~1725) 4. 가중 평균: 고정 40% + adaptive 60% → 신호 레벨에 자동 적응하되 급격한 변동 방지 WaveformChart: threshold 점선도 adaptive 값으로 표시 useAdaptiveThreshold 플래그로 on/off 전환 가능 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../managers/PiezoEchoAnalyzer.kt | 41 ++++++++++++++++++- .../views/monitoring/PiezoMonitoringView.kt | 4 +- 2 files changed, 42 insertions(+), 3 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 155aa46..406c2a1 100644 --- a/app/src/main/java/com/example/medilightv2android/managers/PiezoEchoAnalyzer.kt +++ b/app/src/main/java/com/example/medilightv2android/managers/PiezoEchoAnalyzer.kt @@ -321,11 +321,48 @@ class PiezoEchoAnalyzer private constructor() { return max(q25, median - 0.5 * iqr) } - /** 단일 1D 채널 → urine region 탐지 (고정 임계값만 사용) */ + var useAdaptiveThreshold: Boolean = true + + /** 단일 1D 채널 → urine region 탐지 */ fun detectLowEcho(raw: DoubleArray, denoised: DoubleArray): LowEchoResult? { val sg = denoised if (sg.size < 10) return null - return detectLowEchoCore(sg = sg, threshold = lowEchoAmpDefault) + + if (!useAdaptiveThreshold) { + return detectLowEchoCore(sg = sg, threshold = lowEchoAmpDefault) + } + + // Adaptive threshold: percentile + depth 보상 조합 + val thr = computeAdaptiveThreshold(sg) + return detectLowEchoCore(sg = sg, threshold = thr) + } + + /** + * Adaptive threshold: 신호 통계 + depth attenuation 보상 + * + * 1) Percentile 기반 베이스라인: 전체 신호의 q25~median 사이에서 결정 + * 2) 초반 peak(피부 반사) 제외: ringSkip(3) 이후 사용 + * 3) 고정 threshold와의 가중 평균으로 급격한 변동 방지 + */ + fun computeAdaptiveThreshold(sg: DoubleArray): Double { + val skip = GreenZoneConstants.ringSkip + val usable = if (sg.size > skip + 10) sg.sliceArray(skip until sg.size) else sg + + val sorted = usable.sorted() + val n = sorted.size + val q25 = sorted[n / 4] + val q50 = sorted[n / 2] + val q75 = sorted[3 * n / 4] + val iqr = q75 - q25 + + // Percentile 기반: 소변 영역은 보통 하위 25~50% + val percThr = max(q25 + iqr * 0.3, q50 - iqr * 0.3) + + // 고정값과 adaptive의 가중 평균 (급격한 변동 방지) + val adaptive = percThr.coerceIn(lowEchoAmpDefault * 0.7, lowEchoAmpDefault * 1.5) + val blended = lowEchoAmpDefault * 0.4 + adaptive * 0.6 + + return blended } /** 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 1c3622c..bfd84eb 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 @@ -985,7 +985,9 @@ private fun WaveformChart( val raw = buffer.map { it.toDouble() }.toDoubleArray() val denoised = analyzer.denoise(raw) val maxVal = 2500f - val threshold = analyzer.lowEchoAmpDefault.toFloat() + val threshold = if (analyzer.useAdaptiveThreshold) + analyzer.computeAdaptiveThreshold(denoised).toFloat() + else analyzer.lowEchoAmpDefault.toFloat() val distPerSample = 1.771f // mm per sample val yLabels = listOf(0, 500, 1000, 1500, 2000, 2500)