bcbc7b440c
사용자앱 (feature/tab-navigation) 과 패키지 이름 일치. namespace + applicationId 둘 다 com.medithings.vesiscan 로 변경 (기존 데모 앱은 재설치 필요). ## 변경 범위 - Kotlin 148 파일: package + import 문 (714 occurrences) - 디렉토리 이동: com/example/medilightv2android → com/medithings/vesiscan (main, test, androidTest 각각) - app/build.gradle.kts: namespace, applicationId - docs/FLAVOR_DEMO_STABLE.md: 참조 갱신 - V41DetectorCH4Test: BvDispatchResult.methodChosen → method (dto field name fix) ## 주의 - applicationId 가 바뀌므로 기존 데모 앱 (com.example.medilightv2android.demo) 은 Android 관점에서 다른 앱으로 취급 — 재설치 시 PIN/설정 초기화됨. - Fresh install 권장. 기존 앱 (com.example...) 은 별도로 uninstall 필요. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
452 lines
14 KiB
Markdown
452 lines
14 KiB
Markdown
# Sensor Alignment Mode — 발표 자료 outline
|
||
|
||
## 슬라이드 구조 (총 15-18장 권장)
|
||
|
||
---
|
||
|
||
### 슬라이드 1: 제목
|
||
**Sensor Alignment Mode**
|
||
방광 측정용 6채널 piezo 센서의 자동 정렬 가이드
|
||
|
||
- VesiScan-Basic 앱
|
||
- 발표자: 장동우
|
||
- 2026-06-30
|
||
|
||
---
|
||
|
||
### 슬라이드 2: Why Alignment Matters
|
||
|
||
**문제: 센서 위치가 살짝만 어긋나도 BV 측정 정확도 급락**
|
||
|
||
| 정렬 오차 | center wall 검출 | BV bias | 임상 결과 |
|
||
|---|---|---|---|
|
||
| 0 cm (정렬) | 100% | ±10% | 정확 |
|
||
| 2 cm 오프셋 | 40% | +50% (lateral fail → lr=1.0) | 과대 |
|
||
| 3 cm 오프셋 | 31% | +40% | 과대 |
|
||
| 4 cm 오프셋 | 13% | -120 mL | 측정 불가 |
|
||
|
||
→ **임상 적용 = alignment 가 first-class problem**
|
||
|
||
(실측 데이터: kai SUPINE 195 mL, 2 devices × 5 alignments × 31 cycles)
|
||
|
||
---
|
||
|
||
### 슬라이드 3: Hardware Background
|
||
|
||
**6채널 piezo 센서 배치 (정면 단면도)**
|
||
|
||
```
|
||
┌─────────────────┐
|
||
│ 센서 패드 (probe)│
|
||
│ │
|
||
│ CH0 ● (top, 20.1mm 위)
|
||
│ CH1 ● (12.8mm 위)
|
||
│ CH2 ● (7.0mm 위)
|
||
│ CH3 ● (basis, 0mm)
|
||
│ CH4● ●CH5 (lateral, ±10mm 좌우)
|
||
└─────────────────┘
|
||
```
|
||
|
||
- **Center channels (CH0-3)**: SI (superior-inferior, 위-아래) 축
|
||
- **Lateral channels (CH4-5)**: LR (left-right) 축
|
||
- 각 channel = 펄스 송신 → 에코 수신 → ant/post wall index 검출
|
||
- 100 samples / channel, ~1.98 mm/sample
|
||
|
||
---
|
||
|
||
### 슬라이드 4: 정렬의 목표
|
||
|
||
**좋은 측정을 위한 sensor 위치 조건:**
|
||
|
||
1. **CH3 (가장 아래) 가 방광 위에 있어야** (anatomical anchor)
|
||
- CH3 미검출 → 센서가 치골 아래 (방광 못 봄)
|
||
- CH3 검출 → 방광 lower pole 위치 확인됨
|
||
|
||
2. **모든 center channel (CH0-3) 이 방광 단면 위에 있어야**
|
||
- 일부만 검출 → 방광 edge 에 걸침 → BV 계산 부정확
|
||
|
||
3. **Lateral (CH4, CH5) 이 좌우 균형:**
|
||
- |u4 - u5| ≤ 8 samples (~16mm) 면 균형
|
||
- 차이 크면 한쪽으로 치우침
|
||
|
||
이 3 조건을 **실시간 자동 가이드** 로 충족시키는 게 alignment mode 의 역할.
|
||
|
||
---
|
||
|
||
### 슬라이드 5: V1 vs V2 비교
|
||
|
||
| | V1 (legacy) | V2 (Method D) |
|
||
|---|---|---|
|
||
| **알고리즘** | computePlacementGuide (단발) | RollingAligner + AlignGuide4Stage |
|
||
| **데이터 누적** | 1 cycle 즉시 판단 | N-cycle sliding window 평균 |
|
||
| **Phase** | 없음 (score 0~100 단일) | 5-phase state machine |
|
||
| **노이즈 robust** | 약함 (cycle 마다 score flap) | 강함 (10-cycle 평균 후 결정) |
|
||
| **임상 적용** | 일반 사용자 home use | clinical R&D / 정밀 측정 |
|
||
|
||
**현재 default:** 일반 사용자 = V1, 임상 clinical session = V2
|
||
|
||
발표 focus = **V2 (Method D)**
|
||
|
||
---
|
||
|
||
### 슬라이드 6: V2 전체 흐름
|
||
|
||
```
|
||
[BLE 연결] → [Personalization] → [Placement V2 진입] → [4-Phase 진행] → [측정]
|
||
↓
|
||
매 cycle mtb 송신 (~1Hz)
|
||
↓
|
||
6채널 raw + IMU 응답
|
||
↓
|
||
RollingAligner (sliding window)
|
||
↓
|
||
MethodDRunner.detectMultichannel
|
||
↓
|
||
AlignGuide4Stage (state machine)
|
||
↓
|
||
AdvisorState (action, hint, msg)
|
||
↓
|
||
UI 화살표 + 텍스트 표시
|
||
```
|
||
|
||
---
|
||
|
||
### 슬라이드 7: RollingAligner — 데이터 누적
|
||
|
||
**왜 평균을 내는가?**
|
||
|
||
Single cycle 데이터의 noise:
|
||
- per-cycle wall index 변동: ±5 samples (~10mm)
|
||
- per-cycle BV 변동: ±50 mL
|
||
- → 매 cycle 판단하면 화살표 마구 흔들림
|
||
|
||
**Sliding window 평균:**
|
||
- Window size = N cycles (phase 별 조정)
|
||
- Buffer 6채널 × 100 sample 그대로 평균 → "avg" signal
|
||
- avg → MethodDRunner 통과 → 안정적 wall 위치
|
||
|
||
```kotlin
|
||
fun push(raw6) {
|
||
buf.addLast(raw6)
|
||
while (buf.size > windowSize) buf.removeFirst()
|
||
val avg = average(buf) // 6 × 100 평균
|
||
val dets = MethodDRunner.detectMultichannel(avg)
|
||
...
|
||
}
|
||
```
|
||
|
||
**Window size:**
|
||
- INITIAL_ACCUM / VERTICAL / CH3_STABILIZE: **10**
|
||
- LR_BALANCE: **5** (반응성 우선)
|
||
- FINAL_CONFIRM: **5** (재확인)
|
||
|
||
---
|
||
|
||
### 슬라이드 8: Method D — Wall Detection Algorithm
|
||
|
||
**Per-channel pipeline:**
|
||
|
||
```
|
||
raw signal (100 samples)
|
||
↓
|
||
1. Heavy SG denoise (강한 smoothing — wall 위치용)
|
||
2. Light SG denoise (약한 smoothing — peak 보존)
|
||
↓
|
||
3. TGC pipeline (Time Gain Compensation)
|
||
- depth attenuation 보정
|
||
- adaptive ratio_min = 0.1
|
||
↓
|
||
4. OS-CFAR detector (ordered statistic — constant false alarm)
|
||
- cos(beam_angle) 로 otsu_ratio per channel 보정
|
||
- urine span detection (low-echo 구간 찾기)
|
||
- shoulder prominence γ = 1.5
|
||
↓
|
||
5. Cross-channel ant tie-break (in-place)
|
||
- 이웃 채널의 ant 후보들 비교
|
||
- top1/top2 ratio < 1.3 OR z-position |Δ| < 8mm
|
||
- → 같은 depth 의 ant 우선 선택 (해부학 일관성)
|
||
↓
|
||
MethodDResult { ant, post, urineLen, score }
|
||
```
|
||
|
||
**Python `for_app_share` reference 와 1:1 정렬** (2026-06-30 commit b733d4f).
|
||
|
||
---
|
||
|
||
### 슬라이드 9: AlignGuide4Stage — 4-Phase 상태 기계
|
||
|
||
```
|
||
┌────────────────────────────────────────────────────────────┐
|
||
│ │
|
||
│ ① INITIAL_ACCUM (10 cycles) │
|
||
│ "잠시 그대로 두세요 (n/10)" │
|
||
│ 데이터 누적만, 아직 판단 X │
|
||
│ ↓ │
|
||
│ ② VERTICAL_CLIMB │
|
||
│ CH3 미검출 → "↑ 위로 조금씩 올리세요" │
|
||
│ CH3 3 연속 검출 → 다음 │
|
||
│ ↓ │
|
||
│ ③ CH3_STABILIZE (10 cycles) │
|
||
│ CH3 안정 유지 확인 │
|
||
│ CH3 잃음 → ②로 복귀 │
|
||
│ bestCenterSet 저장 │
|
||
│ ↓ │
|
||
│ ④ LR_BALANCE │
|
||
│ lost channel → ↑/↓ 회복 (3 연속 lost 필요) │
|
||
│ |u4-u5| ≤ 8 → 균형, ⑤ 진입 │
|
||
│ |u4-u5| > 8 → ← or → 작은쪽으로 │
|
||
│ ↓ │
|
||
│ ⑤ FINAL_CONFIRM (5 cycles) │
|
||
│ buf clear → fresh 5 cycle 누적 │
|
||
│ |u4-u5| ≤ 8 유지 → "✓ 정렬 완료!" │
|
||
│ 실패 → ④로 │
|
||
│ │
|
||
└────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
### 슬라이드 10: Phase 1 — INITIAL_ACCUM
|
||
|
||
**역할:** RollingAligner buffer 를 10 cycles 채우기.
|
||
|
||
**왜 필요한가?**
|
||
- buffer 1개 frame 만 있으면 평균 = single cycle (noise 그대로)
|
||
- 10 frame 평균 → σ가 √10 ≈ 3배 감소
|
||
|
||
**UI:**
|
||
- 화살표 없음
|
||
- "잠시 그대로 두세요 (5/10)" 진행률 표시
|
||
- 사용자는 그냥 가만히 들고 있기
|
||
|
||
**기간:** ~10초 (1Hz × 10 cycles)
|
||
|
||
---
|
||
|
||
### 슬라이드 11: Phase 2 — VERTICAL_CLIMB
|
||
|
||
**역할:** CH3 (가장 아래 채널) 가 방광 위에 오도록 유도.
|
||
|
||
**Logic:**
|
||
```kotlin
|
||
if (!s.ch3) {
|
||
verticalHitStreak = 0 // false detection 거부
|
||
return MOVE_UP // "↑ 위로 조금씩"
|
||
}
|
||
verticalHitStreak++
|
||
if (verticalHitStreak < 3) {
|
||
return STOP // "✓ 확인 중 (n/3)"
|
||
}
|
||
phase = CH3_STABILIZE
|
||
```
|
||
|
||
**핵심:**
|
||
- CH3 3 회 연속 검출되어야 다음 phase
|
||
- 1회만 검출 (false echo 가능성) → 일시 멈춤 + 확인 중
|
||
|
||
**UI:**
|
||
- 화살표: ↑
|
||
- 메시지: "위로 조금씩 올리세요" / "확인 중 (n/3)"
|
||
|
||
---
|
||
|
||
### 슬라이드 12: Phase 3 — CH3_STABILIZE
|
||
|
||
**역할:** CH3 가 안정적으로 잡힌 위치인지 10 cycles 확인.
|
||
|
||
**Logic:**
|
||
```kotlin
|
||
if (!s.ch3) {
|
||
phase = VERTICAL_CLIMB // 잃음 → 처음으로
|
||
return
|
||
}
|
||
stabilizeCount++
|
||
if (stabilizeCount >= 10) {
|
||
bestCenterSet = curSet // 현재 잡힌 채널 set 저장
|
||
phase = LR_BALANCE
|
||
}
|
||
```
|
||
|
||
**핵심:**
|
||
- bestCenterSet 저장: 이 시점 잡힌 center channel (예: {1, 2, 3} 또는 {0, 1, 2, 3})
|
||
- 이후 LR_BALANCE 에서 이 set 유지 여부 확인
|
||
|
||
**UI:**
|
||
- 화살표: ✓ (멈춰 있어요)
|
||
- 메시지: "좋아요! 그 위치 유지 (n/10)"
|
||
|
||
---
|
||
|
||
### 슬라이드 13: Phase 4 — LR_BALANCE (좌우 균형)
|
||
|
||
**역할:** lateral channel (CH4, CH5) 의 신호 차이로 좌우 균형 잡기.
|
||
|
||
**Logic 1 — 채널 lost 감지:**
|
||
```kotlin
|
||
val lost = bestCenterSet - curSet
|
||
if (lost.isNotEmpty()) {
|
||
lostStreak++
|
||
if (lostStreak >= 3) { // hysteresis (6/30 추가)
|
||
if (3 in lost) return MOVE_UP // CH3 잃음 → 위
|
||
else return MOVE_DOWN // 다른 채널 잃음 → 아래
|
||
}
|
||
return STOP // noise, 무시
|
||
} else {
|
||
lostStreak = 0
|
||
}
|
||
```
|
||
|
||
**Logic 2 — 좌우 균형:**
|
||
```kotlin
|
||
val imbal = abs(u4 - u5)
|
||
if (imbal <= 8) { // LAT_TOL (~16mm)
|
||
phase = FINAL_CONFIRM
|
||
return STOP "✓ 균형 도달"
|
||
}
|
||
if (u4 < u5) return MOVE_RIGHT // CH4 신호 짧음 → 우측이 멀음
|
||
else return MOVE_LEFT // CH5 신호 짧음 → 좌측이 멀음
|
||
```
|
||
|
||
**Deadband:** 방향 반전 시 imbal 이 이전 + 3 이상 증가 + 2 연속 → 반전 (oscillation 차단)
|
||
|
||
---
|
||
|
||
### 슬라이드 14: Phase 5 — FINAL_CONFIRM
|
||
|
||
**역할:** 마지막 5 cycles 로 정렬 안정성 재확인.
|
||
|
||
**Logic:**
|
||
```kotlin
|
||
buf.clear() // RollingAligner buffer 초기화 (fresh window)
|
||
finalConfirmCount = 0
|
||
|
||
// 5 cycles 동안 imbal ≤ 8 유지하면 STOP
|
||
while (finalConfirmCount < 5) {
|
||
if (imbal > 8 || ch3 lost) {
|
||
phase = LR_BALANCE // 실패 → 복귀
|
||
return
|
||
}
|
||
finalConfirmCount++
|
||
return STOP "✓ 최종 확인 중 (n/5)"
|
||
}
|
||
return STOP "✓ 정렬 완료 — 측정 가능"
|
||
```
|
||
|
||
**핵심:**
|
||
- buf clear → 이전 cycles 영향 제거
|
||
- 5 fresh cycles 로 안정성 재검증
|
||
- 실패 시 LR_BALANCE 로 즉시 복귀
|
||
|
||
---
|
||
|
||
### 슬라이드 15: 최근 개선 (2026-06-30)
|
||
|
||
**1. lost streak hysteresis (50b8620, b2546d1)**
|
||
|
||
문제: 실측 데이터 (244 cycle 정렬 세션) 분석:
|
||
- LR_BALANCE 에서 |u4-u5| 항상 8 이하 (좌우 균형 OK)
|
||
- 하지만 CH1, CH3 가 cycle 마다 15%, 12% 노이즈 깜빡임
|
||
- 매 cycle `lost` 감지 → 화살표 띄움 → 다음 cycle 복구 → STOP 반복
|
||
- **사용자 체감: "어디로 옮길지 모르겠음"**
|
||
|
||
해결: 같은 채널이 3 연속 commit 에서 lost 여야 회복 모드.
|
||
- 화살표 flap rate 55% → ~5% 예상
|
||
|
||
---
|
||
|
||
**2. 그래프 ↔ 알고리즘 일치 (9e52019, 886d403)**
|
||
|
||
문제:
|
||
- 그래프: real-time single cycle raw ADC
|
||
- 알고리즘 입력: 10-cycle averaged signal
|
||
- 사용자가 그래프 보면서 "wall 보이는데 알고리즘 미검출" 또는 반대
|
||
|
||
해결: V2 활성 시 그래프도 RollingAligner.lastAvg 사용.
|
||
- `PlacementWaveformChart(overrideRaw = avgRaw)`
|
||
- 알고리즘 입력 == 시각화 신호 == 사용자 판단
|
||
|
||
---
|
||
|
||
### 슬라이드 16: 실측 검증 — kai 195 mL ALIGNMENT 세션
|
||
|
||
**11 sessions: 2 devices × 5 alignments × ~31 cycles**
|
||
|
||
| Step | center detect % | BV bias |
|
||
|---|---|---|
|
||
| 0 cm | 70% | -36 mL |
|
||
| 1 cm | 62% | -28 |
|
||
| 2 cm | 40% | +55 (lr=1.0 fallback) |
|
||
| 3 cm | 31% | +42 |
|
||
| 4 cm | 13% | -120 (fail) |
|
||
|
||
**Key finding:**
|
||
- Method D 자체는 정상 동작 (0 cm: center 70%, 1 cm: 62%)
|
||
- 2 cm 부터는 detection 불완전 → BV 부정확
|
||
- **alignment 가 충실히 0-1 cm 수렴해야 임상 정확도 보장**
|
||
|
||
→ V2 의 4-phase 가 0-1 cm 자동 수렴하는지가 핵심 검증 포인트
|
||
|
||
---
|
||
|
||
### 슬라이드 17: 알고리즘 파라미터 (Tunable)
|
||
|
||
`AlignmentConstants.kt`:
|
||
- `LAT_TOL = 8` samples (~16mm) — 좌우 균형 허용 오차
|
||
- `ANT_TIEBREAK_RATIO = 1.3` — cross-channel ant tiebreak
|
||
- `ANT_NEARFIELD_TOL_MM = 8.0`
|
||
|
||
`AlignGuide4Stage.kt`:
|
||
- `accumKVertical = 10` cycles
|
||
- `accumKLateral = 5` cycles
|
||
- `accumKConfirm = 5` cycles
|
||
- `verticalHitRequired = 3`
|
||
- `lrReverseDeadband = 3` samples
|
||
- `lrReverseConsecutive = 2`
|
||
- `lostStreakRequired = 3` ← **신규 (6/30)**
|
||
- `finalConfirmTarget = 5`
|
||
|
||
`MethodDParams.kt`:
|
||
- `oscfarWin = 9` (이전 5)
|
||
- `shoulderPromGamma = 1.5` (이전 1.0)
|
||
- `otsuRatio` per channel = base × cos(angle)
|
||
|
||
dev mode 에서 슬라이더로 실시간 튜닝 가능.
|
||
|
||
---
|
||
|
||
### 슬라이드 18: 결론 + 향후 과제
|
||
|
||
**현재 상태:**
|
||
- V2 (Method D) 4-phase 알고리즘 안정 동작
|
||
- 실측 데이터로 검증 완료 (kai 11 sessions)
|
||
- 최근 lost streak / 그래프 일치 개선
|
||
- Python reference 와 1:1 정렬
|
||
|
||
**향후 검증/개선:**
|
||
1. **임상 N=20+ 환자** 데이터로 alignment 수렴률 / 시간 측정
|
||
2. **3 cm 이상에서도 측정 거부** logic 강화 (현재는 측정 진행됨)
|
||
3. **자동 measurement 트리거** — 정렬 완료 시 사용자 액션 없이 측정 시작
|
||
4. **alignment 실패 mode** 처리 — 30초 내 수렴 못 하면 가이드 다이얼로그
|
||
5. **POSTURE-aware** — supine 외 자세에서 alignment 동작 검증
|
||
|
||
---
|
||
|
||
## 추가 자료 (appendix)
|
||
|
||
### A. 코드 위치
|
||
- `app/.../managers/AlignmentAdvisorV2.kt` — main state machine
|
||
- `app/.../walldetect/MethodDRunner.kt` — wall detection
|
||
- `app/.../walldetect/algo/methodd/` — Method D internals
|
||
- `app/.../ui/views/monitoring/PlacementGuideView.kt` — UI 통합
|
||
|
||
### B. 검증 툴
|
||
- `tools/analyze_csv.py` — App CSV → Python pipeline 비교
|
||
- ablation mode: DPS / lr_floor 등 각 fix 영향
|
||
|
||
### C. 관련 commits
|
||
- `02bed02` (appshare) — Method D 02bed02 원본
|
||
- `64c15a8` (demo) — Method D 02bed02 Kotlin 포팅
|
||
- `b733d4f` (feature) — BV Python 1:1 정렬
|
||
- `50b8620` (feature) — lost streak hysteresis
|
||
- `9e52019` — 그래프 10-cycle 평균 표시
|