Files
VesiscanClinicalAndroid/ppt/alignment_mode_presentation_outline.md
T
dw.jang bcbc7b440c 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>
2026-07-02 14:30:43 +09:00

14 KiB
Raw Blame History

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 위치
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:

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:

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 감지:

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 — 좌우 균형:

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:

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