refactor: 데모 브랜치 패키지 통일 com.example.medilightv2android → com.medithings.vesiscan

사용자앱 (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>
This commit is contained in:
2026-07-02 14:30:43 +09:00
parent 55d55225ab
commit bcbc7b440c
159 changed files with 2865 additions and 717 deletions
+451
View File
@@ -0,0 +1,451 @@
# 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 평균 표시