# 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 평균 표시