4252464ef6
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>
273 lines
11 KiB
Markdown
273 lines
11 KiB
Markdown
# 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로만 확인 가능) |
|