# Method A / B / C Algorithm Comparison & Placement Test _마지막 업데이트: 2026-07-13 — V2 6-stage / CH3 flicker 3-Layer 반영._ ## Wall Detection Comparison ### Pipeline ``` Method A: raw → SG → composite(sg+|d1|+|d2|) → plateau score → plateau spans → prominence wall (ANT=nearest, POST=edge_score) → cross-validation → BV(frustum+ellipse cap, 1ch=sphere) ※ 채널별 min_wall_contrast_ratio × cos(θ) 보정 Method B: raw → SG → TGC(multichannel log-fit) → initial detect(Otsu×0.9×cos(θ)) → BR candidate(per-ch, post-only, prominence≥50) → consensus(median, outlier_tol=4, min_votes=3) → is_reflection_only(shared+cluster, amp_ratio=1.5) → resolve(suspicious post: post>ant×1.5) → suppress(valley early-stop 50.0) → rerun detect → choose(rerun vs initial) → BV(frustum+ellipse cap, 1ch=sphere) Method C: raw → MedianFilter(win=7) → SG+wavelet → ImpulseReject(2-pass Hampel, lumen clean) → OS-CFAR(per-sample) → lumen mask → prominence wall + subsample refine → StaLta impulse purity check → anatomical gate (antDepth≥10mm, chord 5-110mm) → BModeScore V41 (wamp=0.45, stalta=0.20, far=0.15, dark=0.10) → ChordConsensus (Tukey MAD + Fischler-Bolles pair test) → BV dispatch(frustum/chord/sphere, trusted channels only) ``` ### Stage-by-Stage | Stage | **A (Plateau)** | **B (Otsu+TGC+BR)** | **C (V4.1 CFAR)** | |-------|-----------------|----------------------|---------------------| | Pre-filter | none | none | MedianFilter(win=7) + ImpulseReject(Hampel) | | Denoising | SG(5,2) | SG(5,2) → log-TGC | SG(5,2) + db4 wavelet 3-level | | Threshold | sliding plateau score Q=0.5 | Otsu×0.9×cos(θ) | OS-CFAR (win=15, rank=40%, scale=1.05) | | Low-echo mask | platScore < thr | sg <= otsu (full) | sg <= median(CFAR) | | Span merge | gap≤5, simple | gap<3: unconditional, ≥3: wall check | mergeGapMax=5, gapPeakMargin=50 | | Span refine | none | refineRightEdge | none | | Wall select | ANT=nearest, POST=edge_score, decay=0.15 | ANT=nearest, POST=edge_score, decay=0.12 | prominence + subsample, MAX_CANDIDATES_POST=64 | | Valley logic | descending-first, rise=5 | descending-first, rise=50 | rise=50 | | Extra conditions | contrast×cos(θ)≥1.2, prom≥3 | peakMin=max(low+30,thr), sg[ant/post]≥thr, postProm≥30 | Kremkau ant fallback, antMinIdx=7 | | Score | sg[ant]+sg[post] | asymmetric ant_reliability | BModeScore V41 (wamp+stalta+far+dark+grad) | | FP filter | cross-validation | score<3000 | AnatomicalGate + ChordConsensus | | Impulse check | none | none | StaLta (STA/LTA ratio) | | Channel filter | none | none | ChordConsensus (MAD outlier + pair test) | | Back reflection | none | consensus multichannel BR | none | | Fallback | none | ant=max(0,s-(post-e)) | B-mode tier | ### BV Estimation | | **A** | **B** | **C** | |---|---|---|---| | Model | shared with B | frustum + ellipse cap | Multi-dispatch (trusted ch only) | | 1-channel | sphere V=(4/3)πR³ | same | Verathon chord | | Channel filter | none | none | ChordConsensus (drop inconsistent) | | Cap height | 8-point ellipse LSQ | same | fixed: bottom=hemisphere, top=cone | | LR ratio | shared with B | 2-lateral ellipse solve + shrinkage | 1/avg_ratio, clamp[1.0,1.6] | | Confidence | none | none | 0.95(4C+2L) ~ 0.20(1ch) | | DPS | PiezoHW (1.968) | same | 1.9309 | ### Angle Correction (PR#20) Both Method A and B apply per-channel beam angle correction: - **Method B**: `otsu_ratio = OTSU_RATIO × cos(θ)` per channel - **Method A**: `min_wall_contrast_ratio = 1.2 × cos(θ)` per channel - Larger angles → lower threshold → better detection of weak posterior walls ### Back Reflection System (Method B) ``` Per-channel: 1. initial detect → (ant, post) 2. build_candidate: strongest peak AFTER post (prominence≥50) - global BR ≈ post (±3) → None (no separate BR) - else → strongest significant peak after post Multichannel: 3. consensus: median, outlier_tol=4, min_votes=3 4. is_reflection_only: post > ant × 1.5 (amp_ratio) - Condition A: shared_idx based - Condition B: post cluster based Per-channel resolve: 5. is_suspicious_post: post > ant × 1.5 OR post ≈ shared (±2) 6. select_idx → resolve → suppress(valley early-stop 50.0) 7. rerun detect → choose(rerun vs initial, min_gap=8) ``` ### Method C V4.1 New Features (2026-05-04) ``` Pre-processing: MedianFilter(win=7) → remove speckle bumps ImpulseReject(2-pass Hampel) → clean 1-3 sample impulses in lumen Detection: DetectLumenFirst V41_PARAMS: mergeGapMax=5, gapPeakMargin=50 maxCandidatesPost=64, medianWin=7, antMinIdx=7 Kremkau half-amplitude ant fallback Quality: StaLta: STA/LTA ratio → wall edge vs reverberation discrimination BModeScore V41_WEIGHTS: wamp=0.45 (wall-peak amplitude) stalta=0.20 (impulse purity) far=0.15, dark=0.10, ant=0.05, post=0.05 Channel Filtering: ChordConsensus: Tukey MAD outlier + Fischler-Bolles pair test → drop geometrically inconsistent channels before BV Anatomical Gate: antDepthMin: 22 → 10mm (Neyman-Pearson loose prior) strict threshold removed → delegated to B-mode score ``` --- ## Python vs Kotlin Sync Status (2026-05-04) ### Method A/B: 18 functions compared — CRITICAL 0, MINOR 5 All config constants match: LOW_ECHO_AMP=1250, OTSU_RATIO=0.9, MIN_SCORE=3000, EDGE_DIST_DECAY=0.12, VALLEY_STOP_RISE=50, etc. ### Method C: Direct copy from CharlesKWONsLaw (45 files, package renamed) --- ## Placement Test Algorithm ### Overview (2026-05-26 updated — v6) ``` Screen entry → "Place VesiScan above the pubic bone, then press Start" ↓ Start Alignment 버튼 탭 (waitingForStart=false) 전체 상태 리셋 ↓ [600ms interval loop] ← v6: 1000ms → 600ms (BleManager throttle과 동기화) ↓ mpa (first time, 500ms delay) → maa → 6ch reb ↓ (maa 전 BleManager.canSendMaa() 게이트 통과 필요) │ - State gate: !piezoCollector.isComplete 이면 차단 (3s 후 FORCE) │ - Time gate: 마지막 송신 < 600ms 이면 차단 │ Detachment check (6ch received, THR=30) → LED 4 if detached ↓ Wall detection (Method A/B/C) → urineLen ≥ 12 ↓ computePlacementGuide() → 3-stage guide (VERTICAL → LATERAL → GREEN) ↓ Debounce (2 consecutive + 1.5s/3s hold) + Phase protection (3 scans) ↓ Arrow direction: always updated (directionIcon synced with guideResult) ↓ LED: stage change only (4=detach, 5=searching, 6=complete) ↓ GREEN: 7s hold + 3 consecutive fail to exit ↓ Screen exit → LED OFF ``` ### v6 BLE 사이클 실측 (VBTFW0116 + MTU 247 + CONN_PRIORITY HIGH) 한 측정 사이클 (TX maa → RX raa): **평균 ~330ms** (이전 ~1.2~1.4s 대비 4배 향상). 600ms loop delay 안에 한 사이클이 안정적으로 완료됨. ### Guide Modes **Gradient** (default): weighted center gradient → LATERAL → GREEN **Boundary**: up → detect boundary (max drops) → come back → LATERAL **SWEEP** (CKLaw): navel → slide down → sagittalArgMaxCh → LATERAL → GREEN ### 3-Stage Guide (V1 — 일반 사용자 진입, `computePlacementGuide`) **Stage 1 VERTICAL**: detect count + gradient + "Slide up/down ↑↓" **Stage 2 LATERAL**: |len4-len5| ≤ 10 → GREEN, else "Slide left/right ←→" **Stage 3 GREEN**: CV ≤ threshold AND |LR dev| ≤ 0.20 → "In position!" (7s hold) ### 6-Stage Guide (V2 — Clinical Alignment 세션, `AlignGuide4Stage`) `ClinicalHome → Sensor Alignment` 진입 시 자동 활성화. Method D chord 기반 CV 로 정밀 vertical 조정 + CH3 flicker 대응 sliding-window majority. | # | Phase | 조건 | |:---:|---|---| | 1 | INITIAL_ACCUM | baseline 10 cycle 누적 | | 2 | VERTICAL_CLIMB | CH3 sliding window majority — 최근 6 프레임 중 hit ≥ 3 | | 3 | CH3_STABILIZE | 10 cycle 위치 유지, lost 판정도 sliding (window=4, majority=3) | | 4 | CENTER_OPTIMIZE | CH0~CH3 chord CV ≤ 0.15 (3연속 hysteresis) | | 5 | LR_BALANCE | \|u4−u5\| ≤ 8 samples, lost 채널 3연속 hysteresis | | 6 | FINAL_CONFIRM | buf clear → 5 fresh 프레임 재확인 통과 → GREEN | **CH3 flicker 대응 3-Layer** (2026-07-06 이후, commit e808b5b / 9261bc7 / 4f8d536): - **Layer 1 — Sliding-Window Majority**: 3 연속 hit 대체. 인체 flick 패턴 (YYNNYY 등) 통과 가능. - **Layer 2 — Stuck Detection + Relaxed Mode**: VERTICAL_CLIMB 20 프레임 (~5초) 갇힘 + CH3 hit 저조 + CH0~CH2 all detected → CH3 없이 상단 3채널로 CENTER_OPT 진입 허용. `AlignmentConstants.VERTICAL_STUCK_THRESHOLD`. - **Layer 3 — UX Soft-Hint**: 12 프레임 (~3초) 반복 시 "↑ 위로" → "CH3 신호 확인 중 · 위치 유지" 로 문구 자동 완화. **Python replay 검증** (`tools/align_analyze.py`, 2026-07-10~13): data123/ 인체 4 세션 (Phantom + Human 0/1/3 CM). NEW vs LEGACY 병행 replay 결과 Human 0CM (CH3 31.8%) 회귀 방지 + Human 1CM (77.3%) 조기 진입 확인, 팬텀 false-positive 없음. 상세 표: `tools/README.md`. ### Measurement Modes (v6 업데이트) - **Auto Scan**: **min 600ms gap** (state-based throttle: `!isComplete` + 600ms), 10-sample trimmed mean (max/min removed, 8 avg) - **Single Scan (Spot)**: 5회 측정 → trimmed mean (max 8 attempts, min 3) - valid ≥ 4 channels only - Analysis on coroutine (Dispatchers.Default) - Auto fail: 2ch+ missing 5 consecutive → realign dialog - **Voiding/Catheterization 클릭 시 Auto Scan 자동 stop + 측정 상태 정규화** (displayMaxVolumeMl/window/timer 모두 0) - Detachment → LED 4 (once), recovery → LED 0 (once) - Foreground Service: BLE connection maintained during screen off - BLE 로그: 실시간 파일 기록 (Downloads/VesiScan_BLE_*.log, 화면 잠금에도 보존) - CSV: 세션별 파일 (YYYY-MM-DD_HHmmss.csv) ### Placement GREEN 기준 ``` CV threshold = DEFAULT_CV_THR + (scanCount / 5) × 0.03 DEFAULT_CV_THR: V0=0.07, V1/V2=0.10, 기타=0.08 LR deviation ≤ 0.20 GREEN hold: 7초 (greenLockedAtMs 기준) GREEN exit: 3회 연속 실패 (greenFailCount) ``` ### Test Results (2026-05-04, 500ml phantom, Method C V4.1) ``` 총 측정: 223회 평균: 492.4ml (오차 -1.5%) ±15% 이내: 223/223 (100%) 범위: 441~537ml ``` --- ## BLE maa Throttle — State-Based Gate (v6, 2026-05-26) ### 배경: CH0/CH1만 도착 + BV_FAIL/BV_SKIP 증상 이전 throttle은 시간 단독 `now - lastMaaSentMs < 800ms` 만 검사. 펌웨어 응답이 ~1.2~1.4초 걸리는 환경에서 800ms 시점에 새 maa가 송신되면: 1. `sendChannelsOnly()` 통과 → `piezoCollector.startMultiChannel(6)` → collector reset 2. **큐에 떠다니던 이전 사이클의 reb / raa 패킷이 새 collector의 시작 부분에 잘못 들어감** 3. 결과: 한 채널만 들고 raa로 종료 → `BV_FAIL no valid channels` 또는 `BV_SKIP center=0 total=5/6` 실측 로그 (FW VBTFW0111, 2026-05-22): ``` TX maa #1 → reb×5 → (raa 큐 밀림) → TX maa #2 → reb×1 + raa → BV_FAIL scan=656 ``` ### v6 신규 게이트 ```kotlin private fun canSendMaa(caller: String): Boolean { val now = System.currentTimeMillis() val collectorBusy = piezoCollector.isMultiChannel && !piezoCollector.isComplete val sinceLast = now - lastMaaSentMs if (collectorBusy) { if (sinceLast < 3000) return false // (A) state 차단 // 3s 넘으면 강제 통과 (FORCE) — deadlock 방지 } if (sinceLast < 600) return false // (B) time 차단 return true } ``` | 게이트 | 차단 | 풀림 | |---|---|---| | (A) State | `isMultiChannel && !isComplete` | raa 도착 OR 3s FORCE | | (B) Time | `sinceLast < 600` | 600ms 경과 | 두 게이트 모두 통과해야 maa 송신. State gate가 race 방어, time gate가 burst 방어. ### 효과 (FW VBTFW0116 + MTU 247 + CONN_PRIORITY HIGH 조합) - 한 사이클 응답: 1.2s → **330ms** (4배) - 6/6 채널 도착률: 부분응답 잦음 → **100% 도착, 유실 0건** - BV_FAIL/BV_SKIP 빈도: 자주 → 관찰되지 않음 ### Logcat 키워드 | 로그 | 의미 | |---|---| | `maa BUSY [...] — prev response in progress (Xms)` | State gate 차단 (정상 방어) | | `maa FORCE [...] — prev incomplete after Xms` | 3s 넘게 raa 없음 → 포기하고 진행 | | `maa THROTTLED [...] — Xms since last` | Time gate 차단 (정상 방어) | | (로그 없음) | 정상 송신 | | `MTU changed: 247 (status=0)` | MTU 정상 협상 (CCCD write 직전) | | `CONN_PRIORITY HIGH requested (ok=true)` | priority 요청 큐잉 성공 (수락 여부는 sniffer로만 확인 가능) |