feat: 채널별 빔 각도 보정 (Python PR#20 동기화)
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<LowEchoResult?>()
|
||||
val brCandidates = mutableListOf<Int?>()
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
+7
-1
@@ -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<Unit>() }
|
||||
val lastMultiChannelResult = remember { mutableStateListOf<PiezoChannelData>() }
|
||||
@@ -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) {
|
||||
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
|
||||
if (wasDetached) {
|
||||
handler.post { bleManager.sendLedMode(0) }
|
||||
wasDetached = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user