feat(demo): BV METHOD_D toggle + alignment graph 10-cycle 평균 표시
사용자앱 변경 (e9b5d9a) 을 데모에도 적용 + clinical alignment 그래프 개선. ## BV METHOD_D toggle (사용자앱과 동기화) - BvMethod.METHOD_D 추가 (FRUSTUM/V41/METHOD_D) - PiezoMonitoringView: useMethodDBv 분기. METHOD_D 선택 시 MethodDRunner walls, 그 외엔 legacy analyzer walls. (계산 cost 회피 위해 toggle ON 일 때만 실행) - Dev panel BV toggle: 3-버튼 (Method D 보라색 0xFF7C3AED) - ANT_OFFSET verify 로그: 어느 wall source 인지 표시 ## Clinical alignment graph 10-cycle 평균 표시 이전: 매 cycle 마다 raw 단일 신호 그래프 갱신 → 알고리즘이 실제 보는 신호 (10-frame 평균) 와 시각 불일치. 수정: V2 활성 시 RollingAligner.lastAvg 노출 → PlacementWaveformChart 가 overrideRaw 로 평균 신호 표시. V1 일 땐 기존 raw 단일 cycle 유지. - AlignmentAdvisorV2: lastAvg: List<DoubleArray>? 노출 (push 마다 set, reset 시 null) - PlacementWaveformChart: overrideRaw 파라미터 (null=기존 raw, non-null=avg) - ChannelGraphItem: V2 일 때 v2Aligner.lastAvg?.getOrNull(ch) 전달 알고리즘 ↔ 그래프 일치. 임상 alignment 평가 시 정확한 신호 시각 가능. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -341,8 +341,13 @@ class RollingAligner(
|
||||
fun reset() {
|
||||
buf.clear()
|
||||
guide4.reset()
|
||||
lastAvg = null
|
||||
}
|
||||
|
||||
/** UI 그래프에서 N-cycle 평균 신호를 보여주기 위해 마지막 avg 노출. */
|
||||
var lastAvg: List<DoubleArray>? = null
|
||||
private set
|
||||
|
||||
/** 단일 채널 길이 100 가정. raw6.size == 6 이어야 함. */
|
||||
fun push(raw6: List<DoubleArray>): AdvisorState {
|
||||
// ring buffer — accumK 사이즈 sliding window. buf 가 덜 찼어도 항상 step 호출
|
||||
@@ -359,6 +364,7 @@ class RollingAligner(
|
||||
sum / buf.size
|
||||
}
|
||||
}
|
||||
lastAvg = avg
|
||||
val dets = MethodDRunner.detectMultichannel(avg, methodDParams)
|
||||
val summary = summarize(dets)
|
||||
val out = guide4.step(summary)
|
||||
|
||||
@@ -14,7 +14,13 @@ package com.example.medilightv2android.managers
|
||||
*/
|
||||
enum class DetectionMethod { METHOD_A, METHOD_B, METHOD_C }
|
||||
enum class PlacementGuideMode { SIMPLE, BOUNDARY, SWEEP }
|
||||
enum class BvMethod { FRUSTUM, V41 }
|
||||
/**
|
||||
* BV 계산 방법.
|
||||
* - FRUSTUM : legacy analyzer walls + estimateBladderVolume6ch (Tanaka frustum)
|
||||
* - V41 : Method C 의 SphereFit (methodCBvMl 직접 사용)
|
||||
* - METHOD_D : Method D walls (cross-channel ant tie-break + OS-CFAR) + estimateBladderVolume6ch
|
||||
*/
|
||||
enum class BvMethod { FRUSTUM, V41, METHOD_D }
|
||||
|
||||
/** Sensor alignment 알고리즘 — V1=기존 (computePlacementGuide), V2=신규 (alignment.py 포팅). */
|
||||
enum class AlignmentAlgo { V1, V2 }
|
||||
|
||||
+22
-7
@@ -452,9 +452,22 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
(antOffsetMm.toDouble() /
|
||||
com.example.medilightv2android.managers.PiezoHW.distancePerSample).toInt()
|
||||
else 0
|
||||
|
||||
// 2026-06-30 (사용자앱 동기화): BvMethod.METHOD_D 선택 시 Method D walls 사용.
|
||||
val useMethodDBv = bvMethodSetting == com.example.medilightv2android.managers.BvMethod.METHOD_D
|
||||
val sourceAllWalls: List<Pair<Int, Int>?> = if (useMethodDBv) {
|
||||
val signals = (0..5).map { ch ->
|
||||
val cd = channels.find { it.channel == ch }
|
||||
cd?.buffer?.map { it.toDouble() }?.toDoubleArray() ?: DoubleArray(0)
|
||||
}
|
||||
if (signals.all { it.isEmpty() }) allWalls
|
||||
else com.example.medilightv2android.walldetect.MethodDRunner
|
||||
.detectMultichannel(signals)
|
||||
.map { r -> r?.let { Pair(it.ant, it.post) } }
|
||||
} else allWalls
|
||||
val effectiveAllWalls: List<Pair<Int, Int>?> =
|
||||
if (antOffsetSamples == 0) allWalls
|
||||
else allWalls.map { w ->
|
||||
if (antOffsetSamples == 0) sourceAllWalls
|
||||
else sourceAllWalls.map { w ->
|
||||
if (w == null) null
|
||||
else Pair(maxOf(0, w.first - antOffsetSamples), w.second)
|
||||
}
|
||||
@@ -476,11 +489,12 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
} else if (effectiveCenterWalls >= 4) {
|
||||
// 검증 로그 — ant offset 비활성(0)이 아닐 때 chord 변화 출력
|
||||
if (antOffsetSamples != 0) {
|
||||
val antOrig = (0..3).map { allWalls[it]?.first ?: -1 }
|
||||
val antOrig = (0..3).map { sourceAllWalls[it]?.first ?: -1 }
|
||||
val antAdj = (0..3).map { effectiveAllWalls[it]?.first ?: -1 }
|
||||
val chordOrig = (0..3).map { allWalls[it]?.let { w -> w.second - w.first } ?: -1 }
|
||||
val chordOrig = (0..3).map { sourceAllWalls[it]?.let { w -> w.second - w.first } ?: -1 }
|
||||
val chordAdj = (0..3).map { effectiveAllWalls[it]?.let { w -> w.second - w.first } ?: -1 }
|
||||
Log.d("PiezoMonitor", "ANT_OFFSET verify: mm=$antOffsetMm samples=$antOffsetSamples " +
|
||||
val srcLabel = if (useMethodDBv) "Method D" else "legacy"
|
||||
Log.d("PiezoMonitor", "ANT_OFFSET verify ($srcLabel walls): mm=$antOffsetMm samples=$antOffsetSamples " +
|
||||
"ant_orig=$antOrig ant_adj=$antAdj " +
|
||||
"chord_orig=$chordOrig chord_adj=$chordAdj " +
|
||||
"chord_delta(samples)=${chordAdj.mapIndexed { i, c -> if (c >= 0 && chordOrig[i] >= 0) c - chordOrig[i] else 0 }}")
|
||||
@@ -1846,9 +1860,10 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
val bvMethods = listOf(
|
||||
com.example.medilightv2android.managers.BvMethod.FRUSTUM to "Frustum",
|
||||
com.example.medilightv2android.managers.BvMethod.V41 to "V41"
|
||||
com.example.medilightv2android.managers.BvMethod.V41 to "V41",
|
||||
com.example.medilightv2android.managers.BvMethod.METHOD_D to "Method D"
|
||||
)
|
||||
val bvColors = listOf(MlPrimary, Color(0xFFFF5722))
|
||||
val bvColors = listOf(MlPrimary, Color(0xFFFF5722), Color(0xFF7C3AED))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
bvMethods.forEachIndexed { idx, (m, label) ->
|
||||
val sel = selectedBvMethod == m
|
||||
|
||||
+13
-5
@@ -740,13 +740,19 @@ fun PlacementGuideView(appState: AppState) {
|
||||
}
|
||||
}
|
||||
if (chData != null && chData.buffer.isNotEmpty()) {
|
||||
// V2 활성 시 RollingAligner 의 10-cycle 평균 신호로 표시 →
|
||||
// 알고리즘 입력 신호와 그래프 일치 (실제 측정 시 보는 화면).
|
||||
val isV2 = com.example.medilightv2android.managers.GreenZoneConstants.alignmentAlgo ==
|
||||
com.example.medilightv2android.managers.AlignmentAlgo.V2
|
||||
val avgRaw = if (isV2) v2Aligner.lastAvg?.getOrNull(ch) else null
|
||||
PlacementWaveformChart(
|
||||
buffer = chData.buffer,
|
||||
peakIndex = chData.peakIndex.toInt(),
|
||||
analyzer = analyzer,
|
||||
antIndex = showWalls?.ant,
|
||||
postIndex = showWalls?.post,
|
||||
modifier = Modifier.fillMaxWidth().height(120.dp)
|
||||
modifier = Modifier.fillMaxWidth().height(120.dp),
|
||||
overrideRaw = avgRaw,
|
||||
)
|
||||
} else {
|
||||
Box(modifier = Modifier.fillMaxWidth().height(120.dp).background(Color.Gray.copy(alpha = 0.05f), RoundedCornerShape(4.dp)),
|
||||
@@ -2138,12 +2144,14 @@ private fun PlacementWaveformChart(
|
||||
analyzer: PiezoEchoAnalyzer,
|
||||
antIndex: Int?,
|
||||
postIndex: Int?,
|
||||
modifier: Modifier = Modifier
|
||||
modifier: Modifier = Modifier,
|
||||
/** V2 활성 시 RollingAligner 의 N-cycle 평균 신호로 raw 덮어쓰기.
|
||||
* null → 기존 single-cycle 표시. */
|
||||
overrideRaw: DoubleArray? = null,
|
||||
) {
|
||||
val count = buffer.size
|
||||
val raw = overrideRaw ?: buffer.map { it.toDouble() }.toDoubleArray()
|
||||
val count = raw.size
|
||||
if (count < 2) return
|
||||
|
||||
val raw = buffer.map { it.toDouble() }.toDoubleArray()
|
||||
val denoised = analyzer.denoise(raw)
|
||||
val maxVal = 2500f
|
||||
val thrVal = if (analyzer.useAdaptiveThreshold && analyzer.lastOtsuThreshold > 0)
|
||||
|
||||
Reference in New Issue
Block a user