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)
|
// TGC 전체 채널 통합 (Python preprocess_multichannel)
|
||||||
val preprocessed = applyTgcMultichannel(denoised)
|
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
|
// 1) Initial detect + BR candidate
|
||||||
val initialResults = mutableListOf<LowEchoResult?>()
|
val initialResults = mutableListOf<LowEchoResult?>()
|
||||||
val brCandidates = mutableListOf<Int?>()
|
val brCandidates = mutableListOf<Int?>()
|
||||||
for (i in channelData.indices) {
|
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)
|
initialResults.add(r)
|
||||||
brCandidates.add(if (r != null) BackReflection.buildCandidate(preprocessed[i], r.post, postMaxIdx) else null)
|
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 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)
|
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)
|
ChannelAnalysisResult(ch.channel, chosen, raws[i], cleaned)
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
@@ -402,7 +409,7 @@ class PiezoEchoAnalyzer private constructor() {
|
|||||||
val minScore: Double = 3000.0
|
val minScore: Double = 3000.0
|
||||||
|
|
||||||
/** 단일 1D 채널 → urine region 탐지 (Otsu-only, Python 동기화) */
|
/** 단일 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
|
val sg = denoised
|
||||||
if (sg.size < 10) return null
|
if (sg.size < 10) return null
|
||||||
val effectivePostMax = dynamicPostMax ?: postMaxIdx
|
val effectivePostMax = dynamicPostMax ?: postMaxIdx
|
||||||
@@ -417,10 +424,11 @@ class PiezoEchoAnalyzer private constructor() {
|
|||||||
return detectLowEchoCore(sg = sg, threshold = lowEchoAmpDefault, effectivePostMax = effectivePostMax)
|
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 otsuInput = otsuSignal ?: sg
|
||||||
val (thr, sep) = otsu1dWithSeparability(otsuInput, otsuInput.size)
|
val (thr, sep) = otsu1dWithSeparability(otsuInput, otsuInput.size)
|
||||||
lastOtsuThreshold = thr * otsuRatio
|
lastOtsuThreshold = thr * effectiveRatio
|
||||||
lastOtsuSeparability = sep
|
lastOtsuSeparability = sep
|
||||||
return detectLowEchoCore(sg = sg, threshold = lastOtsuThreshold, effectivePostMax = effectivePostMax)
|
return detectLowEchoCore(sg = sg, threshold = lastOtsuThreshold, effectivePostMax = effectivePostMax)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,14 +51,18 @@ class PiezoEchoAnalyzerA private constructor() {
|
|||||||
}
|
}
|
||||||
return try {
|
return try {
|
||||||
val sg = PiezoEchoAnalyzer.shared.denoise(raw)
|
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)
|
ChannelAnalysisResult(channel, result, raw, sg)
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
ChannelAnalysisResult(channel, null, raw, raw.copyOf())
|
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
|
if (sg.size < 10) return null
|
||||||
|
|
||||||
// 1) Composite signal y = sg + α|d1| + β|d2|
|
// 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]
|
for (i in leftLo..min(spanStart, sg.size - 1)) if (sg[i] > leftMax) leftMax = sg[i]
|
||||||
var rightMax = 0.0
|
var rightMax = 0.0
|
||||||
for (i in spanEnd until rightHi) if (sg[i] > rightMax) rightMax = sg[i]
|
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
|
// Find peaks
|
||||||
val leftPks = findPeaksInRange(sg, leftLo, spanStart).filter { it < spanStart }
|
val leftPks = findPeaksInRange(sg, leftLo, spanStart).filter { it < spanStart }
|
||||||
|
|||||||
+11
-5
@@ -77,6 +77,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
var measureRetryCount by remember { mutableIntStateOf(0) }
|
var measureRetryCount by remember { mutableIntStateOf(0) }
|
||||||
val maxMeasureRetries = 2
|
val maxMeasureRetries = 2
|
||||||
var lastVolumeMl by remember { mutableStateOf(0.0) }
|
var lastVolumeMl by remember { mutableStateOf(0.0) }
|
||||||
|
var wasDetached by remember { mutableStateOf(false) }
|
||||||
var maxVolumeMl by remember { mutableStateOf(0.0) }
|
var maxVolumeMl by remember { mutableStateOf(0.0) }
|
||||||
var measureDoneSignal = remember { kotlinx.coroutines.CompletableDeferred<Unit>() }
|
var measureDoneSignal = remember { kotlinx.coroutines.CompletableDeferred<Unit>() }
|
||||||
val lastMultiChannelResult = remember { mutableStateListOf<PiezoChannelData>() }
|
val lastMultiChannelResult = remember { mutableStateListOf<PiezoChannelData>() }
|
||||||
@@ -129,12 +130,17 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
val sortedBuffers = channels.sortedBy { it.channel }.map { it.buffer }
|
val sortedBuffers = channels.sortedBy { it.channel }.map { it.buffer }
|
||||||
val detachResult = com.example.medilightv2android.managers.DetachmentDetection.detect(sortedBuffers)
|
val detachResult = com.example.medilightv2android.managers.DetachmentDetection.detect(sortedBuffers)
|
||||||
if (detachResult.isDetached) {
|
if (detachResult.isDetached) {
|
||||||
Log.w("PiezoMonitor", "DETACHED mean=${"%.1f".format(detachResult.meanAbs)} std=${"%.1f".format(detachResult.stdVal)}")
|
if (!wasDetached) {
|
||||||
bleManager.debugLogger.warn("DETACHED mean=${"%.1f".format(detachResult.meanAbs)} std=${"%.1f".format(detachResult.stdVal)}")
|
Log.w("PiezoMonitor", "DETACHED mean=${"%.1f".format(detachResult.meanAbs)} std=${"%.1f".format(detachResult.stdVal)}")
|
||||||
handler.post { bleManager.sendLedMode(4) }
|
bleManager.debugLogger.warn("DETACHED mean=${"%.1f".format(detachResult.meanAbs)} std=${"%.1f".format(detachResult.stdVal)}")
|
||||||
|
handler.post { bleManager.sendLedMode(4) }
|
||||||
|
wasDetached = true
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 정상 복귀 → LED OFF
|
if (wasDetached) {
|
||||||
handler.post { bleManager.sendLedMode(0) }
|
handler.post { bleManager.sendLedMode(0) }
|
||||||
|
wasDetached = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user