사용자앱 (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>
14 KiB
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 위치 조건:
-
CH3 (가장 아래) 가 방광 위에 있어야 (anatomical anchor)
- CH3 미검출 → 센서가 치골 아래 (방광 못 봄)
- CH3 검출 → 방광 lower pole 위치 확인됨
-
모든 center channel (CH0-3) 이 방광 단면 위에 있어야
- 일부만 검출 → 방광 edge 에 걸침 → BV 계산 부정확
-
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 = 8samples (~16mm) — 좌우 균형 허용 오차ANT_TIEBREAK_RATIO = 1.3— cross-channel ant tiebreakANT_NEARFIELD_TOL_MM = 8.0
AlignGuide4Stage.kt:
accumKVertical = 10cyclesaccumKLateral = 5cyclesaccumKConfirm = 5cyclesverticalHitRequired = 3lrReverseDeadband = 3sampleslrReverseConsecutive = 2lostStreakRequired = 3← 신규 (6/30)finalConfirmTarget = 5
MethodDParams.kt:
oscfarWin = 9(이전 5)shoulderPromGamma = 1.5(이전 1.0)otsuRatioper channel = base × cos(angle)
dev mode 에서 슬라이더로 실시간 튜닝 가능.
슬라이드 18: 결론 + 향후 과제
현재 상태:
- V2 (Method D) 4-phase 알고리즘 안정 동작
- 실측 데이터로 검증 완료 (kai 11 sessions)
- 최근 lost streak / 그래프 일치 개선
- Python reference 와 1:1 정렬
향후 검증/개선:
- 임상 N=20+ 환자 데이터로 alignment 수렴률 / 시간 측정
- 3 cm 이상에서도 측정 거부 logic 강화 (현재는 측정 진행됨)
- 자동 measurement 트리거 — 정렬 완료 시 사용자 액션 없이 측정 시작
- alignment 실패 mode 처리 — 30초 내 수렴 못 하면 가이드 다이얼로그
- POSTURE-aware — supine 외 자세에서 alignment 동작 검증
추가 자료 (appendix)
A. 코드 위치
app/.../managers/AlignmentAdvisorV2.kt— main state machineapp/.../walldetect/MethodDRunner.kt— wall detectionapp/.../walldetect/algo/methodd/— Method D internalsapp/.../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 hysteresis9e52019— 그래프 10-cycle 평균 표시