feat: adaptive threshold 구현 (percentile + 고정값 blended)
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) <noreply@anthropic.com>
This commit is contained in:
@@ -321,11 +321,48 @@ class PiezoEchoAnalyzer private constructor() {
|
|||||||
return max(q25, median - 0.5 * iqr)
|
return max(q25, median - 0.5 * iqr)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 단일 1D 채널 → urine region 탐지 (고정 임계값만 사용) */
|
var useAdaptiveThreshold: Boolean = true
|
||||||
|
|
||||||
|
/** 단일 1D 채널 → urine region 탐지 */
|
||||||
fun detectLowEcho(raw: DoubleArray, denoised: DoubleArray): LowEchoResult? {
|
fun detectLowEcho(raw: DoubleArray, denoised: DoubleArray): LowEchoResult? {
|
||||||
val sg = denoised
|
val sg = denoised
|
||||||
if (sg.size < 10) return null
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+3
-1
@@ -985,7 +985,9 @@ private fun WaveformChart(
|
|||||||
val raw = buffer.map { it.toDouble() }.toDoubleArray()
|
val raw = buffer.map { it.toDouble() }.toDoubleArray()
|
||||||
val denoised = analyzer.denoise(raw)
|
val denoised = analyzer.denoise(raw)
|
||||||
val maxVal = 2500f
|
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 distPerSample = 1.771f // mm per sample
|
||||||
|
|
||||||
val yLabels = listOf(0, 500, 1000, 1500, 2000, 2500)
|
val yLabels = listOf(0, 500, 1000, 1500, 2000, 2500)
|
||||||
|
|||||||
Reference in New Issue
Block a user