Files
VesiscanClinicalAndroid/docs/ALGORITHM_COMPARISON.md
T
dw.jang 4252464ef6 docs: v6 전체 업데이트 (1.0.0-design, VBTFW0116 짝, 2026-05-26 기준)
a1042b8(v5) 이후 13개 커밋을 반영해 세 문서 일괄 갱신.

USER_GUIDE.md:
- 헤더: VBTAND0101 → 1.0.0-design (versionCode 24), 권장 펌웨어 VBTFW0116
- PIN DEMO 자동통과 안내 (배포 전 정리 필요)
- Personalization 평행 2-카드 (Maximum Bladder Capacity + Catheter Threshold)
- Sensor Alignment 리디자인 (38sp 큰 타이틀, pubic bone 빨강, 3-stop gradient, Canvas 화살표/아치 제거)
- Measurement Screen top bar에 Home 아이콘 + Voiding/ 텍스트 + 48sp Recorded Dialog
- Auto Scan 600ms로 단축 + 측정 사이클 ~330ms 실측 안내
- 설정 패널 폰/태블릿 자동 폰트 분기 (fontScale 표 추가)
- Dev Mode 추가 기능 (DeviceScan 측정 직진입 버튼, Status 더미 주입)
- Navigation에 placementFromMonitoring 라우팅 분기 설명 + startFromHome fix

VesiScan_Android_Pipeline_Summary.md:
- v6 주요 변경 요약 (헤더에 한눈 보기)
- §5.1 BLE 통신: Connection Parameter 협상 (MTU 247, CONN_PRIORITY HIGH), maa throttle
  state-based gate, 측정 사이클 실측 표
- 측정 흐름: Auto/Single Scan 600ms 동기화 + Voiding 시 자동 stop
- 연속 스캔 동작: Placement loop 1000 → 600ms + v6 화면 디자인 변경 박스
- §14 BLE 명령어 포맷: VBTFW0116 신규 사항 (pending slot 1→8, 안드로이드 짝꿍 변경)
- §22 향후 과제: v6 완료 항목 12개 추가

docs/ALGORITHM_COMPARISON.md:
- Placement loop 1s → 600ms, canSendMaa 게이트 단계 명시
- v6 BLE 사이클 실측 (330ms 평균)
- Measurement Modes: 800 → 600ms, Voiding 자동 stop
- 신규 섹션: BLE maa Throttle — State-Based Gate (배경/구현/효과/logcat 키워드)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 17:36:52 +09:00

273 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Method A / B / C Algorithm Comparison & Placement Test
## 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
**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)
### 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로만 확인 가능) |