- docs/ALGORITHM_COMPARISON.md * "V3: CenterAligner 2-Pass BVCV" 신규 서브섹션 * Python 원본 매핑 · 검증 방식 · commit ID 명시 * V1 3-stage / V2 6-stage / V3 2-Pass 세 알고리즘 병행 정리 - VesiScan_Android_Pipeline_Summary.md * v7 patch 헤더 2026-07-13 → 2026-07-15 * Alignment 섹션 V3 항목 신규 (piezophantomtest PR #35 링크, Pass 1/2 요약, 검증 결과 요약, UI 후속 작업 표기) 기능 변화 없음. V3 라이브러리 이식 (커밋9387552/fcdce12) 을 문서에 반영.
15 KiB
Method A / B / C Algorithm Comparison & Placement Test
마지막 업데이트: 2026-07-15 — V3 CenterAligner 2-Pass BVCV 이식 추가.
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.21.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 저조 + CH0CH2 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.
V3: CenterAligner 2-Pass BVCV (AlignmentAdvisorV3.kt, 2026-07-15)
piezophantomtest ffd436b
(PR #35 "feat: alignment bvcv 규칙 추가") 의 vesiscan_test.alignment.CenterAligner
를 Kotlin 으로 이식. 기존 V2 (실시간 sliding 6-stage) 와 병행. Clinical 세션
전용 batch 방식.
Pass 1 — scanSelect(cyclesByCm)
- 사용자가 치골 위 0/1/2/3/4 cm 각각에서 20 cycle 씩 측정.
- 각 위치마다:
- 20 cycle → sliding window 10 → 11 trace
mean_scan(전체 20 cycle 평균) →detectMultichannel(applyCross=false)→nch,ch3per-trace→detectMultichannel(applyCross=true)→estimateBladderVolume6ch→ volume_ml 리스트 →bv_cv = std/mean
- Rule A + bvcv tie 로 최적 위치 선택:
- ch3 필수 (mean_scan base 검출)
- per-trace ch3 검출률 ≥
CH3_HIT_MIN = 0.80(임계 통과 없으면 ch3=O 전체 fallback) - max
nch - 동률 → min
bv_cv
- 선택 위치의 record 를
target으로 저장 (Pass 2 기준값).
Pass 2 — guide(cycles, cm)
- 재측정한 한 위치 (20 cycle) 가 target 기준 충족 시
STOP:ch3='O'- per-trace ch3 검출률 ≥ 80%
rec.nch ≥ target.nchrec.bv_cv ≤ target.bv_cv × (1 + bvCvTol)(기본bvCvTol = 0.15)
- 미충족:
MOVE_UP(다음 위치로 이동 안내) + 실패 사유 한글 표시.
Python 1:1 매핑:
_traces(cycles, win=10)↔buildRecord()slidingalignment_selection.select_cm(rule='A', tie='bvcv')↔selectCm()CenterAligner._fail_reason()↔failReason()- BV 리스트 축적: Python
if bv is not None: bvs.append(volume_ml)↔ Kotlinbv?.let { bvs.add(it.volumeMl) }(nan/0 필터 없음 — Python 1:1)
검증 (app/src/test/java/com/medithings/vesiscan/managers/CenterAlignerValidationTest.kt,
data123/ 인체 3 세션):
- Python reference 와
nch / ch3 / ch3_hit완전 일치 bv_cv오차 <2% (정상 케이스). BV 발산 케이스만 큰 오차 — Rule A 필터링으로 선택 영향 없음- 최종 선택 위치 = cm=3 (Python 과 일치)
주의: unit test 는 PiezoHW.activePreset = DevicePreset.V1 명시 설정 필요.
실기기는 BleManager.autoDetectPreset(deviceName) 이 자동 처리.
UI 통합 (Pass 1 위치 안내 · 진행바 · Pass 2 판정 화면) 은 후속 작업.
관련 commit: 9387552 (이식) + fcdce12 (Python 1:1 nan/0 필터 제거).
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.21.4초 걸리는 환경에서 800ms 시점에 새 maa가 송신되면:
sendChannelsOnly()통과 →piezoCollector.startMultiChannel(6)→ collector reset- 큐에 떠다니던 이전 사이클의 reb / raa 패킷이 새 collector의 시작 부분에 잘못 들어감
- 결과: 한 채널만 들고 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 신규 게이트
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로만 확인 가능) |